# 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 one ages better than restating it.
## What this crate is
Two slices in one crate, each behind Cargo features: the `ReleaseProvider` trait
(`src/release.rs:236`) with six backends — GitHub, GitLab, Bitbucket, Gitea,
Codeberg, Direct — and the `Repo` type (`src/git/mod.rs:69`), an async wrapper
over `gix`, behind the default-on `git` feature.
Both are narrower than their names suggest, and that gap is where newcomers come
unstuck. `ReleaseProvider`'s four methods are all reads — no create-release, no
upload, no tag editing, none planned. `Repo` covers eleven operations and stops:
no branch, merge, rebase, stash, tag or remote management. Neither slice
"supports GitLab" or "wraps git" in general; each supports the subset those two
types name. Before asserting the crate can do something, check
[`docs/explanation/04-what-this-does-not-do.md`](docs/explanation/04-what-this-does-not-do.md)
— a complete inventory of limits, tabulating even where the crate's own rustdoc
contradicts its behaviour.
Version 0.7.1, pre-1.0, so the surface still moves, though release-plz runs
`cargo-semver-checks` (`.gitlab-ci.yml:12`) so a break moves the version rather
than slipping out in a patch. Formerly `rtb-vcs`, and **the rename changed the
crate identity and nothing else** — types, modules, features and the internal
`RTB_VCS_GIT_TOKEN` credential-helper variable (`src/git/auth.rs:16`) keep the old
name on purpose. Tidying it is a behaviour change, not a rename.
## `unsafe_code` is `deny` here, not `forbid` — leave it alone
rust-tool-base's workspace manifest requires every `src/lib.rs` to ship
`#![forbid(unsafe_code)]` as "the production guarantee". This crate deliberately
does not, and an agent applying that rule mechanically will break the build.
`linkme`'s `#[distributed_slice]` expands to a `#[link_section]` attribute, which
Rust 1.95 and later attribute to the `unsafe_code` lint. `forbid` cannot be
locally overridden; `deny` can. So `src/lib.rs:49` is `#![deny(unsafe_code)]`,
`Cargo.toml:20` matches it, and the six backend modules plus `src/http.rs` each
carry a module-level `#![allow(unsafe_code)]`. No hand-written `unsafe` block
exists in `src/`; the allowance covers a lint fired by an attribute.
`src/release.rs:304-311` explains why `RELEASE_PROVIDERS` holds
`fn() -> Box<dyn ProviderRegistration>` rather than structs — same workaround, so
do not flatten that either.
Nor is it local to this crate: `rtb-chat`, `rtb-cli`, `rtb-docs`, `rtb-mcp` and
`rtb-update` all ship `deny` for the same reason, with the allow at each `linkme`
site (`rust/cli/crates/rtb-cli/src/telemetry.rs:33`, for one). The manifest
comment is what has drifted — and so has `src/lib.rs:41-45`, which says every
other rtb-* crate keeps `forbid` when five no longer do. Background:
[`docs/explanation/02-link-time-registration.md`](docs/explanation/02-link-time-registration.md).
## The write paths shell out to `git`
`gix` handles `open`, `init`, `walk`, `diff`, `blame`, `status` and anonymous
`clone`. `commit`, `checkout`, `fetch`, `push` and *authenticated* `clone` run the
`git` binary as a subprocess, so a binary using those needs `git` on `PATH` at
runtime — which is why a distroless image fails on the first commit, not at build
time. Do not build behaviour on the split; moving a write path back to pure gix is
meant to be a patch release. Per-operation table in
[`docs/explanation/03-why-write-paths-shell-out.md`](docs/explanation/03-why-write-paths-shell-out.md).
## Keeping the feature matrix coherent when you add a backend
Every backend is gated so a tool with a binary-size budget compiles only what it
uses, and `rtb-update` exercises that for real: it takes this crate with
`default-features = false` and no features at all
(`rust/cli/crates/rtb-update/Cargo.toml:21`), so no backend registers in that
build and the binary supplies them. Gating that drifts breaks a consumer you
cannot see from here. Adding a backend means touching all of:
- **`Cargo.toml:32-67`** — declare the feature, and add it to `default` (`:38`)
if it should be on. A REST backend takes `_http`; one that delegates takes what
it delegates to, as `codeberg = ["gitea"]` does (`:47`), which is why no build
has Codeberg without Gitea.
- **`src/lib.rs`** — a `#[cfg(feature = "…")] pub mod` line, plus that
module-level `#![allow(unsafe_code)]` inside the new module.
- **`src/config.rs`** — the params struct and the `ReleaseSourceConfig` variant,
neither of which is feature-gated. The variant exists even in a build without
its backend, so such a config deserialises fine and then `lookup` returns
`None`. That asymmetry is intentional; do not cfg-gate the enum without a spec.
- **`docs/reference/01-cargo-features.md`**, a table people configure from that
goes stale silently, and a wiremock-backed `tests/<name>_backend.rs`.
A *downstream* tool's own backend needs none of this and must not be added here;
its route is
[`docs/how-to/04-add-a-custom-backend.md`](docs/how-to/04-add-a-custom-backend.md).
## Its place in the decomposition
- **Depends on `rtb-credentials`**, optionally, only under the `git` feature
(`Cargo.toml:54`, `:121`). It releases ahead of this crate, so absorb its bump
rather than tagging past it.
- **Three crates depend on this one**, all resolving by published crates.io
version rather than by path: `rtb-cli-bin` and `rtb-update` require it, and the
`rust-tool-base` umbrella takes it optionally behind a `forge` feature that is
*not* in that crate's default set — though it still reaches a default umbrella
build transitively via `rtb-update`. `rtb-update` lives in the four-crate
`rust/cli` workspace, so a repo-level dependency search misses it.
- **This crate carries its own `docs/`**, the Rust toolkit's pattern, unlike the
Go side where a family's docs live with its core. Specs, though, are **wiki
pages** on rust-tool-base: the git-operations slice is
[spec 0023](https://gitlab.com/phpboyscout/rust-tool-base/-/wikis/specs/0023-v0.5-scope),
and four places here cite a `docs/development/specs/…` path that does not exist.
- Releases run through `release-plz`, not the releaser-pleaser flow the Go side
uses. `just ci` (`fmt-check`, `lint`, `docs`, `test`, `audit`) is the local gate;
it excludes the testcontainers Gitea suite, which is `just test-integration` and
needs Docker.
## Which skills apply here
> **The estate has no Rust-specific skills yet**, so this is the
> language-agnostic set only. Known gap, worth revisiting once it is filled.
| Starting a feature, a component, or any non-trivial change | `spec-driven-development` |
| Working in a repo other than the one you were invoked in | `cross-repo-worktree` |
| Building a feature or fixing a bug against the BDD suite | `test-first-discipline` |
| Deciding whether a change belongs in `Repo` or in the caller | `deep-modules` |
| Writing or restructuring this crate's `docs/` | `diataxis-docs` |
| Writing anything others will read and check | `checkable-claims` |
| Concluding anything from code or a checkout you did not just write | `verify-dont-trust` |
| 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` |
| Judging a Renovate bump, especially a `gix` one | `assess-before-bumping` |
| Releasing several crates in this toolkit together | `release-train` |
> Skills are a Claude Code mechanism, shipped by 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, not load.
## House rules
- Linear history: rebase and fast-forward, never squash-merge from the UI. No AI
attribution in anything published, and never at-mention anyone.
- Only `feat` and `fix` cut a release, and a change that repoints or removes a
public interface is `feat`, not `refactor`, or it lands and never ships. Never
cut a release yourself; that is the maintainer's call, every time.