yarli 0.6.1

CLI, stream mode renderer, interactive TUI, scheduler, store, and API
Documentation
# YARLI API Contract Matrix (Loop-8 Contract Scope)

Version: 1.1
Scope: Canonical API contract used by Loop-8 acceptance
Generated by: 2026-02-15
Source file: `crates/yarli-api/src/server.rs`

## In-Scope Endpoints

`yarli-api` currently exposes one health probe, one durable run-status read endpoint, and one durable task-status read endpoint. These are the in-scope API routes for Loop-8.

| Route | Method | Contract | Success response | Failure semantics | Data source + consistency | Verifier command |
|---|---|---|---|---|---|---|
| `/health` | `GET` | Liveness endpoint with static payload. No auth required. | `200 OK`, JSON `{ "status": "ok" }`. | Not documented as an error domain; endpoint should not fail under normal healthy service startup. | Purely in-process response, always computed from handler function state. | `rg -n "fn health|/health|HealthResponse" crates/yarli-api/src/server.rs` |
| `/v1/runs/{run_id}/status` | `GET` | Read-only query for a run aggregate status projection reconstructed from persisted events. `run_id` MUST be a UUID string. | `200 OK`, JSON object `RunStatusResponse` with fields:<br> - `run_id` (UUID)<br> - `state` (string)<br> - `last_event_type` (string)<br> - `updated_at` (RFC 3339 timestamp)<br> - `correlation_id` (UUID)<br> - `objective` (optional string)<br> - `deterioration` (optional object, latest `DeteriorationReport`)<br> - `task_summary` (counts by task state) | `400 Bad Request`, `{"error":"invalid run ID (expected UUID)"}` when `run_id` is not parseable as UUID.<br>`404 Not Found`, `{"error":"run <run_id> not found"}` when no events exist for `run_id`.<br>`500 Internal Server Error` when store reads fail. | Reads from `EventStore` directly, reconstructing state by replaying run events and task events by correlation id. Read-your-writes is expected for the same store instance/path used by the serving process. No external caching layer is involved in this endpoint. | `rg -n "fn run_status|RunStatusResponse|ApiError::InvalidRunId|ApiError::RunNotFound" crates/yarli-api/src/server.rs` |
| `/v1/tasks/{task_id}` | `GET` | Read-only query for an individual task aggregate status reconstructed from persisted task events. `task_id` MUST be a UUID string. | `200 OK`, JSON object `TaskStatusResponse` with fields:<br> - `task_id` (UUID)<br> - `state` (string)<br> - `last_event_type` (string)<br> - `updated_at` (RFC 3339 timestamp)<br> - `correlation_id` (UUID) | `400 Bad Request`, `{"error":"invalid task ID (expected UUID)"}` when `task_id` is not parseable as UUID.<br>`404 Not Found`, `{"error":"task <task_id> not found"}` when no events exist for `task_id`.<br>`500 Internal Server Error` when task events are present but cannot be linked to a run correlation. | Reads from `EventStore` by `EntityType::Task` and `entity_id`, replaying events by `occurred_at` to determine latest state. The run correlation must resolve to at least one run event so orphan task streams are rejected. | `rg -n "fn task_status|TaskStatusResponse|ApiError::InvalidTaskId|ApiError::TaskNotFound|ApiError::CorrelatedRunMissing" crates/yarli-api/src/server.rs` |
| `/v1/tasks/{task_id}/annotate` | `POST` | Attach blocker detail to a task (`task.annotated` event). | `200 OK`, JSON object `TaskStatusResponse`. | `400 Bad Request` for invalid `task_id`.<br>`404 Not Found` when task does not exist. | Write path persists event only when task exists, then returns current task projection. | `POST /v1/tasks/{task_id}/annotate` handler in `crates/yarli-api/src/server.rs`, `TaskAnnotateRequest`. |
| `/v1/tasks/{task_id}/unblock` | `POST` | Transition `TaskBlocked` tasks to `TaskReady` (`task.unblocked` event). | `200 OK`, JSON object `TaskStatusResponse` with updated state.<br>`409 Conflict` when transition is invalid for current state. | `400 Bad Request` for invalid `task_id`.<br>`404 Not Found` when task does not exist.<br>`409 Conflict` when state transition is invalid. | Replays persisted task events for state inference, validates transition, persists `task.unblocked`, returns updated task projection. | `rg -n "fn task_unblock|apply_task_transition_to_state|TaskUnblockRequest|TaskStatusResponse" crates/yarli-api/src/server.rs` |
| `/v1/tasks/{task_id}/retry` | `POST` | Transition failed tasks to ready and increase retry attempt (`task.retrying` event). | `200 OK`, JSON object `TaskStatusResponse` with updated state.<br>`409 Conflict` when transition is invalid for current state. | `400 Bad Request` for invalid `task_id`.<br>`404 Not Found` when task does not exist.<br>`409 Conflict` when state transition is invalid. | Validates `TaskFailed` → `TaskReady` transition, then persists `task.retrying` event with incremented `attempt_no`. | `rg -n "fn task_retry|apply_task_transition_to_state|TaskStatusResponse" crates/yarli-api/src/server.rs` |
| `/v1/tasks/{task_id}/priority` | `POST` | Override queued priority for a task via queue scheduler backend. Requires debug queue-capable server (`router_with_queue`). | `200 OK`, JSON object `TaskStatusResponse` of the current task projection. | `400 Bad Request` for invalid `task_id`.<br>`404 Not Found` when task does not exist or queue has no rows for task.<br>`503 Service Unavailable` when router lacks queue wiring. | Loads task projection for correlation validation and delegates scheduler `override_priority`. Requires queue backend feature. | `rg -n "fn task_override_priority|TaskPriorityOverrideRequest|TaskQueue" crates/yarli-api/src/server.rs` |
| `/v1/events/ws` | `GET` | Open websocket stream with optional filters: `run_id`, `task_id`, `event_type`, `severity`, `after_event_id`, `poll_ms`. | Streamed JSON text frames containing an envelope with `event`, `run_id`, `task_id`, and `severity`. | `400 Bad Request` when query UUID filters cannot be parsed. | Query filters run over event-store order; cursor advances even when events are filtered out so stream does not replay already-seen events. | `rg -n "event_stream_ws|EventStreamQuery|handle_event_stream|matches_stream_filters|EventStreamEnvelope" crates/yarli-api/src/server.rs` |
| `/v1/webhooks` | `POST` | Register webhook delivery endpoint with optional filters: `callback_url` (required), `run_id`, `task_id`, `event_type`, `severity`. | `200 OK`, JSON object with generated `webhook_id`, `callback_url`, normalized `filters`, and `created_at`. | `400 Bad Request` for missing/invalid callback URL or unsupported scheme. | Deliveries are persisted in-memory and dispatched by background polling of the event store with exponential backoff retry (`250ms`, `500ms`, `1s`, ... capped at `10s`). | `rg -n "register_webhook|WebhookRegistration|webhook_dispatcher|deliver_webhook_with_retry|webhook_backoff_delay" crates/yarli-api/src/server.rs` |

### Authentication and rate limiting

- When `YARLI_API_KEYS` is set, all endpoints except `/health` and `/metrics` require one of the configured keys.
- Keys are accepted via:
  - `Authorization: Bearer <key>`
  - `Authorization: Token <key>`
  - `Authorization: <key>`
  - `x-api-key` header
  - `api-key` header
- `YARLI_API_RATE_LIMIT_PER_MINUTE` controls fixed-window request throttling (default `120` requests/minute).
- Failure responses:
  - `401 Unauthorized` for missing/invalid key when auth is enabled.
  - `429 Too Many Requests` when a key exceeds the rate limit, with a `Retry-After` header indicating seconds to retry.

## Mapping to Acceptance Matrix

- `health` row is covered by Loop-8 contract checks requiring a successful health handler test.
- `run status` row is covered by Loop-8 contract checks for success projection replay and not-found failure semantics.
- `task status` row is covered by Loop-8 contract checks for success replay and failure semantics.
- Invalid UUID semantics are covered by code-path mapping for `ApiError::InvalidRunId` and `ApiError::InvalidTaskId`.

## Not In Scope

- No request bodies are defined for current endpoints.