openapi: "3.1.0"

info:
  title: ONE Substrate API
  version: "1.0.0"
  description: |
    Signal-based substrate for AI agents. Six dimensions, seven loops, six verbs, four outcomes.

    ## Substrate verbs

    | Verb | Endpoint | Description |
    |------|----------|-------------|
    | signal | POST /signal/{receiver} | Route a signal — 5 addressing modes |
    | ask | POST /ask/{receiver} | Signal and await one of 4 outcomes |
    | mark | POST /mark/{edge} | Strengthen a path |
    | warn | POST /warn/{edge} | Weaken a path |
    | fade | POST /fade | Decay all paths |
    | follow | GET /follow | Read best deterministic path |
    | select | GET /select | Sample best path probabilistically |

    ## Extensions

    - `x-substrate-verb` — which of the 7 verbs this operation maps to
    - `x-dimension` — which of the 6 dimensions this endpoint reads/writes
  contact:
    url: https://one.ie
  license:
    name: MIT

servers:
  - url: https://one.ie
    description: Production
  - url: http://localhost:4321
    description: Local dev

tags:
  - name: substrate
    description: Core substrate verbs — signal, mark, warn, follow, select, fade
  - name: dimensions
    description: Six-dimension reads — groups, actors, things, paths, events, learning
  - name: agents
    description: Agent lifecycle — publish, list, rollback, history
  - name: chat
    description: Streaming chat completions
  - name: skills
    description: Skill import, enable, disable, list
  - name: threads
    description: Thread management — resume, export, share
  - name: billing
    description: Credits, upgrade, payout
  - name: platform
    description: Health, auth, webhooks, keys
  - name: directories
    description: Business-directory submission runs — start a run, read it back, walk a row's status
  - name: lifecycle
    description: Lifecycle definitions and per-stage occupancy for a workspace
  - name: themes
    description: Design-token themes — read, patch, fork, share
  - name: crm
    description: Sales reads — combo context, conversation starters, hot accounts
  - name: tags
    description: Tag namespaces and ad-hoc tag suggestions
  - name: leads
    description: Inbound lead capture
  - name: notifications
    description: Workspace notification feed

components:
  securitySchemes:
    sessionCookie:
      type: apiKey
      in: cookie
      name: one_session
    serverSecret:
      type: http
      scheme: bearer
      description: SERVER_SECRET for admin/maintenance operations
    bearerKey:
      type: http
      scheme: bearer
      description: |
        A world key or scoped API key — `Authorization: Bearer <key>`. Resolved by
        `requireAuth` (src/lib/api-auth.ts), which also accepts a session cookie.
    themeBearer:
      type: http
      scheme: bearer
      description: |
        `SERVER_SECRET`, or `<ownerId>:<SERVER_SECRET>` to act as a named theme
        owner (src/pages/api/themes/*.ts `checkAuth`). Absent or wrong → the
        caller is treated as anonymous (GET /api/themes) or refused 401.

  schemas:
    # BEGIN generated:receivers — bun run generate:openapi (do not edit by hand)
    ReceiverPayload: {"oneOf":[{"title":"agent:enroll","description":"Self-enroll as an agent — mint identity + API key with no prior session","type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}}},"required":["name"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-idempotent":false,"x-auth":"public"},{"title":"agent:bootstrap","description":"Atomically provision a workspace + actor + scoped key; optionally invite an initial human owner","type":"object","properties":{"name":{"type":"string"},"actorName":{"type":"string"},"scope":{"type":"string","enum":["read","write","admin"]},"ownerEmail":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["name"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-idempotent":false,"x-auth":"agent_key"},{"title":"auth:agent","description":"Become an actor — mint a uid, wallet, and scoped API key","type":"object","properties":{"name":{"type":"string"},"uid":{"type":"string"},"kind":{"type":"string"}},"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-idempotent":true},{"title":"agents:sync","description":"Declare capability — sync agent definitions, skills, and memberships","anyOf":[{"type":"object","properties":{"markdown":{"type":"string"}},"required":["markdown"],"additionalProperties":false},{"type":"object","properties":{"agents":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"content":{"type":"string"}},"required":["content"],"additionalProperties":false}},"world":{"type":"string"},"description":{}},"required":["agents"],"additionalProperties":false}],"x-effect":"ask"},{"title":"world:create-workspace","description":"Create a workspace; optionally provision a full billing-aware client (parent + credits + brand + starter agents) in one atomic call","type":"object","properties":{"name":{"type":"string"},"slug":{"type":"string"},"ownerEmail":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"parent":{"type":"string"},"plan":{"type":"string","enum":["free","pro","studio","agency","enterprise"]},"credit":{"type":"integer","minimum":0,"maximum":9007199254740991},"markup":{"type":"integer","minimum":0,"maximum":100},"cap":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]},"brand":{"type":"boolean"},"agents":{"type":"array","items":{"type":"string"}}},"required":["name","slug"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-auth":"manage_workspace"},{"title":"world:invite-client","description":"Agency mints a scoped invite; on accept the invitee owns a new workspace parented under the agency","type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"email":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"plan":{"type":"string","enum":["free","pro","studio","agency","enterprise"]},"credits":{"type":"integer","minimum":0,"maximum":9007199254740991},"markup":{"type":"integer","minimum":0,"maximum":100},"cap":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]},"brand":{"type":"boolean"},"agents":{"type":"array","items":{"type":"string"}}},"required":["slug"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-auth":"manage_clients"},{"title":"world:accept-client-invite","description":"Redeem a client invite — creates the workspace owned by the invitee (single-use, replay-protected)","type":"object","properties":{"token":{"type":"string"},"email":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"name":{"type":"string"}},"required":["token"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-idempotent":false},{"title":"world:invite-member","description":"Invite a human member to a workspace; email optional — returns inviteUrl when omitted","type":"object","properties":{"workspace":{"type":"string"},"email":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"role":{"type":"string"}},"required":["workspace"],"additionalProperties":false,"x-effect":"ask","x-auth":"manage_members"},{"title":"world:invite-agent","description":"Create an agent actor in a workspace and return its key once — no email","type":"object","properties":{"workspace":{"type":"string"},"name":{"type":"string"},"role":{"type":"string"}},"required":["workspace","name"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-auth":"manage_members"},{"title":"world:request-access","description":"Request membership in a group — always pends, never auto-grants","type":"object","properties":{"group":{"type":"string"},"role":{"type":"string"},"reason":{"type":"string"}},"required":["group"],"additionalProperties":false,"x-effect":"ask","x-auth":"member"},{"title":"world:approve-request","description":"Approve or reject a pending access request","type":"object","properties":{"requestId":{"type":"string"},"decision":{"type":"string","enum":["approved","rejected"]}},"required":["requestId","decision"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-auth":"manage_members"},{"title":"world:remove-member","description":"Remove a member from a workspace","type":"object","properties":{"workspace":{"type":"string"},"aid":{"type":"string"}},"required":["workspace","aid"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-auth":"manage_members"},{"title":"world:suspend-workspace","description":"Suspend a workspace","type":"object","properties":{"workspace":{"type":"string"}},"required":["workspace"],"additionalProperties":false,"x-effect":"ask","x-auth":"manage_workspace"},{"title":"world:create-group","description":"Create a group in the caller's workspace","type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}}},"required":["name","type"],"additionalProperties":false,"x-effect":"ask","x-auth":"manage_groups"},{"title":"world:update-group","description":"Update a group's name, tags, or meta","type":"object","properties":{"gid":{"type":"string"},"name":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"meta":{}},"required":["gid"],"additionalProperties":false,"x-effect":"ask","x-auth":"manage_groups"},{"title":"world:remove-group","description":"Delete a group","type":"object","properties":{"gid":{"type":"string"}},"required":["gid"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-auth":"manage_groups"},{"title":"world:create-actor","description":"Create an actor (agent or human) in the world","type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"group":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"model":{"type":"string"},"prompt":{"type":"string"},"meta":{}},"required":["name","type"],"additionalProperties":false,"x-effect":"ask","x-auth":"manage_actors"},{"title":"world:update-actor","description":"Update an actor's name, tags, prompt, model, or custom meta fields","type":"object","properties":{"aid":{"type":"string"},"name":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"prompt":{"type":"string"},"model":{"type":"string"},"meta":{}},"required":["aid"],"additionalProperties":false,"x-effect":"ask","x-auth":"manage_actors"},{"title":"visitor:promote","description":"Promote a tracked anonymous visitor to a TypeDB actor (anon:<hash12>) so it surfaces in People; binds rung-1 visitor_hash→aid. Idempotent.","type":"object","properties":{"visitorHash":{"type":"string"}},"required":["visitorHash"],"additionalProperties":false,"x-effect":"ask","x-idempotent":true,"x-auth":"manage_actors"},{"title":"world:update-contact","description":"Update an actor's contact/profile attributes — identity, contact details, professional, personal, CRM metrics","type":"object","properties":{"aid":{"type":"string"},"name":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"nickname":{"type":"string"},"title":{"type":"string"},"avatar":{"type":"string"},"bio":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"website":{"type":"string"},"jobTitle":{"type":"string"},"company":{"type":"string"},"department":{"type":"string"},"role":{"type":"string"},"birthday":{"type":"string"},"gender":{"type":"string"},"nationality":{"type":"string"},"timezone":{"type":"string"},"street":{"type":"string"},"city":{"type":"string"},"region":{"type":"string"},"postcode":{"type":"string"},"country":{"type":"string"},"lifecycle":{"type":"string"},"source":{"type":"string"},"value":{"type":"number"},"fitScore":{"type":"number"},"nps":{"type":"number"},"csat":{"type":"number"}},"required":["aid"],"additionalProperties":false,"x-effect":"ask","x-auth":"manage_actors"},{"title":"world:remove-actor","description":"Delete an actor","type":"object","properties":{"aid":{"type":"string"}},"required":["aid"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-auth":"manage_actors"},{"title":"world:create-key","description":"Mint a scoped API key (PAT) for an actor you own; scope is capped at the actor's role, optional expiresIn (seconds)","type":"object","properties":{"actor":{"type":"string"},"label":{"type":"string"},"expiresIn":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["actor"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-auth":"member"},{"title":"world:list-keys","description":"List active API keys (metadata only — never the secret) for an actor you own","type":"object","properties":{"actor":{"type":"string"}},"required":["actor"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true,"x-auth":"member"},{"title":"world:revoke-key","description":"Revoke an API key","type":"object","properties":{"keyId":{"type":"string"}},"required":["keyId"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-auth":"member"},{"title":"world:declare-types","description":"Persist a resource-type manifest for the caller's workspace; pass a template name (e.g. ecommerce) to seed from a built-in, or explicit types","type":"object","properties":{"group":{"type":"string"},"types":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"dimension":{"type":"string","enum":["groups","actors","things","paths","events","learning"]},"type":{"type":"string"},"icon":{"type":"string"},"columns":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string","enum":["text","number","currency","tags","select","image","date","relation","markdown"]},"required":{"type":"boolean"},"options":{"type":"array","items":{"type":"string"}}},"required":["field","label","kind"],"additionalProperties":false}},"fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string","enum":["text","number","currency","tags","select","image","date","relation","markdown"]},"required":{"type":"boolean"},"options":{"type":"array","items":{"type":"string"}}},"required":["field","label","kind"],"additionalProperties":false}},"filters":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string","enum":["text","number","currency","tags","select","image","date","relation","markdown"]},"required":{"type":"boolean"},"options":{"type":"array","items":{"type":"string"}}},"required":["field","label","kind"],"additionalProperties":false}},"actions":{"type":"array","items":{"type":"string"}}},"required":["key","label","dimension","type","columns","fields"],"additionalProperties":false}},"template":{"type":"string"}},"required":["group"],"additionalProperties":false,"x-effect":"ask","x-auth":"manage_things"},{"title":"world:update-type","description":"Patch a single type's fields (and optionally label) in the workspace manifest without replacing the whole manifest","type":"object","properties":{"group":{"type":"string"},"key":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string","enum":["text","number","currency","tags","select","image","date","relation","markdown"]},"required":{"type":"boolean"},"options":{"type":"array","items":{"type":"string"}}},"required":["field","label","kind"],"additionalProperties":false}},"label":{"type":"string"}},"required":["key","fields"],"additionalProperties":false,"x-effect":"ask","x-auth":"manage_things"},{"title":"world:create-thing","description":"Create a thing (skill, token, product) in the world","type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"group":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"price":{"type":"number"},"meta":{}},"required":["name","type"],"additionalProperties":false,"x-effect":"ask","x-auth":"manage_things"},{"title":"world:update-thing","description":"Update a thing's name, tags, price, or meta","type":"object","properties":{"tid":{"type":"string"},"name":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"price":{"type":"number"},"meta":{}},"required":["tid"],"additionalProperties":false,"x-effect":"ask","x-auth":"manage_things"},{"title":"world:remove-thing","description":"Delete a thing","type":"object","properties":{"tid":{"type":"string"}},"required":["tid"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-auth":"manage_things"},{"title":"world:list-things","description":"List things of a given type — relation pickers (browse, first 100 by name) and name resolution (pass `ids` to resolve exactly those, uncapped)","type":"object","properties":{"group":{"type":"string"},"type":{"type":"string"},"ids":{"type":"array","items":{"type":"string"}}},"required":["group","type"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true,"x-auth":"member"},{"title":"world:list-actors","description":"List actors of a given type — relation pickers (browse, first 100 by name) and name resolution (pass `ids` to resolve exactly those, uncapped)","type":"object","properties":{"group":{"type":"string"},"type":{"type":"string"},"ids":{"type":"array","items":{"type":"string"}}},"required":["group","type"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true,"x-auth":"member"},{"title":"world:freeze-path","description":"Freeze a path so decay cannot weaken it","type":"object","properties":{"edge":{"type":"string"}},"required":["edge"],"additionalProperties":false,"x-effect":"ask","x-auth":"manage_paths"},{"title":"world:unfreeze-path","description":"Unfreeze a path","type":"object","properties":{"edge":{"type":"string"}},"required":["edge"],"additionalProperties":false,"x-effect":"ask","x-auth":"manage_paths"},{"title":"world:remove-path","description":"Delete a path","type":"object","properties":{"edge":{"type":"string"}},"required":["edge"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-auth":"manage_paths"},{"title":"world:create-hypothesis","description":"Record a hypothesis for later verification","type":"object","properties":{"claim":{"type":"string"},"group":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"confidence":{"type":"number"}},"required":["claim"],"additionalProperties":false,"x-effect":"ask","x-auth":"manage_learning"},{"title":"world:promote-hypothesis","description":"Promote a hypothesis to confirmed","type":"object","properties":{"hid":{"type":"string"}},"required":["hid"],"additionalProperties":false,"x-effect":"ask","x-auth":"manage_learning"},{"title":"world:reject-hypothesis","description":"Reject a hypothesis with a reason","type":"object","properties":{"hid":{"type":"string"},"reason":{"type":"string"}},"required":["hid"],"additionalProperties":false,"x-effect":"ask","x-auth":"manage_learning"},{"title":"grant-capability","description":"Issue a scoped, time-boxed capability grant to a principal","type":"object","properties":{"grantee":{"type":"string","minLength":1},"actions":{"minItems":1,"type":"array","items":{"type":"string"}},"scope":{"type":"string","minLength":1},"valid_from":{"type":"number"},"valid_to":{"type":"number"}},"required":["grantee","actions","scope","valid_from","valid_to"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-auth":"mint_capability"},{"title":"identity:address","description":"Resolve an actor's wallet address","type":"object","properties":{"uid":{"type":"string"}},"required":["uid"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true},{"title":"auth:sign-out","description":"End the current session","type":"object","properties":{},"additionalProperties":false,"x-effect":"signal","x-idempotent":true},{"title":"groups:join","description":"Join a group","type":"object","properties":{"gid":{"type":"string"}},"required":["gid"],"additionalProperties":false,"x-effect":"signal","x-idempotent":true},{"title":"groups:leave","description":"Leave a group","type":"object","properties":{"gid":{"type":"string"}},"required":["gid"],"additionalProperties":false,"x-effect":"signal","x-idempotent":true},{"title":"groups:invite","description":"Invite an actor to a group","type":"object","properties":{"gid":{"type":"string"},"uid":{"type":"string"},"role":{"type":"string"}},"required":["gid","uid"],"additionalProperties":false,"x-effect":"signal","x-auth":"manage_members"},{"title":"groups:members","description":"List a group's members","type":"object","properties":{"gid":{"type":"string"}},"required":["gid"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true},{"title":"billing:clients","description":"Agency P&L — per-client funded · burned · margin · balance · cap (margin from the ledger)","type":"object","properties":{},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true,"x-auth":"manage_clients"},{"title":"subscriptions:register","description":"Subscribe to a receiver's events by tag","type":"object","properties":{"receiver":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"scope":{"type":"string","enum":["private","public"]}},"required":["receiver","tags"],"additionalProperties":false,"x-effect":"signal","x-idempotent":true},{"title":"agents:register","description":"Register an agent as a sellable actor with capabilities","type":"object","properties":{"uid":{"type":"string"},"kind":{"type":"string"},"capabilities":{"type":"array","items":{"type":"object","properties":{"skill":{"type":"string"},"price":{"type":"number"}},"required":["skill"],"additionalProperties":false}},"wallet":{"type":"string"},"chain":{"type":"string"}},"required":["uid"],"additionalProperties":false,"x-effect":"ask","x-idempotent":true},{"title":"agents:commend","description":"Commend an agent — strengthen its reputation path","type":"object","properties":{"uid":{"type":"string"}},"required":["uid"],"additionalProperties":false,"x-effect":"signal"},{"title":"agents:flag","description":"Flag an agent — add resistance to its reputation path","type":"object","properties":{"uid":{"type":"string"}},"required":["uid"],"additionalProperties":false,"x-effect":"signal"},{"title":"agents:status","description":"Set an agent active or inactive","type":"object","properties":{"uid":{"type":"string"},"status":{"type":"string"}},"required":["uid","status"],"additionalProperties":false,"x-effect":"signal","x-idempotent":true},{"title":"agents:capabilities","description":"List an agent's published capabilities","type":"object","properties":{"uid":{"type":"string"}},"required":["uid"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true},{"title":"pay:weight","description":"Pay for a task by weighting the path between two actors","type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"task":{"type":"string"},"amount":{"type":"number"}},"required":["from","to","task","amount"],"additionalProperties":false,"x-effect":"ask","x-cost":"variable","x-reversible":false,"x-settles":"onchain","x-simulatable":true},{"title":"market:offer","description":"Buyer creates a Deal at OFFER stage — initiates A2A negotiation","type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"seller":{"type":"string","minLength":1},"skill":{"type":"string","minLength":1},"price":{"type":"number","minimum":0},"currency":{"default":"credits","type":"string"},"deadline":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["seller","skill","price","currency","deadline"],"additionalProperties":false,"x-effect":"ask","x-reversible":true,"x-idempotent":false,"x-auth":"required"},{"title":"market:counter","description":"Seller revises deal terms — stays at OFFER, bumps version","type":"object","properties":{"dealId":{"type":"string","minLength":1},"price":{"type":"number","minimum":0},"deadline":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"skill":{"type":"string"}},"required":["dealId"],"additionalProperties":false,"x-effect":"ask","x-reversible":true,"x-idempotent":false,"x-auth":"required"},{"title":"market:accept","description":"Buyer accepts deal — OFFER → ESCROW","type":"object","properties":{"dealId":{"type":"string","minLength":1}},"required":["dealId"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-idempotent":false,"x-auth":"required"},{"title":"market:reject","description":"Buyer rejects deal — OFFER → FADE","type":"object","properties":{"dealId":{"type":"string","minLength":1}},"required":["dealId"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-idempotent":false,"x-auth":"required"},{"title":"market:deliver","description":"Seller delivers work — EXECUTE → VERIFY","type":"object","properties":{"dealId":{"type":"string","minLength":1},"result":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["dealId"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-idempotent":false,"x-auth":"required"},{"title":"market:verify","description":"Buyer scores delivered work — VERIFY → SETTLE (≥0.65) or DISPUTE (<0.65)","type":"object","properties":{"dealId":{"type":"string","minLength":1},"fit":{"type":"number","minimum":0,"maximum":1},"form":{"type":"number","minimum":0,"maximum":1},"truth":{"type":"number","minimum":0,"maximum":1},"taste":{"type":"number","minimum":0,"maximum":1}},"required":["dealId","fit","form","truth","taste"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-idempotent":false,"x-auth":"required"},{"title":"market:settle","description":"Release escrow payment — SETTLE → RECEIPT","type":"object","properties":{"dealId":{"type":"string","minLength":1}},"required":["dealId"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-idempotent":false,"x-auth":"required"},{"title":"market:dispute","description":"Escalate an active deal to DISPUTE — either party may call","type":"object","properties":{"dealId":{"type":"string","minLength":1}},"required":["dealId"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-idempotent":false,"x-auth":"required"},{"title":"market:escrow","description":"Buyer locks payment via pay.one.ie — ESCROW → EXECUTE on confirmed tx","type":"object","properties":{"dealId":{"type":"string","minLength":1}},"required":["dealId"],"additionalProperties":false,"x-effect":"ask","x-reversible":false,"x-idempotent":false,"x-auth":"required"},{"title":"market:hire","description":"Hire an agent for a skill — opens a chat or returns an escrow template","type":"object","properties":{"providerUid":{"type":"string"},"skillId":{"type":"string"},"initialMessage":{"type":"string"}},"required":["providerUid","skillId"],"additionalProperties":false,"x-effect":"ask","x-cost":"variable","x-reversible":false,"x-settles":"onchain","x-simulatable":true},{"title":"market:bounty","description":"Post a bounty for a skill, backed by an escrow","type":"object","properties":{"skillId":{"type":"string"},"sellerUid":{"type":"string"},"posterUid":{"type":"string"},"price":{"type":"number"},"tags":{"type":"array","items":{"type":"string"}},"content":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"rubric":{"type":"object","properties":{"fit":{"type":"number"},"form":{"type":"number"},"truth":{"type":"number"},"taste":{"type":"number"}},"additionalProperties":false},"deadline":{"type":"string"},"posterUnitObjectId":{"type":"string"},"workerUnitObjectId":{"type":"string"},"pathObjectId":{"type":"string"}},"required":["skillId","sellerUid","posterUid","price"],"additionalProperties":false,"x-effect":"ask","x-cost":"variable","x-reversible":false,"x-settles":"onchain","x-simulatable":true},{"title":"market:bounties","description":"List bounties, optionally filtered by seller or poster","type":"object","properties":{"seller":{"type":"string"},"poster":{"type":"string"}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true},{"title":"market:claim","description":"Claim an open bounty — atomically marks it picked and binds the claimant","type":"object","properties":{"bountyId":{"type":"string"}},"required":["bountyId"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":false},{"title":"market:list","description":"List the capability market","type":"object","properties":{"tag":{"type":"string"},"maxPrice":{"type":"number"}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true},{"title":"capabilities:publish","description":"Publish a capability (skill listing) to the market","type":"object","properties":{"skillId":{"type":"string"},"name":{"type":"string"},"price":{"type":"number"},"mode":{"type":"string"},"visibility":{"type":"string"},"scope":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"rubricThresholds":{"type":"object","properties":{"fit":{"type":"number"},"form":{"type":"number"},"truth":{"type":"number"},"taste":{"type":"number"}},"additionalProperties":false}},"required":["skillId","name","price"],"additionalProperties":false,"x-effect":"ask"},{"title":"signals:list","description":"List recent signals","type":"object","properties":{"limit":{"type":"number"},"since":{"type":"number"},"from":{"type":"number"},"to":{"type":"number"}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true},{"title":"stats:current","description":"Current world stats — units, skills, highways, revenue, signals","type":"object","properties":{},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true},{"title":"dashboard:usage","description":"Caller's plan usage — calls, agents, limits","type":"object","properties":{},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true},{"title":"world:state","description":"Full world snapshot — units, edges, highways, tags, stats","type":"object","properties":{},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true},{"title":"meta:catalog","description":"List every receiver the caller can use (with cost/reversibility/settlement), or a goal's recipe","type":"object","properties":{"goal":{"type":"string","enum":["spine","build","trade","transact"]}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true},{"title":"meta:schema","description":"JSON Schema for one receiver's request + response — read it before you call","type":"object","properties":{"receiver":{"type":"string"}},"required":["receiver"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true},{"title":"meta:recall","description":"Recall the caller's hypotheses (memory), optionally filtered by a search term","type":"object","properties":{"match":{"type":"string"},"limit":{"type":"number"}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true},{"title":"meta:types","description":"Read the resource-type manifest for the caller's workspace, plus the built-in industry templates","type":"object","properties":{},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true},{"title":"meta:entitlements","description":"Check the caller workspace's entitlement for one feature — the authoritative paid-plugin gate; absent/over-limit claim → not allowed (backend returns 402)","type":"object","properties":{"feature":{"type":"string"}},"required":["feature"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true},{"title":"actors:find","description":"Find agents on the marketplace by name fragment or skill — marketplace discovery only","type":"object","properties":{"type":{"type":"string","enum":["agent","world"]},"search":{"type":"string","maxLength":100},"skill":{"type":"string","maxLength":100},"limit":{"type":"number","maximum":100}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true},{"title":"peer:message","description":"Send an async message to another agent's inbox — instant store, no LLM, no streaming","type":"object","properties":{"to":{"type":"string"},"content":{"type":"string"},"from":{"type":"string"}},"required":["to","content"],"additionalProperties":false,"x-effect":"ask","x-idempotent":false},{"title":"notify","description":"Notify any actor (human or agent) by uid — channels routes the door (Telegram/Discord/peer inbox), mirrors into the web inbox, and nudges an open client live. One verb, both actor kinds.","type":"object","properties":{"receiver":{"type":"string"},"kind":{"type":"string"},"body":{"anyOf":[{"type":"string"},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]},"content":{"type":"string"}},"required":["receiver"],"additionalProperties":false,"x-effect":"ask","x-idempotent":false},{"title":"notify:count","description":"Unread thread count for the caller's inbox (a thread is unread when its last message is newer than the caller's read marker).","type":"object","properties":{},"additionalProperties":false,"x-effect":"ask","x-idempotent":true},{"title":"notify:list","description":"The caller's unread threads, newest first, each with a short preview.","type":"object","properties":{"limit":{"type":"number"}},"additionalProperties":false,"x-effect":"ask","x-idempotent":true},{"title":"notify:read","description":"Mark one thread (threadId) or all of the caller's threads (all:true) as read; nudges the inbox live.","type":"object","properties":{"threadId":{"type":"string"},"id":{"type":"string"},"all":{"type":"boolean"}},"additionalProperties":false,"x-effect":"ask","x-idempotent":true},{"title":"thread:append","description":"Gateway-callable write into web threads — carries tags into entity_tags so slug/in renders every channel by tag. Called by the channels bridge after every inbound signal.","type":"object","properties":{"slug":{"type":"string"},"group":{"type":"string"},"sender":{"type":"string"},"label":{"type":"string"},"chatName":{"type":"string"},"content":{"type":"string"},"ts":{"type":"number"},"tags":{"type":"array","items":{"type":"string"}}},"required":["slug","group","sender","content"],"additionalProperties":false,"x-effect":"ask","x-idempotent":false},{"title":"space:post","description":"Post a message to a Space — authenticated member or public Space; stamps space:<slug> tag so the message appears in the Space's thread on slug/in alongside every channel's conversation","type":"object","properties":{"space":{"type":"string"},"content":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"replyTo":{"type":"string"}},"required":["space","content"],"additionalProperties":false,"x-effect":"ask","x-idempotent":false},{"title":"space:create","description":"Create a new Space — registers the channel_link inbox door in channels so every inbound signal is mirrored to the workspace thread read model; private flag restricts posting to members","type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"private":{"type":"boolean"}},"required":["slug"],"additionalProperties":false,"x-effect":"ask","x-idempotent":false},{"title":"space:join","description":"Join a public Space — caller begins receiving its signals and their posts are stamped with the space tag","type":"object","properties":{"space":{"type":"string"}},"required":["space"],"additionalProperties":false,"x-effect":"ask","x-idempotent":true},{"title":"space:invite","description":"Invite an actor to a private Space — grants membership so they can read and post; requires the caller to be a member or admin of the Space","type":"object","properties":{"space":{"type":"string"},"who":{"type":"string"},"role":{"type":"string"}},"required":["space","who"],"additionalProperties":false,"x-effect":"ask","x-idempotent":false},{"title":"chat:send","description":"Send a message to a group the caller is a member of — sender is always the authenticated caller","type":"object","properties":{"group":{"type":"string"},"text":{"type":"string"},"slug":{"type":"string"}},"required":["group","text"],"additionalProperties":false,"x-effect":"ask","x-auth":"member"},{"title":"chat:context","description":"Assemble a budget-bounded context pack (spine + pins + siblings + summary) for a thread message — structured + serialized for an agent","type":"object","properties":{"entityId":{"type":"string"},"targetId":{"type":"string"},"maxTokens":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"asActor":{"type":"string"}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"member"},{"title":"chat:grade","description":"Grade a chat message 1..5 — mark(>=4)/warn(<=2) the responder's path so routing learns who answers well","type":"object","properties":{"messageId":{"type":"string"},"score":{"type":"integer","minimum":1,"maximum":5}},"required":["messageId","score"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":true,"x-auth":"member"},{"title":"chat:harden","description":"Harden a resolved thread into a dim-6 learning — talk → grade → harden → knowledge; refuses an unresolved thread","type":"object","properties":{"entityId":{"type":"string"},"claim":{"type":"string"}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":false,"x-auth":"member"},{"title":"lens:run","description":"Browse a thread's forest by tag query (tags AND, not exclude) ordered by recency or salience","type":"object","properties":{"entityId":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"not":{"type":"array","items":{"type":"string"}},"order":{"type":"string","enum":["recency","salience"]},"limit":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"member"},{"title":"lens:save","description":"Save a named tag-query lens for the workspace (upsert by name) — a reusable browse / followable feed","type":"object","properties":{"name":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"not":{"type":"array","items":{"type":"string"}},"order":{"type":"string","enum":["recency","salience"]},"limit":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["name"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"member"},{"title":"lens:list","description":"List the workspace's saved lenses","type":"object","properties":{},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"member"},{"title":"lens:delete","description":"Delete a saved lens by name","type":"object","properties":{"name":{"type":"string"}},"required":["name"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":true,"x-auth":"member"},{"title":"entity:reply","description":"Reply to a conversation — persists the message, delivers to the customer's channel (telegram/discord) when not web, clears the pending draft; kind:'note' stays internal","type":"object","properties":{"entityId":{"type":"string"},"content":{"type":"string"},"kind":{"type":"string","enum":["send","note"]},"author":{"type":"string","enum":["staff","agent"]},"replyTo":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}}},"required":["entityId","content"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":false,"x-auth":"member"},{"title":"entity:tag","description":"Tag a conversation/entity (add/remove) or move it between lifecycle stages — the sole stage_transition writer","type":"object","properties":{"id":{"type":"string"},"add":{"type":"array","items":{"type":"string"}},"remove":{"type":"array","items":{"type":"string"}},"move":{"type":"object","properties":{"lifecycle":{"type":"string"},"from":{"type":"string"},"to":{"type":"string"},"by":{"type":"string"},"source":{"type":"string"}},"required":["lifecycle","to"],"additionalProperties":false}},"required":["id"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":false,"x-auth":"member"},{"title":"entity:meta","description":"Patch an entity's profile fields (website, industry, location, city, country, size, description, phone, email) — empty value clears the field","type":"object","properties":{"id":{"type":"string"},"meta":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["id","meta"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"member"},{"title":"entity:connect","description":"Link an inbox conversation to a known person (by uid or label); omit both to unlink","type":"object","properties":{"id":{"type":"string"},"personUid":{"type":"string"},"personLabel":{"type":"string"}},"required":["id"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"member"},{"title":"entity:rename","description":"Set or clear a conversation's subject line (≤140 chars; empty clears)","type":"object","properties":{"id":{"type":"string"},"subject":{"type":"string"}},"required":["id"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"member"},{"title":"entity:participant","description":"Add or remove an actor on a conversation (pass remove:<uid> to remove, or uid/kind/label to add)","type":"object","properties":{"id":{"type":"string"},"uid":{"type":"string"},"kind":{"type":"string"},"label":{"type":"string"},"remove":{"type":"string"}},"required":["id"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"member"},{"title":"products:create","description":"Create a storefront product with one or more prices","type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"prices":{"type":"array","items":{"type":"object","properties":{"unit_amount":{"type":"number"},"currency":{"type":"string"},"price_type":{"type":"string"},"interval":{"type":"string"}},"required":["unit_amount"],"additionalProperties":false}},"description":{"type":"string"},"images":{"type":"array","items":{"type":"string"}},"product_type":{"type":"string"},"collection":{"type":"string"}},"required":["slug","name","prices"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":false,"x-auth":"member"},{"title":"products:update","description":"Update a product's name, description, images, or collection","type":"object","properties":{"slug":{"type":"string"},"pid":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"images":{"type":"array","items":{"type":"string"}},"collection":{"type":"string"}},"required":["slug","pid"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":true,"x-auth":"member"},{"title":"products:list","description":"List a workspace's storefront products with their prices (public)","type":"object","properties":{"slug":{"type":"string"}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"public"},{"title":"products:archive","description":"Archive a product (reversible via products:update)","type":"object","properties":{"slug":{"type":"string"},"pid":{"type":"string"}},"required":["slug","pid"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"member"},{"title":"storefront:stats","description":"Storefront funnel stats — views, checkouts, purchases, revenue, per-product breakdown","type":"object","properties":{"slug":{"type":"string"}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"member"},{"title":"pages:create","description":"Create a workspace page (draft) from a title + sections","type":"object","properties":{"slug":{"type":"string"},"title":{"type":"string"},"sections":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"pageSlug":{"type":"string"}},"required":["slug","title","sections"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":false,"x-auth":"member"},{"title":"pages:edit","description":"Edit a workspace page's title or sections","type":"object","properties":{"slug":{"type":"string"},"page":{"type":"string"},"title":{"type":"string"},"sections":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["slug","page"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":false,"x-auth":"member"},{"title":"pages:list","description":"List a workspace's pages with status","type":"object","properties":{"slug":{"type":"string"}},"required":["slug"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"member"},{"title":"pages:publish","description":"Publish a workspace page","type":"object","properties":{"slug":{"type":"string"},"page":{"type":"string"}},"required":["slug","page"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"member"},{"title":"settings:write","description":"Write a workspace setting value (caller or authorized descendant)","type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"value":{"type":"string"}},"required":["value"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":false,"x-auth":"member"},{"title":"domain:set","description":"Bind a custom domain to a workspace — returns the verification TXT/CNAME records to add","type":"object","properties":{"slug":{"type":"string"},"actorId":{"type":"string"},"host":{"type":"string"}},"required":["slug","actorId","host"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":false,"x-auth":"verify_domain"},{"title":"course:save","description":"Create or update a course (upsert by courseId)","type":"object","properties":{"courseId":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"published":{"type":"boolean"},"price_cents":{"type":"number"}},"required":["courseId"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":true,"x-auth":"member"},{"title":"course:get","description":"Get a course with its lessons (lesson bodies redacted unless enrolled or owner)","type":"object","properties":{"workspace":{"type":"string"},"slug":{"type":"string"},"courseId":{"type":"string"}},"required":["courseId"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"public"},{"title":"course:list","description":"List a workspace's courses (published for others, all for the owner)","type":"object","properties":{"workspace":{"type":"string"},"slug":{"type":"string"}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"public"},{"title":"course:enroll","description":"Enroll the caller in a course (payment_required if priced and not comped)","type":"object","properties":{"workspace":{"type":"string"},"slug":{"type":"string"},"courseId":{"type":"string"}},"required":["courseId"],"additionalProperties":false,"x-effect":"ask","x-cost":"variable","x-reversible":false,"x-idempotent":true,"x-auth":"member"},{"title":"course:lessons-save","description":"Upsert one or more lessons on a course","type":"object","properties":{"courseId":{"type":"string"},"lessons":{"anyOf":[{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}},"required":["courseId","lessons"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":true,"x-auth":"member"},{"title":"playbook:save","description":"Save a learner's playbook entry for a course lesson (upsert)","type":"object","properties":{"course":{"type":"string"},"lesson":{"type":"string"},"content":{"type":"string"}},"required":["course","lesson"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"public"},{"title":"playbook:get","description":"Get the caller's playbook entries (optionally scoped to one course)","type":"object","properties":{"course":{"type":"string"}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"public"},{"title":"playbook:promote","description":"Promote a completed playbook into a reusable skill in the workspace's content repo","type":"object","properties":{"course":{"type":"string"}},"required":["course"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":false,"x-auth":"member"},{"title":"social:publish","description":"Publish a drafted social post to its platform","type":"object","properties":{"slug":{"type":"string"},"actorId":{"type":"string"},"id":{"type":"string"}},"required":["slug","actorId","id"],"additionalProperties":false,"x-effect":"ask","x-cost":"variable","x-reversible":false,"x-idempotent":false,"x-auth":"update_group"},{"title":"social:news-to-drafts","description":"Turn fresh news items into platform-specific social post drafts","type":"object","properties":{"groupId":{"type":"string"},"maxItems":{"type":"integer","minimum":-9007199254740991,"maximum":10},"platforms":{"type":"array","items":{"type":"string"}}},"additionalProperties":false,"x-effect":"ask","x-cost":"variable","x-reversible":false,"x-idempotent":false,"x-auth":"member"},{"title":"web:crawl","description":"Fetch a URL and extract domain, name, description, and readable text (public)","type":"object","properties":{"url":{"type":"string"}},"required":["url"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"public"},{"title":"people:create","description":"Create a person actor in the workspace","type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"}},"required":["name"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":false,"x-auth":"member"},{"title":"companies:create","description":"Create a company group in the workspace","type":"object","properties":{"name":{"type":"string"}},"required":["name"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":false,"x-auth":"member"},{"title":"conversation:start","description":"Start a new conversation thread (optionally seeded with a message, function lens, or recipient)","type":"object","properties":{"message":{"type":"string"},"fn":{"type":"string"},"actorId":{"type":"string"},"to":{"type":"string"}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":false,"x-auth":"member"},{"title":"billing:plan","description":"Change a workspace's plan — returns a checkout URL to complete the upgrade","type":"object","properties":{"slug":{"type":"string"},"actorId":{"type":"string"},"plan":{"type":"string","enum":["pro","agency","scale"]}},"required":["slug","actorId","plan"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":false,"x-auth":"update_group"},{"title":"billing:topup","description":"Top up workspace AI credits — returns a checkout URL for the amount","type":"object","properties":{"slug":{"type":"string"},"actorId":{"type":"string"},"amount":{"type":"number"},"currency":{"type":"string"}},"required":["slug","actorId","amount"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":false,"x-auth":"update_group"},{"title":"video:ensure-defaults","description":"Ensure a workspace's default video rooms exist — returns which were created vs already present","type":"object","properties":{"workspace":{"type":"string"},"slug":{"type":"string"}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"member"},{"title":"booking:confirm-payment","description":"Confirm a booking after payment settles, by payment reference (public)","type":"object","properties":{"paymentRef":{"type":"string"}},"required":["paymentRef"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"public"},{"title":"booking:list-slots","description":"List open slots for a workspace's service on a date (or next open days)","type":"object","properties":{"slug":{"type":"string"},"service":{"type":"string"},"date":{"type":"string"},"days":{"type":"integer","minimum":1,"maximum":14}},"required":["slug"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-simulatable":false,"x-idempotent":true},{"title":"booking:create","description":"Hold a slot for a customer; returns a ref and a payment intent when price > 0","type":"object","properties":{"slug":{"type":"string"},"service":{"type":"string"},"slotDate":{"type":"string"},"slotTime":{"type":"string"},"customer":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["name"],"additionalProperties":false},"paymentRail":{"type":"string","enum":["stripe","sui"]}},"required":["slug","service","slotDate","slotTime","customer"],"additionalProperties":false,"x-effect":"ask","x-cost":"variable","x-reversible":false,"x-settles":"offchain","x-simulatable":true,"x-idempotent":true},{"title":"booking:configure","description":"Provider-gated: create or update a bookable service (hours, slot length, price, currency, tz, active)","type":"object","properties":{"slug":{"type":"string"},"actorId":{"type":"string"},"service":{"type":"string"},"name":{"type":"string"},"duration_min":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"price":{"type":"integer","minimum":0,"maximum":9007199254740991},"currency":{"type":"string"},"hours_json":{"type":"array","items":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string"},"end":{"type":"string"}},"required":["start","end"],"additionalProperties":false}}},"tz":{"type":"string"},"active":{"type":"boolean"}},"required":["slug","name","duration_min","hours_json"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"manage_clients"},{"title":"booking:list-bookings","description":"Provider-gated: list a workspace's bookings for a month (customer, time, service, status)","type":"object","properties":{"slug":{"type":"string"},"month":{"type":"string"}},"required":["slug"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":true,"x-auth":"manage_clients"},{"title":"booking:cancel","description":"Provider-gated: dissolve a booking by ref, releasing the slot","type":"object","properties":{"slug":{"type":"string"},"ref":{"type":"string"}},"required":["slug","ref"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":true,"x-auth":"manage_clients"},{"title":"workflow:list","description":"List a workspace's workflows (D1 mirror); pass templates=true for the public SOP catalog","type":"object","properties":{"slug":{"type":"string"},"templates":{"type":"boolean"}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-simulatable":false,"x-idempotent":true,"x-auth":"view_workflows"},{"title":"workflow:get","description":"Fetch one workflow's full graph — steps (kind, config, position) and edges (with conditions)","type":"object","properties":{"workflowId":{"type":"string"}},"required":["workflowId"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-simulatable":false,"x-idempotent":true,"x-auth":"view_workflows"},{"title":"workflow:runs","description":"List recent runs of a workflow (D1 workflow_run) for the monitor + history surfaces","type":"object","properties":{"workflowId":{"type":"string"},"runId":{"type":"string"},"limit":{"type":"integer","minimum":1,"maximum":200}},"required":["workflowId"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-simulatable":false,"x-idempotent":true,"x-auth":"view_workflows"},{"title":"workflow:create","description":"Create a blank workflow (group group-type=workflow) — optionally cloned from a template","type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"fromTemplate":{"type":"string"},"simulate":{"type":"boolean"}},"required":["name"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-simulatable":true,"x-idempotent":false,"x-auth":"manage_workflows"},{"title":"workflow:apply-diff","description":"Apply a WorkflowDiff (add/remove/connect/disconnect/update). simulate=true validates the DAG without persisting","type":"object","properties":{"workflowId":{"type":"string"},"diff":{"type":"object","properties":{"add":{"type":"array","items":{"type":"object","properties":{"tempId":{"type":"string"},"kind":{"type":"string","enum":["trigger","tool","skill","agent","condition","human","delay","sell"]},"name":{"type":"string"},"config":{"type":"string"},"position":{"type":"string"},"afterStepId":{"type":"string"}},"required":["tempId","kind","name","config"],"additionalProperties":false}},"remove":{"type":"array","items":{"type":"string"}},"connect":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string"},"target":{"type":"string"},"condition":{"type":"string"}},"required":["source","target"],"additionalProperties":false}},"disconnect":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string"},"target":{"type":"string"}},"required":["source","target"],"additionalProperties":false}},"update":{"type":"array","items":{"type":"object","properties":{"stepId":{"type":"string"},"field":{"type":"string","enum":["name","config","position","exit-condition","owner"]},"value":{"type":"string"}},"required":["stepId","field","value"],"additionalProperties":false}}},"additionalProperties":false},"simulate":{"type":"boolean"},"version":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["workflowId","diff"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-simulatable":true,"x-idempotent":false,"x-auth":"manage_workflows"},{"title":"workflow:run","description":"Start a run — spawns the WorkflowRun DO, executes each step, marks path strength on traversed edges","type":"object","properties":{"workflowId":{"type":"string"},"triggerPayload":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"idempotencyKey":{"type":"string"}},"required":["workflowId"],"additionalProperties":false,"x-effect":"ask","x-cost":"variable","x-reversible":false,"x-settles":"none","x-simulatable":false,"x-idempotent":true,"x-auth":"manage_workflows"},{"title":"human:resolve","description":"Resolve a suspended human step — carries { runId, stepId, decision | formPayload } to the run DO","type":"object","properties":{"runId":{"type":"string"},"stepId":{"type":"string"},"decision":{"type":"string","enum":["approved","rejected"]},"formPayload":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"slug":{"type":"string"},"attestation":{"type":"string"}},"required":["runId","stepId"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-simulatable":false,"x-idempotent":true,"x-auth":"manage_workflows"},{"title":"checkout:create","description":"The `sell` step's binding — mint a Connect checkout session for the buyer, stamping { runId, stepId } into session metadata so checkout:resume can wake the run","type":"object","properties":{"workspace":{"type":"string"},"ppid":{"type":"string"},"runId":{"type":"string"},"stepId":{"type":"string"}},"required":["workspace","ppid"],"additionalProperties":false,"x-effect":"ask","x-cost":"variable","x-reversible":false,"x-settles":"offchain","x-simulatable":false,"x-idempotent":false,"x-auth":"manage_workflows"},{"title":"checkout:resume","description":"Wake a run suspended at a `sell` step — the storefront webhook fires this on checkout.session.completed with the (runId, stepId) it stamped at create. Sibling of human:resolve.","type":"object","properties":{"runId":{"type":"string"},"stepId":{"type":"string"},"slug":{"type":"string"}},"required":["runId","stepId"],"additionalProperties":false,"x-effect":"signal","x-cost":"free","x-simulatable":false,"x-idempotent":true,"x-auth":"manage_workflows"},{"title":"human:notify","description":"Post a channel-shaped approval into a run's conversation when it suspends at a human step — web card · Telegram inline keys · Discord components. Sibling of chat:send.","type":"object","properties":{"runId":{"type":"string"},"stepId":{"type":"string"},"stepName":{"type":"string"},"workflowName":{"type":"string"},"group":{"type":"string"}},"required":["runId","stepId"],"additionalProperties":false,"x-effect":"signal","x-cost":"free","x-simulatable":false,"x-idempotent":true,"x-auth":"manage_workflows"},{"title":"workflow:stop","description":"Park a live run at its current step (status → paused); preserves the resume cursor so human:resolve or a re-run continues it","type":"object","properties":{"runId":{"type":"string"}},"required":["runId"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-simulatable":false,"x-idempotent":true,"x-auth":"manage_workflows"},{"title":"workflow:trigger","description":"Fire every workspace workflow whose trigger step matches a channel source (e.g. webhook:telegram)","type":"object","properties":{"source":{"type":"string"},"slug":{"type":"string"},"triggerPayload":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["source"],"additionalProperties":false,"x-effect":"signal","x-cost":"variable","x-reversible":false,"x-simulatable":false,"x-idempotent":false,"x-auth":"manage_workflows"},{"title":"workflow:delete","description":"Delete a workflow and all its runs, steps, and edges","type":"object","properties":{"workflowId":{"type":"string"}},"required":["workflowId"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":true,"x-auth":"manage_workflows"},{"title":"workflow:update","description":"Rename a workflow or change its status (draft|active|paused)","type":"object","properties":{"workflowId":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","enum":["draft","active","paused"]}},"required":["workflowId"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"manage_workflows"},{"title":"skills:list","description":"List the caller's workspace skill catalog (R2) — powers the canvas SkillPicker","type":"object","properties":{},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-simulatable":false,"x-idempotent":true,"x-auth":"view_workflows"},{"title":"skill:run","description":"Run a workspace skill — loads its body from R2, runs one bounded turn via channels, returns { text }","type":"object","properties":{"skill":{"type":"string"}},"required":["skill"],"additionalProperties":{},"x-effect":"ask","x-cost":"variable","x-reversible":false,"x-settles":"none","x-simulatable":false,"x-idempotent":false,"x-auth":"manage_workflows"},{"title":"agent:run","description":"Invoke a bound actor (optionally skill-constrained) for one bounded turn via channels; returns { text }","type":"object","properties":{"actorId":{"type":"string"},"skill":{"type":"string"},"instructions":{"type":"string"}},"required":["actorId"],"additionalProperties":{},"x-effect":"ask","x-cost":"variable","x-reversible":false,"x-settles":"none","x-simulatable":false,"x-idempotent":false,"x-auth":"manage_workflows"},{"title":"video:create-room","description":"Create a 100ms video room for a workspace; returns the room slug and URL","type":"object","properties":{"workspace":{"type":"string"},"roomSlug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"type":{"type":"string","enum":["meeting","classroom","webinar"]},"region":{"type":"string"}},"required":["workspace","roomSlug","name"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":false,"x-auth":"manage_clients"},{"title":"video:delete-room","description":"Disable a video room and its 100ms counterpart; room is no longer joinable","type":"object","properties":{"workspace":{"type":"string"},"roomSlug":{"type":"string"}},"required":["workspace","roomSlug"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":true,"x-auth":"manage_clients"},{"title":"video:contact-call","description":"Provision a one-to-one video room for a contact; links the room thread to the contact CRM record","type":"object","properties":{"workspace":{"type":"string"},"actorId":{"type":"string"},"name":{"type":"string"}},"required":["workspace","actorId"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":false,"x-auth":"manage_clients"},{"title":"video:invite","description":"Generate a tracked /go/ join link for an actor into an existing room","type":"object","properties":{"workspace":{"type":"string"},"roomSlug":{"type":"string"},"actorId":{"type":"string"}},"required":["workspace","roomSlug"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":false,"x-auth":"manage_clients"},{"title":"video:schedule-webinar","description":"Create a webinar room and send tracked invite links to N contacts in one call","type":"object","properties":{"workspace":{"type":"string"},"name":{"type":"string"},"actorIds":{"minItems":1,"maxItems":500,"type":"array","items":{"type":"string"}}},"required":["workspace","actorIds"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":false,"x-auth":"manage_clients"},{"title":"video:create-session","description":"Create a video session with host + guest tracked join links; optionally schedules the call","type":"object","properties":{"workspace":{"type":"string"},"roomSlug":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["meeting","classroom","webinar"]},"region":{"type":"string"},"guestActorId":{"type":"string"},"scheduledAt":{"type":"number"}},"required":["workspace","roomSlug","name"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":false,"x-auth":"manage_clients"},{"title":"video:summary","description":"Run an AI summary of a call transcript and append it as a system message on the room thread","type":"object","properties":{"threadId":{"type":"string"},"transcript":{"type":"string"}},"required":["threadId","transcript"],"additionalProperties":false,"x-effect":"ask","x-cost":"variable","x-idempotent":false,"x-auth":"manage_clients"},{"title":"video:quick-room","description":"Ad-hoc video room from any chat thread — creates the room, sends the join link into the thread","type":"object","properties":{"workspace":{"type":"string"},"threadId":{"type":"string"},"name":{"type":"string"}},"required":["workspace"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":false,"x-auth":"manage_clients"},{"title":"video:join-event","description":"Record a peer join event for tracking and lifecycle; public — called from the /go/ link handler","type":"object","properties":{"workspace":{"type":"string"},"roomSlug":{"type":"string"},"role":{"type":"string"},"actorId":{"type":"string"}},"required":["workspace","roomSlug"],"additionalProperties":false,"x-effect":"signal","x-cost":"free","x-idempotent":true},{"title":"video:start-recording","description":"Start cloud recording for an active room via 100ms REST; inserts a video_recordings row","type":"object","properties":{"workspace":{"type":"string"},"roomSlug":{"type":"string"}},"required":["workspace","roomSlug"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":false,"x-auth":"manage_clients"},{"title":"video:start-stream","description":"Start an HLS live stream for a room; returns the playback URL when the stream is ready","type":"object","properties":{"workspace":{"type":"string"},"roomSlug":{"type":"string"},"hlsConfig":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["workspace","roomSlug"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-idempotent":false,"x-auth":"manage_clients"},{"title":"connect:channel","description":"Connect a channel (e.g. telegram) by pasting its bot token; verifies, sets the webhook, seals the credential under the owner gate","type":"object","properties":{"channel":{"type":"string"},"token":{"type":"string"},"slug":{"type":"string"}},"required":["channel","token"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":true,"x-auth":"manage_clients"},{"title":"connect:status","description":"List connected channels for the workspace — channel, account, status, runtime; never the credential","type":"object","properties":{},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"manage_clients"},{"title":"broadcast:create","description":"Create a broadcast draft — subject, body_md, audience_tag, channel","type":"object","properties":{"workspace":{"type":"string"},"subject":{"type":"string"},"body_md":{"type":"string"},"audience_tag":{"type":"string"},"channel":{"type":"string","enum":["email","sms","whatsapp"]}},"required":["workspace"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":false,"x-auth":"write"},{"title":"broadcast:update","description":"Update a broadcast draft — subject, body, audience, schedule","type":"object","properties":{"workspace":{"type":"string"},"broadcastId":{"type":"string"},"subject":{"type":"string"},"body_md":{"type":"string"},"audience_tag":{"type":"string"},"scheduled_at":{"type":"number"}},"required":["workspace","broadcastId"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"write"},{"title":"broadcast:list","description":"List broadcasts for a workspace with status and sent_at","type":"object","properties":{"workspace":{"type":"string"},"status":{"type":"string","enum":["draft","scheduled","sending","sent","cancelled"]},"limit":{"type":"integer","minimum":-9007199254740991,"maximum":100}},"required":["workspace"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":true,"x-auth":"read"},{"title":"broadcast:get","description":"Get a single broadcast with recipient counts","type":"object","properties":{"workspace":{"type":"string"},"broadcastId":{"type":"string"}},"required":["workspace","broadcastId"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":true,"x-auth":"read"},{"title":"broadcast:test","description":"Send a test email for a broadcast to one address","type":"object","properties":{"workspace":{"type":"string"},"broadcastId":{"type":"string"},"to":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["workspace","broadcastId","to"],"additionalProperties":false,"x-effect":"signal","x-cost":"variable","x-reversible":false,"x-idempotent":false,"x-auth":"write"},{"title":"broadcast:send","description":"Seed recipients from audience tag, apply suppression, enqueue the send batch","type":"object","properties":{"workspace":{"type":"string"},"broadcastId":{"type":"string"}},"required":["workspace","broadcastId"],"additionalProperties":false,"x-effect":"signal","x-cost":"variable","x-reversible":false,"x-idempotent":false,"x-auth":"write"},{"title":"broadcast:schedule","description":"Schedule a broadcast to send at a future unix timestamp","type":"object","properties":{"workspace":{"type":"string"},"broadcastId":{"type":"string"},"scheduled_at":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["workspace","broadcastId","scheduled_at"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"write"},{"title":"broadcast:cancel","description":"Cancel a scheduled or in-progress broadcast","type":"object","properties":{"workspace":{"type":"string"},"broadcastId":{"type":"string"}},"required":["workspace","broadcastId"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":true,"x-auth":"write"},{"title":"voice:provision","description":"Configure a voice agent for the workspace — creates/updates the ElevenLabs agent, links an optional Twilio number, returns the embed snippet","type":"object","properties":{"agentName":{"type":"string"},"voiceId":{"type":"string"},"phone":{"type":"string"},"greeting":{"type":"string"},"instructions":{"type":"string"}},"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":true,"x-idempotent":true,"x-auth":"manage_clients"},{"title":"voice:token","description":"Mint a short-lived ElevenLabs conversation token for the workspace; 404 if voice is disabled (slug-opaque: disabled ≡ not found)","type":"object","properties":{"slug":{"type":"string"}},"required":["slug"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":false,"x-auth":"public"},{"title":"voice:call","description":"Trigger an outbound call to a phone number via the workspace ElevenLabs agent; logs the call to D1, links to workflowRunId when present","type":"object","properties":{"phone":{"type":"string"},"instructions":{"type":"string"},"workflowRunId":{"type":"string"}},"required":["phone"],"additionalProperties":false,"x-effect":"ask","x-cost":"variable","x-reversible":false,"x-settles":"none","x-idempotent":false,"x-auth":"manage_clients"},{"title":"audience:subscribe","description":"Begin double opt-in for a contact — writes consent:email:pending tag, sends confirm email","type":"object","properties":{"workspace":{"type":"string"},"address":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"list":{"type":"string"},"ref":{"type":"string"}},"required":["workspace","address"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":true,"x-auth":"public"},{"title":"audience:unsubscribe","description":"One-click unsubscribe — writes suppression row and flips consent tag; RFC 8058 safe","type":"object","properties":{"workspace":{"type":"string"},"channel":{"type":"string","enum":["email","sms","whatsapp"]},"address":{"type":"string"},"token":{"type":"string"}},"required":["workspace","address","token"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":true,"x-auth":"public"},{"title":"audience:confirm","description":"Confirm email subscription via opt-in token — flips consent tag to subscribed","type":"object","properties":{"token":{"type":"string"}},"required":["token"],"additionalProperties":false,"x-effect":"ask","x-cost":"free","x-reversible":false,"x-idempotent":true,"x-auth":"public"},{"title":"audience:winback","description":"Re-engage dormant subscribers (batch winback sequence tool) — queries contacts with no send in `days` days, sends one re-engage email each through the suppression-checked send path","type":"object","properties":{"days":{"type":"number"},"campaign":{"type":"string"}},"additionalProperties":false,"x-effect":"signal","x-cost":"variable","x-reversible":false,"x-idempotent":false,"x-auth":"write"},{"title":"message:send","description":"Send a message to one address via channel — suppression-checked; sms/whatsapp dissolve until adapters ship","type":"object","properties":{"workspace":{"type":"string"},"channel":{"type":"string","enum":["email","sms","whatsapp"]},"to":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"},"campaign":{"type":"string"}},"required":["workspace","channel","to","body"],"additionalProperties":false,"x-effect":"signal","x-cost":"variable","x-reversible":false,"x-idempotent":false,"x-auth":"write"}]}
    # END generated:receivers
    # BEGIN generated:components — bun run codegen (do not edit by hand)
    GroupEntity:
      type: object
      properties:
        gid: {type: string}
        name: {type: string}
        "group-type": {type: string, enum: ["company", "community", "dao", "friends", "org", "personal", "team", "workspace", "world"]}
        brand: {type: string}
        scope: {type: string, enum: ["private", "group", "public"]}
        plan: {type: string}
        tag: {type: string}
      additionalProperties: true
    ActorEntity:
      type: object
      properties:
        aid: {type: string}
        name: {type: string}
        "actor-type": {type: string, enum: ["agent", "animal", "human", "world"]}
        model: {type: string}
        prompt: {type: string}
        tag: {type: string}
      additionalProperties: true
    ThingEntity:
      type: object
      properties:
        tid: {type: string}
        name: {type: string}
        "thing-type": {type: string, enum: ["plan", "service", "skill", "step", "task", "token"]}
        tag: {type: string}
        price: {type: number}
        "task-status": {type: string, enum: ["blocked", "dissolved", "done", "failed", "open", "picked", "verified"]}
        "step-kind": {type: string, enum: ["agent", "condition", "delay", "human", "sell", "skill", "tool", "trigger"]}
      additionalProperties: true
    PathEntity:
      type: object
      properties:
        strength: {type: number}
        resistance: {type: number}
        traversals: {type: integer}
        scope: {type: string, enum: ["private", "group", "public"]}
        "bridge-kind": {type: string}
      additionalProperties: true
    SignalEntity:
      type: object
      properties:
        payload: {type: string}
        data: {type: string}
        amount: {type: number}
        success: {type: boolean}
        latency: {type: number}
        ts: {type: string, format: date-time}
        scope: {type: string, enum: ["private", "group", "public"]}
      additionalProperties: true
    HypothesisEntity:
      type: object
      properties:
        hid: {type: string}
        statement: {type: string}
        "hypothesis-status": {type: string, enum: ["pending", "testing", "confirmed", "rejected"]}
        confidence: {type: number}
        scope: {type: string, enum: ["private", "group", "public"]}
        source: {type: string}
      additionalProperties: true
    # END generated:components
    Outcome:
      type: object
      required: [outcome, latencyMs]
      properties:
        outcome:
          type: string
          enum: [result, timeout, dissolved, failure]
          description: One of the four substrate outcomes.
        result:
          description: Present when outcome=result.
        reason:
          type: string
          description: Present when outcome=dissolved or failure.
        latencyMs:
          type: integer
          description: Round-trip time in milliseconds.

    SignalResponse:
      type: object
      required: [outcome, signalId]
      properties:
        outcome:
          type: string
          enum: [queued, dissolved]
        signalId:
          type: string
          format: ulid

    PathStrength:
      type: object
      required: [target, strength]
      properties:
        target:
          type: string
          nullable: true
        strength:
          type: number
          minimum: 0

    Error:
      type: object
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri
          description: Stable problem type URI.
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        traceId:
          type: string
          format: ulid

    Agent:
      type: object
      required: [id, title, description]
      properties:
        id:
          type: string
        title:
          type: string
        description:
          type: string
        model:
          type: string
        skills:
          type: array
          items:
            $ref: "#/components/schemas/Skill"
        verified:
          type: boolean

    Skill:
      type: object
      required: [name, title]
      properties:
        name:
          type: string
        title:
          type: string
        description:
          type: string
        price:
          type: number

    Thread:
      type: object
      required: [id, slug, createdAt]
      properties:
        id:
          type: string
        slug:
          type: string
        agentId:
          type: string
        createdAt:
          type: integer
          description: Unix epoch ms.

    Group:
      type: object
      required: [id, kind]
      properties:
        id:
          type: string
        kind:
          type: string
          enum: [world, team, org]
        name:
          type: string

    Actor:
      type: object
      required: [id, type]
      properties:
        id:
          type: string
        type:
          type: string
          enum: [human, agent, world]
        name:
          type: string

    Thing:
      type: object
      required: [id, type]
      properties:
        id:
          type: string
        type:
          type: string
          enum: [skill, task, token]
        name:
          type: string

    Path:
      type: object
      required: [from, to, strength]
      properties:
        from:
          type: string
        to:
          type: string
        strength:
          type: number
        resistance:
          type: number
        traversals:
          type: integer

    Event:
      type: object
      required: [id, receiver, createdAt]
      properties:
        id:
          type: string
        receiver:
          type: string
        data:
          description: Arbitrary signal payload.
        createdAt:
          type: integer

    Hypothesis:
      type: object
      required: [id, claim, confidence]
      properties:
        id:
          type: string
        claim:
          type: string
        confidence:
          type: number
          minimum: 0
          maximum: 1
        status:
          type: string
          enum: [candidate, active, retired]


    # ── Feature-route schemas (hand-derived from src/pages/api/**) ───────────

    SkillCatalogEntry:
      type: object
      description: |
        One entry of the bundled skill catalogue, as projected by
        `loadCatalog()` in src/pages/api/skills.ts. NOT the same shape as
        `Skill` (which the Agent schema uses).
      required: [id, label]
      properties:
        id:
          type: string
          description: Directory name of the SKILL.md / skill.md that produced it.
        label:
          type: string
          description: Frontmatter `title`, else `labelFor(id)`.
        description:
          type: string
        tags:
          type: array
          items:
            type: string
        price:
          type: number
        category:
          type: string
          enum: [fleet, legacy]
        content:
          type: string
          description: Raw markdown — present only when `?expand=content`.

    SubmissionPack:
      type: object
      description: |
        The directory-submission payload frozen at run time
        (`buildSubmissionPack`, src/lib/directories/pack.ts).
      required: [fields, missing]
      properties:
        fields:
          type: object
          description: Directory field key → the value taken from the workspace profile.
          additionalProperties:
            type: string
        missing:
          type: array
          description: Directory fields the profile had no value for.
          items:
            type: object
            required: [key, label, type, required]
            properties:
              key:
                type: string
              label:
                type: string
              type:
                type: string
                enum: [text, textarea, url, email, phone, select]
              required:
                type: boolean

    DirectorySubmission:
      type: object
      required: [id, directorySlug, status]
      properties:
        id:
          type: string
          format: uuid
        directorySlug:
          type: string
          description: A `slug` from the DIRECTORIES registry (src/lib/directories/registry.ts).
        status:
          type: string
          enum: [queued, submitted, live, rejected]
        pack:
          description: Null when the stored pack column is empty.
          oneOf:
            - $ref: "#/components/schemas/SubmissionPack"
            - type: "null"
        listingUrl:
          type: [string, "null"]

    LifecycleStage:
      type: object
      required: [id, label, tag, slot, subSteps, enter_when, levers]
      properties:
        id:
          type: string
        label:
          type: string
        tag:
          type: string
          description: Namespaced tag — `lifecycle:<lifecycleId>:<stageId>`. Computed on read, never stored.
        slot:
          type: integer
        subSteps:
          type: array
          items:
            type: string
        enter_when:
          type: [string, "null"]
        levers:
          type: array
          items:
            type: object

    LifecycleTransition:
      type: object
      required: [source, target, kind]
      properties:
        source:
          type: string
        target:
          type: string
        kind:
          type: string
          description: Unknown values are COERCED to `forward` on read, never rejected.
        trigger:
          type: string
        sh:
          type: string
        th:
          type: string

    LifecycleDef:
      type: object
      description: A workspace's named lifecycle, as returned by `loadDef` (src/lib/lifecycle/def.ts).
      required: [id, name, version, stages, transitions, arcs, monotonic]
      properties:
        id:
          type: string
        name:
          type: string
        version:
          type: integer
        stages:
          type: array
          items:
            $ref: "#/components/schemas/LifecycleStage"
        transitions:
          type: array
          items:
            $ref: "#/components/schemas/LifecycleTransition"
        arcs:
          type: array
          items:
            type: object
            properties:
              row:
                type: integer
              label:
                type: string
        monotonic:
          type: boolean
          description: Forward-only when true; false permits loop re-entry.

    TagNamespace:
      type: object
      required: [id, workspace_id, namespace, values, colour, acl, locked, usage_count, created_at, updated_at]
      properties:
        id:
          type: string
          description: "`<workspace_id>:<namespace>`."
        workspace_id:
          type: string
        namespace:
          type: string
        values:
          type: array
          description: Empty array = freeform.
          items:
            type: string
        colour:
          type: string
        acl:
          type: object
          required: [read, write]
          properties:
            read:
              type: array
              items:
                type: string
            write:
              type: array
              items:
                type: string
        locked:
          type: boolean
        usage_count:
          type: integer
        created_at:
          type: integer
          description: Epoch milliseconds.
        updated_at:
          type: integer
          description: Epoch milliseconds.

    TagSuggestion:
      type: object
      required: [id, workspace_id, tag, usage_count, first_seen, last_seen, cluster_id, status]
      properties:
        id:
          type: string
        workspace_id:
          type: string
        tag:
          type: string
        usage_count:
          type: integer
        first_seen:
          type: integer
        last_seen:
          type: integer
        cluster_id:
          type: [string, "null"]
        status:
          type: string
          enum: [pending, promoted, dismissed]

    Theme:
      type: object
      description: A design-token theme row (src/lib/db/themes.ts).
      required: [id, owner_id, name, tokens, is_public, fork_of, created_at]
      properties:
        id:
          type: string
        owner_id:
          type: [string, "null"]
        name:
          type: string
        tokens:
          type: string
          description: Serialised token blob — an opaque string at this layer.
        is_public:
          type: integer
          enum: [0, 1]
        fork_of:
          type: [string, "null"]
        created_at:
          type: integer
          description: Epoch milliseconds.

  parameters:
    Idempotency:
      name: Idempotency-Key
      in: header
      required: false
      description: ULID — identical keys replay cached response.
      schema:
        type: string
        format: ulid

    TraceId:
      name: X-Trace-Id
      in: header
      required: false
      schema:
        type: string
        format: ulid

  responses:
    Problem:
      description: Problem+JSON error (RFC 9457).
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Error"

    WorkspaceAuthzError:
      description: |
        Refusal from `authorizeWorkspace` (src/lib/analytics/authz.ts). The same
        body appears under three statuses — `bad_slug` (400), `forbidden` (403)
        and `no_db` (503, D1 unreachable with no other check able to pass).
      content:
        application/json:
          schema:
            type: object
            required: [error]
            properties:
              error:
                type: string
                enum: [bad_slug, forbidden, no_db]

    SimpleError:
      description: "`{ error }` — the bare error body most feature routes return."
      content:
        application/json:
          schema:
            type: object
            required: [error]
            properties:
              error:
                type: string

    OkFalseError:
      description: |
        `{ ok: false, error }` — the `errJson` shape from
        src/lib/analytics/http.ts, used by every /api/lifecycle/* branch
        including its authorization refusals.
      content:
        application/json:
          schema:
            type: object
            required: [ok, error]
            properties:
              ok:
                type: boolean
                const: false
              error:
                type: string

    AuthErrorResponse:
      description: |
        `authErrorResponse(AuthError)` from src/lib/api-auth.ts —
        `{ error: "unauthorized", reason }`. Status carries the meaning:
        401 `no_session`, 402 `billing_over_limit` / `billing_suspended`,
        403 `ip_not_allowed` / `action_not_permitted`,
        503 `authority_undetermined` / `audit_write_failed`.
      content:
        application/json:
          schema:
            type: object
            required: [error, reason]
            properties:
              error:
                type: string
                const: unauthorized
              reason:
                type: string

paths:

  # ── Substrate verbs ────────────────────────────────────────────────────────

  /signal/{receiver}:
    post:
      operationId: signal
      summary: Route a signal
      x-substrate-verb: signal
      tags: [substrate]
      description: |
        Accepts the 5-mode addressing grammar:
        - `alice` — unit id
        - `alice:review` — unit:task
        - `world:review` — group:task
        - `all:review` — broadcast
        - `sub:news:crypto` — topic subscription
      parameters:
        - name: receiver
          in: path
          required: true
          schema:
            type: string
          description: Receiver address (any of the 5 modes).
        - $ref: "#/components/parameters/Idempotency"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  $ref: "#/components/schemas/ReceiverPayload"
      responses:
        "202":
          description: Signal accepted.
          headers:
            X-Trace-Id:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SignalResponse"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"

  /ask/{receiver}:
    post:
      operationId: ask
      summary: Signal and await one of 4 outcomes
      x-substrate-verb: ask
      tags: [substrate]
      description: Always returns HTTP 200. Outcome lives in the response body.
      parameters:
        - name: receiver
          in: path
          required: true
          schema:
            type: string
        - name: timeout
          in: query
          required: false
          schema:
            type: integer
            default: 5000
            description: Max wait in milliseconds.
        - $ref: "#/components/parameters/Idempotency"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  $ref: "#/components/schemas/ReceiverPayload"
      responses:
        "200":
          description: One of the four outcomes.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Outcome"

  /mark/{edge}:
    post:
      operationId: mark
      summary: Strengthen a path
      x-substrate-verb: mark
      tags: [substrate]
      description: Writes strength to TypeDB + D1 mirror. Optional atomic payment.
      parameters:
        - name: edge
          in: path
          required: true
          schema:
            type: string
          description: Edge in `from→to` or `from:to` format.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                strength:
                  type: number
                  default: 1
                weight:
                  type: number
                  description: Payment weight (credits).
                currency:
                  type: string
                  enum: [USD, SUI, ETH, BTC]
      responses:
        "200":
          description: Path updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  strength:
                    type: number
        "400":
          $ref: "#/components/responses/Problem"

  /warn/{edge}:
    post:
      operationId: warn
      summary: Weaken a path
      x-substrate-verb: warn
      tags: [substrate]
      parameters:
        - name: edge
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                strength:
                  type: number
                  default: 1
      responses:
        "200":
          description: Resistance updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  resistance:
                    type: number

  /fade:
    post:
      operationId: fade
      summary: Decay all paths
      x-substrate-verb: fade
      tags: [substrate]
      description: SERVER_SECRET gated. Resistance fades 2x faster than strength.
      security:
        - serverSecret: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                rate:
                  type: number
                  default: 0.05
                  description: Decay rate 0–1.
      responses:
        "200":
          description: Paths decayed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  rate:
                    type: number
                  paths:
                    type: integer

  /follow:
    get:
      operationId: follow
      summary: Best deterministic path
      x-substrate-verb: follow
      tags: [substrate]
      parameters:
        - name: tag
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Best path or null.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PathStrength"

  /select:
    get:
      operationId: select
      summary: Best path (probabilistic)
      x-substrate-verb: select
      tags: [substrate]
      parameters:
        - name: tag
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Sampled path.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PathStrength"

  /sub:
    post:
      operationId: subscribe
      summary: Register a webhook subscription
      x-substrate-verb: signal
      tags: [substrate]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [topic, url]
              properties:
                topic:
                  type: string
                url:
                  type: string
                  format: uri
                secret:
                  type: string
                  description: Used for HMAC-SHA256 delivery signatures.
      responses:
        "200":
          description: Subscription created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  topic:
                    type: string
    delete:
      operationId: unsubscribe
      summary: Remove a webhook subscription
      x-substrate-verb: signal
      tags: [substrate]
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
        - name: topic
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Subscription removed.

  # ── Dimension reads ────────────────────────────────────────────────────────

  /groups:
    get:
      operationId: listGroups
      summary: List groups (worlds, teams, orgs)
      x-dimension: groups
      tags: [dimensions]
      parameters:
        - name: search
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        "200":
          description: Paginated groups.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Group"
                  total:
                    type: integer

  /actors:
    get:
      operationId: listActors
      summary: List actors (humans, agents, worlds)
      x-dimension: actors
      tags: [dimensions]
      parameters:
        - name: type
          in: query
          schema:
            type: string
            enum: [human, agent, world]
        - name: search
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        "200":
          description: Paginated actors.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Actor"
                  total:
                    type: integer

  /things:
    get:
      operationId: listThings
      summary: List things (skills, tasks, tokens)
      x-dimension: things
      tags: [dimensions]
      parameters:
        - name: type
          in: query
          schema:
            type: string
            enum: [skill, task, token]
        - name: search
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        "200":
          description: Paginated things.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Thing"
                  total:
                    type: integer

  /paths:
    get:
      operationId: listPaths
      summary: List paths (highways + strength/resistance)
      x-dimension: paths
      tags: [dimensions]
      parameters:
        - name: from
          in: query
          schema:
            type: string
        - name: to
          in: query
          schema:
            type: string
        - name: min_strength
          in: query
          schema:
            type: number
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        "200":
          description: Paths matching filters.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Path"

  /events:
    get:
      operationId: listEvents
      summary: List signal events
      x-dimension: events
      tags: [dimensions]
      parameters:
        - name: receiver
          in: query
          schema:
            type: string
        - name: since
          in: query
          schema:
            type: integer
            description: Unix epoch ms — events after this timestamp.
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        "200":
          description: Event list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Event"

  /learning:
    get:
      operationId: listLearning
      summary: List hypotheses (ordered by confidence)
      x-dimension: learning
      tags: [dimensions]
      parameters:
        - name: search
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            type: string
            enum: [candidate, active, retired]
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        "200":
          description: Hypotheses.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Hypothesis"

  # ── Chat ──────────────────────────────────────────────────────────────────

  /api/chat:
    post:
      operationId: chat
      summary: Streaming chat completion
      tags: [chat]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [messages]
              properties:
                messages:
                  type: array
                  items:
                    type: object
                agentId:
                  type: string
                threadId:
                  type: string
      responses:
        "200":
          description: AI SDK data stream.
          headers:
            Content-Type:
              schema:
                type: string
                enum: ["text/event-stream"]
        "402":
          description: Insufficient credits.
          $ref: "#/components/responses/Problem"
        "403":
          description: Public chat disabled or rate limited.
          $ref: "#/components/responses/Problem"

  # ── Agents ────────────────────────────────────────────────────────────────

  /api/agents:
    get:
      operationId: listAgents
      summary: List agents for the current workspace
      tags: [agents]
      responses:
        "200":
          description: Agent list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  agents:
                    type: array
                    items:
                      $ref: "#/components/schemas/Agent"

  /api/agents/publish:
    post:
      operationId: publishAgent
      summary: Publish an agent markdown file to R2
      tags: [agents]
      description: |
        Validates markdown against the Zod schema, scans for XSS, checks publish limits
        per billing tier (free=5, starter=20, pro=100, agency=unlimited), and stores to R2.
        Returns 402 with `upgrade` URL when limit reached.
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id, content]
              properties:
                id:
                  type: string
                  pattern: "^[a-z0-9][a-z0-9-]*$"
                content:
                  type: string
                  description: Raw agent markdown.
      responses:
        "200":
          description: Published.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  version:
                    type: string
                    format: ulid
        "400":
          $ref: "#/components/responses/Problem"
        "402":
          description: Publish limit reached for billing tier.
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"

  /api/agents/history:
    get:
      operationId: agentHistory
      summary: Version history for an agent
      tags: [agents]
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        "200":
          description: History entries.
          content:
            application/json:
              schema:
                type: object
                properties:
                  versions:
                    type: array
                    items:
                      type: object
                      properties:
                        version:
                          type: string
                        createdAt:
                          type: integer

  /api/agents/rollback:
    post:
      operationId: rollbackAgent
      summary: Restore a previous version of an agent
      tags: [agents]
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id, version]
              properties:
                id:
                  type: string
                version:
                  type: string
                  format: ulid
      responses:
        "200":
          description: Restored.

  /api/agents/{id}:
    get:
      operationId: getAgent
      summary: Get agent metadata
      tags: [agents]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Agent metadata.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Agent"
        "404":
          $ref: "#/components/responses/Problem"
    delete:
      operationId: deleteAgent
      summary: Delete an agent
      tags: [agents]
      security:
        - sessionCookie: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Deleted.
        "401":
          $ref: "#/components/responses/Problem"

  # ── Skills ────────────────────────────────────────────────────────────────

  /api/skills:
    get:
      operationId: listSkills
      summary: List the bundled skill catalogue
      description: |
        Public — no authentication and no workspace scope. Returns the two
        bundled sources (`ai/skills/*/SKILL.md` and `web/ai/skills/*/skill.md`)
        projected and de-duplicated by id, sorted by label. Cached in KV for
        300s per `q`+`expand` variant.
      tags: [skills]
      parameters:
        - name: q
          in: query
          description: Case-insensitive substring filter over id, label, description and tags.
          schema:
            type: string
        - name: expand
          in: query
          description: "`content` to include each skill's raw markdown in `content`."
          schema:
            type: string
            enum: [content]
      responses:
        "200":
          description: Skill catalogue.
          content:
            application/json:
              schema:
                type: object
                required: [ok, skills, total]
                properties:
                  ok:
                    type: boolean
                    const: true
                  skills:
                    type: array
                    items:
                      $ref: "#/components/schemas/SkillCatalogEntry"
                  total:
                    type: integer
                    description: Size of the UNFILTERED catalogue — not the length of `skills`.

  /api/skills/{name}:
    delete:
      operationId: deleteSkill
      summary: Remove a skill
      tags: [skills]
      security:
        - sessionCookie: []
      parameters:
        - name: name
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Removed.
        "404":
          $ref: "#/components/responses/Problem"

  /api/skills/{name}/enable:
    post:
      operationId: enableSkill
      summary: Enable or disable a skill for the workspace
      tags: [skills]
      security:
        - sessionCookie: []
      parameters:
        - name: name
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [enabled]
              properties:
                enabled:
                  type: boolean
      responses:
        "200":
          description: Updated.

  # ── Threads ───────────────────────────────────────────────────────────────

  /api/threads/{tid}:
    get:
      operationId: getThread
      summary: Get thread metadata and messages
      tags: [threads]
      security:
        - sessionCookie: []
      parameters:
        - name: tid
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Thread.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Thread"
        "403":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"

  /api/threads/{tid}/share:
    post:
      operationId: shareThread
      summary: Create a read-only share link for a thread
      tags: [threads]
      security:
        - sessionCookie: []
      parameters:
        - name: tid
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Share URL.
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri

  /api/threads/{tid}/export:
    get:
      operationId: exportThread
      summary: Export thread as JSON
      tags: [threads]
      security:
        - sessionCookie: []
      parameters:
        - name: tid
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Thread JSON export.
          headers:
            Content-Disposition:
              schema:
                type: string

  /api/threads/{tid}/pdf:
    get:
      operationId: exportThreadPdf
      summary: Export thread as print-ready HTML
      tags: [threads]
      security:
        - sessionCookie: []
      parameters:
        - name: tid
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Print-ready HTML for download.
          headers:
            Content-Disposition:
              schema:
                type: string

  # ── Platform ──────────────────────────────────────────────────────────────

  /api/health:
    get:
      operationId: health
      summary: Health check
      description: Public — no authentication. Reports the configured default agent.
      tags: [platform]
      responses:
        "200":
          description: Service healthy.
          content:
            application/json:
              schema:
                type: object
                required: [status, agent, model, hasOpenRouter]
                properties:
                  status:
                    type: string
                    const: ok
                  agent:
                    type: string
                    description: Default agent id (`loadAgent(env).id`).
                  model:
                    type: string
                  hasOpenRouter:
                    type: boolean
                    description: Whether OPENROUTER_API_KEY is bound. Never the key itself.

  /api/abuse/report:
    post:
      operationId: reportAbuse
      summary: Report a message for moderation review
      description: |
        Unauthenticated; rate-limited to 3 reports per IP per hour. The report is
        written to KV for platform admin review with a 30-day TTL. Every field is
        optional and truncated server-side — `reason` to 100 chars (defaulting to
        `other`), `agentId` to 64, `threadUrl` to 256.
      tags: [platform]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                agentId:
                  type: string
                threadUrl:
                  type: string
      responses:
        "200":
          description: Report queued.
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    const: true
        "400":
          description: 'Body was not valid JSON — `{ "error": "Invalid JSON" }`.'
          $ref: "#/components/responses/SimpleError"
        "429":
          description: Rate limit — max 3 reports per hour per IP.
          $ref: "#/components/responses/SimpleError"
        "500":
          description: Unexpected error; `error` carries the message.
          $ref: "#/components/responses/SimpleError"

  /api/admin/agents/freeze:
    post:
      operationId: freezeAgent
      summary: Freeze an agent (admin only)
      tags: [platform]
      description: Frozen agents block new publishes and return 451 in chat. UNDELETE-friendly — R2 content preserved.
      security:
        - serverSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id, reason]
              properties:
                id:
                  type: string
                reason:
                  type: string
                slug:
                  type: string
      responses:
        "200":
          description: Frozen.
        "401":
          $ref: "#/components/responses/Problem"

  # ── Directories — business-directory submission runs ───────────────────────
  #
  # Every branch below is derived from src/pages/api/directories/{run,update}.ts.
  # Both routes read the D1 binding first (503 `no_db`), then run
  # `authorizeWorkspace(locals, slug, DB, env)` — a session-cookie walk, so a
  # bearer world key is NOT accepted here.

  /api/directories/run:
    post:
      operationId: startDirectoryRun
      summary: Start a submission run across N directories
      description: |
        Inserts one `queued` row per named directory under a fresh `runId`, each
        carrying a submission pack FROZEN at run time — a later registry edit
        cannot retroactively change what was filed.
      tags: [directories]
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [slug, directories]
              properties:
                slug:
                  type: string
                  description: Workspace slug. Must pass the authority walk for the caller.
                directories:
                  type: array
                  minItems: 1
                  description: Directory slugs from the DIRECTORIES registry.
                  items:
                    type: string
      responses:
        "200":
          description: Run started.
          content:
            application/json:
              schema:
                type: object
                required: [runId, count]
                properties:
                  runId:
                    type: string
                    format: uuid
                  count:
                    type: integer
                    description: Number of rows inserted — equals `directories.length`.
        "400":
          description: |
            One of three, in the order the route checks them — `bad_slug` (the
            slug failed the charset guard), `directories required` (empty list),
            or `unknown_directory`, which additionally carries `unknown` listing
            the slugs absent from the registry.
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error:
                    type: string
                    enum: [bad_slug, "directories required", unknown_directory]
                  unknown:
                    type: array
                    description: Present only on `unknown_directory`.
                    items:
                      type: string
        "403":
          $ref: "#/components/responses/WorkspaceAuthzError"
        "503":
          $ref: "#/components/responses/WorkspaceAuthzError"
    get:
      operationId: getDirectoryRun
      summary: Read one run's per-directory status
      tags: [directories]
      security:
        - sessionCookie: []
      parameters:
        - name: slug
          in: query
          required: true
          schema:
            type: string
        - name: runId
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: |
            The run's rows. An unknown `runId` is NOT an error — it returns an
            empty `submissions` array.
          content:
            application/json:
              schema:
                type: object
                required: [submissions]
                properties:
                  submissions:
                    type: array
                    items:
                      $ref: "#/components/schemas/DirectorySubmission"
        "400":
          description: "`bad_slug`, or `runId required` when the query param is absent."
          $ref: "#/components/responses/SimpleError"
        "403":
          $ref: "#/components/responses/WorkspaceAuthzError"
        "503":
          $ref: "#/components/responses/WorkspaceAuthzError"

  /api/directories/update:
    post:
      operationId: updateDirectorySubmission
      summary: Walk one submission row to its next status
      description: |
        The status walk is data, not branching — `ALLOWED_TRANSITIONS` in
        src/pages/api/directories/update.ts permits `queued → submitted`,
        `submitted → live` and `submitted → rejected`. `live` and `rejected` are
        terminal: any transition out of them is refused. `filedBy` is recorded
        only on the move to `submitted` (defaulting to `human`); `listingUrl` and
        `note` are COALESCEd, so omitting one preserves the stored value.
      tags: [directories]
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [slug, id, status]
              properties:
                slug:
                  type: string
                id:
                  type: string
                  description: The `directory_submissions` row id. Scoped to `slug` — a row in another workspace reads as 404.
                status:
                  type: string
                  enum: [submitted, live, rejected]
                listingUrl:
                  type: string
                  description: REQUIRED when `status` is `live`.
                note:
                  type: string
                filedBy:
                  type: string
                  description: Recorded only when `status` is `submitted`. Defaults to `human`.
      responses:
        "200":
          description: Row updated.
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    const: true
        "400":
          description: |
            `bad_slug`; `id and status required`; `invalid_transition` (which
            carries `from` and `to`); or `listingUrl required to mark live`.
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error:
                    type: string
                    enum: [bad_slug, "id and status required", invalid_transition, "listingUrl required to mark live"]
                  from:
                    type: string
                    description: Present only on `invalid_transition` — the row's current status.
                  to:
                    type: string
                    description: Present only on `invalid_transition` — the status that was refused.
        "403":
          $ref: "#/components/responses/WorkspaceAuthzError"
        "404":
          description: 'No row with that id in that workspace — `{ "error": "not found" }`.'
          $ref: "#/components/responses/SimpleError"
        "503":
          $ref: "#/components/responses/WorkspaceAuthzError"

  # ── Lifecycle ──────────────────────────────────────────────────────────────
  #
  # Every response on these three routes — successes AND authorization refusals —
  # uses the `errJson`/`json` pair from src/lib/analytics/http.ts, so an error is
  # `{ ok: false, error }`, not a bare `{ error }`.

  /api/lifecycle/counts:
    get:
      operationId: lifecycleCounts
      summary: Per-stage occupancy for one named lifecycle
      description: |
        `count` is current occupancy (the back-compat key); `reached` is
        cumulative funnel reach. A workspace with no such lifecycle gets
        `counts: {}` at 200 — an empty board is a different fact from a refusal.
      tags: [lifecycle]
      security:
        - sessionCookie: []
      parameters:
        - name: slug
          in: query
          required: true
          schema:
            type: string
        - name: lifecycle
          in: query
          description: Lifecycle id. Defaults to `default`.
          schema:
            type: string
            default: default
      responses:
        "200":
          description: Stage counts keyed by stage id.
          content:
            application/json:
              schema:
                type: object
                required: [ok, lifecycle, counts]
                properties:
                  ok:
                    type: boolean
                    const: true
                  lifecycle:
                    type: string
                  counts:
                    type: object
                    additionalProperties:
                      type: object
                      required: [count, reached]
                      properties:
                        count:
                          type: integer
                        reached:
                          type: integer
        "400":
          description: "`missing slug`, or `bad_slug` from the authority walk."
          $ref: "#/components/responses/OkFalseError"
        "403":
          description: "`forbidden`."
          $ref: "#/components/responses/OkFalseError"
        "503":
          description: "`no_db`."
          $ref: "#/components/responses/OkFalseError"

  /api/lifecycle/defs:
    get:
      operationId: lifecycleDefs
      summary: Every lifecycle in a workspace, with live stage occupancy
      description: |
        The read `/u/<slug>/lifecycles` does server-side, exposed for client
        islands. A workspace with no lifecycles gets `[]` at 200.
      tags: [lifecycle]
      security:
        - sessionCookie: []
      parameters:
        - name: slug
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Lifecycles with per-stage counts.
          content:
            application/json:
              schema:
                type: object
                required: [ok, lifecycles]
                properties:
                  ok:
                    type: boolean
                    const: true
                  lifecycles:
                    type: array
                    items:
                      type: object
                      required: [id, name, stages]
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        stages:
                          type: array
                          items:
                            type: object
                            required: [id, label, slot, count, reached]
                            properties:
                              id:
                                type: string
                              label:
                                type: string
                              slot:
                                type: integer
                              count:
                                type: integer
                              reached:
                                type: integer
        "400":
          description: "`missing slug`, or `bad_slug`."
          $ref: "#/components/responses/OkFalseError"
        "403":
          description: "`forbidden`."
          $ref: "#/components/responses/OkFalseError"
        "503":
          description: "`no_db`."
          $ref: "#/components/responses/OkFalseError"

  /api/lifecycle/def:
    get:
      operationId: getLifecycleDef
      summary: Read one lifecycle definition
      tags: [lifecycle]
      security:
        - sessionCookie: []
      parameters:
        - name: slug
          in: query
          required: true
          schema:
            type: string
        - name: lifecycle
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The definition.
          content:
            application/json:
              schema:
                type: object
                required: [ok, def]
                properties:
                  ok:
                    type: boolean
                    const: true
                  def:
                    $ref: "#/components/schemas/LifecycleDef"
        "400":
          description: "`missing_params` (either query param absent), or `bad_slug`."
          $ref: "#/components/responses/OkFalseError"
        "403":
          description: "`forbidden`."
          $ref: "#/components/responses/OkFalseError"
        "404":
          description: "`not_found` — no such lifecycle in that workspace."
          $ref: "#/components/responses/OkFalseError"
        "503":
          description: "`no_db`."
          $ref: "#/components/responses/OkFalseError"
    post:
      operationId: saveLifecycleDef
      summary: Upsert a lifecycle definition
      description: |
        Gated by `requireAuthOver(request, 'manage_lifecycle', slug)` — a session
        cookie OR a bearer key, unlike the GET on this same path which uses the
        `authorizeWorkspace` session walk. The target workspace comes from the
        QUERY STRING, never the body. `saveDef` bumps `version` on every write.
        The per-workspace definition cap is checked only when the id is new.
      tags: [lifecycle]
      security:
        - sessionCookie: []
        - bearerKey: []
      parameters:
        - name: slug
          in: query
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LifecycleDef"
      responses:
        "201":
          description: Saved. `def` is re-read from D1 after the write.
          content:
            application/json:
              schema:
                type: object
                required: [ok, def]
                properties:
                  ok:
                    type: boolean
                    const: true
                  def:
                    oneOf:
                      - $ref: "#/components/schemas/LifecycleDef"
                      - type: "null"
        "400":
          description: "`missing_slug`, or `invalid_def` when `id` is not a string or `stages` is not an array."
          $ref: "#/components/responses/OkFalseError"
        "401":
          $ref: "#/components/responses/AuthErrorResponse"
        "402":
          $ref: "#/components/responses/AuthErrorResponse"
        "403":
          $ref: "#/components/responses/AuthErrorResponse"
        "422":
          description: Workspace is at or above the definition cap; `error` carries the refusal reason.
          $ref: "#/components/responses/OkFalseError"
        "503":
          description: "`no_db` (OkFalseError shape), or an authority/audit failure (AuthErrorResponse shape)."
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    required: [ok, error]
                    properties:
                      ok:
                        type: boolean
                        const: false
                      error:
                        type: string
                        const: no_db
                  - type: object
                    required: [error, reason]
                    properties:
                      error:
                        type: string
                        const: unauthorized
                      reason:
                        type: string
    delete:
      operationId: deleteLifecycleDef
      summary: Remove a lifecycle definition and its stages
      description: |
        `manage_lifecycle`-gated. The `stage_transition` log is deliberately left
        intact — it is the immutable audit, not the design.
      tags: [lifecycle]
      security:
        - sessionCookie: []
        - bearerKey: []
      parameters:
        - name: slug
          in: query
          required: true
          schema:
            type: string
        - name: lifecycle
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: 'Removed. Idempotent — deleting an absent definition also returns `{ ok: true }`.'
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    const: true
        "400":
          description: "`missing_params`."
          $ref: "#/components/responses/OkFalseError"
        "401":
          $ref: "#/components/responses/AuthErrorResponse"
        "402":
          $ref: "#/components/responses/AuthErrorResponse"
        "403":
          $ref: "#/components/responses/AuthErrorResponse"
        "503":
          description: "`no_db` (OkFalseError shape), or an authority/audit failure (AuthErrorResponse shape)."
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    required: [ok, error]
                    properties:
                      ok:
                        type: boolean
                        const: false
                      error:
                        type: string
                        const: no_db
                  - type: object
                    required: [error, reason]
                    properties:
                      error:
                        type: string
                        const: unauthorized
                      reason:
                        type: string

  # ── Themes ─────────────────────────────────────────────────────────────────
  #
  # These four routes authenticate with their OWN bearer scheme (`checkAuth` in
  # each file), not the session cookie: `Bearer <SERVER_SECRET>` resolves to the
  # owner id `server`, and `Bearer <ownerId>:<SERVER_SECRET>` acts as that owner.
  # Every route answers 503 `not configured` when DB or SERVER_SECRET is unbound.

  /api/themes:
    get:
      operationId: listThemes
      summary: List public themes, plus the caller's own
      description: |
        The bearer token is OPTIONAL here. Without one, `mine` is `[]` and only
        the public community themes are returned; there is no 401 on this route.
      tags: [themes]
      security:
        - themeBearer: []
        - {}
      responses:
        "200":
          description: Themes.
          content:
            application/json:
              schema:
                type: object
                required: [mine, community]
                properties:
                  mine:
                    type: array
                    description: Empty when the caller presents no valid bearer token.
                    items:
                      $ref: "#/components/schemas/Theme"
                  community:
                    type: array
                    description: Themes with `is_public = 1`.
                    items:
                      $ref: "#/components/schemas/Theme"
        "503":
          description: 'DB or SERVER_SECRET unbound — `{ "error": "not configured" }`.'
          $ref: "#/components/responses/SimpleError"

  /api/themes/{id}:
    get:
      operationId: getTheme
      summary: Read one theme
      description: Unauthenticated — any theme is readable by id, public or not.
      tags: [themes]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The theme.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Theme"
        "400":
          description: "`id required`."
          $ref: "#/components/responses/SimpleError"
        "404":
          description: "`not found`."
          $ref: "#/components/responses/SimpleError"
        "503":
          description: "`not configured`."
          $ref: "#/components/responses/SimpleError"
    patch:
      operationId: patchTheme
      summary: Replace a theme's tokens
      description: |
        `tokens` is the only writable field and the zod body schema accepts
        nothing else. NOTE: the update is not owner-scoped — any valid bearer
        token may patch any theme by id.
      tags: [themes]
      security:
        - themeBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tokens]
              properties:
                tokens:
                  type: string
      responses:
        "200":
          description: The theme, re-read after the write.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Theme"
        "400":
          description: "`id required`, or `invalid body` when `tokens` is absent or not a string."
          $ref: "#/components/responses/SimpleError"
        "401":
          description: "`unauthorized` — no bearer token, or one that did not match SERVER_SECRET."
          $ref: "#/components/responses/SimpleError"
        "404":
          description: "`not found`."
          $ref: "#/components/responses/SimpleError"
        "503":
          description: "`not configured`."
          $ref: "#/components/responses/SimpleError"

  /api/themes/{id}/fork:
    post:
      operationId: forkTheme
      summary: Copy a theme into the caller's ownership
      description: |
        No request body. The new row takes the caller's owner id — `server` for a
        bare `Bearer <SERVER_SECRET>`, else the `<ownerId>` prefix of the token.
      tags: [themes]
      security:
        - themeBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "201":
          description: The new theme.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Theme"
        "400":
          description: "`id required`."
          $ref: "#/components/responses/SimpleError"
        "401":
          description: "`unauthorized`."
          $ref: "#/components/responses/SimpleError"
        "404":
          description: "`not found` — the source theme does not exist."
          $ref: "#/components/responses/SimpleError"
        "503":
          description: "`not configured`."
          $ref: "#/components/responses/SimpleError"

  /api/themes/{id}/share:
    post:
      operationId: shareTheme
      summary: Publish a theme to the community list
      description: |
        Sets `is_public = 1`, and unlike PATCH this write IS owner-scoped — the
        UPDATE is bound to `owner_id = <caller>`.
      tags: [themes]
      security:
        - themeBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Published.
          content:
            application/json:
              schema:
                type: object
                required: [ok, shareUrl]
                properties:
                  ok:
                    type: boolean
                    const: true
                  shareUrl:
                    type: string
                    description: "`/design?theme=<id>`."
        "400":
          description: "`id required`."
          $ref: "#/components/responses/SimpleError"
        "401":
          description: "`unauthorized`."
          $ref: "#/components/responses/SimpleError"
        "404":
          description: "`not found`, or `not found or not owner` when the owner-scoped UPDATE did not apply."
          $ref: "#/components/responses/SimpleError"
        "503":
          description: "`not configured`."
          $ref: "#/components/responses/SimpleError"

  # ── CRM ────────────────────────────────────────────────────────────────────
  #
  # All three are session-cookie reads gated by `authorizeWorkspace`. Note the
  # error string differs from the directories group: these answer `db_unavailable`
  # at 503 when the D1 binding is missing, before the authority walk runs.

  /api/crm/combo-context:
    get:
      operationId: crmComboContext
      summary: Prospect context for a tag combination
      description: |
        Reads the shared combo KV entry (the same key `/api/brain/combos` writes)
        and marks the path on a hit. On a miss it resolves from BrainDO plus D1
        and writes the entry back with a strength-derived TTL — so a 200 is
        returned either way and `hit` is always `true`.
      tags: [crm]
      security:
        - sessionCookie: []
      parameters:
        - name: tags
          in: query
          required: true
          description: Comma-separated. Lower-cased and trimmed; at most 8.
          schema:
            type: string
        - name: workspace
          in: query
          description: Defaults to the caller's own slug.
          schema:
            type: string
      responses:
        "200":
          description: Combo context.
          content:
            application/json:
              schema:
                type: object
                required: [tags, strength, primaryStage, suggestedAction, lastThread, recentThreads, matchedWorkflows, cachedAt, hit]
                properties:
                  tags:
                    type: array
                    items:
                      type: string
                  strength:
                    type: number
                  primaryStage:
                    type: [string, "null"]
                  suggestedAction:
                    description: The top matched workflow, or null.
                    oneOf:
                      - type: object
                        properties:
                          id:
                            type: string
                          name:
                            type: string
                          tags:
                            type: array
                            items:
                              type: string
                      - type: "null"
                  lastThread:
                    oneOf:
                      - type: object
                        properties:
                          id:
                            type: string
                          lastMsgAt:
                            type: integer
                      - type: "null"
                  recentThreads:
                    type: array
                    maxItems: 3
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        lastMsgAt:
                          type: integer
                  matchedWorkflows:
                    type: array
                    maxItems: 3
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        tags:
                          type: array
                          items:
                            type: string
                  cachedAt:
                    type: integer
                    description: Epoch seconds.
                  hit:
                    type: boolean
                    const: true
        "400":
          description: |
            `bad_slug` from the authority walk, or a tag validation refusal —
            `no_tags`, `max_8_tags`, or `invalid_tag:<tag>` naming the offender.
          $ref: "#/components/responses/SimpleError"
        "403":
          $ref: "#/components/responses/WorkspaceAuthzError"
        "503":
          description: "`db_unavailable` — the D1 binding is absent."
          $ref: "#/components/responses/SimpleError"

  /api/crm/starters:
    get:
      operationId: crmStarters
      summary: Hottest tag combos as conversation openers
      description: |
        Scans at most 200 `combo:<workspace>:` KV keys, keeps combos at a sales
        lifecycle stage (`consideration`, `decision`, `purchase`, `upsell`,
        `win-back`) OR with strength above 50, and returns the top N by strength.
        With no KV binding it answers `{ "starters": [] }` — and that body omits
        `workspace`.
      tags: [crm]
      security:
        - sessionCookie: []
      parameters:
        - name: workspace
          in: query
          description: Defaults to the caller's own slug.
          schema:
            type: string
        - name: limit
          in: query
          description: Clamped to 20.
          schema:
            type: integer
            default: 5
            maximum: 20
      responses:
        "200":
          description: Ranked openers.
          content:
            application/json:
              schema:
                type: object
                required: [starters]
                properties:
                  starters:
                    type: array
                    items:
                      type: object
                      required: [tags, strength, lifecycles, suggestedOpener, workflows, recentThreads]
                      properties:
                        tags:
                          type: array
                          items:
                            type: string
                        strength:
                          type: number
                        lifecycles:
                          type: array
                          items:
                            type: string
                        suggestedOpener:
                          type: string
                          description: Template text chosen by lifecycle stage. Not model-generated.
                        workflows:
                          type: array
                          maxItems: 2
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                              name:
                                type: string
                              tags:
                                type: array
                                items:
                                  type: string
                        recentThreads:
                          type: array
                          maxItems: 2
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                              lastMsgAt:
                                type: integer
                  workspace:
                    type: string
                    description: Absent when the KV binding is missing.
        "400":
          description: "`bad_slug`."
          $ref: "#/components/responses/WorkspaceAuthzError"
        "403":
          $ref: "#/components/responses/WorkspaceAuthzError"
        "503":
          description: "`db_unavailable`."
          $ref: "#/components/responses/SimpleError"

  /api/crm/hot-accounts:
    get:
      operationId: crmHotAccounts
      summary: Recently active accounts (JSON snapshot or SSE stream)
      description: |
        Dual-mode. With `?live=1` or `Accept: text/event-stream` it returns an SSE
        stream emitting an `accounts` event immediately and every 15s until the
        client aborts; otherwise one JSON snapshot with `Cache-Control: private,
        max-age=30`.

        CAVEAT, and it is load-bearing for a client: the payload is currently
        HARD-CODED sample data (`sampleAccounts()` in
        src/pages/api/crm/hot-accounts.ts) — three fixed accounts with
        `whenMs` offsets from now. The shape is real; the rows are not.
      tags: [crm]
      security:
        - sessionCookie: []
      parameters:
        - name: slug
          in: query
          description: Defaults to the caller's workspace context.
          schema:
            type: string
        - name: live
          in: query
          description: "`1` forces the SSE stream."
          schema:
            type: string
            enum: ["1"]
      responses:
        "200":
          description: A JSON snapshot, or an SSE stream of `accounts` events.
          content:
            application/json:
              schema:
                type: object
                required: [accounts, freshnessMs]
                properties:
                  accounts:
                    type: array
                    items:
                      type: object
                      required: [id, name, sentence, whenMs, spark]
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        sentence:
                          type: string
                        whenMs:
                          type: integer
                        spark:
                          type: array
                          items:
                            type: number
                  freshnessMs:
                    type: integer
            text/event-stream:
              schema:
                type: string
                description: "`event: accounts` frames whose `data` is the JSON body above."
        "400":
          description: "`slug required` when no slug can be resolved, or `bad_slug`."
          $ref: "#/components/responses/SimpleError"
        "403":
          $ref: "#/components/responses/WorkspaceAuthzError"
        "503":
          $ref: "#/components/responses/WorkspaceAuthzError"

  # ── Tags ───────────────────────────────────────────────────────────────────
  #
  # Two things a client must know before generating against this group:
  #   • a missing D1 binding answers 500 here, NOT 503 as elsewhere in the spec;
  #   • DELETE runs no `authorizeWorkspace` walk — it is guarded only by the
  #     namespace's own `locked` flag.
  # PUT /api/tags is deliberately UNDOCUMENTED: it validates and echoes back but
  # persists nothing and performs no authority walk.

  /api/tags:
    get:
      operationId: listTagNamespaces
      summary: List a workspace's tag namespaces
      tags: [tags]
      security:
        - sessionCookie: []
      parameters:
        - name: workspace
          in: query
          description: Falls back to `locals.workspaceId`, then the literal `default`.
          schema:
            type: string
      responses:
        "200":
          description: Namespaces, plus the framework namespaces no tenant may redefine.
          content:
            application/json:
              schema:
                type: object
                required: [namespaces, locked]
                properties:
                  namespaces:
                    type: array
                    items:
                      $ref: "#/components/schemas/TagNamespace"
                  locked:
                    type: array
                    description: "`lifecycle`, `iab`, `gdpr`, `ccpa`, `status`, `sla`."
                    items:
                      type: string
        "400":
          description: "`bad_slug`."
          $ref: "#/components/responses/WorkspaceAuthzError"
        "403":
          $ref: "#/components/responses/WorkspaceAuthzError"
        "500":
          description: "`no_db` — note the status, which is 500 on this route and not 503."
          $ref: "#/components/responses/SimpleError"
    post:
      operationId: createTagNamespace
      summary: Create a tag namespace
      description: |
        The insert is `ON CONFLICT DO NOTHING`, so re-posting an existing
        namespace returns it unchanged at 200 rather than erroring.
      tags: [tags]
      security:
        - sessionCookie: []
      parameters:
        - name: workspace
          in: query
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: "`namespace` is the field; `ns` is accepted as an alias."
              properties:
                namespace:
                  type: string
                ns:
                  type: string
                values:
                  type: array
                  description: Omit or send `[]` for a freeform namespace.
                  items:
                    type: string
                colour:
                  type: string
                  default: neutral
                acl:
                  type: object
                  description: "Defaults to `{ read: [\"*\"], write: [\"owner\", \"agency\"] }`."
                  required: [read, write]
                  properties:
                    read:
                      type: array
                      items:
                        type: string
                    write:
                      type: array
                      items:
                        type: string
      responses:
        "200":
          description: The namespace.
          content:
            application/json:
              schema:
                type: object
                required: [namespace]
                properties:
                  namespace:
                    $ref: "#/components/schemas/TagNamespace"
        "400":
          description: "`namespace required`, or `bad_slug`."
          $ref: "#/components/responses/SimpleError"
        "403":
          description: |
            `forbidden` from the authority walk, or `locked` with the offending
            `namespace` echoed back when the name is a framework namespace.
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error:
                    type: string
                    enum: [forbidden, locked]
                  namespace:
                    type: string
        "409":
          description: |
            `createNamespace` returned null, so the row could not be read back
            after the insert. The body is `{ "namespace": null }` — there is no
            `error` key on this branch. BELIEVED UNREACHABLE in practice: the
            only other way that function returns null is a locked namespace, and
            the route 403s those before it is called. Do not build a retry path
            on this branch without reproducing it first.
          content:
            application/json:
              schema:
                type: object
                required: [namespace]
                properties:
                  namespace:
                    type: "null"
        "500":
          description: "`no_db`."
          $ref: "#/components/responses/SimpleError"
    delete:
      operationId: deleteTagNamespace
      summary: Delete a tag namespace
      description: |
        NO workspace authority walk runs on this method — the only guard is the
        namespace's stored `locked` flag. The id embeds the workspace, so a
        caller must already know it.
      tags: [tags]
      parameters:
        - name: id
          in: query
          required: true
          description: "`<workspace>:<namespace>`."
          schema:
            type: string
      responses:
        "200":
          description: Deleted.
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    const: true
        "400":
          description: "`id required`."
          $ref: "#/components/responses/SimpleError"
        "403":
          description: |
            The namespace is locked, or there is no row with that id — both
            answer `{ "ok": false }` at 403, with no `error` key.
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    const: false
        "500":
          description: "`no_db`."
          $ref: "#/components/responses/SimpleError"

  /api/tags/suggest:
    get:
      operationId: listTagSuggestions
      summary: Ad-hoc tags above a usage threshold
      description: Returns at most 100 `pending` suggestions, hottest first.
      tags: [tags]
      security:
        - sessionCookie: []
      parameters:
        - name: workspace
          in: query
          description: Defaults to the literal `default`.
          schema:
            type: string
        - name: threshold
          in: query
          description: Minimum `usage_count`.
          schema:
            type: integer
            default: 20
      responses:
        "200":
          description: Suggestions.
          content:
            application/json:
              schema:
                type: object
                required: [suggestions, threshold]
                properties:
                  suggestions:
                    type: array
                    maxItems: 100
                    items:
                      $ref: "#/components/schemas/TagSuggestion"
                  threshold:
                    type: number
        "400":
          description: "`bad_slug`."
          $ref: "#/components/responses/WorkspaceAuthzError"
        "403":
          $ref: "#/components/responses/WorkspaceAuthzError"
        "500":
          description: "`no_db`."
          $ref: "#/components/responses/SimpleError"
    post:
      operationId: recordTagSuggestion
      summary: Record one sighting of an ad-hoc tag
      description: |
        Upsert keyed on `<workspace>:<tag>` — a repeat sighting increments
        `usage_count` and moves `last_seen` rather than inserting a second row.
      tags: [tags]
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [workspace, tag]
              properties:
                workspace:
                  type: string
                tag:
                  type: string
                cluster_id:
                  type: string
      responses:
        "200":
          description: Recorded.
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    const: true
        "400":
          description: "`workspace + tag required`, or `bad_slug`."
          $ref: "#/components/responses/SimpleError"
        "403":
          $ref: "#/components/responses/WorkspaceAuthzError"
        "500":
          description: "`no_db`."
          $ref: "#/components/responses/SimpleError"

  # ── Leads ──────────────────────────────────────────────────────────────────

  /api/leads:
    post:
      operationId: captureLead
      summary: Capture an inbound lead
      description: |
        PUBLIC — no authentication and no workspace scope. The row is keyed on
        the lower-cased email, so re-posting the same address updates rather than
        duplicating: `visitor_hash` is kept if already set, and `persona`, `team`
        and `attrs` are overwritten only when the new payload supplies them.
        Every optional string is truncated server-side (`visitor_hash` 64,
        `persona` 32, `team` 32, `source` 64).
      tags: [leads]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                  maxLength: 254
                visitor_hash:
                  type: string
                persona:
                  type: string
                team:
                  type: string
                source:
                  type: string
                  default: home-chat
                attrs:
                  type: object
                  additionalProperties: true
      responses:
        "200":
          description: Captured.
          content:
            application/json:
              schema:
                type: object
                required: [ok, lead_id]
                properties:
                  ok:
                    type: boolean
                    const: true
                  lead_id:
                    type: string
                    description: "`lead_` + the email slugified to `[a-z0-9-]`, truncated to 64."
        "400":
          description: "`bad_json`, or `invalid_email` when the address fails the regex or exceeds 254 chars."
          $ref: "#/components/responses/SimpleError"
        "503":
          description: "`no_db`."
          $ref: "#/components/responses/SimpleError"

  # ── Notifications ──────────────────────────────────────────────────────────
  #
  # Both methods run `requireAuth(request, 'read_memory')` FIRST — so a failure
  # there answers `{ error: "unauthorized", reason }` — and only then the
  # `authorizeWorkspace` walk, which answers the bare `{ error }` shape.

  /api/notifications:
    get:
      operationId: listNotifications
      summary: Read the workspace notification feed
      description: |
        Returns the 50 most recent rows, newest first, as a BARE ARRAY — not an
        object wrapper. The one exception is `?type=substrate`, which is a
        declared placeholder: it always answers
        `{ "notifications": [], "source": "substrate" }` and never reads D1.
      tags: [notifications]
      security:
        - sessionCookie: []
        - bearerKey: []
      parameters:
        - name: slug
          in: query
          description: Defaults to the authenticated principal's slug.
          schema:
            type: string
        - name: type
          in: query
          description: "`substrate` selects the placeholder branch described above."
          schema:
            type: string
            enum: [substrate]
      responses:
        "200":
          description: The feed, or the substrate placeholder.
          content:
            application/json:
              schema:
                oneOf:
                  - type: array
                    maxItems: 50
                    items:
                      type: object
                      required: [id, kind, payload, ts, read]
                      properties:
                        id:
                          type: string
                        kind:
                          type: string
                        payload:
                          type: [string, "null"]
                          description: Serialised JSON, or null. Not parsed by the route.
                        ts:
                          type: integer
                        read:
                          type: integer
                          enum: [0, 1]
                  - type: object
                    required: [notifications, source]
                    properties:
                      notifications:
                        type: array
                        items:
                          type: object
                      source:
                        type: string
                        const: substrate
        "401":
          $ref: "#/components/responses/AuthErrorResponse"
        "402":
          $ref: "#/components/responses/AuthErrorResponse"
        "403":
          description: |
            `authErrorResponse` shape when `requireAuth` refuses; the bare
            `{ error: "forbidden" }` shape when the workspace walk refuses.
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    required: [error, reason]
                    properties:
                      error:
                        type: string
                        const: unauthorized
                      reason:
                        type: string
                  - type: object
                    required: [error]
                    properties:
                      error:
                        type: string
                        enum: [bad_slug, forbidden]
        "503":
          description: |
            The D1 binding is unbound — the body is the PLAIN TEXT
            `not configured`, not JSON. An authority failure from `requireAuth`
            also lands here as JSON.
          content:
            text/plain:
              schema:
                type: string
                const: not configured
            application/json:
              schema:
                type: object
                required: [error, reason]
                properties:
                  error:
                    type: string
                    const: unauthorized
                  reason:
                    type: string
    post:
      operationId: markNotificationsRead
      summary: Mark one notification, or all of them, as read
      description: |
        `?action=mark-read` is the only action implemented. With `id` it marks
        that row; without `id` it marks every row for the slug. Any other
        `action` (or none) is a NO-OP that still answers `{ ok: true }`.
      tags: [notifications]
      security:
        - sessionCookie: []
        - bearerKey: []
      parameters:
        - name: action
          in: query
          schema:
            type: string
            enum: [mark-read]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                slug:
                  type: string
                  description: Defaults to the authenticated principal's slug.
                id:
                  type: string
                  description: Omit to mark every notification for the slug.
      responses:
        "200":
          description: Applied — or accepted as a no-op when `action` is not `mark-read`.
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    const: true
        "400":
          description: Body was not valid JSON — the PLAIN TEXT `invalid json`, not JSON.
          content:
            text/plain:
              schema:
                type: string
                const: invalid json
        "401":
          $ref: "#/components/responses/AuthErrorResponse"
        "402":
          $ref: "#/components/responses/AuthErrorResponse"
        "403":
          description: |
            `authErrorResponse` shape when `requireAuth` refuses; the bare
            `{ error }` shape when the workspace walk refuses.
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    required: [error, reason]
                    properties:
                      error:
                        type: string
                        const: unauthorized
                      reason:
                        type: string
                  - type: object
                    required: [error]
                    properties:
                      error:
                        type: string
                        enum: [bad_slug, forbidden]
        "503":
          description: The PLAIN TEXT `not configured`, or an authority failure as JSON.
          content:
            text/plain:
              schema:
                type: string
                const: not configured
            application/json:
              schema:
                type: object
                required: [error, reason]
                properties:
                  error:
                    type: string
                    const: unauthorized
                  reason:
                    type: string
