{
 "version": 1,
 "operations": [
  {
   "method": "POST",
   "path": "/runs",
   "role": "member",
   "summary": "Begin a run, and say so before doing any of it. The row exists from this moment, which is what makes a run that is still happening — or one whose process died holding it — something you can see.",
   "body": {
    "kind": "what sort of work this is",
    "label": "what it is about, for a person reading a list",
    "self": "the handle of the self doing it",
    "parent": "the run that asked, when this is part of a swarm"
   }
  },
  {
   "method": "POST",
   "path": "/runs/:id/steps",
   "role": "member",
   "summary": "Append a step. Send the frame you are already streaming: its type, its step name and its rawEvent. The same rows are what a watcher reads and what an auditor reads.",
   "body": {
    "type": "the event type, e.g. StepStarted",
    "name": "the step name, e.g. recall",
    "data": "the rawEvent",
    "cost": "what THIS run spent — never what its children spent, so a total is a sum"
   }
  },
  {
   "method": "POST",
   "path": "/runs/:id/finish",
   "role": "member",
   "summary": "End a run. Appends the closing step and stops the heartbeat mattering.",
   "body": {
    "ok": "false if it failed",
    "why": "what went wrong, when it did"
   }
  },
  {
   "method": "GET",
   "path": "/runs",
   "role": "member",
   "summary": "Runs that are still open, oldest sign of life first. A run whose process died is one that is still open with a heartbeat that stopped — which is the thing a span cannot tell you.",
   "query": {
    "stale": "seconds without a step before a run is called stale; default 900"
   }
  },
  {
   "method": "GET",
   "path": "/runs/:id",
   "role": "member",
   "summary": "One run, everything it started, and its steps from the beginning. The same rows a live watcher is reading."
  },
  {
   "method": "GET",
   "path": "/runs/:id/steps",
   "role": "member",
   "summary": "Steps after a sequence number. Watching live is asking with the last seq you saw; replaying is asking with 0.",
   "query": {
    "after": "the last seq you have",
    "limit": "how many at most; default 500"
   }
  },
  {
   "method": "GET",
   "path": "/people/:key/memories",
   "role": "member",
   "summary": "What this person of yours has said is true of them, in the scope you ask for. General memories (no scope) come back everywhere.",
   "query": {
    "scope": "your own word for the part of your product this call is in",
    "project": "your project id, when the call is inside one",
    "history": "1 to include memories that have been superseded"
   }
  },
  {
   "method": "POST",
   "path": "/people/:key/memories",
   "role": "member",
   "summary": "Keep something this person said. A restatement of something already held writes nothing and names what it matched.",
   "body": {
    "text": "their words, kept verbatim",
    "scope": "leave out for a memory true of them everywhere",
    "project": "your project id",
    "source": "stated|agreed|observed",
    "corrects": "the id of the memory this replaces; its validity is closed rather than overwritten",
    "label": "a name for the person, stored once"
   }
  },
  {
   "method": "DELETE",
   "path": "/people/:key/memories/:id",
   "role": "member",
   "summary": "Forget one. The row is removed, not flagged, and what was removed comes back."
  },
  {
   "method": "GET",
   "path": "/selves",
   "role": "member",
   "summary": "The tenant's selves with counts of what they hold.",
   "query": {
    "subject": "only the selves belonging to this user of yours"
   }
  },
  {
   "method": "POST",
   "path": "/selves",
   "role": "admin",
   "summary": "Give birth to a self. Fails if the handle exists.",
   "body": {
    "character": "optional {personality: Big Five 0..1 | genome: {effectsSeed, seed}, axes?: {stance: -1..1}, tastes?: {taste: -1..1}}: who it is beneath its charter",
    "procedure": "optional: how it works at birth",
    "handle": "lowercase id",
    "name": "what people call it",
    "charter": "{purpose, answersTo?, authority?: {reach?: source ids}, reviewBy?}",
    "persona": "{default?, surfaces?}",
    "interests": "string[]",
    "routine": "{mode?: scheduled|on-activity, read?, reflect?, reflectAfter?, maxCalls?, maxTokens?}",
    "identity": "[{kind, key, value}]",
    "seed": "{story?, lessons?: [{claim, appliesWhen?, surfaces?}]}",
    "subject": "the user of yours it belongs to",
    "program": "its own instructions per step, after the tenant's",
    "orient": "false to skip orientation (bulk births)",
    "status": "active|paused"
   }
  },
  {
   "method": "PUT",
   "path": "/selves/:handle",
   "role": "admin",
   "summary": "Declare a self: born if absent; otherwise what a person sets (name, charter, persona, routine, interests) is brought in line. Identity and history are never overwritten.",
   "body": {
    "...": "as POST /selves, without handle"
   }
  },
  {
   "method": "GET",
   "path": "/selves/:handle",
   "role": "member",
   "summary": "A self in full: identity, lessons as the core holds them, its procedure and trials, the faults it carries, what is true now, recent episodes, proposals, usage, jobs, outcomes, record integrity."
  },
  {
   "method": "PATCH",
   "path": "/selves/:handle",
   "role": "admin",
   "summary": "Change what a person sets.",
   "body": {
    "name": "",
    "handle": "a new handle; the self keeps its whole life",
    "charter": "",
    "persona": "",
    "routine": "",
    "interests": "",
    "program": "",
    "status": "active|paused"
   }
  },
  {
   "method": "GET",
   "path": "/selves/:handle/procedure",
   "role": "member",
   "summary": "How this self does the work: the adopted text, every candidate on trial with the gate's reading of it, and every version with the diff and the items that decided it."
  },
  {
   "method": "PUT",
   "path": "/selves/:handle/procedure",
   "role": "admin",
   "summary": "Write the procedure yourself. Ends any trial; recorded as authority, never as evidence.",
   "body": {
    "text": "how this self should do the work",
    "why": "optional reason"
   }
  },
  {
   "method": "POST",
   "path": "/selves/:handle/procedure/revert",
   "role": "admin",
   "summary": "Go back to an earlier procedure.",
   "body": {
    "version": "which version",
    "why": "optional reason"
   }
  },
  {
   "method": "POST",
   "path": "/selves/:handle/intake",
   "role": "member",
   "summary": "What a self with a character takes in: news, posts, conversations, literature, media, experiences, meals, purchases. Moves its stances, tastes, temperament, affect and capacities as its personality lets it; never how it works.",
   "body": {
    "items": "[{kind, source, stance?: {axis: -1..1}, intensity?: 0..1, arousal?: 0..1, valence?: -1..1, demand?: 0..1, taste?: {name: -1..1}, body?: {nutrition?, rest?, strain?}}]"
   }
  },
  {
   "method": "POST",
   "path": "/selves/:handle/settle",
   "role": "member",
   "summary": "The world settled something a stance bears on: the stance is pulled toward it and sources that pushed the other way lose trust. Grounding, kept apart from drift.",
   "body": {
    "axis": "the stance",
    "truth": "where the outcome sits, -1..1",
    "outcome": "what settled it",
    "weight": "optional 0..2"
   }
  },
  {
   "method": "POST",
   "path": "/selves/:handle/character/event",
   "role": "admin",
   "summary": "A life event moves temperament directly, scaled by the self's sensitivity; part of it fades back toward its set point.",
   "body": {
    "name": "the event",
    "delta": "{openness?, conscientiousness?, extraversion?, agreeableness?, neuroticism?}, each within ±0.3"
   }
  },
  {
   "method": "POST",
   "path": "/selves/:handle/identity",
   "role": "admin",
   "summary": "A person sets or removes one identity row directly.",
   "body": {
    "kind": "value|trait|commitment|taste|voice|expertise|relationship",
    "key": "",
    "value": "null removes"
   }
  },
  {
   "method": "POST",
   "path": "/selves/:handle/context",
   "role": "member",
   "summary": "The self's context for one call on one surface, and a ticket for its work and outcome.",
   "body": {
    "surface": "where it is being asked",
    "task": "what it is being asked",
    "item": "the item this work is on, in your own terms; a trial pairs on it (default: the ticket)",
    "taskClass": "the kind of task; faults are kept per class (default: default)",
    "interactive": "false for background calls",
    "person": "your own identifier for the person this turn is for; the self is told what they have said is true of them",
    "personLabel": "a name for that person, stored once",
    "personScope": "your word for the part of your product this call is in",
    "personProject": "your project id, when the call is inside one"
   }
  },
  {
   "method": "POST",
   "path": "/selves/:handle/run",
   "role": "admin",
   "summary": "Queue a routine step now.",
   "body": {
    "job": "orient|read|reflect|dream|battery|maintain|fetch"
   }
  },
  {
   "method": "GET",
   "path": "/selves/:handle/export",
   "role": "member",
   "summary": "Identity, lessons and the whole hash-chained record, without authority."
  },
  {
   "method": "GET",
   "path": "/selves/:handle/record",
   "role": "member",
   "summary": "The self's record, in order, a page at a time: every episode and decision as a hash-chained entry.",
   "query": {
    "after": "the last seq you hold (default: from the start)",
    "limit": "up to 1000 (default 200)",
    "type": "only entries of this type or its family, e.g. practice or belief.moved"
   }
  },
  {
   "method": "POST",
   "path": "/selves/:handle/episodes",
   "role": "member",
   "summary": "Record something that happened to a self in your product, in your own type (a session kept, a lived day, a sale). It reflects on it like anything else.",
   "body": {
    "type": "your own lowercase name, e.g. session.kept",
    "brief": "what happened",
    "thought": "optional",
    "refs": "optional object",
    "surface": "optional",
    "importance": "0..1"
   }
  },
  {
   "method": "POST",
   "path": "/lessons/:id/contest",
   "role": "member",
   "summary": "A person doubts a lesson; it returns to being a hunch.",
   "body": {
    "by": "who",
    "why": "optional reason"
   }
  },
  {
   "method": "POST",
   "path": "/tickets/:id/work",
   "role": "member",
   "summary": "What was done with a context.",
   "body": {
    "brief": "what was asked and answered",
    "thought": "optional"
   }
  },
  {
   "method": "POST",
   "path": "/tickets/:id/outcome",
   "role": "member",
   "summary": "How it went, credited to the lessons served on the ticket. A person's verdict is a signal; what the world later did is happened. Both move belief, never practice. A check sent here is read as an outcome: only a registered checker, at POST /checks, can move how a self works.",
   "body": {
    "signal": "a person's verdict, -1..1",
    "happened": "what the world did: 1 or 0",
    "label": "useful, not useful, used...",
    "detail": "optional reason",
    "by": "who",
    "check": "optional {name, ran?, passed}: read as happened = passed"
   }
  },
  {
   "method": "GET",
   "path": "/checkers",
   "role": "member",
   "summary": "The instruments registered in this project: the only things that can mint a check."
  },
  {
   "method": "POST",
   "path": "/checkers",
   "role": "admin",
   "summary": "Register an instrument. Its key is shown once; verdicts sent with it are minted as checks, the only evidence that can change how a self works.",
   "body": {
    "id": "lowercase id",
    "name": "what the check is, e.g. npm test",
    "authoredBy": "who wrote its criteria; a check whose criteria a self wrote never moves that self's practice"
   }
  },
  {
   "method": "POST",
   "path": "/checkers/:id/revoke",
   "role": "admin",
   "summary": "Revoke an instrument's key. Checks it already minted stand in the record."
  },
  {
   "method": "POST",
   "path": "/checks",
   "role": "checker",
   "summary": "A checker's verdicts on a self's work, with the checker's own key. They close one episode: belief and the fault ledger read them, and a trial's gate when they are for one of its arms.",
   "body": {
    "self": "the self's handle (or ticket)",
    "ticket": "the ticket the work was done on; item, task class and version come from it",
    "taskClass": "the kind of task",
    "episode": "optional id for the episode these close",
    "results": "[{item, passed, ran?: complete|partial|inflight|never, ground?: what failed against what, detail?, practice?: the version that did the work}]"
   }
  },
  {
   "method": "POST",
   "path": "/observations",
   "role": "member",
   "summary": "Push what happened in your own systems. It is read as from inside the organisation.",
   "body": {
    "source": "id",
    "items": "[{title, body?, url?, publishedAt?}]",
    "audience": "handles, or empty for all",
    "trust": "0..1"
   }
  },
  {
   "method": "GET",
   "path": "/observations",
   "role": "member",
   "summary": "Recent observations, quarantined ones marked.",
   "query": {
    "limit": "1..200"
   }
  },
  {
   "method": "GET",
   "path": "/sources",
   "role": "member",
   "summary": "The tenant's sources and their last read."
  },
  {
   "method": "PUT",
   "path": "/sources/:id",
   "role": "admin",
   "summary": "Declare a source.",
   "body": {
    "kind": "feed|brave-news|brave-web|mcp|http",
    "label": "",
    "config": "connector settings; secrets by env or file name only",
    "channel": "news|social|trade|own",
    "lineage": "internal|external",
    "trust": "0..1",
    "scheduled": "bool",
    "audience": "handles",
    "every_minutes": ">=15"
   }
  },
  {
   "method": "GET",
   "path": "/proposals",
   "role": "member",
   "summary": "Suggested changes.",
   "query": {
    "status": "open|approved|rejected|superseded|all"
   }
  },
  {
   "method": "POST",
   "path": "/proposals/:id",
   "role": "admin",
   "summary": "Accept or decline a suggested change.",
   "body": {
    "decision": "approved|rejected",
    "by": "who"
   }
  },
  {
   "method": "GET",
   "path": "/events",
   "role": "member",
   "summary": "What happened, in order, after an id.",
   "query": {
    "after": "last id seen",
    "limit": "1..500"
   }
  },
  {
   "method": "GET",
   "path": "/swarms",
   "role": "member",
   "summary": "The tenant's swarms: its designs whose members are selves, with members, process, protocol and last run."
  },
  {
   "method": "PUT",
   "path": "/swarms/:id",
   "role": "admin",
   "summary": "Declare a swarm and its members (replaces membership): the next version of the design of selves called :id, keeping its process and protocol.",
   "body": {
    "name": "",
    "purpose": "",
    "members": "[{handle, weight?, role?}]"
   }
  },
  {
   "method": "GET",
   "path": "/swarms/:id",
   "role": "member",
   "summary": "A swarm (its design's latest version), its members and recent runs, each with its round."
  },
  {
   "method": "POST",
   "path": "/swarms/:id/runs",
   "role": "member",
   "summary": "Put one question to every member: a run of the swarm's design. Rounds (delphi, debate, critique-revise) follow on by themselves.",
   "body": {
    "question": "",
    "choices": "optional 2..8 strings",
    "brief": "optional background",
    "requestedBy": "who"
   }
  },
  {
   "method": "GET",
   "path": "/swarm-runs/:id",
   "role": "member",
   "summary": "A run: answers so far, tally (combined by the design's protocol), status, each answer's ticket, and the chain of rounds it belongs to."
  },
  {
   "method": "POST",
   "path": "/swarm-runs/:id/decide",
   "role": "member",
   "summary": "The principal picks: every member that chose is rated on whether its calls match the principal's. Moves belief, never practice.",
   "body": {
    "pick": "index of the chosen choice, from 0",
    "by": "who decided (default: the key's label)"
   }
  },
  {
   "method": "POST",
   "path": "/swarm-runs/:id/result",
   "role": "checker",
   "summary": "What the world chose, measured by a registered checker: one check per member that chose, on the run as the item (the only settlement that reaches a member's practice), and an outcome for the lessons its ticket served.",
   "body": {
    "pick": "index of what the world chose, from 0",
    "taskClass": "optional (default swarm:<id>)",
    "ground": "a short stable label for what measured it, e.g. crowd-meter; recurring failures are grouped by it",
    "detail": "optional narrative"
   }
  },
  {
   "method": "GET",
   "path": "/designs",
   "role": "member",
   "summary": "Swarm designs: the platform's templates (read-only: audience-test, ab-test, panel-delphi, diffusion) and the tenant's own, latest version of each.",
   "query": {
    "who": "population | personas | selves",
    "status": "active | retired | template"
   }
  },
  {
   "method": "GET",
   "path": "/designs/:key",
   "role": "member",
   "summary": "A design at a version (?version=, default latest), its versions and its recent runs."
  },
  {
   "method": "PUT",
   "path": "/designs/:key",
   "role": "admin",
   "summary": "Save a design's next version (unchanged writes nothing), validated against its pack or its selves. Template keys are refused: start from one with from.",
   "body": {
    "name": "",
    "purpose": "",
    "from": "a template or design key to start from (spec fields given override it)",
    "status": "active | retired",
    "spec": "{who: {population: {pack, simulate, represents?, seed?, segments?, rake?} | personas: {pack, where?, limit?} | selves: [{handle, weight?, role?}], tiers?: {voiced?, surrogate?}}, structure?: {mixing?, graph?, homophily?, stubbornness?, seeding?: {share, who?}}, process?: {kind: one-off|rounds|debate|delphi (living: not yet), rounds?}, protocol?: {kind: ssr|stance-dist (populations) | vote|consensus|stance-dist|critique-revise (selves), threshold?}, time?: {step, horizon, report_every?, scale?, reference_date?}, stimulus?, variants?, ab?, measures?, voice?, dynamics?, observe?: {tool, args?, members?, max?}, calibration?: {targets: [{measure?, metric, target, tolerance?}]}, seeds?}"
   }
  },
  {
   "method": "POST",
   "path": "/designs/:key/runs",
   "role": "member",
   "summary": "Run a design. Selves: a swarm run (any key). Population or personas: a simulation run in /sim/runs (admin key), or one per variant on the same seeds with a compare link.",
   "body": {
    "stimulus": "{text, kind?} (or the design's own)",
    "variants": "[{label, text}] 2..3",
    "seed": "override the population's seed",
    "reference_date": "YYYY-MM-DD",
    "hold": "true: wait for observations, then release",
    "version": "run an earlier version",
    "question": "selves: the question",
    "choices": "selves: 2..8",
    "brief": "selves: background"
   }
  },
  {
   "method": "POST",
   "path": "/designs/:key/ask",
   "role": "member",
   "summary": "Bulk ask: one question to n members in one call. Selves: a weighted sample, each answer weighted by what it stands for. Population or personas (admin key): n voiced members, evenly or a weighted stratum sample, rated against the choices; partial results stream in the run's progress and sim.progress events.",
   "body": {
    "question": "",
    "choices": "optional 2..11 strings",
    "n": "1..500",
    "strata": "[category dimensions or \"segment\"]",
    "seed": "",
    "brief": "selves: background"
   }
  },
  {
   "method": "PATCH",
   "path": "/tenant",
   "role": "admin",
   "summary": "Tenant settings.",
   "body": {
    "models": "{chat?: {rungs?: [rung names], model?, effort?}, embed?: {rung?, model?}} or null: which of this core's rungs and models answer this tenant; never a URL or key",
    "timezone": "IANA",
    "vocabulary": "{organisation: how your selves refer to you, e.g. the house}",
    "program": "{orient|read|reflect|context|swarm: {instruction}}: your own logic for each step",
    "policy": "{ratersToPromote, retireAfter, recordBudget, recordsConfirm (its own records confirm lessons), confirmGraceDays, heldBudget, candidateBudget, perPersonCap, procedureTrials, procedureBudget}",
    "identityKinds": "your own identity kinds beyond the core's",
    "battery": "{version, items}",
    "dailyCalls": "tenant-wide ceiling",
    "webhook": "{url} to set (secret returned once) or null to remove",
    "tools": "{mcp_url, auth: {tokenEnv: a variable granted to this tenant (LW_SECRET_<TENANT>_*)} | {tokenFile} | null, dialect?: standard|praecise, host?, effects?: [tool names that have effects], timeout_ms? 100..30000 (15000), max_output? 200..20000 chars (4000), budget? 1..12 calls per run (4)} or null: the tenant MCP endpoint its selves may call during work; a raw secret is refused"
   }
  },
  {
   "method": "GET",
   "path": "/tenant/tools",
   "role": "member",
   "summary": "The tenant's own tools: its endpoint setting (the credential never shown; auth.via env|file) and the tools it lists, each with effects (bool) and the selves allowed it. ?refresh=1 skips the cache."
  },
  {
   "method": "GET",
   "path": "/packs",
   "role": "member",
   "summary": "Persona packs this tenant can use: the platform's (standard; read-only) and its own, latest version of each, with what each extends."
  },
  {
   "method": "POST",
   "path": "/packs",
   "role": "admin",
   "summary": "Declare a pack of the tenant's own, or write its next version. It may extend a platform pack or one of its own (pinned to that pack's latest version). A platform pack key is refused (403): platform packs are read-only.",
   "body": {
    "key": "lower_snake",
    "label": "",
    "description": "",
    "extends": "a pack key, or null"
   }
  },
  {
   "method": "GET",
   "path": "/packs/:key",
   "role": "member",
   "summary": "A pack at a version (?version=, default latest), its versions, and every dimension it carries: its own over those it inherits, each at the pinned version."
  },
  {
   "method": "GET",
   "path": "/dimensions",
   "role": "member",
   "summary": "Declared persona dimensions, latest version of each, platform and own; ?pack= narrows to one pack."
  },
  {
   "method": "POST",
   "path": "/dimensions",
   "role": "admin",
   "summary": "Declare a dimension in one of the tenant's packs: its next version (an unchanged declaration writes nothing) and the pack's next version pinning it. Validated per type; every fault comes back at once in faults [{path, message}] (400). A dimension that plausibly duplicates an inherited one (the same key stem, e.g. age and age_band, or spec.refines naming it) is refused (409, faults at inherited.<key>) until the caller chooses: inherited: {<key>: {derive: fn}} makes the inherited dimension the pack's own, DERIVED from this one (never drawn, so members never contradict themselves), or {<key>: \"keep\"} keeps drawing both (recorded in spec.keeps). Examples per type: scalar {range: {min: 18, max: 90}, distribution: {kind: \"normal\", params: {mean: 40, sd: 12}}}; category {categories: [{label: \"north\", share: 0.4}, {label: \"south\", share: 0.6}]}; derived category {categories: [\"under 30\", \"30+\"], derived: {from: [\"age\"], fn: {kind: \"band\", of: \"age\", edges: [30], labels: [\"under 30\", \"30+\"]}}}; embedding {dims: 512}; set {items: [\"vinyl\", \"streaming\"], max_items: 2}; timeseries {of: \"number\", dynamics: {kind: \"drift\", rate: 0.01}}; edge {relation: \"friend\", structure: \"mixing_group\", homophily: [\"region\", \"age_band\"]}; event_process {process: \"poisson\", rate: {mean: 2, per: \"month\"}}.",
   "body": {
    "pack": "your pack key",
    "key": "lower_snake",
    "label": "",
    "type": "scalar | category | embedding | set | timeseries | edge | event_process",
    "inherited": "{<inherited key>: {derive: {kind: band|lookup|arith, ...}} | \"keep\"} when this key duplicates an inherited one",
    "spec": "{range: {min, max} (scalar) | categories: [label | {label, share}], open? (category) | dims: 1..4096 (embedding) | items?, max_items? (set) | of: number|category|vector|point|object, dims? (timeseries) | relation, structure: explicit|mixing_group, homophily: [dimension keys] (mixing_group edge), to?: persona|self|group (edge) | process: poisson|hawkes|scheduled|renewal, rate: {mean >= 0, per: day|week|month|year} (event_process); distribution?: {kind: normal {mean, sd} | lognormal {mu, sigma} | uniform | beta {a, b} | gamma {shape, rate} | poisson {lambda} | categorical {shares: {label: share}} | empirical | fixed | derived (with from), params, source?: {kind: research|data|hand|derived, ref}}, derived?: {from: [1..8 dimension keys], fn: {kind: band, of, edges: [ascending], labels: [edges+1]} | {kind: lookup, of, map: {value: result}, default?} | {kind: arith, op: add|sub|mul|div|min|max, args: [keys | numbers | \"$reference_year\"]}}, refines?: <inherited key>, conditional_on?: [{dimension, when?, tilt: {}}], dynamics?: static | {kind: drift, rate: <per day>} | {kind: decay, half_life_days: >0} | {kind: event, on: name | [names]}, effects?: [{measure, direction: up|down|mixed|none, strength?: 0..1}], privacy?: normal|special_aggregate_only, ui_group?: Identity|Life|Money|Taste & style|Social|Shopping|custom}"
   }
  },
  {
   "method": "GET",
   "path": "/dimensions/:pack/:key",
   "role": "member",
   "summary": "One dimension at a version (?version=, default latest) and its versions."
  },
  {
   "method": "GET",
   "path": "/personas",
   "role": "member",
   "summary": "The tenant's personas (current versions), filtered by any dimension of a pack. Special-category values are withheld and cannot be filtered on here.",
   "query": {
    "pack": "pack key (needed to filter)",
    "where": "JSON {dimension key: value | {in: [..]} | {gte|lte|gt|lt: n} (scalars) | {has: item} (sets) | {exists: bool}}",
    "self": "voiced by this self",
    "limit": "1..1000 (100)",
    "after": "handle to page after",
    "retired": "1 to include retired"
   }
  },
  {
   "method": "POST",
   "path": "/personas",
   "role": "admin",
   "summary": "Create a persona: values validated against its pack's dimensions.",
   "body": {
    "handle": "",
    "pack": "",
    "values": "{dimension key: value}",
    "grounding": "demographic | persona (default) | evidence (provenance required)",
    "provenance": "{source, ...}",
    "self": "handle of the self that voices it"
   }
  },
  {
   "method": "GET",
   "path": "/personas/aggregate",
   "role": "member",
   "summary": "Counts per group of a category, set or mixing-group dimension, with mean/min/max of a scalar measure. When a special-category dimension is involved, groups under 10 are suppressed.",
   "query": {
    "pack": "",
    "by": "dimension key",
    "measure": "scalar dimension key",
    "where": "JSON {dimension key: value | {in: [..]} | {gte|lte|gt|lt: n} (scalars) | {has: item} (sets) | {exists: bool}}"
   }
  },
  {
   "method": "GET",
   "path": "/personas/export",
   "role": "member",
   "summary": "Export a pack's personas as JSON or CSV text (special-category values withheld).",
   "query": {
    "pack": "",
    "format": "json | csv",
    "where": "JSON {dimension key: value | {in: [..]} | {gte|lte|gt|lt: n} (scalars) | {has: item} (sets) | {exists: bool}}"
   }
  },
  {
   "method": "POST",
   "path": "/personas/import",
   "role": "admin",
   "summary": "Import personas from CSV (header: handle, grounding?, dimension keys; sets as a|b, JSON cells allowed) or JSON (a list). Good rows are written, bad rows reported per row with their faults; dryRun writes nothing.",
   "body": {
    "pack": "",
    "format": "csv | json",
    "data": "CSV text or a list of personas",
    "grounding": "default for rows",
    "provenance": "default for rows",
    "dryRun": "boolean"
   }
  },
  {
   "method": "GET",
   "path": "/personas/:handle",
   "role": "member",
   "summary": "A persona at a version (?version=, default current) and its versions. Special-category values are withheld."
  },
  {
   "method": "PUT",
   "path": "/personas/:handle",
   "role": "admin",
   "summary": "Write a persona's next version (re-validated against its pack's latest version). merge keeps values not named; self links (or null unlinks) the self that voices it.",
   "body": {
    "values": "",
    "merge": "boolean",
    "pack": "",
    "grounding": "",
    "provenance": "",
    "self": "handle or null"
   }
  },
  {
   "method": "DELETE",
   "path": "/personas/:handle",
   "role": "admin",
   "summary": "Retire a persona. Its versions stay readable."
  },
  {
   "method": "POST",
   "path": "/sim/runs",
   "role": "admin",
   "summary": "Start a simulation run: a population drawn from a pack (or a selection of personas), a stimulus, measures, a voiced tier (free text rated by SSR, with collapse checks) and dynamics (adoption over mixing groups, or opinion over an explicit graph). Runs as `simulate-voice` then `simulate` jobs; watch sim.progress / sim.done in /events. At most 3 queued or running per tenant (429).",
   "body": {
    "pack": "the pack whose dimensions the population is drawn from",
    "population": "{simulate: rows, represents?: people (each row stands for represents/simulate), seed?, segments?: [{name, share, values: {dimension: value | {shares} | {range: [lo, hi]} | {distribution}}}], rake?: [category dims] | {dim: {label: share}}, mixing?: {by: [dims or a mixing_group edge], edges?: {dim: [cut points]}}} or {personas: {where?, limit?}, represents?, mixing?}",
    "stimulus": "{text, kind?, structure?}",
    "measures": "[purchase_intent | appeal | share_intent | attend_intent | {key, anchors: [5 statements low..high] | [[..], [..]], ask?}] (default purchase_intent)",
    "voice": "{members: 0..500 voiced by the tenant's model, rated by SSR; retest?: 0..20 (2); prior?: true; expected_sd?: 1.1; temperature?; strata?: [category dims or \"segment\"] for a weighted stratum sample}",
    "dynamics": "{model: adoption, channels: {name: rate/day | {default, general, <segment>, \"dim=value\", \"scalar<25\", \"scalar>=65\", \"scalar=18..24\": rate} (every key is checked: an unknown one is refused by name)}, imitation?, across?: 0..1, fatigue?, fatigue_shape?, appeal?: scalar dim, appeal_measure?, events?: [{at, exposure, half_life?, duration?, who?}], event_process?: {rate: {mean, per}, exposure, half_life?, who?}} | {model: opinion, graph?: {edges: [[listener, speaker, w?]]}, homophily?: [dims], stubbornness?: 0..1, rate?}",
    "step": "any duration (\"10min\", \"1h\", \"1d\")",
    "horizon": "a duration (\"40d\")",
    "report_every": "a duration",
    "chunk": "rows per chunk",
    "seeds": "1..20 (spread across seeds)",
    "reference_date": "YYYY-MM-DD the run's time starts at (default today): durations map onto the calendar from it, and derived dimensions (a cohort from age) count years from it",
    "hold": "true: created but not started until POST /sim/runs/:id/observations {release: true}",
    "observe": "{tool, args?, members?: voiced|all, max?: 1..5000}: pull per-member observations from one of the tenant's read-only tools first",
    "calibration": "{targets: [{measure?, metric: mean|would_act|have_it_end|ever_had_it_end, target, tolerance?}]}: checked in results.calibration"
   }
  },
  {
   "method": "GET",
   "path": "/sim/runs",
   "role": "member",
   "summary": "Simulation runs, newest first, with status, progress and the design (and version) each was made from.",
   "query": {
    "status": "queued|running|done|failed|cancelled",
    "limit": "1..100 (20)",
    "design": "only this design's runs"
   }
  },
  {
   "method": "GET",
   "path": "/sim/runs/:id",
   "role": "member",
   "summary": "One run: spec, progress {phase, voiced, voice_of, seed, seeds, step, steps, pct}, and when done its aggregate results (represented vs simulated, reactions per measure with distribution, divides, would_act and collapse checks, adoption curves with the seed spread, opinion) and results.trust (version 1): fidelity tier mix, seed spread, reliability (test-retest), collapse warnings, calibration state with raw/debiased/calibrated signals, and the design's backtest skill against naive baselines. Special-category dimensions only in groups of 10+."
  },
  {
   "method": "GET",
   "path": "/sim/runs/:id/members",
   "role": "member",
   "summary": "A run's sampled members, a page at a time by stable index: segment, weight, public traits (special-category values never), and the persona's handle when members are personas.",
   "query": {
    "after": "the last index of the previous page",
    "limit": "1..1000 (100)"
   }
  },
  {
   "method": "POST",
   "path": "/sim/runs/:id/observations",
   "role": "admin",
   "summary": "External per-member observations: values of the pack's dimensions keyed by member index, before the run starts (a held run) or between its steps. They replace drawn values in profiles, rates and breakdowns. release: true starts a held run.",
   "body": {
    "observations": "[{i, values: {dimension: value}}] (at most 5000 per call)",
    "source": "what produced them",
    "release": "true starts a held run"
   }
  },
  {
   "method": "GET",
   "path": "/sim/compare",
   "role": "member",
   "summary": "Two finished runs side by side (an A/B of stimuli or designs): per measure means, would-act and JSD between distributions, adoption at the end, and whether they ran on the same seeds.",
   "query": {
    "a": "run id",
    "b": "run id"
   }
  },
  {
   "method": "POST",
   "path": "/backtests",
   "role": "admin",
   "summary": "Backtest: fit the release of a design, a run or an inline release on a measured series BEFORE the cut and forecast after it; scored only out of sample at each horizon (MAPE, sMAPE, MAE, pinball, cumulative error) beside naive baselines (last period carried forward, no further change, seasonal-naive when the cadence has a season), with a plain verdict on whether the model beat each. A signal and an outcome make it a calibration pair. All faults come back at once in faults [{path, message}].",
   "body": {
    "subject": "what the series measures",
    "series": "{values: [8..1000 numbers >= 0], kind: cumulative|per_period, cadence: hour|day|week|month|quarter, start?: YYYY-MM-DD, measure?: adoption (new adopters per period, default) | attention (level per period)}",
    "cut": "points before the cut (4..n-1), or a date with series.start",
    "horizons": "days, 1..8 (default [7, 30, 90])",
    "design": "a design key (its release seeds the fit)",
    "run": "or a run id",
    "release": "or {channels, imitation, fatigue, fatigue_shape}",
    "seed": "whole number (1): the same seed, the same fit",
    "season": "periods per season (default day 7, week 52, month 12, quarter 4)",
    "margin": "0..1 (0.05): how much lower the MAE must be to count as beating",
    "signal": "{run, metric: mean|would_act|have_it_end|ever_had_it_end, measure?} (debiased by the run's measured prior) or {value, metric?}",
    "outcome": "number observed (default: the total after the cut to the longest horizon)"
   }
  },
  {
   "method": "GET",
   "path": "/backtests",
   "role": "member",
   "summary": "Backtests, newest first, with their verdicts.",
   "query": {
    "design": "only this design's",
    "subject": "only this subject's",
    "limit": "1..100 (20)"
   }
  },
  {
   "method": "GET",
   "path": "/backtests/:id",
   "role": "member",
   "summary": "One backtest: the series, the fitted parameters (with an in-sample error that is never judged), per-horizon scores and skill against each baseline, the verdict, and the forecast with its 10–90% band beside the actuals and baselines."
  },
  {
   "method": "GET",
   "path": "/calibration",
   "role": "member",
   "summary": "The tenant's calibration maps, per design and signal metric: isotonic (5+ backtests) or bounded linear (3+) from the swarm signal to the observed outcome, with the leave-one-out error; uncalibrated below 3. Runs show raw, debiased and calibrated values in results.trust.",
   "query": {
    "design": "only this design's"
   }
  },
  {
   "method": "POST",
   "path": "/calibrate/rectify",
   "role": "member",
   "summary": "Correct a model's answers with a small real sample (prediction-powered inference, PPI++): an estimate and interval that stay valid however wrong the model is, beside the real-sample-only answer, and the effective sample size the model adds. Per cell and poststratified with cells and shares. Nothing is stored.",
   "body": {
    "labelled": "[{y, f}]: real answers y beside the model's f for the same units",
    "unlabelled": "[f]: the model's answers where no real one exists",
    "share": "true for a 0/1 outcome (interval kept in [0, 1])",
    "alpha": "default 0.05",
    "lambda": "weight on the model in [0, 1]; tuned by default",
    "cells": "instead: [{cell, labelled, unlabelled}]",
    "shares": "with cells: {cell: population share}"
   }
  },
  {
   "method": "POST",
   "path": "/calibrate/allocate",
   "role": "member",
   "summary": "Where to spend the next real answers: labels per cell, more where the model is hardest to correct, every cell at least 2, summing to the budget.",
   "body": {
    "cells": "[{cell, sdResidual, sdClassical?, share?, cost?}]",
    "budget": "total labels"
   }
  },
  {
   "method": "POST",
   "path": "/calibrate/intervals",
   "role": "member",
   "summary": "Intervals that keep their coverage (conformal): split; by subgroup, so no cell is covered only on average; or weighted to recent years, so a change of regime widens them instead of hiding.",
   "body": {
    "method": "split | group | weighted",
    "alpha": "miscoverage, default 0.1",
    "scores": "split and weighted: past |error| values",
    "predictions": "split and weighted: points to put intervals round",
    "calibration": "group: [{groups, score}]",
    "points": "group: [{groups, prediction}]",
    "times": "weighted: when each score was made",
    "now": "weighted",
    "halfLife": "weighted: years for a score's weight to halve"
   }
  },
  {
   "method": "POST",
   "path": "/calibrate/regions",
   "role": "member",
   "summary": "Regions forecast as the nation plus an offset that fades (AR(1)), so a region inherits a national stall instead of its own old slope; gains that taper as life expectancy rises, fitted over many populations; and a score against the truth.",
   "body": {
    "national": "[{year, e0}]",
    "regions": "{region: [{year, e0}]}",
    "nationalForecast": "[{year, e0}]",
    "horizon": "years ahead",
    "nationalSd": "sd of the national forecast",
    "pooled": "{population: [{year, e0}]} to fit tapered gains",
    "start": "e0 to project from with the fitted gains",
    "years": "default 10",
    "predicted": "{region: value}",
    "truth": "{region: value}"
   }
  },
  {
   "method": "POST",
   "path": "/calibrate/kin",
   "role": "member",
   "summary": "Expected siblings (older, younger, alive) from fertility and survival alone, with their variance, per cell; beside a simulation's counts and a survey's, it says whether the rates or the simulation's linking of births to parents is at fault.",
   "body": {
    "schedules": "{fertility, survival} by single year of age, or {year: {fertility, survival}}",
    "cells": "[{cell, birthYear, egoAge}]",
    "birthYear": "without cells",
    "egoAge": "without cells; omit for lifetime",
    "alive": "true to count siblings still alive at egoAge",
    "simulated": "{cell: count}",
    "survey": "{cell: count}",
    "tolerance": "default 0.1"
   }
  },
  {
   "method": "POST",
   "path": "/calibrate/cells",
   "role": "member",
   "summary": "Subgroups held to the truth: multicalibration over overlapping groups (patches learned and applied to new units), small cells smoothed across ordered age bands, or each cell checked against its own sampling-noise floor.",
   "body": {
    "method": "multicalibrate | smooth | floor",
    "units": "multicalibrate: [{p, y, groups}]",
    "apply": "multicalibrate: [{p, groups}] to correct",
    "alpha": "multicalibrate: tolerance, default 0.02",
    "cells": "smooth: [{party, ageIndex, n, mean}]; floor: [{cell, predicted, observed, n}]",
    "priorSdRW": "smooth: step between age bands, default 0.05",
    "shares": "smooth: {party|age: share}",
    "q": "floor: default 0.95"
   }
  },
  {
   "method": "POST",
   "path": "/calibrate/assimilate",
   "role": "member",
   "summary": "A running world corrected by what it observes (ensemble Kalman filter), or parameter space ruled out before expensive runs (history matching on Gaussian-process emulators). Seeded: the same body gives the same answer.",
   "body": {
    "method": "enkf | history",
    "ensemble": "enkf: members × parameters",
    "predicted": "enkf: members × observations",
    "observed": "enkf",
    "obsSd": "enkf: one or per observation",
    "inflation": "enkf: default 1",
    "X": "history: design points",
    "outputs": "history: simulator outputs per design point",
    "targets": "history: [{value, obsSd, modelDiscrepancySd}]",
    "candidates": "history: points to test, or bounds and nCandidates",
    "cutoff": "history: default 3",
    "seed": "default 1"
   }
  },
  {
   "method": "POST",
   "path": "/forecasts",
   "role": "admin",
   "summary": "Register a forecast BEFORE the moment it is for (409 once target_at has passed): value and 10–90% band, how it was made, an inputs hash (or inputs to hash), and the naive baselines' values for skill.",
   "body": {
    "subject": "what is forecast",
    "target_at": "ISO date/time it is for",
    "value": "number",
    "low": "10% quantile",
    "high": "90% quantile",
    "method": "name (e.g. swarm+release)",
    "design": "design key",
    "run": "run id",
    "inputs": "anything; hashed (sha256, canonical JSON) when inputs_hash is absent",
    "inputs_hash": "",
    "baselines": "{name: value}, e.g. {last: 120, no_change: 0}"
   }
  },
  {
   "method": "GET",
   "path": "/forecasts",
   "role": "member",
   "summary": "Registered forecasts, by target date, each with its outcome once scored.",
   "query": {
    "subject": "",
    "method": "",
    "scored": "true | false",
    "limit": "1..500 (50)"
   }
  },
  {
   "method": "DELETE",
   "path": "/forecasts/:id",
   "role": "admin",
   "summary": "Remove a registered forecast and its outcome."
  },
  {
   "method": "POST",
   "path": "/forecasts/actuals",
   "role": "admin",
   "summary": "Post actuals; every open forecast for that subject and day, made before it, is scored at once (absolute and percentage error, sMAPE, pinball, within band, error of each baseline).",
   "body": {
    "actuals": "[{subject, at, value}] (or one at the top level)",
    "source": "where they came from"
   }
  },
  {
   "method": "POST",
   "path": "/forecasts/actuals/pull",
   "role": "admin",
   "summary": "Pull actuals from one of the tenant's read-only tools (called with {subject, ...args}, answering {actuals: [{at, value}]}) and score them.",
   "body": {
    "tool": "tool name",
    "subject": "",
    "args": "{}"
   }
  },
  {
   "method": "GET",
   "path": "/forecasts/ledger",
   "role": "member",
   "summary": "The accuracy ledger: per method, design or the whole tenant: scored count, MAE, MAPE, sMAPE, pinball, band coverage, skill against each baseline (1 - MAE/MAE baseline), month by month; per design also its backtests' skill.",
   "query": {
    "by": "method | design | tenant",
    "subject": ""
   }
  },
  {
   "method": "POST",
   "path": "/sim/runs/:id/cancel",
   "role": "admin",
   "summary": "Stop a queued, running or paused run for good (409 once finished)."
  },
  {
   "method": "POST",
   "path": "/sim/runs/:id/pause",
   "role": "admin",
   "summary": "Pause a running run: status pausing until its next chunk boundary (a voiced batch in flight finishes first), then paused. A paused run holds no worker and survives restarts; idempotent."
  },
  {
   "method": "GET",
   "path": "/universe",
   "role": "member",
   "summary": "This project's universe: its laws (the weakest gate any self here may run), its kinds of world, and how many worlds and residents it has."
  },
  {
   "method": "PATCH",
   "path": "/universe",
   "role": "admin",
   "summary": "Set the laws. The floor may be raised; it is never looser than the core's own.",
   "body": {
    "gateFloor": "{trialsNeeded?, alpha?, maxUnchecked?}"
   }
  },
  {
   "method": "PUT",
   "path": "/universe/kinds/:id",
   "role": "admin",
   "summary": "Declare a kind of world as data: what it is made of, where selves work in it, the checks that can fail there (none makes it belief-only), a stricter local gate, and what an arriving self must re-earn. A kind worlds were made under cannot be changed.",
   "body": {
    "description": "",
    "places": "[...]",
    "hosts": "[worker|studio|body]",
    "taskClasses": "[...]",
    "checks": "[check names]",
    "gate": "optional {trialsNeeded, alpha, maxUnchecked}, never looser than the laws",
    "seed": "optional first state of a world of this kind",
    "quarantine": "optional {beliefs?: true, baseline?: procedure text an arriving self must beat}",
    "dayMs": "optional wall milliseconds per world-day"
   }
  },
  {
   "method": "GET",
   "path": "/worlds/:id/residents",
   "role": "member",
   "summary": "The selves who live in this world."
  },
  {
   "method": "POST",
   "path": "/selves/:handle/travel",
   "role": "admin",
   "summary": "Move a self into a world: its first arrival, or a crossing. Its record goes with it untouched, a running trial stays behind, and the destination kind's quarantine applies. Refused if its gate is looser than the laws.",
   "body": {
    "to": "the world's id"
   }
  },
  {
   "method": "GET",
   "path": "/selves/:handle/passages",
   "role": "member",
   "summary": "Where a self lives, every border it crossed, and whether its record shows it arrived whole each time."
  },
  {
   "method": "POST",
   "path": "/checkers/:id/audit",
   "role": "admin",
   "summary": "Report a checker's verdicts on items known to be bad. One pass on a bad item suspends it, and every adoption it helped decide is named for review.",
   "body": {
    "knownBad": "[{item, passed}]: what it returned on each known-bad item"
   }
  },
  {
   "method": "POST",
   "path": "/worlds",
   "role": "admin",
   "summary": "Make a world in this project's universe. With no pack it is a place selves live in (kind optional: its seed is the world's first state). With a pack it also holds a living population: members drawn from a pack go on over simulated time (households, ties, circles, followers, life events from event_process dimensions, word of mouth, fatigue, bounded memory). Created paused at tick 0; the reply carries the plan (see /worlds/:id/plan).",
   "body": {
    "kind": "a kind declared at PUT /universe/kinds/:id (optional)",
    "name": "text",
    "pack": "the pack",
    "population": "{simulate: 2..20000 members, represents?, seed?, segments?, mixing?}",
    "stimulus": "{text, kind?}: what members meet and are voiced about",
    "time": "{start: YYYY-MM-DD, horizon: \"30d\", step?: \"1d\", resolution?: [{until, step}] (finer steps early), scale?: realtime|1h=1min|1h=1s|1d=1min|1d=10s|1d=1s|1w=1s|{sim, real}}",
    "feed": "{channels: {name: rate/day}, imitation?, across?, fatigue?, fatigue_shape?}",
    "social": "{ties?, household?, influencers?, follows?, circles?: {by, size}, homophily?, pull?}",
    "life": "{processes?: [event_process dims], budget?: scalar dim, age?: scalar dim}",
    "memory": "{cap?: 1..64, reflect_days?, consolidate_days?}",
    "budget": "{calls_per_day?: members voiced by the model per simulated day (the realism knob)}",
    "interventions": "[{at, kind, label?, params?}]"
   }
  },
  {
   "method": "GET",
   "path": "/worlds",
   "role": "member",
   "summary": "Worlds, newest first: status (paused|running|done|failed), tick, parent and branch tick.",
   "query": {
    "status": "paused|running|done|failed",
    "limit": "1..200 (50)"
   }
  },
  {
   "method": "GET",
   "path": "/worlds/:id",
   "role": "member",
   "summary": "One world: spec, clock (tick, sim time, target), run (scale and whether it was capped), interventions, pending voiced members."
  },
  {
   "method": "DELETE",
   "path": "/worlds/:id",
   "role": "admin",
   "summary": "Delete a world with its member states and log. Its branches stay (they no longer name a parent)."
  },
  {
   "method": "POST",
   "path": "/worlds/:id/run",
   "role": "admin",
   "summary": "Run the world in real time at its scale, capped to what the model and the arithmetic sustain (the reply says so). At most 3 running per tenant (429).",
   "body": {
    "scale": "optional: a preset or {sim, real}",
    "calls_per_day": "optional: members voiced per simulated day"
   }
  },
  {
   "method": "POST",
   "path": "/worlds/:id/pause",
   "role": "admin",
   "summary": "Pause a running world; idempotent."
  },
  {
   "method": "POST",
   "path": "/worlds/:id/step",
   "role": "admin",
   "summary": "Advance a paused world by ticks (default 1) as fast as the arithmetic goes; voiced members still answer.",
   "body": {
    "ticks": "1..10000"
   }
  },
  {
   "method": "POST",
   "path": "/worlds/:id/jump",
   "role": "admin",
   "summary": "Advance a paused world to a later time.",
   "body": {
    "to": "a duration from the start (\"14d\"), seconds or a date"
   }
  },
  {
   "method": "POST",
   "path": "/worlds/:id/branch",
   "role": "admin",
   "summary": "Branch at a tick the world has reached (default now): the new world shares everything before it (re-simulated, with the recorded voices, no model calls) and goes on with its own interventions. The parent is untouched.",
   "body": {
    "at": "tick",
    "name": "text"
   }
  },
  {
   "method": "GET",
   "path": "/worlds/:id/plan",
   "role": "member",
   "summary": "Feasibility: the requested and effective (capped) scale, what limits it (model or arithmetic), model calls per second needed, estimated real time to the horizon, model calls to the horizon, and a plain sentence.",
   "query": {
    "scale": "a preset or sim=real",
    "calls_per_day": "members voiced per simulated day"
   }
  },
  {
   "method": "GET",
   "path": "/worlds/:id/replay",
   "role": "member",
   "summary": "The world's log, tick by tick: aggregates (have_it, aware, opinion, new adopters, exposures, life events, daily circle tiles), up to 25 sampled events per tick, group leanings.",
   "query": {
    "from": "tick",
    "to": "tick",
    "limit": "1..2000 (500)",
    "events": "false to leave events out"
   }
  },
  {
   "method": "GET",
   "path": "/worlds/:id/members",
   "role": "member",
   "summary": "Members, paged, at the current or an earlier tick: public traits (aggregate-only dimensions withheld), household, circle, awareness, opinion.",
   "query": {
    "after": "member index",
    "limit": "1..500 (100)",
    "tick": "a reached tick"
   }
  },
  {
   "method": "GET",
   "path": "/worlds/:id/members/:i",
   "role": "member",
   "summary": "One member: as in the list, plus ties and memory (private life events withheld).",
   "query": {
    "tick": "a reached tick"
   }
  },
  {
   "method": "GET",
   "path": "/worlds/:id/interventions",
   "role": "member",
   "summary": "The world's interventions."
  },
  {
   "method": "POST",
   "path": "/worlds/:id/interventions",
   "role": "admin",
   "summary": "Add an intervention while paused, at or after the current time (409 otherwise).",
   "body": {
    "at": "duration from the start, seconds or a date",
    "kind": "drop|campaign|outside|price|influencer|life",
    "label": "text",
    "params": "{exposure?, duration?, half_life?, who?, factor? (price), count? (influencer), member?, event? (life)}"
   }
  },
  {
   "method": "POST",
   "path": "/worlds/:id/advance",
   "role": "admin",
   "summary": "A world of lives (made with POST /worlds {lives: {count, hazards, seed?, places?, year?}}): advance it {months: 1..1200} by its hazard table. Returns the world in aggregate: lives held and alive, the year, births, deaths and partnerships so far, and which hazards are measured and which are placeholders.",
   "body": {
    "months": "1..1200"
   }
  },
  {
   "method": "GET",
   "path": "/worlds/:id/lives/:life",
   "role": "member",
   "summary": "Meet a life in a world of lives: its own record as it knows it (age, partner and for how long, children and their ages, whether it lives with its mother) and the same in its own words."
  },
  {
   "method": "POST",
   "path": "/worlds/:id/lives/:life/voice",
   "role": "admin",
   "summary": "A met life's voiced decision, written back. decision: birth (a woman, the next months, default 12). The life answers through this tenant's model with its story as memory; the world host anchors the answer to the population's hazard (a voice moves one life around the level, never sets it) and draws the outcome as a real event. An answer with no chance in it is did-not-run and changes nothing.",
   "body": {
    "decision": "birth",
    "months": "1..60"
   }
  },
  {
   "method": "POST",
   "path": "/worlds/:id/save",
   "role": "admin",
   "summary": "Save a world of lives to object storage, so it outlives the host's memory and comes back exactly."
  },
  {
   "method": "PATCH",
   "path": "/worlds/:id/interventions/:iid",
   "role": "admin",
   "summary": "Change an intervention that has not happened yet, while paused."
  },
  {
   "method": "DELETE",
   "path": "/worlds/:id/interventions/:iid",
   "role": "admin",
   "summary": "Remove an intervention that has not happened yet, while paused."
  },
  {
   "method": "POST",
   "path": "/sim/runs/:id/resume",
   "role": "admin",
   "summary": "Continue a paused run from its checkpoint: resuming, then running. Results equal an uninterrupted run with the same seed. 429 when the tenant already has its maximum active runs."
  },
  {
   "method": "DELETE",
   "path": "/sim/runs/:id",
   "role": "admin",
   "summary": "Delete a run with its observations and results, for good (404 when not the tenant's). Event sim.deleted."
  },
  {
   "method": "POST",
   "path": "/sim/runs/delete",
   "role": "admin",
   "summary": "Delete many runs at once; those not found come back under missing.",
   "body": {
    "ids": "[ids], 1..200"
   }
  },
  {
   "method": "POST",
   "path": "/sim/runs/:id/archive",
   "role": "admin",
   "summary": "Archive (archived: true, the default) or bring back (false) a run. Archived runs leave lists unless ?archived=1 (only archived) or all.",
   "body": {
    "archived": "true | false"
   }
  },
  {
   "method": "POST",
   "path": "/sim/runs/archive",
   "role": "admin",
   "summary": "Archive or bring back many runs at once.",
   "body": {
    "ids": "[ids], 1..200",
    "archived": "true | false"
   }
  },
  {
   "method": "DELETE",
   "path": "/backtests/:id",
   "role": "admin",
   "summary": "Delete a backtest and its calibration pair, for good (404 when not the tenant's). Event backtest.deleted."
  },
  {
   "method": "POST",
   "path": "/backtests/delete",
   "role": "admin",
   "summary": "Delete many backtests at once; those not found come back under missing.",
   "body": {
    "ids": "[ids], 1..200"
   }
  },
  {
   "method": "POST",
   "path": "/backtests/:id/archive",
   "role": "admin",
   "summary": "Archive (archived: true, the default) or bring back (false) a backtest. Archived backtests leave lists unless ?archived=1 (only archived) or all.",
   "body": {
    "archived": "true | false"
   }
  },
  {
   "method": "POST",
   "path": "/backtests/archive",
   "role": "admin",
   "summary": "Archive or bring back many backtests at once.",
   "body": {
    "ids": "[ids], 1..200",
    "archived": "true | false"
   }
  },
  {
   "method": "DELETE",
   "path": "/designs/:id",
   "role": "admin",
   "summary": "Delete every version of the tenant's own design (templates are never touched; runs keep their results), for good (404 when not the tenant's). Event design.deleted."
  },
  {
   "method": "POST",
   "path": "/designs/delete",
   "role": "admin",
   "summary": "Delete many designs at once; those not found come back under missing.",
   "body": {
    "ids": "[ids], 1..200"
   }
  },
  {
   "method": "POST",
   "path": "/designs/:id/archive",
   "role": "admin",
   "summary": "Archive (archived: true, the default) or bring back (false) a design. Archived designs leave lists unless ?archived=1 (only archived) or all.",
   "body": {
    "archived": "true | false"
   }
  },
  {
   "method": "POST",
   "path": "/designs/archive",
   "role": "admin",
   "summary": "Archive or bring back many designs at once.",
   "body": {
    "ids": "[ids], 1..200",
    "archived": "true | false"
   }
  },
  {
   "method": "PUT",
   "path": "/selves/:handle/tools",
   "role": "admin",
   "summary": "Which tenant tools a self may call during work (its charter's authority.tools) and whether it may use tools with effects.",
   "body": {
    "tools": "[tool names; a trailing * matches a prefix; [] none]",
    "effects": "read (default) | act"
   }
  },
  {
   "method": "GET",
   "path": "/tenant",
   "role": "member",
   "summary": "This tenant: name, timezone, settings (a webhook secret never), what its model calls resolve to, and its members."
  },
  {
   "method": "GET",
   "path": "/tenant/members",
   "role": "member",
   "summary": "Who may manage this tenant: the owner of record, admins and members, by account id with the email beside it."
  },
  {
   "method": "GET",
   "path": "/work/schedules",
   "role": "member",
   "summary": "Standing work: each schedule, its cadence, when it runs next, and its last run.",
   "query": {
    "self": "only this self's"
   }
  },
  {
   "method": "POST",
   "path": "/work/schedules",
   "role": "admin",
   "summary": "Give a self standing work. A task answers its instructions; research reads the web for its topics (one per line) into the self's memory. Times are the tenant's timezone.",
   "body": {
    "self": "handle",
    "kind": "task|research",
    "title": "",
    "instructions": "",
    "every_hours": "1..672, default 24",
    "at_hour": "0..23 or null",
    "weekdays": "[1..7] ISO, or null for every day",
    "review_required": "true holds the output for a person",
    "deliver": "an object your product reads when it is done, e.g. {slack: true}",
    "status": "active|paused",
    "by": "who"
   }
  },
  {
   "method": "PATCH",
   "path": "/work/schedules/:id",
   "role": "admin",
   "summary": "Change a schedule. A new cadence counts from its last run.",
   "body": {
    "...": "any field of POST /work/schedules except self"
   }
  },
  {
   "method": "DELETE",
   "path": "/work/schedules/:id",
   "role": "admin",
   "summary": "Remove a schedule. Its items stay."
  },
  {
   "method": "POST",
   "path": "/work/schedules/:id/run",
   "role": "admin",
   "summary": "Run it now, unless an item of it is already queued or running. Its cadence is untouched.",
   "body": {
    "by": "who"
   }
  },
  {
   "method": "POST",
   "path": "/work/items",
   "role": "member",
   "summary": "Ask a self for one piece of work. It is run as a `work` job; watch for work.done in /events.",
   "body": {
    "self": "handle",
    "title": "what you would like done",
    "instructions": "",
    "requested_by": "who",
    "kind": "request|handoff",
    "parent_id": "the item this was handed on from"
   }
  },
  {
   "method": "GET",
   "path": "/work/items",
   "role": "member",
   "summary": "Items newest first, a page at a time, with counts per local day and status.",
   "query": {
    "self": "only this self's",
    "status": "queued|running|needs_review|approved|rejected|done|failed",
    "day": "YYYY-MM-DD, local",
    "before": "the last id of the previous page",
    "limit": "1..100",
    "days": "how many days of counts, default 14"
   }
  },
  {
   "method": "GET",
   "path": "/work/items/:id",
   "role": "member",
   "summary": "One item, its output {text, sources, summary}, and its schedule."
  },
  {
   "method": "POST",
   "path": "/work/items/:id/retry",
   "role": "member",
   "summary": "Put a failed item back in the queue, as it was asked."
  },
  {
   "method": "POST",
   "path": "/work/items/:id/deliveries",
   "role": "member",
   "summary": "Record what your product did with a finished item's output on one channel (the core delivers nowhere itself). Replaces that channel's record.",
   "body": {
    "channel": "e.g. slack",
    "results": "[{channel, posted, why?}] or whatever your product reports"
   }
  },
  {
   "method": "POST",
   "path": "/work/items/:id/review",
   "role": "admin",
   "summary": "Decide an item that waited for review.",
   "body": {
    "decision": "approved|rejected",
    "by": "who",
    "note": "why",
    "rating": "-2..5"
   }
  },
  {
   "method": "GET",
   "path": "/jobs",
   "role": "member",
   "summary": "Recent and pending jobs, and the model slot's state."
  },
  {
   "method": "GET",
   "path": "/contract",
   "role": "member",
   "summary": "This contract."
  },
  {
   "method": "POST",
   "path": "/mcp",
   "role": "member",
   "summary": "The same capabilities as MCP tools, for agents. Streamable HTTP, both protocol eras."
  },
  {
   "method": "GET",
   "path": "/mcp",
   "role": "member",
   "summary": "405. The GET stream was removed in MCP 2026-07-28; named here so the refusal is contractual rather than a gap."
  },
  {
   "method": "DELETE",
   "path": "/mcp",
   "role": "member",
   "summary": "405. Sessions were removed in MCP 2026-07-28; there is nothing to delete."
  }
 ]
}