# Ops

Ops, billing and admin shapes.

- Registry name: `ops` (`@candor/ops`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/ops

## Install

```bash
npx candor-ui add ops
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/ops`.
It brings in: chat, status.

## Types

### ModelUsage

```ts
{ model: string inputTokens: number outputTokens: number cachedTokens?: number | undefined /** USD. Leave it out when the cost wasn't measured; it shows as not measured, never $0.00. */ cost?: number | undefined /** The cost as your server wrote it ("about $0.40"), shown instead of Candor's formatting. Needs `cost`. */ costText?: string | undefined }
```

### RunReceipt

```ts
{ runId: string title: string startedAt: string /** Seconds; absent while running. */ duration?: number | undefined status: RunStatus models: ModelUsage[] toolCalls: number /** * Other measured figures the app wants on the receipt, e.g. API calls * against a rate limit or compute minutes. A value of undefined shows "—". */ meters?: { label: string; value?: string | undefined }[] | undefined /** Wh
```

### SpanKind

"external" is a call to another system (an API, a database).

```ts
"agent" | "model" | "tool" | "external" | "wait"
```

### TraceSpan

```ts
{ id: string parentId?: string | undefined name: string kind: SpanKind /** ms from the run's start. */ start: number /** ms; absent while still open. */ duration?: number | undefined status: RunStatus agent?: AgentIdentity | undefined detail?: string | undefined error?: string | undefined }
```

### Usage

```ts
{ label: string used: number /** null when there's no limit (the plan sets none). */ limit: number | null unit: string resetsAt?: string | undefined note?: string | undefined /** * Your own estimate of where it will end, in the same unit (a quote, a forecast), marked on the bar. It is shown as * given and labelled as an estimate; nothing is extrapolated. */ estimate?: number | undefined /** Words 
```

### SpendRow

One line of spend. Each value is in USD for one period; null when that period's spend isn't known.

```ts
{ key: string; label: string; values: (number | null)[] }
```

### Criterion

```ts
{ id: string label: string passed: number failed: number /** Pass rate of the baseline run, 0–1. */ baseline: number | null }
```

### QualityPoint

```ts
{ date: string; score: number | null; change?: string | undefined }
```

### FailureCluster

```ts
{ id: string label: string count: number /** Change in count vs the previous period. */ delta: number examples: string[] agent?: AgentIdentity | undefined }
```

### FailureClusterInput

What FailureClusters takes: a FailureCluster whose change can be null when there's nothing to compare with, shown as unknown rather than as no change.

```ts
Omit<FailureCluster, "delta"> & { delta: number | null }
```

### ReviewItem

```ts
{ id: string agent: AgentIdentity ask: string answer: string /** The judge's grade, if it ran; the person's grade is separate. */ judge?: { verdict: "pass" | "fail"; reason: string } | undefined grade?: "pass" | "fail" | undefined note?: string | undefined }
```

### PlanTier

```ts
{ id: "team" | "enterprise" | "consultancy" name: string perSeat: number tagline: string includes: string[] recommended?: boolean | undefined }
```

### Subscription

```ts
{ plan: PlanTier["id"] seats: number seatsUsed: number status: "active" | "past-due" | "canceling" | "canceled" /** Next renewal, or when access ends if canceling. */ periodEnd: string card?: { brand: string; last4: string; expires: string } | undefined /** Required for past-due: what failed and what happens next. */ issue?: string | undefined }
```

### DeviceSession

```ts
{ id: string device: string method: "browser" | "device" | "environment" location?: string | undefined signedInAt: string lastActiveAt: string current?: boolean | undefined }
```

### SsoState

```ts
| { kind: "not-set" } | { kind: "verifying"; domain: string; record: string } | { kind: "active"; provider: string; domains: string[]; enforced: boolean; connectedAt: string }
```

### Schedule

```ts
{ id: string title: string /** Plain words, e.g. "Every weekday at 07:00 ET". */ when: string nextRunAt: string | null paused: boolean /** Last runs, oldest first. */ history: RunStatus[] agent?: AgentIdentity | undefined }
```

### Channel

```ts
{ id: string kind: "slack" | "teams" | "email" status: "connected" | "not-connected" | "broken" /** Where it posts, e.g. "#revops-agents". */ target?: string | undefined /** When an agent may post there without asking. */ replies: "mentions" | "updates" | "off" /** Required when broken. */ problem?: string | undefined }
```

### DigestItem

```ts
{ id: string kind: "finished" | "waiting" | "failed" title: string detail?: string | undefined at: string }
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
