IMPLEMENTED
PROPOSED 30 Aug 2026 · IMPLEMENTED 30 Aug 2026.
Its message taxonomy (pre-flight refusal / lifecycle / runtime failure) is the live one, and its “what this needs that does not exist” section drove ae050e56, then 1d1eb3a0 and 96de7724. Read that section as a diagnosis of a fixed problem, not outstanding work — and its note that CodebookPanel renders err.detail describes the v1 lens, deleted in 0.29.0.

Codebook messages

Every message the install-and-autotag path can produce, in the two renderings that carry it: the SPA toast and the macOS project-sidebar row. No new glyphs, no new colours, no new surfaces — the five kinds from bristlenose/ui_kinds.py, the shipped .autocode-toast, and the existing sidebar status line and popover.
Artefact — the toast and sidebar renderings are real pixels. Commentary — hatched blocks. Not the product. Budgets — toast 60 chars, cause label 40, pill 28.

Pre-flight — the install is refused, nothing is spent

These fire on the click, before a job exists. Under D4 the researcher performed one act — get and do — so a refusal must explain which half could not happen. Three of the seven have no job to attach to on the Mac, which is a finding rather than a design: see the commentary below.
P1
503 — no API key for the current provider
✗ error
SPA toast
✗
Add an API key in Settings to autotag.
×
38 chars / 60
macOS sidebar row
project-ikea
✗Needs an API key
✗AutoCodeMissing API key
Pre-flight. No job is created, nothing is spent.
P2
503 — provider is a local model (Ollama)
✗ error
SPA toast
✗
Autotagging needs a cloud provider.
×
35 chars / 60
macOS sidebar row
project-ikea
✗Needs a cloud provider
✗AutoCodeLocal model
States the requirement, not the refusal.
P3
409 — several providers, none selected
✗ error
SPA toast
✗
Choose an AI provider in Settings.
×
34 chars / 60
macOS sidebar row
project-ikea
✗Choose a provider
✗AutoCodeNo provider selected
The CLI detail names a shell command; the app must not.
P4
400 — project has no quotes
⚠ warning
SPA toast
⚠
No quotes yet — analyse your sessions first.
×
44 chars / 60
macOS sidebar row
project-ikea
⚠No quotes to tag
⚠AutoCodeNothing to tag
Warning, not error: correct behaviour, wrong order.
P5
409 — a job is already running
ℹ info
SPA toast
ℹ
Already tagging with this codebook.
×
35 chars / 60
macOS sidebar row
project-ikea
no status line — a clean row shows none
no popover row
Info. The running toast is already on screen.
P6
409 — already applied
ℹ info
SPA toast
ℹ
Already applied.
×
16 chars / 60
macOS sidebar row
project-ikea
no status line — a clean row shows none
no popover row
Info. The page already shows the result.
P7
400 — template not found
✗ error
SPA toast
✗
That codebook is no longer available.
×
37 chars / 60
macOS sidebar row
project-ikea
✗Codebook unavailable
✗AutoCodeTemplate missing
Shipping bug, not user error. Still needs a sentence.
Commentary

Nothing here reaches the Mac today, and three of them never can as written. A pre-flight refusal creates no AutoCodeJob, so there is no job for a status line to describe. P5 and P6 are fine — they are answers to a click and the toast is the whole message. But P1, P2, P3 and P7 are configuration problems that outlive the click, and a toast that vanishes in four seconds is the wrong home for a state that will still be true tomorrow. Those belong in the same place a missing API key already belongs, not invented here.

The HTTP details are developer copy and must not be shown. Today CodebookPanel renders err.detail straight into the toast. Those strings name framework ids in quotes and, in one case, tell the researcher to run bristlenose use <provider> — a shell command, inside a Mac app. Every sentence above replaces one of them.

Lifecycle — the job exists

Running is a status, not a kind: ui_kinds.py says so in its own docstring, and the toast already renders it with a spinner and a progress track rather than a glyph.
L1
Job running
status
SPA toast
Tagging with Nielsen…
×
21 chars / 60
macOS sidebar row
project-ikea
Tagging with Nielsen…
no popover row
Status, not a kind — ui_kinds.py says so explicitly.
L2
Job completed
✓ success
SPA toast
✓
Nielsen: 36 tags on 72 quotes.
View Report×
30 chars / 60
macOS sidebar row
project-ikea
✓36 tags on 72 quotes
no popover row
Carries View Report, which opens the same modal as the Review door.
L3
Completed, no matches
ℹ info
SPA toast
ℹ
Nielsen found nothing to tag.
×
29 chars / 60
macOS sidebar row
project-ikea
ℹNothing matched
no popover row
Not a failure. The codebook ran and disagreed with the corpus.
L4
Completed, some batches failed
⚠ warning
SPA toast
⚠
Nielsen tagged 58 of 72 quotes.
×
31 chars / 60
macOS sidebar row
project-ikea
⚠58 of 72 quotes tagged
⚠AutoCodePartial run
The state autocode.py:425 already produces and nothing currently reports.
L5
Cancelled by the researcher
— skipped
SPA toast
—
Tagging cancelled.
×
18 chars / 60
macOS sidebar row
project-ikea
—Cancelled
no popover row
SKIPPED, not ERROR — they chose it.
Commentary

L4 is a state the code already produces and nothing reports. autocode.py:425 logs "Batch failed: %s" and carries on, so a job can complete having tagged a subset. Today that finishes as an unqualified success and the researcher is told a number that is quietly short. It is the clearest case in this table for WARNING: complete, usable, and not what was asked for.

L3 is not a failure and must not look like one. A codebook that finds nothing has run correctly and disagreed with the corpus — which is a finding about the corpus. INFO.

Runtime failure — the job died

Mapped onto the existing classifier: LLMFailureKind in bristlenose/llm/failure_classifier.py already distinguishes all seven, and CauseCategoryEnum already has a case for each. No new category is proposed here.
F1
out_of_credit
✗ error
SPA toast
✗
Your Claude account is out of credit.
×
37 chars / 60
macOS sidebar row
project-ikea
✗Out of credit
✗AutoCodeOut of credit
MUST NOT read "rate limited" — the doc names that exact bug.
F2
rate_limited → quota
⚠ warning
SPA toast
⚠
Rate limited — tagging stopped. Try again soon.
×
47 chars / 60
macOS sidebar row
project-ikea
⚠Rate limited
⚠AutoCodeRate limited
Transient. Warning because retrying is the fix.
F3
invalid_key → auth
✗ error
SPA toast
✗
Your Claude API key was rejected.
×
33 chars / 60
macOS sidebar row
project-ikea
✗Key rejected
✗AutoCodeKey rejected
Terminal until the key changes.
F4
server_error → api_server
⚠ warning
SPA toast
⚠
Claude is unavailable. Try again shortly.
×
41 chars / 60
macOS sidebar row
project-ikea
⚠Provider unavailable
⚠AutoCodeProvider unavailable
Their fault, transient.
F5
bad_request → api_request
✗ error
SPA toast
✗
Claude rejected the request.
×
28 chars / 60
macOS sidebar row
project-ikea
✗Request rejected
✗AutoCodeRequest rejected
Often a retired model id. Detail carries the raw message.
F6
network → network
⚠ warning
SPA toast
⚠
Couldn’t reach Claude. Check your connection.
×
45 chars / 60
macOS sidebar row
project-ikea
⚠No connection
⚠AutoCodeNo connection
F7
unknown
✗ error
SPA toast
✗
Tagging failed.
×
15 chars / 60
macOS sidebar row
project-ikea
✗Tagging failed
✗AutoCodeUnknown error
Deliberately short. The detail is in the popover, not the sentence.
Commentary

F1 against F2 is the one that has bitten before. The popover doc records a bankrupt account being told to wait, because out-of-credit was rendered as rate-limited. Anthropic returns 400 for exhausted billing rather than 402, which is exactly why the classifier exists. These two must never share a string.

The kinds split on whether waiting fixes it. F2, F4 and F6 are WARNING — transient, retry is the remedy. F1, F3, F5 and F7 are ERROR — terminal until the researcher changes something. That is the whole rule, and it is why F2 is not an error despite stopping the run.

Today none of this survives the trip. autocode.py:458 catches bare Exception and stores str(exc), so the job's error_message is raw exception text — unclassified, unbounded, and occasionally a stack-adjacent string. The classifier is imported nowhere in the autocode path. Wiring it is the prerequisite for every row in this table.

What this needs that does not exist

Commentary — findings, not designs

0. The palette can colour one of the five kinds. Measured across bristlenose/theme and frontend/src: --bn-colour-success, --bn-colour-danger and --bn-colour-warning are used sixteen times and defined nowhere. Eleven uses carry a hard-coded fallback and work; five do not, and two of those five are the surfaces on this page — .toast-check and .toast-error (autocode-toast.css:32,37), plus both activity-chip states and the autocode report. An undefined custom property makes color invalid at computed-value time, so it inherits: the tick and the cross on the shipped autocode toast have no colour at all.

--bn-colour-danger is simply a misspelling of --bn-colour-negative, which does exist — light-dark(#dc2626, #ef4444). Worse, the eleven fallbacks pin its light-mode hex, so every one of them is wrong in dark mode. --bn-colour-success and --bn-colour-warning have no counterpart token at all, and the loose greens disagree with each other (#22c55e against #16a34a). A five-kind taxonomy needs three semantic colours and the palette defines one.

1. The SPA toast has two kinds, not five. autocode-toast.css ships .toast-check and .toast-error. Warning, info and skipped have no treatment, and toast() itself takes (message, duration) with no kind at all. Completing that is finishing an existing pattern with the existing vocabulary — not new UX.

2. The autocode path never classifies. Bare except Exception → str(exc). classify_exception() already exists and is not called.

3. Nothing under bristlenose/server/ writes to the events log (Q17), which is what feeds the Mac glyph and popover. Every macOS rendering above is therefore a drawing of a surface that exists, fed by a channel that does not.

4. Pre-flight configuration refusals want a durable home, not a four-second toast. Named, not solved.