<!-- source: openspec/current/status-resolver.md -->

---
title: Status resolver — six visual roles
status: stable
owner: pavel
audience: [pavel, architect, executor]
tldr: One pill, one resolver, one taxonomy. Every studio status string resolves to one of six visual roles — live, progressing, idle, archived, error, embargo. Ratifies D-ATLAS-2/3.
canonical_at: openspec/current/status-resolver.md
related:
  - openspec/current/entity-densities.md
  - openspec/brands/studio/design-tokens.md
  - openspec/brands/studio/design-components.md
  - openspec/_atlas/current/atlas.html
ratifies: D-ATLAS-2, D-ATLAS-3
---

# Status resolver — six visual roles

> One pill, one resolver, one taxonomy. Every status string the studio emits
> resolves to exactly one of six visual roles. The pill renders by role; the
> raw value is preserved as honest metadata.

## The six visual roles

| Role | Reads as | Token | Examples |
|---|---|---|---|
| `live` | Currently producing / visible / advancing automatically | `--color-accent` (terracotta) | Project ACTIVE; Forge run RUNNING; Open call OPEN |
| `progressing` | Advancing toward live | `--color-blueprint` | Forge run QUEUED; Inbox REVIEWING; File ACCEPTED |
| `idle` | Waiting on human input | `--color-ink-muted` | Inbox NEW; Idea DRAFT; Forge run AWAITING |
| `archived` | Past tense | `--color-ink-muted` at 60% | Project ARCHIVED; File APPLIED; Open call FILLED |
| `error` | Technical failure | `--color-destructive` (crimson) | Forge run FAILED; Connection BROKEN |
| `embargo` | Deliberately held back | `--status-embargo` (plum) | Idea REJECTED; Forge run CANCELLED |

## The fix that embargo plum solves

Prior to D-ATLAS-3, `--status-live` and `--status-embargo` both aliased
`var(--color-accent)`. Semantically opposite states rendered identically.
`--status-embargo` is now a distinct hue:

- **Light mode**: `oklch(0.520 0.085 305)` — muted plum.
- **Dark mode**: `oklch(0.660 0.110 305)` — lifted plum.

The hue is intentionally desaturated; it reads as "present but held back" rather
than as an alert (crimson) or active (terracotta).

## The resolver function

The single source of truth is `packages/ui/src/lib/status-resolver.ts`:

```ts
type VisualRole = "live" | "progressing" | "idle" | "archived" | "error" | "embargo";

interface ResolvedStatus {
  role: VisualRole;
  label: string;       // human-readable, uppercase mono
  pulse?: boolean;     // animated dot for "currently running"
  note?: string;       // optional clarification ("deliberate decline")
}

function resolveStatus(
  dbField: `${string}.${string}`,
  dbValue: string | null | undefined,
): ResolvedStatus;
```

The full mapping table is maintained inside the resolver file. New status values
must be added there with an explicit role assignment — silent fallthrough returns
`role: "idle"` and emits `console.warn` so unmapped values are noisy.

## Forge phase palette — separate axis

`--color-done / running / awaiting / failed / queued` (the Forge phase palette)
is a **separate visual axis** for the `<PhasePipeline>` timeline component.
It does not feed `<StatusPill>`. A Forge phase has both a phase color (timeline)
and a status pill (workflow); the two never share a token.

## Pill consumption

`<StatusPill>` accepts either path:

```tsx
// Direct role (when the consumer already knows the visual meaning):
<StatusPill role="live" label="ACTIVE" pulse />

// Resolver path (when the consumer has only the DB value):
<StatusPill dbField="forge_runs.status" dbValue={run.status} />
```

The direct `role` path is preferred when the visual meaning is editorial
(e.g. labelling a section header). The `dbField + dbValue` path is preferred
when the value comes straight from a query — it preserves the trace.

## Why six (not five, not seven)

- **Five was the prior count** (`StatusKind` in pre-Atlas code). It collapsed
  `error` and `embargo` together as "destructive". Editorial drift: a build
  failure and a deliberate decline both painted crimson.
- **Seven was considered** during Atlas drafting (adding `pending` between
  `progressing` and `idle`). Rejected — `progressing` covers automatic
  advancement and `idle` covers waiting-on-human; nothing falls between cleanly.
- **Six is the minimum** that lets the studio distinguish failure (`error`,
  involuntary) from decline (`embargo`, voluntary).
