# AGENTS.md
This file provides guidance to AI coding agents (Claude Code, agy, codex, etc.) when working with code in this repository.
Ways of working are deliberately not repeated here. They live in the phpboyscout
skills, and naming a skill tends to age better than restating it.
## What this is
`rtb-error` is the error vocabulary for the
[phpboyscout Rust toolkit](https://rust.phpboyscout.uk): the `Error` enum, the
`Result<T, E = Error>` alias, the process-edge `miette` hook pipeline
(`src/hook.rs`) and exit-code attachment (`src/exit_code.rs`). Three source
files, 370 lines. It is one of the small, framework-free crates extracted from
[rust-tool-base](https://gitlab.com/phpboyscout/rust-tool-base), and
`deny.toml:36-45` enforces the framework-free half by banning a dependency back
onto `rust-tool-base` or `rtb-cli-bin`.
Errors are values: no `ErrorHandler` trait, no `.check()` funnel — propagate
with `?`, render once at `main()`. It does not log, does not apply the exit code
it stores, and cannot recover a `miette` hook something else installed first.
`docs/explanation/03-what-this-does-not-do.md` covers those limits accurately
and is worth reading before concluding something is missing by accident.
`src/lib.rs:15` declares `#![forbid(unsafe_code)]`, on top of the manifest-level
`unsafe_code = "deny"` at `Cargo.toml:20`. The manifest uses `deny` so test
files can `#![allow]` where they must; the library itself forbids.
## Its place in the estate
Inter-repo dependencies resolve by **published crates.io version, not path**, so
a change lands in a sibling only once it is released. Four crates in three repos
depend on `rtb-error`, none of them `optional`, all requiring `version = "0.6.0"`:
`rtb-app` (`rust/app` → `crates/rtb-app/Cargo.toml:20`), `rtb-cli` (`rust/cli` →
`crates/rtb-cli/Cargo.toml:21`), and `rust-tool-base`'s own
`crates/rtb/Cargo.toml:52` and `crates/rtb-cli-bin/Cargo.toml:83`. Keep that
qualifier: in the umbrella `rtb-error` is one of only four `rtb-*` crates taken
unconditionally (`crates/rtb/Cargo.toml:51-54`), while the other nine sit behind
features (lines 55-63). No feature switches the error type off.
That count understates the reach, in a way a `grep` for `rtb-error` will not
show. `src/lib.rs:17` re-exports `miette::{Diagnostic, Report}`, so **miette is
a public dependency here** — a consumer that resolves `miette` to a different
major ends up with two incompatible `Diagnostic` traits and a type error that
blames the wrong thing. Fourteen manifests across `phpboyscout/rust/*` and
`rust-tool-base` pin `miette = "7.6.0"`, matching `Cargo.toml:33`, so a miette
major is a fleet-wide, release-train-shaped event rather than a local one.
On the version gap: `version = "0.6.0"` is a caret requirement, and for a `0.x`
crate `^0.6.0` admits any `0.6.y`, so the published 0.6.3 already satisfies every
consumer's manifest. The lag is in the lockfile — `rust-tool-base`'s `Cargo.lock`
at `origin/main` holds `rtb-error 0.6.1` — and it costs nothing today: between
`rtb-error-v0.6.1` and `rtb-error-v0.6.3` no file under `src/` changed and the
only `Cargo.toml` edit was the version line. Re-check before relying on it.
## What newcomers get wrong
**Run the tests with `cargo nextest run`, not `cargo test`.** The `miette` hook
and this crate's footer slot (`src/hook.rs:21`) are process-global, and T12 and
T13 in `tests/unit.rs` both install a footer. Under plain libtest they share a
process and race: measured here, 33 failures in 600 runs of the `unit` binary, a
few of them a double-panic abort rather than a clean assertion failure. `nextest`
isolates each test in its own process and the race disappears, as `README.md:40`
notes. But `justfile:21` is `cargo nextest run || cargo test`, so on a machine
without nextest `just test` silently takes the flaky path.
**A toolchain bump needs the trybuild fixtures re-blessed.** `rust-toolchain.toml`
pins an exact stable (1.97.1) rather than the floating channel because
`tests/trybuild/non_exhaustive.stderr` matches rustc's diagnostic output
byte-for-byte. On the Renovate toolchain MR, regenerate with `TRYBUILD=overwrite
cargo test --test trybuild` and review the diff (`tests/trybuild.rs:4`).
`.config/nextest.toml:15-17` also reserves every test thread for the trybuild
binary; that is deliberate — the nested `cargo build` flakes under contention.
**The specs are on the rust-tool-base wiki, not in this repo.** `src/lib.rs:12`,
`src/exit_code.rs:10` and `tests/unit.rs:4` point at `docs/development/specs/…`
paths that survived the extraction when the files did not. They are now the wiki
pages [`specs/0006-rtb-error-v0.1`](https://gitlab.com/phpboyscout/rust-tool-base/-/wikis/specs/0006-rtb-error-v0.1)
and [`specs/0034-rtb-error-exit-code-attachment`](https://gitlab.com/phpboyscout/rust-tool-base/-/wikis/specs/0034-rtb-error-exit-code-attachment).
## The quality gate
`just ci` runs `fmt-check`, `lint`, `docs`, `test` and `audit` — read the
`justfile` rather than assuming the recipe names. All five pass on a clean
checkout at `origin/main`. MSRV is `rust-version = "1.82"` (`Cargo.toml:6`),
older than the pinned toolchain, so do not assume a feature is available because
it compiles here. Releases run through **`release-plz`** (`release-plz.toml`),
not the Go side's releaser-pleaser flow: do not import its conventions.
## Which skills apply here
> **The estate has no Rust-specific skills yet**, and no release-plz counterpart
> to `releaser-pleaser-releases`, so this is the language-agnostic set only.
| Starting a feature, a component, or any non-trivial change | `spec-driven-development` |
| Building a feature or fixing a bug test-first | `test-first-discipline` |
| Deciding whether a change is finished | `definition-of-done` |
| Chasing an intermittent test failure | `diagnose-with-a-red-loop` |
| Drawing a conclusion from a sibling checkout or a lockfile | `verify-dont-trust` |
| Writing anything others will read and check | `checkable-claims` |
| Judging a Renovate version jump, including the toolchain | `assess-before-bumping` |
| Working in a repo other than the one you were invoked in | `cross-repo-worktree` |
| Writing or restructuring this crate's documentation | `diataxis-docs` |
| Writing a commit message or a merge request description | `conventional-commits`, `pre-1-0-release-safety` |
| Committing, branching, merging, or opening a merge request | `forge-publish-workflow` |
| Releasing this crate alongside its consumers | `release-train` |
> Skills ship via the [phpboyscout marketplace](https://gitlab.com/phpboyscout/claude-code-plugins).
> An agent without them should treat a named skill as a topic to ask about.
## House rules
`forge-publish-workflow` carries most of these: linear history via rebase and
fast-forward, never squash-merging from the UI, no AI attribution anywhere, and
never at-mentioning anyone. The one worth repeating is that under release-plz the
commit type decides whether a change ships — only `feat` and `fix` cut a release,
so a change that repoints or removes a public interface is `feat`, not
`refactor`, or it lands and never ships. Never cut a release yourself.