diff --git a/.impeccable/live/config.json b/.impeccable/live/config.json new file mode 100644 index 0000000..c1cd308 --- /dev/null +++ b/.impeccable/live/config.json @@ -0,0 +1,6 @@ +{ + "files": ["web/index.html"], + "insertBefore": "", + "commentSyntax": "html", + "cspChecked": false +} diff --git a/CLAUDE.md b/CLAUDE.md index a4b8cd3..e762931 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,6 +42,27 @@ request that adds a heavy dependency to either is wrong. - 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. @@ -110,19 +131,28 @@ alone. The loop: ### Model selection -| Difficulty | Model | -|---|---| -| `difficulty/hard` | Opus 5 | -| `difficulty/moderate` | Sonnet 5 | -| `difficulty/easy` | Sonnet 5 | -| `difficulty/trivial` | Haiku 4.5 | +Sessions are spawned with `aoe`, which can run `claude`, `codex`, `opencode` and +`pi`. The GPT-5.6 models are `terra` and `sol`, reached through the `codex` tool. + +| Difficulty | Model | Spawn | +|---|---|---| +| `difficulty/hard` | Opus 5 | `aoe add --tool claude --model claude-opus-5 -l` | +| `difficulty/moderate` | Sonnet 5 or `gpt-5.6-sol` | `aoe add --tool codex --extra-args "-m gpt-5.6-sol" -l` | +| `difficulty/easy` | `gpt-5.6-terra` | `aoe add --tool codex --extra-args "-m gpt-5.6-terra" -l` | +| `difficulty/trivial` | Haiku 4.5 | `aoe add --tool claude --model claude-haiku-4-5-20251001 -l` | + +Use `-w -b` to put each session in its own git worktree, which is what +makes parallel issues safe. + +`gpt-5.6-terra` is confirmed — it is the default in `~/.codex/config.toml`. +`gpt-5.6-sol` is inferred from the sibling naming and should be verified on +first use. Escalate one tier if a session fails CI twice on the same issue. Never de-escalate mid-issue. -> The operator mentioned a model named "terra" for the lower tiers. No such -> model was identified — confirm what it refers to and correct this table -> before relying on it. +`area/web` issues ignore this table's lower tiers — see the design section +above. They go through the `impeccable` skill regardless of difficulty label. ### What a driver must not do diff --git a/scripts/seed-issues.py b/scripts/seed-issues.py index 904184e..eebdd8f 100644 --- a/scripts/seed-issues.py +++ b/scripts/seed-issues.py @@ -170,6 +170,9 @@ embedded into the binary with `include_dir` and served with correct MIME types. - Dev mode proxies `/api/*` to the running daemon so the SPA has hot reload. - One page that calls `GET /api/health` and renders it, to prove the whole path. +Use the `impeccable` skill for this — see the design section of `CLAUDE.md`. + + Acceptance: `cargo run` serves the built SPA from a single binary with no external files. """, @@ -625,10 +628,40 @@ client compiles. ), # ---------------------------------------------------------------- phase 5 + issue( + "design", "Design system tokens", + "phase/5-ui", "area/web", "difficulty/moderate", + deps=["web"], + body=""" +Author `.impeccable/design.json` — colours, typography, spacing, the token set +every component consumes. **Use the `impeccable` skill for this**; it is what +the file's schema belongs to (`CLAUDE.md`, design section). + +`~/tea/arcada` has a finished `design.json` at `schemaVersion: 2` worth reading +first for the shape. + +Constraints specific to this app: + +- The manual-search view (`DESIGN.md` §9.3) is dense, tabular and chip-heavy. + The palette has to carry three verdict states — eligible, waived, rejected — + legibly at small sizes and against each other, without relying on colour + alone. +- Library status (§4.2) is five states, and they are informational rather than + severity-ranked. Do not borrow an error/warning/success ramp for them. +- It runs on a NAS dashboard, often on a phone, often in a dark room. Dark mode + is not an afterthought. + +`.impeccable/live/config.json` is already scaffolded and points at +`web/index.html`. + +Acceptance: components consume tokens, never literal colour or spacing values, +and CI can assert that. +""", + ), issue( "uisearch", "Unified search box", "phase/5-ui", "area/web", "difficulty/moderate", - deps=["web", "searchapi", "moviesapi"], + deps=["web", "design", "searchapi", "moviesapi"], body=""" `DESIGN.md` §9.2. One box, results grouped: in-library first, TMDB below. Enter on a TMDB result opens the add flow with root and policy pre-filled. @@ -639,13 +672,16 @@ The requirement, stated as a test: there is never a moment where the user has to know whether they are searching or adding. If the UI has an "Add Movie" page separate from search, this issue is not done. +Use the `impeccable` skill for this — see the design section of `CLAUDE.md`. + + Debounce, and cancel in-flight requests on keystroke. """, ), issue( "uimanual", "Manual search buckets and attribute chips", "phase/5-ui", "area/web", "difficulty/moderate", - deps=["web", "searchapi"], + deps=["web", "design", "searchapi"], body=""" `DESIGN.md` §9.3. The fix for Radarr's unusable manual search. @@ -659,12 +695,15 @@ Debounce, and cancel in-flight requests on keystroke. - One click on a `waived` row grabs it and writes the override. The client must not re-implement any policy. Verdicts arrive from the API. + +Use the `impeccable` skill for this — see the design section of `CLAUDE.md`. + """, ), issue( "uilibrary", "Library views with derived status", "phase/5-ui", "area/web", "difficulty/moderate", - deps=["web", "moviesapi"], + deps=["web", "design", "moviesapi"], body=""" `DESIGN.md` §4.2. The default view shows `airing` and `incomplete` only; everything else collapses behind one toggle. @@ -675,12 +714,15 @@ the exact problem this design removed. Show waivers honestly: a file imported under `allow_english_audio` reads as "English, no dub", not as a clean match. + +Use the `impeccable` skill for this — see the design section of `CLAUDE.md`. + """, ), issue( "uiqueues", "Attention queues: no-PT-source and needs-decision", "phase/5-ui", "area/web", "difficulty/easy", - deps=["uilibrary", "failure"], + deps=["uilibrary", "design", "failure"], body=""" `DESIGN.md` §5.2 and §5.7. Two lists of things a human has to look at. @@ -691,6 +733,9 @@ Show waivers honestly: a file imported under `allow_english_audio` reads as These are the only places the app asks for attention, and they are the same events that notify (§9.5). Keep them consistent. + +Use the `impeccable` skill for this — see the design section of `CLAUDE.md`. + """, ), @@ -929,32 +974,42 @@ def main(): by_title = {i["title"]: i["number"] for i in open_issues} numbers = {} + # Pass 1 — ensure every issue exists, so dependency numbers are all known + # before any body is rendered. for spec in ISSUES: title = spec["title"] if title in by_title: numbers[spec["key"]] = by_title[title] - print(f"issue = #{by_title[title]} {title}") continue - - body = spec["body"] - if spec["deps"]: - refs = [] - for d in spec["deps"]: - refs.append(f"#{numbers[d]}" if d in numbers else f"`{d}` (not yet created)") - body += "\n\n---\n\n**Depends on:** " + ", ".join(refs) - body += f"\n\nLabels: `{'`, `'.join(spec['labels'])}`" - print(f"issue + {title}") if DRY: continue made = req("POST", f"/repos/{OWNER}/{REPO}/issues", { "title": title, - "body": body, + "body": spec["body"], "labels": [label_ids[n] for n in spec["labels"] if n in label_ids], }) if made: numbers[spec["key"]] = made["number"] + # Pass 2 — render bodies with real dependency numbers and push them. + # Declarative: this overwrites hand-edits to issue bodies. Edit the spec + # here, not the issue on Gitea. + for spec in ISSUES: + if spec["key"] not in numbers: + continue + body = spec["body"] + if spec["deps"]: + refs = ", ".join(f"#{numbers[d]}" for d in spec["deps"] if d in numbers) + body += f"\n\n---\n\n**Depends on:** {refs}" + body += f"\n\nLabels: `{'`, `'.join(spec['labels'])}`" + if DRY: + continue + req("PATCH", f"/repos/{OWNER}/{REPO}/issues/{numbers[spec['key']]}", { + "body": body, + "labels": [label_ids[n] for n in spec["labels"] if n in label_ids], + }) + print(f"\n{len(numbers)}/{len(ISSUES)} issues present")