docs: require impeccable skill for design and frontend

Adds the design-system issue, scaffolds .impeccable/live/config.json against
web/index.html, and records the aoe spawn commands for the codex GPT-5.6
models in the driver table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude
2026-08-22 18:48:44 +01:00
parent 3804e62eea
commit af33b38c1b
3 changed files with 115 additions and 24 deletions
+6
View File
@@ -0,0 +1,6 @@
{
"files": ["web/index.html"],
"insertBefore": "</body>",
"commentSyntax": "html",
"cspChecked": false
}
+39 -9
View File
@@ -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 <path> --tool claude --model claude-opus-5 -l` |
| `difficulty/moderate` | Sonnet 5 or `gpt-5.6-sol` | `aoe add <path> --tool codex --extra-args "-m gpt-5.6-sol" -l` |
| `difficulty/easy` | `gpt-5.6-terra` | `aoe add <path> --tool codex --extra-args "-m gpt-5.6-terra" -l` |
| `difficulty/trivial` | Haiku 4.5 | `aoe add <path> --tool claude --model claude-haiku-4-5-20251001 -l` |
Use `-w <branch> -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
+70 -15
View File
@@ -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")