docs(agent): simplify project guidance
- commit
- a63250632720c5957f0bef09e2656a5c84fd7cc1
- author
- brice · · 2026-07-05 05:26 UTC
- parent
- fcf4de3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +54 -46
M AGENTS.md +54 -46
| @@ -1,67 +1,75 @@ | |||
| 1 | - | # CLAUDE.md | |
| 1 | + | # vev agent guide | |
| 2 | 2 | ||
| 3 | - | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | |
| 3 | + | ## Project | |
| 4 | 4 | ||
| 5 | - | ## What this is | |
| 6 | - | ||
| 7 | - | vev is a terminal multiplexer for Linux written in Go (deps: only `golang.org/x/sys`, `golang.org/x/term`). Sessions live in a per-user daemon; the client is a thin pipe. Rendering happens server-side with damage tracking so only minimal diffs cross the unix socket (or the SSH link on remote attach). | |
| 5 | + | 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. | |
| 8 | 6 | ||
| 9 | 7 | ## Commands | |
| 10 | 8 | ||
| 11 | 9 | ```sh | |
| 12 | - | go build -o vev . # build | |
| 13 | - | make test # go test ./... -race | |
| 14 | - | go test ./internal/usecase/daemon -race -run TestName # single test | |
| 15 | - | make lint # fails if goimports -l finds anything, then go vet | |
| 16 | - | make mocks # regenerate mocks (mockery, config in .mockery.yml) | |
| 17 | - | go test ./pkg/renderer ./pkg/vt ./internal/adapters/ipc -bench=. -benchmem # microbenchmarks | |
| 10 | + | go build -o vev . | |
| 11 | + | make test | |
| 12 | + | go test ./internal/usecase/daemon -race -run TestName | |
| 13 | + | make lint | |
| 14 | + | make mocks | |
| 15 | + | go test ./pkg/renderer ./pkg/vt ./internal/adapters/ipc -bench=. -benchmem | |
| 18 | 16 | ``` | |
| 19 | 17 | ||
| 20 | - | - Format with **goimports**, not gofmt — lint enforces it. | |
| 21 | - | - Daemon pprof is opt-in via `VEV_PPROF_ADDR=127.0.0.1:6060 vev --daemon` (see `docs/performance.md`). | |
| 22 | - | - Live logs are under `$XDG_STATE_HOME/vev` (or `~/.local/state/vev` when `XDG_STATE_HOME` is unset): `vev-daemon.log`, `vev-client.log`, `vev-stdio.log`, plus matching `*-crash.log` files. | |
| 23 | - | - Feature branches live in git worktrees under `.worktrees/<branch-name>`. | |
| 18 | + | - Format with **goimports**. | |
| 19 | + | - `make test` runs `go test ./... -race`. | |
| 20 | + | - `make lint` checks `goimports -l`, then `go vet`. | |
| 21 | + | - Enable daemon pprof with `VEV_PPROF_ADDR=127.0.0.1:6060 vev --daemon`. | |
| 22 | + | - Logs live in `$XDG_STATE_HOME/vev`, or `~/.local/state/vev` when unset. | |
| 23 | + | - Use feature worktrees under `.worktrees/<branch-name>`. | |
| 24 | + | ||
| 25 | + | ## Architecture rules | |
| 26 | + | ||
| 27 | + | Hexagonal boundaries are enforced by `boundary_test.go`. | |
| 24 | 28 | ||
| 25 | - | ## Architecture | |
| 29 | + | - `pkg/` is reusable and must not import `internal/`. | |
| 30 | + | - `internal/usecase/` must not import `internal/adapters/`. | |
| 31 | + | - Usecases depend on `internal/ports` and `internal/domain`. | |
| 26 | 32 | ||
| 27 | - | Hexagonal, enforced mechanically by `boundary_test.go` at the repo root: | |
| 28 | - | - `pkg/` must not import `internal/` (pure, reusable: `pkg/vt` VT emulator, `pkg/renderer` damage-diff renderer, `pkg/kv` store). | |
| 29 | - | - `internal/usecase/` must not import `internal/adapters/`. Usecases depend only on `internal/ports` (interfaces + wire protocol) and `internal/domain`. | |
| 33 | + | Layer map: | |
| 30 | 34 | ||
| 31 | - | Layers: | |
| 32 | - | - `main.go` → `internal/app`: CLI parsing (`parseArgs`/`dispatch` in `run.go`), dependency wiring, daemon spawn/ensure logic. Hidden subcommands (e.g. `_stdio`) live here. | |
| 33 | - | - `internal/ports`: all interfaces (`Transport`, `Listener`, `PTY`, `Terminal`, `Clock`, …) plus the wire protocol (`frame.go`, `wire.go`). | |
| 34 | - | - `internal/usecase/daemon`: the server. Owns sessions → tabs → panes, one `pkg/vt.Screen` per pane (server-side authoritative terminal state), and one `pkg/renderer.Renderer` shadow per attached client (per-client diff state). `daemon/client.go` documents the lock ordering at the top of the file — read it before touching teardown paths (`clientGone`, `killSession`, `shutdownAll`). | |
| 35 | - | - `internal/usecase/client`: the thin client — raw-mode ownership, stdin/resize pumps, writes received output bytes verbatim to the terminal. It interprets nothing. | |
| 36 | - | - `internal/adapters`: `ipc` (unix socket framing, socket dir), `sshstdio` (remote attach), `pty`, `term`, `clock`. | |
| 37 | - | - Other usecases (`keys`, `layout`, `copy`, `palette`, `picker`, `prompt`, `theme`, `ui`, …) are daemon-side features routed through `daemon/client.go`. | |
| 35 | + | - `main.go` → `internal/app`: CLI parsing, wiring, daemon startup, hidden subcommands. | |
| 36 | + | - `internal/ports`: interfaces and wire protocol. | |
| 37 | + | - `internal/usecase/daemon`: sessions, tabs, panes, VT screens, renderer shadows, daemon features. | |
| 38 | + | - `internal/usecase/client`: raw-mode thin client; writes output bytes verbatim and interprets nothing. | |
| 39 | + | - `internal/adapters`: IPC, SSH stdio, PTY, terminal, and clock implementations. | |
| 38 | 40 | ||
| 39 | - | ### Ports interfaces and mocks (mockery) | |
| 41 | + | Before touching daemon teardown paths, read the lock-ordering notes at the top of `internal/usecase/daemon/client.go`. | |
| 40 | 42 | ||
| 41 | - | Every cross-layer abstraction is an interface in `internal/ports` (`Transport`, `Listener`, `PTY`, `PTYFactory`, `Terminal`, `Store`, `Clock`, `Timer`). Adapters implement them; usecases and `internal/app` consume them. This is the seam that makes the daemon and client testable without real sockets, PTYs, or wall-clock time (e.g. tests drive grace timers through the `Clock`/`Timer` ports instead of sleeping). | |
| 43 | + | ## Ports and mocks | |
| 42 | 44 | ||
| 43 | - | Mocks are generated by **mockery** (testify template), configured in `.mockery.yml`: | |
| 44 | - | - `internal/ports` has `all: true` → every interface in the package gets a mock in `internal/ports/mocks/mocks.go` (package `portsmocks`, `MockTransport`, `MockPTY`, …). | |
| 45 | - | - `internal/usecase/command.Context` gets a package-local `context_mock_test.go`. | |
| 45 | + | - Define cross-layer interfaces only in `internal/ports`. | |
| 46 | + | - Adapters implement ports; usecases and `internal/app` consume ports. | |
| 47 | + | - Tests should prefer generated `portsmocks.MockX` with `.EXPECT()`. | |
| 48 | + | - Hand-written fakes are okay only when mocks make the test harder to understand. | |
| 49 | + | - After changing a port, run `make mocks` and commit the generated updates. | |
| 50 | + | - Time-dependent daemon code must use `ports.Clock`/`ports.Timer`, not direct wall-clock APIs. | |
| 46 | 51 | ||
| 47 | - | The workflow when adding or changing an abstraction: | |
| 48 | - | 1. Add/modify the interface in `internal/ports` (never define the interface in a usecase or adapter). | |
| 49 | - | 2. Run `make mocks` — the generated file is committed, and forgetting this step leaves stale mocks that still compile silently. | |
| 50 | - | 3. In tests, prefer the generated `portsmocks.MockX` with `.EXPECT()` over hand-rolled stubs; hand-rolled fakes are acceptable only when a mock's call-expectation style fights the test (e.g. long-lived pumps). | |
| 52 | + | ## Tests | |
| 51 | 53 | ||
| 52 | - | New time-dependent daemon code must go through `ports.Clock`/`ports.Timer`, not `time.Now`/`time.NewTimer`, or it becomes untestable under the existing fake clock. | |
| 54 | + | - Write Go tests as table-driven tests by default. | |
| 55 | + | - Keep wire tests byte-for-byte and include truncated-prefix and trailing-garbage coverage. | |
| 56 | + | - Prefer fake clocks over sleeps. | |
| 53 | 57 | ||
| 54 | - | ### Wire protocol rules | |
| 58 | + | ## Wire protocol | |
| 55 | 59 | ||
| 56 | - | The IPC protocol lives entirely in `internal/ports` (`frame.go`, `wire.go`): frames are 4-byte big-endian length + 1 type byte + payload; messages are hand-marshalled big-endian. | |
| 60 | + | Wire code lives in `internal/ports/frame.go` and `internal/ports/wire.go`. | |
| 57 | 61 | ||
| 58 | - | - Client-originated message types are 1–15, server-originated 16+. | |
| 59 | - | - Version negotiation is strict equality; the version must stay the **first field** of `Hello` so `PeekHelloVersion` keeps working on any version's payload. Bump `ProtocolVersion` when changing any message layout. | |
| 60 | - | - `wire_test.go` contains byte-for-byte golden tests plus truncated-prefix / trailing-garbage invariants for every message. Any wire change must update the goldens and keep those invariants. | |
| 62 | + | - Frames are 4-byte big-endian length, 1 type byte, then payload. | |
| 63 | + | - Client message types are `1–15`; server message types are `16+`. | |
| 64 | + | - Version negotiation requires strict equality. | |
| 65 | + | - `Hello.Version` must stay first so `PeekHelloVersion` works. | |
| 66 | + | - Bump `ProtocolVersion` for any message layout change. | |
| 61 | 67 | ||
| 62 | - | ### Session/attach flow | |
| 68 | + | ## Session flow | |
| 63 | 69 | ||
| 64 | - | - Ephemeral sessions (numbered) die on detach; named sessions survive headless. The daemon starts on first use and exits when the last session ends. | |
| 65 | - | - Local attach: client dials the unix socket (spawning the daemon if needed via `ensureDaemon` in `internal/app`), sends `Hello{Intent, Name, Size, …}`, gets `Welcome`, then pumps `Input`/`Resize` against `Output` frames. | |
| 66 | - | - Remote attach: `vev attach user@host[:session]` runs `ssh -- host vev _stdio [session]`; the remote `_stdio` process proxies frames between ssh stdio and the remote daemon's unix socket. Same protocol end to end, so vev must be installed on the remote host. | |
| 67 | - | - Only one client per session: a new attach displaces the previous one (`detachReplacedClient`). | |
| 70 | + | - Ephemeral numbered sessions die on detach. | |
| 71 | + | - Named sessions survive headless. | |
| 72 | + | - The daemon starts on first use and exits when the last session ends. | |
| 73 | + | - Local attach sends `Hello`, receives `Welcome`, then pumps `Input`/`Resize` and `Output` frames. | |
| 74 | + | - Remote attach runs `ssh -- host vev _stdio [session]` and proxies the same protocol over stdio. | |
| 75 | + | - Only one client may attach to a session; a new attach replaces the old client. | |
| @@ -1,67 +1,75 @@ | |||||
| 1 | - | # CLAUDE.md | 1 | + | # vev agent guide |
| 2 | 2 | ||||
| 3 | - | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | 3 | + | ## Project |
| 4 | 4 | ||||
| 5 | - | ## What this is | 5 | + | 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. |
| 6 | - | ||||
| 7 | - | vev is a terminal multiplexer for Linux written in Go (deps: only `golang.org/x/sys`, `golang.org/x/term`). Sessions live in a per-user daemon; the client is a thin pipe. Rendering happens server-side with damage tracking so only minimal diffs cross the unix socket (or the SSH link on remote attach). | |||
| 8 | 6 | ||||
| 9 | ## Commands | 7 | ## Commands | ||
| 10 | 8 | ||||
| 11 | ```sh | 9 | ```sh | ||
| 12 | - | go build -o vev . # build | 10 | + | go build -o vev . |
| 13 | - | make test # go test ./... -race | 11 | + | make test |
| 14 | - | go test ./internal/usecase/daemon -race -run TestName # single test | 12 | + | go test ./internal/usecase/daemon -race -run TestName |
| 15 | - | make lint # fails if goimports -l finds anything, then go vet | 13 | + | make lint |
| 16 | - | make mocks # regenerate mocks (mockery, config in .mockery.yml) | 14 | + | make mocks |
| 17 | - | go test ./pkg/renderer ./pkg/vt ./internal/adapters/ipc -bench=. -benchmem # microbenchmarks | 15 | + | go test ./pkg/renderer ./pkg/vt ./internal/adapters/ipc -bench=. -benchmem |
| 18 | ``` | 16 | ``` | ||
| 19 | 17 | ||||
| 20 | - | - Format with **goimports**, not gofmt — lint enforces it. | 18 | + | - Format with **goimports**. |
| 21 | - | - Daemon pprof is opt-in via `VEV_PPROF_ADDR=127.0.0.1:6060 vev --daemon` (see `docs/performance.md`). | 19 | + | - `make test` runs `go test ./... -race`. |
| 22 | - | - Live logs are under `$XDG_STATE_HOME/vev` (or `~/.local/state/vev` when `XDG_STATE_HOME` is unset): `vev-daemon.log`, `vev-client.log`, `vev-stdio.log`, plus matching `*-crash.log` files. | 20 | + | - `make lint` checks `goimports -l`, then `go vet`. |
| 23 | - | - Feature branches live in git worktrees under `.worktrees/<branch-name>`. | 21 | + | - Enable daemon pprof with `VEV_PPROF_ADDR=127.0.0.1:6060 vev --daemon`. |
| 22 | + | - Logs live in `$XDG_STATE_HOME/vev`, or `~/.local/state/vev` when unset. | |||
| 23 | + | - Use feature worktrees under `.worktrees/<branch-name>`. | |||
| 24 | + | ||||
| 25 | + | ## Architecture rules | |||
| 26 | + | ||||
| 27 | + | Hexagonal boundaries are enforced by `boundary_test.go`. | |||
| 24 | 28 | ||||
| 25 | - | ## Architecture | 29 | + | - `pkg/` is reusable and must not import `internal/`. |
| 30 | + | - `internal/usecase/` must not import `internal/adapters/`. | |||
| 31 | + | - Usecases depend on `internal/ports` and `internal/domain`. | |||
| 26 | 32 | ||||
| 27 | - | Hexagonal, enforced mechanically by `boundary_test.go` at the repo root: | 33 | + | Layer map: |
| 28 | - | - `pkg/` must not import `internal/` (pure, reusable: `pkg/vt` VT emulator, `pkg/renderer` damage-diff renderer, `pkg/kv` store). | |||
| 29 | - | - `internal/usecase/` must not import `internal/adapters/`. Usecases depend only on `internal/ports` (interfaces + wire protocol) and `internal/domain`. | |||
| 30 | 34 | ||||
| 31 | - | Layers: | 35 | + | - `main.go` → `internal/app`: CLI parsing, wiring, daemon startup, hidden subcommands. |
| 32 | - | - `main.go` → `internal/app`: CLI parsing (`parseArgs`/`dispatch` in `run.go`), dependency wiring, daemon spawn/ensure logic. Hidden subcommands (e.g. `_stdio`) live here. | 36 | + | - `internal/ports`: interfaces and wire protocol. |
| 33 | - | - `internal/ports`: all interfaces (`Transport`, `Listener`, `PTY`, `Terminal`, `Clock`, …) plus the wire protocol (`frame.go`, `wire.go`). | 37 | + | - `internal/usecase/daemon`: sessions, tabs, panes, VT screens, renderer shadows, daemon features. |
| 34 | - | - `internal/usecase/daemon`: the server. Owns sessions → tabs → panes, one `pkg/vt.Screen` per pane (server-side authoritative terminal state), and one `pkg/renderer.Renderer` shadow per attached client (per-client diff state). `daemon/client.go` documents the lock ordering at the top of the file — read it before touching teardown paths (`clientGone`, `killSession`, `shutdownAll`). | 38 | + | - `internal/usecase/client`: raw-mode thin client; writes output bytes verbatim and interprets nothing. |
| 35 | - | - `internal/usecase/client`: the thin client — raw-mode ownership, stdin/resize pumps, writes received output bytes verbatim to the terminal. It interprets nothing. | 39 | + | - `internal/adapters`: IPC, SSH stdio, PTY, terminal, and clock implementations. |
| 36 | - | - `internal/adapters`: `ipc` (unix socket framing, socket dir), `sshstdio` (remote attach), `pty`, `term`, `clock`. | |||
| 37 | - | - Other usecases (`keys`, `layout`, `copy`, `palette`, `picker`, `prompt`, `theme`, `ui`, …) are daemon-side features routed through `daemon/client.go`. | |||
| 38 | 40 | ||||
| 39 | - | ### Ports interfaces and mocks (mockery) | 41 | + | Before touching daemon teardown paths, read the lock-ordering notes at the top of `internal/usecase/daemon/client.go`. |
| 40 | 42 | ||||
| 41 | - | Every cross-layer abstraction is an interface in `internal/ports` (`Transport`, `Listener`, `PTY`, `PTYFactory`, `Terminal`, `Store`, `Clock`, `Timer`). Adapters implement them; usecases and `internal/app` consume them. This is the seam that makes the daemon and client testable without real sockets, PTYs, or wall-clock time (e.g. tests drive grace timers through the `Clock`/`Timer` ports instead of sleeping). | 43 | + | ## Ports and mocks |
| 42 | 44 | ||||
| 43 | - | Mocks are generated by **mockery** (testify template), configured in `.mockery.yml`: | 45 | + | - Define cross-layer interfaces only in `internal/ports`. |
| 44 | - | - `internal/ports` has `all: true` → every interface in the package gets a mock in `internal/ports/mocks/mocks.go` (package `portsmocks`, `MockTransport`, `MockPTY`, …). | 46 | + | - Adapters implement ports; usecases and `internal/app` consume ports. |
| 45 | - | - `internal/usecase/command.Context` gets a package-local `context_mock_test.go`. | 47 | + | - Tests should prefer generated `portsmocks.MockX` with `.EXPECT()`. |
| 48 | + | - Hand-written fakes are okay only when mocks make the test harder to understand. | |||
| 49 | + | - After changing a port, run `make mocks` and commit the generated updates. | |||
| 50 | + | - Time-dependent daemon code must use `ports.Clock`/`ports.Timer`, not direct wall-clock APIs. | |||
| 46 | 51 | ||||
| 47 | - | The workflow when adding or changing an abstraction: | 52 | + | ## Tests |
| 48 | - | 1. Add/modify the interface in `internal/ports` (never define the interface in a usecase or adapter). | |||
| 49 | - | 2. Run `make mocks` — the generated file is committed, and forgetting this step leaves stale mocks that still compile silently. | |||
| 50 | - | 3. In tests, prefer the generated `portsmocks.MockX` with `.EXPECT()` over hand-rolled stubs; hand-rolled fakes are acceptable only when a mock's call-expectation style fights the test (e.g. long-lived pumps). | |||
| 51 | 53 | ||||
| 52 | - | New time-dependent daemon code must go through `ports.Clock`/`ports.Timer`, not `time.Now`/`time.NewTimer`, or it becomes untestable under the existing fake clock. | 54 | + | - Write Go tests as table-driven tests by default. |
| 55 | + | - Keep wire tests byte-for-byte and include truncated-prefix and trailing-garbage coverage. | |||
| 56 | + | - Prefer fake clocks over sleeps. | |||
| 53 | 57 | ||||
| 54 | - | ### Wire protocol rules | 58 | + | ## Wire protocol |
| 55 | 59 | ||||
| 56 | - | The IPC protocol lives entirely in `internal/ports` (`frame.go`, `wire.go`): frames are 4-byte big-endian length + 1 type byte + payload; messages are hand-marshalled big-endian. | 60 | + | Wire code lives in `internal/ports/frame.go` and `internal/ports/wire.go`. |
| 57 | 61 | ||||
| 58 | - | - Client-originated message types are 1–15, server-originated 16+. | 62 | + | - Frames are 4-byte big-endian length, 1 type byte, then payload. |
| 59 | - | - Version negotiation is strict equality; the version must stay the **first field** of `Hello` so `PeekHelloVersion` keeps working on any version's payload. Bump `ProtocolVersion` when changing any message layout. | 63 | + | - Client message types are `1–15`; server message types are `16+`. |
| 60 | - | - `wire_test.go` contains byte-for-byte golden tests plus truncated-prefix / trailing-garbage invariants for every message. Any wire change must update the goldens and keep those invariants. | 64 | + | - Version negotiation requires strict equality. |
| 65 | + | - `Hello.Version` must stay first so `PeekHelloVersion` works. | |||
| 66 | + | - Bump `ProtocolVersion` for any message layout change. | |||
| 61 | 67 | ||||
| 62 | - | ### Session/attach flow | 68 | + | ## Session flow |
| 63 | 69 | ||||
| 64 | - | - Ephemeral sessions (numbered) die on detach; named sessions survive headless. The daemon starts on first use and exits when the last session ends. | 70 | + | - Ephemeral numbered sessions die on detach. |
| 65 | - | - Local attach: client dials the unix socket (spawning the daemon if needed via `ensureDaemon` in `internal/app`), sends `Hello{Intent, Name, Size, …}`, gets `Welcome`, then pumps `Input`/`Resize` against `Output` frames. | 71 | + | - Named sessions survive headless. |
| 66 | - | - Remote attach: `vev attach user@host[:session]` runs `ssh -- host vev _stdio [session]`; the remote `_stdio` process proxies frames between ssh stdio and the remote daemon's unix socket. Same protocol end to end, so vev must be installed on the remote host. | 72 | + | - The daemon starts on first use and exits when the last session ends. |
| 67 | - | - Only one client per session: a new attach displaces the previous one (`detachReplacedClient`). | 73 | + | - Local attach sends `Hello`, receives `Welcome`, then pumps `Input`/`Resize` and `Output` frames. |
| 74 | + | - Remote attach runs `ssh -- host vev _stdio [session]` and proxies the same protocol over stdio. | |||
| 75 | + | - Only one client may attach to a session; a new attach replaces the old client. | |||