CLAUDE.md@pfm · diff
git:20260921.53cde57 to git:20260930.e6045d8
70 added, 91 removed. Audit A to A.
- # pfm (Professor-Fleet-Manager)
-
- Go engine for the chat fleet — one binary that indexes, lists, names, kills, attaches, injects, and drives every live and resumable Claude Code and Codex chat on the box. It replaced 1,293 lines of zsh (`cc-ls`) whose cost was structural: ~300 forks per refresh over a 12,000-transcript corpus, and the same naming / kill-ratchet logic written three and four times over. The flow matrix is `TESTPLAN.md`.
-
- ## Quick Start
-
- ```bash
- go build -o pfm.dev ./cmd/pfm # from pfm/
- ./pfm.dev doctor # environment self-check
- ./pfm.dev ls --plain # read-only listing, no TUI
- make tools # pinned golangci-lint + go-test-coverage (infra/fence/tools.env)
- make fmt && make lint # rewrite formatting; the full lint backlog
- make cover # cross-package coverage against .testcoverage.yml
- ```
-
- Use `/dev build pfm` · `/dev verify pfm` · `/dev test pfm` for anything the pipeline reads.
-
- ## Stack
+ # pfm — the Go fleet engine: one binary that indexes, lists, names, kills, attaches, injects and drives every live and resumable Claude Code and Codex chat on the box
- - **Runtime:** Go 1.24.13 — **pinned, not latest** (`go.mod`; mise: `mise use -g go@1.24`)
- - **TUI:** `charm.land/bubbletea/v2` + `bubbles/v2` + `lipgloss/v2` — verify the v2 API against the upstream repo before writing UI code; v1 examples do not compile
- - **Fuzzy match:** `sahilm/fuzzy` · **MCP:** `modelcontextprotocol/go-sdk`
- - **Storage:** `modernc.org/sqlite` — pure Go, `CGO_ENABLED=0`, static binary. On-demand incremental indexing; **no daemon.**
- - **CLI:** stdlib `flag` dispatch in `cmd/pfm/`. **No cobra, no config system, no telemetry** — boring Go for one box.
- - **Platforms:** Linux and Darwin, one codebase; see § Platforms.
+ It reads and writes the user's real chat state: a destructive operation on a live socket is not recoverable by a rerun, so verify the target resolves before acting, and prefer refusing to guessing.
- ## File Structure
+ # Vocabulary
- `cmd/pfm/` holds the CLI adapters and the end-to-end jail tests; the doctor, the hook entries, the picker, the self-update and the project baselines are `internal/{doctor,hookentry,picker,update,professor}`; `pfm help` lists the subcommands. Each `internal/` package opens with a doc comment naming what it owns: `go list -f '{{.ImportPath}}: {{.Doc}}' ./internal/...` is the package map.
+ - flow matrix: every flow, its test tier and its jail status · `TESTPLAN.md`
+ - CLI adapters: stdlib `flag` dispatch plus the end-to-end jail tests; a file name starts with its unit (`chat_` is the one multi-file unit), so `ls cmd/pfm/{unit}_*` lists a unit; `pfm help` lists the subcommands · `cmd/pfm/`
+ - package map: each `internal/` package opens with a doc comment naming what it owns; `go list -f '{{.ImportPath}}: {{.Doc}}' ./internal/...` prints the map · `internal/doctor/`, `internal/hookentry/`, `internal/picker/`, `internal/update/`, `internal/professor/`
+ - tagged e2e: the `-tags e2e` tier, run serially · `e2e/`
+ - jail: a test run with every filesystem location redirected by `PFM_*` overrides (§ Environment Variables); reference harness `testdata/e2e.sh`, fixtures `testdata/{claude-store|codex-store|crumbs|proc|golden}/`
+ - eval protocol (K1): the binary prints one shell line on stdout for the shim to `eval`, never execing the final tmux attach itself
+ - K3: one implementation per rule — naming precedence, the kill ratchet, row classification, run-string synthesis — each in exactly one package with table-driven tests
+ - façades: the one door per cross-cutting primitive — `atomicfile.Write` (whole-file replace), `sqlitedb` (every SQLite open), `tmux.Exec` and `tmux.Socket` over it (every tmux call on a chat socket), `internal/chat/` (the chat verbs a surface calls typed) · `internal/atomicfile/`, `internal/sqlitedb/`, `internal/tmux/`
+ - architecture ratchet: C1–C21 checked against baselines that only shrink; `--measure` locks a shrink · `scripts/arch-check.sh`, `.arch/`
+ - lint law: gofumpt + gci + golines at 120, plus dupl, goconst, gocritic, revive, staticcheck; coverage thresholds · `.golangci.yml`, `.testcoverage.yml`, tools pinned in `infra/fence/tools.env`
+ - shim: the thin zsh wrapper that `eval`s the eval-protocol line, sourced in place from the clone · `internal/installer/assets/shim/pfm.zsh`, its tests `internal/installer/shim/`
+ - installer: stages every host asset embedded from `internal/installer/assets/`; the fleet prompts and the shim are read from the clone instead · `internal/installer/`
+ - composed prompts: the tracked fleet prompt per engine, drift-gated by a test · `harness-prompts/composed/` · generated from `harness-prompts/` by `make -C pfm prompts`
+ - machine config: gitignored `{clone}/pfm.config.json`, seeded from `example.pfm.config.json`; `PFM_CONFIG` overrides · design `docs/design/engines/pfm-home.md`
+ - launch registry: every flag, env value and `--settings` key a Claude chat starts with · `internal/claudelaunch/` · design `docs/design/engines/claude-launch.md`
+ - `deps.Registry`: the single place a platform difference is declared; `deps.Resolve` refuses a gated name off-platform even when it is on PATH · `internal/deps/registry.go`
+ - config: account identity, emoji, theme and permission posture · `internal/config/`
+ - lineage: folds a Codex subagent thread into its parent seat · `internal/store/lineage.go`
+ - migrations: additive, numbered `migration_v{N}.sql` files, each with its `go:embed` · `internal/store/schema.sql`
+ - `pfm.dev`: the local build artifact, never the shipped path; the host mirror build is `make host-install` (root § Host), which stamps `-X main.version` from `VERSION`
- `internal/installer/assets/shim/pfm.zsh` is the thin post-cutover wrapper (`internal/installer/shim/` holds only its tests). `cmd/pfm` file names start with their unit — `chat_` is the one multi-file unit still in main; `doctor ls internal update` left for `internal/` — so `ls cmd/pfm/<unit>_*` lists a unit. `testdata/` holds `claude-store/ codex-store/ crumbs/ proc/ golden/` plus the reference harness `e2e.sh`.
+ # Runtime
- ## Platforms
+ ## Fence
- `internal/deps.Registry` is the single place a platform difference is declared. An entry with no `Platforms` field applies everywhere; a gated entry is skipped on any other OS, and `deps.Resolve` REFUSES a gated name off-platform even when the binary is present on PATH — so a gate is a capability claim, not a hint.
+ - `iso verify pfm` runs vet, fmt-check, lint-new (lines changed since the Makefile's `LINT_BASE`, origin/main: a develop base judges nothing on a push to develop), the architecture ratchet and the gate scripts' own `scripts/*_test.sh`; `iso test pfm` runs unit plus tagged e2e, each against its timing budget; `iso e2e` the tagged tier alone.
+ - One package or probe: `.claude/scripts/dev.sh iso run 'go -C pfm test -count=1 ./internal/{pkg}/'`; the lint burn-down view: `iso run 'make -C pfm lint'`.
+ - The fence mounts the worktree read-only: formatting rewrites the tree, so `make -C pfm fmt` runs on the host in the worktree, after `make -C pfm tools` installs the pinned tools; `make -C pfm prompts` likewise runs on the host.
- Platform-gated entries:
+ ## Environment Variables
- | Dependency | Platform | Required | Purpose |
- | --- | --- | --- | --- |
- | `ps` | darwin | yes | process-table inspection |
- | `lsof` | darwin | yes | open-file inspection |
- | `launchctl` | darwin | yes | launch-agent wiring |
- | `setsid` | linux | yes | detached helper processes |
- | `systemctl` | linux | no | user-service wiring |
- | `systemd-run` | linux | no | durable chat scopes from user services |
- | `nohup` | linux, darwin | no | detach fallback where `setsid` is absent |
- | `uv` | linux, darwin | yes | provisioned harvestpy package verifier |
- | `harvestpy` | linux, darwin | yes | provisioned harvestpy interpreter |
+ - Test-jail overrides, not a config system (`internal/paths/paths.go`): `PFM_HOME` · `PFM_STATE_DB` · `PFM_CACHE_DB` · `PFM_CONFIG` · `PFM_MANAGED_SETTINGS_DIR` · `PFM_SID_DIR` · `PFM_CLAUDE_ROOTS` · `PFM_CODEX_ROOT` · `PFM_TMUX_DIR` · `PFM_PROC_ROOT` · `PFM_TMUX_CONF` · `PFM_SKILL_SOURCES_OFFLINE` (install fetches no source-fetched skill; doctor reports OFFLINE).
+ - `PFM_TMUX_CONF` unset: a chat's tmux server loads the user's own `~/.tmux.conf`, since a chat is a terminal the user lives in; a jail sets it to `/dev/null` so a real machine config never steers a fixture.
+ - Test knobs outside `internal/paths/`: the scan clock `PFM_TEST_NOW_NS` (`internal/fleet/scan.go`), `PFM_TEST_FRESH_SOCKET` (`internal/spawn/socket.go`).
- Everything else (`tmux`, `git`, `sh`, `bash`, `zsh`, `sleep`, `script`, `go`) is ungated and must work identically on both.
+ # Rules
- Two rules follow from the table:
+ ## Live chat state (sacred ground)
- - A fallback's gate must cover every platform that reaches it. `setsid` is linux-only, so darwin is the only platform that ever takes the `nohup` branch — gating `nohup` to linux would make the fallback unreachable exactly where it is needed.
- - Gate on the binary's real availability, and give an entry `VersionArgs` only if every gated platform's build accepts them. BSD and GNU builds of the same name diverge: `nohup --version` is a GNU extension that BSD `nohup` rejects, so a version probe there reports a working binary as broken.
+ - **Tests NEVER touch a live `cc-*` / `cx-*` socket or the real `/tmp/cc-sid`:** every test sets `TMUX_TMPDIR = t.TempDir()`.
+ - **Destructive commands default to a dry run, and the dry run IS the apply's preview:** `reap`, `archive` and `heal` classify identically with and without `--apply`; every unknown — an unanswerable busy query, a silent socket, a chat writing its transcript right now — resolves toward keeping what exists.
+ - **No suite runs a real engine or a real credential:** a flow the fake engine cannot play is named `UNPLAYED` in `TESTPLAN.md` § Flows the fake engine does not yet play, never run against a real engine and never left quietly uncovered.
## Code Standards
- - **The eval protocol (K1) — the binary never execs the final tmux attach.** TUI renders on `/dev/tty`, informational output goes to **stderr**, and exactly ONE shell line goes to **stdout** for the zsh wrapper to `eval`. Bunker semantics need `exec tmux attach` to replace the interactive shell; a ✦-new Claude row spawns natively through `action.ClaudeSpawn` (no shell function), while a ✦-new Codex row still calls the user's own `cx`. Emitted lines are golden-testable — keep them that way.
- - **One implementation each (K3).** Naming precedence, the kill ratchet, row classification, and run-string synthesis each live in exactly ONE package with table-driven tests. A second copy is the bug this rewrite exists to kill. The cross-cutting primitives have one façade each: `atomicfile.Write` for a whole-file replace, `sqlitedb` for every SQLite open, `tmux.Exec` (and `tmux.Socket` over it) for every tmux invocation on a chat socket, and `internal/chat` for the chat verbs a surface (CLI, MCP) calls typed — MCP reaches the rest through argv `Dispatch`, which C10 counts and only lets shrink.
- - **The architecture ratchet.** `scripts/arch-check.sh` checks C1–C21 against the `.arch/` baselines, and `.claude/scripts/dev.sh verify pfm` runs it. A baseline only shrinks: `--measure` locks a shrink, a new entry is a FAIL to fix, and a genuinely new exception is a hand edit to `.arch/` named in the commit message.
- - **The lint law.** `.golangci.yml` (dupl, goconst, gocritic, revive, staticcheck; gofumpt + gci + golines at 120) and `.testcoverage.yml` are gates, not advice: `make lint-new` blocks a wave on the lines it changed, `make lint` is the burn-down view, `make cover` thresholds only ratchet up. Tools are pinned in `infra/fence/tools.env` — `make tools` on the host, baked into the fence image.
- - **A kill is permanent; the store table keeps its `hidden` name.** A kill of a LIVE chat always runs the detached exit choreography; `--exit` is its explicit form.
- - **One binary, two kernels — and the seam is a build tag, never a runtime `if`.** Linux and macOS are both supported. Where they differ, a `_linux.go` / `_darwin.go` pair defines the same identifier and the caller stays platform-blind (`getTermios`, `nativeProcFS`, `nativeProcesses`, `schedulerIsLaunchd`). Three differences are load-bearing: there is no `/proc`, so the process table comes from `sysctl` and `ProcFS` dispatches through `gather.NewProcFS` — which still honours an EXISTING root, because that is how the jail feeds it fixtures; tmux's ioctl and format-separator spellings differ, so parse through `internal/tmux` (`format.go`) and never against one spelling; and there is no dead-launchd jail, so the installer's rc 97 gate narrows to "not mid-execution" rather than "manager not live". A platform whose constants nobody has confirmed must fail to BUILD, not fall back to a guess.
- - **Never hardcode `$HOME`, `/tmp`, a socket dir, or `/proc`.** Every filesystem location resolves through `internal/paths`; `/proc` sits behind the `ProcFS` interface. This is not decoration — it is the only reason the suite can run in a jail.
- - **The ratchet counts prompts, not bytes (K2).** Kill baselines are `baseline_prompts`; auto-unkill is `prompt_count > baseline`. Legacy byte baselines convert exactly once at import.
- - **Account identity, emoji, theme, and permission posture come ONLY from `internal/config`** — a hardcoded account count, `.cc/N` literal, medal emoji, or bypass flag outside the config package is a defect.
- - **A seat is identified by lineage, never by file recency.** Codex ≥0.146 writes subagent threads into `~/.codex/sessions` beside real seats, so the newest rollout there is usually a child — its `session_meta` carries `thread_source: subagent` and a `parent_thread_id`. `store/lineage.go` folds a child into its parent so it never earns a row; anything reading the rollout tree directly checks those two fields first, or it reads a subagent's empty context as a seat's amnesia.
- - **Identity is derived where the chat is, never where the message is delivered.** A detached process — the `--then` waiter, any dispatcher a chat backgrounds — is reparented, severing the process chain ancestry recovery walks, and a codex tool shell carries neither `$TMUX` nor a session id of its own: it derives NOTHING and its message goes out UNSIGNED. The chat resolves its own identity while it still can and states it through `CHAT_SENDER_SESSION` / `CHAT_SENDER_LABEL` / `CHAT_SENDER_SID`, which `inject` reads from its OWN environment only — a chat states who IT is, never who somebody else is. A codex seat driven by `codex app-server` never has tmux in its ancestry at all (one server, reparented to init, serving every seat), so there identity comes from `CODEX_THREAD_ID` through the fleet's thread→socket binding — the last rung, because that variable is inherited and must never rename a process that has a pane of its own.
- - **Destructive commands default to a dry run, and the dry run IS the apply's preview.** `reap`, `archive`, and `heal` classify identically with and without `--apply`; only the actions differ. A preview that disagrees with the run it previews is worse than none, and every unknown — an unanswerable busy query, a socket that did not respond, a chat writing its transcript right now — resolves toward keeping what exists.
- - **A probe that could not run never returns "nothing found."** A pane capture on the wrong socket returns silence identical to a quiet chat; `kill -0` cannot tell a healthy waiter from a reparented deaf one. Build the distinguishing signal into the probe and return an error, not an empty set. This is the root law at engine scale, and it is the single most common defect class here.
- - Errors wrap with context (`fmt.Errorf("...: %w", err)`); a swallowed error in a gather path renders as a missing chat row, which reads as "no such chat."
- - SQLite migrations are additive and numbered — a new `migration_vN.sql` plus its `go:embed`, never an edit to `schema.sql` that existing databases will never see.
- - Behavior belongs in Go, not in the shim (`internal/installer/assets/shim/pfm.zsh`). If a fix is easier in the shim, that is a signal the Go surface is wrong.
+ - Eval protocol (K1): the TUI renders on `/dev/tty`, information goes to stderr, exactly one shell line to stdout; bunker semantics need `exec tmux attach` to replace the interactive shell.
+ - A ✦-new Claude row spawns natively through `action.ClaudeSpawn`; a ✦-new Codex row calls the user's own `cx`; emitted lines stay golden-testable.
+ - K3: a second copy of a K3 rule is a defect; MCP reaches everything outside `internal/chat/` through argv `Dispatch`, which C10 counts and lets only shrink.
+ - Architecture ratchet: `.claude/scripts/dev.sh iso verify pfm` runs it; a new entry is a FAIL to fix; a genuinely new exception is a hand edit to `.arch/` named in the commit message.
+ - Lint law: `.golangci.yml` and `.testcoverage.yml` are gates — lint-new blocks a wave on the lines it changed, `make lint` is the burn-down view, coverage thresholds only ratchet up.
+ - A kill is permanent, and the store table keeps its `hidden` name; a kill of a live chat always runs the detached exit choreography, `--exit` its explicit form.
+ - The ratchet counts prompts, not bytes (K2): kill baselines are `baseline_prompts`; auto-unkill is `prompt_count > baseline`; legacy byte baselines convert once, at import.
+ - One binary, two kernels: where Linux and macOS differ, a `_linux.go` / `_darwin.go` pair defines the same identifier and the caller stays platform-blind (`getTermios`, `nativeProcFS`, `nativeProcesses`, `schedulerIsLaunchd`) — the seam is a build tag, not a runtime `if`.
+ - Darwin has no `/proc`: the process table comes from `sysctl`, and `ProcFS` dispatches through `gather.NewProcFS`, which still honours an existing root, because that is how the jail feeds it fixtures.
+ - tmux's ioctl and format-separator spellings differ by kernel: parse through `internal/tmux/format.go`, not against one spelling.
+ - Darwin has no dead-launchd jail, so the installer's rc 97 gate narrows to "not mid-execution" instead of "manager not live".
+ - A platform whose constants nobody has confirmed fails to build instead of falling back to a guess.
+ - `deps.Registry`: an entry with no `Platforms` field applies everywhere; everything ungated (`tmux`, `git`, `sh`, `bash`, `zsh`, `sleep`, `script`, `go`) works identically on both kernels.
+ - A fallback's gate covers every platform that reaches it: `setsid` is linux-only, so darwin alone takes the `nohup` branch, and gating `nohup` to linux would strand the fallback exactly where it is needed.
+ - Gate an entry on the binary's real availability, and give it `VersionArgs` only when every gated platform's build accepts them: BSD `nohup` rejects GNU's `--version`, reporting a working binary as broken.
+ - Every filesystem location resolves through `internal/paths/`, and `/proc` sits behind the `ProcFS` interface — `$HOME`, `/tmp`, a socket dir and `/proc` are never literals; this is what lets the suite run in a jail.
+ - Account identity, emoji, theme and permission posture come from `internal/config/` alone: a hardcoded account count, `.cc/{N}` literal, medal emoji or bypass flag elsewhere is a defect.
+ - Every Claude launch flag, env value and `--settings` key comes from the launch registry alone; a launch literal elsewhere fails `TestNoLaunchLiteralOutsideRegistry`.
+ - A seat is identified by lineage, not file recency: Codex writes subagent threads into `~/.codex/sessions` beside real seats, marked `thread_source: subagent` with a `parent_thread_id`; code reading the rollout tree directly checks those two fields first.
+ - Identity is derived where the chat is, not where the message is delivered: a detached process (the `--then` waiter, any dispatcher a chat backgrounds) is reparented, and a Codex tool shell has neither `$TMUX` nor a session id — it derives nothing, and its message goes out unsigned.
+ - A chat states its own identity through `CHAT_SENDER_SESSION` / `CHAT_SENDER_LABEL` / `CHAT_SENDER_SID`, which `inject` reads from its own environment only — a chat states who it is, never who somebody else is.
+ - A seat driven by `codex app-server` has no tmux ancestry: its identity comes from `CODEX_THREAD_ID` through the fleet's thread→socket binding, the last rung, since that variable is inherited and never renames a process that has a pane of its own.
+ - A probe that could not run returns an error, not "nothing found": a pane capture on the wrong socket reads as a quiet chat, and `kill -0` cannot tell a healthy waiter from a reparented deaf one — build the distinguishing signal into the probe; the most common defect class here.
+ - Errors wrap with context (`fmt.Errorf("…: %w", err)`): a swallowed error in a gather path renders as a missing chat row, which reads as "no such chat".
+ - A schema change is a new `migration_v{N}.sql` plus its `go:embed`; `internal/store/schema.sql` is never edited for it, since existing databases never see it.
+ - Behavior belongs in Go, not the shim: a fix that is easier in the shim signals that the Go surface is wrong.
+ - The CLI stays boring Go for one box: stdlib `flag`, no cobra, no config system, no telemetry.
+ - Storage is modernc.org/sqlite, pure Go with `CGO_ENABLED=0` for a static binary; indexing is on-demand and incremental, with no index daemon.
+ - The TUI is charm.land/bubbletea, bubbles and lipgloss, all v2: verify the v2 API against upstream before writing UI code, since v1 examples do not compile.
+ - Go is pinned in `go.mod` (mise: `mise use -g go@1.27`).
## Testing Rules
- Package tests beside every package, plus the end-to-end jail tests under `cmd/pfm/`. Tiers are defined in `TESTPLAN.md` § Legend:
-
- | Tier | Means |
- | --- | --- |
- | `JAIL` | fully exercisable via the `PFM_*` env overrides; reference harness `testdata/e2e.sh` |
- | `JAIL+tmux` | needs a real tmux server on a **scratch socket inside the jail's `TMUX_TMPDIR`** |
- | `JAIL+sh` | zsh/bash fixture with a fake `$HOME`, scratch DB, and an EMPTY socket dir |
- | `LIVE-READ` | read-only observation of live state (`ls --tsv`, `--plain`, `doctor`, `sqlite3 -readonly`) |
- | `REAL-SESSION` | ⚠ cannot be jailed — needs a genuine `claude`/`codex` process. Schedule deliberately, never incidentally |
-
- - **No test may touch a live `cc-*` / `cx-*` socket or the real `/tmp/cc-sid`.** Every test sets `TMUX_TMPDIR = t.TempDir()`.
- - Keep scratch socket paths SHORT — a long `TMUX_TMPDIR` hits "File name too long" and the failure looks like a tmux bug.
- - Table-driven tests for every single-implementation rule (K3); golden files for emitted shell lines.
- - A `REAL-SESSION` flow that cannot be jailed is NAMED in `TESTPLAN.md` § "Flows that CANNOT be jailed" — never quietly left uncovered.
-
- ## Environment Variables
-
- Test-jail overrides only — **not a config system** (`internal/paths/paths.go`):
-
- `PFM_HOME` · `PFM_DB` · `PFM_FLEET_DB` · `PFM_SID_DIR` · `PFM_CLAUDE_ROOTS` · `PFM_CODEX_ROOT` · `PFM_TMUX_DIR` · `PFM_PROC_ROOT` · `PFM_TMUX_CONF`
-
- `PFM_TMUX_CONF` is load-bearing beyond the jail: unset, a chat's tmux server loads the user's own `~/.tmux.conf` — because a chat IS a terminal the user lives in, and one that ignores their config wears the wrong status bar. Jails set it to `/dev/null` so a real machine config can never steer a fixture.
-
- Test knobs outside `internal/paths`: the scan clock `PFM_TEST_NOW_NS` (`internal/fleet/scan.go`); `PFM_TEST_FRESH_SOCKET` (`internal/spawn/socket.go`).
-
- ## Boundaries
-
- - The engine reads and writes the user's real chat state. A destructive operation on a live socket is not recoverable by a rerun — verify the target resolves before acting, and prefer refusing to guessing.
- - `pfm.dev` is a local build artifact, never the shipped path. The host mirror build is `make host-install` — it stamps `-ldflags "-X main.version=$(cat VERSION)"`, so a bare `go build -o ~/.local/bin/pfm ./cmd/pfm` is never the documented command (it reports `--version` as `dev`, not the release).
- - The installer lives in `internal/installer/`, and every staged host asset (shim, units, command cards) comes from `internal/installer/assets/`, the fleet prompt parts from the embedded `harness-prompts/` package — the binary is the single source of truth; no external template dir exists.
+ - Package tests sit beside every package; the end-to-end jail tests under `cmd/pfm/`; tiers per `TESTPLAN.md` § Legend — the SAFETY column.
+ - Keep scratch socket paths short: a long `TMUX_TMPDIR` hits "File name too long", and the failure looks like a tmux bug.
+ - Every K3 rule gets table-driven tests; every emitted shell line a golden file.