chore(docs): add agents md
- commit
- e0cc36ec71864b413c15f2eed74de8ca40ff6064
- author
- brice · · 2026-07-04 11:57 UTC
- parent
- e8e16e8
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +67 -0
A AGENTS.md +66 -0
| @@ -0,0 +1,66 @@ | |||
| 1 | + | # CLAUDE.md | |
| 2 | + | ||
| 3 | + | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | |
| 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). | |
| 8 | + | ||
| 9 | + | ## Commands | |
| 10 | + | ||
| 11 | + | ```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 | |
| 18 | + | ``` | |
| 19 | + | ||
| 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 | + | - Feature branches live in git worktrees under `.worktrees/<branch-name>`. | |
| 23 | + | ||
| 24 | + | ## Architecture | |
| 25 | + | ||
| 26 | + | Hexagonal, enforced mechanically by `boundary_test.go` at the repo root: | |
| 27 | + | - `pkg/` must not import `internal/` (pure, reusable: `pkg/vt` VT emulator, `pkg/renderer` damage-diff renderer, `pkg/kv` store). | |
| 28 | + | - `internal/usecase/` must not import `internal/adapters/`. Usecases depend only on `internal/ports` (interfaces + wire protocol) and `internal/domain`. | |
| 29 | + | ||
| 30 | + | Layers: | |
| 31 | + | - `main.go` → `internal/app`: CLI parsing (`parseArgs`/`dispatch` in `run.go`), dependency wiring, daemon spawn/ensure logic. Hidden subcommands (e.g. `_stdio`) live here. | |
| 32 | + | - `internal/ports`: all interfaces (`Transport`, `Listener`, `PTY`, `Terminal`, `Clock`, …) plus the wire protocol (`frame.go`, `wire.go`). | |
| 33 | + | - `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`). | |
| 34 | + | - `internal/usecase/client`: the thin client — raw-mode ownership, stdin/resize pumps, writes received output bytes verbatim to the terminal. It interprets nothing. | |
| 35 | + | - `internal/adapters`: `ipc` (unix socket framing, socket dir), `sshstdio` (remote attach), `pty`, `term`, `clock`. | |
| 36 | + | - Other usecases (`keys`, `layout`, `copy`, `palette`, `picker`, `prompt`, `theme`, `ui`, …) are daemon-side features routed through `daemon/client.go`. | |
| 37 | + | ||
| 38 | + | ### Ports interfaces and mocks (mockery) | |
| 39 | + | ||
| 40 | + | 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). | |
| 41 | + | ||
| 42 | + | Mocks are generated by **mockery** (testify template), configured in `.mockery.yml`: | |
| 43 | + | - `internal/ports` has `all: true` → every interface in the package gets a mock in `internal/ports/mocks/mocks.go` (package `portsmocks`, `MockTransport`, `MockPTY`, …). | |
| 44 | + | - `internal/usecase/command.Context` gets a package-local `context_mock_test.go`. | |
| 45 | + | ||
| 46 | + | The workflow when adding or changing an abstraction: | |
| 47 | + | 1. Add/modify the interface in `internal/ports` (never define the interface in a usecase or adapter). | |
| 48 | + | 2. Run `make mocks` — the generated file is committed, and forgetting this step leaves stale mocks that still compile silently. | |
| 49 | + | 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). | |
| 50 | + | ||
| 51 | + | 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. | |
| 52 | + | ||
| 53 | + | ### Wire protocol rules | |
| 54 | + | ||
| 55 | + | 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. | |
| 56 | + | ||
| 57 | + | - Client-originated message types are 1–15, server-originated 16+. | |
| 58 | + | - 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. | |
| 59 | + | - `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. | |
| 60 | + | ||
| 61 | + | ### Session/attach flow | |
| 62 | + | ||
| 63 | + | - Ephemeral sessions (numbered) die on detach; named sessions survive headless. The daemon starts on first use and exits when the last session ends. | |
| 64 | + | - 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. | |
| 65 | + | - 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. | |
| 66 | + | - Only one client per session: a new attach displaces the previous one (`detachReplacedClient`). | |
| @@ -0,0 +1,66 @@ | |||||
| 1 | + | # CLAUDE.md | |||
| 2 | + | ||||
| 3 | + | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | |||
| 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). | |||
| 8 | + | ||||
| 9 | + | ## Commands | |||
| 10 | + | ||||
| 11 | + | ```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 | |||
| 18 | + | ``` | |||
| 19 | + | ||||
| 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 | + | - Feature branches live in git worktrees under `.worktrees/<branch-name>`. | |||
| 23 | + | ||||
| 24 | + | ## Architecture | |||
| 25 | + | ||||
| 26 | + | Hexagonal, enforced mechanically by `boundary_test.go` at the repo root: | |||
| 27 | + | - `pkg/` must not import `internal/` (pure, reusable: `pkg/vt` VT emulator, `pkg/renderer` damage-diff renderer, `pkg/kv` store). | |||
| 28 | + | - `internal/usecase/` must not import `internal/adapters/`. Usecases depend only on `internal/ports` (interfaces + wire protocol) and `internal/domain`. | |||
| 29 | + | ||||
| 30 | + | Layers: | |||
| 31 | + | - `main.go` → `internal/app`: CLI parsing (`parseArgs`/`dispatch` in `run.go`), dependency wiring, daemon spawn/ensure logic. Hidden subcommands (e.g. `_stdio`) live here. | |||
| 32 | + | - `internal/ports`: all interfaces (`Transport`, `Listener`, `PTY`, `Terminal`, `Clock`, …) plus the wire protocol (`frame.go`, `wire.go`). | |||
| 33 | + | - `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`). | |||
| 34 | + | - `internal/usecase/client`: the thin client — raw-mode ownership, stdin/resize pumps, writes received output bytes verbatim to the terminal. It interprets nothing. | |||
| 35 | + | - `internal/adapters`: `ipc` (unix socket framing, socket dir), `sshstdio` (remote attach), `pty`, `term`, `clock`. | |||
| 36 | + | - Other usecases (`keys`, `layout`, `copy`, `palette`, `picker`, `prompt`, `theme`, `ui`, …) are daemon-side features routed through `daemon/client.go`. | |||
| 37 | + | ||||
| 38 | + | ### Ports interfaces and mocks (mockery) | |||
| 39 | + | ||||
| 40 | + | 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). | |||
| 41 | + | ||||
| 42 | + | Mocks are generated by **mockery** (testify template), configured in `.mockery.yml`: | |||
| 43 | + | - `internal/ports` has `all: true` → every interface in the package gets a mock in `internal/ports/mocks/mocks.go` (package `portsmocks`, `MockTransport`, `MockPTY`, …). | |||
| 44 | + | - `internal/usecase/command.Context` gets a package-local `context_mock_test.go`. | |||
| 45 | + | ||||
| 46 | + | The workflow when adding or changing an abstraction: | |||
| 47 | + | 1. Add/modify the interface in `internal/ports` (never define the interface in a usecase or adapter). | |||
| 48 | + | 2. Run `make mocks` — the generated file is committed, and forgetting this step leaves stale mocks that still compile silently. | |||
| 49 | + | 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). | |||
| 50 | + | ||||
| 51 | + | 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. | |||
| 52 | + | ||||
| 53 | + | ### Wire protocol rules | |||
| 54 | + | ||||
| 55 | + | 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. | |||
| 56 | + | ||||
| 57 | + | - Client-originated message types are 1–15, server-originated 16+. | |||
| 58 | + | - 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. | |||
| 59 | + | - `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. | |||
| 60 | + | ||||
| 61 | + | ### Session/attach flow | |||
| 62 | + | ||||
| 63 | + | - Ephemeral sessions (numbered) die on detach; named sessions survive headless. The daemon starts on first use and exits when the last session ends. | |||
| 64 | + | - 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. | |||
| 65 | + | - 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. | |||
| 66 | + | - Only one client per session: a new attach displaces the previous one (`detachReplacedClient`). | |||
A CLAUDE.md +1 -0
| @@ -0,0 +1,1 @@ | |||
| 1 | + | AGENTS.md⏎̸ | |
| @@ -0,0 +1,1 @@ | |||||
| 1 | + | AGENTS.md⏎̸ | |||