Files
arr/CLAUDE.md
T
Claude 1e590e56c0 docs: add driver prompt and merge policy
Relaxes the concurrency rule to per-crate edits, records that the PR daemon
spawns land and review-pr sessions automatically, and makes the driver the
only thing that merges — skipping a stalled reviewer rather than blocking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 19:09:47 +01:00

254 lines
11 KiB
Markdown

# arr
Single-service replacement for Radarr and Sonarr, later Bazarr. Rust workspace,
API-first, SQLite, no authentication layer.
**Read `DESIGN.md` before touching anything.** It is the contract. Issues
reference its sections rather than restating them. If an issue and the design
document disagree, the design document wins and the issue is wrong — say so
rather than implementing the discrepancy.
## Repo layout
```
crates/
├── arr-core/ domain types, policy engine, scoring (no IO, no heavy deps)
├── arr-parse/ release name parsing (no IO)
├── arr-meta/ TMDB client
├── arr-indexer/ Torznab via Prowlarr
├── arr-dl/ Transmission RPC
├── arr-probe/ ffprobe wrapper
├── arr-db/ sqlx + migrations
├── arr-api/ axum + OpenAPI
├── arr-compat/ Radarr/Sonarr v3 shim for Jellyseerr
├── arr-daemon/ reconcile loop, wires everything
└── arr-e2e/ cross-process integration tests
web/ Vite + TypeScript SPA, embedded via include_dir
```
**The invariant that matters:** `arr-core` and `arr-parse` must never depend on
axum, sqlx or reqwest. They hold the logic that gets tested constantly and they
must stay fast to compile. Everything expensive is downstream of them. A pull
request that adds a heavy dependency to either is wrong.
## Conventions
- Dependency versions are pinned once in the root `[workspace.dependencies]`.
Member crates reference them by name, never by version.
- Lints are declared once in the root `[workspace.lints]`. Member crates carry
only `[lints] workspace = true`.
- Commits follow Conventional Commits, subject ≤ 50 characters, body only when
the "why" is not obvious from the diff.
- No `unwrap()` in non-test code. The lint is on.
- SQL goes through `sqlx` compile-time-checked queries.
## Design and frontend work
**Any issue labelled `area/web`, and any work that decides how something looks
or behaves on screen, goes through the `impeccable` skill.** That includes the
design system itself, component work, layout, visual hierarchy, empty and error
states, and any change to the UI described in `DESIGN.md` §9.2 and §9.3. Invoke
it before writing markup, not as a review pass afterwards.
`.impeccable/design.json` is the token system — colours, typography, spacing.
It is authored once by that skill and then treated as the source of truth;
components consume tokens rather than literal values. `.impeccable/live/config.json`
points the skill's live browser iteration at `web/index.html`.
`~/tea/arcada` has the same setup and is worth reading for the shape of a
finished `design.json`.
This matters more here than in a typical CRUD app because the manual-search
view (§9.3) is the whole reason for the project's UI existing — Radarr's is
unusable specifically because of a layout decision. Getting it right is a design
problem, not a markup problem.
## Working an issue
1. Read `DESIGN.md`, then the issue, then the sections the issue cites.
2. Branch from `main`. One issue per branch.
3. Implement the issue as scoped. Do not widen it. If you find adjacent work,
open a new issue and keep going.
4. `just ci` must pass locally before pushing. That runs the same gate as CI.
5. Open a pull request referencing the issue number.
If an issue turns out to be blocked by something not yet built, say so on the
issue and stop. Do not implement the dependency inline — it has its own issue,
or it needs one.
## Label taxonomy
Every issue carries exactly one `phase/`, one `area/`, one `difficulty/`, and
one `type/`. Dependencies are expressed in the issue body as `Depends on: #N`.
### `phase/` — build order, from `DESIGN.md` §13
| Label | Meaning |
|---|---|
| `phase/1-skeleton` | workspace, CI, config, database, empty API and SPA |
| `phase/2-logic` | parsing and the policy engine, pure, no network |
| `phase/3-sourcing` | TMDB and Prowlarr, read-only, grabs nothing |
| `phase/4-movies` | movies end to end, first real cutover test |
| `phase/5-ui` | search, buckets, library views, queues |
| `phase/6-tv` | seasons, episodes, tracking, derived status |
| `phase/7-people` | owner tags and notifications |
| `phase/8-compat` | Jellyseerr shim |
Phases are ordered but not strictly serial — issues within a phase often
parallelise, and some later-phase issues unblock early. `Depends on:` is
authoritative, the phase label is a hint.
### `area/` — which crate
`area/core`, `area/parse`, `area/meta`, `area/indexer`, `area/dl`,
`area/probe`, `area/db`, `area/api`, `area/compat`, `area/daemon`, `area/web`,
`area/ci`, `area/infra`
### `difficulty/` — drives model selection
| Label | Shape of the work |
|---|---|
| `difficulty/trivial` | one file, mechanical, no design decisions |
| `difficulty/easy` | bounded, obvious approach, few files |
| `difficulty/moderate` | multiple files, some judgement, a real interface to design |
| `difficulty/hard` | subtle correctness, cross-cutting, or the design document itself is thin here |
### `type/`
`type/feature`, `type/chore`, `type/test`, `type/bug`
## Driving this autonomously
A driver session should be able to work from `DESIGN.md` plus the issue list
alone. The loop:
1. List open issues. Drop any whose `Depends on:` references an open issue.
2. Of what remains, take the lowest phase number first, then whatever
parallelises within it.
3. Spawn one session per issue, selecting the model from `difficulty/`.
4. Merge on green and reviewed, then recompute the ready set.
### What happens after a pull request opens
The **PR daemon** (`pr-daemon.service`, org-wide over `yolo/*`) picks up every
pull request automatically. It spawns two sessions per PR:
- `<n>-<slug>` running the `land` skill — the author side, fixing CI failures
and addressing review comments.
- `rev-<n>-<slug>` running `review-pr` — the reviewer side, posting findings to
Gitea.
The driver does **not** babysit either of them, and does not re-implement what
they do. It waits.
**Neither of them merges.** That is the driver's job, and the operator does not
review personally.
Merge when CI is green and the review session has finished. If the review
session never spawned, or is stuck — no progress for ~20 minutes, or the
session is gone — **skip waiting and merge on CI green alone**. A stalled
reviewer must not block the queue.
Squash merge, delete the branch, close the issue.
### Model selection
Sessions are spawned with `aoe`. Each difficulty tier has **two models**, and
issues at that tier are distributed between them — not run twice. Splitting
across two providers keeps a single provider's rate limits or an outage from
stalling the whole queue, and keeps one model's blind spots from shaping the
entire codebase.
| Difficulty | Models | `--tool` | `--extra-args` |
|---|---|---|---|
| `difficulty/hard` | Fable 5 | `claude` | `--model claude-fable-5` |
| | Sol | `codex` | `-m gpt-5.6-sol` |
| `difficulty/moderate` | Opus 5 | `claude` | `--model claude-opus-5` |
| | Sol, low effort | `codex` | `-m gpt-5.6-sol -c model_reasoning_effort="low"` |
| `difficulty/easy` | Sonnet 5 | `claude` | `--model claude-sonnet-5` |
| | Terra | `codex` | `-m gpt-5.6-terra` |
| `difficulty/trivial` | Terra | `codex` | `-m gpt-5.6-terra` |
Alternate within a tier rather than draining one model first, so a bad run is
visible early instead of after ten issues.
### Launching a session
Verified working against this repo:
```sh
aoe add /home/naps62/tea/arr \
--title "arr-42-torznab" \
--worktree issue/42-torznab --new-branch \
--tool codex --extra-args "-m gpt-5.6-sol" \
--launch
```
Things that are easy to get wrong:
- **The worktree path comes from `--title`, not from the branch.** That command
creates `/home/naps62/tea/arr/worktrees/arr-42-torznab` on branch
`issue/42-torznab`. Give every session a distinct title or the second one
collides.
- `--new-branch` is required alongside `--worktree` for a branch that does not
exist yet. Without it, `aoe` expects the branch to be there already.
- The base branch defaults to the repo default (`main`). Pass `--base-branch`
only when stacking on an in-flight branch.
- YOLO mode came out enabled without passing `-y`, from profile config. Check
that is what you want before an unattended run.
- `aoe remove <title>` keeps the worktree. Use `--delete-worktree`, or clean up
with `git worktree remove --force` — and note `aoe` leaves the worktree
*locked*, so `git worktree unlock` comes first.
### Model IDs, verified
All five were checked against the live CLIs on 2026-08-22, with a deliberately
bogus ID as a control to prove the check discriminates:
| ID | Tool | Result |
|---|---|---|
| `claude-fable-5` | claude | accepted |
| `claude-opus-5` | claude | accepted |
| `claude-sonnet-5` | claude | accepted |
| `gpt-5.6-sol` | codex | accepted |
| `gpt-5.6-terra` | codex | accepted, and the default in `~/.codex/config.toml` |
`-c model_reasoning_effort="low"` was exercised on a real `codex exec` run and
accepted. The bogus control (`gpt-5.6-nonesuch`) failed with HTTP 400, and
`claude-bogus-9` was rejected by the CLI.
**`area/web` issues run on Fable 5 regardless of their difficulty label**, since
they go through the `impeccable` skill and the UI is the reason this project
has a frontend at all (see the design section above).
Escalate one tier if a session fails CI twice on the same issue — and escalate
to the *other* model at that tier first, before going up. Never de-escalate
mid-issue.
### What a driver must not do
- Do not run two sessions **editing the same crate** at the same time. Each
session gets its own worktree, so the risk is merge conflicts rather than
build contention. In practice this means most of `phase/2-logic` is a chain,
since `policy`, `lang`, `hdr` and `score` all live in `arr-core`. Issues in
different crates parallelise freely.
- Do not start `phase/4-movies` work until `phase/2-logic` is complete. The
policy engine is the thing everything else calls.
- Do not merge a pull request that skips tests to get CI green. Failing tests
are the signal the design document is wrong somewhere; surface it.
## Environment
- **Prowlarr** — `prowlarr` container on the Dokploy host, port 9696. Owns
tracker auth, FlareSolverr and the Cardigann definitions. Not replaced.
- **Transmission** — native in LXC 130 at `10.6.10.45:9091`, RPC
unauthenticated. Download dir `/mnt/media/transmission/complete`.
- **Jellyfin** — native in LXC at `10.6.10.18:8096`. Library roots under
`/mnt/media-v2`.
- **Media** — ZFS, single dataset, bind-mounted as `/mnt/media`. Downloads and
library share it, so hardlinks work.
- **ntfy** — running on the Dokploy host, one topic per person.
Development happens on the same machine that runs the stack, so all of the
above are directly reachable. Never point tests at a live tracker.