diff --git a/api-reference/endpoint/agent-stream.mdx b/api-reference/endpoint/agent-stream.mdx
deleted file mode 100644
index 386d130..0000000
--- a/api-reference/endpoint/agent-stream.mdx
+++ /dev/null
@@ -1,128 +0,0 @@
----
-title: Agent stream
-description: Stateless agent relay for spec init and impact preview.
----
-
-## `POST /v1/agent/stream`
-
-Stateless relay between the CLI and the workspace's LLM provider. `zombied` adds the system prompt and API key, forwards to the provider, and streams the response back as SSE.
-
-The CLI manages conversation history and resends the full message array with each request. `zombied` holds no state between requests.
-
-### Request body
-
-```json
-{
- "mode": "spec_init",
- "messages": [
- {
- "role": "user",
- "content": "Generate a spec template for: Add rate limiting per API key with Redis backend"
- }
- ],
- "tools": [
- {
- "name": "read_file",
- "description": "Read a file from the user's repo",
- "input_schema": {
- "type": "object",
- "properties": { "path": { "type": "string" } },
- "required": ["path"]
- }
- },
- {
- "name": "list_dir",
- "description": "List directory contents",
- "input_schema": {
- "type": "object",
- "properties": { "path": { "type": "string" } },
- "required": ["path"]
- }
- },
- {
- "name": "glob",
- "description": "Find files matching a glob pattern",
- "input_schema": {
- "type": "object",
- "properties": { "pattern": { "type": "string" } },
- "required": ["pattern"]
- }
- }
- ]
-}
-```
-
-| Field | Type | Required | Description |
-|-------|------|----------|-------------|
-| `mode` | string | yes | System prompt selector. One of: `spec_init`, `preview` |
-| `messages` | array | yes | Conversation history in Messages API format. CLI accumulates and resends with each request. |
-| `tools` | array | yes | Tool definitions. CLI sends read-only tools (read_file, list_dir, glob). |
-
-### Response
-
-`200 text/event-stream` — SSE stream with the following event types:
-
-**`tool_use`** — The model wants to call a tool. CLI should execute locally and send the result back.
-
-```
-event: tool_use
-data: {"id":"tu_01","name":"read_file","input":{"path":"go.mod"}}
-```
-
-**`text_delta`** — Streaming text from the model's response.
-
-```
-event: text_delta
-data: {"text":"# M5_001: Rate Limiting\n\n**Prototype:** v1.0.0\n"}
-```
-
-**`done`** — Stream complete. Includes usage and cost.
-
-```
-event: done
-data: {"usage":{"input_tokens":12450,"output_tokens":3200,"cost_usd":0.085,"provider":"anthropic","model":"claude-sonnet-4-6","round_trips":4}}
-```
-
-**`error`** — Provider error or timeout.
-
-```
-event: error
-data: {"message":"provider timeout after 30s"}
-```
-
-### Tool call round trip
-
-When the CLI receives a `tool_use` event:
-
-1. Execute the tool locally (read file from laptop, list directory, etc.)
-2. Append the assistant's `tool_use` message and the user's `tool_result` to the message history
-3. POST the updated messages to the same endpoint
-
-The loop continues until the model returns text (no more tool calls) and a `done` event.
-
-### Modes
-
-| Mode | System prompt behavior | Typical tool calls |
-|------|----------------------|-------------------|
-| `spec_init` | Explore the repo, detect language/ecosystem, generate a milestone spec | 3-5 (list root, read manifest, read Makefile, list src/) |
-| `preview` | Read the spec, explore the repo, predict which files will be touched | 4-8 (read spec, list directories, read key files, grep patterns) |
-
-### Provider resolution
-
-`zombied` resolves the LLM provider from workspace configuration. The CLI never specifies or sees the provider. Supported providers include Anthropic, OpenAI, Google, and user-supplied keys.
-
-### Security
-
-- Tool calls are executed by the CLI on the user's machine, not by `zombied`
-- The CLI validates all paths against the repo root before reading (prevents path traversal)
-- `zombied` has no filesystem awareness and never sees file contents directly
-- Files only leave the laptop one at a time when the model explicitly requests them
-
-### Errors
-
-| Status | Code | Meaning |
-|--------|------|---------|
-| 400 | `INVALID_MODE` | Unknown mode value |
-| 401 | `UNAUTHORIZED` | Missing or invalid auth token |
-| 403 | `FORBIDDEN` | Insufficient role for workspace |
-| 500 | — | SSE `event: error` emitted before stream closes |
diff --git a/api-reference/endpoint/list-specs.mdx b/api-reference/endpoint/list-specs.mdx
deleted file mode 100644
index ebc7c7d..0000000
--- a/api-reference/endpoint/list-specs.mdx
+++ /dev/null
@@ -1,48 +0,0 @@
----
-title: "List specs"
-description: "List specs in a workspace."
-api: "GET /v1/specs"
----
-
-## Query parameters
-
-
- The workspace to list specs for.
-
-
-## Response
-
-Returns an array of spec objects belonging to the workspace.
-
-
- Array of spec objects.
-
-
-
-```json
-{
- "specs": [
- {
- "spec_id": "spec_01J8XW2A1K",
- "name": "M4_001_CLI_RUNTIME",
- "version": "v1.0.0",
- "milestone": "M4",
- "workstream": "001",
- "status": "ACTIVE",
- "created_at": "2026-03-20T09:00:00Z",
- "updated_at": "2026-03-28T11:30:00Z"
- },
- {
- "spec_id": "spec_01J8XW2B3L",
- "name": "M4_002_NPM_PUBLISH",
- "version": "v1.0.0",
- "milestone": "M4",
- "workstream": "002",
- "status": "ACTIVE",
- "created_at": "2026-03-21T10:15:00Z",
- "updated_at": "2026-03-27T14:00:00Z"
- }
- ]
-}
-```
-
diff --git a/api-reference/error-codes.mdx b/api-reference/error-codes.mdx
new file mode 100644
index 0000000..b5f94c2
--- /dev/null
+++ b/api-reference/error-codes.mdx
@@ -0,0 +1,212 @@
+---
+title: 'Error Codes'
+description: 'All error responses from zombied use RFC 7807 (application/problem+json). This page lists every error code, its HTTP status, and common causes.'
+---
+
+## Response format
+
+Every `4xx` and `5xx` response uses `Content-Type: application/problem+json`:
+
+```json
+{
+ "docs_uri": "https://docs.usezombie.com/api-reference/error-codes#UZ-ZMB-009",
+ "title": "Zombie not found",
+ "detail": "No zombie with id 'abc123' in this workspace.",
+ "error_code": "UZ-ZMB-009",
+ "request_id": "req_a1b2c3d4e5f6"
+}
+```
+
+| Field | Description |
+|---|---|
+| `docs_uri` | Stable link to this page for the specific code |
+| `title` | Short label — identical for every occurrence of a given code |
+| `detail` | Instance-specific context (varies per call) |
+| `error_code` | Machine-readable code. Use this for programmatic handling. |
+| `request_id` | Correlation ID for support and log tracing |
+
+---
+
+## UUID validation
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-UUIDV7-003` | 400 | Invalid UUID canonical format | ID passed is not a valid canonical UUIDv7 string |
+| `UZ-UUIDV7-005` | 500 | ID generation failed | Internal failure generating a new UUIDv7 |
+| `UZ-UUIDV7-009` | 400 | Invalid ID shape | Path or body ID does not match UUIDv7 format |
+| `UZ-UUIDV7-010` | 409 | UUID backfill conflict | Duplicate ID detected during backfill |
+| `UZ-UUIDV7-011` | 500 | Rollback blocked | Rollback cannot proceed due to existing state |
+| `UZ-UUIDV7-012` | 500 | Error response linking failed | Internal error linking error response to trace |
+
+## Internal errors
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-INTERNAL-001` | 503 | Database unavailable | Database server unreachable. Check `DATABASE_URL`. |
+| `UZ-INTERNAL-002` | 500 | Database error | Query failed. Check DB logs. |
+| `UZ-INTERNAL-003` | 500 | Internal operation failed | Unexpected internal failure. Check `err=` in logs. |
+
+## Request validation
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-REQ-001` | 400 | Invalid request | Missing or malformed field in request body or query |
+| `UZ-REQ-002` | 413 | Payload too large | Request body exceeds 2MB limit |
+
+## Authentication / authorization
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-AUTH-001` | 403 | Forbidden | Token valid but lacks permission for this resource |
+| `UZ-AUTH-002` | 401 | Unauthorized | Missing or invalid Bearer token |
+| `UZ-AUTH-003` | 401 | Token expired | JWT has passed its expiry time. Re-authenticate. |
+| `UZ-AUTH-004` | 503 | Authentication service unavailable | OIDC provider unreachable |
+| `UZ-AUTH-005` | 404 | Session not found | Auth session ID not found or already expired |
+| `UZ-AUTH-006` | 401 | Session expired | Auth session timed out before completion |
+| `UZ-AUTH-007` | 409 | Session already complete | Auth session was already resolved |
+| `UZ-AUTH-008` | 503 | Session limit reached | Too many concurrent auth sessions. Retry shortly. |
+| `UZ-AUTH-009` | 403 | Insufficient role | Token role is too low for this endpoint |
+| `UZ-AUTH-010` | 403 | Unsupported role | Token contains an unrecognized role claim |
+
+## API / queue
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-API-001` | 503 | API saturated | Too many in-flight requests. Back off and retry. |
+| `UZ-API-002` | 503 | Queue unavailable | Redis queue is unreachable |
+
+## Workspace
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-WORKSPACE-001` | 404 | Workspace not found | No workspace with this ID exists |
+| `UZ-WORKSPACE-002` | 402 | Workspace paused | Workspace billing is paused. Update payment. |
+| `UZ-WORKSPACE-003` | 402 | Workspace free limit reached | Free-tier execution limit reached. Upgrade plan. |
+
+## Billing
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-BILLING-001` | 400 | Invalid subscription ID | Subscription ID format is invalid |
+| `UZ-BILLING-002` | 500 | Billing state missing | Workspace has no billing record |
+| `UZ-BILLING-003` | 500 | Billing state invalid | Workspace billing record is in an inconsistent state |
+| `UZ-BILLING-004` | 400 | Invalid billing event | Billing webhook payload is malformed or unknown |
+| `UZ-BILLING-005` | 402 | Credit exhausted | Workspace has no remaining execution credit |
+
+## Entitlement
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-ENTL-001` | 503 | Entitlement service unavailable | Could not verify plan entitlements |
+| `UZ-ENTL-003` | 402 | Stage limit reached | Plan does not allow more pipeline stages |
+| `UZ-ENTL-004` | 403 | Skill not allowed | Plan does not include this skill |
+
+## Spec
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-SPEC-001` | 404 | Spec not found | No spec for this agent/run combination |
+| `UZ-SPEC-002` | 400 | Spec is empty | SKILL.md or TRIGGER.md has no content |
+| `UZ-SPEC-003` | 422 | Spec has no actionable content | Spec parsed but no runnable instructions found |
+| `UZ-SPEC-004` | 422 | Spec has unresolved file ref | Spec references a file that could not be fetched |
+
+## Run
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-RUN-001` | 404 | Run not found | No run with this ID in this workspace |
+| `UZ-RUN-002` | 409 | Invalid state transition | Run cannot move to requested state from current state |
+| `UZ-RUN-003` | 429 | Run token budget exceeded | Run hit `max_tokens` limit. Increase in agent profile. |
+| `UZ-RUN-004` | 408 | Run wall time exceeded | Run hit `max_wall_time_seconds`. Increase in profile. |
+| `UZ-RUN-005` | 429 | Workspace monthly budget exceeded | Monthly token budget exhausted. Resets next month. |
+| `UZ-RUN-006` | 409 | Run already in terminal state | Run is DONE/BLOCKED/CANCELLED; no further changes allowed |
+| `UZ-RUN-007` | 500 | Run cancel signal failed | Redis publish failed. Retry the cancel request. |
+| `UZ-RUN-008` | 500 | Run interrupt signal failed | Redis interrupt write failed. Check Redis. |
+| `UZ-RUN-009` | 409 | Run not interruptible | Run state does not support interrupts |
+
+## Agent
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-AGENT-001` | 404 | Agent not found | No agent profile with this ID |
+
+## Proposal
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-PROPOSAL-001` | 400 | Invalid proposal JSON | Proposal body is not valid JSON |
+| `UZ-PROPOSAL-002` | 400 | Proposal not an array | Top-level proposal value must be a JSON array |
+| `UZ-PROPOSAL-003` | 400 | Proposal change not an object | Each change in the array must be a JSON object |
+| `UZ-PROPOSAL-004` | 400 | Missing target field | Change object lacks required `target` field |
+| `UZ-PROPOSAL-005` | 400 | Unsupported target field | `target` value is not a supported stage field |
+| `UZ-PROPOSAL-006` | 400 | Missing stage ID | Change object lacks required `stage_id` |
+| `UZ-PROPOSAL-007` | 400 | Missing role | Stage change lacks required `role` field |
+| `UZ-PROPOSAL-008` | 400 | Missing insert-before stage ID | Insert operation lacks `insert_before_stage_id` |
+| `UZ-PROPOSAL-009` | 400 | Disallowed field | Change contains a field not allowed in proposals |
+| `UZ-PROPOSAL-010` | 400 | Unregistered agent reference | Proposal references an unknown agent ID |
+| `UZ-PROPOSAL-011` | 400 | Invalid skill reference | Skill reference does not match registry format |
+| `UZ-PROPOSAL-012` | 400 | Unknown stage reference | Stage ID in proposal does not exist in pipeline |
+| `UZ-PROPOSAL-013` | 409 | Duplicate stage reference | Same stage ID appears twice in proposal |
+| `UZ-PROPOSAL-014` | 422 | Proposal would not compile | Applying proposal produces an invalid pipeline |
+| `UZ-PROPOSAL-015` | 422 | No valid proposal template | No template matches for AI proposal generation |
+| `UZ-PROPOSAL-016` | 500 | Proposal generation failed | AI proposal generation encountered an error |
+| `UZ-PROPOSAL-017` | 404 | Proposal not found | No pending proposal with this ID |
+
+## Webhook
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-WH-001` | 404 | Zombie not found for webhook | Webhook routing found no matching zombie |
+| `UZ-WH-002` | 400 | Malformed webhook | Webhook body is missing required fields |
+| `UZ-WH-003` | 403 | Zombie paused | Zombie exists but is not active |
+| `UZ-WH-010` | 401 | Invalid webhook signature | Slack signature verification failed |
+| `UZ-WH-011` | 401 | Stale webhook timestamp | Slack timestamp is >5 min old (replay protection) |
+
+## Tool
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-TOOL-001` | 424 | Tool credential missing | Required vault credential not found for skill |
+| `UZ-TOOL-002` | 502 | Tool API call failed | External API returned an error |
+| `UZ-TOOL-003` | 502 | Tool git operation failed | Git operation failed. Check repo URL and credentials. |
+| `UZ-TOOL-004` | 400 | Tool not attached | Tool name not in zombie's TRIGGER.md skills list |
+| `UZ-TOOL-005` | 400 | Unknown tool | Tool name not recognized |
+| `UZ-TOOL-006` | 504 | Tool call timed out | External tool did not respond within timeout |
+
+## Zombie
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-ZMB-001` | 402 | Zombie budget exceeded | Daily dollar budget hit. Raise via `zombiectl config set`. |
+| `UZ-ZMB-002` | 500 | Zombie agent timeout | Agent timed out processing an event. Check logs. |
+| `UZ-ZMB-003` | 424 | Zombie credential missing | Required vault credential absent. Use `zombiectl credential add`. |
+| `UZ-ZMB-004` | 500 | Zombie claim failed | Could not claim zombie from DB. Verify zombie status. |
+| `UZ-ZMB-005` | 500 | Zombie checkpoint failed | Session checkpoint write to Postgres failed |
+| `UZ-ZMB-006` | 409 | Zombie name already exists | Name taken. Kill existing zombie first. |
+| `UZ-ZMB-007` | 400 | Zombie credential value too long | Credential value exceeds 4KB limit |
+| `UZ-ZMB-008` | 400 | Invalid zombie config | TRIGGER.md config_json fails schema validation |
+| `UZ-ZMB-009` | 404 | Zombie not found | No zombie with this ID in the workspace |
+
+## Approval gate
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-APPROVAL-001` | 400 | Approval parse failed | `gates` in TRIGGER.md config_json has invalid JSON |
+| `UZ-APPROVAL-002` | 404 | Approval not found | Approval action not found or already resolved |
+| `UZ-APPROVAL-003` | 401 | Approval invalid signature | Slack signature or timestamp verification failed |
+| `UZ-APPROVAL-004` | 503 | Approval Redis unavailable | Gate service down; default-deny applied |
+| `UZ-APPROVAL-005` | 400 | Approval condition invalid | Gate condition expression is invalid |
+
+## Credentials
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-CRED-001` | 503 | Anthropic API key missing | Workspace `anthropic_api_key` not in vault. Set via credentials API. |
+| `UZ-CRED-002` | 503 | GitHub token failed | GitHub App token request failed. Check `GITHUB_APP_ID`. |
+| `UZ-CRED-003` | 503 | Platform LLM key missing | No active platform LLM key. Admin must set via platform-keys API. |
+
+## Relay
+
+| Code | HTTP | Title | Common Causes |
+|---|---|---|---|
+| `UZ-RELAY-001` | 400 | No LLM provider configured | Workspace has no LLM credentials configured |
diff --git a/changelog.mdx b/changelog.mdx
index edbad8b..e8077e3 100644
--- a/changelog.mdx
+++ b/changelog.mdx
@@ -7,6 +7,222 @@ description: "Stay up to date with UseZombie product updates, new features, and
UseZombie is in **Early Access Preview**. Features below are live in the current release. APIs and agent behavior may evolve before GA.
+
+ ## Persistent Zombie Memory (M14_001)
+
+ Zombies now remember facts across runs. Memory persists in Postgres even after a
+ workspace is destroyed, so a Lead-Collector zombie doesn't re-research every lead
+ and a Customer-Support zombie doesn't re-ask customers their plan.
+
+ **How it works:**
+ - NullClaw's `memory_store` / `memory_recall` / `memory_list` / `memory_forget` tools
+ route to a dedicated `memory` Postgres schema, isolated by the `memory_runtime` role.
+ - Each zombie's memory is row-scoped by `instance_id = "zmb:{zombie_uuid}"` — two
+ concurrent zombies cannot read each other's entries.
+ - `conversation` category remains ephemeral (workspace SQLite), as intended.
+ - The executor receives `memory_connection` + `memory_namespace` in the RPC payload
+ and bypasses NullClaw's instance_id propagation gap directly in `zombie_memory.zig`.
+
+ **External-agent memory API (Path B):**
+ - `POST /v1/memory/store` — store or upsert a key-value entry for a zombie
+ - `POST /v1/memory/recall` — keyword search across key and content
+ - `POST /v1/memory/list` — list entries, optionally filtered by category
+ - `POST /v1/memory/forget` — delete a specific entry by key (idempotent)
+ - All endpoints enforce workspace scope: a caller can only access zombies in their workspace.
+
+
+
+
+ ## Integration Grant & Execute API — credentialed proxy with human approval (M9_001)
+
+ Zombies (both internal and external LangGraph/CrewAI agents) can now call external
+ services through UseZombie's credentialed proxy. The credential never leaves UseZombie —
+ it is injected server-side and stripped from any accidental echo in the response.
+
+ **Integration Grant system:**
+ - A zombie requests a grant for a service (`POST /v1/zombies/{id}/integration-requests`).
+ UseZombie fans out an Approve/Deny notification to Slack, Discord, and/or the dashboard
+ simultaneously. Human clicks Approve once — grant is durable; no per-call approval needed.
+ - High-risk endpoints still run through the existing M4 per-request approval gate independently.
+ - Revoke at any time via `DELETE /v1/zombies/{id}/integration-grants/{grant_id}` or
+ `zombiectl grant revoke`.
+
+ **`POST /v1/execute`** — the new proxy endpoint:
+ - Auth: zombie session (internal Path A) or `Authorization: Bearer zmb_xxx` (external Path B).
+ - Pipeline: grant check → firewall domain/endpoint/injection policy → approval gate → credential
+ inject → outbound HTTP → credential echo strip → activity log.
+ - Returns the service response with `X-UseZombie-Action-Id` and `X-UseZombie-Firewall-Decision` headers.
+
+ **External agent keys (`zmb_` prefix):**
+ - Create via `POST /v1/workspaces/{ws}/external-agents` (Clerk-protected). Raw key shown once; only
+ SHA-256 hash stored. Key resolves to a full zombie identity (workspace_id + zombie_id).
+ - CLI: `zombiectl agent create / list / delete`, `zombiectl grant list / revoke`.
+
+ **Services supported:** Slack, Gmail/AgentMail, Discord, Grafana.
+ **New error codes:** `UZ-APIKEY-001/002`, `UZ-GRANT-001/002/003`, `UZ-PROXY-001`.
+ **Schema:** `core.integration_grants` (slot 026), `core.external_agents` (slot 027).
+
+
+
+ ## Zombie execution telemetry — per-delivery metrics store and dual API (M18_001)
+
+ UseZombie now records `token_count`, `time_to_first_token_ms`, `wall_seconds`, and
+ `credit_deducted_cents` for every zombie event delivery, with two query surfaces:
+
+ - **`GET /v1/workspaces/{ws}/zombies/{id}/telemetry?limit=50&cursor=`** — workspace-scoped
+ cursor-paginated list of the last N deliveries for a single zombie. Returns newest-first
+ with an opaque cursor for subsequent pages.
+ - **`GET /internal/v1/telemetry?workspace_id=&zombie_id=&after=&limit=100`** — operator
+ cross-workspace query. All params optional; `after` accepts epoch ms for time-window queries.
+
+ Writes are idempotent on `event_id` — crash-recovery redelivery of the same event does not
+ create duplicate rows. A write failure in the telemetry path is logged but does not affect
+ credit deduction or event acknowledgement.
+
+ Additionally, each delivery now emits an OTel span (`zombie.delivery`) with `zombie_id`,
+ `workspace_id`, `event_id`, `token_count`, and `plan_tier` attributes. The span uses the
+ actual wall-clock delivery window so it appears at the correct position in Grafana Tempo.
+
+
+
+ ## Slack plugin — OAuth install, event routing, and approval interactions (M8_001)
+
+ UseZombie workspaces can now connect Slack via the "Add to Slack" OAuth flow or
+ the `zombiectl credential add slack` CLI path:
+
+ - **`GET /v1/slack/install`** — redirects workspace owners to Slack OAuth with an
+ HMAC-signed CSRF state and a 10-minute Redis nonce for replay protection.
+ - **`GET /v1/slack/callback`** — exchanges the OAuth code for a bot token, stores it
+ in the zombie vault (`vault.secrets(workspace_id, "slack")`), creates a
+ `core.workspace_integrations` routing record, and posts a one-time confirmation
+ message to the workspace.
+ - **`POST /v1/slack/events`** — verifies Slack signing secret (HMAC-SHA256, RULE CTM),
+ filters bot-loop events, resolves the workspace via `workspace_integrations`, finds
+ the zombie configured with a `slack_event` trigger, and enqueues the event to Redis.
+ - **`POST /v1/slack/interactions`** — verifies signing, URL-decodes the Slack
+ `payload=` form field, parses `gate_*` action IDs, and relays approve/deny decisions
+ to the existing approval gate (`resolveApproval`).
+
+ The zombie vault holds the credential; `workspace_integrations` holds only routing
+ metadata (no credentials). OAuth and CLI paths converge at the same vault slot.
+ Schema: `core.workspace_integrations` (migration slot 028).
+
+
+
+ ## Harness & agent-profile scope removed (M17_001)
+
+ The legacy harness control plane — config compilation pipeline, `/v1/harness/*`
+ endpoints, `agent.agent_profiles` / `agent_config_versions` / `workspace_active_config` /
+ `config_compile_jobs` / `config_linkage_audit_artifacts` tables, and the
+ `agent_profile_version` audit linkage — had zero runtime consumers after the
+ zombie execution model replaced it. All of it is gone:
+
+ - Server handlers (`harness_http.zig`, `harness_control_plane/*`, `profile_linkage.zig`)
+ and the `src/harness/` module removed. `/v1/workspaces/{id}/harness/*` routes no
+ longer exist.
+ - Five agent-schema tables removed via full teardown (pre-v2.0 policy): SQL files
+ `schema/008_harness_control_plane.sql` and `schema/011_profile_linkage_audit.sql`
+ deleted outright; `schema/009_rls_tenant_isolation.sql` stripped of agent.*
+ RLS policies; canonical migration array shrunk from 21 to 19 entries.
+ - `zombiectl`: `harness`, `harness source`, `harness compile`, `harness activate`,
+ `harness active`, and `agent harness revert` subcommands removed. The CLI no
+ longer references dropped endpoints.
+ - `/v1/agents/{id}` handler was a dead client of `agent_profiles` and is removed.
+ - `workspace_entitlements.max_profiles` column dropped and `UZ-ENTL-002`
+ (Profile limit reached) error retired — both were harness-only.
+ - Website marketing copy no longer advertises "harness checks"; the step is
+ now described as "validation" in the flow.
+
+ ~4,200 lines of dead infrastructure removed. No behavior change for zombie
+ workflows. No API breaking changes visible to any live client.
+
+
+
+ ## Zombie observability (M15_002)
+
+ Zombie triggers and event deliveries now emit PostHog events and increment
+ Prometheus counters, making zombie throughput visible in Grafana and product
+ analytics. Previously, `/metrics` contained no zombie counters and PostHog
+ dashboards showed no zombie activity.
+
+ - **Prometheus:** `zombies_triggered_total`, `zombies_completed_total`,
+ `zombies_failed_total`, `zombie_tokens_total`, and a `zombie_execution_seconds`
+ wall-time histogram.
+ - **PostHog:** `zombie_triggered` fired from the webhook receiver; `zombie_completed`
+ fired after each delivery attempt with tokens, wall-time, and exit status.
+ - Graceful no-ops when the PostHog client is unavailable — metrics always record.
+
+
+
+ ## Zombie credit metering (M15_001)
+
+ Free-plan zombies now deduct from `consumed_credit_cents` after each successful event
+ delivery, at 1 cent per agent-second. Previously, the pre-execution credit gate blocked
+ exhausted workspaces but the balance never decremented — free-plan zombies effectively
+ consumed unlimited quota. Now:
+
+ - **Free plan:** Each delivery records a `CREDIT_DEDUCTED` audit row and updates
+ `consumed_credit_cents` / `remaining_credit_cents` atomically.
+ - **Scale plan:** Short-circuits with no DB write — Scale is unlimited.
+ - **Exhausted credit:** Still writes a zero-delta audit row for observability, so
+ operators can see post-exhaustion usage events.
+ - **Crash recovery:** Replay of the same `event_id` is idempotent — no double-charge.
+ - **DB failure:** Logged and the event is still acknowledged, so a transient Postgres
+ outage never causes message loss or redelivery storms.
+
+ No API changes.
+
+
+
+ ## Scoring infrastructure removed (M10_004)
+
+ The legacy agent scoring pipeline had no callers after the zombie execution model replaced
+ it. All dead code has been removed: scoring Prometheus gauges, PostHog scoring events,
+ scoring atomic counters, and the scoring duration histogram. The billing summary endpoint
+ (`GET /v1/workspaces/:id/billing/summary`) now returns zeros correctly instead of 500-ing
+ against dropped tables. No user-visible API changes.
+
+
+
+ ## Zombie directory format, AI Firewall, error standardization, pipeline v1 removal
+
+ ### Zombie directory format
+ Zombies are now two-file directories (`SKILL.md` + `TRIGGER.md`) instead of a single `.md` file.
+ `SKILL.md` follows the ClaHub registry format — the same file you upload to the CLI is publishable to the skill registry.
+ `TRIGGER.md` carries deployment config: trigger, chain, budget, network policy, credentials.
+ `zombiectl install` scaffolds both files; `zombiectl up` sends them raw to the API.
+
+ ### Dynamic skills (no compiled Zig per skill)
+ Skills are now config-driven. The NullCraw executor reads `SKILL.md` instructions and uses
+ built-in tools (`shell`, `http`, `file_read`) to call external APIs. Adding a new skill requires
+ only a new directory — no rebuild of the server binary.
+
+ ### AI Firewall — 4-layer outbound inspection
+ Every outbound request from a Zombie now passes through an AI Firewall before reaching external APIs:
+ - **Domain allowlist** — only domains declared in `TRIGGER.md` `network.allow` can be reached
+ - **Endpoint policy** — per-endpoint rules in `TRIGGER.md` `firewall:` section (e.g., allow GET, deny POST)
+ - **Prompt injection detection** — scans outbound bodies for instruction override, role hijacking, and jailbreak patterns
+ - **Content scanning** — inspects response bodies for credential leakage and PII (credit cards, SSNs, API keys)
+ All firewall decisions are logged as activity events. Fails closed on errors.
+
+ ### API error format standardized (RFC 7807)
+ All error responses now use `application/problem+json` with `UZ-` prefixed error codes.
+ Every error code has a stable HTTP status — callers no longer need to parse HTTP status codes independently.
+
+ ### Pipeline v1 removed
+ The v1 GitHub PR-solver pipeline has been removed. All `/v1/runs/*` and `/v1/specs` endpoints
+ return **HTTP 410 Gone** with error code `ERR_PIPELINE_V1_REMOVED`. Use zombie-native SSE stream
+ and chat-inject API instead (see v0.5.0 release notes).
+
+ ### Webhook auth — URL-embedded secret
+ Preferred webhook URL format: `POST /v1/webhooks/{zombie_id}/{secret}`.
+ Bearer token remains supported as fallback.
+
+ ### Handler context layer (internal)
+ All HTTP handler boilerplate (arena setup, request ID, Bearer auth) is now handled by a shared
+ `hx.zig` wrapper. Handlers contain only business logic. No user-visible behavior change.
+
+
## Lead Zombie — v2 core ships
@@ -35,6 +251,14 @@ description: "Stay up to date with UseZombie product updates, new features, and
skill invoked, response returned — is timestamped and queryable.
`zombiectl logs` streams the activity log. Cursor-based pagination for replay.
+ ### Credential injection
+ Credentials are resolved from the vault at runtime and injected into the sandbox.
+ No credentials in config files. Add credentials with `zombiectl credential add`.
+
+ ### Session checkpoint
+ The zombie's conversation context is checkpointed to Postgres after each event.
+ On crash and restart, the zombie resumes from the last checkpoint — no lost context.
+
### New CLI commands
`zombiectl install`, `zombiectl up`, `zombiectl status`, `zombiectl kill`,
`zombiectl logs`, `zombiectl credential add`, `zombiectl credential list`.
@@ -52,6 +276,10 @@ description: "Stay up to date with UseZombie product updates, new features, and
### Version tooling
`make sync-version` / `make check-version` prevent VERSION drift across `build.zig.zon` and `zombiectl/package.json`.
+
+ ### Bug fixes
+ - Fixed YAML parser silently dropping array items in CLI config upload
+ - Fixed UTF-8 truncation splitting multi-byte characters in session context
diff --git a/docs.json b/docs.json
index e10ffec..5b1bf20 100644
--- a/docs.json
+++ b/docs.json
@@ -78,7 +78,8 @@
{
"group": "Overview",
"pages": [
- "api-reference/introduction"
+ "api-reference/introduction",
+ "api-reference/error-codes"
]
},
{
@@ -106,12 +107,6 @@
"POST /v1/workspaces/{workspace_id}:sync"
]
},
- {
- "group": "Specs",
- "pages": [
- "GET /v1/specs"
- ]
- },
{
"group": "Runs",
"pages": [
@@ -124,28 +119,6 @@
"POST /v1/runs/{run_id}:cancel"
]
},
- {
- "group": "Agents",
- "pages": [
- "GET /v1/agents/{agent_id}",
- "GET /v1/agents/{agent_id}/scores",
- "GET /v1/agents/{agent_id}/improvement-report",
- "GET /v1/agents/{agent_id}/proposals",
- "POST /v1/agents/{agent_id}/proposals/{proposal_id}:approve",
- "POST /v1/agents/{agent_id}/proposals/{proposal_id}:reject",
- "POST /v1/agents/{agent_id}/proposals/{proposal_id}:veto",
- "POST /v1/agents/{agent_id}/harness/changes/{change_id}:revert"
- ]
- },
- {
- "group": "Harness",
- "pages": [
- "PUT /v1/workspaces/{workspace_id}/harness/source",
- "POST /v1/workspaces/{workspace_id}/harness/compile",
- "POST /v1/workspaces/{workspace_id}/harness/activate",
- "GET /v1/workspaces/{workspace_id}/harness/active"
- ]
- },
{
"group": "Credentials",
"pages": [
@@ -161,13 +134,6 @@
"DELETE /v1/workspaces/{workspace_id}/skills/{skill_ref}/secrets/{key_name}"
]
},
- {
- "group": "Agent Relay",
- "pages": [
- "POST /v1/workspaces/{workspace_id}/spec/template",
- "POST /v1/workspaces/{workspace_id}/spec/preview"
- ]
- },
{
"group": "Admin",
"pages": [
@@ -180,8 +146,7 @@
"group": "Billing",
"pages": [
"POST /v1/workspaces/{workspace_id}/billing/scale",
- "POST /v1/workspaces/{workspace_id}/billing/events",
- "POST /v1/workspaces/{workspace_id}/scoring/config"
+ "POST /v1/workspaces/{workspace_id}/billing/events"
]
}
]