Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Build the unified error / result taxonomy — classification tree, canonical error shape, alphabetical error-code registry with stable UPPER_SNAKE codes, wrapping rules across component boundaries, per-surface rendering, and a deprecation policy. Use when asked to create the error taxonomy, register error codes, build the error registry, define the unified error model, or produce an ERRORS.md.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 290% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 687% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 1001% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 623% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 408% | 0% |
Produce an ERRORS.md that serves as the single source of truth for every error code the system can emit, its HTTP status, CLI exit code, UI message key, user action, retry semantics, log level, and owning bounded context. The document fixes a classification tree and per-class policy, pins the canonical error response body, publishes the complete alphabetical registry of stable UPPER_SNAKE error codes, names the wrapping and translation rules across component boundaries, states how each error is rendered on each surface, and commits a deprecation policy for retiring codes. An agent reading this document alone can — for any code the system emits — cite its HTTP status, CLI exit, UI key, user action, retry semantics, and owner, without opening INTERFACES.md or any IA.
ERRORS.md sits between INTERFACES.md (which owns the shape of error responses and per-endpoint error-code listings) and the per-surface IAs (which own user-facing error UX) and resolves them to one taxonomy. It is small, stable, widely cited, and read by IAs (for user-facing message copy), /spec (for the codes a unit may emit), /review (for contract conformance), and /system-verify (when diagnosing failures). The defining discipline — and the commonest violation — is codes are stable: once a code string is assigned, it is never reused, never re-meaning'd, never silently deleted.
EP-name boundaries. Every error code mentioned in an endpoint response in INTERFACES.md must have a registry row here.WEB_IA.md, CLI_IA.md, MOBILE_IA.md, TUI_IA.md, VOICE_IA.md. Only the IAs that were produced for this project are read; surfaces the project does not build have no IA and no column in § 5. Each IA's error-surface section names the ui_message_key or template it expects.owner_context value on every row; § 9 Invariants Index supplies the INV-NN references that certain codes signal (a uniqueness invariant violation surfaces as a specific code, named here).throw, raise, panic, return Err(...), respondError(...) sites and collect codes already emitted that are not yet documented. Surface undocumented codes as rows or as open questions if the mapping is ambiguous.deprecated_since / removed_in columns populated.Read set size: 3 required (INTERFACES, DOMAIN, each surface IA in scope) + optional source scan + optional prior ERRORS. Read all IAs and INTERFACES.md end-to-end before drafting. Omitted reading causes two specific failure modes: codes cited in INTERFACES or an IA without a registry row (contract drift) and codes emitted by code that never reach the registry (silent divergence).
Error taxonomy construction proceeds in seven phases: harvest, classification, canonical shape, registry, wrapping, per-surface mapping, and validation. Phases are sequential. Revisit earlier phases if a later one reveals a missing code or a contradiction.
Collect every error code candidate from four sources:
errors: field of an endpoint. These are the externally-visible codes; every one must land in § 3.ui_message_key an IA expects to render; the corresponding code must exist here.INV-NN that is enforced at runtime typically signals through a specific code when violated. Name one code per enforced invariant, citing the INV-NN in the row's references column.throw, raise, panic, Err(...), error-response helpers. Normalise to UPPER_SNAKE. Codes found only in code with no INTERFACES / IA reference are either internal-only (log-level error or critical; never reach users) or missing from INTERFACES — surface as an open question if the boundary is unclear.Deduplicate by meaning, not by spelling. NOT_FOUND and MISSING and UNKNOWN_RESOURCE are the same code in three forms; pick one canonical UPPER_SNAKE string. Record the rejected forms so the registry does not accumulate synonyms.
Partition the harvested codes into three top-level classes and their conventional HTTP families:
auth, authz, validation, not-found, conflict, rate-limit, idempotency.unhandled bug, dependency failure, overload, degraded.malformed request, unsupported version, unsupported media type.Write § 1 as a short bulleted tree plus a policy table. The policy table has exactly these columns, one row per sub-class: Class, Retryable?, Idempotent retry safe?, User-actionable?, Log level, Paged?. Every cell is a single-word answer (yes / no / warn / error / critical); no prose.
Class totals land in frontmatter: client_error_codes:, server_error_codes:, protocol_error_codes:. They sum to error_codes:.
State the JSON body the system emits on HTTP errors. If INTERFACES.md pins the shape, cite it verbatim and do not re-design. If INTERFACES.md delegates to ERRORS.md for the shape, define it here with these fields: code (stable UPPER_SNAKE, required), message (human-readable developer-facing English summary, stable across versions, required), details (optional structured payload — schema per code), request_id (correlation id, required), docs_url (optional).
Follow the shape block with a one-paragraph statement of how non-HTTP surfaces use the same taxonomy: CLI surfaces emit message to stderr and set cli_exit; UI surfaces look up code and render the template named by ui_message_key (allowing i18n without touching the API); log sinks record code, request_id, and the full structured context per log_level. The shape lives here; per-endpoint application of the shape lives in INTERFACES.md.
§ 3 is the master registry — one row per code, sorted alphabetically by code. Every row populates every column:
| Column | Meaning | Allowed values | |---|---|---| | code | Stable UPPER_SNAKE string (REPO_NOT_FOUND, PUSH_CONFLICT, AUTH_REQUIRED). Never reused. | [A-Z][A-Z0-9_]+ | | http_status | HTTP status emitted at the INTERFACES boundary | 3-digit HTTP code | | cli_exit | Process exit code for CLI surface. 0 is reserved for success; pick integers per CLI_IA's exit-code taxonomy | integer ≥ 1 or — if not emitted by CLI | | ui_message_key | Key the UI maps to an i18n message template (or the literal template if no i18n) | short dotted string error.repo.not_found | | user_action | One-line instruction to the user in the affected surface's voice ("Verify the repo name and try again.") | full sentence ending in . | | retryable | Retry safety | yes / yes-with-backoff / no | | log_level | Log level at emission | warn / error / critical | | owner_context | Bounded context that owns this error (from DOMAIN.md § 2) | context name | | references | INV-NN invariants this code signals, THREAT-NN if security-related, EP-name if emitted by a specific endpoint pattern | comma-separated IDs or (none) |
Write the table once; sort alphabetically by code for stability across regenerations. The row-count lands in frontmatter error_codes:. Retryable count lands in retryable_codes: (rows where retryable is yes or yes-with-backoff).
Keep message and details out of the registry — they are emitted at runtime from the shape in § 2, not enumerated here. The registry is a contract, not a message catalogue.
When an error crosses a component boundary (Worker → API, API → Client, Plugin → Host, external system → ACL), specify what happens. § 4 is a boundary × rule table with these columns: Boundary, Add fields?, Translate codes?, Redact details?, Rationale.
Typical boundary rules to document:
request_id; never translate codes; in production redact details for codes with log level critical.GIT_CORRUPT, CACHE_THRASH) to the nearest external code (INTERNAL / DEPENDENCY_FAILURE); never expose internal codes to clients.details.upstream_code when useful for support.One row per boundary. If a boundary has no wrapping rules (rare), write the row with Add fields? = no, Translate codes? = no, Redact details? = no, Rationale = "transparent passthrough; boundary preserves code semantics".
For each surface that exists in this project (one column per IA that was produced), state how codes are rendered. § 5 has one subsection per surface, in this fixed form:
http_status from the registry is the response status; code verbatim.message written to stderr; process exits with cli_exit from the registry; user_action is suffixed to stderr when surface is interactive (isatty(stderr)); — registry rows never trigger a CLI path.ui_message_key from the registry and renders the IA's template for that key (toast / inline / modal per WEB_IA); details may be rendered for log_level: warn codes in developer mode.ui_message_key; rendering per MOBILE_IA (dialog / banner / toast). Offline codes render via cached templates.ui_message_key; rendering per TUI_IA (status line / modal / error panel).ui_message_key; TTS template per VOICE_IA; user_action is spoken.Surfaces the project does not build are omitted — not listed with "(not in scope)". The scope is named in § 5's opening sentence: "Surfaces in scope: WEB, CLI." Omit the surfaces not in scope.
§ 6 Deprecation Policy. State: how a code is retired (add deprecated_since: YYYY-MM-DD and removed_in: vX.Y columns to the row; do not delete the row), the minimum deprecation window (e.g., "one major version or 6 months, whichever is longer"), what consumers must do to migrate (typically: update switch statements on code, update i18n keys, update retry policy if the new code's retryable differs). Deprecated codes continue to appear in the registry — the row persists forever with its deprecated_since populated — so that consumers reading the registry after retirement still find the history.
§ 7 Relationship to Other Artifacts. One bullet per relationship, in this fixed order:
code string in endpoint response listings; every such citation must match a registry row in § 3.ui_message_key; every key appearing in an IA must map to a row in § 3./spec reads this file to draw from the registry, not to invent new codes.THREAT-NN entries to codes where the error is a security signal; the registry's references column records the linkage.log_level and retryable semantics land there verbatim.§ 8 Validation. Before finalising verify:
ui_message_key cited in every IA appears in § 3.INV-NN.n/a, no tbd.code.owner_context value matches a bounded context name in DOMAIN.md § 2 exactly.client_error_codes + server_error_codes + protocol_error_codes == error_codes.Update frontmatter counts. status is complete if § 8 Open Questions is "All questions resolved." and has_open_questions otherwise.
These rules govern the output document. Violations are detected by the quality checklist.
Once a code string is assigned, it belongs to that meaning forever. Renaming a code is forbidden. Re-meaning a code (keeping the string, changing what it signals) is forbidden. Retiring a code removes it from emission but leaves the row in the registry with deprecated_since / removed_in populated so downstream consumers reading the registry after retirement find its history.
Every code matches [A-Z][A-Z0-9_]+. No dots, no dashes, no spaces, no lowercase. Dotted keys exist — they are ui_message_keys — but they are a separate column and a separate namespace.
One HTTP 404 can represent ten distinct codes (REPO_NOT_FOUND, USER_NOT_FOUND, BRANCH_NOT_FOUND, OBJECT_NOT_FOUND, ...). The http_status column is a mapping, not an identity. A row that has only an HTTP status and no code string is not a row — it is a leak from INTERFACES.md. Every row has a code.
The columns retryable, log_level, http_status, cli_exit, and owner_context carry one value each, from the defined enum or integer set. No prose, no qualifications, no footnotes. Nuance lives in the user_action column (a sentence) or in the wrapping / surface-mapping sections — never in the registry.
Even internal-only errors have a user-facing action: "Contact support and quote request_id." The user_action column is never empty, never n/a, never "(internal only)". If the code is genuinely never rendered to any user on any surface, it does not belong in the registry — it is an internal log-only signal that INTERFACES.md does not need to cite.
owner_context names the bounded context from DOMAIN.md § 2 that owns the error's meaning. Orphan codes — codes with no owning context — reveal unmodeled domain concepts; surface them as open questions rather than assigning a stub context name.
A code string appearing in INTERFACES.md (endpoint responses), an IA (user-facing copy), a SPEC (unit emissions), or SECURITY.md (threat responses) must have a registry row in § 3. This is the contract the document enforces. The validation phase samples citations and confirms each maps to a row.
§ 3 is sorted alphabetically by code. The sort is stable across regenerations — an agent regenerating the document must produce the same row order an earlier agent produced, modulo the new rows added at their alphabetical position. This reduces diff noise and makes review tractable.
Deleted rows break consumers that still recognise the string. Retire codes by adding deprecated_since: YYYY-MM-DD and removed_in: vX.Y columns to the row and leaving it in place. Only after removed_in has passed in the real world does the row move to an archival section at the bottom of § 3 — never silently deleted.
retryable is one of three values. yes means a mechanical retry is safe without operator intervention; yes-with-backoff means a retry is safe if and only if the client observes exponential backoff (per /operations' retry budget); no means any retry is a bug. Surfaces and clients read this value and wire retry policy from it — ambiguity here causes real outages.
The policy table in § 1 sets the default log level and pager gating per class. Individual codes inherit the class default unless their row overrides it with a documented reason in user_action or in an inline note below the registry. Mass-override across a class is a sign the class boundary is wrong — revisit § 1.
The canonical shape in § 2 is referenced or pinned here, but field-by-field wire design — versioning, content negotiation, media type, status-code conventions at large — belongs to INTERFACES.md. An ERRORS.md that starts enumerating HTTP status conventions for the whole API is out of scope; delete that content.
ui_message_key is the handle; the template it resolves to belongs to the IA. A registry that starts carrying long marketing-voice error copy is over-reaching — user_action stays to a single sentence, and ui_message_key stays to a dotted identifier. Layout (toast / inline / modal) is the IA's concern.
One YAML frontmatter block at the top containing common fields (skill, date, status) and errors-specific fields (error_codes, retryable_codes, client_error_codes, server_error_codes, protocol_error_codes, open_questions). Never emit a second YAML block. Counts match the body exactly; client + server + protocol == error_codes.
No "appropriate", "relevant", "as needed", "etc.", "various", "and so on", "many", "some", "a few". Use exact code strings, exact HTTP statuses, exact context names, exact INV-NN / THREAT-NN IDs. Unresolvable ambiguity surfaces in § 8 Open Questions with options, tradeoffs, and a recommendation.
markdown--- skill: ERRORS.md date: {YYYY-MM-DD} status: {complete | has_open_questions | blocked} error_codes: {N} retryable_codes: {N} client_error_codes: {N} server_error_codes: {N} protocol_error_codes: {N} open_questions: {N} --- # ERRORS — {ProductName} > Unified error / result taxonomy. Every error the system emits is registered > here with HTTP status, CLI exit, UI message key, user action, retry > semantics, log level, and owning bounded context. Downstream artifacts cite > codes by `code` string. Wire shape of error bodies lives in INTERFACES.md; > UI rendering lives in the per-surface IAs; alert thresholds live in > QUALITY.md; threat-to-code mapping lives in SECURITY.md. ## § 1. Classification Tree - **Client errors (4xx family)** — the request was invalid or unauthorised. - `auth` — identity not established - `authz` — identity established, authority insufficient - `validation` — request syntactically or semantically invalid - `not-found` — addressed resource does not exist - `conflict` — request violates current server state - `rate-limit` — client exceeded quota - `idempotency` — replayed request with divergent payload - **Server errors (5xx family)** — the server or a dependency failed. - `unhandled bug` — unexpected exception crossed a boundary - `dependency failure` — a downstream system failed - `overload` — resource budget exhausted - `degraded` — a subsystem is operating in reduced-capability mode - **Protocol errors** — the request could not be parsed or versioned. - `malformed request` — request bytes did not parse - `unsupported version` — API version negotiation failed - `unsupported media type` — content-type not accepted ### Policy per class | Class | Retryable? | Idempotent retry safe? | User-actionable? | Log level | Paged? | |-------|------------|------------------------|------------------|-----------|--------| | `auth` | no | — | yes | warn | no | | `authz` | no | — | yes | warn | no | | `validation` | no | — | yes | warn | no | | `not-found` | no | — | yes | warn | no | | `conflict` | no | — | yes | warn | no | | `rate-limit` | yes-with-backoff | yes | yes | warn | no | | `idempotency` | no | — | yes | warn | no | | `unhandled bug` | no | — | no | error | yes | | `dependency failure` | yes-with-backoff | yes | no | error | yes | | `overload` | yes-with-backoff | yes | no | error | yes | | `degraded` | no | — | no | warn | yes | | `malformed request` | no | — | yes | warn | no | | `unsupported version` | no | — | yes | warn | no | | `unsupported media type` | no | — | yes | warn | no | (Repeat/adjust the policy table if classes differ for this project. Every cell is a single-word answer.) --- ## § 2. Canonical Error Shape HTTP error responses carry the following JSON body (pinned here; INTERFACES.md cites this section verbatim).
{ "code": "string — stable UPPER_SNAKE code from § 3. Required.", "message": "string — human-readable developer-facing English summary. Stable across versions. Required.", "details": "object — optional structured payload; schema per code.", "request_id": "string — correlation id. Required.", "docs_url": "string — optional link to docs." }
Non-HTTP surfaces consume the same taxonomy without the wire envelope:
- **CLI** renders `message` to stderr and sets the process exit to `cli_exit`
from § 3.
- **UI surfaces** (WEB / MOBILE / TUI) look up `code` and render the IA
template identified by `ui_message_key` from § 3. Layout (toast / inline /
modal / banner) is the IA's concern; the key lands here.
- **VOICE** renders the TTS template identified by `ui_message_key` via
VOICE_IA's prompt catalogue.
- **Log sinks** record `code`, `request_id`, `owner_context`, and `details`
at the `log_level` from § 3.
---
## § 3. Error Code Registry
Surfaces in scope: {WEB | CLI | MOBILE | TUI | VOICE — list only those that
exist in this project.}
| `code` | `http_status` | `cli_exit` | `ui_message_key` | `user_action` | `retryable` | `log_level` | `owner_context` | `references` |
|--------|--------------|-----------|------------------|---------------|-------------|-------------|-----------------|--------------|
| `{CODE_STRING}` | `{3-digit}` | `{int or —}` | `{dotted.key}` | "{one sentence.}" | `{yes | yes-with-backoff | no}` | `{warn | error | critical}` | `{ContextName}` | `{INV-NN, THREAT-NN, EP-name or (none)}` |
(Alphabetical by `code`. One row per code. No blanks, no `n/a`. Deprecated
rows retain their entry with two additional inline columns:
`deprecated_since` and `removed_in`.)
---
## § 4. Error Wrapping Rules
| Boundary | Add fields? | Translate codes? | Redact details? | Rationale |
|----------|-------------|------------------|-----------------|-----------|
| `API ingress → client` | `request_id`, `docs_url` | `no` | yes for `log_level: critical` in production | `request_id` enables support; internal details never leave the edge |
| `Internal component → API` | `no` | `yes` — internal-only codes collapse to `INTERNAL` / `DEPENDENCY_FAILURE` | yes | internal vocabulary never leaks to clients |
| `External system → ACL` | `details.upstream_code`, `details.upstream_message` | `yes` — foreign codes map to taxonomy entries | no | preserves debugging trail without exposing foreign dialect |
(Repeat for every component boundary the system crosses. If a boundary is
transparent, still include a row stating so — silence is ambiguous.)
---
## § 5. Per-Surface Mapping
### API
- Response body per § 2.
- `http_status` from § 3.
- `code` verbatim.
- `details` redacted per § 4 rules.
### CLI
- `message` to stderr.
- Process exit = `cli_exit` from § 3.
- `user_action` appended to stderr when `isatty(stderr)`.
- Registry rows with `cli_exit: —` never reach CLI surfaces.
### Web UI
- Template picked by `ui_message_key` from § 3; layout per WEB_IA.
- `details` rendered only in developer mode for `log_level: warn` codes.
### Mobile
- Template picked by `ui_message_key`; layout per MOBILE_IA.
- Offline codes render via cached templates; `user_action` is the primary affordance.
### TUI
- Template picked by `ui_message_key`; placement per TUI_IA (status line, modal, error panel).
### Voice
- TTS template per VOICE_IA prompt catalogue, keyed by `ui_message_key`.
- `user_action` is spoken; `request_id` is spelled on request.
(Omit subsections for surfaces the project does not build.)
---
## § 6. Deprecation Policy
- **How to retire a code:** add `deprecated_since: YYYY-MM-DD` and
`removed_in: vX.Y` columns to the registry row. Do not delete the row.
- **Minimum deprecation window:** {one major version or 6 months, whichever
is longer — adjust per project release cadence}.
- **Consumer migration:** consumers must update switch statements keyed on
`code`, update i18n keys to the replacement `ui_message_key`, and adjust
retry policy if the replacement code's `retryable` differs from the retired
code.
- **Post-removal:** after `removed_in` ships, the row moves to the archival
section at the bottom of § 3 and is never re-emitted.
---
## § 7. Relationship to Other Artifacts
- **INTERFACES.md** cites codes by `code` string in per-endpoint response
listings; every such citation maps to a row in § 3.
- **IAs** (`WEB_IA.md`, `CLI_IA.md`, `MOBILE_IA.md`, `TUI_IA.md`,
`VOICE_IA.md`) cite codes via `ui_message_key`; every key maps to a row
in § 3.
- **SPECs** list which codes the unit may emit; `/spec` draws from the
registry and must not invent new codes.
- **SECURITY.md** maps `THREAT-NN` entries to codes where the error is a
security signal; linkage lives in the `references` column of § 3.
- **QUALITY.md** alert catalogue may reference codes directly; `log_level`
and `retryable` semantics land there verbatim.
- **BEHAVIOR.md** may cite codes on state-machine edges where a transition
is blocked by a runtime check; the code names the reason domain-side.
- **DOMAIN.md** supplies `owner_context` for every row; every `INV-NN`
referenced in `references` lives in DOMAIN § 9.
---
## § 8. Open Questions
- [ ] {Question — e.g., "Should `USER_SUSPENDED` return HTTP 403 (forbidden)
or 409 (conflict)? 403 matches the auth/authz family policy; 409
signals state-driven refusal. The choice affects client retry logic:
under 403 the CLI advises the user to contact an admin, under 409 it
advises retrying after remediation."}
- **Option A:** 403 — aligns with `authz` class policy; consistent with
`FORBIDDEN` semantics; user-action is "contact your administrator".
- **Option B:** 409 — signals that the *account* is in a state that
refuses the request; differentiates from permission-denied errors.
- **Recommendation:** 403, with `details.suspension_reason` carrying the
nuance; keeps retry semantics aligned with the class policy.
(If none: "All questions resolved.")/interfaces/interfacesuser_action, not the template body/quality (this document supplies log_level and a paged/not-paged flag per class only)/security (this document records the linkage via the references column)/operations/domain/spec and /implement/use-casesskill, date, status, error_codes, retryable_codes, client_error_codes, server_error_codes, protocol_error_codes, open_questions)code, message, request_id) and every optional field (details, docs_url); states how non-HTTP surfaces consume the taxonomyn/a, no tbdcode matches [A-Z][A-Z0-9_]+ (UPPER_SNAKE); no dots, no dashes, no spaces, no lowercasecode string is unique; no duplicate strings; no reused retired stringscoderetryable is one of yes / yes-with-backoff / nolog_level is one of warn / error / criticalowner_context matches a bounded context name in DOMAIN.md § 2 exactlyuser_action is a single sentence ending in .; never empty, never n/areferences either lists INV-NN / THREAT-NN / EP-name IDs or states (none)ui_message_key cited in every surface IA in scope appears as a row in § 3deprecated_since and removed_in populated; deleted rows do not existclient_error_codes + server_error_codes + protocol_error_codes == error_codesretryable_codes equals the count of rows where retryable is yes or yes-with-backoffstatus is complete if § 8 is "All questions resolved." and has_open_questions otherwiseOther measured skills in the registry, with their headline benchmark lift.