AGENTS.md · git:20261018.f426746 · 2026-10-18 · sha256 224862d9c9b9cc74
AGENTS.md git:20261018.f426746A
Immutable. This exact content never changes and is served at /api/v1/blob/224862d9c9b9cc74.
# AGENTS.md Instructions for coding agents (**Claude Code**, Codex, Copilot, Cursor, anything that reads `AGENTS.md`) working in this repository. `CLAUDE.md` is read by Claude Code too; where the two disagree, this file wins on **what to run**, `CLAUDE.md` wins on **how the code works**. Read [`CLAUDE.md`](CLAUDE.md) (the architecture map) and the `docs/` page for the area you are touching before changing it. `docs/` holds the design rationale — what looks arbitrary in this code is usually a bug someone already paid for. ## The most important rule: run the smallest check that can fail This is a single ~44k-line crate with **~5,360 unit tests** (5,354 under `cargo test --lib` on this machine) and **144 smoke phases**. The full suite is not a "does it still work" button — it is the last step before a commit, and CI runs it for you on every push. Do **not** run `cargo test` (or `cargo clippy --all-targets`, or `scripts/smoke.sh`) after every edit to see whether something broke. Pick the targeted command for the code you touched, and escalate only when the change crosses a boundary. ### What each command actually costs here Measured on an 8-core Apple-silicon Mac, warm `target/` (not a cold checkout): | Command | Wall time | Run it when | | --- | --- | --- | | `cargo check --lib` | ~6 s | after any edit, if you only want "does it compile" | | `cargo test --lib <module>::` | ~0.4 s if nothing rebuilt, **~9 s after an edit** (recompile + link) | default loop after every edit in the library | | `cargo test --lib` (all 5,354) | ~44 s | once, before you call the work done | | `cargo test --test <name>` | ~12–35 s (rebuilds the bin for `CARGO_BIN_EXE`) | you touched a binary-facing seam (see matrix) | | `cargo fmt --check` | ~2 s | before commit | | `cargo clippy --all-targets -- -D warnings` | ~23 s warm | before commit, or after touching a lint-visible shape | | `cargo doc --no-deps --lib` | ~4 s | before commit **or** after moving/renaming a public item | | `cargo test` (everything, 22 integration binaries) | **~2 m 20 s** | once, at the end of a change that spans crates/seams | | `scripts/smoke.sh <id>` (one phase) | ~2–20 s | you touched the terminal boundary | | `scripts/smoke.sh` (all 144) | minutes, parallel | **only** for `src/tui/`, `term.rs`, rendering/geometry | | `cargo build --release` / `--example …` | 40–60 s+, and `target/` is already ~10 GB | only when the task explicitly needs a release artifact | `target/` is ~10 GB and incremental. Deleting it to "clean up" costs a multi-minute full rebuild — don't, unless the user asks. ### Which tests to run after a given change | You changed | Run this | | --- | --- | | `src/app/<area>.rs` (pure state) | `cargo test --lib app::tests::<area>` — the tests live beside the code, often in `src/app/tests/<area>.rs` | | `src/ui/<area>.rs` (renderers, theme) | `cargo test --lib ui::` — plus one smoke phase if the change is visible | | `src/llm/<provider or wire>` | `cargo test --lib llm::` — plus the live test for that provider **only if the user asks** (`--ignored`, spends real tokens) | | `src/tui/`, `term.rs`, `src/main.rs` | **no unit tests exist here** — `cargo build && scripts/smoke.sh <relevant id or range>` | | anything on screen (spacing, glyphs, colours, cursor) | one or two smoke phases; the whole suite only at the end | | a public item moved/renamed, or a `mod.rs` re-export | `cargo test --test api_surface` + `cargo doc --no-deps --lib` | | `src/cli.rs`, `--help`, arg parsing | `cargo test --test cli_help` | | `src/subprocess.rs` detach, `src/pty/` | `cargo test --test detached_exec` | | `src/llm/mcp*`, the `mcp` CLI | `cargo test --test mcp_cli` | | a memory-sensitive path (per-turn/per-request/per-picture allocation) | the matching `tests/*_memory.rs` gate (`image_turn_memory`, `model_parse_memory`, `image_paste_memory`, `write_preview_memory`) | | `providers.toml`, `src/llm/config.rs` | `cargo test --lib llm::config` — it pins the shipped provider set and the `snake_case` spellings | | `scripts/smoke/lib.sh`, `scripts/smoke.sh` (the phase readers or the selector) | `bash scripts/smoke/selftest.sh` — under a second, no app, no tmux | | `scripts/smoke/phases/`, screen behaviour | `cargo build && scripts/smoke.sh <ids>` — by **id**, and widen only if you must | | `scripts/`, `install.sh`, `npm/`, `telemetry/`, `docker/`, `aur/` | that subtree's own suite (see [`CONTRIBUTING.md`](CONTRIBUTING.md) *Testing*) — not `cargo test` | | `prompts/`, `docs/`, `CHANGELOG.md` | nothing; prose has no gate | A filtered run is not a compromise: `cargo test --lib app::tests` executes 1,059 real assertions in ~0.4 s once the binary is built, and while the tree is being edited the only cost is the ~9 s relink. The expensive part is the build, not the tests. libtest takes **one** filter per invocation, and module filters are prefixes, so prefer the wider one: `cargo test --lib app::tests` covers every area under `src/app/`, where `cargo test --lib app::tests::login` covers one file — and the two cost the same. Don't chain two narrow filters; run the prefix that contains both. When you want nearly everything except the environment-dependent modules, `cargo test --lib -- --skip pty:: --skip background:: --skip llm::exec::` runs 4,833 assertions in ~20 s. ### Before you say the work is done The project's gate (also what `.github/workflows/ci.yml` runs for you on push): ```bash cargo fmt --check cargo clippy --all-targets -- -D warnings cargo test # once, at the end cargo doc --no-deps --lib ``` `Cargo.toml`'s `[lints]` table already denies `warnings`, `clippy::all` and `unsafe_code`, so a warning is a build failure locally, not just in CI. The doc build is part of the gate because a broken intra-doc link is an error here: a public item's docs may not link to a private one, so moving an item between modules breaks the links that pointed at it. ## Linux vs macOS — what actually differs CI runs the gate and the smoke suite on `ubuntu-latest`, so **Linux is the reference**: `cargo test --lib` there is expected to be green, and any red is yours. On macOS a red run usually is not. Know which platform you are on before you diagnose anything. | | Linux (CI) | macOS | | --- | --- | --- | | `cargo test --lib` | green | **6 baseline failures** with `setsid` on `PATH`, **46** without it | | `setsid` | ships with util-linux | **absent**; `brew install util-linux`, then put it on `PATH` | | `ps … sid` | supported | **no `sid` keyword** — one test can never pass here | | tests that exist | 5,389 | **5,354** in `--lib` — 35 are `cfg(target_os = "linux")`, and 10 live in `src/tui/` (bin-only, never in `--lib`) | | `scripts/smoke.sh` | bash 5 from the distro | needs **bash 4+**; macOS `/bin/bash` is 3.2 and dies | | `--list`, phase titles/tags | works | works — the readers were BSD-`sed`-broken once; `selftest.sh` now pins them | | `timeout`, `nproc`, `grep -P` | present | **absent** (`smoke.sh` already falls back to `sysctl -n hw.ncpu`) | | a stale incremental link | rare | **`Undefined symbols: … (.llvm.…)`** after a branch switch | | `cargo doc --no-deps --lib` | clean | **fails on `origin/main`**: `src/pty/spawn.rs` links `super::probe::group_program`, which is `cfg(target_os = "linux")` | **35 tests never run on macOS.** They are not skipped-and-reported; the Linux `cfg` compiles them out, so nothing in the output mentions them. By file: `llm/exec.rs` 17, `pty/probe.rs` 8, `clipboard.rs` 4, `subprocess.rs` 4, `background.rs` 1, `pty/spawn.rs` 1. The modules themselves (`pty/probe`, `clipboard/linux`) have Linux-only bodies, and `tests/clipboard_linux.rs` is `#![cfg(target_os = "linux")]` — it compiles to **0 tests** on a Mac, silently. So a green macOS run says nothing about: the `/proc`-based "is it waiting for input" probe, the streamed `image/png` clipboard read, `setsid -c` detaching, or the Linux half of the executor. Say in the commit which platform you actually ran. Those areas need the Linux CI run (or a container) to mean anything. ### macOS: the six baseline failures Do not chase them, do not "fix" them, and do not report them as regressions. They need a real Linux/tty environment or a `setsid` binary: - `pty::spawn::tests::the_session_leads_its_own_group_and_session` — macOS `ps` has no `sid` keyword. - `llm::exec::tests::kill_asks_the_command_to_stop_before_it_insists`, `…::indented_lines_reach_a_program_that_takes_pastes_as_one_paste`, `…::a_key_name_sent_without_brackets_is_the_key_in_a_full_screen_program` — SIGTERM/paste timing through the pty. - `background::tests::a_background_display_that_reads_whole_lines_is_never_told_of`, `…::a_picker_reading_its_terminal_waits_however_its_line_animated`. Without `setsid` on `PATH`, the count is **46**: every tty-spawning test fails with `no way to give the command a terminal of its own here`. macOS ships no `setsid`; util-linux does. Put it on `PATH` before running the lib suite: ```bash PATH="/opt/homebrew/opt/util-linux/bin:$PATH" cargo test --lib ``` A green lib run with the six skipped: ```bash PATH="/opt/homebrew/opt/util-linux/bin:$PATH" cargo test --lib -- \ --skip background::tests::a_background_display_that_reads_whole_lines \ --skip background::tests::a_picker_reading_its_terminal_waits \ --skip llm::exec::tests::a_key_name_sent_without_brackets \ --skip llm::exec::tests::indented_lines_reach_a_program \ --skip llm::exec::tests::kill_asks_the_command_to_stop \ --skip pty::spawn::tests::the_session_leads_its_own_group ``` Other platform notes: - **`scripts/smoke.sh` needs bash 4+** (`declare -A` at its worker table). macOS `/bin/bash` is 3.2, so running it under that shell dies with `declare: -A: invalid option` and `TOTAL: unbound variable` — not a phase failure. Homebrew's bash is on `PATH` here, so `scripts/smoke.sh` works; if it ever stops, `bash --version` is the first thing to check. - **The smoke suite's own tests are `bash scripts/smoke/selftest.sh`** — the phase readers and the selector, over fixture headers, in under a second. Run it after touching `scripts/smoke/lib.sh` or `scripts/smoke.sh`; CI runs it before the phases. It exists because those readers fail *silently*: they decide which phases a query runs, so a reader that returns nothing runs the wrong suite rather than failing. - **`cargo doc --no-deps --lib` fails on macOS, on `origin/main`, for a reason that is not yours.** `src/pty/spawn.rs` links `[`super::probe::group_program`]` and that function is `cfg(target_os = "linux")`, so off Linux the intra-doc link has no target and the crate denies warnings. It is the **only** doc error here, and CI (Linux) is unaffected. So the doc gate cannot be observed locally on a Mac. Two honest options: run it in the Linux container/CI, or read the error list and confirm yours is not in it — `cargo doc --no-deps --lib 2>&1 | grep '^error'` prints exactly two lines here, the known unresolved link and the `could not document` summary. Do **not** delete the link to make it pass: that edits a platform-gated module and belongs in its own PR. `--document-private-items` does not help; it surfaces different, unrelated unresolved links. - **A stale incremental build can fail to link** with `Undefined symbols: … (.llvm.…)` after switching branches. That is incremental codegen, not your change. `cargo clean -p alter-zero` (39 s to rebuild here, and it keeps every dependency) fixes it — do **not** reach for `rm -rf target/`. - Every test that reaches the network, a real clipboard or a real X server is `#[ignore]`d (in `tests/live_*.rs`, `tests/clipboard_*.rs`, `tests/wire_history.rs`, `tests/write_preview_memory.rs`). `cargo test` is offline and deterministic — keep it that way. Never add `-- --ignored` to a routine run: those tests need API keys and **spend real tokens** on whatever account the key belongs to. The clipboard ones also overwrite your real pasteboard / need an X server. - Smoke phases each get their own tmux server, config home, `$HOME` and temp tree, so they neither see nor disturb your real `~/.alter-zero`, clipboard or `/resume` list. ### What a platform-blind diagnosis gets wrong Two failure modes to name, because both look like your bug and are not: - **"My change broke 46 tests"** on a fresh Mac — it did not; `setsid` is missing. Confirm with `command -v setsid` before reading a single stack trace. - **"It passes on macOS, ship it"** for anything in `pty/probe`, `exec`, `subprocess`, `background` or `clipboard` — half of that code is behind `cfg(target_os = "linux")` and macOS never compiled it. Those need the Linux CI run (or a container) to mean anything. ## Working style (standing instructions, not suggestions) From [`CONTRIBUTING.md`](CONTRIBUTING.md), unchanged: - **TDD is rigid**: no production code before a failing test, and you must see the test fail for the right reason. The only exception is `src/main.rs` + `src/tui/` — the terminal I/O boundary, verified by running the app and the smoke suite instead. - **Design before implementing** for a feature or non-trivial change: explore, ask your questions one at a time, propose 2–3 approaches, agree, then write it down in `docs/` (and update `docs/design.md` when behaviour changes). Trivial fixes need no ceremony — the failing test still does. - **One logical change per commit**, message a reader can act on. Don't bump the version or edit a released changelog section. - Intent lives in [`docs/`](docs/), user-visible changes in `CHANGELOG.md`, and the README stays a front door. A `docs/` page that no longer matches the code is a bug. ## Conventions that break the build if ignored - All styling is centralized: glyphs/geometry as `const`s in `src/ui/theme.rs`, colours as accessors reading `src/ui/palette.rs`. A new colour is a new role on `Palette` in **every** theme table plus its accessor — never a literal `Color::Rgb` outside `palette.rs`. - Width math goes through `cols()` (display columns), never `chars().count()`; every cell handed to the backend goes through `term::visible_cells`. - The input line is a `textarea::TextArea`, not a `String`. - Never build a `Value` tree for a body you read a few fields from, and never decode a picture whole to make a small one — resident memory is a feature here ([`docs/memory.md`](docs/memory.md), the `tests/*_memory.rs` gates). - `unsafe_code = "forbid"`. The terminal and the pty are reached through safe wrappers (`rustix`), not `pre_exec`. - Keep logic out of the I/O boundary: when `tui/` needs a decision, the decision belongs in a pure helper it calls (the live-region geometry works this way). - No emoji in code or comments. ## Don't - Don't run the whole `cargo test` or the whole smoke suite "to check" after a small edit — see the table above. - Don't `rm -rf target/`, don't re-enable/disable `incremental`, and don't touch `[profile.*]` or `rust-toolchain.toml` to "make it faster". The toolchain pin exists so the gate is reproducible; a floating `stable` breaks the build. - Don't leave a second filtered `cargo test` in the same edit; widen the filter. - Don't run `scripts/release.sh`, `docker/build.sh`, `makepkg`, or anything that publishes, builds images, or pushes — those are the user's calls. - Don't add `#[ignore]` to a failing test, or `#[allow(...)]` to silence a lint, in order to get green. Fix the cause or say you are blocked.