vev agent guide
Project
vev is a Linux terminal multiplexer written in Go. It has a per-user daemon, a thin client, server-side rendering, and minimal socket/SSH diffs.
Commands
go build -o vev .
make test
go test ./internal/usecase/daemon -race -run TestName
make lint
make mocks
go test ./internal/adapters/ipc ./internal/usecase/daemon -run '^$' -bench=. -benchmem
- Format with goimports.
- Install dev tools once with
go install golang.org/x/tools/cmd/goimports@latest and go install github.com/vektra/mockery/v3@v3.8.0.
- Use Mockery v3 consistently across Go projects.
make test runs go test ./... -race.
make lint checks goimports -l, then go vet.
- Enable daemon pprof with
VEV_PPROF_ADDR=127.0.0.1:6060 vev --daemon.
- Set
VEV_LOG=debug|warn|error to change log verbosity (default info).
- Logs live in
$XDG_STATE_HOME/vev, or ~/.local/state/vev when unset.
- Use feature worktrees under
.worktrees/<branch-name>. Remove the worktree once its branch is merged or abandoned (git worktree remove <path>).
- Create vev-owned private directories with
pkg/safedir.EnsurePrivate, not os.MkdirAll.
Architecture rules
The exhaustive production matrix and separate test-import policy are enforced by boundary_test.go; package ownership is documented in docs/architecture.md.
pkg/ is reusable and never imports internal/.
- Production use cases import only
internal/ports, semantic internal/protocol packages, internal/domain, internal/snapshotcodec, and approved sibling use cases.
- Production use cases never import
internal/protocol/wire, concrete adapters, internal/app, internal/persist, or internal/platform.
internal/ports owns application seams, not codecs, raw frames, environment policy, or worker implementations.
Layer map:
main.go → internal/app: CLI parsing, composition, daemon startup, and hidden subcommands.
internal/domain: pure shared values, the pane layout tree, and terminal capability policy.
internal/snapshotcodec: durable snapshot format, shared by the daemon and the snapshot adapter.
internal/protocol: typed session messages; protocol/catalogue owns remote JSON schema; protocol/wire owns binary encoding and raw carriage contracts.
internal/ports: typed application connections and infrastructure-facing use-case seams.
internal/adapters/sessionwire: typed message ↔ raw frame adaptation.
internal/usecase/daemon: sessions, tabs, panes, VT screens, renderer shadows, and daemon features.
internal/usecase/client: raw-mode thin client; writes output bytes verbatim. On remote attachments it also mirrors them in a local VT screen for predictive echo.
internal/adapters: IPC, UDP, SSH stdio, PTY, terminal, clock, and observability implementations.
Before touching daemon teardown paths, read the lock-ordering notes at the top of internal/usecase/daemon/client.go.
Ports and mocks
- Define application cross-layer interfaces in
internal/ports; raw frame carriage interfaces live in internal/protocol/wire.
- Adapters implement ports and raw carriage contracts; use cases consume application connections through
internal/ports and typed semantic values from internal/protocol, never raw carriage contracts.
- Tests should prefer generated
portsmocks.MockX with .EXPECT().
- Hand-written fakes are okay only when mocks make the test harder to understand.
- After changing a port, run
make mocks and commit the generated updates.
- Time-dependent daemon code must use
ports.Clock/ports.Timer, not direct wall-clock APIs.
Tests
- Write Go tests as table-driven tests by default.
- Keep wire tests byte-for-byte and include truncated-prefix and trailing-garbage coverage.
- Prefer fake clocks over sleeps.
Wire protocol
Typed messages and negotiated version live in internal/protocol. Remote discovery JSON lives in internal/protocol/catalogue. Protobuf schemas, generated wire envelopes, strict scanning, encoded bounds, and raw carriage interfaces live in internal/protocol/wire; internal/adapters/sessionwire owns semantic conversion and Output compression. Shared stream framing lives in internal/adapters/streamframe.
- Stream frames are a 4-byte big-endian length followed by one complete serialized directional Protobuf envelope.
- Client and server directions are separate closed
oneof unions in internal/protocol/wire/schema/envelope.proto.
- Every connection starts with the bounded preamble; magic, epoch, role, protocol version, and negotiated limits are validated before application messages.
- Version negotiation requires strict equality. Bump
internal/protocol.Version for semantic or message layout changes; bump wire.ProtocolEpoch only for an intentional clean break.
- Generated
*.pb.go files are never edited. Run go tool buf generate and make protocol-check after schema changes.
Session flow
- Ephemeral numbered sessions survive detach while the daemon retains them, but are not persisted. With
ephemeral.close-on-exit (default on) the daemon removes one when its last attachment ends definitively: a client Detach{Closed} (sent on SIGHUP/SIGTERM) or an expired park/suspension. Parked, parking, suspended, or live attachments keep it.
- Named sessions survive headless and persist across daemon restarts.
- The daemon starts on first use and survives an empty session registry: final session removal is not shutdown, so it can be reused; it ends only on an explicit shutdown request,
kill --all, or process cancellation.
- A bulk purge (
kill --sessions) takes a transient admission gate and bounds each phase with its own deadline: admission drain, stopped/broken sweep, and a shared live-teardown budget. A repository delete that ignores cancellation is detached behind an exact name/incarnation/created-time fence and reported as a typed failure instead of holding purge admission or control.
- A live unit whose teardown aborts before destructive ownership stays registered, and its snapshot-coordinator quarantine is rolled back so the surviving session resumes checkpoint scheduling and its attached clients are unfrozen.
- Each connection has a 15-second handshake budget from connect through the initial committed publication.
- Local and remote attach use the same typed
Hello/Welcome session protocol. sessionwire translates typed traffic to Protobuf envelopes carried by IPC, QUIC, or SSH stdio.
- Remote attach bootstraps an authenticated direct QUIC connection by default;
vev host add --transport ssh <host> explicitly selects an SSH-only carriage.
- A session owns shared PTYs, VT state, tabs, panes, PTY content geometry selected by the latest valid attachment claim, and ordered mutations. Each attachment owns its window/view, copy and overlay state, rendering/output state, and reconnect lifecycle; when the latest claimant detaches, the most recently claimed remaining attachment becomes authoritative.
- Command requests have a 10-second result deadline and are tracked per connection.
source
1# vev agent guide
2
3## Project
4
5vev is a Linux terminal multiplexer written in Go. It has a per-user daemon, a thin client, server-side rendering, and minimal socket/SSH diffs.
6
7## Commands
8
9```sh
10go build -o vev .
11make test
12go test ./internal/usecase/daemon -race -run TestName
13make lint
14make mocks
15go test ./internal/adapters/ipc ./internal/usecase/daemon -run '^$' -bench=. -benchmem
16```
17
18- Format with **goimports**.
19- Install dev tools once with `go install golang.org/x/tools/cmd/goimports@latest` and `go install github.com/vektra/mockery/v3@v3.8.0`.
20- Use Mockery v3 consistently across Go projects.
21- `make test` runs `go test ./... -race`.
22- `make lint` checks `goimports -l`, then `go vet`.
23- Enable daemon pprof with `VEV_PPROF_ADDR=127.0.0.1:6060 vev --daemon`.
24- Set `VEV_LOG=debug|warn|error` to change log verbosity (default info).
25- Logs live in `$XDG_STATE_HOME/vev`, or `~/.local/state/vev` when unset.
26- Use feature worktrees under `.worktrees/<branch-name>`. Remove the worktree once its branch is merged or abandoned (`git worktree remove <path>`).
27- Create vev-owned private directories with `pkg/safedir.EnsurePrivate`, not `os.MkdirAll`.
28
29## Architecture rules
30
31The exhaustive production matrix and separate test-import policy are enforced by `boundary_test.go`; package ownership is documented in `docs/architecture.md`.
32
33- `pkg/` is reusable and never imports `internal/`.
34- Production use cases import only `internal/ports`, semantic `internal/protocol` packages, `internal/domain`, `internal/snapshotcodec`, and approved sibling use cases.
35- Production use cases never import `internal/protocol/wire`, concrete adapters, `internal/app`, `internal/persist`, or `internal/platform`.
36- `internal/ports` owns application seams, not codecs, raw frames, environment policy, or worker implementations.
37
38Layer map:
39
40- `main.go` → `internal/app`: CLI parsing, composition, daemon startup, and hidden subcommands.
41- `internal/domain`: pure shared values, the pane layout tree, and terminal capability policy.
42- `internal/snapshotcodec`: durable snapshot format, shared by the daemon and the snapshot adapter.
43- `internal/protocol`: typed session messages; `protocol/catalogue` owns remote JSON schema; `protocol/wire` owns binary encoding and raw carriage contracts.
44- `internal/ports`: typed application connections and infrastructure-facing use-case seams.
45- `internal/adapters/sessionwire`: typed message ↔ raw frame adaptation.
46- `internal/usecase/daemon`: sessions, tabs, panes, VT screens, renderer shadows, and daemon features.
47- `internal/usecase/client`: raw-mode thin client; writes output bytes verbatim. On remote attachments it also mirrors them in a local VT screen for predictive echo.
48- `internal/adapters`: IPC, UDP, SSH stdio, PTY, terminal, clock, and observability implementations.
49
50Before touching daemon teardown paths, read the lock-ordering notes at the top of `internal/usecase/daemon/client.go`.
51
52## Ports and mocks
53
54- Define application cross-layer interfaces in `internal/ports`; raw frame carriage interfaces live in `internal/protocol/wire`.
55- Adapters implement ports and raw carriage contracts; use cases consume application connections through `internal/ports` and typed semantic values from `internal/protocol`, never raw carriage contracts.
56- Tests should prefer generated `portsmocks.MockX` with `.EXPECT()`.
57- Hand-written fakes are okay only when mocks make the test harder to understand.
58- After changing a port, run `make mocks` and commit the generated updates.
59- Time-dependent daemon code must use `ports.Clock`/`ports.Timer`, not direct wall-clock APIs.
60
61## Tests
62
63- Write Go tests as table-driven tests by default.
64- Keep wire tests byte-for-byte and include truncated-prefix and trailing-garbage coverage.
65- Prefer fake clocks over sleeps.
66
67## Wire protocol
68
69Typed messages and negotiated version live in `internal/protocol`. Remote discovery JSON lives in `internal/protocol/catalogue`. Protobuf schemas, generated wire envelopes, strict scanning, encoded bounds, and raw carriage interfaces live in `internal/protocol/wire`; `internal/adapters/sessionwire` owns semantic conversion and Output compression. Shared stream framing lives in `internal/adapters/streamframe`.
70
71- Stream frames are a 4-byte big-endian length followed by one complete serialized directional Protobuf envelope.
72- Client and server directions are separate closed `oneof` unions in `internal/protocol/wire/schema/envelope.proto`.
73- Every connection starts with the bounded preamble; magic, epoch, role, protocol version, and negotiated limits are validated before application messages.
74- Version negotiation requires strict equality. Bump `internal/protocol.Version` for semantic or message layout changes; bump `wire.ProtocolEpoch` only for an intentional clean break.
75- Generated `*.pb.go` files are never edited. Run `go tool buf generate` and `make protocol-check` after schema changes.
76
77## Session flow
78
79- Ephemeral numbered sessions survive detach while the daemon retains them, but are not persisted. With `ephemeral.close-on-exit` (default on) the daemon removes one when its last attachment ends definitively: a client `Detach{Closed}` (sent on SIGHUP/SIGTERM) or an expired park/suspension. Parked, parking, suspended, or live attachments keep it.
80- Named sessions survive headless and persist across daemon restarts.
81- The daemon starts on first use and survives an empty session registry: final session removal is not shutdown, so it can be reused; it ends only on an explicit shutdown request, `kill --all`, or process cancellation.
82- A bulk purge (`kill --sessions`) takes a transient admission gate and bounds each phase with its own deadline: admission drain, stopped/broken sweep, and a shared live-teardown budget. A repository delete that ignores cancellation is detached behind an exact name/incarnation/created-time fence and reported as a typed failure instead of holding purge admission or control.
83- A live unit whose teardown aborts before destructive ownership stays registered, and its snapshot-coordinator quarantine is rolled back so the surviving session resumes checkpoint scheduling and its attached clients are unfrozen.
84- Each connection has a 15-second handshake budget from connect through the initial committed publication.
85- Local and remote attach use the same typed `Hello`/`Welcome` session protocol. `sessionwire` translates typed traffic to Protobuf envelopes carried by IPC, QUIC, or SSH stdio.
86- Remote attach bootstraps an authenticated direct QUIC connection by default; `vev host add --transport ssh <host>` explicitly selects an SSH-only carriage.
87- A session owns shared PTYs, VT state, tabs, panes, PTY content geometry selected by the latest valid attachment claim, and ordered mutations. Each attachment owns its window/view, copy and overlay state, rendering/output state, and reconnect lifecycle; when the latest claimant detaches, the most recently claimed remaining attachment becomes authoritative.
88- Command requests have a 10-second result deadline and are tracked per connection.