{
  "openapi": "3.1.0",
  "info": {
    "title": "Pixel Office Public API",
    "version": "1.0.0",
    "summary": "Operate an AI-agent office as a swarm over curl.",
    "description": "Public HTTP API for pixel-office: create offices, populate them with AI agents, hand the swarm work, and watch progress live.\n\n## Authentication\n\nThree credential types cover the surface:\n\n1. **Personal access token (`pat_…`)** — the owner/admin API. Mint one at `POST /api/auth/tokens` (with a session cookie or an existing token) and send it as `Authorization: Bearer pat_xxx`. Governs offices, agents, tasks, and the generate endpoints.\n2. **Webhook token** — a per-office or per-agent secret that IS the credential for the *public* fire + event-feed endpoints (no session needed). Present it either in the URL path segment or, preferably, as `Authorization: Bearer <webhook-token>` (keeps the secret out of proxy/access logs). Mint/rotate via `POST /api/offices/{slug}/rotate-webhook` or `POST /api/agents/{id}/rotate-webhook`; a fresh office/agent is also created with one already minted.\n3. **Agent MCP key (`agt_…`)** — a per-agent key an autodev/MCP client uses to act AS a character (JSON-RPC over `/api/mcp/a2a`, `/api/office-mcp`, hook events). Not needed for the REST swarm flow; documented here for completeness.\n\n## Three event-stream transports\n\nThe same token-authed activity feed (teammate messages + tool/hook events) is available three ways:\n\n- **HTTP poll** — `GET …/events?since=<iso>`; returns `{ events, newest }`. Pass `newest` back as the next `since`.\n- **Server-Sent Events (SSE)** — `GET …/events/stream`; `Content-Type: text/event-stream`, one `data: <json>` frame per new event, heartbeats, self-terminates after 600s or on disconnect.\n- **WebSocket spectator** — connect to `wss://autodev.code.aioffice.works/ws` with `?webhook=<token>` or `?stream_token=<token>` (or an `X-Webhook-Token` header). Read-only: never registers a presence, never receives steers/tasks. See the `Event Streams` tag description.\n\n## Completion callbacks\n\nWebhook-fired tasks accept an optional `callbackUrl` (alias `callback_url`). When the task transitions to `done`, a JSON summary is POSTed to it once. **SSRF constraint:** the URL must be a public `http(s)` URL — loopback, RFC1918/ULA, link-local (169.254/16), `0.0.0.0`, and cloud-metadata addresses are rejected at registration and re-checked at send time (redirects are disabled). Max length 2048 chars.\n\n## Conventions\n\n- REST admin responses wrap payloads as `{ \"success\": bool, \"data\": … }` or `{ \"success\": false, \"error\": \"…\" }`.\n- Public webhook fire/feed responses use `{ \"ok\": … }` / `{ \"events\": … }` shapes instead.\n- All timestamps are ISO-8601 strings. Task and Agent ids are strings (UUID/hex); Office ids are integers.\n- The `/api/agents/generate` and `/api/tasks/generate` LLM endpoints are rate-limited to **10 requests/minute** (`throttle:10,1`). Auth credential endpoints and the public webhook fire/feed routes carry their own throttles (noted per operation)."
  },
  "servers": [
    { "url": "https://autodev.code.aioffice.works", "description": "Live production instance" }
  ],
  "externalDocs": {
    "description": "WebSocket base",
    "url": "wss://autodev.code.aioffice.works/ws"
  },
  "tags": [
    { "name": "System", "description": "Health + WebSocket bootstrap." },
    { "name": "Auth", "description": "Session login and personal access tokens (`pat_…`) for the owner/admin API." },
    { "name": "Offices", "description": "Create and manage offices (a swarm's workspace). Owner-scoped." },
    { "name": "Agents", "description": "Create and manage the AI characters that staff an office." },
    { "name": "Tasks", "description": "Post, update, and steer work for agents (first-party, session/`pat_` auth)." },
    { "name": "Webhooks", "description": "Public 'post a task via curl' endpoints. The per-office / per-agent webhook token IS the auth — no session. Token rides the URL path or `Authorization: Bearer <webhook-token>`." },
    { "name": "Event Streams", "description": "Watch swarm progress. Same token-authed feed via HTTP poll, SSE, and a read-only WebSocket spectator.\n\n**WebSocket spectator:** open `wss://autodev.code.aioffice.works/ws?webhook=<office-or-agent-webhook-token>` (or `?stream_token=<token>`, or send an `X-Webhook-Token` header on the upgrade). It resolves to a read-only 'spectator' connection scoped to the office (an agent token narrows to that agent's activity). It never registers an agent presence and is never a steer/task target. Unknown tokens are rejected. This is distinct from an agent WS client, which connects with `?token=<agt_key>&endpoint=<agent-slug>`." },
    { "name": "Generate", "description": "LLM-assisted drafting of agent rosters and task specs. Rate-limited to 10 req/min." }
  ],
  "security": [
    { "patToken": [] }
  ],
  "components": {
    "securitySchemes": {
      "patToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "pat_…",
        "description": "Owner/admin personal access token. `Authorization: Bearer pat_xxx`. Mint at POST /api/auth/tokens. A live session cookie is also accepted on these routes and wins over a token."
      },
      "webhookBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "<webhook-token>",
        "description": "Per-office or per-agent webhook secret presented as `Authorization: Bearer <webhook-token>`. Recommended over the in-path token so the secret is not captured in access logs. Alternatively pass the token as the `{token}` path segment."
      },
      "webhookTokenHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Webhook-Token",
        "description": "Webhook secret for the read-only WebSocket spectator upgrade (or use the `?webhook=` / `?stream_token=` query param)."
      },
      "agentKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "agt_…",
        "description": "Per-agent MCP key (`agt_…`). Authenticates an autodev/MCP client acting AS a character on /api/mcp/a2a, /api/office-mcp, and /api/hooks/event. Not required for the REST swarm flow."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "const": false },
          "error": { "type": "string" },
          "errors": { "type": "object", "additionalProperties": { "type": "array", "items": { "type": "string" } }, "description": "Field-level validation errors (422 only)." }
        },
        "required": ["success", "error"]
      },
      "WebhookError": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "const": false },
          "error": { "type": "string" }
        },
        "required": ["ok", "error"]
      },
      "Office": {
        "type": "object",
        "description": "Shape of `toApiArray()`. `webhookUrl` is populated (carries the raw secret) only on owner-scoped reads; it is null on public/non-owner paths where `webhookConfigured` still tells you a token exists.",
        "properties": {
          "id": { "type": "integer" },
          "slug": { "type": "string", "examples": ["acme-swarm"] },
          "name": { "type": "string" },
          "description": { "type": ["string", "null"] },
          "isPublic": { "type": "boolean" },
          "protected": { "type": "boolean", "description": "Delete-protection lock." },
          "theme": { "type": ["string", "null"] },
          "projectManagementEnabled": { "type": "boolean" },
          "lastSyncedAt": { "type": ["string", "null"], "format": "date-time" },
          "createdAt": { "type": ["string", "null"], "format": "date-time" },
          "webhookConfigured": { "type": "boolean", "description": "Whether a per-office swarm webhook secret has been minted (safe boolean; always present)." },
          "webhookUrl": { "type": ["string", "null"], "format": "uri", "description": "Absolute POST URL that hands the office a task (contains the raw secret). Owner-only; null otherwise.", "examples": ["https://autodev.code.aioffice.works/api/offices/webhook/<webhook-token>"] },
          "agentsCount": { "type": ["integer", "null"], "description": "Present only when the query eager-loaded counts (office list). Null = unknown." },
          "onlineAgentsCount": { "type": ["integer", "null"] }
        }
      },
      "Agent": {
        "type": "object",
        "description": "Shape of Agent::toApiArray(). `webhookUrl` is owner-only (else null); `apiKey` is returned ONLY once, at creation / regenerate-key.",
        "properties": {
          "id": { "type": "string", "description": "UUID." },
          "uuid": { "type": "string" },
          "officeId": { "type": ["integer", "null"] },
          "deployerAgentId": { "type": ["integer", "null"] },
          "slug": { "type": "string" },
          "name": { "type": "string" },
          "isConnected": { "type": "boolean" },
          "avatarIndex": { "type": "integer" },
          "color": { "type": "string", "examples": ["#00d4ff"] },
          "emoji": { "type": ["string", "null"] },
          "status": { "type": "string", "enum": ["idle", "working", "reading", "waiting", "offline"], "description": "The raw 'active' from the WS layer is normalised to 'idle'." },
          "mood": { "type": ["string", "null"] },
          "provider": { "type": ["string", "null"], "enum": ["claude", "grok", "opencode", "copilot", null] },
          "cliVersion": { "type": ["string", "null"] },
          "mcpOperator": { "type": "boolean", "description": "True when the socket is an `autodev mcp-operate` bridge rather than a loop." },
          "currentTaskId": { "type": ["string", "null"] },
          "seatId": { "type": ["string", "null"] },
          "sortOrder": { "type": ["integer", "null"] },
          "position": { "type": "object", "properties": { "col": { "type": "integer" }, "row": { "type": "integer" } } },
          "xp": { "type": "integer" },
          "level": { "type": "integer" },
          "streak": { "type": "integer" },
          "activityLevel": { "type": "number" },
          "workingProgress": { "type": ["number", "null"] },
          "lastActivityAt": { "type": ["string", "null"], "format": "date-time" },
          "lastWsSeenAt": { "type": ["string", "null"], "format": "date-time" },
          "lastMcpSeenAt": { "type": ["string", "null"], "format": "date-time" },
          "mcpConnected": { "type": "boolean" },
          "presence": { "type": "string", "enum": ["ws", "mcp", "offline"], "description": "Reconciled single-value presence (WS beats MCP)." },
          "projectFolder": { "type": ["string", "null"] },
          "gitBranch": { "type": ["string", "null"] },
          "vncEnabled": { "type": "boolean" },
          "vncHost": { "type": ["string", "null"] },
          "vncPort": { "type": ["integer", "null"] },
          "rdpEnabled": { "type": "boolean" },
          "rdpHost": { "type": ["string", "null"] },
          "rdpPort": { "type": ["integer", "null"] },
          "gitEnabled": { "type": "boolean" },
          "fileBrowserEnabled": { "type": "boolean" },
          "exportEnabled": { "type": "boolean" },
          "exportDailyBackup": { "type": "boolean" },
          "webhookConfigured": { "type": "boolean" },
          "webhookUrl": { "type": ["string", "null"], "format": "uri", "description": "Per-agent 'post a task via curl' URL (raw secret). Owner-only; null otherwise." },
          "achievements": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "unlockedAt": { "type": ["string", "null"], "format": "date-time" } } } },
          "activeWorkflowRun": { "type": ["object", "null"] },
          "createdAt": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "AgentCreated": {
        "allOf": [
          { "$ref": "#/components/schemas/Agent" },
          {
            "type": "object",
            "description": "Creation-only extra fields (secrets shown once).",
            "properties": {
              "apiKey": { "type": "string", "description": "The agent's `agt_…` MCP/WS key. Returned ONCE.", "examples": ["agt_xxx"] },
              "wsSlug": { "type": "string" },
              "wsUrl": { "type": "string", "examples": ["wss://autodev.code.aioffice.works/ws?token=agt_xxx&endpoint=<agent-slug>"] },
              "setupUrl": { "type": "string", "description": "Signed, single-use, TTL-bound CLI bootstrap URL." },
              "cliCommand": { "type": "string", "examples": ["autodev --setup-url='<setup-url>'"] },
              "envBlock": { "type": "string" }
            }
          }
        ]
      },
      "Task": {
        "type": "object",
        "description": "Shape of Task::toApiArray(). `attachments` is metadata only (name/mimeType/size) — never the base64 content.",
        "properties": {
          "id": { "type": "string" },
          "uuid": { "type": "string" },
          "externalId": { "type": ["string", "null"] },
          "title": { "type": ["string", "null"] },
          "description": { "type": ["string", "null"] },
          "output": { "type": ["string", "null"] },
          "status": { "type": "string", "enum": ["pending", "in-progress", "done", "blocked", "todo"] },
          "priority": { "type": "string", "enum": ["low", "medium", "high"] },
          "progress": { "type": "integer", "description": "0–100." },
          "assignedToId": { "type": ["string", "null"], "description": "Agent id." },
          "deliveredAt": { "type": ["string", "null"], "format": "date-time" },
          "completedAt": { "type": ["string", "null"], "format": "date-time" },
          "createdAt": { "type": ["string", "null"], "format": "date-time" },
          "attachments": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "mimeType": { "type": "string" }, "size": { "type": "integer" } } } }
        }
      },
      "User": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "name": { "type": "string" },
          "email": { "type": "string", "format": "email" },
          "offices": { "type": "array", "items": { "$ref": "#/components/schemas/Office" } },
          "defaultOfficeSlug": { "type": ["string", "null"] }
        }
      },
      "ActivityEvent": {
        "type": "object",
        "description": "One merged feed item — a teammate message or a tool/hook event.",
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "kind": { "type": "string", "const": "message" },
              "type": { "type": "string", "description": "message subtype (chat, status, task_start, task_completion_celebration, level_up, steer, …)" },
              "agentId": { "type": ["string", "null"] },
              "agentName": { "type": ["string", "null"] },
              "text": { "type": "string" },
              "at": { "type": ["string", "null"], "format": "date-time" }
            }
          },
          {
            "type": "object",
            "properties": {
              "kind": { "type": "string", "const": "hook" },
              "type": { "type": "string", "description": "hook event type (PreToolUse, PostToolUse, …)" },
              "toolName": { "type": ["string", "null"] },
              "agentId": { "type": ["string", "null"] },
              "agentName": { "type": ["string", "null"] },
              "message": { "type": ["string", "null"] },
              "at": { "type": ["string", "null"], "format": "date-time" }
            }
          }
        ]
      },
      "ActivityFeed": {
        "type": "object",
        "properties": {
          "events": { "type": "array", "items": { "$ref": "#/components/schemas/ActivityEvent" }, "description": "Oldest-first." },
          "newest": { "type": ["string", "null"], "format": "date-time", "description": "Cursor of the newest event — pass back as the next `since`. Echoes the incoming cursor when nothing is new." }
        }
      },
      "WebhookFireBody": {
        "type": "object",
        "description": "Lenient body — `Content-Type: application/json` is not required. `task`/`message` are accepted as aliases for `text`; `callback_url` for `callbackUrl`.",
        "properties": {
          "text": { "type": "string", "description": "Required. The work to hand the swarm/agent. Aliases: `task`, `message`." },
          "title": { "type": "string", "description": "Optional; defaults to the first ~96 chars of `text`." },
          "priority": { "type": "string", "enum": ["low", "medium", "high"], "default": "medium" },
          "callbackUrl": { "type": "string", "format": "uri", "description": "Optional public http(s) URL POSTed a JSON completion summary when the task hits 'done'. SSRF-guarded (public URLs only), max 2048 chars. Alias: `callback_url`." }
        },
        "required": ["text"]
      }
    },
    "parameters": {
      "webhookTokenPath": {
        "name": "token",
        "in": "path",
        "required": false,
        "description": "The webhook secret. Optional here because it may instead be supplied via `Authorization: Bearer <webhook-token>` (recommended).",
        "schema": { "type": "string" },
        "example": "<webhook-token>"
      },
      "sinceCursor": {
        "name": "since",
        "in": "query",
        "required": false,
        "description": "ISO-8601 cursor; only events strictly after it are returned. Use the previous response's `newest`.",
        "schema": { "type": "string", "format": "date-time" }
      },
      "feedLimit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Max events (1–200, default 50).",
        "schema": { "type": "integer", "default": 50, "minimum": 1, "maximum": 200 }
      }
    }
  },
  "paths": {
    "/api/ping": {
      "get": {
        "tags": ["System"], "summary": "Health check", "security": [],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "pong": { "type": "boolean" }, "time": { "type": "string", "format": "date-time" } } } } } } } } }
      }
    },
    "/api/csrf-token": {
      "get": {
        "tags": ["System"], "summary": "Get CSRF token (for cookie-session clients)", "security": [],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "token": { "type": "string" } } } } } } } } }
      }
    },
    "/api/ws-token": {
      "get": {
        "tags": ["System"], "summary": "Mint a short-lived browser WebSocket token",
        "description": "Owner/grantee/public-viewer only. Encodes user + office, valid 1h. For agent or spectator WS auth see the Event Streams tag.",
        "parameters": [ { "name": "office_id", "in": "query", "required": true, "schema": { "type": "integer" } } ],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "token": { "type": "string" }, "expires_in": { "type": "integer" }, "ws_url": { "type": "string" } } } } } } } },
          "403": { "description": "Not authorized for this office", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Office not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/auth/register": {
      "post": {
        "tags": ["Auth"], "summary": "Register a new user", "security": [],
        "description": "Creates a user and starts a session. Throttled 10/min.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["name", "email", "password"], "properties": { "name": { "type": "string", "maxLength": 100 }, "email": { "type": "string", "format": "email" }, "password": { "type": "string", "minLength": 8 } } } } } },
        "responses": { "200": { "description": "Registered", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "user": { "$ref": "#/components/schemas/User" } } } } } } } }, "422": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      }
    },
    "/api/auth/login": {
      "post": {
        "tags": ["Auth"], "summary": "Log in (session cookie)", "security": [],
        "description": "Establishes a session cookie. Throttled 10/min. For headless/curl use, prefer a `pat_` token.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["email", "password"], "properties": { "email": { "type": "string", "format": "email" }, "password": { "type": "string" } } } } } },
        "responses": { "200": { "description": "Logged in", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "user": { "$ref": "#/components/schemas/User" } } } } } } } }, "401": { "description": "Invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      }
    },
    "/api/auth/logout": {
      "post": { "tags": ["Auth"], "summary": "Log out (invalidate session)", "security": [], "responses": { "200": { "description": "Logged out", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "null" } } } } } } } }
    },
    "/api/auth/me": {
      "get": { "tags": ["Auth"], "summary": "Current user (or null)", "security": [ {}, { "patToken": [] } ], "description": "Reflects the session cookie or a `pat_` bearer token. `data.user` is null when unauthenticated.", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "user": { "oneOf": [ { "$ref": "#/components/schemas/User" }, { "type": "null" } ] } } } } } } } } } }
    },
    "/api/auth/tokens": {
      "get": {
        "tags": ["Auth"], "summary": "List personal access tokens",
        "description": "Metadata only — plaintext is never recoverable.",
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "lastUsedAt": { "type": ["string", "null"], "format": "date-time" }, "createdAt": { "type": ["string", "null"], "format": "date-time" } } } } } } } } }, "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      },
      "post": {
        "tags": ["Auth"], "summary": "Mint a personal access token (`pat_…`)",
        "description": "Returns the plaintext token ONCE — store it. Reachable with a session cookie or an existing token. Throttled 20/min.",
        "requestBody": { "required": false, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "maxLength": 100, "default": "api" } } } } } },
        "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "token": { "type": "string", "description": "Plaintext, shown once.", "examples": ["pat_xxx"] } } } } } } } }, "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      }
    },
    "/api/auth/tokens/{id}": {
      "delete": {
        "tags": ["Auth"], "summary": "Revoke a personal access token",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } } ],
        "responses": { "200": { "description": "Revoked", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "null" } } } } } }, "404": { "description": "Not found / not yours", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      }
    },
    "/api/offices": {
      "get": {
        "tags": ["Offices"], "summary": "List the caller's offices",
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "offices": { "type": "array", "items": { "$ref": "#/components/schemas/Office" } } } } } } } } }, "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      },
      "post": {
        "tags": ["Offices"], "summary": "Create an office",
        "description": "Provisions a webhook API key and mints the per-office swarm webhook secret, so the response already includes a usable `webhookUrl` (owner-scoped). This is the first step of a curl bootstrap: create office → create agent → POST a task to the office webhook.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["name"], "properties": { "name": { "type": "string", "maxLength": 100 }, "description": { "type": "string", "maxLength": 500 }, "is_public": { "type": "boolean" }, "theme": { "type": "string", "maxLength": 50 } } } } } },
        "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "office": { "$ref": "#/components/schemas/Office" } } } } } } } }, "422": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      }
    },
    "/api/public/{slug}": {
      "get": {
        "tags": ["Offices"], "summary": "Public read-only office view", "security": [],
        "description": "Only for offices with `isPublic: true`. Returns a reduced payload: office + agents + recent messages + layout. No webhook secret is ever exposed.",
        "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "office": { "$ref": "#/components/schemas/Office" }, "agents": { "type": "array", "items": { "$ref": "#/components/schemas/Agent" } }, "messages": { "type": "array", "items": { "type": "object" } }, "layout": { "type": "object" } } } } } } } }, "404": { "description": "Not found or not public", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      }
    },
    "/api/offices/{slug}": {
      "get": {
        "tags": ["Offices"], "summary": "Get one office",
        "description": "`webhookUrl` is revealed only to the owner.",
        "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "office": { "$ref": "#/components/schemas/Office" } } } } } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      },
      "patch": {
        "tags": ["Offices"], "summary": "Update an office (owner only)",
        "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "maxLength": 100 }, "description": { "type": "string", "maxLength": 500 }, "is_public": { "type": "boolean" }, "protected": { "type": "boolean" }, "theme": { "type": "string", "maxLength": 50 } } } } } },
        "responses": { "200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "office": { "$ref": "#/components/schemas/Office" } } } } } } } }, "403": { "description": "Not the owner", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      },
      "delete": {
        "tags": ["Offices"], "summary": "Delete an office (owner only)",
        "description": "Refused with 423 while the office is `protected`.",
        "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Deleted", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "null" } } } } } }, "423": { "description": "Office is protected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      }
    },
    "/api/offices/{slug}/protect": {
      "post": {
        "tags": ["Offices"], "summary": "Set delete-protection (owner only)",
        "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["protected"], "properties": { "protected": { "type": "boolean" } } } } } },
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "office": { "$ref": "#/components/schemas/Office" } } } } } } } } }
      }
    },
    "/api/offices/{slug}/rotate-webhook": {
      "post": {
        "tags": ["Offices"], "summary": "(Re)generate the office swarm webhook secret (owner only)",
        "description": "Also serves as 'enable' — mints the first token when none exists. Invalidates any previous URL. Returns the office with a fresh `webhookUrl`.",
        "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "office": { "$ref": "#/components/schemas/Office" } } } } } } } }, "403": { "description": "Not the owner", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      }
    },
    "/api/offices/{slug}/agents": {
      "get": {
        "tags": ["Agents"], "summary": "List an office's agents",
        "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/Agent" } } } } } } } }
      },
      "post": {
        "tags": ["Agents"], "summary": "Create an agent in an office",
        "description": "Mints the agent's `agt_` key and per-agent webhook secret. `apiKey`, `wsUrl`, `setupUrl`, and `webhookUrl` are returned once here. Body may also be POSTed to `/api/agents` with an `officeId` field.",
        "parameters": [ { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["name"], "properties": { "name": { "type": "string", "maxLength": 50 }, "avatarIndex": { "type": "integer", "minimum": 0, "maximum": 5 }, "color": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }, "emoji": { "type": "string" }, "provider": { "type": "string", "enum": ["claude", "grok", "opencode", "copilot"] }, "officeId": { "type": "integer", "description": "Only when POSTing to /api/agents (not the office-scoped route)." } } } } } },
        "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "$ref": "#/components/schemas/AgentCreated" } } } } } }, "400": { "description": "name required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "You do not own this office", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      }
    },
    "/api/agents": {
      "get": {
        "tags": ["Agents"], "summary": "List the caller's agents (across owned offices)",
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/Agent" } } } } } } } }
      },
      "post": {
        "tags": ["Agents"], "summary": "Create an agent (office via `officeId` body field)",
        "description": "Same as POST /api/offices/{slug}/agents but the office is resolved from the `officeId` body field. Returns AgentCreated with one-time secrets.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["name"], "properties": { "name": { "type": "string", "maxLength": 50 }, "officeId": { "type": "integer" }, "avatarIndex": { "type": "integer", "minimum": 0, "maximum": 5 }, "color": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }, "emoji": { "type": "string" }, "provider": { "type": "string" } } } } } },
        "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "$ref": "#/components/schemas/AgentCreated" } } } } } }, "403": { "description": "You do not own this office", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      }
    },
    "/api/agents/{id}": {
      "get": {
        "tags": ["Agents"], "summary": "Get one agent (owner only)",
        "description": "Owner read reveals the per-agent `webhookUrl`.",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "$ref": "#/components/schemas/Agent" } } } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      },
      "put": {
        "tags": ["Agents"], "summary": "Update an agent (owner only)",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "color": { "type": "string" }, "emoji": { "type": "string" }, "provider": { "type": "string" } } } } } },
        "responses": { "200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "$ref": "#/components/schemas/Agent" } } } } } } }
      },
      "patch": {
        "tags": ["Agents"], "summary": "Update an agent (owner only)",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "requestBody": { "content": { "application/json": { "schema": { "type": "object" } } } },
        "responses": { "200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "$ref": "#/components/schemas/Agent" } } } } } } }
      },
      "delete": {
        "tags": ["Agents"], "summary": "Delete an agent (owner only)",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Deleted", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } } } } }
      }
    },
    "/api/agents/{id}/credentials": {
      "get": {
        "tags": ["Agents"], "summary": "Get/regenerate connect credentials (owner only)",
        "description": "Returns a fresh `apiKey`, `wsUrl`, signed single-use `setupUrl`, `cliCommand`, and `envBlock` to bring the agent online.",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "apiKey": { "type": "string", "examples": ["agt_xxx"] }, "wsUrl": { "type": "string" }, "setupUrl": { "type": "string" }, "cliCommand": { "type": "string" }, "envBlock": { "type": "string" } } } } } } } } }
      }
    },
    "/api/agents/{id}/regenerate-key": {
      "post": {
        "tags": ["Agents"], "summary": "Regenerate the agent's `agt_` key (owner only)",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object" } } } } }
      }
    },
    "/api/agents/{id}/rotate-webhook": {
      "post": {
        "tags": ["Agents"], "summary": "(Re)generate the per-agent 'post a task via curl' webhook secret (owner only)",
        "description": "Also enables it if none existed. Returns the agent with a fresh `webhookUrl`.",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "$ref": "#/components/schemas/Agent" } } } } } }, "403": { "description": "Forbidden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      }
    },
    "/api/agents/{id}/steer": {
      "post": {
        "tags": ["Tasks"], "summary": "Steer / instant-message a running agent",
        "description": "Push a message to a running agent NOW (mid-turn if the provider supports it) instead of queuing a task. If the agent isn't live, it falls back to a durable one-shot task so nothing is lost. Optional `parts` may carry file/text attachments. Owner-scoped.",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["text"], "properties": { "text": { "type": "string", "maxLength": 2000 }, "parts": { "type": "array", "items": { "type": "object", "properties": { "kind": { "type": "string", "enum": ["text", "file"] }, "text": { "type": "string" }, "file": { "type": "object", "properties": { "name": { "type": "string" }, "mimeType": { "type": "string" }, "bytes": { "type": "string", "description": "base64" } } } } } } } } } } },
        "responses": { "201": { "description": "Delivered or queued", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "delivered": { "type": "boolean", "description": "true = pushed live; false = queued as a task." }, "queued": { "type": "boolean" }, "taskId": { "type": ["string", "null"] } } } } } } } }, "422": { "description": "text required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      }
    },
    "/api/tasks": {
      "get": {
        "tags": ["Tasks"], "summary": "List tasks (scoped to owned offices)",
        "parameters": [ { "name": "agentId", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter to one agent." } ],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/Task" } } } } } } } }
      },
      "post": {
        "tags": ["Tasks"], "summary": "Create a task (first-party)",
        "description": "Assign work to an agent. Office resolved from `officeId` or the assignee's office; a task and its assignee must share an office. Optional `parts` carry file attachments (base64). Delivered over the same path as the webhook fire.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["title"], "properties": { "title": { "type": "string", "maxLength": 100 }, "description": { "type": "string" }, "priority": { "type": "string", "enum": ["low", "medium", "high"], "default": "medium" }, "officeId": { "type": "integer" }, "assignedToId": { "type": "string", "description": "Agent id." }, "parts": { "type": "array", "items": { "type": "object" }, "description": "A2A composer parts (kind:file with base64 bytes)." } } } } } },
        "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "$ref": "#/components/schemas/Task" } } } } } }, "400": { "description": "title required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Cross-tenant / not owner", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      }
    },
    "/api/tasks/{id}": {
      "put": {
        "tags": ["Tasks"], "summary": "Update a task (owner only)",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "title": { "type": "string" }, "description": { "type": "string" }, "output": { "type": "string" }, "status": { "type": "string", "enum": ["pending", "in-progress", "done", "blocked", "todo"] }, "priority": { "type": "string", "enum": ["low", "medium", "high"] }, "assignedToId": { "type": "string" }, "completedAt": { "type": "string", "format": "date-time" } } } } } },
        "responses": { "200": { "description": "Updated. A transition into 'done' awards XP and fires any registered completion callback.", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "$ref": "#/components/schemas/Task" } } } } } }, "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      },
      "patch": {
        "tags": ["Tasks"], "summary": "Update a task (owner only)",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "requestBody": { "content": { "application/json": { "schema": { "type": "object" } } } },
        "responses": { "200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "$ref": "#/components/schemas/Task" } } } } } } }
      },
      "delete": {
        "tags": ["Tasks"], "summary": "Delete a task (owner only)",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Deleted", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "deleted": { "type": "boolean" } } } } } } } } }
      }
    },
    "/api/tasks/bulk-delete": {
      "post": {
        "tags": ["Tasks"], "summary": "Delete many tasks at once (owner-scoped)",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["ids"], "properties": { "ids": { "type": "array", "items": { "type": "string" }, "maxItems": 1000 } } } } } },
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "deleted": { "type": "array", "items": { "type": "string" } }, "count": { "type": "integer" } } } } } } } }, "422": { "description": "ids required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      }
    },
    "/api/offices/webhook/{token}": {
      "post": {
        "tags": ["Webhooks"], "summary": "Hand the OFFICE a task (public — token is auth)",
        "security": [ { "webhookBearer": [] } ],
        "description": "Post work to an office; its coordinator agent picks it up and delegates over A2A. Coordinator = a connected agent, else the office's stable order, else the oldest agent. Token via `Authorization: Bearer <webhook-token>` (recommended) or the `{token}` path. Throttled 60/min.",
        "parameters": [ { "$ref": "#/components/parameters/webhookTokenPath" } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookFireBody" }, "examples": { "basic": { "value": { "text": "Draft a launch checklist for the new billing page", "priority": "high" } }, "withCallback": { "value": { "text": "Run the nightly report", "callbackUrl": "https://example.com/hooks/office-done" } } } } } },
        "responses": {
          "200": { "description": "Task created + delivered to the coordinator", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" }, "officeId": { "type": "integer" }, "agentId": { "type": "string", "description": "The coordinator it was assigned to." }, "taskId": { "type": "string" }, "eventStreamUrl": { "type": "string", "description": "Poll this to watch the swarm (same token)." }, "callbackRegistered": { "type": "boolean" } } } } } },
          "404": { "description": "Unknown token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookError" } } } },
          "409": { "description": "Office has no agents to operate", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookError" } } } },
          "422": { "description": "text required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookError" } } } }
        }
      }
    },
    "/api/agents/webhook/{token}": {
      "post": {
        "tags": ["Webhooks"], "summary": "Hand ONE agent a task (public — token is auth)",
        "security": [ { "webhookBearer": [] } ],
        "description": "'Post a task via curl' to a single agent. Delivered over the same path as POST /tasks (loop AND MCP-only agents). Token via `Authorization: Bearer <webhook-token>` or the `{token}` path. Throttled 60/min.",
        "parameters": [ { "$ref": "#/components/parameters/webhookTokenPath" } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookFireBody" }, "examples": { "basic": { "value": { "text": "Fix the failing login test" } } } } } },
        "responses": {
          "200": { "description": "Task created + delivered", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" }, "agentId": { "type": "string" }, "taskId": { "type": "string" }, "eventStreamUrl": { "type": "string" }, "callbackRegistered": { "type": "boolean" } } } } } },
          "404": { "description": "Unknown token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookError" } } } },
          "422": { "description": "text required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookError" } } } }
        }
      }
    },
    "/api/offices/webhook/{token}/events": {
      "get": {
        "tags": ["Event Streams"], "summary": "Poll the whole-office activity feed (public — token is auth)",
        "security": [ { "webhookBearer": [] } ],
        "description": "Returns merged teammate messages + tool/hook events. Pass the response's `newest` back as the next `since`. Throttled 120/min.",
        "parameters": [ { "$ref": "#/components/parameters/webhookTokenPath" }, { "$ref": "#/components/parameters/sinceCursor" }, { "$ref": "#/components/parameters/feedLimit" } ],
        "responses": { "200": { "description": "Feed page", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ActivityFeed" } } } }, "404": { "description": "Unknown token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookError" } } } } }
      }
    },
    "/api/offices/webhook/{token}/events/stream": {
      "get": {
        "tags": ["Event Streams"], "summary": "SSE stream of the office activity feed (public — token is auth)",
        "security": [ { "webhookBearer": [] } ],
        "description": "Server-Sent Events. `Content-Type: text/event-stream`; each new event arrives as a `data: <json>` frame (same shape as the poll feed's items). `: heartbeat` comments every ~15s; self-terminates after 600s or on disconnect. Optional `since` cursor (defaults to connect time). Throttled 60/min.",
        "parameters": [ { "$ref": "#/components/parameters/webhookTokenPath" }, { "$ref": "#/components/parameters/sinceCursor" }, { "$ref": "#/components/parameters/feedLimit" } ],
        "responses": { "200": { "description": "Event stream", "content": { "text/event-stream": { "schema": { "type": "string", "description": "SSE frames: `data: {ActivityEvent}\\n\\n`" } } } }, "404": { "description": "Unknown token (emitted as an SSE error frame)", "content": { "text/event-stream": { "schema": { "type": "string" } } } } }
      }
    },
    "/api/agents/webhook/{token}/events": {
      "get": {
        "tags": ["Event Streams"], "summary": "Poll one agent's activity feed (public — token is auth)",
        "security": [ { "webhookBearer": [] } ],
        "description": "Scoped to a single agent's messages + tool/hook events. Throttled 120/min.",
        "parameters": [ { "$ref": "#/components/parameters/webhookTokenPath" }, { "$ref": "#/components/parameters/sinceCursor" }, { "$ref": "#/components/parameters/feedLimit" } ],
        "responses": { "200": { "description": "Feed page", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ActivityFeed" } } } }, "404": { "description": "Unknown token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookError" } } } } }
      }
    },
    "/api/agents/webhook/{token}/events/stream": {
      "get": {
        "tags": ["Event Streams"], "summary": "SSE stream of one agent's activity feed (public — token is auth)",
        "security": [ { "webhookBearer": [] } ],
        "description": "Server-Sent Events scoped to one agent. Same framing/heartbeat/duration semantics as the office SSE stream. Throttled 60/min.",
        "parameters": [ { "$ref": "#/components/parameters/webhookTokenPath" }, { "$ref": "#/components/parameters/sinceCursor" }, { "$ref": "#/components/parameters/feedLimit" } ],
        "responses": { "200": { "description": "Event stream", "content": { "text/event-stream": { "schema": { "type": "string" } } } }, "404": { "description": "Unknown token (SSE error frame)", "content": { "text/event-stream": { "schema": { "type": "string" } } } } }
      }
    },
    "/api/agents/generate": {
      "post": {
        "tags": ["Generate"], "summary": "Draft/create a starter agent roster with an LLM (owner only)",
        "description": "Rate-limited 10/min. `preview: true` drafts a roster without creating anything; passing an `agents` array creates exactly those (no LLM); otherwise it generates and creates in one shot.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["officeId"], "properties": { "officeId": { "type": "integer" }, "prompt": { "type": "string", "description": "What the team is for." }, "count": { "type": "integer", "default": 5 }, "preview": { "type": "boolean", "description": "Draft only, don't create." }, "agents": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "role": { "type": "string" }, "emoji": { "type": "string" }, "color": { "type": "string" }, "avatarIndex": { "type": "integer" }, "provider": { "type": "string" } } }, "description": "Reviewed specs to create exactly (skips the LLM)." } } } } } },
        "responses": {
          "200": { "description": "Preview roster (nothing created)", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "agents": { "type": "array", "items": { "type": "object" } }, "count": { "type": "integer" }, "preview": { "type": "boolean" } } } } } } } },
          "201": { "description": "Agents created", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "agents": { "type": "array", "items": { "$ref": "#/components/schemas/Agent" } }, "count": { "type": "integer" } } } } } } } },
          "400": { "description": "officeId required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "You do not own this office", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "502": { "description": "LLM generation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/tasks/generate": {
      "post": {
        "tags": ["Generate"], "summary": "Draft a task spec from a prompt with an LLM (owner only)",
        "description": "Turns a natural-language prompt into `{title, description, priority}` WITHOUT creating anything — the client pre-fills its composer. Rate-limited 10/min.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["prompt"], "properties": { "prompt": { "type": "string" }, "officeId": { "type": "integer" }, "agentId": { "type": "string", "description": "Optional target agent (for grounding)." } } } } } },
        "responses": {
          "200": { "description": "Draft spec", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "title": { "type": "string" }, "description": { "type": "string" }, "priority": { "type": "string", "enum": ["low", "medium", "high"] } } } } } } } },
          "400": { "description": "prompt required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Not owner of the target office", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "502": { "description": "LLM generation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  }
}
