1e590e56c0
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>
254 lines
11 KiB
Markdown
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.
|