{"owner":"gastownhall","repo":"beads","hasSpec":true,"specFile":"internal/httpapi/spec/openapi.v0.yaml","branch":"HEAD","format":"yaml","version":"3.x (YAML)","title":"beads","description":"","endpoints":[],"spec":"openapi: 3.0.3\n\n# Hand-written. This document is the source of truth for the bd serve wire\n# contract: the Go types in internal/httpapi/apigen are generated FROM it\n# (`make api-gen`), never the other way round.\n#\n# CODEGEN TRAP, verified against oapi-codegen v2.6.0 and the reason the pinned\n# schemas below repeat their property lists instead of composing with `allOf`:\n# a component that carries BOTH `allOf` and `x-go-type` silently loses the\n# `x-go-type` — the generator merges the allOf members first and then either\n# inherits the first member's pin (an `IssueWithCounts` composed from `Issue`\n# generated `type IssueWithCounts = types.Issue`, dropping the count fields the\n# ready endpoint exists to carry) or emits a hand-written-struct mirror of the\n# canonical type. Both outcomes are the \"second wire struct\" this whole design\n# forbids, and both are silent. internal/httpapi/pinning.go holds compile-time\n# assertions that fail the build if either ever happens again.\nx-bd-source: spec-first\n\ninfo:\n  title: bd serve HTTP API\n  version: 0.1.0\n  license:\n    name: MIT\n  description: >-\n    Loopback HTTP surface over the same work contract the `bd` CLI serves, for\n    automation clients that would otherwise fork a `bd` subprocess per call.\n\n\n    This document is spec-first and vendor-neutral: it names no product, no\n    deployment and no downstream consumer, and the Go types under\n    `internal/httpapi/apigen` are generated from it.\n\n\n    `info.version` is the revision of THIS DOCUMENT. The wire contract version\n    is the `/v0` path prefix, reported as `ContextResponse.api_version`.\n\n\n    ## Media types\n\n\n    Success bodies are `application/json; charset=utf-8`, with ONE exception:\n    `watchEvents` answers `text/event-stream; charset=utf-8`, because its\n    response is a stream held open for the life of a connection rather than a\n    document. It is the only streaming operation here and the only non-JSON\n    success body; its individual events carry JSON, and the schemas they carry\n    are the ones the paged read already publishes.\n\n\n    EVERY non-2xx body, from every route and from the middleware in front of\n    them, is `application/problem+json` carrying the `Problem` schema — there is\n    exactly one error shape on this surface, and `watchEvents` is no exception:\n    it decides every refusal before opening its stream.\n\n\n    ## Four 400s cut across operations, and are documented here once\n\n\n    * **Host header.** A request whose `Host` header is not in the server's\n      allowlist is refused with `400` / `code: invalid_argument` /\n      `param: \"Host\"` by middleware that runs before any handler.\n\n    * **Unknown query parameter.** Every operation rejects any query parameter\n      outside its own parameter table with `400` / `code: invalid_argument` /\n      `reason: \"unknown_parameter\"` and `param` naming the offending key.\n      Operations that declare no QUERY parameter at all reject every query key\n      the same way. Today that is twenty-five of the forty-one operations\n      here — `GET /healthz`, `GET /v0/beads/context`,\n      `GET /v0/beads/dependencies/cycles`, `POST /v0/beads/issues`,\n      `PATCH /v0/beads/issues/{id}`,\n      `POST /v0/beads/issues/{id}/comments`,\n      `POST /v0/beads/issues/{id}:claim`,\n      `POST /v0/beads/issues/{id}:release`,\n      `POST /v0/beads/issues/{id}:close`,\n      `POST /v0/beads/issues/{id}:reopen`,\n      `POST /v0/beads/issues/{id}:casMetadata`, `POST /v0/beads/issues:sweep`,\n      `POST /v0/beads/issues:delete`,\n      `POST /v0/beads/issues:batchCreate`,\n      `POST /v0/beads/issues:batchApply`,\n      `POST /v0/beads/issues:batchClose`, `GET /v0/beads/config`,\n      `GET /v0/beads/config/{key}`, `PUT /v0/beads/config/{key}`,\n      `DELETE /v0/beads/config/{key}`, `POST /v0/beads/dependencies:add`,\n      `POST /v0/beads/dependencies:remove`, `POST /v0/beads/memories`,\n      `GET /v0/beads/memories/{key}` and `DELETE /v0/beads/memories/{key}`.\n      A path parameter is not a query parameter: several of those carry\n      `{id}` or `{key}` and still take no query key. The authority is each\n      operation's own `parameters` table, which is what a generated client\n      reads; this list is a convenience and, being prose, is the half that can\n      go stale — six entries were missing from it before this revision.\n\n    * **Request media type.** Every operation that carries a request body\n      requires `Content-Type: application/json` and refuses anything else with\n      `400` / `code: invalid_argument` / `param: \"Content-Type\"` /\n      `reason: \"invalid_value\"`. The refusal is a CSRF control rather than\n      pedantry: a JSON content type is not CORS-simple, so a cross-origin write\n      always triggers a preflight this server never approves, and accepting\n      `text/plain` or a form encoding would let an attacker's page skip that\n      preflight and drive a write from any browser on the host. It is\n      unreachable for a client generated from this document — every\n      `requestBody` here declares `application/json` — and is stated so that\n      the one 400 such a client could not otherwise predict is predictable.\n      `415` is deliberately not used: it is not in this surface's status\n      vocabulary, and adding one for this would be permanent wire surface.\n\n    * **Project-id stamp.** A request MAY carry a `Bd-Project-Id` header naming\n      the workspace the client means to address. When the header is present and\n      its value is not the project this server serves, the request is refused\n      with `400` / `code: invalid_argument` / `param: \"Bd-Project-Id\"` /\n      `reason: \"project_mismatch\"`, and that refusal — alone on this surface —\n      carries the server's own project id in the `server_project_id` member so a\n      stamped client can tell a wrong-server refusal from a malformed one. The\n      comparison is LITERAL: a server whose own `project_id` is empty refuses\n      every non-empty stamp, because it cannot assert it is the workspace the\n      client named. An ABSENT header is the backward-compatible path — a client\n      that never sends it is served exactly as before, and enforcement triggers\n      only when the header arrives — so this is additive wire surface, not a new\n      precondition on requests already in the field. The refusal is raised after\n      the Host gate and before any database work, so a misdirected request\n      mutates nothing. It is skipped on `GET /healthz` and `GET /v0/beads/context`\n      alone: liveness must answer whatever workspace a caller believed it\n      reached, and the identity handshake is where a client LEARNS the project\n      id to stamp with. A client detects that a server performs this check by\n      the `project.enforce` capability (see `ContextResponse`).\n\n\n    The first two are uniform rules reachable on EVERY route including\n    `GET /healthz`; the third holds on every body-carrying operation; the fourth\n    holds on every route EXCEPT the two exempt reads named above. All four are\n    stated here instead of being repeated on every operation. The per-operation\n    `responses` below therefore list what an operation produces BEYOND them: a\n    generated client must treat `400 invalid_argument` as possible everywhere,\n    including on the operations whose documented statuses are only\n    `404`/`500`/`503`.\n\n\n    ## Authentication is a deployment posture\n\n\n    Bearer authentication is CONFIGURED PER DEPLOYMENT, not implied by this\n    document. A server started without a token file requires no credential and\n    never emits `401`; a server started with one requires\n    `Authorization: Bearer <token>` on every operation that declares the\n    `bearerToken` scheme below — which is all of them except `GET /healthz`, so\n    that a liveness probe can answer with no credential. `GET\n    /v0/beads/context` is NOT exempt: it reveals the repository root, the beads\n    directory and the database name.\n\n\n    The refusal is `401` / `code: unauthenticated`, with `WWW-Authenticate:\n    Bearer`. Its `detail` is a fixed string and NEVER echoes the presented\n    credential — a missing header, a wrong scheme and an unrecognized token are\n    one code deliberately, because distinguishing them would tell an\n    unauthenticated caller which guess was closer.\n\n\n    The `404` for a path this document does not define needs no credential:\n    paths are public spec, so refusing them first would disclose nothing and\n    hide nothing.\n\n\n    A token ROTATES by rewriting the token file, with no restart: every\n    non-empty line is an accepted token, so an operator writes the new token\n    alongside the old, rolls clients over, then removes the old one. Both the\n    addition and the removal take effect within about a second.\n\n\n    There is NO TLS on this surface. A deployment beyond loopback is expected\n    to supply confidentiality itself — a service mesh, or a trusted network\n    boundary — because otherwise both the token and the issue data travel in\n    plaintext.\n\n\n    The unknown-parameter rule is deliberate and load-bearing: silently\n    ignoring an unrecognized FILTER parameter widens the result set, so a\n    client one version ahead of the server would receive — and act on — rows it\n    believed it had filtered out. Strict rejection is also a client's only\n    per-parameter capability probe, since `capabilities` (see\n    `ContextResponse`) is operation-level.\n\n\n    ## Version skew: what a client may gate on\n\n\n    * `capabilities` gates OPERATION presence, and also advertises server-wide\n      BEHAVIOR tokens such as `project.enforce` (per-request `Bd-Project-Id`\n      enforcement). It is derived from the server's implemented handlers plus the\n      behaviors this build enforces; check the list, never the version string.\n    * `bd_version` gates BEHAVIORAL changes tied to a release, and is the only\n      field a client may compare as a version.\n    * `api_version` gates the path major, and changes only when `/v1` is cut.\n    * Parameter presence is PROBED, not versioned: a 400 with\n      `reason: \"unknown_parameter\"` is the machine-attributable answer.\n    * `schema_version` is NOT an HTTP gate. It is shared with the CLI's stdout\n      JSON envelope, so it can move for CLI-only reasons with no HTTP wire\n      change. Report it, log it, never branch on it.\n    * Clients MUST default-branch on unknown `code` and `reason` values within\n      a status class: both vocabularies grow additively, so an exhaustive\n      switch breaks on the first addition.\n\n\n    ## An `operationId` is not derivable from its path\n\n\n    A custom method usually reads the same in both — `:claim` is `claimIssue`,\n    `:sweep` is `sweepIssues` — and TWO OPERATIONS INVERT IT. Neither is a\n    typo and neither will be renamed, because an `operationId` is a generated\n    client's method name and a path is wire surface, so both are one-way doors:\n\n\n    | Path | `operationId` | Capability token |\n    |---|---|---|\n    | `POST /v0/beads/issues:batchApply` | `applyBatch` | `issues.batchApply` |\n    | `POST /v0/beads/issues/{id}:casMetadata` | `compareAndSetMetadata` | `issues.casMetadata` |\n\n\n    The capability token follows the PATH in both cases, not the\n    `operationId`, so a client probing `capabilities` before calling a\n    generated method reads two different spellings of one operation. A spec\n    author adding an operation should spell them alike; a spec author touching\n    these two should leave both spellings exactly as they are.\n\n\n    ## Schema evolution while `/v0` is the newest path version\n\n\n    `Issue`, `IssueWithCounts`, `IssueDetails`,\n    `IssueWithDependencyMetadata`, `TreeNode`, `Dependency`, `Comment`,\n    `BondRef`, `Statistics`, `Cycle`, `CycleMember`, `IssueBlocking` and\n    `EventRecord` are\n    pinned to the canonical Go structs (`x-go-type`), so the CLI's `--json`\n    output and these response bodies are one compatibility domain and cannot\n    drift apart. Changes to them are limited to NEW optional fields, and each\n    addition must land with its entry in this document in the same change — a\n    two-way JSON-tag bijection test, covering all thirteen, fails CI otherwise.\n    A breaking change to those shapes requires cutting `/v1`, not editing `/v0`.\n\n\n    A member that is absent is not set. When reading a RESPONSE, clients MUST\n    treat an explicit `null` member identically to an absent member. Producers\n    on this surface omit absent members; a profile MAY instead emit `null` for\n    members it declares always-present. Explicit `null` in a REQUEST body is\n    NOT covered by this rule; its meaning is defined per operation.\n\n\n    ## Profiles\n\n\n    A PROFILE is a separate document that re-publishes part of this contract as\n    its own surface. Conformance to this core is a checkable relation over\n    wire-observable JSON — the profile's value model is subsumed by this one,\n    plus the deviations the profile DECLARES — not a claim of shared\n    vocabulary. Five rules make it checkable.\n\n\n    * **Verbatim or absent.** A property emitted under a core name carries the\n      core type, format and semantics, and its value is the canonical value\n      verbatim or is wholly absent. Nothing is transformed in place: a\n      re-spelling, a coercion to another JSON type, or a truncation, published\n      under a core name, is non-conformant — and omitting the member is always\n      the conformant alternative.\n\n    * **An omission is not a licence to re-spell.** A profile that omits a core\n      member MUST NOT publish that member's value under any other name.\n      Omission means the value is not on the wire; an extension that carries\n      it is a re-spelling of a core member, and is non-conformant however\n      carefully the extension itself is declared.\n\n    * **Tightening is bounds-only.** A profile MAY omit optional properties,\n      and MAY tighten `maxLength`, `maxItems` and `maxProperties` or close an\n      open vocabulary, provided every instance it emits is still valid against\n      this document once explicit `null` is read as absence. That list is\n      exhaustive and the proviso is a floor, not a grant: a change the list\n      does not name is not made a tightening by the fact that its instances\n      still validate. Widening a bound, re-typing a member, or extending a\n      vocabulary this document closes is not tightening.\n\n      **A narrowed value schema is a declared deviation.** Constraining what a\n      member's VALUES may be — admitting only short strings, say, where this\n      document admits any JSON value — is a third thing, and it is how a\n      profile caps the size of a response without transforming anything: not\n      one of the bounds above, and not the re-typing this rule excludes,\n      because every value that does go on the wire is still the canonical\n      value. It is a DEVIATION, conformant only where the profile DECLARES it,\n      by the same machinery that declares an embedded-relation omission, and\n      only on these terms. A value that does not satisfy the narrower schema is\n      OMITTED ENTIRELY rather than coerced to fit; stringifying a typed value\n      in place is a transformation, and \"verbatim or absent\" already forbids\n      it. And the declaration states that the member MAY BE INCOMPLETE, because\n      a consumer of the profile cannot distinguish an entry dropped for failing\n      the narrower schema from one that was never set: that consequence must be\n      visible in the declaration, not left to be discovered.\n\n    * **Required members are retained.** A profile keeps the required members of\n      every top-level resource. Dropping a required member of an EMBEDDED\n      relation — a nested edge or summary carried inside another resource — is\n      legal only where the profile DECLARES that omission and its reason. A\n      silent omission never is.\n\n    * **Extensions are declared, and their names ratchet.** A profile MAY add\n      members of its own, provided each is marked as an extension in the\n      profile's own document and its name is not one this document already\n      defines. Because `/v0` grows by ADDING optional members, a later revision\n      of this document may define a name a profile is already using: that\n      collision is reconciled — the core member adopted, or the extension\n      renamed — as part of adopting the revision, never left to resolve by\n      itself.\n\npaths:\n\n  /healthz:\n    get:\n      operationId: health\n      summary: Process liveness\n      description: >-\n        Liveness only: it answers from the process itself and never touches the\n        database, so it stays green while the database is unreachable. For\n        readiness, probe `GET /v0/beads/ready?limit=1` instead.\n      responses:\n        '200':\n          description: The process is running.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Health'\n\n  /v0/beads/context:\n    get:\n      operationId: getContext\n      summary: Workspace and API identity\n      description: >-\n        A startup snapshot of the served workspace plus the API's own identity.\n        Serves a fixed field allowlist; it does not reflect the server's whole\n        configuration, and in particular never carries a sync remote URL (those\n        routinely embed credentials). v0 answers from the snapshot without\n        touching the database, which is why no 503 is documented here.\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: Workspace and API identity.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ContextResponse'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n\n  /v0/beads/ready:\n    get:\n      operationId: listReadyWork\n      summary: List ready work\n      description: >-\n        Unblocked, open work, ordered by the requested sort policy. Items are\n        `IssueWithCounts` — the same element type `bd ready --json` emits — so\n        dependency, dependent and comment counts are present from v0 onward.\n\n\n        The result set is always restricted to `status=open`; there is no\n        status parameter. There is no cursor either: the sort policies admit no\n        keyset predicate, and the intended usage is snapshot-and-requery.\n\n\n        SOME TYPES ARE EXCLUDED BY DEFAULT, exactly as `bd ready` excludes them:\n        `merge-request`, `gate`, `molecule`, `rig` and the workspace's\n        configured infrastructure types, plus anything named in `exclude_type`.\n        Setting `type` DROPS THAT ENTIRE EXCLUSION SET (also matching the CLI),\n        so `type=molecule` returns records the default view never shows. A\n        client scanning the active set with `limit=0` and no `type` therefore\n        never sees those classes at all.\n      parameters:\n        - name: assignee\n          in: query\n          description: Only issues assigned to this actor.\n          schema:\n            type: string\n        - name: unassigned\n          in: query\n          description: Only issues with no assignee.\n          schema:\n            type: boolean\n        - name: type\n          in: query\n          description: >-\n            Issue type. The only normalization is shorthand ALIAS expansion,\n            exactly what `bd ready --type` does: `mr` → `merge-request`,\n            `feat` → `feature`, `mol` → `molecule`, `enhancement` → `feature`,\n            `dec`/`adr` → `decision`. Every other value is used as written —\n            there is NO plural folding, so `bugs` is not `bug`.\n\n\n            An unrecognized type is not an error here: the type vocabulary is\n            workspace-configurable, and `bd ready` does not validate it either,\n            so it simply matches nothing and `items` comes back empty. (The\n            list operation differs — `bd list` DOES validate the type, so\n            `GET /v0/beads/issues?type=bugs` is a 400.)\n\n\n            When set, `exclude_type` is ignored, and so are the default type\n            exclusions described above.\n          schema:\n            type: string\n        - name: exclude_type\n          in: query\n          description: >-\n            Issue types to exclude. Repeat the parameter, or pass a\n            comma-separated list. Ignored when `type` is set.\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: label\n          in: query\n          description: Labels that must ALL be present (AND).\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: label_any\n          in: query\n          description: Labels of which at least one must be present (OR).\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: exclude_label\n          in: query\n          description: Labels that must not be present.\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: label_pattern\n          in: query\n          description: Glob matched against labels.\n          schema:\n            type: string\n        - name: label_regex\n          in: query\n          description: Regular expression matched against labels.\n          schema:\n            type: string\n        - name: priority\n          in: query\n          description: Exact priority (0 is a real value, not \"unset\").\n          schema:\n            type: integer\n        - name: parent\n          in: query\n          description: Restrict to recursive descendants of this issue.\n          schema:\n            type: string\n        - name: metadata_field\n          in: query\n          description: >-\n            Top-level metadata equality filter as `key=value`, split on the\n            first `=`. Repeatable. An invalid key is a 400.\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: has_metadata_key\n          in: query\n          description: Only issues carrying this top-level metadata key.\n          schema:\n            type: string\n        - name: include_ephemeral\n          in: query\n          description: Include ephemeral (non-synced) rows.\n          schema:\n            type: boolean\n            default: false\n        - name: include_deferred\n          in: query\n          description: Include issues whose `defer_until` is still in the future.\n          schema:\n            type: boolean\n            default: false\n        - name: sort\n          in: query\n          description: >-\n            Ready-work ordering. `priority` is priority-first; `hybrid` orders\n            recent issues by priority and older ones by age; `oldest` is\n            creation order. An unrecognized value is a 400.\n\n\n            The default is the one `bd ready --sort` registers, so a client\n            swapping `bd ready --json` for this operation gets the same items in\n            the same order. The storage layer treats an EMPTY policy as\n            `hybrid`, but that fallback is unreachable from the CLI and is NOT\n            this parameter's default: `hybrid` demotes older high-priority work,\n            so defaulting to it would change the item SET as soon as `limit`\n            truncates — silently, and only for the clients this API exists to\n            migrate.\n          schema:\n            type: string\n            enum: [hybrid, priority, oldest]\n            default: priority\n        - name: limit\n          in: query\n          description: >-\n            Maximum number of items to return. `0` means unlimited, exactly as\n            `bd ready --limit 0` does — the two surfaces read the same shared\n            default and the same zero semantics, so they cannot diverge. A\n            negative value is a 400.\n\n\n            The default below is the shared ready-work limit constant\n            (`workapi.DefaultReadyLimit`, the value `bd ready`'s `--limit` flag\n            registers).\n\n\n            One exception, and it is mode-dependent: when the server was\n            started with `--allow-non-loopback`, `limit=0` is refused with 400\n            `invalid_argument`, `param: \"limit\"`, `reason: \"invalid_value\"` and\n            detail \"unlimited reads are loopback-only; pass an explicit limit\".\n            An unlimited read buffers the whole active set and its JSON encoding\n            inside one shared process, which must not be reachable by arbitrary\n            network peers. The bind mode is deliberately NOT advertised in\n            `ContextResponse` — a client that wants an unlimited read asks for\n            one and, on that 400, re-issues with an explicit limit; it is a\n            client-side fix, never a retry.\n          schema:\n            type: integer\n            minimum: 0\n            default: 100\n        - name: brief\n          in: query\n          description: >-\n            Omit the free-form text from every item: `description`, `design`,\n            `acceptance_criteria`, `notes`, `payload` and `waiters` are not\n            selected. Filtering is unaffected, because it selects rows and this\n            selects fields. Default false, so the payload is unchanged for a\n            client that does not ask.\n\n\n            The response carries no marker for the omission, so an omitted\n            field is indistinguishable from a genuinely empty one: only the\n            client that sent this parameter knows the rows are partial. Fetch a\n            whole issue with `GET /v0/beads/issues/{id}`.\n          schema:\n            type: boolean\n            default: false\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: A page of ready work.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ReadyPage'\n        '400':\n          $ref: '#/components/responses/InvalidArgument'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/stats:\n    get:\n      operationId: getStats\n      summary: Workspace summary statistics\n      description: >-\n        The counts `bd status` prints and its `bd stats` alias reprints: rows by\n        status across the durable plane, plus the two DEPENDENCY-AWARE numbers —\n        the blocked count and the readiness derived from it — that make this a\n        different question from a filtered count.\n\n\n        It is `stats` rather than `status` in the path deliberately. `status`\n        on an HTTP surface reads as the SERVER's condition, which is `/healthz`;\n        this operation answers about the WORKSPACE and touches the database to\n        do it.\n\n\n        THE WISP TIER IS NOT INCLUDED in the workspace-wide answer, so a\n        workspace whose work lives in ephemeral rows reports zeros here.\n        Supplying `assignee` changes that, along with three other definitions —\n        see the parameter.\n\n\n        THERE IS NO PREDICATE and there will not be one. A count of a set the\n        caller describes is a different question with a different answer shape;\n        this operation is the summary or it is nothing.\n      parameters:\n        - name: assignee\n          in: query\n          description: >-\n            Answer for ONE ACTOR instead of the workspace. Used as written: no\n            trimming, no case folding, no alias expansion. An empty value is a\n            400 — the workspace-wide question is asked by OMITTING the\n            parameter, and an empty assignee would otherwise select the rows\n            with no assignee and report them as one actor's workload.\n\n\n            IT CHANGES FOUR DEFINITIONS AT ONCE, and they are named on the\n            `Statistics` properties rather than here: the set widens to include\n            the ephemeral tier, `blocked_issues` counts the `blocked` STATUS\n            instead of the dependency flag, `ready_issues` becomes the real\n            ready-work count instead of a subtraction, and `pinned_issues` is\n            always 0. An actor with no rows is a summary of zeros, not a 404.\n          schema:\n            type: string\n        - name: skip_blocked\n          in: query\n          description: >-\n            Ask for the answer WITHOUT the blocked-set scan, which is the\n            expensive half of this query on a large workspace — the same request\n            `bd status --no-blocked` makes.\n\n\n            IT IS A HINT. When it is honored, `blocked_issues` and\n            `ready_issues` are both null and `blocked_count_skipped` is true;\n            when the backend has no cheaper path the full numbers come back and\n            the flag is false. Nothing else in the summary changes either way,\n            so a client that reads neither pointer cannot tell the difference.\n\n\n            IGNORED when `assignee` is set: that answer computes both numbers by\n            a route with no fast path, and it is not an error to ask.\n          schema:\n            type: boolean\n            default: false\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: The workspace summary.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/StatsResponse'\n        '400':\n          $ref: '#/components/responses/InvalidArgument'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/dependencies/cycles:\n    get:\n      operationId: listDependencyCycles\n      summary: List dependency cycles\n      description: >-\n        Every circular blocking dependency in the workspace, the same sweep\n        `bd dep cycles` performs. Items are `Cycle` — the element type that\n        command's `--json` emits — so the two surfaces carry one shape.\n\n\n        THE ANSWER IS CANONICAL: each cycle's members are rotated so the lowest\n        id comes first, and the cycles are sorted against each other, so two\n        calls against an unchanged workspace return the same bytes. A client may\n        diff two snapshots and read a difference as a real change.\n\n\n        EDGES ARE NARROWER THAN THEY ARE AT WRITE TIME. The walk follows\n        `blocks` and `conditional-blocks` only: `waits-for` is gate semantics, so\n        a mutual wait is not a deadlock, and `parent-child` is walked by the\n        refusal a dependency WRITE performs but not by this report. A workspace\n        this operation calls clean can therefore still refuse an edge.\n\n\n        Both dependency planes — durable and ephemeral — are one graph here, so\n        a cycle that runs issue → wisp → issue is reported.\n\n\n        THERE IS NO `limit` AND NO CURSOR, and `has_more` is therefore always\n        false in v0. Truncating would shrink the count, and the count is the\n        number an operator acts on; the response is bounded by the number of\n        cycles in the workspace, which in a healthy one is zero. `has_more` is\n        present so that adding a bound later is additive rather than a new\n        envelope.\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: Every dependency cycle, canonically ordered.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/CyclesPage'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/dependencies/tree:\n    get:\n      operationId: getDependencyTree\n      summary: Walk the dependency tree of one issue\n      description: >-\n        The dependency graph walked recursively from ONE root — the answer\n        `bd dep tree` renders, and the same `TreeNode` elements that command's\n        `--json` emits.\n\n\n        THE ANSWER IS FLAT. Each element carries `depth` and `parent_id`, and a\n        client rebuilds the shape from those two; the elements are in\n        depth-first pre-order, so a subtree is contiguous.\n\n\n        EVERY NODE APPEARS AT MOST ONCE PER WALK, at the depth and parent of the\n        first path that reached it. That single rule is both the cycle policy\n        and the diamond policy: a cycle TERMINATES rather than failing the call\n        or being reported (`GET /v0/beads/dependencies/cycles` is where a cycle\n        is an answer), and a shared subtree is shown under one parent only, with\n        no option to show it twice.\n\n\n        EDGES ARE WIDER THAN THEY ARE ON THE CYCLE REPORT. This walk follows\n        every dependency type except `relates-to`, which is symmetric annotation\n        — following it would make the \"tree\" the connected component. The cycle\n        report follows `blocks` and `conditional-blocks` only, because it is\n        about scheduling deadlock. Note that `related` and `relates-to` are two\n        different types and only the second is excluded.\n\n\n        Both dependency planes — durable and ephemeral — are one graph here, so\n        an ephemeral step in the middle of a chain does not end the picture.\n\n\n        A NODE THIS DATABASE CANNOT DESCRIBE ENDS THAT BRANCH. A `TreeNode` IS\n        an issue, so there is no shape for \"on the tree and undescribable\": an\n        edge whose target is an `external:` reference or an id in another\n        repository's namespace contributes no node, and nothing in the answer\n        says a branch stopped for that reason rather than because it ended. That\n        is the one place this operation is less honest than the cycle report,\n        whose `CycleMember` can carry a bare id.\n\n\n        THERE IS NO `limit` AND NO CURSOR. The walk is bounded by `max_depth`\n        instead, which bounds the DESCENT rather than truncating the answer;\n        `has_more` is therefore always false in v0. There is also no `max_rows`:\n        the CLI's defensive cap is a circuit breaker for a caller that would\n        rather fail than wait, and refusing a whole answer is not something this\n        surface offers a remote client.\n      parameters:\n        - name: root_id\n          in: query\n          required: true\n          description: >-\n            The issue to walk from. It must be an EXACT canonical issue id:\n            there is no fuzzy, prefix or substring resolution on this surface,\n            for the reason `GET /v0/beads/issues/{id}` gives. An empty value is a\n            400 `invalid_argument`; a value that matches no issue and no wisp is\n            a 404 `not_found`, because there is one anchor here and no other\n            answer to preserve.\n          schema:\n            type: string\n        - name: direction\n          in: query\n          description: >-\n            Which way to follow edges. `down` (the default) walks what the root\n            DEPENDS ON; `up` walks what depends ON it; `both` walks each way and\n            returns one list.\n\n\n            For `both` the two walks are independent and the answer is their\n            concatenation: every up node except the root, then the whole down\n            tree beginning with the root. The root appears once. The two halves\n            may repeat a node between them — an issue that both blocks and is\n            blocked by something in the other half — so a client aggregating\n            `items` must not assume the ids are distinct. Both walks see ONE\n            database state.\n\n\n            Any other value is a 400 `invalid_argument`: the vocabulary is\n            closed.\n          schema:\n            type: string\n            enum: [down, up, both]\n            default: down\n        - name: max_depth\n          in: query\n          description: >-\n            How many LEVELS to descend, counting the root as level one:\n            `max_depth=1` is the root alone. A node beyond the bound is ABSENT\n            rather than present and flagged.\n\n\n            Zero and negative values are a 400 `invalid_argument` rather than\n            \"unbounded\": the answer to an unbounded recursive walk on a large\n            workspace is the request that takes the database down.\n          schema:\n            type: integer\n            minimum: 1\n            default: 50\n        - name: status\n          in: query\n          description: >-\n            Prune the walked tree to the nodes carrying this status AND the\n            ancestor chain of each survivor, so the answer is still a tree.\n\n\n            It is a POST-WALK PRUNE, not a filter on the walk, and the difference\n            is observable: a matching node BEHIND a non-matching one is still\n            reached, and the non-matcher is kept as its ancestor. A prune that\n            matches nothing returns NO items at all, root included.\n\n\n            The value is not checked against the workspace's status vocabulary;\n            an unrecognized status simply matches nothing.\n          schema:\n            type: string\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: The walked tree, flat, in depth-first pre-order.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/DependencyTreePage'\n        '400':\n          $ref: '#/components/responses/InvalidArgument'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/ready:count:\n    get:\n      operationId: countReadyWork\n      summary: Count ready work\n      description: >-\n        How many items `GET /v0/beads/ready` would return for the same filters\n        with `limit=0`. That is an identity, not an estimate: the two\n        operations answer from one role over one predicate\n        (`issueops.ReadyCounter`), so a client may page the listing and print\n        this number beside it — which is exactly what `bd ready` does when it\n        reports \"showing 100 of 412\".\n\n\n        THERE IS NO `limit` AND NO `sort` PARAMETER, and both absences are\n        deliberate. A cardinality has no page: a limit here would answer \"how\n        many of the first N\", which breaks the identity above, so the role\n        refuses one rather than accepting it and dropping it. A cardinality has\n        no order either — this operation counts the same set the listing\n        returns under ANY sort policy — so there is nothing for a `sort`\n        parameter to change and it is not published. Every other parameter of\n        the listing is here and means exactly what it means there, including\n        the default type exclusions and the way `type` drops them.\n\n\n        It costs a query of its own. A client that only needs to know whether\n        MORE work exists should read `has_more` on the listing instead; this\n        operation is for the client that needs the number.\n      parameters:\n        - name: assignee\n          in: query\n          description: Only issues assigned to this actor.\n          schema:\n            type: string\n        - name: unassigned\n          in: query\n          description: Only issues with no assignee.\n          schema:\n            type: boolean\n        - name: type\n          in: query\n          description: >-\n            Issue type, with the shorthand alias expansion and the\n            match-nothing-rather-than-fail treatment `GET /v0/beads/ready`\n            documents. Setting it drops the default type exclusions and makes\n            `exclude_type` ignored, there and here alike.\n          schema:\n            type: string\n        - name: exclude_type\n          in: query\n          description: >-\n            Issue types to exclude. Repeat the parameter, or pass a\n            comma-separated list. Ignored when `type` is set.\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: label\n          in: query\n          description: Labels that must ALL be present (AND).\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: label_any\n          in: query\n          description: Labels of which at least one must be present (OR).\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: exclude_label\n          in: query\n          description: Labels that must not be present.\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: label_pattern\n          in: query\n          description: Glob matched against labels.\n          schema:\n            type: string\n        - name: label_regex\n          in: query\n          description: Regular expression matched against labels.\n          schema:\n            type: string\n        - name: priority\n          in: query\n          description: Exact priority (0 is a real value, not \"unset\").\n          schema:\n            type: integer\n        - name: parent\n          in: query\n          description: Restrict to recursive descendants of this issue.\n          schema:\n            type: string\n        - name: metadata_field\n          in: query\n          description: >-\n            Top-level metadata equality filter as `key=value`, split on the\n            first `=`. Repeatable. An invalid key is a 400.\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: has_metadata_key\n          in: query\n          description: Only issues carrying this top-level metadata key.\n          schema:\n            type: string\n        - name: include_ephemeral\n          in: query\n          description: >-\n            Include ephemeral (non-synced) rows, which the count merges exactly\n            as the listing lists them.\n          schema:\n            type: boolean\n            default: false\n        - name: include_deferred\n          in: query\n          description: Include issues whose `defer_until` is still in the future.\n          schema:\n            type: boolean\n            default: false\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: The size of the ready set.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ReadyCount'\n        '400':\n          $ref: '#/components/responses/InvalidArgument'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues:\n    get:\n      operationId: listIssues\n      summary: List issues\n      description: >-\n        Issues under the same default exclusions `bd list` applies (closed and\n        custom done/frozen statuses, templates, gates and configured infra\n        types), each carrying dependency, dependent and comment counts.\n\n\n        ORDERING IS FIXED AND DIVERGES FROM `bd list` DELIBERATELY. Every page\n        of this operation — cursored or not — is ordered by\n        `(created_at DESC, id ASC)`, while `bd list`'s default ordering is\n        priority-first. The divergence is the price of a correct cursor: the\n        cursor is a keyset position in the created order, so if the first page\n        fell back to priority ordering the second page would skip and duplicate\n        rows. That is also why v0 exposes no `sort` parameter — the sort order\n        is welded to the cursor contract, and a new order needs new surface.\n\n\n        The item set, and each item's JSON, are identical to `bd list --json`;\n        only the order differs.\n\n\n        EPHEMERAL ROWS ARE OUT OF SCOPE BY DEFAULT and are admitted by\n        `include_ephemeral`, which merges that tier IN ADDITION to the durable\n        one. The merged page is ordered by the same `(created_at DESC, id ASC)`\n        key across both tiers as if they were one table, and `cursor` pages\n        across the merge without skipping or repeating a row.\n\n\n        `include_ephemeral` and `include_infra` are INDEPENDENT and compose:\n        the first admits a tier, the second takes the infrastructure TYPE\n        exclusions off (and admits the tier those types live in). Ephemeral\n        rows of an infrastructure type therefore need `include_infra`.\n\n\n        The CLI has no `--include-ephemeral` flag for `bd list` yet, so this is\n        the one filter on this operation with no `bd list` spelling;\n        `--include-infra` is the nearest one and is wider.\n\n\n        THE ROWS CARRY NO `revision`, AND THE DETAIL READ DOES. A list-then-guard\n        loop is a real shape, and the reason the token stops at\n        `GET /v0/beads/issues/{id}` is not its size — measured against a 20k-row\n        production export the member is 28 bytes on a 1449-byte median row,\n        under 2%. It is that `IssueWithCounts` is ALSO THE INTERCHANGE ROW: it\n        is the record `bd export` writes to JSONL and the auto-export flushes\n        into a git-tracked `issues.jsonl`, and the token is re-minted by every\n        write, so publishing it on this element would put a per-write-random\n        value into a file whose whole value is that it diffs only when\n        something meant something. That is the loss `Issue`'s own storage field\n        is withheld from generic serialization to prevent, and a wire member\n        cannot opt out of it — the element here and the element there are one\n        pinned Go struct. `IssueDetails` has neither problem: nothing\n        interchanges it, and it is assembled in exactly one place, which is\n        also why its token cannot be silently 0 on some path that forgot to\n        set it. A caller that has a list and wants a guard reads the rows it\n        actually intends to write, one detail read each — the read it needs\n        anyway to decide.\n\n\n        THE DOOR, IF A LATER REVISION WANTS THE TOKEN HERE: it needs a list\n        element that is NOT the interchange element. The exclusion above is a\n        consequence of the two being ONE pinned Go struct, not a judgement that\n        a per-row token is unwanted, so the way in is to separate them — and\n        nothing short of that will do, because any member added to this element\n        ships in `bd export`'s JSONL by construction. Until then a client MUST\n        model the absent member as ABSENT and never as `0`: zero is a real\n        token here (a legacy row backfilled and not mutated since), so a client\n        that defaulted a missing `revision` to 0 would compose guards that\n        match exactly the rows it is most dangerous to be wrong about.\n      parameters:\n        - name: status\n          in: query\n          description: >-\n            Status filter. Repeat the parameter, or pass a comma-separated\n            list. Custom statuses configured for the workspace are honored.\n\n\n            Setting this REPLACES the default status exclusions rather than\n            fighting with them: `status=closed` on its own returns closed\n            issues, and does NOT also need `all=true`. Leave it unset to get\n            the default view described above.\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: type\n          in: query\n          description: >-\n            Issue type. Shorthand aliases are expanded exactly as\n            `bd list --type` expands them (`mr`, `feat`, `mol`, `enhancement`,\n            `dec`, `adr`); there is no plural folding. Unlike on `ready`, an\n            unknown type is a 400 `invalid_argument` with\n            `reason: \"invalid_value\"`, because `bd list` validates the type\n            against the workspace's configured vocabulary and this operation\n            keeps that behavior.\n          schema:\n            type: string\n        - name: assignee\n          in: query\n          description: Only issues assigned to this actor.\n          schema:\n            type: string\n        - name: label\n          in: query\n          description: Labels that must ALL be present (AND).\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: label_any\n          in: query\n          description: Labels of which at least one must be present (OR).\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: exclude_label\n          in: query\n          description: Labels that must not be present.\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: parent\n          in: query\n          description: Restrict to recursive descendants of this issue.\n          schema:\n            type: string\n        - name: all\n          in: query\n          description: >-\n            Drop the default status exclusions (closed plus any custom statuses\n            in the done/frozen categories). Redundant when `status` is set,\n            which drops them already.\n          schema:\n            type: boolean\n            default: false\n        - name: include_templates\n          in: query\n          description: Include template molecules.\n          schema:\n            type: boolean\n            default: false\n        - name: include_gates\n          in: query\n          description: Include gate issues.\n          schema:\n            type: boolean\n            default: false\n        - name: include_infra\n          in: query\n          description: >-\n            Include the workspace's configured infrastructure issue types. This\n            also admits the ephemeral plane those types live in, so it is\n            strictly wider than `include_ephemeral`.\n          schema:\n            type: boolean\n            default: false\n        - name: include_ephemeral\n          in: query\n          description: >-\n            Include the ephemeral tier — ephemeral rows and the non-synced\n            rows stored beside them — merged into the same\n            `(created_at DESC, id ASC)` order as the durable ones.\n\n\n            It admits a TIER and takes no TYPE exclusion off, so a row whose\n            type this operation already hides stays hidden. That includes the\n            configured infrastructure types: ephemeral `agent`, `role` and\n            `message` rows need `include_infra` as well as, or instead of,\n            this one. What `include_ephemeral` alone reaches is the ephemeral\n            rows of the types a listing already shows.\n          schema:\n            type: boolean\n            default: false\n        - name: created_before\n          in: query\n          description: Only issues created strictly before this instant (RFC 3339).\n          schema:\n            type: string\n            format: date-time\n        - name: created_after\n          in: query\n          description: Only issues created strictly after this instant (RFC 3339).\n          schema:\n            type: string\n            format: date-time\n        - name: metadata_field\n          in: query\n          description: >-\n            Top-level metadata equality filter as `key=value`, split on the\n            first `=`. Repeatable. An invalid key is a 400.\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: has_metadata_key\n          in: query\n          description: Only issues carrying this top-level metadata key.\n          schema:\n            type: string\n        - name: cursor\n          in: query\n          description: >-\n            Opaque keyset position, taken verbatim from a previous response's\n            `next_cursor`. Clients MUST NOT construct, parse or mutate it: its\n            encoding is server-private and versioned, and an undecodable or\n            unknown-version value is refused with 400 `invalid_cursor`. The\n            recovery for that refusal is normative: restart paging with no\n            `cursor` at all — the position cannot be salvaged, and re-sending\n            the same value cannot succeed.\n\n\n            LIFETIME: a cursor holds a position and a private encoding version,\n            and nothing else. The server keeps no state for it, so it does not\n            expire, does not become invalid when the server restarts, and is\n            not tied to the connection that issued it; the only thing that\n            invalidates one is a change to the encoding, which surfaces as\n            `invalid_cursor`.\n\n\n            MISUSE IS NOT DETECTABLE, which is why repeating the filters\n            matters. Because the token carries no filters, a page fetched with\n            a cursor minted under DIFFERENT filters is not refused: the server\n            applies the filters of the current request from the position of the\n            old one, silently skipping every row the new filter set would have\n            placed before that position. Repeat every filter verbatim for the\n            whole traversal, and start a new traversal when they change.\n          schema:\n            type: string\n        - name: limit\n          in: query\n          description: >-\n            Maximum number of items to return. `0` means unlimited, exactly as\n            `bd list --limit 0` does — the two surfaces read the same shared\n            default and the same zero semantics, so they cannot diverge. An\n            unlimited page reports `has_more: false` and carries no\n            `next_cursor`. A negative value is a 400.\n\n\n            The default below is the shared list limit constant\n            (`workapi.DefaultListLimit`, the value `bd list`'s `--limit` flag\n            registers).\n\n\n            One exception, and it is mode-dependent: when the server was\n            started with `--allow-non-loopback`, `limit=0` is refused with 400\n            `invalid_argument`, `param: \"limit\"`, `reason: \"invalid_value\"` and\n            detail \"unlimited reads are loopback-only; pass an explicit limit\".\n            An unlimited read buffers the whole active set and its JSON encoding\n            inside one shared process, which must not be reachable by arbitrary\n            network peers. The bind mode is deliberately NOT advertised in\n            `ContextResponse` — a client that wants an unlimited read asks for\n            one and, on that 400, re-issues with an explicit limit (and pages\n            with `cursor`); it is a client-side fix, never a retry.\n          schema:\n            type: integer\n            minimum: 0\n            default: 50\n        - name: brief\n          in: query\n          description: >-\n            Omit the free-form text from every item: `description`, `design`,\n            `acceptance_criteria`, `notes`, `payload` and `waiters` are not\n            selected. Filtering is unaffected, because it selects rows and this\n            selects fields. Default false, so the payload is unchanged for a\n            client that does not ask.\n\n\n            The response carries no marker for the omission, so an omitted\n            field is indistinguishable from a genuinely empty one: only the\n            client that sent this parameter knows the rows are partial. Fetch a\n            whole issue with `GET /v0/beads/issues/{id}`.\n          schema:\n            type: boolean\n            default: false\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: A page of issues in `(created_at DESC, id ASC)` order.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/IssuesPage'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, a malformed parameter\n            value, or an undecodable/unknown-version `cursor`.\n          x-bd-codes: [invalid_argument, invalid_cursor]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n    post:\n      operationId: createIssue\n      summary: Create one issue\n      description: >-\n        Creates one issue, with its parent, its explicit edges and its waits-for\n        gate, as ONE transaction. A plain collection `POST` rather than a custom\n        method, because creating one member of the collection the path names is\n        what `POST` already means — the same argument `memories.remember` makes\n        on its own collection.\n\n\n        ## It publishes the whole create vocabulary\n\n\n        Every member the role accepts and this surface publishes anywhere is\n        here, and that is deliberate: `POST /v0/beads/issues:batchCreate` spells\n        nine of them, which is what makes it unusable for a caller composing a\n        real row — no `status`, no `sender`, no `metadata`, no `ephemeral`, no\n        `no_history`, no `id`. This operation is the single-issue create with\n        `ApplyCreateItem`'s vocabulary, and the two agree member for member\n        except where a difference is stated below.\n\n\n        THE EDGES ARE HERE, unlike on `issues:batchApply`. That operation splits\n        edges into their own `dep_add` items so a plan's edge order is total;\n        one create has no ordering to express, so `parent_id`, `dependencies`\n        and `waits_for` ride on the request the way `bd create --parent`,\n        `--deps` and `--waits-for` do. Every edge lands in the same transaction\n        as the row: an edge this request could not write is a `400` and the\n        issue is not created either.\n\n\n        ## The explicit id\n\n\n        `id` is CREATE-ONLY. An id that already names a stored row — on either\n        plane — is a `409 already_exists` and nothing is written. It is never an\n        adoption and never an overwrite: `PATCH /v0/beads/issues/{id}` acts on a\n        row that already exists, and `bd import` is the upsert surface, which\n        this API does not publish. Absent is the ordinary case and the server\n        mints one.\n\n\n        ## What this operation deliberately cannot set\n\n\n        `created_at` and `created_by` — a create whose stored creation time and\n        author come from the caller makes the row's own timestamp disagree with\n        the journal entry that records it, and re-dating history is what an\n        import is for. `issues:batchApply`'s create item publishes neither\n        either.\n\n\n        `spec_id`, `await_*`, `mol_type`, `wisp_type`, `work_type`,\n        `storage_class`, `source_*`, `pinned`, `is_template` and the event\n        quartet (`event_kind`, `actor`, `target`, `payload`) — workflow and\n        classification plumbing this surface publishes on no operation, read or\n        write. `PATCH /v0/beads/issues/{id}` says the same of `spec_id` and\n        `await_id`.\n\n\n        `comments` and an inline `dependencies` list on the issue itself — the\n        role refuses both, because edges belong to the request's own\n        `dependencies` member where their direction can be stated.\n\n\n        ## Planes\n\n\n        `ephemeral` and `no_history` create the row on the EPHEMERAL plane\n        rather than the durable one, exactly as they do for `issues:batchApply`.\n        They are mutually exclusive; sending both is a `400`. An edge between\n        rows on opposite planes is refused with everything else the request\n        asked for.\n\n\n        ## Hooks and auto-commit\n\n\n        Hooks do not fire and the per-command auto-commit machinery does not\n        run, exactly as for `POST /v0/beads/issues/{id}:claim`. The only durable\n        effect is the single storage commit the role makes in its own\n        transaction.\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CreateIssueRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            The created issue, hydrated with its labels and the edges this\n            request wrote. It is the row as STORED — the minted id, the\n            defaulted status and the persisted timestamps — never the request\n            reflected back.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Issue'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, an unparseable or\n            oversized body, an unknown body member at any level, an `actor` that\n            is empty after trimming, longer than 256 bytes or carrying control\n            characters, a blank or missing `title`, a member carrying the wrong\n            JSON type, an explicit `null` on any member, a value outside its\n            documented bounds, `ephemeral` together with `no_history` — or a\n            value this workspace's own validation refuses, such as an\n            `issue_type` or `status` outside its configured vocabulary, an\n            OMITTED `issue_type` (see that member), an `id` outside its\n            configured prefix, an edge from the new issue to itself, the same\n            pair named twice by two members, or a dependency, parent or\n            waits-for target that names nothing this workspace holds.\n\n\n            A DEPENDENCY TARGET THAT NAMES NOTHING IS A `400`, NOT A `404`,\n            conforming to `POST /v0/beads/dependencies:add` and\n            `POST /v0/beads/issues:batchCreate`: an edge describes a relation\n            rather than a resource this request was asked to address, and this\n            operation names no id in its path to have missed. Nothing is created\n            in any of these cases.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '409':\n          description: >-\n            The request is well-formed and the STATE refuses it, and NOTHING WAS\n            WRITTEN.\n\n\n            `already_exists` is an explicit `id` that already names a stored row.\n            `param` is `id`. There is no force bypass and no upsert: the\n            identical body succeeded before the id was taken and would succeed\n            against a workspace that never took it, which is why this is a `409`\n            rather than a `400` — recovery is to look at the state (adopt the row\n            with `PATCH`, choose another id, or stop) rather than to fix a\n            malformed request.\n\n\n            `dependency_cycle` is the graph refusing the edges this request\n            asked for, spelled exactly as `POST /v0/beads/dependencies:add`\n            spells it and covering both of that operation's refusals: a\n            scheduling cycle, and a blocking edge against the new issue's own\n            ancestor or descendant. The hierarchy case — and only it —\n            additionally carries `issue_id`, `blocker_id` and\n            `blocker_is_ancestor`, so member presence is the discriminator.\n            Neither has a force bypass.\n\n\n            BOTH ARMS ARE REACHABLE HERE because this operation writes BLOCKING\n            edges and hierarchy in one transaction: `parent_id` places the new\n            row under a parent whose ancestry the caller cannot see, and\n            `dependencies[]` may name a blocking edge against that ancestry —\n            while `dependencies[].reverse` writes an edge INTO the id being\n            minted, which is the only way a create can close a scheduling cycle.\n            `PATCH /v0/beads/issues/{id}` publishes no blocking-edge member, so\n            its `dependency_cycle` never carries the hierarchy members; the\n            discriminator is meaningful here and absent there.\n          x-bd-codes: [already_exists, dependency_cycle]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues:query:\n    get:\n      operationId: queryIssues\n      summary: Query issues with a boolean expression\n      description: >-\n        The `bd query` expression language over HTTP: field comparisons\n        combined with `AND`, `OR`, `NOT` and parentheses. It is the one\n        operation on this surface that takes a DISJUNCTION — every filter\n        parameter of `GET /v0/beads/issues` narrows the answer, and no\n        combination of them expresses `type=bug OR label=urgent`.\n\n\n        IT IS NOT A SQL PASSTHROUGH, and the shape of the language is what\n        makes that true rather than a promise about validation: there are no\n        table names, no joins and no way to name a column the vocabulary does\n        not publish. The vocabulary is the one `bd query --help` documents, and\n        an expression outside it is a 400 rather than an empty page.\n\n\n        EVERY MATCH IS IN SCOPE, which has not always been true and is the\n        reason this operation exists in the shape it does. An expression the\n        storage filter cannot express is answered by evaluating the predicate\n        over every candidate row and then cutting the page, so `has_more` means\n        what it says. The CLI used to bound that scan at a few hundred rows and\n        filter what came back, which silently dropped matches from an `OR`\n        query and reported the result as complete. The cost is stated rather\n        than hidden: a broad expression over a large workspace is a large read,\n        and `limit` bounds the RESPONSE rather than the scan.\n\n\n        THERE IS NO CURSOR AND NO `offset`. A cursor is a keyset position in a\n        database order, and the matching set of a predicate query is assembled\n        outside the database, so there is no position to encode; `offset` is\n        absent because the two database sources this server can be built on\n        disagree about whether they can honor one, and a parameter that works\n        under one deployment topology and refuses under another is worse on a\n        wire than an absent one. Raise `limit`, or narrow the expression.\n      parameters:\n        - name: q\n          in: query\n          required: true\n          description: >-\n            The query expression, e.g. `status=open AND priority>1` or\n            `type=bug OR label=urgent`. Blank, unparseable, or naming a field\n            or operator the language does not have is a 400 `invalid_argument`\n            with `param: \"q\"` and `reason: \"invalid_value\"`.\n\n\n            Relative date terms (`created>7d`) are resolved against the\n            SERVER's clock at the moment the request is served.\n          schema:\n            type: string\n        - name: all\n          in: query\n          description: >-\n            Include closed issues. Without it closed issues are excluded —\n            unless the expression itself compares `status`, in which case the\n            expression's own opinion stands and this parameter changes nothing.\n          schema:\n            type: boolean\n            default: false\n        - name: sort\n          in: query\n          description: >-\n            Display order for the page. Absent leaves the rows in the order the\n            query returned them, which is the same thing `bd query` without\n            `--sort` does.\n\n\n            WHAT IT ORDERS is the rows the query bounded, exactly as on the\n            CLI: for an expression the database answered under `limit`, the\n            page; for a predicate expression, which the database cannot bound,\n            the whole matching set.\n          schema:\n            type: string\n            enum: [priority, created, updated, closed, status, id, title, type, assignee]\n        - name: reverse\n          in: query\n          description: Invert the display order. Ignored when `sort` is absent.\n          schema:\n            type: boolean\n            default: false\n        - name: limit\n          in: query\n          description: >-\n            Maximum number of items to return. `0` means unlimited, exactly as\n            `bd query --limit 0` does — the two surfaces read the same shared\n            default (`workapi.DefaultQueryLimit`, the value `bd query --limit`\n            registers) and the same zero semantics. A negative value is a 400.\n\n\n            The same mode-dependent refusal the issue listing carries applies\n            here: under `--allow-non-loopback`, `limit=0` is refused with 400\n            `invalid_argument`, `param: \"limit\"`, `reason: \"invalid_value\"`.\n          schema:\n            type: integer\n            minimum: 0\n            default: 50\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: A page of the issues the expression matched.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/QueryPage'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, a malformed parameter\n            value, or an expression this server cannot parse or evaluate.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues:count:\n    get:\n      operationId: countIssues\n      summary: Count issues matching a predicate\n      description: >-\n        How many issues match, and — with `group_by` — how many in each bucket.\n        It is the operation behind `bd count`, and it answers from\n        `issueops.Counter` over the same predicate that command builds.\n\n\n        ## It is not `GET /v0/beads/issues` with the page taken off\n\n\n        That is the difference to internalize before using it, because the two\n        answer about DIFFERENT SETS by default. A listing hides closed, pinned,\n        template and gate rows; a count hides NONE of them. An empty request\n        here counts every durable row this workspace holds, closed rows\n        included, and it is the ROLE that decides that — the same decision\n        `bd count` has always made — not a default this operation applies.\n\n\n        Nor is it a listing with paging removed at the type level: there is no\n        `limit`, no `offset` and no `cursor`, and the role refuses the first two\n        rather than accepting them and dropping them. A cardinality is a number\n        about a set; bounding the scan would answer \"how many of the first N\",\n        which is the shape that makes a caller believe `limit=10` bounded the\n        answer.\n\n\n        There is no free-text `q` either. The count seam takes one and both of\n        this role's front doors have always passed the empty string, so it is\n        left off rather than published untested. `title`, `title_contains`,\n        `desc_contains` and `notes_contains` are the substring matches that ARE\n        reachable.\n\n\n        ## One operation, two shapes of one answer\n\n\n        `group_by` selects the bucketed form. It is a parameter rather than a\n        second operationId because the role is one role born with two methods,\n        for a reason this document inherits rather than re-decides: the two ask\n        the SAME predicate of the SAME set and differ only in whether the answer\n        is one number or a number per bucket. The grouped response is the scalar\n        response PLUS `groups` — the same schema, with one member that appears\n        when you ask for it — so there is no second contract here to hide under\n        one id.\n\n\n        That is the opposite of `GET /v0/beads/events:watch`, which is a sibling\n        operation rather than a mode of the paged read: those two differ in media\n        type, lifetime, limits and capacity, and one operation carrying both\n        would have documented two of everything. Here nothing differs but one\n        optional response member.\n\n\n        ## `total` is not the sum of `groups`\n\n\n        For four of the five dimensions it happens to be. For `label` it is not,\n        and that is why the role computes it rather than leaving a client to add\n        the buckets up: LABEL BUCKETS OVERLAP. An issue carrying three labels is\n        one row in `total` and one row in each of three buckets, so a client that\n        summed them would report a workspace three times its size.\n\n\n        The two numbers are NOT promised to describe one snapshot. The\n        store-backed implementation runs the scalar and the grouped query\n        separately, so a concurrent write between them can leave them\n        disagreeing by that write. Nothing here is transactional across the two.\n\n\n        ## Planes\n\n\n        A count is DURABLE-PLANE ONLY unless `include_infra` is set. The wisps\n        tier — ephemeral wisps and the `no_history` beads that are durable work\n        stored in that tier — is not counted by default, and `include_infra`\n        changes FOUR things at once rather than one. See that parameter; it is\n        the one place on this operation where a single flag moves the set in\n        more than one direction.\n      parameters:\n        - name: status\n          in: query\n          description: >-\n            One stored status. The empty value and the literal `all` both mean\n            EVERY status, which is what makes a bare count answer for closed\n            rows as well as open ones.\n\n\n            IT IS ONE STATUS, NOT A COMMA-SEPARATED SET, and that is the one\n            parameter name this operation shares with `GET /v0/beads/issues`\n            while meaning something narrower — that one takes a comma-separated\n            OR set. It is stated here rather than quietly reconciled, because a\n            client that assumed otherwise would read a plausible number instead\n            of an error.\n\n\n            It is NOT validated against this workspace's configured vocabulary.\n            An unrecognized name matches nothing and the answer is `0`, not a\n            refusal — the shipped behavior of `bd count`, published rather than\n            tightened, because a scripted caller counting a status its workspace\n            has since dropped currently reads 0 and would otherwise start\n            reading an error.\n          schema:\n            type: string\n        - name: type\n          in: query\n          description: >-\n            One issue type, with `status`'s match-nothing-rather-than-fail\n            treatment and NO shorthand alias expansion at all — unlike\n            `GET /v0/beads/ready`'s `type`, which expands aliases.\n\n\n            It has a SECOND effect under `include_infra`: a type this workspace\n            calls infra routes the count to the ephemeral tier. See that\n            parameter.\n          schema:\n            type: string\n        - name: assignee\n          in: query\n          description: >-\n            Only issues assigned to this actor. Sending it beside\n            `no_assignee` is not refused: the two are handed to the filter as\n            written and answer with the empty intersection, which is `0`.\n          schema:\n            type: string\n        - name: priority\n          in: query\n          description: >-\n            Exact priority. Absent means unfiltered — the distinction matters\n            because `0` is a real priority, which is why the role models this\n            as a pointer.\n          schema:\n            type: integer\n        - name: priority_min\n          in: query\n          description: Lowest priority to count, inclusive.\n          schema:\n            type: integer\n        - name: priority_max\n          in: query\n          description: Highest priority to count, inclusive.\n          schema:\n            type: integer\n        - name: label\n          in: query\n          description: >-\n            Labels that must ALL be present. Repeat the parameter. Entries are\n            trimmed and de-duplicated inside the role, and a set whose entries\n            are all blank is the same as an unset one.\n          schema:\n            type: array\n            items:\n              type: string\n        - name: label_any\n          in: query\n          description: >-\n            Labels of which at least ONE must be present. Repeat the parameter;\n            same normalization as `label`.\n          schema:\n            type: array\n            items:\n              type: string\n        - name: title\n          in: query\n          description: Case-insensitive substring match on the title.\n          schema:\n            type: string\n        - name: id\n          in: query\n          description: >-\n            A comma-separated id set to restrict the count to. Splitting,\n            trimming and de-duplication happen inside the role, so a caller\n            passes the string it was given rather than a slice it had to\n            prepare.\n          schema:\n            type: string\n        - name: title_contains\n          in: query\n          description: >-\n            Substring match on the title, spelled as `GET /v0/beads/issues:query`\n            spells it. It overlaps `title` deliberately: both reach the filter,\n            and the two names exist because the role carries both fields.\n          schema:\n            type: string\n        - name: desc_contains\n          in: query\n          description: Substring match on the description.\n          schema:\n            type: string\n        - name: notes_contains\n          in: query\n          description: Substring match on the notes.\n          schema:\n            type: string\n        - name: created_after\n          in: query\n          description: RFC 3339. Only issues created strictly after this instant.\n          schema:\n            type: string\n            format: date-time\n        - name: created_before\n          in: query\n          description: RFC 3339. Only issues created strictly before this instant.\n          schema:\n            type: string\n            format: date-time\n        - name: updated_after\n          in: query\n          description: RFC 3339.\n          schema:\n            type: string\n            format: date-time\n        - name: updated_before\n          in: query\n          description: RFC 3339.\n          schema:\n            type: string\n            format: date-time\n        - name: closed_after\n          in: query\n          description: >-\n            RFC 3339. Counting closed rows needs no `status` beside it: a bare\n            count already includes them.\n          schema:\n            type: string\n            format: date-time\n        - name: closed_before\n          in: query\n          description: RFC 3339.\n          schema:\n            type: string\n            format: date-time\n        - name: empty_description\n          in: query\n          description: Only issues with no description.\n          schema:\n            type: boolean\n            default: false\n        - name: no_assignee\n          in: query\n          description: Only issues with no assignee.\n          schema:\n            type: boolean\n            default: false\n        - name: no_labels\n          in: query\n          description: Only issues carrying no label.\n          schema:\n            type: boolean\n            default: false\n        - name: include_infra\n          in: query\n          description: >-\n            Count the cardinality of `bd list --include-infra --all` instead of\n            the durable plane. IT CHANGES FOUR THINGS AT ONCE, and they are\n            listed rather than summarized because a caller reading \"include\n            infra\" would expect one:\n\n\n            the ephemeral wisps tier is MERGED IN, picking up both wisps and the\n            `no_history` beads that are durable work stored in that tier;\n            template molecules are EXCLUDED, which a default count includes;\n            gate beads are EXCLUDED unless `type=gate` asks for them by name;\n            and a `type` this workspace calls infra ROUTES the count to the\n            ephemeral tier instead of the durable one.\n\n\n            The infra vocabulary is the WORKSPACE's, read from its configuration\n            inside the role. A caller does not supply it and cannot — that\n            config load is what this role exists to keep off both front doors.\n\n\n            Unset, the count is durable-plane only and applies none of the four:\n            the historical `bd count` answer, kept exactly so a scripted caller\n            reads the same number it read yesterday.\n          schema:\n            type: boolean\n            default: false\n        - name: group_by\n          in: query\n          description: >-\n            Bucket the count by one dimension and return `groups` beside\n            `total`. Absent, the response carries `total` alone.\n\n\n            The set is CLOSED, and a value outside it is a `400` rather than an\n            empty answer: a caller that misspelled a dimension and got zero\n            buckets back has no way to tell that from a workspace with nothing\n            in it. That is the role's own rule, applied at the edge here so the\n            refusal names the parameter.\n\n\n            Bucket KEYS are normalized and printed unmodified by every front\n            door, so the normalization is part of this contract: a priority\n            bucket is `P` followed by the number (`P1`); the assignee bucket for\n            unassigned rows is `(unassigned)`, never the empty string, which\n            would be indistinguishable from a stored empty assignee; the label\n            bucket for rows carrying no label at all is `(no labels)`, and it is\n            ABSENT rather than zero when every matching row has one; `status`\n            and `type` buckets are the stored value verbatim.\n          schema:\n            type: string\n            enum: [status, priority, type, assignee, label]\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: The size of the matching set, and its buckets when asked for.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/IssueCount'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, a repeated\n            single-valued one, a malformed boolean, integer or RFC 3339 instant,\n            or a `group_by` outside the closed set.\n\n\n            EVERY REFUSAL HERE IS THE TRANSPORT'S. Unlike the listings, this\n            operation has no predicate its library surface can turn down: an\n            unrecognized `status` or `type` matches nothing and answers `0`, and\n            the one refusal the count role does make — an unknown bucketing\n            dimension — is refused at the edge above, so it never reaches the\n            role at all.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues/{id}:\n    get:\n      operationId: getIssue\n      summary: Get one issue with its labels, dependencies and counts\n      description: >-\n        Returns a single object, never an array. The id must be the exact\n        canonical issue id: there is no fuzzy, prefix or substring resolution\n        on this surface, so a lookup can never resolve to a different issue\n        than the caller named. Wisp (ephemeral) records are looked up as a\n        fallback when no issue matches.\n\n\n        COMMENT BODIES AND DEPENDENTS ARE ASKED FOR, NEVER VOLUNTEERED. They\n        are the two expensive row lists on this read, so each is absent unless\n        the request sets the parameter that populates it, and a caller that\n        wants only the cardinalities reads `comment_count`, `dependent_count`\n        and `comments_omitted` and pays for neither. A request that sets\n        neither parameter is answered exactly as this operation answered it\n        before the parameters existed.\n\n\n        IT IS THE TOKEN SOURCE FOR A GUARDED WRITE. The response carries\n        `revision`, the row's optimistic-concurrency token, so a\n        read-modify-write loop starts HERE: read the row, decide from what it\n        says, and send the token back as the next request's `expected_version`.\n        Before this member every token on the surface was minted by a WRITE, so\n        the first guarded write of a loop had to be preceded by a write the\n        caller did not want to make — or guarded on nothing at all, which is\n        the lost-update this whole family exists to refuse. The token is\n        carried on every 200, including for an ephemeral wisp resolved by the\n        fallback lookup.\n      parameters:\n        - $ref: '#/components/parameters/IssueID'\n        - name: include_comments\n          in: query\n          description: >-\n            Populate `comments` with the issue's full comment bodies. When it\n            is honored `comments_omitted` is false, so a client is never left\n            to guess whether an absent list means \"no comments\" or \"not asked\n            for\".\n          schema:\n            type: boolean\n            default: false\n        - name: include_dependents\n          in: query\n          description: >-\n            Populate `dependents` with the issues that depend on this one, each\n            carrying its edge type — the shape `dependencies` already carries.\n            Default false, for `include_comments`'s reason.\n          schema:\n            type: boolean\n            default: false\n        - name: brief_deps\n          in: query\n          description: >-\n            Reduce each row in `dependencies` to its identity-and-shape fields\n            (`id`, `title`, `status`, `issue_type`, `priority`,\n            `dependency_type`), dropping `description`, `design`, `notes` and\n            `acceptance_criteria`. Default false, so the payload is unchanged\n            for a client that does not ask.\n          schema:\n            type: boolean\n            default: false\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: The issue.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/IssueDetails'\n        '400':\n          $ref: '#/components/responses/InvalidArgument'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n    patch:\n      operationId: updateIssue\n      summary: Edit the fields of one issue\n      description: >-\n        Partial update of one named issue. A plain `PATCH` rather than a custom\n        method, because partial update of one named resource is what `PATCH`\n        already means — exactly as one named resource with no body is what\n        `DELETE` means for `forgetMemory`. The custom-method namespace is left\n        for the operations that are not CRUD.\n\n\n        ## Member presence is the signal\n\n\n        A member PRESENT in `patch` is written; a member ABSENT is left\n        untouched. That is the whole partial-update rule, and it is why an\n        absent member and a member set to its current value are different\n        requests with the same outcome.\n\n\n        Explicit `null` is defined per member. On the four NULLABLE members —\n        `estimated_minutes`, `external_ref`, `due_at`, `defer_until` — it\n        CLEARS the value, because `null` is the only wire spelling a clear has.\n        On every other member it is a `400` naming the member, so a null is\n        never quietly recorded as an empty string.\n\n\n        An EMPTY `patch` object is a `400`: a write that writes nothing is a\n        client bug, the same judgement `issues:batchCreate` makes about an\n        empty `items`.\n\n\n        `labels` is COMPLETE REPLACEMENT. It is the only shape whose result the\n        client already knows without a read-back; an additive\n        `labels_add`/`labels_remove` pair can join the vocabulary later without\n        disturbing it.\n\n\n        `notes` and `append_notes` are mutually exclusive; sending both is a\n        `400`.\n\n\n        ## Guards, forces and the conflicts they answer\n\n\n        `expected_version`, `expected_status` and `expected_assignee` are\n        compare-and-set preconditions, checked before the patch. A miss refuses\n        the WHOLE request with `409 precondition_failed` and writes nothing, so\n        recovery is to re-read and recompose rather than to retry the same body.\n        They are `ApplyUpdateItem`'s three members with `ApplyUpdateItem`'s\n        contract; `expected_version`'s token is the `revision` this operation\n        answers with.\n\n\n        `force_close_policy` and `force_assignee_transfer` bypass exactly one\n        refusal each and nothing else. Neither bypasses validation, the\n        preconditions above, or the other's guard.\n\n\n        Three members carry policy, which is where this operation's `409`s come\n        from. `status` crossing into the workspace's done category answers to\n        close policy (`not_closable`). `assignee` transferring away from a live\n        foreign in-progress owner answers to the assignee fence\n        (`already_claimed`). `parent_id` is a graph edit and answers to the\n        graph (`dependency_cycle`, `dependency_exists`). Each is the SAME\n        refusal `issues:batchApply` and `dependencies:add` already publish, not\n        a second vocabulary.\n\n\n        ## What this operation deliberately cannot edit\n\n\n        `owner` — additive later, and there is no argument against it any more:\n        the one it had was `assignee`'s, which this operation now publishes.\n        Nothing has asked for it.\n\n\n        `persistence` — a plane move is an atomic aggregate migration rather\n        than a field write, and moving a row between planes mid-patch is a\n        different act from editing it.\n\n\n        `closed_by_session` and `close_reason` — written under first-close-wins\n        by `{id}:close`, which a patch write would bypass. A `status` that\n        crosses into the done category still answers to close POLICY here; it\n        does not acquire the close's semantics, which is why `{id}:close`\n        remains the operation to reach for when what you mean is \"close this\".\n\n\n        `created_at`, `created_by` — `POST /v0/beads/issues` withholds them for\n        the same reason: a caller-chosen creation time makes the row disagree\n        with the journal entry that recorded it, and re-dating history is what an\n        import is for.\n\n\n        `spec_id` and `await_id` — workflow plumbing this surface publishes\n        nowhere yet.\n\n\n        ## Planes\n\n\n        The id resolves across BOTH planes, as the close and reopen do. An\n        update whose target is a wisp lands on the unversioned plane and\n        records no durable history entry.\n\n\n        ## Hooks and auto-commit\n\n\n        Hooks do not fire and the per-command auto-commit machinery does not\n        run, exactly as for `POST /v0/beads/issues/{id}:claim`. The only\n        durable effect is the single storage commit the role makes in its own\n        transaction.\n      parameters:\n        - $ref: '#/components/parameters/IssueID'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/UpdateIssueRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            The update was applied. A same-value patch is a 200 with\n            `changed: false`, not an error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/UpdateIssueResponse'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, an unparseable or\n            oversized body, an unknown body member at any level, an `actor`\n            that is empty after trimming, longer than 256 bytes, or carrying\n            control characters, an empty `patch`, a member carrying the wrong\n            JSON type, an explicit `null` on a member that is not nullable, a\n            value outside its documented bounds, `notes` together with\n            `append_notes`, `metadata.replace` together with any other metadata\n            edit, `force_assignee_transfer` without `patch.assignee` or beside\n            `expected_assignee`, a `patch.parent_id` naming the issue itself or\n            naming no issue this workspace holds — or a value this workspace's\n            own validation refuses, such as an `issue_type` or `status` outside\n            its configured vocabulary.\n\n\n            A NEW PARENT THAT NAMES NOTHING IS A `400`, NOT A `404`, conforming\n            to `POST /v0/beads/dependencies:add`: an edge describes a relation\n            rather than the resource this request addresses, and the `404` below\n            is reserved for the id in the PATH.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '409':\n          description: >-\n            The request is well-formed and the STATE refuses it, and NOTHING WAS\n            WRITTEN. Five codes, each inherited from the operation that already\n            publishes it rather than minted here, and described in the four\n            paragraphs below — `dependency_cycle` and `dependency_exists` share\n            one, because they are one member reaching the graph.\n\n\n            `precondition_failed` is `expected_version`, `expected_status` or\n            `expected_assignee` missing. `param` names the guard member and the\n            `expected_*` member echoes what the request asked for; the\n            `actual_*` members are absent, because the refusal rolled its\n            transaction back and a read afterwards would describe a row the\n            refusal never saw. Re-read and recompose; never retry the same body.\n\n\n            `not_closable` is close policy refusing a `patch.status` that\n            crosses into the workspace's done category: open children, or a live\n            blocker. `open_children` is attached for the first and withheld for\n            the second, so member presence tells them apart.\n            `force_close_policy` is the bypass.\n\n\n            `already_claimed` is the assignee fence: a `patch.assignee` that\n            transfers work away from a live foreign in-progress owner.\n            `force_assignee_transfer` is the bypass and `expected_assignee` is\n            the compare-and-set that replaces the fence outright. The `assignee`\n            extension member is attached only when the refusing transaction\n            reported the holder — the fence itself refuses without one, so a\n            client must treat it as optional and re-read the row.\n\n\n            `dependency_cycle` and `dependency_exists` are `patch.parent_id`\n            reaching the graph: a move under the issue's own descendant, and a\n            pair that already carries an edge of a different type. Neither has a\n            force bypass.\n\n\n            `dependency_cycle` ARRIVES WITHOUT THE HIERARCHY MEMBERS HERE, and a\n            client must not dispatch on their presence on this operation. The\n            hierarchy refusal — `issue_id`, `blocker_id`, `blocker_is_ancestor`\n            — is raised only for a BLOCKING edge, and the only edge this\n            operation writes is the `parent-child` one `patch.parent_id` names,\n            so the discriminator cannot fire. That is the difference from\n            `POST /v0/beads/issues` and from `POST /v0/beads/dependencies:add`,\n            both of which write blocking edges and do carry it: this operation\n            publishes no member that writes one. What you get here is the plain\n            scheduling cycle, every time.\n          x-bd-codes:\n            [already_claimed, dependency_cycle, dependency_exists,\n             not_closable, precondition_failed]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues/{id}/related:\n    get:\n      operationId: listRelatedIssues\n      summary: List one issue's neighbors in a named direction\n      description: >-\n        THE ISSUES ON THE FAR END of this issue's edges, in the direction the\n        request names, each carrying the type of the edge that led to it. It is\n        the read behind `bd dep list`'s neighbor view, and its elements are the\n        same `IssueWithDependencyMetadata` the `dependencies` and `dependents`\n        members of `GET /v0/beads/issues/{id}` already carry.\n\n\n        IT IS A SUB-RESOURCE OF THE ISSUE, not a member of the dependency\n        collection, and the two answer different things about the same edges.\n        `GET /v0/beads/dependencies` returns the stored edge ROWS — targets\n        spelled exactly as stored, nothing looked up — for many anchors at once;\n        this one is anchored on ONE issue and answers with HYDRATED ISSUES, so\n        an edge whose far end this database holds no row for is not a neighbor\n        at all. `issueops.Relations` and `issueops.EdgeReader` state that split\n        once; nothing is restated here.\n\n\n        THE CONSEQUENCE IS WORTH READING BEFORE COUNTING ANYTHING. A dependency\n        target may be an `external:` reference or an id belonging to another\n        repository, and this database holds no issue for either. Such an edge is\n        left out with no placeholder row and no error, so the length of `items`\n        is this issue's NEIGHBOR count and not its EDGE count, and the two\n        differ by however many of its edges point outside this database. A\n        caller that needs the edges themselves reads\n        `GET /v0/beads/dependencies`; a caller that needs the number reads\n        `GET /v0/beads/dependencies:count`, which counts edges and not\n        neighbors.\n\n\n        AN ID THAT NAMES NEITHER AN ISSUE NOR A WISP IS A 404, and that is the\n        difference from every other graph read on this surface. Those are\n        batched and report a miss per anchor, because failing the call would\n        throw away the answers for the ids that were found; here there is one\n        anchor and no other answer to preserve. It matters because an empty\n        `items` is the COMMON case — most issues have neighbors in only one\n        direction — so a typo answered with an empty list would never surface.\n\n\n        BOTH PLANES ARE READ, on the anchor and on its neighbors. The anchor is\n        resolved against the durable and ephemeral planes together, so a wisp id\n        is a legal anchor; and the neighbors are collected from BOTH dependency\n        tables and hydrated from both issue tables, so a durable issue's `in`\n        neighbors include the wisps that depend on it and a `direction=out`\n        answer includes the wisp targets it depends on. Those are the ROLE's\n        rules, stated at `issueops.Relations`, and this document cites them\n        rather than restating them.\n\n\n        THE ORDER IS PINNED: ascending by the neighbor's id, with the edge type\n        breaking a tie. It is pinned rather than left to the query because the\n        rows come from two dependency tables read in sequence, so their natural\n        order is an artifact of which plane a neighbor happens to live on —\n        stable enough to look deliberate and not stable enough to rely on.\n\n\n        THE ROWS CARRY NO `revision`, on `GET /v0/beads/issues`'s terms. The\n        element here is the pinned Go struct `GET /v0/beads/issues/{id}` carries\n        under `dependencies` and `dependents`, so it publishes an issue's own\n        serialized fields and nothing this operation invents; the\n        optimistic-concurrency token stays on the detail read, which is where a\n        guarded write composes its `expected_version` from. A client holding a\n        neighbor list and wanting a guard reads the rows it actually intends to\n        write, one detail read each.\n\n\n        THERE IS NO `limit` AND NO CURSOR. One issue's neighbors are unbounded\n        here exactly as the `dependencies` member of\n        `GET /v0/beads/issues/{id}` already is, and this operation names ONE\n        anchor, so there is no question to bound instead — which is what the\n        dependency-collection reads bound at 100 `issue_id` values apiece. A\n        limit with no cursor behind it would truncate with no way to fetch the\n        rest.\n\n\n        THERE IS NO `both` DIRECTION, for the reason\n        `GET /v0/beads/dependencies:count` has none: a caller that wants the\n        pair asks twice, and one call answering both would have to say which\n        direction each row came from — which is a second member on an element\n        this document deliberately shares with the detail read.\n      parameters:\n        - $ref: '#/components/parameters/IssueID'\n        - name: direction\n          in: query\n          required: true\n          description: >-\n            Which way this issue's edges are walked. `out` answers the issues it\n            DEPENDS ON — the `dependencies` member of\n            `GET /v0/beads/issues/{id}`. `in` answers the issues that depend on\n            it — that read's `dependents` member.\n\n\n            IT IS REQUIRED AND HAS NO DEFAULT, on\n            `GET /v0/beads/dependencies:count`'s terms and for the reason\n            `issueops.RelationDirection` gives: the two answers have the same\n            shape and the same member names, so a caller handed the inverse graph\n            has nothing to notice. An absent or unrecognized value is a 400\n            `invalid_argument` with `param: \"direction\"`, never a walk in some\n            default direction.\n\n\n            The vocabulary is CLOSED — unlike `type` below — because it is a\n            property of the edge's shape rather than of a workspace's\n            configuration.\n          schema:\n            type: string\n            enum: [out, in]\n        - name: type\n          in: query\n          description: >-\n            Edge types to include. Repeat the parameter. Empty means every\n            type.\n\n\n            `GET /v0/beads/dependencies`'s `type` exactly: the vocabulary is\n            OPEN, so an unrecognized value is not an error and simply matches no\n            edge, while a value no edge could ever carry — empty, or longer than\n            the column — is a 400 `invalid_argument` with `param: \"type\"`.\n\n\n            The filter narrows EDGES, never the anchor. An issue whose every\n            edge it rejects is answered with an empty `items` and not with a\n            404, which is a different fact from an id that names nothing.\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: This issue's neighbors, in ascending neighbor id.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RelatedIssues'\n        '400':\n          $ref: '#/components/responses/InvalidArgument'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues/{id}/comments:\n    post:\n      operationId: addComment\n      summary: Append one comment to an issue's thread\n      description: >-\n        Appends ONE comment to the thread the issue owns, as one atomic\n        mutation — the write behind `bd comment`. It leaves every field of the\n        issue untouched: a comment is not a patch, which is why it is not a\n        member of `PATCH /v0/beads/issues/{id}` and why `issueops.Commenter` is\n        its own role rather than a lifecycle verb.\n\n\n        IT IS A SUB-RESOURCE COLLECTION OF THE ISSUE, and the argument is the\n        one `GET /v0/beads/issues/{id}/related` makes, applied to a write: the\n        row this creates is the SAME pinned `Comment`\n        `GET /v0/beads/issues/{id}?include_comments=true` already carries under\n        `comments`, so the operation that appends one belongs on the resource\n        whose members it publishes. A PLAIN collection `POST` rather than a\n        custom method, for `POST /v0/beads/issues`' reason: creating one member\n        of the collection a path names is what `POST` already means, and a\n        `{id}:addComment` spelling would in addition need the claim route's\n        wildcard contortion for no gain. It collides with nothing — the\n        custom-method dispatcher's pattern is one segment shorter, and this\n        path's literal final segment is what ServeMux matches whole.\n\n\n        THIS COLLECTION PUBLISHES NO `GET`, and the absence is a statement\n        rather than a gap. No role answers a comment PAGE: reading the thread is\n        `GET /v0/beads/issues/{id}?include_comments=true`, which returns the\n        bodies in full, and a paged walk is a second question with a cursor of\n        its own — `issueops.Commenter` says exactly that, and a `GET` here would\n        be this surface inventing the role that does not exist. A `GET` on this\n        path is answered `404`, which is what every method mismatch on this\n        surface gets; `405` is not in the v0 status vocabulary.\n\n\n        ## `author` is caller-asserted, and it is not `actor`\n\n\n        The caller always names the author and the server never infers one, for\n        `POST /v0/beads/issues/{id}:claim`'s reason. IT IS NOT THE AUTHENTICATED\n        PRINCIPAL even where a bearer is required: the token a deployment\n        configures admits a client to the whole surface and names nobody, so it\n        can neither confirm nor contradict the name in `author`. A reader of the\n        thread is reading a claim the writer made about itself.\n\n\n        It is spelled `author` rather than the `actor` every issue mutation here\n        carries because it is a different thing. An `actor` is the principal a\n        mutation is attributed to and is not part of what the mutation wrote;\n        this value IS part of the row, echoed back by every read of the thread,\n        and it is spelled the way the `Comment` element that carries it back\n        spells it. `issueops.AddCommentRequest.Author` states the distinction;\n        this member is that field.\n\n\n        ## Planes\n\n\n        The id resolves across BOTH planes, as `POST\n        /v0/beads/issues/{id}:close` does and unlike\n        `POST /v0/beads/issues/{id}:claim`: A WISP IS A LEGAL TARGET. The\n        comment lands on the ephemeral thread and reads back from it, and only\n        the DURABLE trace is missing — a comment on an ephemeral row records NO\n        history entry, none rather than one, because the wisp tables are\n        dolt-ignored precisely so ephemeral work never ships. A caller\n        reconstructing threads from durable history alone will not see it. An id\n        that names neither plane is a `404` and nothing is written.\n\n\n        ## What this operation does not have\n\n\n        NO CONFLICT CODE AND NO `expected_version`. A thread is append-only and\n        this write touches no field of the issue, so there is no row state for a\n        guard to be stale about and no concurrent comment for this one to\n        collide with. Two callers commenting at once both succeed, in whatever\n        order the database commits them.\n\n\n        NO IDEMPOTENCY KEY EITHER: a retried request appends a SECOND comment,\n        because two identical comments are a legitimate thread and nothing here\n        can tell that pair from a retry. A client that must not double-post\n        reads the thread.\n\n\n        Hooks do not fire and the per-command auto-commit machinery does not\n        run, as for every write on this surface. The only durable effect is the\n        single storage commit the role makes inside its own transaction.\n      parameters:\n        - $ref: '#/components/parameters/IssueID'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/AddCommentRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            The stored comment, with the id and `created_at` the row actually\n            got — the stored value at the column's precision, not the wall clock\n            the call happened at, so it is safe to use directly as a\n            comment-page cursor when one exists. It is the row as STORED, never\n            the request reflected back.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Comment'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, an unparseable or\n            oversized body, an unknown body member, a member carrying the wrong\n            JSON type, an explicit `null` on any member, an `author` that is\n            missing, empty after trimming, longer than 256 bytes or carrying\n            control characters, or a `text` that is missing or blank after\n            trimming. Nothing is written in any of these cases.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues/{id}:claim:\n    post:\n      operationId: claimIssue\n      summary: Claim an issue for an actor\n      description: >-\n        Compare-and-set claim. The caller always names the actor: the server\n        never infers one, because its own identity is meaningless for remote\n        callers.\n\n\n        A re-claim by the CURRENT holder is idempotent — 200 with\n        `already_claimed: true` — matching CLI semantics. A claim held by a\n        different actor is 409 `already_claimed` and carries the holder in the\n        `assignee` extension member, read inside the same transaction; an issue\n        in a non-claimable state is 409 `not_claimable` and carries\n        `issue_status`. Neither needs the client to parse prose.\n      parameters:\n        - $ref: '#/components/parameters/IssueID'\n      security:\n        - bearerToken: []\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ClaimRequest'\n      responses:\n        '200':\n          description: The issue is now held by `actor`.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ClaimResponse'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, an unparseable or\n            oversized body, an unknown body member, or an `actor` that is empty\n            after trimming, longer than 256 bytes, or carrying control\n            characters.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '409':\n          description: >-\n            The issue is held by another actor (`already_claimed`, with the\n            `assignee` extension member) or is not in a claimable state\n            (`not_claimable`, with the `issue_status` extension member).\n          x-bd-codes: [already_claimed, not_claimable]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues/{id}:release:\n    post:\n      operationId: releaseIssue\n      summary: Give back the claim on an issue\n      description: >-\n        The claim's inverse, and what `bd unclaim` spells. It ends the\n        OWNERSHIP and leaves the work open for the next taker: assignee\n        cleared, status the literal `open`, `started_at` cleared, the lease\n        dropped, and `revision` reminted so a concurrent reclaim or close\n        conflicts rather than silently merging.\n\n\n        It is a named lifecycle action rather than a `patch`, for\n        `POST /v0/beads/issues/{id}:close`'s reason: an update spells a release\n        as three fields at a time, which puts the transition's definition in\n        the CALLER, and the lease it drops is the part a patch cannot express\n        at all.\n\n\n        The caller always names the actor, for\n        `POST /v0/beads/issues/{id}:claim`'s reason. HERE IT IS ALSO THE\n        OWNERSHIP FENCE'S SUBJECT: a release carrying neither\n        `expected_assignee` nor `force` succeeds only while `actor` is the\n        current holder. That is not authentication — `actor` is caller-asserted\n        provenance exactly as it is on the claim, and it is NOT the\n        authenticated principal even where a bearer is required: the token a\n        deployment configures admits a client to the whole surface and names\n        nobody, so it can neither confirm nor contradict the name in `actor`.\n        The fence is the same anti-yank guard the claim gets from refusing a\n        foreign holder, pointed the other way.\n\n\n        ## NOT IDEMPOTENT, which is the one thing to read before adopting it\n\n\n        A release over a row that holds NO claim is `409` / `not_releasable`,\n        never a 200. There is no `already_released` member here and there must\n        not be one, because the post-state is ANONYMOUS: \"I released this\n        twice\", \"a reaper beat me to it\" and \"nothing ever claimed it\" leave\n        the identical row — assignee cleared, status open, `started_at` gone —\n        so one 200 would report one answer for three situations that want\n        different things from a caller. `claimIssue` can afford\n        `already_claimed: true` because a claim's post-state NAMES the\n        claimant, and this operation has nothing left on the row to name.\n\n\n        ON `not_releasable`, READ THE ROW. Do NOT treat the code as \"already\n        released\", however ordinary that case is for you — it is the one\n        shortcut this operation's taxonomy makes unsafe, and it is unsafe in\n        the direction that strands work.\n\n\n        The code covers TWO conditions and publishes no member telling them\n        apart (see the `409` below). One of them is \"nothing holds this\". The\n        other is \"the status will not accept a release\", which is true of a row\n        that is CLOSED and equally true of a row parked in a status this\n        workspace configured — and such a row can still be ASSIGNED. A reaper\n        that read this code as success would book a claim as dropped while it\n        is still held, by an agent that is already gone, with nothing left to\n        free it. That is exactly the stranding this operation resolves ids\n        across both planes to prevent, reintroduced through the refusal\n        vocabulary instead of through the id.\n\n\n        So the recovery is a read: the row tells you which of the two you got —\n        an assignee, or none. That is one extra request on an uncommon path,\n        and it is the honest price of one code rather than two. A caller that\n        wants the distinction without the read should say so, and it arrives as\n        an ADDITION, which the `Problem.code` rules already tell clients to\n        tolerate.\n\n\n        ## The two ways to release a claim you do not hold\n\n\n        `expected_assignee` is a compare-and-set on the holder and REPLACES the\n        ownership fence: a caller that can name the current holder has\n        demonstrated the view the fence exists to protect, so `actor` need not\n        be that holder. It cannot release a claim that has since moved, which\n        is what makes it the safer of the two for a supervisor reaping one\n        named agent's abandoned work.\n\n\n        `force` ignores the holder entirely, and ignores NOTHING ELSE. It does\n        not make an unheld row releasable, it does not make a closed one\n        releasable, and it may not accompany `expected_assignee` — the two are\n        answers to the same question and they disagree, so sending both is a\n        400.\n\n\n        ## Planes\n\n\n        The id resolves across BOTH planes, unlike\n        `POST /v0/beads/issues/{id}:claim` and like\n        `POST /v0/beads/issues/{id}:close`. The asymmetry is about which\n        direction strands work: a wisp can hold a claim, so an operation that\n        refused to release one would leave an ephemeral row owned by an agent\n        that is gone with no verb able to free it. A release whose target is a\n        wisp records no durable history entry.\n\n\n        ## Hooks and auto-commit\n\n\n        Hooks do not fire and the per-command auto-commit machinery does not\n        run, exactly as for `POST /v0/beads/issues/{id}:claim`. The only\n        durable effect is the single storage commit the role makes inside its\n        own transaction, which is what a proxied CLI write does today.\n      parameters:\n        - $ref: '#/components/parameters/IssueID'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ReleaseIssueRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            The claim is released. The issue is open and unassigned, and the\n            body carries the row the release produced.\n\n\n            THE SET THIS SUCCEEDS OVER IS WIDER THAN `in_progress`, and a\n            client porting a guard needs to know it before it ports one. The\n            transition is defined over `open` AND `in_progress` alike, so a row\n            already in `open` that STILL HOLDS AN ASSIGNEE — the state a\n            partial write or an out-of-band status edit leaves behind — is\n            released here. A caller whose own release-if-current guarded on\n            `in_progress` did nothing for that row and reported no error; this\n            operation writes it. That is a genuine widening rather than a\n            restatement, and a client mapping \"no error\" to \"I released it\"\n            will now be right about rows it was previously never right about —\n            which is the point, and which is also why the mapping below is\n            written in terms of the CODES rather than in terms of success.\n\n\n            THE CODE-TO-OUTCOME MAPPING a release-if-current client wants,\n            stated here so it is written against a documented fact rather than\n            against a reading of the refusals. Four answers mean \"no release\n            happened, and that is not an error\": `not_releasable`,\n            `precondition_failed`, `already_claimed`, and `404`. A client\n            spelling that as a `(released, err)` pair returns\n            `(false, nil)` for all four.\n\n\n            `(false, nil)` MEANS \"NO RELEASE HAPPENED\". It does not mean the\n            claim is gone, and nothing here licenses that reading — see the\n            `not_releasable` paragraph in the operation description, which is\n            the one place the two readings come apart and the place a reaper\n            gets it wrong. A client that needs \"is this row still held\" asks a\n            READER; this operation answers whether IT released anything.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ReleaseIssueResponse'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, an unparseable or\n            oversized body, an unknown body member, an `actor` refused by the\n            rules `ClaimRequest.actor` states, an `expected_assignee` that is\n            empty after trimming, or `force` sent beside `expected_assignee`.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '409':\n          description: >-\n            THE ORDER THESE ARE DECIDED IN IS PART OF THE ANSWER, because one\n            request can fail several ways at once. Request validation runs\n            before anything is read; then existence (the 404); then the row's\n            STATUS, because a closed issue is not a claim question at all; then\n            whether a claim exists; then the precondition or the fence,\n            whichever the request selected. So a CLOSED row guarded on a stale\n            `expected_assignee` is `not_releasable`, not\n            `precondition_failed` — do not read one refusal as evidence about\n            the checks that never ran.\n\n\n            `not_releasable` is the row refusing to produce a release at all,\n            and it covers two conditions: it holds no claim, or its status is\n            neither `open` nor `in_progress` — a closed issue, or one parked in\n            a status this workspace configured, which plainly can still hold a\n            claim. Neither has a bypass: `force` answers \"may I release someone\n            else's claim\" and has no opinion about whether there is a claim\n            here to release.\n\n\n            IT CARRIES NO MEMBER DISTINGUISHING THOSE TWO, and a client must\n            not look for one. The OBSERVATIONS the two refusals made — the\n            status one saw, the emptiness of the assignee the other saw — travel\n            in their messages, and this surface does not scrape its own prose to\n            manufacture a typed member. If the distinction ever earns one, it\n            arrives as an ADDITION (a new member, or a second code), which the\n            `Problem.code` rules already tell clients to tolerate; going the\n            other way, merging two codes back into one, is the change that\n            could not be made.\n\n\n            SO THE RECOVERY IS A READ, and it is a correctness rule rather than\n            a convenience: one of these two conditions leaves the row STILL\n            ASSIGNED, so a client that read this code as \"already released\"\n            books a claim as dropped while it stands. The operation description\n            says why that direction strands work. Read the row.\n\n\n            `already_claimed` is the ownership fence: an unforced, unguarded\n            release by an actor that is not the holder. It is `updateIssue`'s\n            assignee-fence code unchanged — the same situation, and the same\n            two bypasses, spelled `force` and `expected_assignee` here instead\n            of `force_assignee_transfer` and `expected_assignee` there. As\n            there, the `assignee` extension member is NOT attached: the fence\n            refuses without naming the holder, so a client re-reads the row.\n\n\n            `precondition_failed` is `expected_assignee` missing, INCLUDING\n            against a row that holds no claim — that is a mismatch rather than\n            `not_releasable`, because the caller asked about a specific holder\n            and the answer is that it is not the holder. It carries\n            `expected_assignee` (the value the REQUEST sent) and `param`, and\n            NO `actual_assignee`: the refusal rolled its transaction back, and\n            the role carries the holder it observed in prose only. That is\n            `updateIssue`'s rule for the same guard, unchanged.\n          x-bd-codes: [already_claimed, not_releasable, precondition_failed]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues/{id}:close:\n    post:\n      operationId: closeIssue\n      summary: Close one issue\n      description: >-\n        Closes the issue this path names, moving it to the literal `closed`\n        status including from a configured done status. It is the half of the\n        agent loop — claim, work, close — that this surface did not serve\n        before.\n\n\n        A named lifecycle action rather than a status patch, because the close\n        carries semantics a patch has nowhere to put: the reason and session\n        under first-close-wins, the done-status normalization, and the close\n        POLICY vocabulary below.\n\n\n        THE FIRST CLOSE WINS. A re-close of an already-closed issue is\n        idempotent — 200 with `already_closed: true` — and writes neither\n        `reason` nor `session`, so a replayed close cannot rewrite the record of\n        why the work ended. The stored pair keeps what the first close gave it\n        until a reopen clears both.\n\n\n        ## Close policy\n\n\n        An unforced close is refused with `409` / `not_closable` when the issue\n        has open children (the refusal carries `open_children`, the count the\n        refusing transaction observed) or a live blocker (no such member). The\n        MEMBER'S PRESENCE is the discriminator, so telling the two apart never\n        requires reading `detail`. Both refusals are the ROLE's, so `force`\n        bypasses those two and nothing else — this endpoint cannot skip a guard\n        by forgetting one exists.\n\n\n        ## The guard\n\n\n        `expected_version` is a compare-and-set precondition on the row's\n        revision, checked FIRST — before close policy and before the idempotent\n        re-close. A miss refuses the whole request with\n        `409 precondition_failed` and writes nothing.\n\n\n        POLICY AND PRECONDITION ARE DIFFERENT THINGS, and `force` is the bypass\n        for exactly one of them. A forced close still answers to the guard: the\n        two members say \"close it even though the graph objects\" and \"only if\n        this is still the row I read\", which are unrelated claims.\n\n\n        The token travels back on `revision`, so a read-modify-write chain that\n        ends in a close composes its expectation from the value the previous\n        write answered with — or, where the chain STARTS with a read, from\n        `GET /v0/beads/issues/{id}`'s `revision`, which is the same token and\n        agrees with this one.\n\n\n        ## Planes\n\n\n        The id resolves across BOTH planes, unlike\n        `POST /v0/beads/issues/{id}:claim`. A close whose target is a wisp lands\n        on the unversioned plane and records no durable history entry.\n\n\n        ## Hooks and auto-commit\n\n\n        Hooks do not fire and the per-command auto-commit machinery does not\n        run, exactly as for `POST /v0/beads/issues/{id}:claim`. The only durable\n        effect is the single storage commit the role makes in its own\n        transaction.\n      parameters:\n        - $ref: '#/components/parameters/IssueID'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CloseIssueRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: The issue is closed.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/CloseIssueResponse'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, an unparseable or\n            oversized body, an unknown body member, an `actor` that is empty\n            after trimming, longer than 256 bytes, or carrying control\n            characters, a `reason` or `session` longer than the column holds or\n            carrying control characters — or an `expected_version` that is not\n            an integer.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '409':\n          description: >-\n            Two refusals share this status and are told apart by `code`.\n\n\n            `precondition_failed`: `expected_version` did not match the row's\n            current revision. `param` is `expected_version` and the refusal\n            echoes the value the REQUEST guarded on; there is no `actual_version`\n            beside it, because the transaction that saw the mismatch rolled back\n            and a read afterwards would describe a row the refusal never saw.\n            Re-read and recompose rather than retrying the same body.\n\n\n            `not_closable`: close policy refused an unforced close — the issue\n            has open children, carrying the `open_children` extension member, or\n            a live blocker, which carries none. Resend with `force: true` to\n            bypass both. `force` does NOT bypass the guard above.\n\n\n            Nothing was written in either case.\n          x-bd-codes: [not_closable, precondition_failed]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues/{id}:reopen:\n    post:\n      operationId: reopenIssue\n      summary: Reopen one issue\n      description: >-\n        Moves the literal `closed` status and every configured done status back\n        to `open`. It is `POST /v0/beads/issues/{id}:close`'s mirror, and it\n        completes the lifecycle pair so a recovery flow works end to end over\n        this surface.\n\n\n        A reopen of an issue that is NOT done changes nothing and succeeds —\n        200 with `already_open: true`, the re-claim's and the re-close's answer\n        to the same question. An agent replaying its own recovery should not\n        have to classify an error to learn it already ran.\n\n\n        Reopening CLEARS `close_reason` and `closed_by_session`, because they\n        describe a closure that no longer holds. That is what makes the close's\n        first-close-wins rule survivable: the way to write a new reason is to\n        reopen and close again, not to re-close.\n\n\n        ## Where the reason is recorded\n\n\n        On the `reopened` EVENT this move records, not on a field of the issue,\n        and the response does not carry it. A caller that wants it back reads\n        the issue's events — the same place the actor attribution for the\n        reopen lives. A reopen with no reason still records that entry; it\n        simply carries none.\n\n\n        ## No POLICY conflict to name\n\n\n        There is still no policy refusal here, and the absence is deliberate:\n        close has one — open children, a live blocker — and reopen is the\n        direction that removes an issue from the done category rather than\n        adding it, so there is nothing for a policy to refuse. `not_closable`\n        is not in this operation's vocabulary and never will be.\n\n\n        The one `409` it does document is a PRECONDITION rather than a policy.\n        `expected_version` is a compare-and-set on the row's revision, checked\n        FIRST — before the non-done no-op — and a miss refuses the whole request\n        with `precondition_failed` and writes nothing. It is the caller's own\n        guard rather than a rule of the graph, which is why the operation can\n        carry it while carrying no policy conflict at all.\n\n\n        The token travels back on `revision`, so a reopen-then-re-close recovery\n        composes its next expectation from the value this operation answered\n        with, and a recovery that starts by reading composes its first from\n        `GET /v0/beads/issues/{id}`'s `revision`.\n\n\n        ## Planes\n\n\n        The id resolves across BOTH planes, as the close does. A reopen whose\n        target is a wisp lands on the unversioned plane and records no durable\n        history entry.\n\n\n        ## Hooks and auto-commit\n\n\n        Hooks do not fire and the per-command auto-commit machinery does not\n        run, exactly as for `POST /v0/beads/issues/{id}:claim`. The only\n        durable effect is the single storage commit the role makes in its own\n        transaction.\n      parameters:\n        - $ref: '#/components/parameters/IssueID'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ReopenIssueRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: The issue is open.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ReopenIssueResponse'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, an unparseable or\n            oversized body, an unknown body member, an `actor` that is empty\n            after trimming, longer than 256 bytes, or carrying control\n            characters, a `reason` longer than the column holds or carrying\n            control characters — or an `expected_version` that is not an\n            integer.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '409':\n          description: >-\n            `expected_version` did not match the row's current revision\n            (`precondition_failed`). `param` is `expected_version` and the\n            refusal echoes the value the REQUEST guarded on, with no\n            `actual_version` beside it for the reason\n            `POST /v0/beads/issues/{id}:close` gives. Nothing was written.\n\n\n            This operation's ONLY conflict, and it is a precondition rather than\n            a policy: `not_closable` is not in its vocabulary.\n          x-bd-codes: [precondition_failed]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues/{id}:casMetadata:\n    post:\n      operationId: compareAndSetMetadata\n      summary: Conditionally set one metadata key on an issue\n      description: >-\n        Sets `metadata[key]` if and only if it currently holds `expected`, and\n        reports what it found either way. It is the conditional write every\n        coordination protocol over the metadata plane is built from, and the one\n        operation on this surface a client is expected to call in a LOOP.\n\n\n        THE TRANSITION IS A PAIR. `expected` is the key's value before, `value`\n        is its value after, and OMITTING either member means the key is ABSENT\n        there. So a first-writer-wins acquire omits `expected`, a release omits\n        `value`, and an ordinary hand-off carries both. A member present with\n        the JSON value `null` is a real value and does NOT mean absent: a key\n        stored holding null exists, and the server can tell the two apart.\n\n\n        EQUALITY IS CANONICAL. Two JSON values match when their canonical\n        encodings match, so insignificant whitespace is ignored and object keys\n        compare as a set rather than in the order they were written — a client\n        cannot lose a swap to its own serializer.\n\n\n        NUMBERS COMPARE AS THEIR SOURCE LITERAL, so `1` and `1.0` do not match,\n        and that is a CONSTRAINT ON CLIENTS rather than a precision guarantee.\n        The metadata store decodes JSON numbers through a float and re-emits\n        them, so a number is not always stored as it was sent: `1.0` is stored\n        as `1`, an integer past 2^53 is rounded, and `1e300` is stored as three\n        hundred and one digits. COMPOSE `expected` FROM A PREVIOUS `current`,\n        never from your own spelling of a number — `current` is the value the\n        row HOLDS, so a loop that feeds it back converges while a loop that\n        re-sends a renormalized literal is refused forever. For a coordination\n        token, prefer a STRING: it round-trips byte for byte.\n\n\n        SERIALIZERS OMIT NULLS BY DEFAULT, AND HERE THAT CHANGES THE REQUEST.\n        Omitting `value` is a DELETE, not \"leave the value alone\" — so a client\n        that builds this body from a struct under Jackson's NON_NULL, Pydantic's\n        `exclude_none`, or Go's `omitempty` will send a delete when it meant to\n        write null. Send the members explicitly, and check what your serializer\n        does with a null before you rely on either meaning.\n\n\n        A REFUSED SWAP IS A 200, not a 409. `swapped` is false and `current`\n        carries the value that refused it, which is what lets a client recompute\n        and retry without a second read that could itself go stale. A lost race\n        is the ordinary path here rather than an exceptional one, and a client\n        that treats this operation's non-2xx codes as \"the swap did not happen\"\n        would be wrong in both directions. DISPATCH ON `swapped`.\n\n\n        A SUCCESSFUL SWAP THAT CHANGES NOTHING WRITES NOTHING: when the\n        precondition holds over a value already equal to `value`, `swapped` is\n        true, `current` is that value, and no row and no history entry is\n        touched. `swapped` answers the PRECONDITION; it does not claim a write\n        happened.\n\n\n        `current` IS ALWAYS THE VALUE THE ROW HOLDS, read inside the transaction\n        that decided — on a refusal, on a swap that landed, and on a swap that\n        changed nothing. It is not an echo of what was sent, so where the store\n        renormalized a value on the way in, `current` reports the store's form.\n        A member present holding `null` is a value; an ABSENT `current` member\n        means the key is absent.\n\n\n        Sibling keys survive. The read, the comparison and the write share one\n        transaction, so a concurrent write to a DIFFERENT key of the same issue\n        is preserved rather than clobbered.\n\n\n        The id resolves across both planes, so a swap whose target is a wisp\n        lands on the unversioned plane and records no durable history entry.\n\n\n        An issue whose stored metadata is not a JSON object answers 500 rather\n        than 400 or 404, and that is deliberate: the request was well-formed and\n        the issue exists, so both client-error codes would be something a caller\n        could act on. The row is corrupt and no retry converges.\n      parameters:\n        - $ref: '#/components/parameters/IssueID'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CompareAndSetMetadataRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            The comparison ran. `swapped` says whether the transition applied.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/CompareAndSetMetadataResponse'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, an unparseable or\n            oversized body, an unknown body member, an `actor` that is empty\n            after trimming, longer than 256 bytes, or carrying control\n            characters, or a `key` that is empty or outside the metadata-key\n            syntax. Nothing is written.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues:sweep:\n    post:\n      operationId: sweepIssues\n      summary: Delete closed beads in bulk\n      description: >-\n        Bulk clearance of CLOSED beads from ONE tier: the operation behind\n        `bd purge` (`tier: ephemeral`) and `bd prune` (`tier: durable`). It is\n        one of the two DESTRUCTIVE operations on this surface — the other is\n        `issues:delete` — and nothing it deletes comes back.\n\n\n        A collection-level custom method rather than `DELETE\n        /v0/beads/issues?...`: this describes a SET and acts on it, and a\n        `DELETE` with a filtering query string is the shape where a dropped\n        parameter widens what is erased. Sending a body makes every narrowing\n        term a member the server refuses by name if it does not know it.\n\n\n        ## What it selects, in order\n\n\n        The closed rows of `tier`, then `pattern`, then the two protections —\n        pinned beads are never swept and `protect_referenced` holds back beads\n        another live bead cites. The ORDER is part of the answer: `skipped`\n        counts what the request actually reached, so a pinned bead the pattern\n        excluded is not counted as protected.\n\n\n        ## The safety gate is the ROLE'S, not this handler's\n\n\n        A `durable` sweep with neither `closed_before` nor `pattern` is\n        refused with `400` / `invalid_argument`. That refusal comes from the\n        same library surface `bd prune` calls, not from a check written here,\n        which is what makes this endpoint incapable of erasing every closed\n        bead in a workspace by omission. A caller that really means everything\n        closed sends `pattern: \"*\"`.\n\n\n        ## One transaction\n\n\n        The selection and the deletion share one transaction, so the set the\n        response describes IS the set that was deleted. The cost is that a\n        sweep is all-or-nothing: one large enough to exceed the backend's write\n        timeout fails whole and deletes nothing. Narrow the request rather than\n        expecting progress.\n\n\n        `dry_run: true` answers the same question and changes nothing —\n        including history. Ask it first.\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SweepRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            The sweep ran (or, under `dry_run`, was costed). A request that\n            matched nothing is a 200 with `swept: 0`, not a 404 — an empty set\n            of closed beads is the steady state of a swept workspace.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/SweepResult'\n        '400':\n          description: >-\n            Invalid request: an unparseable or oversized body, an unknown body\n            member, a `tier` outside the enum, a malformed `pattern`, an\n            unparseable `closed_before`, an `actor` that is empty after\n            trimming or carries control characters — or an unfiltered\n            `durable` sweep, which the library surface refuses as a safety\n            invariant.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues:delete:\n    post:\n      operationId: deleteIssues\n      summary: Delete named beads\n      description: >-\n        Erases the beads this request NAMES, and — under `cascade` — everything\n        that depends on them. It is the operation behind `bd delete`, the other\n        DESTRUCTIVE operation on this surface alongside `issues:sweep`, and\n        nothing it deletes comes back.\n\n\n        It is a different operation from `POST /v0/beads/issues:sweep`, not a\n        narrower spelling of it. A sweep DESCRIBES a set and lets the server\n        resolve it; this one is handed ids. That difference is what decides\n        where each one's safety lives: a description can be accidentally too\n        wide, so the sweep carries a require-a-filter refusal, while a list of\n        ids cannot be, so this one's guard is about the GRAPH instead.\n\n\n        A collection-level custom method rather than `DELETE\n        /v0/beads/issues/{id}`: this acts on a SET, its behaviour turns on\n        three flags, and a `DELETE` carrying those in a query string is the\n        shape where a dropped parameter changes what is erased. Sending a body\n        makes every one of them a member the server refuses by name if it does\n        not know it.\n\n\n        ## The three modes\n\n\n        With neither `cascade` nor `force`, a named bead that some bead OUTSIDE\n        the request depends on is REFUSED with `400` / `invalid_argument`, and\n        nothing is deleted. `force` deletes the named beads and leaves those\n        dependents ORPHANED — they keep their rows, lose their edges, and come\n        back in `orphaned`. `cascade` deletes the transitive closure instead,\n        so nothing is left outside it to orphan; a request carrying both\n        behaves as `cascade`.\n\n\n        The refusal is the LIBRARY's, not this handler's — the same surface\n        `bd delete` calls — which is what makes this endpoint incapable of\n        orphaning a workspace's graph by omission.\n\n\n        ## Every id must resolve\n\n\n        An id naming no stored bead is a `404` and NOTHING is deleted, not even\n        the ids beside it that did resolve. There is no prefix matching here:\n        ids are exact, because resolving an ambiguous prefix to a bead and then\n        deleting it is the one place that convenience is not one.\n\n\n        ## The guard, and why it takes one id\n\n\n        `expected_version` is a compare-and-set precondition on the row this\n        request NAMES: the deletion proceeds only if that bead's revision still\n        equals it, and otherwise the request is `409 precondition_failed` and\n        nothing is deleted. It is the guard that matters most on this operation,\n        because being wrong about which bead you are looking at is the one\n        mistake here that cannot be undone.\n\n\n        IT REQUIRES A SINGLE-ID REQUEST, and that is a `400` naming\n        `expected_version` rather than a fudge: one token cannot describe two\n        rows. Duplicates collapse first, so repeating one id is still one bead.\n\n\n        THE ORDER THE REFUSALS HAPPEN IN IS PART OF THE ANSWER. Request shape\n        first, then the existence probe (`404`), then this guard (`409`), then\n        the dependents refusal (`400`). A request that is both a typo and a\n        graph problem reports the typo, which is the one a caller can fix\n        without deciding anything; and a stale guard outranks the dependents\n        refusal because a caller whose view has moved should not be asked to\n        choose `cascade` or `force` over information that has already changed.\n\n\n        NEITHER `cascade` NOR `force` BYPASSES IT. They bypass POLICY. Under\n        `cascade` the guard covers the named bead alone — the closure is\n        resolved inside the deleting transaction, so it may have grown since the\n        caller's read.\n\n\n        ## One transaction\n\n\n        The guard, the deletion and the rewrite of surviving beads' text\n        references all share one transaction, so the set the response describes\n        IS the set that was deleted and no bead is left citing an id that no\n        longer exists. The cost is that a delete is all-or-nothing: one whose\n        neighbourhood is large enough to exceed the backend's write timeout\n        fails whole. Split the request rather than expecting progress.\n\n\n        `dry_run: true` answers the same question and changes nothing —\n        including history, and including the two refusals, which a preview\n        reports exactly where the real request would. Ask it first.\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/DeleteIssuesRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            The beads were deleted (or, under `dry_run`, costed).\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/DeleteIssuesResult'\n        '400':\n          description: >-\n            Invalid request: an unparseable or oversized body, an unknown body\n            member, an empty or over-long `ids`, a blank id, an `actor` that is\n            empty after trimming or carries control characters, an\n            `expected_version` beside more than one distinct id — or a named\n            bead with a dependent the request did not name and neither\n            `cascade` nor `force` to say what to do about it, which the library\n            surface refuses as a safety invariant.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '404':\n          description: >-\n            At least one id named no stored bead, and nothing was deleted.\n            `detail` is this surface's fixed not-found sentence and does NOT\n            name which ids missed: a client able to tell a well-formed miss\n            from a malformed id would be probing the id space. `bd delete`\n            names them, because it is answering the person who typed them.\n          x-bd-codes: [not_found]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '409':\n          description: >-\n            `expected_version` did not match the named bead's current revision\n            (`precondition_failed`), and NOTHING was deleted. `param` is\n            `expected_version` and the refusal echoes the value the REQUEST\n            guarded on, with no `actual_version` beside it for the reason\n            `POST /v0/beads/issues/{id}:close` gives.\n\n\n            It outranks the dependents refusal and is outranked by the `404`:\n            see the operation description.\n          x-bd-codes: [precondition_failed]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues:batchCreate:\n    post:\n      operationId: batchCreateIssues\n      summary: Create many issues as one act\n      description: >-\n        Creates every item in the request, or none of them. There is no partial\n        outcome and no per-item status: the whole request is one transaction, so\n        a client that gets a 4xx knows nothing was written and can fix its\n        payload and resend it unchanged.\n\n\n        That is the OPPOSITE of what a batch close would do, and it is the right\n        default here for one reason: half a created plan cannot be re-sent\n        without duplicating the half that landed, and nothing in the response\n        would say which half that was.\n\n\n        THE SERVER ASSIGNS EVERY ID. There is no `id` member on an item, so this\n        operation can never adopt or overwrite a stored row — `bd import` is the\n        upsert surface and it is not published here. The generated ids come back\n        in `items`, in request order, which is the only place a client can learn\n        them.\n\n\n        A dependency target may name an issue this workspace holds, an\n        `external:` reference, or an id belonging to another repository. A\n        target that is none of those is a `400` and nothing is created — an edge\n        silently dropped from a created issue is a relationship the client has\n        no way to discover is missing.\n\n\n        AN ITEM OF THIS REQUEST CANNOT BE A DEPENDENCY TARGET, and this\n        operation is the narrow fast path rather than the one to reach for when\n        it needs to be. The server assigns every id, an item has no name a later\n        item could spell, and only an id a caller already held could ever have\n        addressed a row — so a plan whose edges point at its own new issues is\n        `POST /v0/beads/issues:batchApply`, where a create item may NAME itself\n        and later items address it by that name.\n\n\n        Hooks do not fire and the per-command auto-commit machinery does not\n        run, exactly as for `POST /v0/beads/issues/{id}:claim`. The only durable\n        effect is the single storage commit the role makes in its own\n        transaction.\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/BatchCreateRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            Every item was created. `items` carries the stored issues in request\n            order.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/BatchCreateResponse'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, an unparseable or\n            oversized body, an unknown body member at any level, an `actor` that\n            is empty after trimming, longer than 256 bytes or carrying control\n            characters, an empty or over-long `items` array, an item the\n            workspace's own validation refuses, or a dependency target that\n            names nothing this workspace holds. Nothing is created in any of\n            these cases.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues:batchApply:\n    post:\n      operationId: applyBatch\n      summary: Apply an ordered, heterogeneous plan as one act\n      description: >-\n        Applies an ORDERED list of creates, updates, closes and dependency edges\n        as ONE transaction, or applies none of them. It is the operation for a\n        PLAN — create these three issues, wire them to each other, close the\n        step that spawned them — which every other write here can only\n        approximate as a sequence of calls with a window between each pair.\n\n\n        A collection-level custom method, spelled the way `issues:sweep` and\n        `issues:delete` are: it acts on a set the request DESCRIBES rather than\n        on one addressable resource, and every one of its narrowing terms is a\n        body member the server refuses by name if it does not know it.\n\n\n        ## Order is the contract\n\n\n        Items apply in declaration order and are NEVER reordered. That is the\n        difference from `POST /v0/beads/dependencies:add`, which applies\n        parent-child edges first so the planned hierarchy is visible before any\n        blocking edge is validated against it. Reordering is not available here\n        because the items are not all edges: \"clear the old blockers, then set\n        the new ones\" is a sequence, and a server that reordered it would apply\n        the clear after the set. What that pass buys there, this operation buys\n        with an END GATE instead — after every item has landed, every scheduling\n        edge the request added is re-validated against the parent-child closure\n        the WHOLE request produced. `skip_per_edge_cycle_check` never drops it.\n\n\n        ## Names, and which way they reach\n\n\n        A `create` item may give itself a `key`. Later items address the row it\n        minted with a `Ref` carrying that key, and the response's `keys` member\n        maps each key to the id it was bound to — the one fact the request\n        cannot carry and every caller needs.\n\n\n        A KEY REACHES BACKWARD ONLY. A ref used to ADDRESS a row — an update's\n        or a close's `target`, either endpoint of an edge — may name a key only\n        if the create item declaring it appears EARLIER in `items`. That is\n        forced by what the items do rather than chosen: an update has to see the\n        row it patches. A key declared LATER is a `400` carrying\n        `declared_later: true`, which is a different diagnosis from a key\n        nothing in the request declares at all — one is an ordering mistake and\n        the other is a typo, and a client fixes them differently.\n\n\n        `create.metadata_refs` IS THE ONE EXCEPTION and may reach forward, or\n        name its own item's key. Every id is minted before any splice is\n        applied, so direction cannot matter there; the backward-only rule exists\n        to make a TARGET ROW exist before an item touches it, which a metadata\n        VALUE does not need. The splice is a SECOND WRITE and says so: the row\n        is created with the metadata the item spelled, and the resolved ids are\n        written after every id exists, so a consumer of the event stream sees a\n        create and then an update rather than one create carrying values nothing\n        could have known yet.\n\n\n        ## All or nothing, and what that does to a precondition\n\n\n        A non-2xx means NOTHING WAS WRITTEN — no id was minted, no edge landed,\n        no row was closed — so a client fixes its payload and resends it\n        unchanged. There is no per-item status because there is no outcome but\n        the request's: an item whose target is a key an earlier item failed to\n        create has no outcome that could be reported.\n\n\n        THAT IS WHY A PRECONDITION MISS IS A `409` HERE and not an answer.\n        `POST /v0/beads/issues/{id}:casMetadata` reports a lost compare-and-set\n        as a 200, because a retry loop is its designed caller and a miss is the\n        ordinary path. Here the guarded item is one step of a graph the caller\n        meant to land as a unit, so committing the rest would leave a shape\n        nobody asked for. `update.expected_version`, `update.expected_status`,\n        `update.expected_assignee` and `close.expected_version` therefore refuse\n        the WHOLE request with `precondition_failed`, and the problem names the\n        offending item.\n\n\n        Those guards evaluate AS-MODIFIED: against the row as this request has\n        already changed it at that item's position, not against the row as it\n        was when the request began. An item guarding on what an earlier item of\n        the same request just wrote is asking a coherent question and gets a\n        coherent answer. `expected_version` is the exception and it is a `400`:\n        the token is server-minted and rewritten by every write, so mid-request\n        there is no value a caller COULD send, and guarding on a row an earlier\n        item already touched is refused before anything is written rather than\n        answered with a mismatch the caller would go looking for a concurrent\n        writer to explain.\n\n\n        ## What lands, and what it costs\n\n\n        `items` accepts at most 100 entries. The cap bounds how long one request\n        may hold a write transaction — it is not a statement about batch\n        semantics, and it is the bound the sibling batch operations already run\n        under. Split a larger plan; each request is atomic on its own, but note\n        that splitting it changes what the end gate can see, since the gate runs\n        over one request at a time.\n\n\n        ONE HISTORY ENTRY IS RECORDED FOR THE WHOLE REQUEST, attributed to\n        `actor`, and none at all when nothing durable landed — a request made\n        entirely of ephemeral items writes only to unversioned tables.\n        `provenance` labels that entry: it changes how the entry READS, never\n        whether one is recorded, and an empty one composes a default naming how\n        many items of each kind landed rather than every id, since an entry\n        listing a hundred is the diff written twice.\n\n\n        EPHEMERALITY IS PER ITEM, exactly as it is for\n        `POST /v0/beads/issues:batchCreate`: one request may create durable\n        issues and ephemeral ones together. The two planes hold their edges in\n        different tables, so a `dep_add` BETWEEN two rows this request creates\n        on opposite planes is refused with everything else the request asked\n        for.\n\n\n        IT IS NOT IDEMPOTENT AND CARRIES NO IDEMPOTENCY KEY. Replaying a request\n        applies it again — the creates mint new ids, the edges are idempotent,\n        the closes are no-ops. A caller that needs a replay record makes it an\n        ITEM of the batch, so the record lands or rolls back with the work it\n        describes; a key on the request would be a second, weaker mechanism for\n        the same thing.\n\n\n        Hooks do not fire and the per-command auto-commit machinery does not\n        run, exactly as for `POST /v0/beads/issues/{id}:claim`. The only durable\n        effect is the single storage commit the role makes in its own\n        transaction.\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ApplyBatchRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            Every item applied and the transaction committed. `items` carries\n            one entry per requested item in request order, and `keys` maps every\n            key a create item named to the id it was bound to.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApplyBatchResponse'\n        '400':\n          description: >-\n            Invalid request, and NOTHING WAS WRITTEN. The vocabulary is wide\n            because this body is: an unknown query parameter, an unparseable or\n            oversized body, an unknown body member at ANY of its levels, an\n            `actor` that is empty after trimming, longer than 256 bytes or\n            carrying control characters, an empty or over-long `items` array, an\n            item whose `kind` is outside the enum, an item carrying no payload\n            or a payload its `kind` does not name, a `create` with no `title`, a\n            duplicate `key`, a `Ref` with neither `key` nor `id` or with both, a\n            `dep_add` with an unstorable `type` or pointing a row at ITSELF, a\n            `waits-for` gate outside its two values, an item a workspace's own\n            validation refuses, and an edge endpoint that names nothing this\n            database can see. An explicit `create.id` that already names a\n            stored row is NOT here: it is a 409 `already_exists`, because that\n            body is well-formed and only the state refuses it.\n\n\n            TWO OF THOSE ARE WORTH SPELLING OUT. A ref naming a key declared by\n            a LATER item carries `declared_later: true`; a ref naming a key no\n            item declares carries `declared_later: false`, and member PRESENCE\n            is not the discriminator here — the member is emitted in both\n            polarities, because the two refusals are told apart by its VALUE and\n            a client that read an absent member as false would misread every\n            other 400 on this operation as an ordering mistake.\n\n\n            An `expected_version` on a row an earlier item of the same request\n            already wrote is the other: it is a `400` rather than the `409` a\n            real mismatch earns, because it is a request-SHAPE refusal made\n            before anything is read. No token the caller could have sent would\n            have matched.\n\n\n            An explicit `create.id` that is already taken is a `400` naming that\n            member for `POST /v0/beads/dependencies:add`'s reason: the refusal is\n            about the request BODY, and there is no id in the path to have\n            missed. The same applies to an edge endpoint this database can see\n            the absence of.\n\n\n            `param` names the offending member in the `items[i].kind.member`\n            spelling, so a client learns WHICH item and WHICH member without\n            parsing prose.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '404':\n          description: >-\n            A `target` of an `update` or a `close` named no stored row, and\n            nothing was written.\n\n\n            It is a `404` rather than a `400` on `POST /v0/beads/issues:delete`'s\n            precedent: those refs NAME rows this request acts on, exactly as\n            that operation's `ids` do, so a miss is a resource this request\n            failed to address. An EDGE endpoint is the other case and stays a\n            `400`, conforming to `POST /v0/beads/dependencies:add` — an edge\n            describes a relation rather than acting on a row, and its target may\n            legitimately be an `external:` reference or an id in another\n            repository.\n\n\n            `detail` is this surface's fixed not-found sentence and does NOT say\n            which ref missed, for the reason `issues:delete` gives.\n          x-bd-codes: [not_found]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '409':\n          description: >-\n            The request is well-formed and the STATE refuses it, and NOTHING WAS\n            WRITTEN. Six codes, and every one of them is a statement a caller\n            could not have made without reading state it does not hold. Five\n            paragraphs cover them: `dependency_cycle` and `dependency_exists`\n            share one, because they are the graph's own pair.\n\n\n            `precondition_failed` is a guard that missed:\n            `update.expected_version`, `update.expected_status`,\n            `update.expected_assignee` or `close.expected_version` evaluated\n            as-modified at its item's position. `param` names the guard member\n            and `item_index`, `item_kind`, `item_key` and `item_issue_id` name\n            the item, all read from the refusing transaction rather than parsed\n            out of prose. The `expected_*` members echo what the request asked\n            for; the `actual_*` members are absent on this operation, because\n            the all-or-nothing rollback means a read after the fact would report\n            a row the refusal never saw and this role's refusals do not carry\n            the observed value. A client recovers by re-reading and recomposing,\n            never by retrying the same body.\n\n\n            `not_closable` is close policy refusing a `close` item, or an\n            `update` whose `patch.status` crosses into the workspace's done\n            category: open children, or a live blocker. `close.force` and\n            `update.force_close_policy` are the per-item bypasses, and a forced\n            item never earns this.\n\n\n            `already_claimed` is the assignee fence: an `update` whose\n            `patch.assignee` transfers work away from a live foreign in-progress\n            owner. `update.force_assignee_transfer` is the bypass, and\n            `update.expected_assignee` is the compare-and-set that replaces the\n            fence outright.\n\n\n            `dependency_cycle` and `dependency_exists` are the graph's own two\n            refusals, spelled exactly as `POST /v0/beads/dependencies:add`\n            spells them, including the hierarchy discriminator: the hierarchy\n            case — and only it — additionally carries `issue_id`, `blocker_id`\n            and `blocker_is_ancestor`. They may come from the per-edge probe or\n            from the END GATE, which is the one place an edge that is legal on\n            its own and illegal in the graph this request built is caught.\n            Neither has a force bypass.\n\n\n            `already_exists` is a `create` item whose EXPLICIT `id` already\n            names a stored row. `param` is `items[i].create.id` and the item\n            members name which item carried it. There is no force bypass and\n            there is no upsert here: every create item refuses an occupied id,\n            and adopting a row that already exists is `update` — reference it by\n            `{\"id\": …}` instead. It is a 409 rather than a 400 because the body\n            is well-formed and stays well-formed: the identical request\n            succeeded before the id was taken, so recovery is to look at the\n            state (adopt the row, choose another id, or stop) rather than to fix\n            a malformed request.\n          x-bd-codes:\n            [already_claimed, already_exists, dependency_cycle,\n             dependency_exists, not_closable, precondition_failed]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues:batchClose:\n    post:\n      operationId: batchCloseIssues\n      summary: Close many issues as one act\n      description: >-\n        Closes every item it can and commits them together. It is the write side\n        of `bd close a b c`, and it is the operation `POST /v0/beads/issues:batchCreate`\n        already names as its own opposite.\n\n\n        THE REQUEST IS THE TRANSACTION BOUNDARY, which is the whole reason this\n        is one operation rather than a loop over\n        `POST /v0/beads/issues/{id}:close`: closing N issues one call at a time\n        is N transactions and N history entries, and a caller that wants them to\n        land together has no way to say so.\n\n\n        ## It is NOT all-or-nothing, and that is the point\n\n\n        An id this batch refuses is SKIPPED and the survivors commit. An agent\n        that finishes four of five steps and mistypes the fifth keeps the four;\n        making the batch atomic in the other sense would turn a typo into a\n        rollback of finished work.\n\n\n        SO THE ANSWER IS A 200 CARRYING PER-ITEM OUTCOMES, even when items\n        refused. `outcomes` has exactly one entry per requested item, in REQUEST\n        ORDER, so a client walks it against its own argument list without\n        matching ids back up. A NON-2xx from this operation means the batch\n        NEVER RAN — a refused body, or an infrastructure failure — and never\n        that some items landed.\n\n\n        That divides the refusal vocabulary in two, and the division is the\n        contract: a refusal OF THE REQUEST is a problem document, and a refusal\n        OF AN ITEM is a member of that item's outcome. `code` on an outcome is\n        the same vocabulary `Problem.code` publishes, restricted to\n        `not_found` and `not_closable`, so a client classifies an item exactly\n        as it classifies a request.\n\n\n        WHAT IS ATOMIC IS EVERYTHING THAT LANDS: one transaction, at most one\n        history entry, and none at all when nothing landed. LANDED MEANS\n        CHANGED — an idempotent re-close is a per-item success that persisted\n        nothing, so a batch of them lands nothing and records no history entry,\n        exactly as a batch of typos does.\n\n\n        ## Duplicates and planes\n\n\n        A DUPLICATED id is not a request error: `bd close a b a` is a plausible\n        typo, not a failure. Items are closed in the order given, so the second\n        occurrence finds what the first one did and reports an idempotent\n        re-close at ITS OWN index — a success with `already_closed: true`. The\n        reason on the row is the first occurrence's, because the second mutated\n        nothing and so wrote nothing.\n\n\n        A WISP ID IS AN ADMISSIBLE ITEM, resolved across both planes exactly as\n        `POST /v0/beads/issues/{id}:close` resolves one. What it does not do is\n        reach the durable history entry: the entry a mixed batch records is\n        composed from its DURABLE landings alone.\n\n\n        ## What this operation does not publish\n\n\n        NO COMPOSED CLAIM. The role can claim the next ready issue in the same\n        transaction once the closes land, and that member is deliberately not\n        published here — expressing it would require a second, BODY-shaped\n        spelling of the ready-filter vocabulary that\n        `GET /v0/beads/ready` and `POST /v0/beads/issues:claimNext` both express\n        as query parameters, and two spellings of one predicate eventually\n        disagree. A client that wants both sends the two requests; what it loses\n        is the single transaction, which is a real loss and is named here rather\n        than papered over. It is additive later, once there is one shape for a\n        ready filter in a body.\n\n\n        NO PER-ITEM PRECONDITION. There is no counterpart here to a\n        compare-and-set guard, matching the role, so `force` is a question with\n        only two answers: it bypasses blocker and open-child close policy for\n        every item, and it never bypasses validation or existence.\n\n\n        ## Hooks and auto-commit\n\n\n        Hooks do not fire and the per-command auto-commit machinery does not\n        run, exactly as for `POST /v0/beads/issues/{id}:close`.\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/BatchCloseRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            The batch ran. Every requested item has an outcome, including the\n            ones that refused.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/BatchCloseResponse'\n        '400':\n          description: >-\n            The batch NEVER RAN. An unknown query parameter, an unparseable or\n            oversized body, an unknown body or item member, an `actor` refused\n            by the rules `ClaimRequest.actor` states, an `items` that is absent,\n            empty or longer than 100, an item that is not an object or carries\n            no `id`, or a `reason`/`session` longer than the column holds.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/issues:claimNext:\n    post:\n      operationId: claimNextIssue\n      summary: Claim the next ready issue\n      description: >-\n        Takes ONE ready issue and hands it back claimed, in a single\n        transaction. It is `bd ready --claim`, and it is the operation this\n        surface's polling clients have been composing by hand out of\n        `GET /v0/beads/ready` and `POST /v0/beads/issues/{id}:claim`.\n\n\n        THAT COMPOSITION IS A RACE, and retiring it is why this exists. Between\n        the listing that offered a row and the claim that asked for it, another\n        agent claims it — so the second agent gets `409 already_claimed` for a\n        row it was correctly offered, and a fleet polling one queue spends its\n        requests losing races rather than doing work. Here the ready predicate,\n        the compare-and-set that wins the row, and the hydration of the row that\n        was won are ONE transaction, so the row cannot move between being chosen\n        and being reported.\n\n\n        THE CALLER NAMES A QUESTION, NOT A ROW, which is what makes this a\n        different operation rather than a mode of the claim. There is no id in\n        the path and none in the body: selection is part of the contract.\n\n\n        ## Nothing eligible is a 200\n\n\n        An empty ready front is the steady state of a drained queue, not a\n        failure, so a request that finds nothing answers `200` with `claimed`\n        ABSENT. Its absence is the whole signal and there is no second member\n        beside it: a polling agent branches on presence and sleeps. Nothing is\n        written and no history entry is recorded.\n\n\n        This is the one place this operation deliberately differs from\n        `POST /v0/beads/issues/{id}:claim`, which 404s an id that names nothing\n        — that operation was asked about a ROW, and this one was asked a\n        question whose honest answer can be \"none\".\n\n\n        ## The filters are the listing's, exactly\n\n\n        Every parameter below is `GET /v0/beads/ready`'s, means what it means\n        there, and is decoded by the same function — including the default type\n        exclusions and the way `type` drops them. That is not tidiness: a claim\n        that answered a different question than the listing shows would hand an\n        agent work the listing never offered it, and two predicates that are\n        allowed to differ eventually do.\n\n\n        THERE IS NO `limit`, and sending one is a 400 rather than a silently\n        dropped parameter. A claim delivers exactly the one row it wins no\n        matter how large the pool it scanned, and the scan itself must stay\n        UNBOUNDED: the implementation walks the ready order and continues past\n        rows a racing agent already took, so a bounded window would report\n        \"nothing to claim\" whenever that window happened to be unclaimable while\n        plenty of other ready work remained.\n\n\n        `sort` IS published, unlike on `GET /v0/beads/ready:count`, because\n        order decides WHICH row a claim wins where it cannot change a\n        cardinality.\n\n\n        ## Leases, and the one case that has none\n\n\n        A DURABLE win grants exactly one lease on the row it won — the handle\n        heartbeats extend, and the row lease-expiry recovery walks once nothing\n        extends it.\n\n\n        An EPHEMERAL win carries NO LEASE, and that is the sharper consequence\n        because it has no expiry to wait out. Ephemeral rows are outside the\n        ready set by default; `include_ephemeral` pulls them in for the claim\n        exactly as it does for the listing, and such a row IS claimable —\n        claiming it moves the ephemeral row itself rather than promoting it.\n        But heartbeats refuse an ephemeral row and lease-expiry recovery only\n        walks leased durable ones, so nothing reclaims it if its claimant dies:\n        it stays in progress under a gone actor until something releases or\n        finishes it. A caller handing ephemeral work to agents it does not\n        supervise owns that recovery itself. An ephemeral win records no durable\n        history entry either, so a caller reconstructing who took what from\n        history alone will not see it — read the row.\n\n\n        ## Hooks and auto-commit\n\n\n        Hooks do not fire and the per-command auto-commit machinery does not\n        run, exactly as for `POST /v0/beads/issues/{id}:claim`.\n      parameters:\n        - name: assignee\n          in: query\n          description: Only issues assigned to this actor.\n          schema:\n            type: string\n        - name: unassigned\n          in: query\n          description: Only issues with no assignee.\n          schema:\n            type: boolean\n        - name: type\n          in: query\n          description: >-\n            Issue type. The only normalization is shorthand ALIAS expansion,\n            exactly what `bd ready --type` does: `mr` → `merge-request`,\n            `feat` → `feature`, `mol` → `molecule`, `enhancement` → `feature`,\n            `dec`/`adr` → `decision`. Every other value is used as written —\n            there is NO plural folding, so `bugs` is not `bug`.\n\n\n            An unrecognized type is not an error here: the type vocabulary is\n            workspace-configurable, and `bd ready` does not validate it either,\n            so it simply matches nothing and `items` comes back empty. (The\n            list operation differs — `bd list` DOES validate the type, so\n            `GET /v0/beads/issues?type=bugs` is a 400.)\n\n\n            When set, `exclude_type` is ignored, and so are the default type\n            exclusions described above.\n          schema:\n            type: string\n        - name: exclude_type\n          in: query\n          description: >-\n            Issue types to exclude. Repeat the parameter, or pass a\n            comma-separated list. Ignored when `type` is set.\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: label\n          in: query\n          description: Labels that must ALL be present (AND).\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: label_any\n          in: query\n          description: Labels of which at least one must be present (OR).\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: exclude_label\n          in: query\n          description: Labels that must not be present.\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: label_pattern\n          in: query\n          description: Glob matched against labels.\n          schema:\n            type: string\n        - name: label_regex\n          in: query\n          description: Regular expression matched against labels.\n          schema:\n            type: string\n        - name: priority\n          in: query\n          description: Exact priority (0 is a real value, not \"unset\").\n          schema:\n            type: integer\n        - name: parent\n          in: query\n          description: Restrict to recursive descendants of this issue.\n          schema:\n            type: string\n        - name: metadata_field\n          in: query\n          description: >-\n            Top-level metadata equality filter as `key=value`, split on the\n            first `=`. Repeatable. An invalid key is a 400.\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: has_metadata_key\n          in: query\n          description: Only issues carrying this top-level metadata key.\n          schema:\n            type: string\n        - name: include_ephemeral\n          in: query\n          description: Include ephemeral (non-synced) rows.\n          schema:\n            type: boolean\n            default: false\n        - name: include_deferred\n          in: query\n          description: Include issues whose `defer_until` is still in the future.\n          schema:\n            type: boolean\n            default: false\n        - name: sort\n          in: query\n          description: >-\n            Ready-work ordering. `priority` is priority-first; `hybrid` orders\n            recent issues by priority and older ones by age; `oldest` is\n            creation order. An unrecognized value is a 400.\n\n\n            The default is the one `bd ready --sort` registers, so a client\n            swapping `bd ready --json` for this operation gets the same items in\n            the same order. The storage layer treats an EMPTY policy as\n            `hybrid`, but that fallback is unreachable from the CLI and is NOT\n            this parameter's default: `hybrid` demotes older high-priority work,\n            so defaulting to it would change the item SET as soon as `limit`\n            truncates — silently, and only for the clients this API exists to\n            migrate.\n          schema:\n            type: string\n            enum: [hybrid, priority, oldest]\n            default: priority\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ClaimNextRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            The claim ran. `claimed` carries the row it won, or is ABSENT\n            because nothing was eligible.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ClaimNextResponse'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, a malformed filter\n            value, a `limit` (which this operation refuses rather than drops),\n            an unparseable or oversized body, an unknown body member, or an\n            `actor` refused by the rules `ClaimRequest.actor` states.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/config:\n    get:\n      operationId: listSettings\n      summary: List the workspace's stored settings\n      description: >-\n        The SETTINGS stored in the workspace database, which is the plane\n        `bd config list` reads. It is not the effective configuration: values\n        that reach a running `bd` from `config.yaml`, from environment\n        variables or from git config are absent and cannot be here, because\n        they are files and variables on the CLIENT's machine and this server\n        answers for the database. `bd config show` is the multi-source view and\n        has no HTTP operation for the same reason.\n\n\n        IT IS NOT EVERY ROW OF THAT TABLE. Keys under `kv.` are omitted — the\n        generic `bd kv` namespace and the `bd remember` memories nested under\n        it. Those rows are USER DATA that rides in the settings table because\n        there is one table, not because they are settings, and enumerating them\n        here published a workspace's memories, key and value, to anything that\n        could reach this port. `bd config list` omits them too: the exclusion is\n        in the shared role both doors call, so the two cannot drift.\n\n        `getSetting` still answers a `kv.` key NAMED EXACTLY, and this\n        operation is where it stopped being discoverable.\n\n\n        SETTINGS WHOSE KEY MARKS THEM AS CREDENTIAL-BEARING ARE WITHHELD. Their\n        entry is present with `redacted: true` and no `value`, so a client can\n        see that the key is configured without the surface handing a secret to\n        every process that can reach the port. A CONFIGURED BEARER DOES NOT\n        NARROW IT: authentication on this surface is a deployment posture and the\n        credential, where there is one, is a single shared token that names\n        nobody — it decides WHO may call, never what a caller who is let in may\n        read — and there is no TLS either. The rule is the KEY's and is the same\n        for every caller. See `Setting`, and `PUT` on this key's own path for the\n        write half, which withholds the value in its response for the same\n        reason and permits the write.\n\n\n        The envelope is the paginated one and `has_more` is always false today:\n        settings are a keyed namespace a workspace holds tens of, not a\n        collection to scan, so the whole plane is returned in one page. The\n        envelope is used anyway because entries are ordered by key, which makes\n        a keyset cursor expressible later without a breaking change.\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: The stored settings.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/SettingsPage'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/config/{key}:\n    get:\n      operationId: getSetting\n      summary: Get one stored setting\n      description: >-\n        The value stored for one key, used verbatim: there is no namespace\n        completion, no case folding and no dash/underscore equivalence.\n\n\n        THERE IS NO 404 ON THIS OPERATION, deliberately. A key nothing stored\n        and a key stored as the empty string are the same answer here — 200\n        with `value` absent — because the storage seam behind it cannot tell\n        them apart and neither can `bd config get`, which prints \"(not set)\"\n        for both. Answering 404 for one of them would publish a distinction\n        this server would have to invent.\n      parameters:\n        - $ref: '#/components/parameters/SettingKey'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: The setting.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Setting'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, or a `key` that is\n            empty after trimming or carries a control character.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n    put:\n      operationId: setSetting\n      summary: Store one setting\n      description: >-\n        Stores one setting, REPLACING any value already there — the write\n        `bd config set` spells, on the durable settings plane this document's\n        reads already publish.\n\n\n        `PUT` RATHER THAN A COLLECTION `POST`, and the method is the whole\n        argument: the resource has a canonical URI, the caller names it, and the\n        request carries the value that becomes its whole state. That is what\n        `PUT` already means, and it is idempotent in the strict sense — the same\n        request sent twice leaves the same row.\n        `POST /v0/beads/memories` is the operation this is NOT: that one posts to\n        the COLLECTION because its key may be derived from the content, so the\n        caller cannot always name the resource it is creating. Here the caller\n        always can.\n\n\n        THE KEY IS THE PATH'S and appears nowhere in the body: one anchor, one\n        spelling, and no question about what to do when two disagree. It is used\n        verbatim — no namespace completion, no case folding, no dash/underscore\n        equivalence — and stored UNTRIMMED, because a key differing from another\n        only by surrounding space is a key a reader will never match and\n        trimming it would produce a write the caller cannot find again.\n\n\n        ## What this plane refuses, and what it does not\n\n\n        `issue_prefix` — in either spelling — is a `400` and NOTHING is written.\n        The prefix is owned by `bd init --prefix`, `bd bootstrap` and\n        `bd rename-prefix`, each of which does work this plane cannot: rewriting\n        existing ids, or seeding a workspace that has none. Storing a new one\n        here would leave the beads created before the write and the beads created\n        after it disagreeing about their own namespace with nothing to reconcile\n        them.\n\n\n        A `status.custom` value that does not PARSE is a `400` and nothing is\n        written. That key is not merely stored — it is projected into the\n        `custom_statuses` table, which reads consult first, IN THE SAME\n        TRANSACTION as the row. `types.custom` is projected into `custom_types`\n        the same way. A row without its table is a value that has been stored and\n        has no effect for as long as the table holds something else, so the write\n        and the projection are one durable act or neither happens.\n\n\n        A KEY WHOSE NAME MARKS IT CREDENTIAL-BEARING IS WRITABLE, and that is\n        deliberate rather than an oversight in the redaction posture. Redaction\n        is a rule about DISCLOSURE: it withholds a value from a reader because a\n        bearer on this surface is shared and surface-wide and cannot decide that\n        one caller may read a credential and another may not. A writer supplies\n        the value, so refusing the write protects nothing that is not already in\n        the caller's hand — and it would leave a workspace whose credentials can\n        be seen to EXIST and never configured. The role refuses no such key\n        either; `bd config set`'s own secret guard is about writing a credential\n        into a git-tracked `config.yaml`, which is a different plane with a\n        different hazard, and it returns clean for every key this operation\n        reaches.\n\n\n        THE RESPONSE THEREFORE WITHHOLDS IT ANYWAY. The body is a `Setting`\n        projected by the same rule `GET /v0/beads/config/{key}` projects with, so\n        a `redacted: true` key comes back with no `value` — the response to a\n        write is byte-identical to the read that follows it. It costs the caller\n        nothing: the role promises the stored value equals the value sent for\n        every key this plane accepts, so the echo carries no information the\n        caller does not already hold, and publishing it would make one schema say\n        two different things about `redacted`.\n\n\n        A key belonging to another SOURCE is not refused here and is worth\n        knowing about: `export.*`, `dolt.*`, `federation.*`, `storage-class.*`\n        and the rest of the yaml-only list live in `config.yaml`, and\n        `beads.role` lives in git config. Written through this operation they\n        land a row no reader consults. The role does not police that routing and\n        neither can this server — three of the five sources `bd config show`\n        reads are files on the CLIENT's machine, which is the same reason that\n        command has no operation here.\n\n\n        Hooks do not fire, as for every write on this surface — and on this plane\n        there are none to fire in any case: the workspace hook vocabulary is\n        `on_create`/`on_update`/`on_close`, each of which hands a script an\n        ISSUE, and a settings write has none to name.\n      parameters:\n        - $ref: '#/components/parameters/SettingKey'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SetSettingRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            The setting as it now stands, projected exactly as the `GET` beside\n            it projects it — including the redaction, which is decided on the KEY\n            and never on the caller.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Setting'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, an unparseable or\n            oversized body, an unknown body member, a `value` that is missing,\n            is not a string or is longer than the 65535-byte column, a `key`\n            that is empty after trimming, carries a\n            control character or is longer than the 255-character column — or a\n            value this workspace's own validation refuses, which is `issue_prefix`\n            in either spelling and a `status.custom` that does not parse. NOTHING\n            IS WRITTEN in any of these cases.\n\n\n            The role's two refusals carry no `param`, unlike the transport's\n            above: telling them apart would take a second copy of the protected-key\n            vocabulary on this side of the seam, and `detail` carries the role's\n            own sentence, which names what to send instead.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n    delete:\n      operationId: unsetSetting\n      summary: Remove one stored setting\n      description: >-\n        Removes the setting stored under one key — the write `bd config unset`\n        spells. `DELETE` for `DELETE /v0/beads/memories/{key}`'s reason: it names\n        ONE resource by path, carries no body and takes no flags, which is what\n        the method already means.\n\n\n        REMOVING A KEY NOTHING SET SUCCEEDS, and this is where the operation\n        parts company with the memory delete beside it. That one answers `404`\n        for a key it held nothing under, because its role reports whether a row\n        was found and `bd recall` already has an exit-code contract for the\n        miss. THIS role reports no such thing: the storage seam discards the\n        affected-row count on all three implementations, and an absent key and a\n        key stored as the empty string are one answer on this plane — the same\n        conflation `GET /v0/beads/config/{key}` has no `404` for. So this\n        operation states an INTENDED END STATE rather than an act performed, and\n        a caller clearing configuration it is not sure was ever written does not\n        have to classify an error to learn it was already absent.\n\n\n        THERE IS CONSEQUENTLY NO `removed` MEMBER, and there must not be one: it\n        would be a value one implementation had to invent. Sending the same\n        request twice is `200` and then `200`.\n\n\n        UNSET DOES NOT UNDO `PUT`'s PROJECTION. Removing `status.custom` or\n        `types.custom` deletes the row and LEAVES the normalized table exactly as\n        the last write left it, so the custom statuses and types keep applying\n        after the key that configured them is gone. All three implementations\n        agree, so it is the plane's behavior rather than a divergence, and it is\n        stated here so no reader infers a symmetry with `PUT` that the code does\n        not have.\n\n\n        THE PROTECTED KEY IS NOT REFUSED HERE. `issue_prefix` cannot be WRITTEN\n        through this plane and can be removed through it — an asymmetry that is\n        shipped behavior on all three implementations rather than a decision this\n        document makes, recorded as bd-yby99.34. Removing it does not rename\n        anything; it leaves the workspace resolving its prefix from `config.yaml`\n        or from nothing.\n\n\n        A CREDENTIAL-BEARING KEY IS REMOVABLE, on `PUT`'s reasoning: redaction\n        withholds a value from a READER, and a removal discloses nothing at all.\n\n\n        Hooks do not fire, and this plane has none to fire; see `PUT`.\n      parameters:\n        - $ref: '#/components/parameters/SettingKey'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            The key holds nothing now. Whether it held anything before is not\n            reported — see the description.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RemovedSetting'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, or a `key` that is empty\n            after trimming or carries a control character. It is the `GET`\n            beside it's refusal exactly, unchanged, because the two take the same\n            parameter and judge it the same way. Nothing is removed in either\n            case.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/dependencies:\n    get:\n      operationId: listDependencies\n      summary: List the stored dependency edges of several issues\n      description: >-\n        The STORED EDGE ROWS whose source is each named issue — the answer\n        `bd dep list a b c` gives, and the same `Dependency` elements that\n        command's `--json` emits.\n\n\n        THESE ARE ROWS, NOT NEIGHBOURS, and that is the difference from the\n        `dependencies` member of `GET /v0/beads/issues/{id}`'s issue body. A\n        dependency target may be an `external:` reference or an id belonging to\n        another repository, and this database holds no issue for either; such\n        an edge is returned here with its target spelled exactly as stored, and\n        nothing is looked up on the far end. The `id` member of each element is\n        absent: the read behind this operation does not select the row's\n        surrogate key, so the `(issue_id, depends_on_id, type)` triple is what\n        identifies an edge on this surface.\n\n\n        THE DIRECTION IS OUTGOING ONLY — what each issue depends on. There is\n        no direction parameter: the inbound bulk read is a different read\n        against a different key with its own de-duplication rule across the two\n        dependency tables, and adding it later is additive.\n\n\n        A NAMED ISSUE THAT DOES NOT EXIST IS NOT A 404. It is listed in\n        `missing`, and the edges of the issues that do exist are still\n        returned: a batch that failed on one absent id would throw away the\n        answers for the ids that were found. An issue that exists and depends\n        on nothing is absent from `missing` and contributes no `items`, which\n        is the distinction this operation exists to publish — an empty edge\n        list is otherwise indistinguishable from a typo, and the empty list is\n        the common case.\n\n\n        THERE IS NO `limit` AND NO CURSOR. `bd dep list` has no limit either,\n        so one here would make the two surfaces answer differently by default;\n        instead the QUESTION is bounded — at most 100 `issue_id` values per\n        call — which bounds the answer without truncating it. Each issue's\n        edges are unbounded, exactly as the `dependencies` member of\n        `GET /v0/beads/issues/{id}` already is.\n      parameters:\n        - name: issue_id\n          in: query\n          description: >-\n            The issues to read edges for. Repeat the parameter; at least one is\n            required and at most 100 are accepted, and either bound is a 400\n            `invalid_argument` with `param: \"issue_id\"`, `reason:\n            \"invalid_value\"`.\n\n\n            Each value must be an EXACT canonical issue id: there is no fuzzy,\n            prefix or substring resolution on this surface, for the reason\n            `GET /v0/beads/issues/{id}` gives. A value that matches nothing is\n            reported in `missing` rather than refused. An empty value is a 400.\n\n\n            Repeats collapse: an id named twice is one entry in `missing` at\n            most once, and its edges appear once.\n          style: form\n          explode: true\n          required: true\n          schema:\n            type: array\n            minItems: 1\n            maxItems: 100\n            items:\n              type: string\n        - name: type\n          in: query\n          description: >-\n            Edge types to include. Repeat the parameter. Empty means every\n            type.\n\n\n            The vocabulary is OPEN — a workspace configures its own edge types\n            — so an unrecognized value is not an error here: it simply matches\n            no edge. What IS refused, with a 400 `invalid_argument`, is a value\n            no edge could ever carry: empty, or longer than the column.\n\n\n            The filter narrows EDGES, never the named issues. An issue whose\n            every edge the filter rejects is still not in `missing`.\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: The stored edges of the named issues.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/DependencyEdges'\n        '400':\n          $ref: '#/components/responses/InvalidArgument'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/dependencies:count:\n    get:\n      operationId: countDependencyEdges\n      summary: Count the dependency edges around several issues\n      description: >-\n        HOW MANY EDGES each named issue has in ONE named direction, after the\n        type and status filters — the cardinality behind\n        `GET /v0/beads/dependencies`, plus the direction that read does not\n        take. Nothing is materialized: this is the operation for a caller that\n        wants the numbers for issues whose edges it will never print.\n\n\n        IT IS NOT THE LISTING COUNTED, and the operationId says so rather than\n        pretending otherwise. The listing is OUTGOING ONLY and takes no\n        direction; this one REQUIRES a direction and answers about either end.\n        Naming it `countDependencies` would have promised that it sizes the set\n        `listDependencies` returns, which it does only at `direction=out`.\n\n\n        THE ANSWER IS PER ANCHOR, which is the other difference from the\n        listing. That one flattens every issue's edges onto one array because\n        the rows carry their own `issue_id`; a number does not, so folding\n        these together would produce a total no caller asked for and lose the\n        per-issue answer every caller wants. `anchors` carries one entry per\n        DISTINCT requested id, in the order the request first named it — an\n        array rather than an object, because the request's order is part of the\n        answer and a keyed object would have made that ordering this surface's\n        own invention.\n\n\n        A NAMED ISSUE THAT DOES NOT EXIST IS NOT A 404, on\n        `GET /v0/beads/dependencies`'s terms and one sharper: its entry reports\n        `missing: true` with `count: 0`, and 0 is the COMMON answer here —\n        most issues have no edges in at least one direction — so without that\n        flag a typo would be indistinguishable from a real zero and would never\n        surface. There is no `not_found` on this operation at all.\n\n\n        THE COUNT SPANS BOTH DEPENDENCY PLANES and is a SUM rather than a\n        distinct count of edge rows; a status-narrowed count reads the\n        dependent's status from its own plane; and `status` is legal only with\n        `direction=in` because an outbound edge's far end may be a row this\n        database does not hold. Those are the ROLE's rules, stated once at\n        `issueops.GraphCounter` and its `EdgeCountRequest`, and this document\n        cites them rather than restating them — a second telling is a second\n        thing to keep true.\n\n\n        THERE IS NO `limit` AND NO CURSOR, for the reason\n        `GET /v0/beads/dependencies` has none and `GET /v0/beads/issues:count`\n        gives: a cardinality has no page, and bounding the scan would answer\n        \"how many of the first N\". The QUESTION is bounded instead, at 100\n        `issue_id` values per call — the same bound, the same number and the\n        same constant as the listing on this collection, because the two bound\n        the same thing and a client holding both must not have to learn two\n        numbers.\n\n\n        THERE IS NO `both` DIRECTION. A caller that wants the pair asks twice,\n        which is what every front door in the tree already does. One call\n        answering both would mean two numbers per anchor, and `status` — which\n        narrows by a row only the inbound direction has — would then govern one\n        of them and silently not the other.\n      parameters:\n        - name: issue_id\n          in: query\n          description: >-\n            The anchors to count around. Repeat the parameter; at least one is\n            required and at most 100 are accepted, and either bound is a 400\n            `invalid_argument` with `param: \"issue_id\"`, `reason:\n            \"invalid_value\"`.\n\n\n            Each value must be an EXACT canonical issue id, for the reason\n            `GET /v0/beads/dependencies` gives. A value that matches nothing is\n            reported on its own anchor rather than refused. An EMPTY value is a\n            400: the empty string names nothing a caller can have meant, and\n            reporting it as a missing anchor would put a nameless row in an\n            answer keyed by name.\n\n\n            Repeats collapse onto the first mention — a second entry carries no\n            second fact and would only invite a caller summing the result to\n            count the same edges twice.\n          style: form\n          explode: true\n          required: true\n          schema:\n            type: array\n            minItems: 1\n            maxItems: 100\n            items:\n              type: string\n        - name: direction\n          in: query\n          required: true\n          description: >-\n            Which end of the edge the anchors sit on. `out` counts what each\n            anchor DEPENDS ON — the direction `bd dep list` reads and the number\n            `bd show` prints as the dependency count. `in` counts what depends\n            on it.\n\n\n            IT IS REQUIRED, and that is the one deliberate unfriendliness on\n            this request. The two answers are about DIFFERENT EDGE SETS, and a\n            workspace where most issues have edges in only one direction returns\n            the same number for both often enough that a caller who meant the\n            other one would not notice for a long time. An absent or\n            unrecognized value is a 400 `invalid_argument` with\n            `param: \"direction\"`, never a count in some default direction.\n\n\n            The vocabulary is CLOSED — unlike `type` below — because it is a\n            property of the edge's shape rather than of a workspace's\n            configuration.\n          schema:\n            type: string\n            enum: [out, in]\n        - name: type\n          in: query\n          description: >-\n            Edge types to include. Repeat the parameter. Empty means every\n            type.\n\n\n            `GET /v0/beads/dependencies`'s `type` exactly: the vocabulary is\n            OPEN, so an unrecognized value is not an error and simply matches no\n            edge, while a value no edge could ever carry — empty, or longer than\n            the column — is a 400 `invalid_argument`.\n\n\n            The filter narrows EDGES, never anchors. An anchor whose every edge\n            it rejects comes back present with a count of 0, which is a\n            different fact from an anchor that is not there.\n          style: form\n          explode: true\n          schema:\n            type: array\n            items:\n              type: string\n        - name: status\n          in: query\n          description: >-\n            Count only edges whose DEPENDENT — the issue at the source end, the\n            one doing the depending — is in this stored status. Empty means\n            every status.\n\n\n            IT IS LEGAL ONLY WITH `direction=in`. Sending it beside\n            `direction=out` is a 400 `invalid_argument` with `param: \"status\"`\n            and `reason: \"invalid_value\"`, rather than a filter that is quietly\n            ignored. The asymmetry is the substrate's and\n            `issueops.EdgeCountRequest.Status` states why: narrowing by status\n            joins the far end of the edge to the row holding its status, and an\n            OUTBOUND edge's far end may be an `external:` reference or an id\n            belonging to another repository — rows this database does not hold\n            — so the filter would silently drop every dangling edge.\n\n\n            It is ONE status, not a comma-separated OR set, and it is NOT\n            validated against the workspace vocabulary: an unrecognized name\n            matches nothing and counts 0 rather than failing, exactly as\n            `GET /v0/beads/issues:count`'s `status` does. A scripted caller\n            counting a status its workspace has since dropped reads 0 and\n            should keep reading 0.\n          schema:\n            type: string\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: One entry per distinct anchor, in the order first named.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/EdgeCounts'\n        '400':\n          $ref: '#/components/responses/InvalidArgument'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/dependencies/blocking:\n    get:\n      operationId: listBlockingAnnotations\n      summary: Read the blocking decoration of several issues\n      description: >-\n        The DERIVED blocking summary `bd list` prints beside each row —\n        `(parent: X, blocked by: Y, blocks: Z)` — for the issues named here.\n\n\n        IT IS DERIVED, NOT STORED, and that is the whole difference from\n        `GET /v0/beads/dependencies` next door. That operation returns the edge\n        ROWS: every edge type, targets spelled exactly as stored, nothing looked\n        up. This one answers a summary over TWO of those types with a rule\n        applied: a `blocks` edge counts only while its blocker is open, and a\n        `parent-child` edge becomes `parent` rather than a blocker. A client\n        that wants the rows asks the other operation; a client that wants the\n        decoration a listing shows asks this one.\n\n\n        A CLOSED BLOCKER IS NOT A BLOCKER, in either direction. An issue whose\n        every blocker is closed comes back with an empty `blocked_by`, and a\n        CLOSED issue blocks nothing — its `blocks` is empty even where the edges\n        still exist. `parent` follows the same rule: a closed parent is absent.\n\n\n        A BLOCKER THIS WORKSPACE HOLDS NO ROW FOR STILL BLOCKS. An `external:`\n        reference, an id in another repository's namespace and an id whose issue\n        was deleted out from under its edges are all statuses this database\n        cannot read, and an unreadable status is not `closed`. Hiding such a\n        blocker would report work as unblocked on the strength of a row that was\n        never found, so it is reported.\n\n\n        THERE IS NO `missing` MEMBER, unlike `GET /v0/beads/dependencies`. This\n        operation runs no existence probe: an id that names nothing and an id\n        with no live blocking edges decorate identically, so the probe would be\n        a read whose answer no client could act on. A client that needs to know\n        whether an id exists calls `GET /v0/beads/issues/{id}`, which answers\n        `404`. Every requested id gets an entry here either way.\n\n\n        THERE IS NO `limit` AND NO CURSOR, for the reason\n        `GET /v0/beads/dependencies` has none: the QUESTION is bounded instead,\n        at 100 `issue_id` values per call, which bounds the answer without\n        truncating it.\n      parameters:\n        - name: issue_id\n          in: query\n          description: >-\n            The issues to annotate. Repeat the parameter; at least one is\n            required and at most 100 are accepted, and either bound is a 400\n            `invalid_argument` with `param: \"issue_id\"`, `reason:\n            \"invalid_value\"`.\n\n\n            Each value must be an EXACT canonical issue id: there is no fuzzy,\n            prefix or substring resolution on this surface, for the reason\n            `GET /v0/beads/issues/{id}` gives. A value that matches nothing gets\n            a bare entry rather than being refused. An empty value is a 400.\n\n\n            Repeats collapse: an id named twice is one entry, at the position of\n            its first mention.\n          style: form\n          explode: true\n          required: true\n          schema:\n            type: array\n            minItems: 1\n            maxItems: 100\n            items:\n              type: string\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: The blocking decoration of the named issues.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/BlockingAnnotations'\n        '400':\n          $ref: '#/components/responses/InvalidArgument'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/dependencies:add:\n    post:\n      operationId: addDependencies\n      summary: Assert dependency edges as one act\n      description: >-\n        Asserts every edge in the request, or none of them.\n\n\n        IT IS ALL-OR-NOTHING, and unlike a batch close that is not a policy\n        choice but the shape of the question. Edges asserted together describe a\n        GRAPH, and half a graph is a graph nobody asked for — the cycle a caller\n        was refused for is exactly the state a partial commit would leave\n        behind. A client that gets a 4xx knows nothing was written, and can fix\n        its payload and resend it unchanged. There is no per-edge outcome\n        because there is no outcome but the request's.\n\n\n        AN EDGE IS IDEMPOTENT AT ITS OWN TYPE. A pair that already carries an\n        edge of the requested type refuses nothing and is still echoed in\n        `added`; a pair that carries a DIFFERENT type is a `409`\n        `dependency_exists`, whichever of the two types was stored first.\n        Repetition WITHIN one request answers the same way: the second\n        occurrence of a pair finds the first already written, and two different\n        types for one pair in one request is the same `409`.\n\n\n        A TARGET NEED NOT BE AN ISSUE THIS DATABASE HOLDS. An `external:`\n        reference and an id belonging to another repository are legitimate\n        targets — the vocabulary is open — so only an absence this database can\n        SEE is refused. A SOURCE has no such latitude: an edge follows its\n        source, so a source this database holds no row for has no plane to land\n        in.\n\n\n        Edges are applied parent-child first regardless of request order, so the\n        complete planned hierarchy is visible before any blocking edge is\n        validated against it, and a whole-graph gate runs once at the end. Both\n        hold ACROSS the durable and ephemeral planes: a request may mix them,\n        and it is still one transaction.\n\n\n        A request that wrote no genuinely new durable edge records no history\n        entry, and a request made entirely of wisp-sourced edges records none\n        either — an edge follows its source, and the wisp plane is not\n        versioned. Each genuinely new edge records a `dependency_added` entry on\n        its source's event stream, attributed to `actor`.\n\n\n        Hooks do not fire and the per-command auto-commit machinery does not\n        run, exactly as for `POST /v0/beads/issues/{id}:claim`. The only durable\n        effect is the single storage commit the role makes in its own\n        transaction.\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/AddDependenciesRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            Every edge landed. `added` echoes the request's edges in request\n            order.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/AddDependenciesResponse'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, an unparseable or\n            oversized body, an unknown body member at any level, an `actor` that\n            is empty after trimming, longer than 256 bytes or carrying control\n            characters, an empty or over-long `edges` array, a blank or\n            over-long endpoint id, an unstorable `type`, an edge that points an\n            issue at ITSELF, or an edge endpoint that names nothing this\n            database can see. Nothing is written in any of these cases.\n\n\n            A self-dependency is a `400` rather than a `409` because it is\n            request-intrinsic: it is invalid whatever the graph holds, so it is\n            a refusal of a VALUE and not a statement about state. An endpoint\n            that names nothing is a `400` rather than a `404` for\n            `POST /v0/beads/issues:batchCreate`'s reason: the refusal is about\n            the request BODY, and there is no id in the path to have missed.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '409':\n          description: >-\n            The graph refuses the requested edge set, and NOTHING WAS WRITTEN.\n\n\n            `dependency_cycle` means the set can never make progress: a\n            scheduling cycle, or a blocking edge against the issue's own\n            ancestor or descendant. The hierarchy case — and ONLY the hierarchy\n            case — additionally carries `issue_id`, `blocker_id` and\n            `blocker_is_ancestor`. Member PRESENCE is the discriminator: absent\n            means the plain cycle refusal, present means the hierarchy one, and\n            the three together are enough to rebuild the refusal whole rather\n            than parse it out of prose.\n\n\n            `dependency_exists` means the pair already carries an edge of a\n            DIFFERENT type; `existing_type` and `requested_type` carry both.\n\n\n            Neither has a force bypass. The recovery for both is to rethink the\n            edge, which is why they are one status and not a retry.\n          x-bd-codes: [dependency_cycle, dependency_exists]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/dependencies:remove:\n    post:\n      operationId: removeDependency\n      summary: Remove one dependency edge\n      description: >-\n        Removes exactly the edge the request names — source, target — and at\n        most that one edge.\n\n\n        IT IS IDEMPOTENT, and `removed: false` is a SUCCESS rather than a\n        refusal. Removing an edge twice leaves the same graph as removing it\n        once, so an agent replaying its own teardown does not have to classify\n        an error to discover it already ran. Nothing is written for it and no\n        event is recorded.\n\n\n        THERE IS NO `404` ON THIS OPERATION, deliberately. An edge that is not\n        there is `removed: false`, and an endpoint id that names nothing holds\n        no edge either, so this operation probes no id's existence and has\n        nothing it could report a miss on.\n\n\n        It is a collection-level custom method rather than a `DELETE`: an edge\n        is named by TWO endpoints, so there is no single-segment resource path\n        for a `DELETE` to address, and a `DELETE` carrying a body is the shape\n        proxies mangle. `DELETE /v0/beads/memories/{key}` is a `DELETE` for the\n        opposite reason — one named resource, no body.\n\n\n        A removal that found its edge records a `dependency_removed` entry on\n        the source's event stream, attributed to `actor`. An edge FOLLOWS ITS\n        SOURCE, so a wisp-sourced edge is removed from the unversioned plane\n        and leaves no durable history entry.\n\n\n        Hooks do not fire and the per-command auto-commit machinery does not\n        run, exactly as for `POST /v0/beads/issues/{id}:claim`. The only\n        durable effect is the single storage commit the role makes in its own\n        transaction.\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/RemoveDependencyRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            The removal ran. `removed` says whether an edge was there to\n            remove.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RemoveDependencyResponse'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, an unparseable or\n            oversized body, an unknown body member, a missing or non-string\n            member, an `actor` that is empty after trimming, longer than 256\n            bytes or carrying control characters, or an endpoint id that is\n            empty or longer than storage holds. Nothing is removed.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/memories:\n    get:\n      operationId: listMemories\n      summary: List the workspace's stored memories\n      description: >-\n        Every memory this workspace holds, optionally narrowed by `search`. It\n        is the plane `bd memories` reads.\n\n\n        ONLY THE MEMORY PLANE. Memories ride in the same database table as the\n        workspace's settings and as the generic `bd kv` namespace, and neither\n        of those appears here, whatever it contains — including a memory whose\n        key SHADOWS a settings name: a memory called `issue_prefix` is a memory,\n        and the workspace's real issue prefix is not one. That separation is the\n        role's, pinned by its conformance contract, not a filter applied to this\n        answer.\n\n\n        MEMORY CONTENT IS SERVED IN FULL, and authentication does not narrow\n        that. Every entry carries its value verbatim; there is no `redacted`\n        member and nothing is withheld. A bearer decides WHO may call, not what\n        a caller who is let in may read, so a token-protected server serves\n        every memory to every holder of the token. See\n        `POST /v0/beads/memories` for why a key-name heuristic would be worse\n        here than no promise at all. This is the operation that makes stored\n        memories DISCOVERABLE rather than merely readable by a caller who\n        already knows a key — an operator binding beyond loopback is accepting\n        exactly that for everyone the token admits.\n\n\n        A memory stored as the EMPTY STRING is enumerated here, because its key\n        exists, while `GET /v0/beads/memories/{key}` answers `404` for it. That\n        asymmetry is the one way a client can tell a row stored empty from a row\n        that is not there, and it is the storage seam's conflation showing\n        through rather than a rule this surface invented.\n\n\n        The envelope is the paginated one and `has_more` is ALWAYS false:\n        memories are a keyed namespace a workspace holds tens of, not a\n        collection to scan, so the whole plane comes back in one page. There is\n        no `limit` and no cursor. The envelope is used anyway because entries\n        are ordered by key, which makes a keyset cursor expressible later\n        without a breaking change.\n      parameters:\n        - name: search\n          in: query\n          description: >-\n            Narrows the answer to memories that MATCH: a memory matches when the\n            lowercase of its key, or the lowercase of its value, contains the\n            lowercase of this term. Absent or empty means everything, and a term\n            nothing matches is a `200` with an empty `items`.\n\n\n            IT IS A SUBSTRING MATCH, NOT THE `issues:query` EXPRESSION LANGUAGE,\n            and it is spelled `search` rather than `q` FOR THAT REASON. On `GET\n            /v0/beads/issues:query`, `q` is a boolean expression over issue\n            fields that is refused when it does not parse; here there is nothing\n            to parse, no vocabulary and no refusal — every string is a legal\n            search term, `status=open` included, and it is matched literally.\n            Two names because two questions: a client that sent this operation\n            the other `q` would otherwise get a literal substring search back\n            instead of an error.\n\n\n            The term reaches the role UNFOLDED. Case folding is the role's, so\n            that this surface and `bd memories` cannot come to disagree about\n            what matching means; a client sends what its user typed.\n          schema:\n            type: string\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: The stored memories, ordered by key.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/MemoriesPage'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, or a repeated\n            `search`. `search` is the only parameter this operation accepts —\n            `q` is NOT one of them, and is refused as unknown rather than\n            treated as a search term.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n    post:\n      operationId: rememberMemory\n      summary: Store one memory\n      description: >-\n        Stores one memory in the workspace's persistent memory plane — the\n        operation behind `bd remember`. It is an UPSERT: a key that already\n        holds a memory is overwritten, and `replaced` in the response reports\n        which of the two happened.\n\n\n        `key` is OPTIONAL, and omitting it is the normal case. The server then\n        derives the key from `content` using the one derivation `bd remember`\n        has used since it shipped, and the response's `key` is where the caller\n        learns what to recall. A `key` that IS supplied is used verbatim: no\n        trimming, no slugging, no charset restriction, because a stored key has\n        to stay recallable under the exact bytes the caller used.\n\n\n        MEMORY CONTENT IS SERVED IN FULL, here and on every other operation of\n        this plane, and a bearer does not change that — it gates WHO may call,\n        never what a caller who is let in may read. There is no `redacted`\n        member and no value withholding: the settings surface's redaction is a\n        heuristic over the KEY NAME, and memory keys are derived from the\n        content, so the same rule would withhold a memory ABOUT tokens while\n        serving one that CONTAINS a token under an innocuous slug. A promise\n        that cannot be kept is not made. Do not store credentials in this plane.\n\n\n        There is no `201`/`200` split. `replaced` already says whether a row\n        existed, saying it twice in a second vocabulary would add nothing, and\n        an upsert whose key the server may derive has no stable `Location` to\n        point at.\n\n\n        Hooks do not fire and the per-command auto-commit machinery does not\n        run, exactly as for the other write operations on this surface. The\n        only durable effect is the single storage commit the role makes in its\n        own transaction — which also observes whether a previous value existed,\n        so `replaced` is a statement about the row this request wrote rather\n        than about a row some earlier read happened to see.\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/RememberRequest'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            The memory was stored. `key` is where it lives, derived or verbatim.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RememberedMemory'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, an unparseable or\n            oversized body, an unknown body member, a missing or non-string\n            `content`, a `content` that is empty after trimming, or a `content`\n            from which no key can be derived when none was supplied — the last\n            two the ROLE's refusals, reaching the wire with its own sentences.\n            Nothing is stored in any of these cases.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/memories/{key}:\n    get:\n      operationId: getMemory\n      summary: Get one stored memory\n      description: >-\n        The memory stored under one key, used verbatim: no namespace completion,\n        no case folding, no dash/underscore equivalence. It is the operation\n        behind `bd recall`.\n\n\n        THIS OPERATION HAS A REAL 404, and it is the one place this surface\n        diverges from `GET /v0/beads/config/{key}`, which deliberately has none.\n        The planes genuinely differ. On the settings plane a key nothing stored\n        and a key stored empty are one answer that `bd config get` prints\n        identically, so a 404 would publish a distinction the server would have\n        to invent. On this plane `bd recall` ALREADY distinguishes a miss, by\n        exit code, and the role answers a miss as a result rather than a value\n        — so a 404 reports a distinction that exists rather than minting one.\n\n\n        A MEMORY STORED AS THE EMPTY STRING IS A 404 TOO. No front door can\n        create one — storing empty content is refused — but an out-of-band write\n        to the config table can, and the role does not invent a distinction the\n        storage seam beneath it cannot see. `GET /v0/beads/memories` DOES\n        enumerate such a row, because its key exists, and that asymmetry is the\n        one way a client can tell the two apart.\n\n\n        MEMORY CONTENT IS SERVED IN FULL, and a configured bearer does not\n        narrow it: `Memory` carries no `redacted` member and withholds nothing\n        from any caller the token admits. See `POST /v0/beads/memories` for why\n        a key-name heuristic would be worse than no promise at all.\n\n\n        KEYS THIS OPERATION CANNOT REACH. `bd remember --key` accepts any\n        string, so a stored key may carry a control character — and `key` is one\n        path segment, percent-decoded once, so such a key would arrive here as a\n        decoded control character in a path. This operation refuses it with a\n        `400` rather than looking it up, exactly as `getSetting` does. The ROLE\n        stays verbatim: breaking `bd recall` of an odd key someone already\n        stored, to tidy a wire rule, would be the tail wagging the dog. Such a\n        memory is reachable from the CLI and from `GET /v0/beads/memories`, and\n        not by path.\n      parameters:\n        - $ref: '#/components/parameters/MemoryKey'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: The stored memory.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Memory'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, or a `key` that is\n            empty after trimming or carries a control character.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '404':\n          description: >-\n            This workspace holds no memory under that key — or holds one stored\n            as the empty string, which answers the same way. `detail` says which\n            plane was asked, not which of those two it was.\n          x-bd-codes: [not_found]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n    delete:\n      operationId: forgetMemory\n      summary: Forget one stored memory\n      description: >-\n        Removes the memory stored under one key and answers with what it held —\n        the operation behind `bd forget`. IT IS DESTRUCTIVE and nothing it\n        removes comes back, on a surface that has NO AUTHENTICATION: every\n        process that can reach the port can erase any memory whose key it knows,\n        exactly as `issues:sweep` and `issues:delete` state for beads. The\n        recovery is the workspace's version control, not this API.\n\n\n        THE FIRST `DELETE` METHOD ON THIS SURFACE, and the shape is why. The two\n        destructive issue operations are collection-level custom methods because\n        they act on a SET the request describes and carry flags that change what\n        is erased — a query string is exactly where a dropped parameter widens a\n        deletion. This one names ONE resource by path, carries no body and takes\n        no flags, which is what the `DELETE` method already means. The\n        alternative spelling `POST /v0/beads/memories/{key}:forget` would in\n        addition recreate the claim route's wildcard contortion, since\n        `{key}:forget` is not a router pattern, for no gain.\n\n\n        REMOVING EXACTLY THE NAMED ROW is the ROLE's promise, pinned by its\n        conformance contract rather than restated here: the memory plane shares\n        one table with the workspace's settings and with the generic `bd kv`\n        namespace, and a memory called `issue_prefix` is not the workspace's\n        issue prefix.\n\n\n        Forgetting a key nothing stored is a `404` and removes nothing.\n        Forgetting the same key twice is therefore a `200` and then a `404`,\n        which is what a retrying client actually sees; the second answer is not\n        a failure to act, it is the same fact reported after the act.\n\n\n        The same keys are unreachable here as on the `GET` beside it: a stored\n        key carrying a control character is refused with a `400` rather than\n        looked up, while the role stays verbatim, so such a memory can be\n        forgotten from the CLI and not through this operation.\n\n\n        Hooks do not fire, as for every write on this surface. There is no\n        `dry_run`: one named row is not a set to cost first.\n      parameters:\n        - $ref: '#/components/parameters/MemoryKey'\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            The memory was removed. The body carries what it held, read in the\n            same transaction as the deletion — so it is what was actually\n            removed rather than what an earlier read happened to see.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Memory'\n        '400':\n          description: >-\n            Invalid request: an unknown query parameter, or a `key` that is\n            empty after trimming or carries a control character. Nothing is\n            removed in either case.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '404':\n          description: >-\n            This workspace holds no memory under that key — or holds one stored\n            as the empty string, which answers the same way — and NOTHING WAS\n            REMOVED.\n          x-bd-codes: [not_found]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/events:\n    get:\n      operationId: listEvents\n      summary: Read the durable events journal\n      description: >-\n        The workspace's append-only record of every committed issue mutation,\n        paged from a caller-held checkpoint. It is the HTTP form of\n        `bd events tail --since`, and it exists so a hosted consumer can mirror\n        or replay a workspace without shelling out to the CLI.\n\n\n        THE RECORDS ARE THE CLI'S RECORDS, byte for byte. `records[]` elements\n        are the same `EventRecord` objects `bd events tail` and\n        `bd events export` print one per line, produced by the same projection\n        and covered by the same committed golden fixture. A consumer may\n        reconcile an HTTP mirror against a CLI export without a translation\n        layer.\n\n\n        `since` IS THE CURSOR, and it is not one this server minted. It is a\n        sequence number the journal itself assigned, gapless and strictly\n        increasing in commit order, so a consumer's position is durable across\n        restarts on both sides and means the same thing to the CLI. Read with\n        `since` set to the highest `seq` you have DURABLY PROCESSED — not the\n        highest you have received — and advance it only after your own write\n        lands, because this server keeps no per-consumer state and cannot\n        redeliver.\n\n\n        THIS OPERATION POLLS; there is no long poll and no `follow` parameter.\n        `head` is what makes polling cheap to pace: when the last record's `seq`\n        equals `head` you are caught up and can back off until your next\n        interval. A caught-up read is a 200 with an EMPTY `records` array, never\n        a 404 — \"nothing new yet\" is an ordinary answer about a log.\n\n\n        FOR A PUSH FEED USE `watchEvents`, the sibling operation at\n        `GET /v0/beads/events:watch`, which streams the same records from the\n        same `since` as `text/event-stream`. It is a separate operation rather\n        than a mode of this one because the two differ in media type, lifetime\n        and capacity; a client that streams still needs this operation, because\n        this is the one that is never refused for capacity and the one a stream\n        falls back to.\n\n\n        SCOPE IS PER REPLICA AND PER BRANCH, and this is the part that most\n        often surprises. The journal records what THIS clone mutated on the\n        branch its writer commits to. Rows arrive by direct write, never by\n        merge, so `bd dolt pull` and the changes a merge settles into this\n        workspace are NOT journaled — they arrived as data, and nothing here\n        wrote them through the mutation seam. Each replica also has its OWN seq\n        space, counted from its own first mutation: a checkpoint taken against\n        one server is meaningless against another, where the same number names a\n        different record and a number above that replica's head reads as\n        \"caught up\" and stalls forever. Track a checkpoint per server URL, and\n        re-baseline (a fresh export or full re-read) after a sync rather than\n        carrying one across.\n\n\n        NOT EVERY MUTATION IS COVERED. Raw DML through `bd sql` bypasses the\n        mutation seam and is not journaled; nor are the schema migrations and\n        version reconciliation that run while a store is being opened, which\n        touch no bead. Dependency records are not symmetric either: `dep_add` is\n        emitted for an idempotent same-type re-add that only refreshes edge\n        metadata, so treat it as an upsert of the edge rather than proof the\n        edge is new, and a `dep_remove` naming an edge that is already gone\n        emits nothing at all.\n\n\n        THIS OPERATION NEVER DELETES. Retention is the workspace's decision,\n        made by `bd events prune` and by the automatic bounding that keeps an\n        enabled journal inside its floors; no prune is reachable over HTTP, and\n        reading a record does not acknowledge or release it.\n\n\n        IT ALSO DEPENDS ON WORKSPACE STATE, alone among the operations here.\n        `events.list` in `ContextResponse.capabilities` says this BUILD serves\n        the operation; it does not say this workspace has a journal, because the\n        journal is a per-workspace setting that is off by default. A server that\n        advertises the capability and answers 409 `events_journal_disabled` to\n        every request is behaving correctly. Handle that 409 as \"not on this\n        workspace\" rather than as a fault, and do not read the capability as a\n        promise that records will arrive.\n\n\n        A workspace that HAS enabled the journal on a storage backend with no\n        journal seam does not reach this operation at all: `bd serve` refuses to\n        start, matching the refusal that opening such a workspace already\n        produces. Either the server is running and this operation can answer, or\n        the operator saw the failure at startup.\n      parameters:\n        - name: since\n          in: query\n          required: true\n          description: >-\n            Return records with `seq` strictly greater than this value. Pass `0`\n            to read from the beginning of the retained journal.\n\n\n            REQUIRED, and deliberately not defaulted to zero. A consumer that\n            omitted its checkpoint by mistake would be served the whole retained\n            window, which reads as a flood of duplicate records rather than as\n            an error. A negative value is a 400 `invalid_argument` for the same\n            reason `bd events tail --since` refuses one: it is almost always\n            arithmetic on an empty cursor, and `seq > -5` would quietly serve\n            everything as though it were a legitimate resume.\n\n\n            A value at or above `head` is not an error — it is the caught-up\n            case, a 200 with an empty `records` array.\n          schema:\n            type: integer\n            format: int64\n            minimum: 0\n        - name: limit\n          in: query\n          description: >-\n            Maximum number of records to return, from 1 to 10000. A value\n            outside that range — `0` included — is a 400 `invalid_argument`.\n\n\n            THERE IS NO UNLIMITED READ HERE, and `0` does NOT mean unlimited as\n            it does on `GET /v0/beads/issues`. A caller resuming from an old\n            checkpoint would otherwise ask one process to buffer the entire\n            retained window — a hundred thousand records under the shipped\n            `events-journal-retain-rows` floor — and encode it into a single\n            response. The ceiling is unconditional and does not depend on the\n            bind mode.\n\n\n            The default of 1000 is deliberately much larger than the issue\n            listings' 50: a journal consumer is a machine draining a backlog in\n            order rather than a person reading a page, and the number that\n            matters to it is round trips to catch up.\n\n\n            A FULL PAGE DOES NOT MEAN THERE IS MORE, and a short one does not\n            mean there is not. Compare the last record's `seq` against `head`;\n            that is the only correct test, and it is why this envelope carries\n            no `has_more`.\n          schema:\n            type: integer\n            minimum: 1\n            maximum: 10000\n            default: 1000\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            A page of journal records in ascending `seq` order, with the\n            journal head.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/EventsPage'\n        '400':\n          description: >-\n            Invalid request: `since` absent, negative or unparseable, a `limit`\n            outside 1..10000, or an unknown query parameter.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '409':\n          description: >-\n            The durable events journal is NOT ENABLED on this workspace, so it\n            records nothing and never will until an operator turns it on\n            (`events-journal true`, or `BD_EVENTS_JOURNAL=1` in the server's\n            environment) and restarts the server.\n\n\n            This refusal exists because the honest alternative does not: a\n            disabled journal presents as zero rows and a head of zero, which is\n            byte-identical to an enabled journal nothing has written to yet. A\n            consumer given that answer would poll a workspace that will never\n            produce a record and report itself caught up indefinitely. An EMPTY\n            journal is the 200 — `records: []` with `head: 0` — and the two are\n            distinguishable only here.\n\n\n            A 409 rather than a 404 because the operation and the resource both\n            exist: this is a statement about the workspace's current\n            configuration, which the same request stops earning the moment it\n            changes. It is a server-side fix, never a retry and never a\n            client-side one.\n          x-bd-codes: [events_journal_disabled]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '410':\n          description: >-\n            The checkpoint in `since` has fallen BELOW the retained window: the\n            records that came next were pruned, and this server cannot serve\n            them. Retrying the same `since` can never succeed.\n\n\n            The read FAILS rather than returning an empty success or silently\n            skipping ahead, and that is the whole point of the code. `WHERE seq\n            > since` cannot itself distinguish \"nothing new\" from \"your prefix\n            is gone\", so a consumer resuming past a prune would either stall\n            forever or jump to the current floor and lose every record in\n            between — both silent data loss.\n\n\n            The response carries `since`, `floor` and `head`: `floor` is the\n            lowest seq still retained, `head` the highest ever assigned. The\n            recovery is a decision the CONSUMER makes and this server will not\n            make for it — resume from `floor - 1` and accept a known gap, or\n            rebuild from a full export.\n\n\n            `since` IN THE RESPONSE IS NOT ALWAYS THE VALUE YOU SENT, and the\n            case where it differs has a third recovery. On an interior hole in\n            the retained window — a restored, hand-edited or half-copied journal\n            table; nothing bd does produces one — the response `since` is the\n            last seq that could be served CONTIGUOUSLY from your checkpoint, and\n            everything between your checkpoint and it is intact and servable.\n            This refusal does not hand it over, because a read that returned rows\n            and a gap would be the silent loss the code exists to prevent. Drain\n            it explicitly first: re-request with the SAME `since` you sent and\n            `limit` set to `response.since - request.since`, which stops the page\n            exactly at the hole and succeeds. Then resume from `floor - 1` and\n            take the gap, or re-baseline. A consumer that skips straight to\n            `floor - 1` loses records it could have had.\n\n\n            `since` is never BELOW what you sent in any of these cases, so\n            echoing it back can never re-deliver records you already have.\n\n\n            Sizing the floors — `events-journal-retain-days` and\n            `events-journal-retain-rows` — is how an operator decides the\n            longest consumer outage the workspace can survive. They are time and\n            count based and are NOT a consumer watermark: nothing here knows how\n            far any consumer has got.\n          x-bd-codes: [events_journal_truncated]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          $ref: '#/components/responses/Unavailable'\n\n  /v0/beads/events:watch:\n    get:\n      operationId: watchEvents\n      summary: Stream the durable events journal\n      description: >-\n        The same journal as `listEvents`, PUSHED: a held-open\n        `text/event-stream` response that emits each committed mutation as it\n        lands, so a consumer learns about a write when it happens rather than\n        on its next interval.\n\n\n        THE CURSOR IS STILL THE CONTRACT. This is not a subscription — the\n        server keeps no per-consumer state, remembers nothing between\n        connections and cannot redeliver. A stream is the reads you would have\n        performed yourself, performed on your behalf, and every event carries\n        `id:` set to the record's `seq`: the same number `since` takes, the same\n        number `bd events tail --since` takes. Advance your own checkpoint only\n        after your write lands, exactly as on the paged read.\n\n\n        RECONNECTION IS THE NORMAL CASE and it is free. When a stream drops, a\n        client re-requests this operation with the standard `Last-Event-ID`\n        header carrying the last `seq` it processed; that header WINS over the\n        `since` query parameter, which is what makes a browser's `EventSource`\n        correct without any code — it re-sends the original URL, and its\n        original `since` would otherwise replay everything since the consumer\n        started. `since` is still required on every connect, because the header\n        is absent on the first one.\n\n\n        WATCH OR POLL is a real choice and the answer is usually poll. A stream\n        costs a connection and a goroutine for its whole life, and this server\n        holds a bounded number of them; a poller holds nothing between requests\n        and can never be refused for capacity. Stream when the delay between a\n        mutation and your reaction is the point — a live mirror, an agent\n        waiting on a gate — and poll for anything that can afford its interval.\n        A backlog is drained at read speed either way, so a stream is not a\n        faster way to catch up, only a shorter wait once you have.\n\n\n        THE STREAM ONLY OPENS ON A SERVABLE CURSOR. Every refusal below is an\n        ordinary `application/problem+json` response with its documented status,\n        decided BEFORE any stream byte — including the 410 for a checkpoint that\n        has been pruned past, which is the same body `listEvents` returns for\n        the same condition. There is exactly one failure a client can meet after\n        the status is spent: see the `truncated` event.\n\n\n        EVERYTHING THE PAGED READ SAYS ABOUT THE RECORDS APPLIES UNCHANGED —\n        they are the same `EventRecord` objects from the same projection; scope\n        is per replica and per branch, so a checkpoint is meaningful only\n        against the server that issued it; merges and `bd sql` are not\n        journaled; and `events.watch` in `ContextResponse.capabilities` says\n        this BUILD serves the operation, not that this workspace has a journal.\n      parameters:\n        - name: since\n          in: query\n          required: true\n          description: >-\n            Emit records with `seq` strictly greater than this value. Pass `0`\n            to stream from the beginning of the retained journal.\n\n\n            REQUIRED on every connect, including a reconnect that also carries\n            `Last-Event-ID`, and refused when negative — both for the reasons\n            `listEvents` gives. When the header is present this value is\n            ignored, but it is still validated: one rule, one spelling, whether\n            or not the client is a browser.\n          schema:\n            type: integer\n            format: int64\n            minimum: 0\n        - name: Last-Event-ID\n          in: header\n          required: false\n          description: >-\n            The last `seq` this client processed, as emitted in the `id:` field\n            of a previous event. Present, it REPLACES `since` as the resume\n            point.\n\n\n            This is the standard SSE reconnection header and browsers attach it\n            automatically, which is the whole reason it outranks the query\n            parameter: an `EventSource` reconnects to the URL it was built with,\n            so honoring `since` there would re-deliver every record since the\n            consumer started on every reconnect.\n\n\n            A NONEMPTY value that is not a non-negative 64-bit integer is a 400\n            `invalid_argument` naming this header, rather than a silent fallback\n            to `since`: a client that invented its own id has a broken\n            checkpoint, and a stream that quietly started somewhere else would\n            look correct and lose records.\n\n\n            An EMPTY value is treated exactly as an absent one — `since`\n            decides — because it says the same thing: no id yet. A client or\n            intermediary that always sets the header sends it empty on the first\n            connect, and refusing that would break the one request this header\n            exists to make work.\n          schema:\n            type: integer\n            format: int64\n            minimum: 0\n      security:\n        - bearerToken: []\n      responses:\n        '200':\n          description: >-\n            An open event stream. It ends when the client disconnects, when the\n            server shuts down, or with the `truncated` event below; there is no\n            end-of-stream marker otherwise, and a client is expected to\n            reconnect.\n\n\n            FRAMES, in the order a client meets them:\n\n\n            `retry: 3000` — the reconnection delay, stated once at the top so it\n            does not depend on the client's default.\n\n\n            `id: <seq>` + `data: <EventRecord>` — one record, as an UNNAMED\n            (default `message`) event, so a bare `onmessage` receives it. The\n            `data` payload is exactly one line: a single JSON object identical\n            to an element of `listEvents`'s `records` array\n            (`#/components/schemas/EventRecord`).\n\n\n            `: heartbeat` — a comment line every ~20 seconds of silence. It\n            carries no information and exists to keep idle connections alive\n            through intermediaries; clients ignore comments by construction.\n\n\n            `event: truncated` + `data: <Problem>` — the ONE in-band failure,\n            emitted when a prune removes the records this stream was about to\n            send. Its data is a `#/components/schemas/Problem` object with code\n            `events_journal_truncated`, carrying the same `since` / `floor` /\n            `head` window and the same `detail` a 410 for that condition\n            carries. It is the 410's body in the same encoding, on one line\n            (an SSE `data:` field cannot contain the newline an HTTP body ends\n            with); `request_id` is this STREAM's id, not a reconnect's, since no\n            second request has happened. Whatever parses the 410 parses this\n            unchanged. The stream closes immediately after it, behind a raised\n            `retry`.\n\n\n            TREAT `truncated` AS STOP-AND-RE-BASELINE. The recovery is the\n            410's: resume from `floor - 1` and accept a known gap, or rebuild\n            from a full export. It is deliberately a NAMED event, so a client\n            that registered only `onmessage` never mistakes it for a record —\n            but such a client will simply reconnect and then meet a\n            connect-time 410 on every attempt, which is why the delay is raised\n            to a minute first. A consumer that ignores this event does not lose\n            records silently; it stalls loudly.\n          content:\n            text/event-stream:\n              schema:\n                type: string\n        '400':\n          description: >-\n            Invalid request: `since` absent, negative or unparseable, a\n            `Last-Event-ID` header that is not a non-negative sequence number,\n            or an unknown query parameter.\n          x-bd-codes: [invalid_argument]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '401':\n          $ref: '#/components/responses/Unauthenticated'\n        '409':\n          description: >-\n            The durable events journal is NOT ENABLED on this workspace, so\n            there is nothing to stream and never will be until an operator turns\n            it on (`events-journal true`, or `BD_EVENTS_JOURNAL=1` in the\n            server's environment) and restarts the server. Identical in every\n            respect to `listEvents`'s 409, and refused before the stream opens.\n          x-bd-codes: [events_journal_disabled]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '410':\n          description: >-\n            The resume point — `Last-Event-ID` if present, otherwise `since` —\n            has fallen BELOW the retained window, so the records that came next\n            were pruned and this server cannot serve them. The body is\n            `listEvents`'s 410 exactly, carrying the same `since` / `floor` /\n            `head` window and the same recoveries, and no stream is opened.\n\n\n            A CLIENT THAT RECONNECTS BLINDLY LOOPS HERE. This is the status a\n            consumer earns after ignoring a `truncated` event, and retrying with\n            the same id can never succeed; re-baseline instead.\n          x-bd-codes: [events_journal_truncated]\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n        '500':\n          $ref: '#/components/responses/InternalError'\n        '503':\n          description: >-\n            The stream could not be opened right now and may be retried:\n            `events_watch_saturated` (this server is already holding as many\n            concurrent streams as it will), `db_unavailable`, or `busy`.\n\n\n            `events_watch_saturated` is the one code here that is about\n            CONNECTIONS rather than data, and the one with a recovery that is\n            not waiting: `GET /v0/beads/events` answers the same records from\n            the same checkpoint and is never refused for this reason. The cap\n            exists because a stream is the only request on this server that can\n            last hours, and streams are held until their consumers leave — so\n            `Retry-After` here is a human-scale hint, not a request-scale one.\n          x-bd-codes: [busy, db_unavailable, events_watch_saturated]\n          headers:\n            Retry-After:\n              description: Seconds to wait before retrying.\n              schema:\n                type: integer\n          content:\n            application/problem+json:\n              schema:\n                $ref: '#/components/schemas/Problem'\n\ncomponents:\n\n  securitySchemes:\n    bearerToken:\n      type: http\n      scheme: bearer\n      description: >-\n        A shared bearer token from the server's token file. It carries no\n        identity and no scopes: presenting an accepted token grants every\n        operation. Which token is presented is never observable in a response.\n\n  parameters:\n    SettingKey:\n      name: key\n      in: path\n      required: true\n      description: >-\n        Exact setting key, used verbatim. Keys routinely contain dots\n        (`status.custom`); they occupy one path segment and are\n        percent-decoded once.\n      schema:\n        type: string\n        minLength: 1\n    MemoryKey:\n      name: key\n      in: path\n      required: true\n      description: >-\n        Exact memory key, used verbatim. It occupies one path segment and is\n        percent-decoded once. Keys may contain spaces, dots and unicode — the\n        plane stores what `bd remember --key` was given — and a key carrying a\n        CONTROL character is refused here rather than looked up; see the\n        operation description.\n      schema:\n        type: string\n        minLength: 1\n    IssueID:\n      name: id\n      in: path\n      required: true\n      description: >-\n        Exact canonical issue id. No fuzzy, prefix or substring resolution.\n      schema:\n        type: string\n\n  responses:\n    Unauthenticated:\n      description: >-\n        The bearer credential was missing, malformed, or is not one this server\n        accepts. Emitted only by a server configured with a token file; one\n        without never emits it. `detail` is a fixed string, which is what\n        guarantees the presented credential is never echoed back to the client\n        or into any log between here and the caller.\n      x-bd-codes: [unauthenticated]\n      headers:\n        WWW-Authenticate:\n          description: The challenge, always `Bearer`.\n          schema:\n            type: string\n      content:\n        application/problem+json:\n          schema:\n            $ref: '#/components/schemas/Problem'\n    InvalidArgument:\n      description: >-\n        Invalid request: an unknown query parameter, or a malformed parameter\n        value.\n      x-bd-codes: [invalid_argument]\n      content:\n        application/problem+json:\n          schema:\n            $ref: '#/components/schemas/Problem'\n    NotFound:\n      description: No issue or wisp with that id.\n      x-bd-codes: [not_found]\n      content:\n        application/problem+json:\n          schema:\n            $ref: '#/components/schemas/Problem'\n    InternalError:\n      description: >-\n        Unexpected server-side failure. `detail` is a fixed string per code:\n        the underlying error goes to the server log only, because driver and\n        dial errors routinely embed the database DSN, user and host:port.\n      x-bd-codes: [internal]\n      content:\n        application/problem+json:\n          schema:\n            $ref: '#/components/schemas/Problem'\n    Unavailable:\n      description: >-\n        The request could not be served right now and may be retried:\n        `db_unavailable` (the database is unreachable) or `busy` (write\n        contention exhausted the transaction retry budget, or the in-flight\n        request limit was saturated). `detail` is a fixed string per code, for\n        the same reason as 500.\n      x-bd-codes: [busy, db_unavailable]\n      headers:\n        Retry-After:\n          description: Seconds to wait before retrying.\n          schema:\n            type: integer\n      content:\n        application/problem+json:\n          schema:\n            $ref: '#/components/schemas/Problem'\n\n  schemas:\n\n    # ---------------------------------------------------------------------\n    # Pinned schemas.\n    #\n    # Each of the thirteen below is `x-go-type`-pinned to the canonical Go struct,\n    # so the generator emits a type ALIAS and no mirror struct exists anywhere.\n    # `types.Issue`'s JSON encoding IS this contract; the CLI's `--json`, the\n    # JSONL interchange and these bodies all marshal the same struct.\n    #\n    # They therefore repeat each other's properties rather than composing with\n    # `allOf` — see the codegen trap documented at the top of this file. The\n    # repetition is machine-checked in both directions by\n    # TestWireTagBijection (internal/httpapi/wire_bijection_test.go): a field\n    # added to the Go struct without an entry here fails CI, and an entry here\n    # with no Go field fails CI.\n    #\n    # Nine of the thirteen are pinned to `internal/types`. The other four are\n    # pinned where their canonical struct actually lives: `CycleMember`,\n    # `Cycle` and `IssueBlocking`, which appear after `Statistics`, to the role\n    # package `issueops`; `EventRecord`, last of the group, to\n    # `internal/eventsjournal`. TestWireTagBijection's `pinnedSchemas` table is\n    # the enumeration that is machine-checked; the counts here are prose and go\n    # stale in silence, so a schema added to that table updates them too.\n    # ---------------------------------------------------------------------\n\n    Issue:\n      type: object\n      description: >-\n        A tracked work item. Property semantics documented here apply to every\n        schema that repeats them below.\n      x-go-type: types.Issue\n      x-go-type-import:\n        name: types\n        path: github.com/steveyegge/beads/internal/types\n      required: [id, title, priority, created_at, updated_at]\n      properties:\n        id:\n          type: string\n          description: Canonical issue id, e.g. `bd-abc`.\n        title:\n          type: string\n        description:\n          type: string\n        design:\n          type: string\n        acceptance_criteria:\n          type: string\n        notes:\n          type: string\n        spec_id:\n          type: string\n        status:\n          type: string\n          description: >-\n            `open`, `in_progress`, `blocked`, `closed`, or any custom status\n            configured for the workspace. NOT a closed vocabulary: clients must\n            treat an unrecognized value as \"some status I do not know\".\n        priority:\n          type: integer\n          description: 0 (most urgent) through 4. Always present; 0 is a real value.\n        issue_type:\n          type: string\n          description: >-\n            `bug`, `feature`, `task`, `epic`, `chore`, `gate`, and workspace\n            configured types. Not a closed vocabulary.\n        is_blocked:\n          type: boolean\n          description: >-\n            The persisted readiness projection — true when an open blocking\n            dependency keeps this issue out of the ready set. Derived from the\n            dependency graph; never set by a client.\n\n\n            NOT POPULATED by the /v0 read endpoints today: they hydrate issues\n            without it, so the field is absent from their responses and its\n            absence says nothing about whether an issue is blocked. Use\n            `/v0/beads/ready` or the dependency endpoints to answer that. The\n            field is carried by events-journal snapshots, which is where it is\n            read back, and it is documented here because those snapshots\n            serialize the same type this schema is pinned to.\n        assignee:\n          type: string\n          description: The actor currently holding the issue, if any.\n        owner:\n          type: string\n        estimated_minutes:\n          type: integer\n        created_at:\n          type: string\n          format: date-time\n        created_by:\n          type: string\n        updated_at:\n          type: string\n          format: date-time\n        started_at:\n          type: string\n          format: date-time\n        closed_at:\n          type: string\n          format: date-time\n        close_reason:\n          type: string\n        closed_by_session:\n          type: string\n        lease_expires_at:\n          type: string\n          format: date-time\n          description: Expiry of the current claim's lease, when one is active.\n        heartbeat_at:\n          type: string\n          format: date-time\n        lease_granted_node:\n          type: string\n          description: >-\n            The replica that granted the lease. Empty means the provenance is\n            unknown, which is treated as local.\n        due_at:\n          type: string\n          format: date-time\n        defer_until:\n          type: string\n          format: date-time\n        external_ref:\n          type: string\n        source_system:\n          type: string\n        metadata:\n          type: object\n          additionalProperties: true\n          description: >-\n            Arbitrary caller-supplied JSON object. VALUES MAY BE OF ANY JSON\n            TYPE — string, number, boolean, array or nested object — because\n            typed values enter through the explicit JSON metadata path and\n            persist in older rows. Clients MUST NOT decode this into a\n            string-to-string map; a strict decode fails on the first typed\n            value and takes the whole response with it. That prohibition is on\n            assuming a narrower type when reading THIS document: a profile MAY\n            declare a narrower value schema under the rules in **Profiles**,\n            and a client of that profile may rely on the profile's declaration.\n\n\n            The object-at-top-level shape is the contract every producer and\n            every metadata filter (`metadata_field`, `has_metadata_key`)\n            assumes, but be aware the store validates only that the value is\n            WELL-FORMED JSON: a row created with a non-object (e.g.\n            `bd create --metadata '[1,2]'`) can carry an array or a scalar\n            here. That is a storage-side gap, not licence for a producer to\n            emit one — but a tolerant client should skip such a row rather than\n            fail the whole response.\n        compaction_level:\n          type: integer\n        compacted_at:\n          type: string\n          format: date-time\n        compacted_at_commit:\n          type: string\n        original_size:\n          type: integer\n        labels:\n          type: array\n          items:\n            type: string\n        dependencies:\n          type: array\n          description: >-\n            Dependency edges. Populated in export/interchange contexts; the\n            detail endpoint returns richer entries — see `IssueDetails`.\n          items:\n            $ref: '#/components/schemas/Dependency'\n        comments:\n          type: array\n          items:\n            $ref: '#/components/schemas/Comment'\n        sender:\n          type: string\n        ephemeral:\n          type: boolean\n          description: Ephemeral records are not synced.\n        no_history:\n          type: boolean\n        wisp_type:\n          type: string\n        storage_class:\n          type: string\n          description: >-\n            The record's history/replication contract. Empty means unset, which\n            resolves to ephemeral for wisp-plane records and versioned\n            otherwise.\n        pinned:\n          type: boolean\n        is_template:\n          type: boolean\n        bonded_from:\n          type: array\n          items:\n            $ref: '#/components/schemas/BondRef'\n        await_type:\n          type: string\n          description: Gate condition type.\n        await_id:\n          type: string\n        timeout:\n          type: integer\n          format: int64\n          description: >-\n            NANOSECONDS. This is a Go `time.Duration`, which encodes as an\n            int64 nanosecond count — a pre-existing wire fact, not a choice.\n        waiters:\n          type: array\n          items:\n            type: string\n        source_formula:\n          type: string\n        source_location:\n          type: string\n        mol_type:\n          type: string\n        work_type:\n          type: string\n        event_kind:\n          type: string\n        actor:\n          type: string\n          description: >-\n            For event records, the entity that caused the event. Unrelated to\n            the claiming actor in `ClaimRequest`.\n        target:\n          type: string\n        payload:\n          type: string\n\n    IssueWithCounts:\n      type: object\n      description: >-\n        An `Issue` plus relationship cardinalities. This is the element type of\n        both `/v0/beads/ready` and `/v0/beads/issues`, matching what\n        `bd ready --json` and `bd list --json` emit. Property semantics are\n        documented on `Issue`.\n      x-go-type: types.IssueWithCounts\n      x-go-type-import:\n        name: types\n        path: github.com/steveyegge/beads/internal/types\n      required:\n        [id, title, priority, created_at, updated_at, dependency_count, dependent_count, comment_count]\n      properties:\n        # --- repeated from Issue (see the pinned-schema note above) ---\n        id: { type: string }\n        title: { type: string }\n        description: { type: string }\n        design: { type: string }\n        acceptance_criteria: { type: string }\n        notes: { type: string }\n        spec_id: { type: string }\n        status: { type: string }\n        priority: { type: integer }\n        issue_type: { type: string }\n        is_blocked: { type: boolean }\n        assignee: { type: string }\n        owner: { type: string }\n        estimated_minutes: { type: integer }\n        created_at: { type: string, format: date-time }\n        created_by: { type: string }\n        updated_at: { type: string, format: date-time }\n        started_at: { type: string, format: date-time }\n        closed_at: { type: string, format: date-time }\n        close_reason: { type: string }\n        closed_by_session: { type: string }\n        lease_expires_at: { type: string, format: date-time }\n        heartbeat_at: { type: string, format: date-time }\n        lease_granted_node: { type: string }\n        due_at: { type: string, format: date-time }\n        defer_until: { type: string, format: date-time }\n        external_ref: { type: string }\n        source_system: { type: string }\n        metadata: { type: object, additionalProperties: true }\n        compaction_level: { type: integer }\n        compacted_at: { type: string, format: date-time }\n        compacted_at_commit: { type: string }\n        original_size: { type: integer }\n        labels: { type: array, items: { type: string } }\n        dependencies:\n          type: array\n          items: { $ref: '#/components/schemas/Dependency' }\n        comments:\n          type: array\n          items: { $ref: '#/components/schemas/Comment' }\n        sender: { type: string }\n        ephemeral: { type: boolean }\n        no_history: { type: boolean }\n        wisp_type: { type: string }\n        storage_class: { type: string }\n        pinned: { type: boolean }\n        is_template: { type: boolean }\n        bonded_from:\n          type: array\n          items: { $ref: '#/components/schemas/BondRef' }\n        await_type: { type: string }\n        await_id: { type: string }\n        timeout: { type: integer, format: int64 }\n        waiters: { type: array, items: { type: string } }\n        source_formula: { type: string }\n        source_location: { type: string }\n        mol_type: { type: string }\n        work_type: { type: string }\n        event_kind: { type: string }\n        actor: { type: string }\n        target: { type: string }\n        payload: { type: string }\n        # --- added by IssueWithCounts ---\n        dependency_count:\n          type: integer\n          description: Number of issues this one depends on.\n        dependent_count:\n          type: integer\n          description: Number of issues that depend on this one.\n        comment_count:\n          type: integer\n        parent:\n          type: string\n          description: Parent issue id, computed from the parent-child edge.\n\n    IssueDetails:\n      type: object\n      description: >-\n        An `Issue` with its labels, dependency edges and cardinalities — the\n        body of `GET /v0/beads/issues/{id}`. `dependencies` and `dependents`\n        carry FULL issue objects plus the edge type, not bare edges. Property\n        semantics are documented on `Issue`.\n      x-go-type: types.IssueDetails\n      x-go-type-import:\n        name: types\n        path: github.com/steveyegge/beads/internal/types\n      required: [id, title, priority, created_at, updated_at, revision]\n      properties:\n        # --- repeated from Issue (see the pinned-schema note above) ---\n        id: { type: string }\n        title: { type: string }\n        description: { type: string }\n        design: { type: string }\n        acceptance_criteria: { type: string }\n        notes: { type: string }\n        spec_id: { type: string }\n        status: { type: string }\n        priority: { type: integer }\n        issue_type: { type: string }\n        is_blocked: { type: boolean }\n        assignee: { type: string }\n        owner: { type: string }\n        estimated_minutes: { type: integer }\n        created_at: { type: string, format: date-time }\n        created_by: { type: string }\n        updated_at: { type: string, format: date-time }\n        started_at: { type: string, format: date-time }\n        closed_at: { type: string, format: date-time }\n        close_reason: { type: string }\n        closed_by_session: { type: string }\n        lease_expires_at: { type: string, format: date-time }\n        heartbeat_at: { type: string, format: date-time }\n        lease_granted_node: { type: string }\n        due_at: { type: string, format: date-time }\n        defer_until: { type: string, format: date-time }\n        external_ref: { type: string }\n        source_system: { type: string }\n        metadata: { type: object, additionalProperties: true }\n        compaction_level: { type: integer }\n        compacted_at: { type: string, format: date-time }\n        compacted_at_commit: { type: string }\n        original_size: { type: integer }\n        sender: { type: string }\n        ephemeral: { type: boolean }\n        no_history: { type: boolean }\n        wisp_type: { type: string }\n        storage_class: { type: string }\n        pinned: { type: boolean }\n        is_template: { type: boolean }\n        bonded_from:\n          type: array\n          items: { $ref: '#/components/schemas/BondRef' }\n        await_type: { type: string }\n        await_id: { type: string }\n        timeout: { type: integer, format: int64 }\n        waiters: { type: array, items: { type: string } }\n        source_formula: { type: string }\n        source_location: { type: string }\n        mol_type: { type: string }\n        work_type: { type: string }\n        event_kind: { type: string }\n        actor: { type: string }\n        target: { type: string }\n        payload: { type: string }\n        # --- shape of these three differs from Issue's ---\n        labels:\n          type: array\n          items: { type: string }\n        dependencies:\n          type: array\n          description: Issues this one depends on, each carrying its edge type.\n          items: { $ref: '#/components/schemas/IssueWithDependencyMetadata' }\n        comments:\n          type: array\n          description: >-\n            The issue's comment bodies. Populated only when the request sets\n            `getIssue`'s `include_comments`; absent otherwise, which is what\n            `comment_count` and `comments_omitted` report about.\n          items: { $ref: '#/components/schemas/Comment' }\n        # --- added by IssueDetails ---\n        dependents:\n          type: array\n          description: >-\n            Issues that depend on this one, each carrying its edge type.\n            Populated only when the request sets `getIssue`'s\n            `include_dependents`; absent otherwise, where `dependent_count` is\n            the cardinality.\n          items: { $ref: '#/components/schemas/IssueWithDependencyMetadata' }\n        parent:\n          type: string\n          description: Parent issue id, computed from the parent-child edge.\n        dependency_count:\n          type: integer\n          format: int64\n        dependent_count:\n          type: integer\n          format: int64\n        comment_count:\n          type: integer\n          format: int64\n        comments_omitted:\n          type: boolean\n          description: >-\n            True when `comment_count` is nonzero and `comments` was left out —\n            every such issue on a request that did not set `include_comments`.\n            Without it, an absent `comments` key is ambiguous between \"no\n            comments\" and \"comments not included in this response\".\n        epic_total_children:\n          type: integer\n        epic_closed_children:\n          type: integer\n        epic_closeable:\n          type: boolean\n        revision:\n          type: integer\n          format: int64\n          description: >-\n            The row's optimistic-concurrency token, and THE READ THAT SOURCES A\n            GUARD. Every `expected_version` on this surface is composed from a\n            token some response carried; until this member existed the only\n            responses that carried one were WRITES, so a caller's first guarded\n            write had to be preceded by a write it did not want to make. This\n            is that member, and it is the one the write responses' \"when a read\n            publishes one, this member is what it will agree with\" was written\n            against.\n\n\n            IT IS EQUALITY-ONLY: compare it, never order or interpret it. A\n            change signals the row was mutated since you read it, and nothing\n            more — it is a random value the engine rewrites, not a counter.\n            `ApplyItemResult.revision` states the full rule and this member\n            carries it verbatim.\n\n\n            ITS COVERAGE IS PARTIAL, on the same inherited terms: the token is\n            rewritten by claim, close, unclaim and the generic update path, and\n            NOT by the direct-update paths that rewrite text without touching\n            it. A client needing complete change detection combines it with\n            `updated_at`, `status` and the label set.\n\n\n            It is ALWAYS PRESENT, including as 0 — a legacy row backfilled and\n            not mutated since — because an absent member would be ambiguous\n            between a legacy-zero row and a server that does not publish the\n            token.\n\n\n            DECODE IT AS A 64-BIT INTEGER, for the reason\n            `UpdateIssueRequest.expected_version` spells out: an IEEE-754-double\n            parser corrupts it silently, and the corruption only shows up as a\n            `precondition_failed` on the NEXT request.\n\n\n            IT IS THE ANCHOR ROW'S ALONE. The issues nested under\n            `dependencies` and `dependents` are `IssueWithDependencyMetadata`\n            and carry no token: a caller that means to guard a NEIGHBOUR reads\n            that neighbour with its own `GET /v0/beads/issues/{id}`, and a\n            token lifted off an embedded relation would be one the response\n            never promised was fresh.\n\n\n            IT IS NOT ON THE LIST ROWS, and that is a decision rather than an\n            oversight — see `GET /v0/beads/issues`.\n\n    IssueWithDependencyMetadata:\n      type: object\n      description: >-\n        A full issue plus the type of the dependency edge that led to it.\n        Property semantics are documented on `Issue`.\n      x-go-type: types.IssueWithDependencyMetadata\n      x-go-type-import:\n        name: types\n        path: github.com/steveyegge/beads/internal/types\n      required: [id, title, priority, created_at, updated_at, dependency_type]\n      properties:\n        # --- repeated from Issue (see the pinned-schema note above) ---\n        id: { type: string }\n        title: { type: string }\n        description: { type: string }\n        design: { type: string }\n        acceptance_criteria: { type: string }\n        notes: { type: string }\n        spec_id: { type: string }\n        status: { type: string }\n        priority: { type: integer }\n        issue_type: { type: string }\n        is_blocked: { type: boolean }\n        assignee: { type: string }\n        owner: { type: string }\n        estimated_minutes: { type: integer }\n        created_at: { type: string, format: date-time }\n        created_by: { type: string }\n        updated_at: { type: string, format: date-time }\n        started_at: { type: string, format: date-time }\n        closed_at: { type: string, format: date-time }\n        close_reason: { type: string }\n        closed_by_session: { type: string }\n        lease_expires_at: { type: string, format: date-time }\n        heartbeat_at: { type: string, format: date-time }\n        lease_granted_node: { type: string }\n        due_at: { type: string, format: date-time }\n        defer_until: { type: string, format: date-time }\n        external_ref: { type: string }\n        source_system: { type: string }\n        metadata: { type: object, additionalProperties: true }\n        compaction_level: { type: integer }\n        compacted_at: { type: string, format: date-time }\n        compacted_at_commit: { type: string }\n        original_size: { type: integer }\n        labels: { type: array, items: { type: string } }\n        dependencies:\n          type: array\n          items: { $ref: '#/components/schemas/Dependency' }\n        comments:\n          type: array\n          items: { $ref: '#/components/schemas/Comment' }\n        sender: { type: string }\n        ephemeral: { type: boolean }\n        no_history: { type: boolean }\n        wisp_type: { type: string }\n        storage_class: { type: string }\n        pinned: { type: boolean }\n        is_template: { type: boolean }\n        bonded_from:\n          type: array\n          items: { $ref: '#/components/schemas/BondRef' }\n        await_type: { type: string }\n        await_id: { type: string }\n        timeout: { type: integer, format: int64 }\n        waiters: { type: array, items: { type: string } }\n        source_formula: { type: string }\n        source_location: { type: string }\n        mol_type: { type: string }\n        work_type: { type: string }\n        event_kind: { type: string }\n        actor: { type: string }\n        target: { type: string }\n        payload: { type: string }\n        # --- added by IssueWithDependencyMetadata ---\n        dependency_type:\n          type: string\n          description: >-\n            The edge type, e.g. `blocks`, `parent-child`, `related`,\n            `discovered-from`. Not a closed vocabulary.\n\n    TreeNode:\n      type: object\n      description: >-\n        One node of a walked dependency tree: a full issue plus where the walk\n        reached it. Property semantics for the issue members are documented on\n        `Issue`.\n\n\n        The tree is FLAT. A node's place in it is read from `depth` and\n        `parent_id`, not from nesting, and a subtree is contiguous in `items`.\n      x-go-type: types.TreeNode\n      x-go-type-import:\n        name: types\n        path: github.com/steveyegge/beads/internal/types\n      required: [id, title, priority, created_at, updated_at, depth, parent_id, truncated]\n      properties:\n        # --- repeated from Issue (see the pinned-schema note above) ---\n        id: { type: string }\n        title: { type: string }\n        description: { type: string }\n        design: { type: string }\n        acceptance_criteria: { type: string }\n        notes: { type: string }\n        spec_id: { type: string }\n        status: { type: string }\n        priority: { type: integer }\n        issue_type: { type: string }\n        is_blocked: { type: boolean }\n        assignee: { type: string }\n        owner: { type: string }\n        estimated_minutes: { type: integer }\n        created_at: { type: string, format: date-time }\n        created_by: { type: string }\n        updated_at: { type: string, format: date-time }\n        started_at: { type: string, format: date-time }\n        closed_at: { type: string, format: date-time }\n        close_reason: { type: string }\n        closed_by_session: { type: string }\n        lease_expires_at: { type: string, format: date-time }\n        heartbeat_at: { type: string, format: date-time }\n        lease_granted_node: { type: string }\n        due_at: { type: string, format: date-time }\n        defer_until: { type: string, format: date-time }\n        external_ref: { type: string }\n        source_system: { type: string }\n        metadata: { type: object, additionalProperties: true }\n        compaction_level: { type: integer }\n        compacted_at: { type: string, format: date-time }\n        compacted_at_commit: { type: string }\n        original_size: { type: integer }\n        labels: { type: array, items: { type: string } }\n        dependencies:\n          type: array\n          items: { $ref: '#/components/schemas/Dependency' }\n        comments:\n          type: array\n          items: { $ref: '#/components/schemas/Comment' }\n        sender: { type: string }\n        ephemeral: { type: boolean }\n        no_history: { type: boolean }\n        wisp_type: { type: string }\n        storage_class: { type: string }\n        pinned: { type: boolean }\n        is_template: { type: boolean }\n        bonded_from:\n          type: array\n          items: { $ref: '#/components/schemas/BondRef' }\n        await_type: { type: string }\n        await_id: { type: string }\n        timeout: { type: integer, format: int64 }\n        waiters: { type: array, items: { type: string } }\n        source_formula: { type: string }\n        source_location: { type: string }\n        mol_type: { type: string }\n        work_type: { type: string }\n        event_kind: { type: string }\n        actor: { type: string }\n        target: { type: string }\n        payload: { type: string }\n        # --- added by TreeNode ---\n        depth:\n          type: integer\n          description: >-\n            How far the walk was from the root when it reached this node. The\n            root is 0.\n\n\n            For a `both` walk each half measures its own depth from the root, so\n            a depth does not say which half a node came from and nothing else\n            on the node does either.\n        parent_id:\n          type: string\n          description: >-\n            The node this one was reached FROM, empty on the root. A node is\n            reached ONCE per walk, by the first path that got there, so a shared\n            child of two parents names only the first of them here and the\n            second has no visible edge to it.\n        edge_from_parent:\n          type: string\n          description: >-\n            The dependency type of the edge that led here, e.g. `blocks`,\n            `parent-child`, `discovered-from`. Absent on the root. Not a closed\n            vocabulary.\n        truncated:\n          type: boolean\n          description: >-\n            ALWAYS FALSE. No implementation sets it: a node beyond `max_depth`\n            is ABSENT from the answer rather than present and flagged, so a\n            client cannot tell a tree that ended from one that was cut. It is\n            documented rather than removed because it is on the wire today.\n\n    Dependency:\n      type: object\n      description: A dependency edge between two issues.\n      x-go-type: types.Dependency\n      x-go-type-import:\n        name: types\n        path: github.com/steveyegge/beads/internal/types\n      required: [issue_id, depends_on_id, type, created_at]\n      properties:\n        id: { type: string }\n        issue_id: { type: string }\n        depends_on_id: { type: string }\n        type:\n          type: string\n          description: Edge type. Not a closed vocabulary.\n        created_at: { type: string, format: date-time }\n        created_by: { type: string }\n        metadata:\n          type: string\n          description: Free-form edge annotation. A STRING, unlike `Issue.metadata`.\n        thread_id: { type: string }\n\n    Comment:\n      type: object\n      x-go-type: types.Comment\n      x-go-type-import:\n        name: types\n        path: github.com/steveyegge/beads/internal/types\n      required: [id, issue_id, author, text, created_at]\n      properties:\n        id: { type: string }\n        issue_id: { type: string }\n        author: { type: string }\n        text: { type: string }\n        created_at: { type: string, format: date-time }\n\n    AddCommentRequest:\n      type: object\n      additionalProperties: false\n      required: [author, text]\n      description: >-\n        One comment to append. The issue is named by the path, so it is not a\n        member here: a body carrying it too would give one request two spellings\n        of one anchor and a question about what to do when they disagree.\n      properties:\n        author:\n          type: string\n          minLength: 1\n          maxLength: 256\n          pattern: '^[^\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029]+$'\n          description: >-\n            Who is signing the comment. CALLER-ASSERTED, and not the\n            authenticated principal — see the operation description.\n\n\n            Trimmed of surrounding space, then refused when the result is empty,\n            when it exceeds 256 bytes or 255 characters (the storage column),\n            or when it carries a control character. The bounds and the character\n            rule are `actor`'s, unchanged, because the value lands in a column\n            of the same width that every renderer of the thread prints, where an\n            unfiltered C1 introducer is an escape-sequence payload.\n        text:\n          type: string\n          description: >-\n            The comment body, stored VERBATIM: newlines, surrounding space and\n            unicode all survive, and nothing trims the value that lands in the\n            row.\n\n\n            NO LENGTH BOUND AND NO CHARACTER RULE, unlike `author` beside it,\n            and both absences are the column: this one is `LONGTEXT` rather than\n            a 255-character field, and a comment that is a stack trace or a diff\n            is an ordinary comment. The only cap is the 1 MiB every body on this\n            surface shares.\n\n\n            BOTH PLANES AGREE ABOUT THAT, which is worth stating because they\n            did not. `wisp_comments.text` was left `TEXT` — 65535 bytes — when\n            the durable column was widened, so a comment past that limit wrote\n            fine against an issue and failed against a wisp, on an operation\n            that resolves its anchor across both planes deliberately. A caller\n            therefore could not know which side of the bound it was on until the\n            write failed. The ephemeral column is widened to match, so this\n            member's bound is one number rather than two.\n\n\n            Blank after trimming is a `400` — a comment of nothing but\n            whitespace carries no information and is almost always a shell\n            quoting accident — and blankness is judged on a TRIMMED COPY while\n            the stored value is untrimmed, so a comment that merely begins with\n            a newline is a comment.\n\n    BondRef:\n      type: object\n      description: A constituent of a compound molecule.\n      x-go-type: types.BondRef\n      x-go-type-import:\n        name: types\n        path: github.com/steveyegge/beads/internal/types\n      required: [source_id, bond_type]\n      properties:\n        source_id: { type: string }\n        bond_type: { type: string }\n        bond_point: { type: string }\n\n    Statistics:\n      type: object\n      description: >-\n        Workspace summary counts. Two of them are DEPENDENCY-AWARE and two are\n        structurally always zero; both facts are stated on the properties\n        themselves, because every number here is the same JSON type and nothing\n        else on the wire distinguishes them.\n\n\n        This is the struct `bd status --json` marshals under `summary`, pinned\n        so the two surfaces are one compatibility domain.\n      x-go-type: types.Statistics\n      x-go-type-import:\n        name: types\n        path: github.com/steveyegge/beads/internal/types\n      required:\n        - total_issues\n        - open_issues\n        - in_progress_issues\n        - closed_issues\n        - blocked_issues\n        - deferred_issues\n        - ready_issues\n        - pinned_issues\n        - epics_eligible_for_closure\n        - average_lead_time_hours\n      properties:\n        total_issues:\n          type: integer\n          description: >-\n            Every row in the workspace-wide answer's scan, INCLUDING closed and\n            pinned ones. The status counts below do not sum to it: a row whose\n            status is none of the four falls into no bucket, and `pinned_issues`\n            counts a flag that overlaps all of them.\n        open_issues:\n          type: integer\n        in_progress_issues:\n          type: integer\n        closed_issues:\n          type: integer\n        deferred_issues:\n          type: integer\n        blocked_issues:\n          type: integer\n          nullable: true\n          description: >-\n            Rows the dependency graph blocks — the transitive is_blocked flag,\n            NOT the count of rows whose status is `blocked`. An open row with an\n            unfinished blocker is counted here and its status is still `open`.\n\n\n            NULL when `skip_blocked` was honored, always together with\n            `ready_issues`. The two are nil together or populated together;\n            there is no state in which one is knowable and the other is not.\n\n\n            When `assignee` is set this is a different number: the count of that\n            actor's rows whose STATUS is `blocked`, and never null.\n        ready_issues:\n          type: integer\n          nullable: true\n          description: >-\n            ARITHMETIC, not a query: `open_issues` minus `blocked_issues`,\n            clamped at zero. It is NOT the cardinality of `GET /v0/beads/ready`,\n            which applies type exclusions, the deferral window and a limit that\n            none of this touches.\n\n\n            NULL under the same conditions as `blocked_issues`.\n\n\n            When `assignee` is set this is the real ready-work count for that\n            actor, and never null.\n        pinned_issues:\n          type: integer\n          description: >-\n            Rows carrying the pinned flag, overlapping every status bucket.\n            Always 0 when `assignee` is set: that answer tallies the five\n            statuses and nothing else.\n        epics_eligible_for_closure:\n          type: integer\n          description: >-\n            ALWAYS 0. No implementation computes it, on any backend or either\n            surface. It is documented rather than dropped because this schema is\n            pinned to the struct both surfaces marshal, and a caller reading a 0\n            here is reading an absent computation rather than an answer.\n        average_lead_time_hours:\n          type: number\n          description: ALWAYS 0, for the reason above.\n    # The two cycle schemas are pinned like the seven above, and to the same end,\n    # but to the canonical types in `issueops` rather than in `internal/types`:\n    # the role's own result IS the CLI's `--json` element, so `bd dep cycles`\n    # and this operation cannot drift apart. TestWireTagBijection covers them on\n    # the same terms and reads each schema's declared import path.\n\n    CycleMember:\n      type: object\n      description: >-\n        One node on a dependency cycle.\n\n\n        `id` is always present, and its presence is what proves the node is on\n        the cycle. `issue` is the row behind it, and is ABSENT — never null —\n        when this workspace holds no record for that id: a target in another\n        repository's namespace, an `external:` reference, or a row whose edges\n        outlived it. That absence means the node cannot be DESCRIBED here, never\n        that it is not really on the cycle.\n\n\n        `issue` is spelled as a bare `$ref` with no sibling keywords, following\n        the codegen note at the top of this document.\n      x-go-type: issueops.CycleMember\n      x-go-type-import:\n        name: issueops\n        path: github.com/steveyegge/beads/issueops\n      required: [id]\n      properties:\n        id:\n          type: string\n        issue:\n          $ref: '#/components/schemas/Issue'\n\n    Cycle:\n      type: object\n      description: >-\n        One circular blocking dependency: its members in EDGE ORDER, so\n        `members[i]` blocks on `members[i+1]` and the last member blocks on the\n        first. The closing edge is implied and is not repeated as a final member.\n\n\n        The rotation is canonical — the lowest id comes first — which is what\n        makes two snapshots of an unchanged workspace comparable.\n      x-go-type: issueops.Cycle\n      x-go-type-import:\n        name: issueops\n        path: github.com/steveyegge/beads/issueops\n      required: [members, partial]\n      properties:\n        members:\n          type: array\n          description: The nodes, in edge order, starting at the lowest id. Never empty.\n          items:\n            $ref: '#/components/schemas/CycleMember'\n        partial:\n          type: boolean\n          description: >-\n            True when at least one member has no `issue`. It is always present,\n            including when false: a consumer must be able to read \"this path is\n            complete\" from the answer rather than from the absence of a key.\n\n\n            `members` is complete either way. This flag says the DESCRIPTIONS\n            beside the ids are not.\n\n    # Pinned to `issueops` for the reason the cycle pair is: the role's own\n    # result IS what the CLI renders, so `bd list`'s decoration and this\n    # operation cannot drift apart. TestWireTagBijection covers it on the same\n    # terms and reads this schema's declared import path.\n    IssueBlocking:\n      type: object\n      description: >-\n        One issue's derived blocking decoration.\n\n\n        `blocked_by` and `blocks` are ASCENDING BY ID with repeats collapsed,\n        and both are always present — an empty array, never null and never\n        absent, so a client reads \"nothing blocks this\" from the answer rather\n        than from a missing key. `parent` is absent when the issue has none and\n        when the parent it has is closed.\n      x-go-type: issueops.IssueBlocking\n      x-go-type-import:\n        name: issueops\n        path: github.com/steveyegge/beads/issueops\n      required: [id, blocked_by, blocks]\n      properties:\n        id:\n          type: string\n          description: The annotated id, spelled exactly as the request spelled it.\n        blocked_by:\n          type: array\n          description: >-\n            The OPEN issues this one is blocked by: the targets of its `blocks`\n            edges whose own status is not closed.\n          items:\n            type: string\n        blocks:\n          type: array\n          description: >-\n            The issues this one blocks. Empty when this issue is itself closed,\n            which is the same rule `blocked_by` applies from the other end.\n          items:\n            type: string\n        parent:\n          type: string\n          description: >-\n            This issue's parent id. AT MOST ONE is reported; where an issue\n            carries several `parent-child` edges, which one appears here is not\n            specified. A client that needs every structural edge reads them from\n            `GET /v0/beads/dependencies`.\n\n    # ---------------------------------------------------------------------\n    # Envelope schemas. Almost all of these have no canonical Go type behind\n    # them: they are new wire surface, generated normally, and the generated\n    # type is the only implementation — so there is nothing for them to drift\n    # against.\n    #\n    # `EventRecord`, further down this region, is the exception and is pinned\n    # to `eventsjournal.Record`: the journal's published envelope is shared with\n    # `bd events tail`, so it HAS a canonical implementation to drift against.\n    # TestWireTagBijection's `pinnedSchemas` is the authority on which schemas\n    # are pinned; do not read this region's position as the answer.\n    # ---------------------------------------------------------------------\n\n    Health:\n      type: object\n      required: [status]\n      properties:\n        status:\n          type: string\n          enum: [ok]\n\n    ContextResponse:\n      type: object\n      description: >-\n        The server's identity handshake. Every member is a deliberate,\n        permanent choice; the field set is an allowlist frozen by a test that\n        checks it against BOTH this document and the generated Go struct, so a\n        field cannot arrive here as a side effect of the server's configuration\n        growing one. In particular the workspace's sync remote is EXCLUDED, in\n        this and every future version, because remote URLs routinely embed\n        credentials — as are the database bind host/port (advertising them\n        invites clients to bypass this API and dial the database directly) and\n        the loopback/non-loopback bind mode.\n      required:\n        [api_version, bd_version, schema_version, backend, dolt_mode, database,\n         beads_dir, repo_root, project_id, capabilities]\n      properties:\n        api_version:\n          type: string\n          description: The path major this server serves. `v0` for this document.\n        bd_version:\n          type: string\n          description: >-\n            The release version of the serving binary. The only field a client\n            may compare as a version, and only for behavioral changes tied to\n            a release.\n        schema_version:\n          type: integer\n          description: >-\n            The shared JSON schema version — the same constant the CLI's stdout\n            JSON envelope reports. Diagnostic only: it can move for CLI-only\n            reasons with no HTTP wire change, so clients MUST NOT branch on it.\n        backend:\n          type: string\n          description: Storage backend name.\n        dolt_mode:\n          type: string\n          description: Which storage mode this workspace is served from.\n        database:\n          type: string\n          description: Logical database name (not a host or a DSN).\n        beads_dir:\n          type: string\n          description: >-\n            Absolute path of the served workspace's `.beads` directory. A host\n            path, kept because it is the single-workspace server's only\n            workspace-identity handshake; disclosing it to network peers is\n            part of what an operator accepts when binding beyond loopback.\n        repo_root:\n          type: string\n          description: Absolute path of the served repository root. See `beads_dir`.\n        project_id:\n          type: string\n          description: Logical project identifier.\n        capabilities:\n          type: array\n          description: >-\n            The tokens this server advertises: the OPERATIONS it implements,\n            derived from its route table, and the server-wide BEHAVIORS it\n            enforces. v0's operation vocabulary is `ready.list`, `ready.count`,\n            `issues.list`, `issues.query`, `issues.count`, `issues.get`,\n            `issues.related`, `issues.create`, `issues.addComment`,\n            `issues.batchClose`,\n            `issues.claim`, `issues.claimNext`, `issues.release`,\n            `issues.close`, `issues.reopen`, `issues.update`,\n            `issues.sweep`, `issues.delete`, `issues.batchCreate`,\n            `issues.batchApply`,\n            `stats.get`, `config.list`, `config.get`, `config.set`,\n            `config.unset`, `dependencies.cycles`,\n            `dependencies.list`, `dependencies.count`,\n            `dependencies.blocking`, `dependencies.tree`,\n            `dependencies.add`, `dependencies.remove`,\n            `memories.list`, `memories.get`, `memories.remember`,\n            `memories.forget`, `events.list`, `events.watch`,\n            `issues.casMetadata`; the one behavior token is `project.enforce`,\n            which announces that a `Bd-Project-Id` stamp for the wrong workspace\n            is refused here rather than silently ignored. The list grows\n            additively, and an operation never appears here unless it is fully\n            implemented. This is how a client checks for an operation or a\n            behavior — never the version string.\n\n\n            THIS LIST IS BUILD-LEVEL, NOT WORKSPACE-LEVEL. It says which\n            operations this binary serves, and for every entry but two that is\n            the whole answer. `events.list` and `events.watch` are the\n            exceptions: the durable events journal is a per-workspace setting\n            that is OFF by default, so a server that advertises them may still\n            refuse every request to both with 409 `events_journal_disabled` —\n            correctly, because the operations exist and the workspace has no\n            journal. A consumer of either MUST treat the capability as \"this\n            server speaks it\" and the 409 as \"not on this workspace\", and must\n            not read the capability as a promise that records will arrive.\n          items:\n            type: string\n\n    IssueCount:\n      type: object\n      required: [total]\n      description: >-\n        The size of a matching set, and its buckets when `group_by` asked for\n        them. It carries no items and no cursor: this is a number about a set,\n        and the operations that return rows are `GET /v0/beads/issues` and\n        `GET /v0/beads/issues:query`.\n\n\n        ONE SCHEMA FOR BOTH SHAPES, because the grouped answer is the scalar\n        answer plus one member rather than a different answer. See the\n        operation's own description for why that is one operation and not two.\n\n\n        It is NOT `x-go-type`-pinned, for `ReadyCount`'s reason: there is no\n        canonical Go struct whose JSON encoding is this contract.\n      properties:\n        total:\n          type: integer\n          format: int64\n          description: >-\n            How many issues match. Never negative; `0` when nothing matches,\n            which is a 200 rather than a 404 — a question about a set has an\n            answer even when the set is empty, and a client polling for work\n            would otherwise have to classify an error to read a zero.\n\n\n            Under `group_by` this is still the cardinality of the WHOLE matching\n            set and NOT the sum of `groups`. The two differ for `label`, whose\n            buckets overlap; see the operation description.\n        groups:\n          type: object\n          additionalProperties:\n            type: integer\n          description: >-\n            Bucket key to cardinality, PRESENT exactly when the request carried\n            `group_by` and ABSENT otherwise. That absence is the answer to \"you\n            did not ask for buckets\"; an empty OBJECT is the answer to \"nothing\n            matched\", and the two are deliberately different — a client must be\n            able to tell a scalar count from a grouped count of an empty set\n            without re-reading its own request.\n\n\n            Buckets with no rows are absent rather than present at zero. The\n            dimensions are open-ended — any assignee, any label, any custom\n            status — so there is no closed set of keys to enumerate and a client\n            reads an absent key as zero. The KEY normalization is part of the\n            contract and is documented on `group_by`.\n\n    ReadyCount:\n      type: object\n      required: [total]\n      properties:\n        total:\n          type: integer\n          format: int64\n          description: >-\n            How many items `GET /v0/beads/ready` would return for these filters\n            with `limit=0`. Never negative; 0 when nothing is ready, which is a\n            200 rather than a 404 — a question about a set has an answer even\n            when the set is empty.\n      description: >-\n        The size of a ready set. It carries no items, no `has_more` and no\n        cursor: this is a number about a set, and the operation that returns\n        rows is `GET /v0/beads/ready`.\n\n\n        It is NOT `x-go-type`-pinned, unlike the seven schemas above, and that\n        is a decision rather than an omission: those seven are pinned because a\n        canonical Go struct's JSON encoding IS the contract and a second wire\n        struct would let the CLI's `--json` drift from these bodies. There is\n        no canonical struct here — the CLI publishes this number inside its own\n        stdout envelope's `pagination` member, which is not a wire type — so\n        pinning would weld this body to a CLI presentation type instead of\n        preventing a drift.\n\n    DeleteIssuesRequest:\n      type: object\n      additionalProperties: false\n      required: [ids]\n      description: >-\n        Which beads to erase, and what to do about the beads that point at\n        them. There is no predicate here — no status, no cutoff, no glob — and\n        that absence is the reason this operation needs no require-a-filter\n        gate: a caller cannot spell \"everything\" without typing every id.\n\n\n        `additionalProperties: false`, so an unknown member is a `400` naming\n        the member. On this operation a silently ignored member is the\n        difference between orphaning a dependent and deleting it.\n      properties:\n        ids:\n          type: array\n          minItems: 1\n          maxItems: 1000\n          description: >-\n            The beads to delete, exact ids, in either plane. DUPLICATES\n            COLLAPSE. An empty array is a `400` rather than a no-op — a caller\n            whose id list came out empty because its own construction broke\n            would read \"deleted 0\" and conclude the workspace was already\n            clean.\n\n\n            The cap is on the REQUEST rather than on what a cascade expands to:\n            the whole delete is one transaction, so the practical bound is the\n            backend's write timeout and no number here can promise it.\n          items:\n            type: string\n        actor:\n          type: string\n          minLength: 1\n          maxLength: 256\n          pattern: '^[^\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029]+$'\n          description: >-\n            Caller-asserted attribution, under the same rules as\n            `SweepRequest`'s `actor`: trimmed, refused when empty after\n            trimming, over 256 BYTES, or carrying any control character.\n            Optional — a deleted bead leaves no row to attribute the deletion\n            on — but it does reach the SURVIVING beads whose text this\n            operation rewrites, so a workspace that cares who rewrote a\n            description sends one.\n        cascade:\n          type: boolean\n          default: false\n          description: >-\n            Also delete the transitive closure of everything that depends on\n            the named beads. With `cascade` there is nothing left outside the\n            set to orphan, so it makes `force` moot rather than conflicting\n            with it: a request carrying both behaves as `cascade` and\n            `orphaned` comes back empty.\n        force:\n          type: boolean\n          default: false\n          description: >-\n            Delete the named beads and leave their dependents ORPHANED,\n            reported in `orphaned`. Without it and without `cascade`, a named\n            bead with a dependent the request did not name is refused.\n\n\n            It defaults FALSE, which is the guarded mode, and the default is\n            the protection. Authentication here is a deployment posture, and\n            where it is configured it is a single shared bearer that admits a\n            client to the WHOLE surface — it names no principal this operation\n            could weigh and grants no narrower right — so an omitted member\n            must not silently choose the answer that changes another bead's\n            graph.\n        dry_run:\n          type: boolean\n          default: false\n          description: >-\n            Report what the deletion WOULD do and change nothing. The counts\n            and BOTH refusals are the ones the real request would produce,\n            computed against the same snapshot, and nothing is recorded in\n            history either.\n        expected_version:\n          type: integer\n          format: int64\n          description: >-\n            Requires the named bead's revision to equal this value before\n            anything is erased. A miss refuses the whole request with\n            `409 precondition_failed` and deletes NOTHING —\n            `UpdateIssueRequest.expected_version`'s contract, on the operation\n            where being wrong about which row you are looking at cannot be\n            undone.\n\n\n            IT REQUIRES A SINGLE-ID REQUEST. Sending it beside more than one\n            DISTINCT id is a `400` naming this member, refused before anything\n            is read. One token cannot describe two rows: the version space is\n            per-row, so checking one number against a list would pass by\n            coincidence for a list of never-written rows — every one of them\n            holds 0 — and fail forever for a list whose rows have since\n            diverged. A guard that passes by coincidence and a guard nobody can\n            satisfy are one defect seen from two sides. Delete one bead per\n            guarded request; the per-id shape a batch would need is a token PER\n            id, which is a different request type.\n\n\n            DUPLICATES COLLAPSE FIRST, so `{\"ids\":[\"be-1\",\"be-1\"],\n            \"expected_version\":N}` names one bead and is legal. The refusal\n            counts DISTINCT ids, not mentions, exactly as the library surface\n            does.\n\n\n            NEITHER `cascade` NOR `force` BYPASSES IT. Both bypass POLICY — the\n            dependents guard — and never a precondition. Under `cascade` the\n            guard still covers only the NAMED bead: the closure is resolved\n            inside the deleting transaction, so a matching token promises the\n            row is the one you read and promises nothing about how far the\n            closure has grown since. A caller that needs the closure itself\n            pinned wants `dry_run` first.\n\n\n            IT GUARDS LIFECYCLE STATE, NOT THE GRAPH. The token is reminted by\n            status, assignee and started-at writes and deliberately not by\n            label, dependency or rename writes, so a match does not promise the\n            bead's edges are the ones you saw.\n\n\n            The token is the `revision` a lifecycle write answers with, and the\n            one `GET /v0/beads/issues/{id}` publishes — which is where a delete\n            guard should seed itself, since reading the bead before erasing it\n            is the only way to be sure it is the bead you meant. DECODE IT AS A\n            64-BIT INTEGER, for the reason\n            `UpdateIssueRequest.expected_version` spells out.\n\n    DeleteIssuesResult:\n      type: object\n      required: [dry_run, deleted, dependencies, labels, events, references_updated]\n      description: >-\n        What one delete did. Every number describes the SAME snapshot, because\n        the guard, the deletion and the reference rewrite ran in one\n        transaction.\n\n\n        It is NOT `x-go-type`-pinned, for the reason `SweepResult` is not:\n        there is no canonical Go struct whose JSON encoding is this contract.\n        `bd delete --json` publishes these numbers under its own per-shape keys\n        (`deleted_count`, `dependencies_removed`, and a scalar `deleted` on the\n        single-id form), which are a stdout presentation rather than a wire\n        type, so pinning would weld this body to one of them.\n      properties:\n        dry_run:\n          type: boolean\n          description: >-\n            Echoes the request, so a result carries whether its numbers\n            describe beads that are gone or beads that would go.\n        deleted:\n          type: integer\n          description: >-\n            How many beads were deleted, or under `dry_run` would be. Under\n            `cascade` this counts the whole closure, so it is normally larger\n            than `ids` and it — not the request length — is the number to show.\n        dependencies:\n          type: integer\n          description: >-\n            Dependency edge rows removed with them, in either direction.\n            Reported because a delete's visible effect is much larger than its\n            bead count.\n        labels:\n          type: integer\n          description: Label rows removed with the deleted beads.\n        events:\n          type: integer\n          description: Event rows removed with the deleted beads.\n        references_updated:\n          type: integer\n          description: >-\n            How many SURVIVING beads had their text rewritten — beads, not\n            occurrences. Always 0 under `dry_run`, because a preview rewrites\n            nothing.\n        orphaned:\n          type: array\n          description: >-\n            The surviving beads that depended on something this request\n            deleted, in ascending id order. Present exactly when the request\n            carried `force` without `cascade`, which is the only mode in which\n            orphaning is possible; absent otherwise.\n\n\n            DIRECT dependents only. A bead two edges away lost no edge.\n          items:\n            type: string\n\n    SweepRequest:\n      type: object\n      additionalProperties: false\n      required: [tier]\n      description: >-\n        Which closed beads to clear. The predicate is FIXED at \"closed beads of\n        one tier\" and the two narrowing members only narrow it: there is no\n        status, no assignee, no label and no free-text query here, because\n        every one of those would be another way to spell a destructive\n        selection that a caller could get subtly wrong.\n\n\n        `additionalProperties: false`, so an unknown member is a `400` naming\n        the member — the same posture the query-parameter rule takes, and for\n        the same reason: on this operation a silently ignored narrowing term\n        widens what is erased.\n      properties:\n        tier:\n          type: string\n          enum: [ephemeral, durable]\n          description: >-\n            Which plane to clear. `ephemeral` is the wisp tier (`bd purge`) and\n            `durable` is the issue tier (`bd prune`). The two are DISJOINT: a\n            sweep of one can never touch a bead of the other. Required, with no\n            default — a caller handed the wrong tier has nothing to notice\n            until the beads are gone.\n        actor:\n          type: string\n          minLength: 1\n          maxLength: 256\n          pattern: '^[^\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029]+$'\n          description: >-\n            Caller-asserted attribution for wherever the backend records it,\n            under the same rules and for the same reasons as `ClaimRequest`'s\n            `actor`: trimmed, refused when empty after trimming, over 256\n            BYTES, or carrying any control character. Optional — a deleted bead\n            leaves no row to attribute the deletion on.\n        closed_before:\n          type: string\n          format: date-time\n          description: >-\n            Keep only beads closed STRICTLY BEFORE this instant (RFC 3339). A\n            bead closed exactly at it is kept, which is the half-open interval\n            every other time bound on this surface uses. `bd prune\n            --older-than 30d` resolves the duration itself and sends the\n            resulting instant.\n        pattern:\n          type: string\n          description: >-\n            Keep only beads whose id matches this shell glob (`*`, `?`,\n            `[...]`, `\\` escapes; `*` also crosses `-` and `.`, since an id is\n            not a path). Absent matches every bead in the tier. A MALFORMED\n            glob is a `400`, never a pattern that matches nothing.\n        protect_referenced:\n          type: boolean\n          default: true\n          description: >-\n            Skip candidates whose id is CITED — as a literal, at word\n            boundaries — in the description, notes or comments of any bead that\n            is not done, so a decision trail a live bead still points at is not\n            deleted out from under it.\n\n\n            It DEFAULTS ON here, unlike the library default, and that is\n            deliberate. This is the only destructive operation on the surface,\n            and the bearer a deployment may configure is not a per-caller\n            right: one shared token admits a client to everything published\n            here, so being authenticated says nothing about whether this\n            particular deletion was meant. A caller that omits the member must\n            therefore not get weaker protection than the operator typing `bd\n            prune`, which protects unless `--ignore-references`. The inverse —\n            opt OUT locally, opt IN remotely — is the shape that lets a stray\n            request delete a decision trail nothing brings back.\n\n\n            It costs a full scan of the not-done set and its comments. Sending\n            `protect_referenced: false` buys the cheaper sweep, and asking for\n            it explicitly is the point: that is the request that should be the\n            deliberate one.\n        dry_run:\n          type: boolean\n          default: false\n          description: >-\n            Report what the sweep WOULD do and delete nothing. The counts, the\n            skips and the refusals are the same ones the real sweep would\n            produce, computed against the same snapshot — and nothing is\n            recorded in history either.\n\n    SweepResult:\n      type: object\n      required: [dry_run, swept, dependencies, labels, events, skipped]\n      description: >-\n        What one sweep did. Every number describes the SAME snapshot, because\n        the selection and the deletion ran in one transaction.\n\n\n        It is NOT `x-go-type`-pinned, for the reason `ReadyCount` is not: there\n        is no canonical Go struct whose JSON encoding is this contract. The CLI\n        publishes these numbers under its own per-command keys (`purged_count`,\n        `pruned_count`), which are a stdout presentation rather than a wire\n        type, so pinning would weld this body to one of them.\n      properties:\n        dry_run:\n          type: boolean\n          description: >-\n            Echoes the request, so a result carries whether its numbers\n            describe beads that are gone or beads that would go.\n        swept:\n          type: integer\n          description: How many beads were deleted, or under `dry_run` would be.\n        dependencies:\n          type: integer\n          description: >-\n            Dependency edge rows removed with them. Reported because a sweep's\n            visible effect is much larger than its bead count.\n        labels:\n          type: integer\n          description: Label rows removed with the swept beads.\n        events:\n          type: integer\n          description: Event rows removed with the swept beads.\n        skipped:\n          $ref: '#/components/schemas/SweepSkips'\n        referenced_ids:\n          type: array\n          description: >-\n            A BOUNDED SAMPLE of the ids `skipped.referenced` counts — at most\n            100, in the order the candidate query returned them. It is a\n            sample, not the set: compare its length against 100 to tell a\n            truncated one from a complete one. Absent when nothing was\n            protected.\n          items:\n            type: string\n\n    SweepSkips:\n      type: object\n      required:\n        - pinned\n        - referenced\n        - not_closed\n        - unknown_closed_at\n        - closed_at_or_after_cutoff\n        - unreadable\n      description: >-\n        The candidates a sweep declined to delete, bucketed by WHY. They are\n        separate counters rather than one number because they mean different\n        things: the first two are PROTECTIONS, and the last four are the sweep\n        declining to trust its own input.\n      properties:\n        pinned:\n          type: integer\n          description: >-\n            Candidates protected by the pinned flag. No request member\n            overrides it — a caller who wants a pinned bead gone unpins it\n            first.\n        referenced:\n          type: integer\n          description: >-\n            Candidates protected by `protect_referenced`. Always 0 when that\n            member is false or absent, so a 0 read without having asked says\n            nothing about whether beads are cited.\n        not_closed:\n          type: integer\n          description: >-\n            Candidates the tier query returned that the recheck found were not\n            closed. A NON-ZERO VALUE HERE IS A DEFENSE FIRING, not a normal\n            outcome: the query asked for exactly the beads this excludes.\n        unknown_closed_at:\n          type: integer\n          description: Closed candidates carrying no close timestamp. See `not_closed`.\n        closed_at_or_after_cutoff:\n          type: integer\n          description: >-\n            Candidates whose close timestamp did not satisfy `closed_before`.\n            See `not_closed`.\n        unreadable:\n          type: integer\n          description: >-\n            Rows the tier query returned as nothing at all. A defense of the\n            same kind, on a shape rather than a value.\n\n    ReadyPage:\n      type: object\n      required: [items, has_more]\n      properties:\n        items:\n          type: array\n          description: Empty array (never null) when nothing is ready.\n          items:\n            $ref: '#/components/schemas/IssueWithCounts'\n        has_more:\n          type: boolean\n          description: >-\n            True when the limit truncated the result. There is no cursor on\n            this operation; narrow the filters or raise the limit.\n\n    CyclesPage:\n      type: object\n      required: [items, has_more]\n      properties:\n        items:\n          type: array\n          description: >-\n            Empty array (never null) when the workspace has no cycles. Its\n            LENGTH is the total: a cycle whose members this workspace cannot\n            describe is still counted here, so the number cannot shrink because\n            a row went missing.\n          items:\n            $ref: '#/components/schemas/Cycle'\n        has_more:\n          type: boolean\n          description: >-\n            Always false in v0: this operation takes no limit, so the report is\n            never truncated. Present so that adding a bound later is additive.\n\n    DependencyTreePage:\n      type: object\n      required: [items, has_more]\n      properties:\n        items:\n          type: array\n          description: >-\n            The walked nodes in DEPTH-FIRST PRE-ORDER: a node appears before\n            every node it led to, and a subtree is contiguous. Never null.\n\n\n            It is empty only when a `status` filter matched nothing — the root\n            is kept in a filtered answer solely as an ancestor of a match, never\n            for its own sake. Without `status` the root is always the first\n            element, which is what lets a client tell \"this issue depends on\n            nothing\" from \"this issue is not there\" (a 404).\n          items:\n            $ref: '#/components/schemas/TreeNode'\n        has_more:\n          type: boolean\n          description: >-\n            Always false in v0: this operation takes no limit, so the walk is\n            bounded by `max_depth` rather than truncated after the fact. Present\n            so that adding a bound later is additive.\n\n    IssuesPage:\n      type: object\n      required: [items, has_more]\n      properties:\n        items:\n          type: array\n          description: Empty array (never null) when nothing matches.\n          items:\n            $ref: '#/components/schemas/IssueWithCounts'\n        has_more:\n          type: boolean\n        next_cursor:\n          type: string\n          description: >-\n            Present if and only if `has_more` is true. Pass it back verbatim as\n            `cursor` to fetch the next page. Opaque and server-private.\n\n    EventRecord:\n      type: object\n      description: >-\n        One record of the durable events journal: a single committed issue\n        mutation, as a replaying consumer receives it.\n\n\n        THIS IS THE CLI'S RECORD. It is pinned to the same Go struct\n        `bd events tail` and `bd events export` marshal one per line, so the\n        JSONL a consumer reads from stdout and the elements of an\n        `EventsPage.records` array are the same bytes for the same row. A\n        committed golden fixture pins that encoding field by field.\n\n\n        `issue` is the full issue state AFTER the mutation and is ALWAYS\n        PRESENT, carrying the literal `null` on a delete — where there is no\n        surviving row to describe. That is the one place this document's general\n        \"treat null as absent\" rule does not apply to a member's meaning: a\n        consumer must be able to tell a delete from a payload the server failed\n        to record, so the member is emitted rather than omitted. `dep` and\n        `comment` are the opposite: they are ABSENT on the ops that have no such\n        half, because their absence says the op has none, not that one was\n        empty.\n      x-go-type: eventsjournal.Record\n      x-go-type-import:\n        name: eventsjournal\n        path: github.com/steveyegge/beads/internal/eventsjournal\n      required: [seq, ts, op, issue_id, issue]\n      properties:\n        seq:\n          type: integer\n          format: int64\n          description: >-\n            Counter-assigned inside the mutation's own transaction: gapless,\n            strictly increasing in commit order, never reused and never reset.\n            This is the value to pass back as `since`. It is scoped to ONE\n            replica — see the operation description.\n        ts:\n          type: string\n          description: >-\n            UTC insert time, stamped inside the committing transaction and\n            normalized to RFC 3339. It is NOT monotone in `seq`: two writers\n            against one SQL server, or a clock stepped back by NTP, can commit\n            an earlier `seq` with a later timestamp. Order by `seq`, never by\n            this.\n        op:\n          type: string\n          description: >-\n            What was done: `create`, `update`, `close`, `delete`, `dep_add`,\n            `dep_remove` or `comment`. The set is closed in v0; a client MUST\n            default-branch on an unknown value rather than fail, so that adding\n            one stays additive.\n        issue_id:\n          type: string\n          description: The mutated issue's canonical id.\n        issue:\n          type: object\n          nullable: true\n          description: >-\n            The full issue state after the mutation — the same object shape\n            `Issue` describes — or `null` on a delete. Always present.\n        dep:\n          type: object\n          description: >-\n            On `dep_add` and `dep_remove` only: `{\"kind\",\"target\",\"metadata\"}`\n            for the edge. Absent on every other op.\n\n\n            `metadata` differs in PROVENANCE between the two: on `dep_add` it is\n            the value being written as the caller supplied it, on `dep_remove`\n            it is the stored column read back just before the delete. The two\n            can differ byte for byte while meaning the same thing, so compare\n            parsed values rather than strings.\n        comment:\n          type: object\n          description: >-\n            On `comment` only: `{\"id\",\"author\",\"text\",\"created_at\",\"source\"}`.\n            Absent on every other op.\n\n    EventsPage:\n      type: object\n      required: [records, head]\n      description: >-\n        One page of the journal plus the position of its end.\n\n\n        THERE IS NO `has_more`, and that is deliberate rather than an omission.\n        Every other page on this surface reports truncation with a boolean\n        because its ordering is a query's; here the answer is a number the\n        client already needs for its next request. Compare the last record's\n        `seq` with `head`: equal means caught up, lower means keep reading. A\n        full page proves nothing either way, and a `has_more` computed from the\n        limit would be a second, weaker way to ask the same question.\n      properties:\n        records:\n          type: array\n          description: >-\n            Records with `seq` strictly greater than the requested `since`, in\n            ASCENDING `seq` order and contiguous — a gap in the retained window\n            is a 410, never a quietly shortened list. Empty array (never null)\n            when the caller is caught up.\n          items:\n            $ref: '#/components/schemas/EventRecord'\n        head:\n          type: integer\n          format: int64\n          description: >-\n            The highest `seq` this journal has ever assigned, read in the same\n            transaction as the records above.\n\n\n            It is the journal's HISTORY, not its contents: pruning deletes rows\n            and never touches the counter, so a fully pruned journal still\n            reports the head it reached. `0` means no mutation has ever been\n            journaled here — which, given that a disabled journal is refused\n            with 409 rather than answered, means an enabled journal on a\n            workspace that has not been written to yet.\n\n\n            Because it is read after the rows within one transaction, it is\n            always greater than or equal to the last record's `seq`; it may be\n            greater simply because a mutation committed while the page was being\n            read, which is the ordinary signal to poll again.\n\n    QueryPage:\n      type: object\n      required: [items, has_more]\n      properties:\n        items:\n          type: array\n          description: Empty array (never null) when the expression matched nothing.\n          items:\n            $ref: '#/components/schemas/IssueWithCounts'\n        has_more:\n          type: boolean\n          description: >-\n            True when `limit` truncated the result. It is exact for every\n            expression, including the ones evaluated outside the database:\n            those are matched against every candidate row, so the count of\n            matches is known before the page is cut. There is no cursor —\n            raise `limit` or narrow the expression.\n      description: >-\n        A page of query results. It is `ReadyPage`'s shape rather than\n        `IssuesPage`'s, and the missing member is the point: a page of this\n        operation carries no `next_cursor`, because a cursor is a keyset\n        position in a database order and a predicate query's matching set is\n        assembled outside the database.\n\n    Setting:\n      type: object\n      description: >-\n        One entry of the workspace's stored settings plane.\n\n\n        THIS SCHEMA IS DELIBERATELY NOT `x-go-type`-PINNED, and it is one of\n        the two ROW schemas on this surface that is not — `Memory` is the\n        other. (Envelopes and page wrappers are unpinned as a class, for a\n        different reason: they are new wire surface with no prior\n        implementation. `TestWireTagBijection`'s `pinnedSchemas` is the\n        authority on what is pinned.) The thirteen pinned schemas above are\n        pinned because a canonical Go struct already IS the contract —\n        `types.Issue`'s JSON encoding is what `bd show --json` emits. A setting\n        has no such struct: the CLI marshals an ad-hoc\n        `map[string]string` per verb, so there is nothing to pin TO, and\n        minting a type to pin to would mean changing what `bd config get\n        --json` prints in order to satisfy a rule about not changing it.\n\n\n        The two surfaces are still one shape where they overlap — `key` and\n        `value` are spelled as the CLI spells them — and they diverge in\n        exactly one deliberate place, `redacted`, which exists because a bearer\n        on this surface is optional, shared and surface-wide — it cannot decide\n        that one caller may read a credential and another may not — while the\n        CLI requires access to the database anyway.\n      required: [key, redacted]\n      properties:\n        key:\n          type: string\n          description: The setting's key, echoed verbatim.\n        value:\n          type: string\n          description: >-\n            The stored value, verbatim.\n\n\n            ABSENT MEANS ONE OF TWO THINGS, and `redacted` says which. With\n            `redacted: false` the workspace stores nothing for this key OR\n            stores the empty string; those are indistinguishable through this\n            surface and through the CLI. With `redacted: true` a value may well\n            be stored and is withheld.\n\n\n            It is never emitted as an empty string and never transformed: a\n            value that is not the stored value is omitted rather than masked,\n            so a client can never mistake a placeholder for configuration.\n        redacted:\n          type: boolean\n          description: >-\n            True when `value` is withheld because the KEY marks the setting as\n            credential-bearing — the name contains `token`, `secret`,\n            `password`, or an API-key spelling. It is a decision about the key\n            alone: no value is inspected, so a credential stored under an\n            innocuous name is NOT protected by this and must not be stored in\n            this plane at all.\n\n\n            Always present, including when false, so a client never has to\n            infer redaction from an absent member.\n\n    Memory:\n      type: object\n      required: [key, value]\n      description: >-\n        One entry of the workspace's persistent memory plane.\n\n\n        NOT `x-go-type`-PINNED, for the reason `Setting` is not: the CLI\n        marshals an ad-hoc map per verb, so there is no canonical Go struct\n        whose JSON encoding is this contract, and minting one to pin to would\n        mean changing what `bd recall --json` prints in order to satisfy a rule\n        about not changing it.\n\n\n        IT HAS NO `redacted` MEMBER, and that is the deliberate difference from\n        `Setting`. Redaction there is a decision about the KEY NAME, which works\n        because settings keys are configured names; memory keys are derived from\n        the content, so the same rule would withhold a memory about credentials\n        and serve one containing a credential under an innocuous slug. A\n        configured bearer would not close that either — it admits a client to\n        the whole surface rather than to particular keys — so this schema\n        states the exposure rather than implying a protection it does not have.\n      properties:\n        key:\n          type: string\n          description: The memory's key, echoed verbatim.\n        value:\n          type: string\n          description: >-\n            The stored content, verbatim: newlines, surrounding space and\n            unicode as stored, never truncated and never withheld.\n\n\n            Always present. It is the empty string only where a row was written\n            out of band with an empty value, which\n            `GET /v0/beads/memories/{key}` answers as a `404` and\n            `GET /v0/beads/memories` enumerates.\n\n    MemoriesPage:\n      type: object\n      required: [items, has_more]\n      properties:\n        items:\n          type: array\n          description: >-\n            The stored memories, ordered by key. Empty array (never null) when\n            the workspace holds none, or when `q` matched none.\n          items:\n            $ref: '#/components/schemas/Memory'\n        has_more:\n          type: boolean\n          description: >-\n            Always false in v0: the whole plane is returned in one page. It is\n            present so that a later revision can page this collection without\n            changing the response shape.\n        next_cursor:\n          type: string\n          description: >-\n            Present if and only if `has_more` is true, which is never in v0.\n\n    RememberRequest:\n      type: object\n      additionalProperties: false\n      required: [content]\n      description: >-\n        What to remember, and optionally under what key.\n      properties:\n        content:\n          type: string\n          description: >-\n            The memory itself, stored VERBATIM: newlines, surrounding space and\n            unicode all survive. Flattening it to one line is what a front door\n            does when it prints, not what this plane does when it stores.\n\n\n            Empty after trimming is a `400`. So is content from which no key can\n            be derived when `key` is omitted — `\"!!!\"` derives to nothing — and\n            the recovery for that one is to send a `key`.\n        key:\n          type: string\n          description: >-\n            The key to store under. OMIT IT to have the server derive one from\n            `content`; the response's `key` is then how the caller learns where\n            the memory landed.\n\n\n            Supplied, it is used verbatim — no trimming, no slugging, no charset\n            restriction. A key carrying a control character is storable this way\n            and by `bd remember --key`, and is then unreachable through\n            `GET`/`DELETE /v0/beads/memories/{key}`, which refuse one: see those\n            operations.\n\n    RememberedMemory:\n      type: object\n      required: [key, value, replaced]\n      description: >-\n        One stored memory, plus whether storing it overwrote a previous value.\n\n\n        It is `Memory`'s shape with `replaced` added rather than a composition\n        of it, because this document repeats property lists instead of using\n        `allOf` (see the note at the top of the file).\n      properties:\n        key:\n          type: string\n          description: >-\n            The key the memory now lives under: the one the request supplied, or\n            the one derived from `content`. Recall it under exactly these bytes.\n        value:\n          type: string\n          description: >-\n            The stored content, echoed verbatim. Always present, and never\n            withheld — this plane has no redaction; see the operation\n            description.\n        replaced:\n          type: boolean\n          description: >-\n            True when a previous value existed under `key` and this request\n            overwrote it; false when the key was new. It is observed in the same\n            transaction as the write, so it describes the row this request\n            wrote.\n\n\n            A previous value that was the EMPTY STRING reports true: the ROW\n            existed, even though `GET /v0/beads/memories/{key}` would have\n            answered `404` for it. That divergence is the storage seam's\n            conflation showing through, and it is stated rather than smoothed\n            over, because smoothing it would mean this member reporting\n            \"nothing was there\" about a write that overwrote something.\n\n    SetSettingRequest:\n      type: object\n      additionalProperties: false\n      required: [value]\n      description: >-\n        What to store under the key the path names. The key is not a member\n        here: it has one spelling, and a body carrying it too would give one\n        request two anchors and a question about what to do when they disagree.\n\n\n        THERE IS NO `actor`, unlike every issue mutation on this surface, and no\n        guard member either. This plane records no history entry to attribute a\n        write on and holds no row version to compare, so both would be members\n        with nothing behind them.\n      properties:\n        value:\n          type: string\n          description: >-\n            The value to store, VERBATIM. It is not trimmed and not\n            character-filtered: two of the keys this plane holds carry\n            structured configuration a filter would corrupt.\n\n\n            IT IS BOUNDED AT 65535 BYTES, which is the storage column, and the\n            refusal is a `400` naming this member rather than the `500` the\n            column would otherwise produce for a request the caller could have\n            fixed. BYTES rather than characters, because that is how the column\n            counts: 40000 multi-byte characters overflow it and 65000 ASCII ones\n            do not. The 1 MiB body cap every operation shares still applies above\n            this and is never the binding limit here.\n\n\n            The bound is NOT the one `addComment`'s `text` carries, and the\n            difference is what the two members are for. A comment is a document —\n            a stack trace, a diff, a captured transcript — so its column is\n            `LONGTEXT`. A setting is a value: nothing this plane holds is a\n            megabyte of configuration, so the narrow bound is the honest\n            description rather than a limitation to widen later.\n\n\n            The empty string is a legal value and is stored. Read back it is\n            INDISTINGUISHABLE from a key nothing ever set — `Setting.value` is\n            absent for both — which is this plane's shipped conflation rather\n            than something this operation introduces. A caller that means\n            \"remove it\" sends `DELETE`.\n\n\n            What comes back is this value, for every key this plane accepts:\n            the one stored key with a normalization step is `issue_prefix`,\n            which is also the one key this plane refuses, so no write through\n            this door is transformed on its way in. The one thing the response\n            may not repeat is a value the KEY marks credential-bearing; see the\n            operation.\n\n    RemovedSetting:\n      type: object\n      required: [key]\n      description: >-\n        The outcome of removing one setting.\n\n\n        IT CARRIES THE KEY AND NOTHING ELSE, and the absence is the contract\n        rather than an unfinished shape. There is no `removed` flag because the\n        storage seam discards the affected-row count on every implementation, so\n        the member would be a value one of them had to invent — and no `value`,\n        because reporting what was there would publish, on the one operation that\n        withholds nothing, exactly the credential `GET /v0/beads/config/{key}`\n        redacts.\n      properties:\n        key:\n          type: string\n          description: The key that now holds nothing, echoed verbatim.\n\n    SettingsPage:\n      type: object\n      required: [items, has_more]\n      properties:\n        items:\n          type: array\n          description: >-\n            The stored settings, ordered by key. Empty array (never null) when\n            the workspace stores none.\n          items:\n            $ref: '#/components/schemas/Setting'\n        has_more:\n          type: boolean\n          description: >-\n            Always false in v0: the whole plane is returned in one page. It is\n            present so that a later revision can page this collection without\n            changing the response shape.\n        next_cursor:\n          type: string\n          description: >-\n            Present if and only if `has_more` is true, which is never in v0.\n    StatsResponse:\n      type: object\n      required: [summary, blocked_count_skipped]\n      description: >-\n        The same envelope `bd status --json` prints, minus one member: the CLI\n        also carries `recent_activity`, which every shipped code path leaves\n        absent, so it is not published here.\n      properties:\n        summary:\n          $ref: '#/components/schemas/Statistics'\n        blocked_count_skipped:\n          type: boolean\n          description: >-\n            True when the summary came back without the blocked-set scan, which\n            is exactly `summary.blocked_issues == null`. It is DERIVED from the\n            answer rather than echoed from the request: `skip_blocked` is a\n            hint, and a backend with no cheaper path answers with the full\n            numbers and this flag false.\n    DependencyEdges:\n      type: object\n      description: >-\n        The stored edges of the named issues, plus the ids that named nothing.\n        It is NOT a page: this operation has no limit and no cursor, because\n        the number of issues asked about is what bounds it.\n      required: [items, missing]\n      properties:\n        items:\n          type: array\n          description: >-\n            Every matching edge, flattened across the named issues rather than\n            keyed by source — the same flat array `bd dep list a b c --json`\n            emits, so the two surfaces are one compatibility domain. Group by\n            `issue_id` to recover the per-source view. Empty array (never null)\n            when nothing matches.\n\n\n            The order is by the named issues in the order the request named\n            them, and within each issue by target id.\n          items:\n            $ref: '#/components/schemas/Dependency'\n        missing:\n          type: array\n          description: >-\n            The requested `issue_id` values that name neither an issue nor a\n            wisp, in the order they were named. Empty array (never null) when\n            every named issue exists.\n\n\n            An id here contributes no `items`, and the absence of an id here is\n            NOT a claim that it has edges — an issue that exists and depends on\n            nothing is in neither list.\n          items:\n            type: string\n\n    EdgeCounts:\n      type: object\n      description: >-\n        Each anchor's edge cardinality — the body of\n        `GET /v0/beads/dependencies:count`. It is NOT a page and carries no\n        total: the answer is per anchor, and a sum across anchors would\n        double-count every edge whose two ends were both named.\n\n\n        It is NOT `x-go-type`-pinned, for `IssueCount`'s reason: there is no\n        canonical Go struct whose JSON encoding is this contract. The role\n        answers with `issueops.EdgeCountResult`, whose members carry no JSON\n        tags at all because nothing marshals it.\n      required: [anchors]\n      properties:\n        anchors:\n          type: array\n          description: >-\n            One entry per DISTINCT requested `issue_id`, in the order the\n            request first named it. Empty array (never null) when the request\n            named no anchors this server accepted — which it cannot, since\n            `issue_id` is required and bounded below at one.\n\n\n            A repeated id appears ONCE. The collapse happens before anything is\n            counted, so a caller that summed the entries would not count the\n            same edges twice.\n          items:\n            $ref: '#/components/schemas/AnchorEdgeCount'\n\n    AnchorEdgeCount:\n      type: object\n      description: One anchor's edge cardinality, or the report that it is not there.\n      required: [id, count, missing]\n      properties:\n        id:\n          type: string\n          description: >-\n            The anchor, spelled exactly as the request spelled it. It is not\n            re-canonicalized: there is no fuzzy, prefix or substring resolution\n            on this surface, so what comes back is what went out.\n        count:\n          type: integer\n          format: int64\n          description: >-\n            How many stored edges match, in the requested direction, after the\n            type and status filters. Never negative.\n\n\n            It SPANS BOTH DEPENDENCY PLANES and is a SUM over them rather than a\n            distinct count of edge rows — a durable issue's dependent count\n            includes the wisps that depend on it, and a wisp's dependency count\n            includes the durable issues it depends on.\n            `issueops.AnchorEdgeCount.Count` states the rule, the one state that\n            can make the sum differ from a distinct count, and why this role\n            answers with the sum. Nothing is restated here.\n\n\n            ALWAYS PRESENT, including as 0, and 0 is the COMMON answer: most\n            issues have no edges in at least one direction. It is 0 for a\n            missing anchor too, which is exactly why `missing` is beside it.\n\n\n            DECODE IT AS A 64-BIT INTEGER. The member is `format: int64` and a\n            workspace's graph is not bounded by 2^53; a lossy parser would\n            answer a number NEAR the count, which on a cardinality is worse\n            than an error because nothing downstream can tell.\n        missing:\n          type: boolean\n          description: >-\n            True when no issue and no wisp carries this id.\n\n\n            ALWAYS PRESENT, including as `false`. It is the member that keeps\n            this from being a question a caller cannot tell it got wrong: a\n            count of 0 is otherwise indistinguishable from a typo, and an\n            absent boolean would be ambiguous between \"present\" and \"this\n            producer does not report misses\".\n\n\n            A missing anchor counts 0 whatever rows are still keyed to it — a\n            dependency row whose source has been deleted is orphaned data, and\n            counting it would contradict this flag.\n\n\n            DANGLING EDGES ARE NOT MISSING ANCHORS. This is about the ANCHOR. An\n            edge whose OTHER end names nothing is counted like any other edge,\n            and nothing here reports on it.\n\n    BlockingAnnotations:\n      type: object\n      description: >-\n        The blocking decoration of the named issues. It is NOT a page: this\n        operation has no limit and no cursor, because the number of issues asked\n        about is what bounds it.\n      required: [items]\n      properties:\n        items:\n          type: array\n          description: >-\n            One entry per DISTINCT requested id, in the order the request first\n            named it — so a client can zip this against the ids it sent. Empty\n            array (never null) when the request named none.\n\n\n            There is no `missing` beside it, unlike `DependencyEdges`: this\n            operation probes no id's existence, so every requested id has an\n            entry and an id that names nothing is simply bare.\n          items:\n            $ref: '#/components/schemas/IssueBlocking'\n\n    RelatedIssues:\n      type: object\n      description: >-\n        One issue's neighbors — the body of\n        `GET /v0/beads/issues/{id}/related`. It is NOT a page: this operation\n        has no limit and no cursor, and it names ONE anchor, so there is nothing\n        for a `has_more` to be about.\n\n\n        There is no `missing` beside `items`, unlike `DependencyEdges`. That\n        member exists because a batch cannot report an absent anchor any other\n        way without discarding the anchors it did find; here the single absent\n        anchor is a 404.\n\n\n        It is NOT `x-go-type`-pinned, for `EdgeCounts`' reason: there is no\n        canonical Go struct whose JSON encoding is this envelope. The role\n        answers with a bare slice of `issueops.RelatedIssue`, and THAT element\n        is pinned — see `IssueWithDependencyMetadata`.\n      required: [items]\n      properties:\n        items:\n          type: array\n          description: >-\n            The neighbors, ascending by id with the edge type breaking a tie.\n            Empty array (never null) when this issue has none in the requested\n            direction, or when the `type` filter rejected every edge.\n\n\n            Each element is a full issue plus `dependency_type`, the type of the\n            edge that led to it — the same element\n            `GET /v0/beads/issues/{id}` carries under `dependencies` and\n            `dependents`, so the two surfaces are one compatibility domain.\n\n\n            AN EDGE WITH NO FAR END IN THIS DATABASE CONTRIBUTES NOTHING here:\n            an `external:` reference, an id in another repository's namespace\n            and an id whose issue was deleted out from under its edges are all\n            silently absent, with no placeholder row and no error. The length of\n            this array is a NEIGHBOR count, never an edge count.\n          items:\n            $ref: '#/components/schemas/IssueWithDependencyMetadata'\n\n    BatchCreateRequest:\n      type: object\n      additionalProperties: false\n      required: [actor, items]\n      properties:\n        actor:\n          type: string\n          minLength: 1\n          maxLength: 256\n          pattern: '^[^\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029]+$'\n          description: >-\n            Who is creating the issues, under `ClaimRequest.actor`'s rules and\n            for the same reasons: the server trims it, refuses an empty result,\n            anything longer than 256 BYTES, and any control character including\n            newline. It is attributed to every item and interpolated into the\n            storage commit message.\n        items:\n          type: array\n          minItems: 1\n          maxItems: 100\n          description: >-\n            The issues to create, in order. An empty array is a `400` rather\n            than a successful no-op: a write request that writes nothing is a\n            client bug, and answering it with a cheerful empty success is how a\n            client whose own list filtered to nothing silently stops creating\n            anything.\n\n\n            The 100-item cap is a bound on how long one request may hold a write\n            transaction, not a statement about batch semantics. Split a larger\n            plan; each request is atomic on its own.\n          items:\n            $ref: '#/components/schemas/BatchCreateItem'\n\n    BatchCreateItem:\n      type: object\n      additionalProperties: false\n      required: [title]\n      properties:\n        title:\n          type: string\n          minLength: 1\n          maxLength: 255\n        description:\n          type: string\n        design:\n          type: string\n        acceptance_criteria:\n          type: string\n        priority:\n          type: integer\n          minimum: 0\n          maximum: 4\n          description: 0 is P0/critical. Absent means the workspace default.\n        issue_type:\n          type: string\n          description: >-\n            Issue type. Spelled `issue_type` rather than `type`, matching the\n            member `Issue` carries, and validated against the built-ins plus the\n            workspace's configured custom types — an unknown one is a `400`.\n        assignee:\n          type: string\n          maxLength: 255\n        labels:\n          type: array\n          items:\n            type: string\n            maxLength: 255\n        dependencies:\n          type: array\n          maxItems: 100\n          description: >-\n            The edges this issue is created carrying. They are written in the\n            same transaction as the issue, so this operation never publishes an\n            issue whose declared relationships are not there yet.\n          items:\n            $ref: '#/components/schemas/BatchCreateDependency'\n\n    BatchCreateDependency:\n      type: object\n      additionalProperties: false\n      required: [target_id, type]\n      properties:\n        target_id:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            The far end of the edge: an issue this workspace holds, an\n            `external:` reference, or an id whose prefix belongs to another\n            repository. Anything else is a `400` and nothing is created.\n\n\n            NOT AN ITEM OF THIS REQUEST. The server assigns every id and an item\n            carries no name, so there is nothing here a caller could write to\n            address one; see the operation's description for the operation that\n            can.\n        type:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            The edge type, from the same OPEN vocabulary `Dependency.type`\n            carries. It is spelled `type` because that is the member an edge\n            carries everywhere else on this surface.\n\n    BatchCreateResponse:\n      type: object\n      required: [items]\n      properties:\n        items:\n          type: array\n          description: >-\n            One entry per requested item, in REQUEST ORDER, each the stored\n            issue with its generated id and its labels. Never null and never\n            shorter than the request: a partial outcome does not exist on this\n            operation.\n\n\n            There is no `has_more` and no `next_cursor`. This is not a page —\n            the client already knows how many items it sent — and publishing a\n            paging envelope over a fixed-length answer would invite a client to\n            look for a second page that can never exist.\n          items:\n            $ref: '#/components/schemas/Issue'\n\n    ApplyBatchRequest:\n      type: object\n      additionalProperties: false\n      required: [actor, items]\n      properties:\n        actor:\n          type: string\n          minLength: 1\n          maxLength: 256\n          pattern: '^[^\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029]+$'\n          description: >-\n            Who is applying the plan, under `ClaimRequest.actor`'s rules and for\n            the same reasons: the server trims it, refuses an empty result,\n            anything longer than 256 BYTES (the `maxLength` above counts\n            characters — the byte limit is the binding one), and any control\n            character including newline.\n\n\n            It is attributed to every item and to the ONE history entry the\n            request records, because a batch is one act by one caller.\n        items:\n          type: array\n          minItems: 1\n          maxItems: 100\n          description: >-\n            The items to apply, IN THE ORDER THEY ARE TO BE APPLIED. An empty\n            array is a `400` rather than a successful no-op: a write request that\n            writes nothing is a client bug, and answering it cheerfully is how a\n            client whose own plan filtered to nothing silently stops writing.\n\n\n            The 100-item cap bounds how long one request may hold a write\n            transaction, not batch semantics. Split a larger plan; each request\n            is atomic on its own — but splitting it changes what the end gate\n            can see, since the gate runs over one request at a time.\n\n\n            A per-item refusal names its offender as `items[i].kind.member`.\n          items:\n            $ref: '#/components/schemas/ApplyItem'\n        provenance:\n          type: string\n          maxLength: 255\n          description: >-\n            Labels the version-control history entry this request records, under\n            `updateIssue`'s rule: it changes how the entry READS, never whether\n            one is recorded. Empty composes a default naming how many items of\n            each kind landed and no ids.\n        force_id_prefix:\n          type: boolean\n          default: false\n          description: >-\n            Permits an explicit `create.id` outside the workspace's configured\n            issue prefix, for EVERY create item in the request. Without it such\n            an id is refused by the role and arrives as a `400`.\n        skip_per_edge_cycle_check:\n          type: boolean\n          default: false\n          description: >-\n            Drops the PER-EDGE cycle probe for a caller wiring a large graph,\n            exactly as it does on `POST /v0/beads/dependencies:add`.\n\n\n            IT NEVER DROPS THE END GATE, which runs once after every item and\n            re-validates the whole graph this request built, and it never drops\n            the self-dependency refusal. It trades per-edge attribution for\n            speed, not validation for speed.\n\n    ApplyItem:\n      type: object\n      additionalProperties: false\n      required: [kind]\n      description: >-\n        One item of a plan: a `kind` naming what it does, plus exactly one\n        payload member matching it.\n\n\n        IT IS A TAGGED SINGLE-SHAPE OBJECT rather than a polymorphic one, and\n        the spelling is deliberate. This document uses no `oneOf`, `anyOf` or\n        `allOf` anywhere: a component carrying a composition keyword alongside\n        the `x-go-type` pins the response schemas depend on silently loses the\n        pin, and the generated result is a second wire struct that drifts from\n        the canonical one. So the union is carried as four OPTIONAL members with\n        a required tag rather than as a schema alternation.\n\n\n        WHAT A CLIENT MUST DO, since no validator can enforce it from this\n        schema alone: send `kind`, send the ONE member `kind` names, and send no\n        other. An item carrying no payload does nothing; an item carrying a\n        payload its `kind` does not name has two halves that disagree; an item\n        carrying two payloads cannot say which it meant. All three are a `400`\n        and nothing in the request is written. A generated client's type will\n        make all four members constructible at once — that is the cost of the\n        spelling, and checking it is the client's.\n\n\n        READING one is the same rule from the other side: dispatch on `kind` and\n        read only that member. The other three are absent.\n      properties:\n        kind:\n          type: string\n          enum: [create, update, close, dep_add]\n          description: >-\n            Which member below is read. A CLOSED set, unlike a dependency\n            `type`: every value here is a verb this operation implements, and an\n            unknown one is a request the server cannot execute rather than a\n            workspace's own vocabulary.\n        create:\n          $ref: '#/components/schemas/ApplyCreateItem'\n        update:\n          $ref: '#/components/schemas/ApplyUpdateItem'\n        close:\n          $ref: '#/components/schemas/ApplyCloseItem'\n        dep_add:\n          $ref: '#/components/schemas/ApplyDepAddItem'\n\n    Ref:\n      type: object\n      additionalProperties: false\n      description: >-\n        Names ONE issue, either by an id that already exists or by the `key` a\n        create item earlier in the same request gave itself.\n\n\n        EXACTLY ONE OF THE TWO IS SET, and both cases the schema cannot express\n        are a `400`: both members set is a caller that cannot say which it\n        meant, and neither set is a reference to nothing. (Spelling that as a\n        schema alternation would need `oneOf`, which this document does not\n        use — see `ApplyItem`.)\n\n\n        A KEY REACHES BACKWARD ONLY where the ref ADDRESSES a row — an\n        `update.target`, a `close.target`, either endpoint of a `dep_add`. The\n        one exception is `create.metadata_refs`, whose values may reach forward\n        or name their own item's key; the operation's description says why.\n      properties:\n        key:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            The `key` a create item in THIS REQUEST gave itself. It is not an\n            id, it is not stored anywhere, and it is resolved to the id the\n            request minted — which the response's `keys` member reports.\n        id:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            An id that already exists, EXACTLY. There is no fuzzy, prefix or\n            cross-repo resolution on this surface.\n\n    ApplyCreateItem:\n      type: object\n      additionalProperties: false\n      required: [title]\n      description: >-\n        Creates one issue and optionally NAMES it, so later items can reach the\n        row without knowing an id the request has not minted yet.\n\n\n        It publishes the whole create vocabulary rather than\n        `POST /v0/beads/issues:batchCreate`'s narrow one, and the additions are\n        the point: `status`, `sender`, `metadata`, `ephemeral` and `no_history`\n        are the members whose absence there makes that operation unusable for a\n        caller composing a real plan.\n\n\n        THE EDGES ARE NOT HERE. An issue's dependencies and its parent are\n        `dep_add` ITEMS, so the order of every edge in the request is total and\n        there is exactly one spelling for an edge. An item carrying comments or\n        dependencies on the issue is a `400`.\n\n\n        `metadata` is the issue's own metadata document and must be a JSON\n        OBJECT where it is present at all. It is stored as sent; the resolved\n        ids `metadata_refs` splices are written over its top-level keys after\n        every id in the request exists.\n      properties:\n        key:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            This item's name inside the request. OPTIONAL — an item nothing\n            refers to needs no name — and unique across the request's create\n            items; a repeat is a `400`. It is what a later `Ref.key` resolves\n            to, and the response's `keys` member is where the id it was bound to\n            is read.\n        id:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            An explicit id for the new row, CREATE-ONLY: an id that already\n            names a stored row is a `409` `already_exists` and the whole request\n            is refused — never an adoption and never an overwrite. To act on a\n            row that already exists, send an `update` item referencing it by\n            `{\"id\": …}`. The id is checked against the workspace's configured\n            issue prefix unless the request sets `force_id_prefix`.\n\n\n            Absent is the ordinary case and the server mints one. This is the\n            member `POST /v0/beads/issues:batchCreate` deliberately does not\n            publish, which is why that operation can never adopt or overwrite a\n            stored row and this one can be refused for trying.\n        title:\n          type: string\n          minLength: 1\n          maxLength: 255\n        description:\n          type: string\n        design:\n          type: string\n        acceptance_criteria:\n          type: string\n        notes:\n          type: string\n        issue_type:\n          type: string\n          maxLength: 255\n          description: >-\n            Issue type. Spelled `issue_type` rather than `type`, matching the\n            member `Issue` carries, and validated against the built-ins plus the\n            workspace's configured custom types by the ROLE — this server cannot\n            read that vocabulary without a transaction, so it checks only what\n            this schema declares and an unknown one arrives as a `400`.\n        status:\n          type: string\n          maxLength: 255\n          description: >-\n            The status the issue is created in, from this workspace's own\n            configured vocabulary. Absent means the workspace default.\n        priority:\n          type: integer\n          minimum: 0\n          maximum: 4\n          description: 0 is P0/critical. Absent means the workspace default.\n        assignee:\n          type: string\n          maxLength: 255\n        owner:\n          type: string\n          maxLength: 255\n          description: >-\n            The human owner, which is a different member from `assignee`: the\n            assignee is who is working it now, the owner is who it is attributed\n            to.\n        labels:\n          type: array\n          items:\n            type: string\n            maxLength: 255\n          description: >-\n            The complete label set the issue is created with. Authoritative, not\n            a patch — a create has nothing to add to.\n        estimated_minutes:\n          type: integer\n          description: An estimate in minutes. Absent leaves it unset.\n        external_ref:\n          type: string\n          maxLength: 255\n        due_at:\n          type: string\n          format: date-time\n          description: RFC 3339.\n        defer_until:\n          type: string\n          format: date-time\n          description: RFC 3339. The issue is hidden from ready work until then.\n        sender:\n          type: string\n          maxLength: 255\n          description: >-\n            Who sent this, for the message-shaped rows a plan creates. Stored\n            verbatim and interpreted by nothing on this surface.\n        metadata:\n          $ref: '#/components/schemas/MetadataValue'\n        ephemeral:\n          type: boolean\n          default: false\n          description: >-\n            Creates the issue on the EPHEMERAL plane rather than the durable\n            one. Per item, exactly as it is for\n            `POST /v0/beads/issues:batchCreate`, so one request may create\n            durable issues and ephemeral ones together.\n\n\n            The two planes hold their edges in different tables, so a `dep_add`\n            between two rows this request creates on OPPOSITE planes is refused\n            with everything else the request asked for. Mutually exclusive with\n            `no_history`.\n        no_history:\n          type: boolean\n          default: false\n          description: >-\n            Creates the issue on the ephemeral plane WITHOUT history, and\n            without the garbage collection an ordinary ephemeral row is eligible\n            for. Mutually exclusive with `ephemeral`.\n        metadata_refs:\n          type: object\n          additionalProperties:\n            $ref: '#/components/schemas/Ref'\n          description: >-\n            Splices resolved ids into this issue's metadata: each entry writes\n            the id its `Ref` resolves to as the WHOLE VALUE of one top-level\n            metadata key.\n\n\n            IT IS THE ONE PLACE A KEY MAY REACH FORWARD, or name this item's own\n            `key` — see the operation's description. A ref here that names a key\n            NO item declares is still a `400`.\n\n\n            IT IS A TYPED MAP, NOT TEMPLATING. A `${key}` placeholder inside a\n            JSON string would have no escape for a literal dollar-brace, would\n            collide with every other templating language a caller's own values\n            might carry, and could not be type-checked at all. This is one key,\n            one whole value, one level deep.\n\n\n            The splice is applied AFTER the row is created, so a consumer of the\n            event stream sees a create and then an update on the spliced row.\n\n    ApplyUpdateItem:\n      type: object\n      additionalProperties: false\n      required: [target, patch]\n      description: >-\n        Patches one existing issue, under `PATCH /v0/beads/issues/{id}`'s rules.\n\n\n        The two carry the same preconditions and the same force flags; what is\n        this operation's alone is that its guards evaluate AS-MODIFIED — against\n        the row as earlier items of this same request have already changed it —\n        and that a miss takes the whole plan down rather than one write.\n      properties:\n        target:\n          $ref: '#/components/schemas/Ref'\n        patch:\n          $ref: '#/components/schemas/ApplyPatchBody'\n        expected_version:\n          type: integer\n          format: int64\n          description: >-\n            Requires the row's `revision` to equal this value before the patch.\n            A miss refuses the WHOLE request with `409 precondition_failed`.\n\n\n            IT IS A `400`, NOT A `409`, ON A ROW THIS REQUEST HAS ALREADY\n            WRITTEN — including one an earlier item created. The token is minted\n            by the write, so mid-request there is no value a caller could send:\n            the pre-request token is stale by construction and a row this\n            request just created never had one the caller could read. Refusing\n            statically says so; answering with a mismatch would send the caller\n            looking for a concurrent writer that does not exist.\n\n\n            `expected_status` and `expected_assignee` carry no such rule,\n            because a caller CAN know what its own earlier item set them to.\n\n\n            DECODE IT AS A 64-BIT INTEGER, for the reason\n            `UpdateIssueRequest.expected_version` spells out. It bites harder\n            here than anywhere else on the surface: a corrupted token refuses\n            the WHOLE plan rather than one write, so a client with a lossy\n            parser loses every item of every batch it guards.\n        expected_status:\n          type: string\n          maxLength: 255\n          description: >-\n            Requires the issue's status to equal this value, evaluated\n            AS-MODIFIED — against the row as this request has already changed it\n            at this item's position. A miss refuses the whole request with\n            `409 precondition_failed`.\n        expected_assignee:\n          type: string\n          maxLength: 255\n          description: >-\n            Requires the issue's assignee to equal this value, evaluated\n            as-modified. A match AUTHORIZES the requested `patch.assignee`\n            transfer: this compare-and-set replaces the ordinary anti-steal\n            fence, so it must not be combined with `force_assignee_transfer`. A\n            miss refuses the whole request with `409 precondition_failed`.\n        force_close_policy:\n          type: boolean\n          default: false\n          description: >-\n            Bypasses ONLY close policy — the open-children refusal and the live\n            blocker refusal — for a `patch.status` that crosses into the\n            workspace's done category. It has no effect without such a status\n            change, and it never bypasses validation, the preconditions above,\n            or the assignee fence.\n        force_assignee_transfer:\n          type: boolean\n          default: false\n          description: >-\n            Bypasses ONLY a genuine transfer away from a live foreign\n            in-progress owner. Reasserting the exact current assignee is\n            idempotent and needs no force. It requires `patch.assignee` — a\n            request setting it without one is a `400` — and it must be false\n            when `expected_assignee` is sent.\n\n    ApplyPatchBody:\n      type: object\n      additionalProperties: false\n      description: >-\n        The fields an `update` item writes. Every member is optional and\n        PRESENCE is the signal: a member present is written, a member absent is\n        untouched. An empty object is a `400` — a write that writes nothing is a\n        client bug.\n\n\n        It mirrors `IssuePatchBody` member for member and diverges in exactly\n        two places now that `PATCH /v0/beads/issues/{id}` publishes `status`,\n        `assignee` and the same `metadata` algebra.\n\n\n        `owner` is published here and not there, which is an accident of order\n        rather than a decision: nothing has asked for it on the single patch.\n\n\n        `labels` is a full patch rather than a complete replacement, and that\n        one is a real difference: a plan has to be able to REMOVE one label\n        without knowing the rest of the set, because it edits a set it did not\n        compose. A caller patching one row it just read already knows the set.\n\n\n        `parent_id` is deliberately absent, and its absence is this operation's\n        one-edge-one-spelling rule: a parent is a `dep_add` item of type\n        `parent-child`, so the order of every edge in the request stays total.\n        The single patch has no ordering to express and publishes it directly.\n        `persistence` is absent from both — moving a row between planes mid-plan\n        is a different act from writing its fields, and nothing has asked for it\n        here.\n      properties:\n        title:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            Must not be blank after trimming; the length bound is what the\n            column holds.\n        description:\n          type: string\n        design:\n          type: string\n        acceptance_criteria:\n          type: string\n        notes:\n          type: string\n          description: >-\n            Replaces the notes. Mutually exclusive with `append_notes`; sending\n            both is a `400`.\n        append_notes:\n          type: string\n          description: >-\n            Appends to the notes rather than replacing them. Mutually exclusive\n            with `notes`.\n        priority:\n          type: integer\n          minimum: 0\n          maximum: 4\n        issue_type:\n          type: string\n          maxLength: 255\n          description: >-\n            The issue type, from this workspace's own configured vocabulary. A\n            type outside it is refused by the ROLE and reaches the client as a\n            `400`.\n        status:\n          type: string\n          maxLength: 255\n          description: >-\n            The issue's status, from this workspace's own configured vocabulary.\n\n\n            A STATUS THAT CROSSES INTO THE DONE CATEGORY ANSWERS TO CLOSE\n            POLICY: the item is refused with `409 not_closable` for open\n            children or a live blocker unless `force_close_policy` is set. A\n            done-to-done change and a move OUT of the done category are\n            unaffected — which is how a plan reopens a row, since there is no\n            reopen item.\n        assignee:\n          type: string\n          maxLength: 255\n          description: >-\n            The assignee. A transfer away from a live foreign in-progress owner\n            is refused with `409 already_claimed` unless\n            `force_assignee_transfer` is set or `expected_assignee` matched.\n        owner:\n          type: string\n          maxLength: 255\n        labels:\n          $ref: '#/components/schemas/ApplyLabelPatch'\n        estimated_minutes:\n          type: integer\n          nullable: true\n          description: 'Explicit `null` CLEARS the estimate.'\n        external_ref:\n          type: string\n          nullable: true\n          maxLength: 255\n          description: 'Explicit `null` CLEARS the reference.'\n        due_at:\n          type: string\n          format: date-time\n          nullable: true\n          description: 'RFC 3339. Explicit `null` CLEARS the due date.'\n        defer_until:\n          type: string\n          format: date-time\n          nullable: true\n          description: 'RFC 3339. Explicit `null` CLEARS the deferral.'\n        metadata:\n          $ref: '#/components/schemas/ApplyMetadataPatch'\n\n    ApplyLabelPatch:\n      type: object\n      additionalProperties: false\n      description: >-\n        An ordered label edit: `replace` first, then `add`, then `remove`, so\n        REMOVAL WINS when the same label appears in more than one member.\n\n\n        It is the full patch rather than `IssuePatchBody.labels`' complete\n        replacement because a plan edits a set it did not compose: replacing\n        would mean reading the labels back first, and the read this operation\n        exists to avoid is exactly that one.\n\n\n        Repetition is free in both directions — a label named twice in one\n        member is applied once, and removing a label the issue does not carry is\n        a no-op. An EMPTY-STRING entry is dropped rather than refused: a label\n        row holding \"\" renders as nothing and matches nothing, so refusing the\n        whole request for one stray entry would fail an otherwise-good edit.\n      properties:\n        replace:\n          type: array\n          items:\n            type: string\n            maxLength: 255\n          description: >-\n            The complete starting label set. An empty array CLEARS every label;\n            omitting the member leaves the current set as the starting point.\n        add:\n          type: array\n          items:\n            type: string\n            maxLength: 255\n          description: Labels to add after any replacement.\n        remove:\n          type: array\n          items:\n            type: string\n            maxLength: 255\n          description: Labels to remove after replacement and addition.\n\n    ApplyMetadataPatch:\n      type: object\n      additionalProperties: false\n      description: >-\n        A metadata edit. `replace` is mutually exclusive with the other three;\n        without it the edits apply as `merge`, then `set` in key order, then\n        `unset`, so UNSETTING A KEY WINS over setting or merging it. Sending\n        `replace` beside any of the others is a `400`.\n\n\n        `replace` replaces the whole document. Present holding `null`, `{}` or\n        an empty value CLEARS metadata — and clearing STORES THE EMPTY JSON\n        DOCUMENT rather than SQL null, so \"created with no metadata\" and \"given\n        metadata and then cleared\" are the same stored value; a reader must\n        treat absent, empty and `{}` as one value on the way out. `merge` must\n        be a nonempty JSON OBJECT and is merged into the current document.\n      properties:\n        replace:\n          $ref: '#/components/schemas/MetadataValue'\n        merge:\n          $ref: '#/components/schemas/MetadataValue'\n        set:\n          type: object\n          additionalProperties:\n            $ref: '#/components/schemas/MetadataValue'\n          description: >-\n            Individual top-level keys to write, in deterministic key order. A\n            value present holding `null` writes JSON null; a key is removed with\n            `unset`, never by sending a null here.\n        unset:\n          type: array\n          items:\n            type: string\n            maxLength: 255\n          description: Top-level keys to remove, applied after every other edit.\n\n    ApplyCloseItem:\n      type: object\n      additionalProperties: false\n      required: [target]\n      description: >-\n        Closes one existing issue, under `POST /v0/beads/issues/{id}:close`'s\n        rules including first-close-wins.\n      properties:\n        target:\n          $ref: '#/components/schemas/Ref'\n        reason:\n          type: string\n          maxLength: 255\n          description: >-\n            Why the issue is closed, stored and read back as `close_reason`. THE\n            FIRST CLOSE WINS: an idempotent re-close writes neither this nor\n            `session`.\n        session:\n          type: string\n          maxLength: 255\n          description: >-\n            The working session that closed the issue, stored and read back as\n            `closed_by_session`, under the same first-close-wins rule.\n        force:\n          type: boolean\n          default: false\n          description: >-\n            Bypasses close policy — the open-children refusal and the\n            live-blocker refusal — and nothing else.\n\n\n            CLOSE POLICY EVALUATES AT THIS ITEM, against the row as this request\n            has already changed it. A LATER item that gives a closed parent an\n            open child is NOT refused: the policy is a gate on the closing act,\n            not an invariant the store maintains.\n        expected_version:\n          type: integer\n          format: int64\n          description: >-\n            Requires the row's `revision` to equal this value, evaluated\n            as-modified and checked before the idempotent close. A miss refuses\n            the whole request with `409 precondition_failed`, and\n            `ApplyUpdateItem.expected_version`'s already-written rule applies\n            here identically.\n\n\n            THERE IS DELIBERATELY NO `expected_status` HERE. A close is\n            idempotent — re-closing a closed issue is `changed: false` — so a\n            guard spelled to refuse an already-closed row is asking for a\n            REFUSAL where this verb answers with a no-op. That belongs on an\n            `update` item whose `patch.status` crosses into the done category.\n\n\n            DECODE IT AS A 64-BIT INTEGER, on\n            `ApplyUpdateItem.expected_version`'s terms, including its note that\n            a corrupted token here costs the whole plan.\n\n    ApplyDepAddItem:\n      type: object\n      additionalProperties: false\n      required: [source, target, type]\n      description: >-\n        Asserts ONE dependency edge, under `POST /v0/beads/dependencies:add`'s\n        rules. An edge from a row to itself is a `400`.\n\n\n        A TARGET NEED NOT BE A ROW THIS DATABASE HOLDS: an `external:` reference\n        and an id belonging to another repository are legitimate targets, so\n        only an absence this database can SEE is refused. A SOURCE has no such\n        latitude — an edge follows its source, so a source this database holds\n        no row for has no plane to land in.\n\n\n        `metadata` is the edge's type-specific JSON blob, and an OBJECT where it\n        is present at all. Most edge types carry none.\n\n\n        A WAITS-FOR EDGE IS NORMALIZED RATHER THAN STORED AS ASKED. An absent,\n        empty or `{}` `metadata` on a `waits-for` edge is STORED as\n        `{\"gate\":\"all-children\"}`, because a stored waits-for row must be\n        self-describing: readers predating the gate's introduction do not\n        default a missing one, so an empty gate is a row those readers get\n        wrong. A metadata that names a gate keeps it, along with the spawner and\n        also-blocks members a caller may carry, and a gate that is neither\n        `all-children` nor `any-children` is a `400`. Nothing else about that\n        member is interpreted.\n\n\n        THERE IS NO TYPED `waits_for` MEMBER, and that is the shape rather than\n        an omission: every measured caller already carries the gate as metadata,\n        a typed spelling lowers to these same bytes, and the blob carries\n        members a two-field typed member could not express. One spelling, and it\n        is this one.\n      properties:\n        source:\n          $ref: '#/components/schemas/Ref'\n        target:\n          $ref: '#/components/schemas/Ref'\n        type:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            The edge type, from the same OPEN vocabulary `Dependency.type`\n            carries: checked for BEING a storable value, never for membership of\n            a known-types list, so a workspace's own type passes.\n        metadata:\n          $ref: '#/components/schemas/MetadataValue'\n\n    ApplyBatchResponse:\n      type: object\n      required: [keys, items]\n      properties:\n        keys:\n          type: object\n          additionalProperties:\n            type: string\n          description: >-\n            Each create item's `key` mapped to the id it was bound to. It is the\n            one fact the request cannot carry and every caller needs.\n\n\n            It carries only the keys the request NAMED: an unnamed create item\n            is in `items` and not here. A request whose create items named\n            nothing answers with an empty object, never `null`.\n        items:\n          type: array\n          description: >-\n            One entry per requested item, in REQUEST ORDER. Never null and never\n            shorter than the request: a batch that could not apply every item\n            applied none, so there is no index with nothing to put at it.\n\n\n            There is no `has_more` and no `next_cursor`. This is not a page — the\n            client already knows how many items it sent.\n          items:\n            $ref: '#/components/schemas/ApplyItemResult'\n\n    ApplyItemResult:\n      type: object\n      required: [kind, issue_id, changed, revision]\n      description: >-\n        What ONE item did, at the index the item occupied.\n\n\n        IT IS LEAN, AND CARRIES NO ISSUE. Every other write on this surface\n        answers with the stored row; this one answers with ids and a revision,\n        and a client that wants the rows reads them back. A hundred hydrated\n        issues with their labels and edges is a response an order of magnitude\n        larger than the request that produced it, and no client needs all of\n        them: the ids are what a plan's next step is composed from. The library\n        contract behind this operation DOES carry a post-item snapshot, because\n        its completion hooks hand a script the row it is being told about — and\n        hooks never fire on this surface at all, which is exactly why the\n        snapshot stops here.\n      properties:\n        kind:\n          type: string\n          enum: [create, update, close, dep_add]\n          description: >-\n            Echoes the item's kind, so a caller walking the results does not\n            have to walk the request alongside them.\n        issue_id:\n          type: string\n          description: >-\n            The row the item acted on: the minted or explicit id for a `create`,\n            the resolved target for an `update` or a `close`, and the edge's\n            SOURCE for a `dep_add`.\n        depends_on_id:\n          type: string\n          description: >-\n            The edge's target. Present for `dep_add` and ABSENT for every other\n            kind, which act on a row rather than on a pair.\n        changed:\n          type: boolean\n          description: >-\n            Whether this item persisted a semantic mutation. A `create` is\n            always true. An `update` and a `close` follow their own operations'\n            `changed`/`already_closed` answers, and a `dep_add` is false for an\n            idempotent re-add of an edge that already existed with the same\n            type.\n        revision:\n          type: integer\n          format: int64\n          description: >-\n            The row's optimistic-concurrency token AFTER the item, and the value\n            an `expected_version` guard is composed from. The same member the\n            CLI's detail view publishes under this name; it is not a new word.\n\n\n            IT IS EQUALITY-ONLY: compare it, never order or interpret it. A\n            change signals the row was mutated since you read it, and nothing\n            more — it is a random value the engine rewrites, not a counter.\n\n\n            ITS COVERAGE IS PARTIAL, and the partiality is inherited rather than\n            introduced: the token is rewritten by claim, close, unclaim and the\n            generic update path, and NOT by the direct-update paths that rewrite\n            text without touching it. A client needing complete change detection\n            combines it with `updated_at`, `status` and the label set.\n\n\n            It is ALWAYS PRESENT, including as 0. Zero is a real value — a\n            legacy row backfilled and not mutated since — and a `dep_add` is 0\n            too, because an edge acts on no single row's version. An absent\n            member would be ambiguous between the two.\n\n\n            DECODE IT AS A 64-BIT INTEGER, for the reason\n            `UpdateIssueRequest.expected_version` spells out: an IEEE-754-double\n            parser corrupts it silently, and the corruption only shows up as a\n            `precondition_failed` on the NEXT request. This member is the one\n            every other `revision` on the surface is spelled against, so the\n            warning belongs here most of all — a client that reads it through a\n            lossy parser here carries the damage into every guard it composes.\n\n    AddDependenciesRequest:\n      type: object\n      additionalProperties: false\n      required: [actor, edges]\n      properties:\n        actor:\n          type: string\n          minLength: 1\n          maxLength: 256\n          pattern: '^[^\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029]+$'\n          description: >-\n            Who is asserting the edges, under `ClaimRequest.actor`'s rules and\n            for the same reasons: the server trims it, refuses an empty result,\n            anything longer than 256 BYTES, and any control character including\n            newline. It is attributed on each `dependency_added` event a\n            genuinely new edge records, and interpolated into the storage commit\n            message.\n        edges:\n          type: array\n          minItems: 1\n          maxItems: 100\n          description: >-\n            The edges to assert, in the caller's order. An empty array is a\n            `400` rather than a successful no-op: a write request that writes\n            nothing is a client bug, and answering it cheerfully is how a client\n            whose own list filtered to nothing silently stops wiring anything.\n\n\n            The 100-edge cap is a bound on how long one request may hold a write\n            transaction, not a statement about batch semantics. Split a larger\n            graph; each request is atomic on its own — but note that splitting\n            it changes what the cycle gate can see, since the gate runs over one\n            request at a time.\n\n\n            A per-edge refusal names its offender as `edges[i].member`.\n          items:\n            $ref: '#/components/schemas/DependencyEdge'\n\n    DependencyEdge:\n      type: object\n      additionalProperties: false\n      required: [issue_id, depends_on_id, type]\n      description: >-\n        One directed edge, as a REQUEST names it. It is not `Dependency`, which\n        is the stored row `GET /v0/beads/dependencies` returns and carries the\n        columns storage assigned; this is the three members a caller supplies.\n      properties:\n        issue_id:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            The edge's SOURCE — the issue that depends on the other end. An\n            EXACT canonical id, and one this database holds: an edge follows its\n            source, so a source that names nothing is a `400`.\n        depends_on_id:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            The edge's TARGET — the issue depended upon. An exact canonical id,\n            an `external:` reference, or an id belonging to another repository.\n            Only an absence this database can SEE is refused. It must differ\n            from `issue_id`.\n        type:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            The edge type, from the same OPEN vocabulary `Dependency.type`\n            carries: checked for being a storable value, never for membership of\n            a known-types list, so a workspace's own type passes.\n\n    AddDependenciesResponse:\n      type: object\n      required: [added]\n      properties:\n        added:\n          type: array\n          description: >-\n            The request's edges, in REQUEST ORDER. It echoes the request because\n            all-or-nothing means it is either every edge or the call failed, so\n            a caller reporting what landed reads the result and never has to\n            know which of the two it is safe to read. An idempotent same-type\n            re-add is echoed like any other edge; the response does not say\n            which edges were genuinely new, because nothing a client does\n            depends on that.\n\n\n            Never null and never shorter than the request: a partial outcome\n            does not exist on this operation.\n          items:\n            $ref: '#/components/schemas/DependencyEdge'\n\n    RemoveDependencyRequest:\n      type: object\n      additionalProperties: false\n      required: [actor, issue_id, depends_on_id]\n      properties:\n        actor:\n          type: string\n          minLength: 1\n          maxLength: 256\n          pattern: '^[^\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029]+$'\n          description: >-\n            Who is removing the edge, under `ClaimRequest.actor`'s rules and\n            for the same reasons: the server trims it, refuses an empty result,\n            anything longer than 256 BYTES, and any control character including\n            newline. It is attributed on the `dependency_removed` event a real\n            removal records, and interpolated into the storage commit message.\n        issue_id:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            The edge's SOURCE — the issue that depends on the other end. An\n            EXACT canonical id: there is no fuzzy, prefix or substring\n            resolution on this surface.\n        depends_on_id:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            The edge's TARGET — the issue depended upon. An exact canonical id,\n            under `issue_id`'s rule.\n\n    RemoveDependencyResponse:\n      type: object\n      required: [removed]\n      properties:\n        removed:\n          type: boolean\n          description: >-\n            True when an edge was there and is now gone. FALSE IS A SUCCESS,\n            not a refusal: it says this pair carried no such edge, which is the\n            same graph a second removal leaves. Nothing was written for it.\n\n    ClaimRequest:\n      type: object\n      additionalProperties: false\n      required: [actor]\n      properties:\n        actor:\n          type: string\n          minLength: 1\n          maxLength: 256\n          pattern: '^[^\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029]+$'\n          description: >-\n            Who is claiming the issue. The server trims it, then refuses an\n            empty result, anything longer than 256 BYTES (the `maxLength` above\n            counts characters — the byte limit is the binding one), and any\n            control character including newline: Unicode category Cc — C0, DEL\n            and the C1 block — plus the U+2028/U+2029 line separators, which is\n            the set the `pattern` above spells.\n\n\n            The value is persisted as the assignee and interpolated into the\n            storage commit message, so an unvalidated newline would forge\n            audit-trail lines. C1 is refused for that same reason and not for\n            tidiness: U+0085 is a line break on a VT-conformant terminal, and\n            U+009B is the one-byte CSI introducer, which would make an actor an\n            escape-sequence payload in anything that prints an assignee.\n\n    ClaimResponse:\n      type: object\n      required: [issue, already_claimed]\n      properties:\n        issue:\n          $ref: '#/components/schemas/Issue'\n        already_claimed:\n          type: boolean\n          description: >-\n            True when the caller already held the issue and this call changed\n            nothing — the idempotent re-claim. A claim held by a DIFFERENT\n            actor is a 409, not a 200 with this flag.\n\n    ReleaseIssueRequest:\n      type: object\n      additionalProperties: false\n      required: [actor]\n      properties:\n        actor:\n          type: string\n          minLength: 1\n          maxLength: 256\n          pattern: '^[^\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029]+$'\n          description: >-\n            Who is releasing the claim. `ClaimRequest.actor`'s rules exactly:\n            the server trims it, then refuses an empty result, anything longer\n            than 256 BYTES (the `maxLength` above counts characters — the byte\n            limit is the binding one), and any control character including\n            newline. The value reaches the event the release records and the\n            storage commit message, so an unvalidated newline would forge\n            audit-trail lines.\n\n\n            It is REQUIRED, and for one reason beyond the audit trail: a\n            release is the moment work stops being owned, and the one question\n            asked of its history entry afterwards is who let it go. On the\n            unconditional path it is ALSO the ownership fence's subject — see\n            the operation description.\n        expected_assignee:\n          type: string\n          description: >-\n            Compare-and-set on the holder: the release proceeds only while the\n            issue is still assigned to this actor, and otherwise refuses with\n            `409` / `precondition_failed` naming this value, having written\n            nothing.\n\n\n            A MATCH REPLACES THE OWNERSHIP FENCE, so `actor` need not be the\n            holder. Sending it beside `force` is a 400: the two are answers to\n            the same question and they disagree.\n\n\n            THE COMPARISON IS SEPARATOR-INSENSITIVE AND NOTHING ELSE.\n            A run of `.`, `_` or `-` matches any other such run, so `agent-a`,\n            `agent_a` and `agent.a` are one holder — that is deliberate, so a\n            caller naming the holder under a different layer's spelling is a\n            match rather than a mismatch. NOTHING ELSE IS FORGIVEN: the value\n            is not trimmed and not case-folded, so `\" agent-a\"` and `Agent-a`\n            are both refusals. The server trims only far enough to tell a blank\n            expectation from a real one and never sends the trimmed form on, so\n            a caller that pads its expectation loses EVERY time rather than\n            intermittently. Compose it from a holder a read gave you.\n\n\n            THE EMPTY STRING IS A 400, and this is the one place this member\n            disagrees with `UpdateIssueRequest.expected_assignee`, where an\n            empty string is a real guard meaning \"expected unassigned\". Here\n            \"release a row nobody holds\" describes no release at all; a caller\n            that wants to assert a row is unheld is asking a READER a question,\n            not asking this operation to do nothing. Absent, and only absent,\n            selects the unconditional path.\n\n\n            IT IS NOT LENGTH- OR PATTERN-BOUNDED the way `actor` is, and the\n            asymmetry is deliberate: this value is COMPARED and never stored,\n            so a value no assignee column could hold simply cannot match, and\n            refusing it at the edge would be a refusal the role does not have.\n        force:\n          type: boolean\n          default: false\n          description: >-\n            Bypass the ownership fence, so an actor that is not the holder may\n            release the claim. It is the escape hatch `bd unclaim --force`\n            spells, for an abandoned claim whose holder crashed.\n\n\n            IT BYPASSES THE FENCE AND NOTHING ELSE. It does not make an unheld\n            row releasable, it does not make a closed one releasable, and it\n            never bypasses a precondition — sending it beside\n            `expected_assignee` is a 400 rather than a silent win for either.\n\n    ReleaseIssueResponse:\n      type: object\n      required: [issue, changed, revision]\n      properties:\n        issue:\n          $ref: '#/components/schemas/Issue'\n        changed:\n          type: boolean\n          description: >-\n            Whether the release WROTE the row. It is TRUE on every 200 this\n            operation returns, because every shape that would not write is\n            refused above it — an unheld row is a 409, not an idempotent no-op.\n            DO NOT WRITE A `changed: false` BRANCH: no request reaches one.\n\n\n            It is published rather than omitted because `claimIssue` and\n            `updateIssue` publish the same fact, and a caller holding all three\n            should not have to read them two ways. It is also the negative\n            space that answers \"where is the `already_released` member\" —\n            there is none, and the operation description says why.\n        revision:\n          type: integer\n          format: int64\n          description: >-\n            The row's optimistic-concurrency token AFTER the release, spelled\n            the way `UpdateIssueResponse.revision` spells it and carrying the\n            same promise: a read-modify-write loop composes its next\n            `expected_version` from THIS value, never from a number it\n            incremented itself.\n\n\n            A release REMINTS the token by design, so a caller that guarded a\n            following write on a version it read BEFORE the release will miss.\n            That is the point — a concurrent reclaim or close conflicts rather\n            than silently merging — and this member is how the caller stays in\n            step.\n\n\n            DECODE IT AS A 64-BIT INTEGER, for the reason\n            `UpdateIssueRequest.expected_version` spells out: an IEEE-754-double\n            parser corrupts it silently, and the corruption only shows up as a\n            `precondition_failed` on the NEXT request.\n\n    ClaimNextRequest:\n      type: object\n      additionalProperties: false\n      required: [actor]\n      properties:\n        actor:\n          type: string\n          minLength: 1\n          maxLength: 256\n          pattern: '^[^\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029]+$'\n          description: >-\n            Who is claiming. `ClaimRequest.actor`'s rules exactly: the server\n            trims it, then refuses an empty result, anything longer than 256\n            BYTES (the `maxLength` above counts characters — the byte limit is\n            the binding one), and any control character including newline. The\n            value is persisted as the assignee and interpolated into the storage\n            commit message, so an unvalidated newline would forge audit-trail\n            lines.\n\n\n            IT IS THE ONLY BODY MEMBER, and the FILTER travels in the query\n            string instead. That split is deliberate: the filter vocabulary is\n            `GET /v0/beads/ready`'s and is decoded by the same function, so\n            re-spelling it as a body object would create a second expression of\n            one predicate — and two spellings of one predicate eventually\n            disagree. The actor cannot go the same way: it is provenance that\n            lands in a column, and this surface has always carried that in a\n            body.\n\n    ClaimNextResponse:\n      type: object\n      properties:\n        claimed:\n          $ref: '#/components/schemas/IssueWithCounts'\n      description: >-\n        The outcome of one atomic take of ready work.\n\n\n        `claimed` IS ABSENT WHEN NOTHING WAS ELIGIBLE, and its absence is the\n        whole signal — there is no boolean beside it, because a second member\n        carrying the same fact is a second member that can disagree with the\n        first. A polling client branches on presence.\n\n\n        IT IS ALSO THE ONLY MEMBER, deliberately. A count of what was scanned,\n        or how many rows a racing agent had already taken, would describe a\n        moment inside a transaction that has committed and is not a fact about\n        the claim.\n\n\n        The row is `IssueWithCounts` — the element type `GET /v0/beads/ready`\n        returns and `bd ready --json` emits — hydrated INSIDE the transaction\n        that committed the claim, so its counts describe the state the claim\n        produced rather than a later one. It is not an `Issue` because the\n        listing this replaces answers with counts, and a client swapping the\n        composed pair for this operation should not lose a field doing it.\n\n    BatchCloseRequest:\n      type: object\n      additionalProperties: false\n      required: [actor, items]\n      properties:\n        actor:\n          type: string\n          minLength: 1\n          maxLength: 256\n          pattern: '^[^\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029]+$'\n          description: >-\n            Who is closing. `ClaimRequest.actor`'s rules exactly, and the value\n            is recorded against every item.\n        items:\n          type: array\n          minItems: 1\n          maxItems: 100\n          items:\n            $ref: '#/components/schemas/BatchCloseItem'\n          description: >-\n            The issues to close, in the order the caller asked for them. Every\n            item appears in `outcomes` at the same index.\n\n\n            An EMPTY array is a `400` rather than an empty answer, and the cap\n            is `batchCreateIssues`' cap for its reason: it bounds how long one\n            request may hold a write transaction.\n        session:\n          type: string\n          maxLength: 255\n          description: >-\n            The working session, recorded against every item that closes, under\n            `CloseIssueRequest.session`'s first-close-wins rule and bounds.\n        force:\n          type: boolean\n          default: false\n          description: >-\n            Bypass close policy — the open-children refusal and the live-blocker\n            refusal — for EVERY item, and nothing else. It never bypasses\n            validation and it never bypasses existence: an id that names nothing\n            refuses whether or not this is set. It is request-wide because the\n            flag that spells it is.\n\n    BatchCloseItem:\n      type: object\n      additionalProperties: false\n      required: [id]\n      properties:\n        id:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            Exact canonical issue id, resolved across BOTH planes. No fuzzy,\n            prefix or substring resolution — `IssueID`'s rule.\n\n\n            A DUPLICATE is admissible; see the operation description.\n        reason:\n          type: string\n          maxLength: 255\n          description: >-\n            Why THIS issue is closed. It is per item rather than per request\n            because `bd close a b c --reason x --reason y --reason z` has always\n            mapped them positionally, and one request-wide reason could not\n            express it. `CloseIssueRequest.reason`'s rules and first-close-wins.\n\n    BatchCloseResponse:\n      type: object\n      required: [outcomes]\n      properties:\n        outcomes:\n          type: array\n          items:\n            $ref: '#/components/schemas/CloseOutcome'\n          description: >-\n            Exactly one entry per requested item, in REQUEST ORDER — including\n            for items that refused, so a client walks this against its own\n            argument list without matching ids back up.\n\n    CloseOutcome:\n      type: object\n      required: [issue_id]\n      description: >-\n        What happened to ONE requested item.\n\n\n        `code` IS THE DISCRIMINATOR. Present means the item REFUSED and nothing\n        was written for it; absent means it succeeded, and `issue`,\n        `already_closed` and `open_children` are all present. A client branches\n        on `code` first and reads nothing else until it has.\n      properties:\n        issue_id:\n          type: string\n          description: >-\n            The id the caller asked for, echoed verbatim so an outcome can be\n            read without indexing back into the request.\n        issue:\n          $ref: '#/components/schemas/Issue'\n        already_closed:\n          type: boolean\n          description: >-\n            True when the issue was already closed and this item changed\n            nothing — the idempotent re-close, spelled the way\n            `CloseIssueResponse.already_closed` spells it. Present only on a\n            successful item.\n\n\n            A BATCH WHOSE ITEMS ARE ALL `true` LANDED NOTHING, and records no\n            history entry: a per-item success that changed nothing is not work\n            the caller did.\n        open_children:\n          type: integer\n          description: >-\n            How many open children the transaction observed for this item.\n\n\n            ITS MEANING FOLLOWS `code`, and both readings are the ones the\n            single close already publishes. On a SUCCESSFUL item it is always\n            present and is `CloseIssueResponse.open_children` — the number a\n            forced close bypassed, reported even for an idempotent re-close, and\n            `0` for an unforced close that got that far. On a REFUSED item its\n            PRESENCE is the discriminator between the two `not_closable`\n            refusals, exactly as it is on a problem document: present means open\n            children, absent means a live blocker.\n        code:\n          type: string\n          description: >-\n            This item's refusal, from `Problem.code`'s vocabulary and restricted\n            to `not_found` (the id names no row in either plane) and\n            `not_closable` (close policy refused it: open children, or a live\n            blocker — see `open_children`). ABSENT means the item succeeded.\n\n\n            It is the problem vocabulary rather than a second one because an\n            item refusal and a request refusal are the same question asked at\n            two scopes, and a client that had to learn two vocabularies to\n            classify one condition would be classifying the SCOPE rather than\n            the condition.\n        detail:\n          type: string\n          description: >-\n            Prose for a refusal, never load-bearing and present only with\n            `code`. It reflects the request and this server's own words rather\n            than the role's message, for the reason `Problem.detail` gives.\n\n    CloseIssueRequest:\n      type: object\n      additionalProperties: false\n      required: [actor]\n      properties:\n        actor:\n          type: string\n          minLength: 1\n          maxLength: 256\n          pattern: '^[^\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029]+$'\n          description: >-\n            Who is closing the issue. `ClaimRequest.actor`'s rules exactly: the\n            server trims it, then refuses an empty result, anything longer than\n            256 BYTES (the `maxLength` above counts characters — the byte limit\n            is the binding one), and any control character including newline.\n            The value reaches stored columns, event-stream attribution and the\n            storage commit message, so an unvalidated newline would forge\n            audit-trail lines.\n        reason:\n          type: string\n          maxLength: 255\n          description: >-\n            Why the issue is closed. Stored on the issue and read back as\n            `close_reason`. THE FIRST CLOSE WINS: an idempotent re-close writes\n            neither this nor `session`, so a replayed close cannot rewrite the\n            record of why the work ended. Refused for control characters, and\n            bounded by what the column holds rather than by the number above.\n        session:\n          type: string\n          maxLength: 255\n          description: >-\n            The working session that closed the issue, stored and read back as\n            `closed_by_session`, under the same first-close-wins rule and the\n            same bounds as `reason`.\n        force:\n          type: boolean\n          default: false\n          description: >-\n            Bypass close policy — the open-children refusal and the\n            live-blocker refusal — and nothing else. The refusals are the\n            ROLE's, so this endpoint cannot skip a guard by forgetting one\n            exists. A forced close still reports `open_children`.\n\n\n            IT BYPASSES POLICY, NEVER A PRECONDITION. `expected_version` is\n            still checked with it set, for the reason\n            `issueops.CloseRequest.Force` gives: a caller saying \"close it\n            anyway\" has said nothing about whether the row is still the one it\n            read.\n        expected_version:\n          type: integer\n          format: int64\n          description: >-\n            Requires the row's revision to equal this value BEFORE the close. A\n            miss refuses the whole request with `409 precondition_failed` and\n            writes nothing — `UpdateIssueRequest.expected_version`'s contract,\n            on the operation that closes one row.\n\n\n            IT IS CHECKED BEFORE THE IDEMPOTENT RE-CLOSE, which is the one place\n            this guard differs from the update's. A re-close of a row somebody\n            else has moved since the caller read it is a `409` and not the\n            200-with-`already_closed` the same body earns without a guard: a\n            replay whose premise has expired is a refusal the caller wants to\n            see, and it is the only way `already_closed` can be trusted as\n            \"nothing has happened here since\".\n\n\n            The token is the `revision` this operation's own response carries.\n            Compose the next expectation from the value a write ANSWERED with,\n            never from a number the client incremented itself: the token is\n            OPAQUE and compared for equality alone, so it has no predecessor a\n            client can compute. A first guarded close seeds itself from\n            `GET /v0/beads/issues/{id}`'s `revision` — the read that sources a\n            guard — or, for a chain already mid-flight, from an unguarded\n            lifecycle write or `POST /v0/beads/issues:batchApply`'s\n            `ApplyItemResult.revision`.\n\n\n            DECODE IT AS A 64-BIT INTEGER, for the reason\n            `UpdateIssueRequest.expected_version` spells out: an IEEE-754-double\n            parser corrupts it silently, and the corruption only surfaces as a\n            `precondition_failed` on the NEXT request.\n\n    CloseIssueResponse:\n      type: object\n      required: [issue, already_closed, open_children, revision]\n      properties:\n        issue:\n          $ref: '#/components/schemas/Issue'\n        already_closed:\n          type: boolean\n          description: >-\n            True when the issue was already closed and this call changed\n            nothing — the idempotent re-close, mirroring\n            `ClaimResponse.already_claimed`. The response still carries the\n            row, and `reason`/`session` were not rewritten.\n        open_children:\n          type: integer\n          description: >-\n            How many open children the close observed. Reported by a FORCED\n            close — including an idempotent re-close — because a caller that\n            bypassed the guard is exactly the caller that wants the number. An\n            unforced close that got this far had none, so it reports 0.\n        revision:\n          type: integer\n          format: int64\n          description: >-\n            The row's optimistic-concurrency token AFTER this close, spelled the\n            way `UpdateIssueResponse.revision` spells it.\n\n\n            It is here because `expected_version` is: a guard whose token no\n            response carries is a guard a caller cannot fill, and a\n            close-then-reopen or close-then-delete chain has to compose its next\n            expectation from the value the close ANSWERED with. An idempotent\n            re-close carries one too — the row still has a version, and a caller\n            that guarded a replay needs the token whether or not the replay\n            wrote.\n\n\n            DECODE IT AS A 64-BIT INTEGER, for the reason\n            `UpdateIssueResponse.revision` spells out.\n\n    ReopenIssueRequest:\n      type: object\n      additionalProperties: false\n      required: [actor]\n      properties:\n        actor:\n          type: string\n          minLength: 1\n          maxLength: 256\n          pattern: '^[^\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029]+$'\n          description: >-\n            Who is reopening the issue. `ClaimRequest.actor`'s rules exactly:\n            the server trims it, then refuses an empty result, anything longer\n            than 256 BYTES (the `maxLength` above counts characters — the byte\n            limit is the binding one), and any control character including\n            newline. The value reaches the `reopened` event's attribution and\n            the storage commit message, so an unvalidated newline would forge\n            audit-trail lines.\n        reason:\n          type: string\n          maxLength: 255\n          description: >-\n            Why the issue is being reopened. Recorded on the `reopened` EVENT\n            this move records — not on a field of the issue, and not carried in\n            the response, so a caller that wants it back reads the issue's\n            events. Refused for control characters, and bounded by what the\n            column holds rather than by the number above.\n        expected_version:\n          type: integer\n          format: int64\n          description: >-\n            Requires the row's revision to equal this value BEFORE the reopen. A\n            miss refuses the whole request with `409 precondition_failed` and\n            writes nothing — `CloseIssueRequest.expected_version`'s contract, on\n            the close's mirror.\n\n\n            IT IS CHECKED BEFORE THE NON-DONE NO-OP, the mirror of the close's\n            check-before-the-idempotent-re-close, and for the same reason: a\n            reopen of a row somebody else has moved is a `409` rather than the\n            200-with-`already_open` the same body earns unguarded, which is what\n            lets `already_open` be read as \"nothing has happened here since\".\n\n\n            The token is the `revision` this operation's own response carries;\n            compose the next expectation from a value a write ANSWERED with and\n            never from one the client computed. DECODE IT AS A 64-BIT INTEGER,\n            for the reason `UpdateIssueRequest.expected_version` spells out.\n\n    ReopenIssueResponse:\n      type: object\n      required: [issue, already_open, revision]\n      properties:\n        issue:\n          $ref: '#/components/schemas/Issue'\n        already_open:\n          type: boolean\n          description: >-\n            True when the issue was not in a done status and this call changed\n            nothing — idempotent, mirroring `CloseIssueResponse.already_closed`\n            and `ClaimResponse.already_claimed`. The response still carries the\n            row.\n        revision:\n          type: integer\n          format: int64\n          description: >-\n            The row's optimistic-concurrency token AFTER this reopen, spelled\n            the way `CloseIssueResponse.revision` spells it and here for the same\n            reason: a recovery flow that reopens and then re-closes composes its\n            next `expected_version` from this value. DECODE IT AS A 64-BIT\n            INTEGER.\n\n    MetadataValue:\n      # No x-go-type-import: the generator already imports encoding/json\n      # unconditionally, and naming it here emits the import a second time.\n      x-go-type: json.RawMessage\n      # nullable, because `null` is a REAL VALUE of this type rather than the\n      # absence of one — the whole point of the member. It is declared for the\n      # validating gateway in front of somebody's server, which would otherwise\n      # be entitled to reject a legitimate `\"expected\": null`.\n      nullable: true\n      # skip-optional-pointer, and this one is a CORRECTNESS fix rather than\n      # ergonomics. A *json.RawMessage cannot READ a present null: encoding/json\n      # handles a JSON null against a pointer by setting the pointer to nil\n      # before any UnmarshalJSON runs, so a generated client decoding\n      # {\"swapped\":false,\"current\":null} gets Current==nil — byte-identical to an\n      # omitted member, which on this operation means the opposite thing. A\n      # retry loop against a null-valued key would read \"absent\", swap with\n      # `expected` omitted, mismatch, and never converge. A bare json.RawMessage\n      # is an Unmarshaler in its own right, so it receives the literal `null` and\n      # stores it, while an omitted member leaves it nil and `omitempty` still\n      # omits it on the way out — the wire is unchanged in both directions.\n      x-go-type-skip-optional-pointer: true\n      description: >-\n        One metadata value: ANY JSON value — string, number, boolean, null,\n        array or object — because typed values enter through the explicit JSON\n        metadata path and persist in older rows. It is not a string, and a\n        client must not decode it as one.\n\n\n        Where a member of this type is OMITTED, the key is absent; where it is\n        present holding `null`, the key exists and holds null. Those are\n        different states and this surface reports both.\n\n    CompareAndSetMetadataRequest:\n      type: object\n      additionalProperties: false\n      required: [actor, key]\n      properties:\n        actor:\n          type: string\n          minLength: 1\n          maxLength: 256\n          pattern: '^[^\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029]+$'\n          description: >-\n            Who is performing the swap. `ClaimRequest.actor`'s rules exactly:\n            the server trims it, then refuses an empty result, anything longer\n            than 256 BYTES (the `maxLength` above counts characters — the byte\n            limit is the binding one), and any control character including\n            newline. It reaches the update event's attribution and the storage\n            commit message, so an unvalidated newline would forge audit-trail\n            lines.\n\n\n            It is REQUIRED here rather than optional, because a swap is a\n            coordination write between racing callers and the one question\n            asked of its history entry afterwards is which of them won.\n        key:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            The single metadata key to read and write. It must match the\n            workspace's metadata-key syntax — a letter or underscore, then\n            letters, digits, underscores, dots and slashes — so a key the query\n            layer could not later spell is refused rather than written.\n\n\n            ONE KEY, NOT A PATH: a dotted key like `gc.lease` names a top-level\n            key spelled with a dot, not a nested field. The metadata object's\n            nesting is VALUE structure, and this operation swaps whole values.\n        expected:\n          $ref: '#/components/schemas/MetadataValue'\n        value:\n          $ref: '#/components/schemas/MetadataValue'\n\n    CompareAndSetMetadataResponse:\n      type: object\n      required: [swapped]\n      properties:\n        swapped:\n          type: boolean\n          description: >-\n            Whether the precondition held and the transition applied. THIS IS\n            THE VERDICT and the only member to dispatch on. False is a lost\n            race — an answer, not a failure — and the response is still a 200.\n        current:\n          $ref: '#/components/schemas/MetadataValue'\n\n    UpdateIssueRequest:\n      type: object\n      additionalProperties: false\n      required: [actor, patch]\n      properties:\n        actor:\n          type: string\n          minLength: 1\n          maxLength: 256\n          pattern: '^[^\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029]+$'\n          description: >-\n            Who is editing the issue. `ClaimRequest.actor`'s rules exactly: the\n            server trims it, then refuses an empty result, anything longer than\n            256 BYTES (the `maxLength` above counts characters — the byte limit\n            is the binding one), and any control character including newline.\n            The value reaches the history entry's attribution and the storage\n            commit message, so an unvalidated newline would forge audit-trail\n            lines.\n        patch:\n          $ref: '#/components/schemas/IssuePatchBody'\n        expected_version:\n          type: integer\n          format: int64\n          description: >-\n            Requires the row's revision to equal this value before the patch. A\n            miss refuses the WHOLE request with `409 precondition_failed` and\n            writes nothing — `ApplyUpdateItem.expected_version`'s contract, on\n            the operation that patches one row.\n\n\n            The token is the `revision` this operation's own response carries,\n            and the same one `GET /v0/beads/issues/{id}` publishes — which is\n            where a first guarded write seeds itself, rather than from an\n            unguarded one or from `POST /v0/beads/issues:batchApply`'s\n            `ApplyItemResult.revision`.\n            Compose the next expectation from the value the write ANSWERED with,\n            never from a number the client incremented itself: the token is\n            OPAQUE and compared for equality alone, so it has no predecessor a\n            client can compute.\n\n\n            DECODE IT AS A 64-BIT INTEGER. Live tokens run past 5e17, where an\n            IEEE-754 double's ulp is already 64, so a parser that decodes JSON\n            numbers as doubles — JavaScript's `JSON.parse`, Go's `any`, Python's\n            `float` — hands back a value NEAR the token that is not it, and the\n            guard is refused against a row nothing else touched.\n        expected_status:\n          type: string\n          maxLength: 255\n          description: >-\n            Requires the issue's status to equal this value before the patch. A\n            miss refuses the whole request with `409 precondition_failed`.\n\n\n            Unlike `expected_version` this one is readable: `Issue.status` is on\n            every read of this surface, so a caller can guard a status\n            transition without any token at all.\n        expected_assignee:\n          type: string\n          maxLength: 255\n          description: >-\n            Requires the issue's assignee to equal this value before the patch.\n            A match AUTHORIZES the requested `patch.assignee` transfer: this\n            compare-and-set replaces the ordinary anti-steal fence, so it must\n            not be combined with `force_assignee_transfer`. A miss refuses the\n            whole request with `409 precondition_failed`.\n        force_close_policy:\n          type: boolean\n          default: false\n          description: >-\n            Bypasses ONLY close policy — the open-children refusal and the live\n            blocker refusal — for a `patch.status` that crosses into the\n            workspace's done category. It has no effect without such a status\n            change, and it never bypasses validation, the preconditions above,\n            or the assignee fence.\n        force_assignee_transfer:\n          type: boolean\n          default: false\n          description: >-\n            Bypasses ONLY a genuine transfer away from a live foreign\n            in-progress owner. Reasserting the exact current assignee is\n            idempotent and needs no force. It requires `patch.assignee` — a\n            request setting it without one is a `400` — and it must be false\n            when `expected_assignee` is sent.\n\n    IssuePatchBody:\n      type: object\n      additionalProperties: false\n      description: >-\n        The fields to write. Every member is optional and PRESENCE is the\n        signal: a member present is written, a member absent is untouched. An\n        empty object is a `400` — a write that writes nothing is a client bug.\n\n\n        This is a deliberate SUBSET of the fields an issue carries; the members\n        it does not spell are future surface rather than oversights, and\n        `updateIssue`'s own description says which and why.\n\n\n        It now agrees with `ApplyPatchBody` on every member it publishes, and\n        the two differ only in the SHAPE of two of them: `labels` is complete\n        replacement here and an ordered add/remove/replace patch there, because\n        that operation edits a set it did not compose. Everything else — down to\n        the `metadata` algebra and the four nullable members — is one\n        definition, so a caller cannot get a different answer for the same edit\n        depending on which operation it sent.\n      properties:\n        title:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            The issue's title. Must not be blank after trimming; the length\n            bound is what the column holds.\n        description:\n          type: string\n        design:\n          type: string\n        acceptance_criteria:\n          type: string\n        notes:\n          type: string\n          description: >-\n            Replaces the notes. Mutually exclusive with `append_notes`; sending\n            both is a `400`.\n        append_notes:\n          type: string\n          description: >-\n            Appends to the notes rather than replacing them. Mutually exclusive\n            with `notes`.\n        priority:\n          type: integer\n          minimum: 0\n          maximum: 4\n        issue_type:\n          type: string\n          maxLength: 255\n          description: >-\n            The issue type, from this workspace's own configured vocabulary. A\n            type outside it is refused by the ROLE and reaches the client as a\n            `400` — this server cannot read the vocabulary without a\n            transaction, so it checks only what this schema declares.\n        status:\n          type: string\n          maxLength: 255\n          description: >-\n            The issue's status, from this workspace's own configured vocabulary.\n\n\n            A STATUS THAT CROSSES INTO THE DONE CATEGORY ANSWERS TO CLOSE\n            POLICY: the update is refused with `409 not_closable` for open\n            children or a live blocker unless `force_close_policy` is set. A\n            done-to-done change and a move OUT of the done category are\n            unaffected.\n\n\n            IT IS NOT A SECOND SPELLING OF `{id}:close` AND `{id}:reopen`. Those\n            two carry semantics a status write has nowhere to put — the reason\n            and session under first-close-wins, the done-status normalization,\n            the `already_closed`/`already_open` idempotence flags — and they\n            remain the operations to reach for when what you mean is \"close\n            this\". This member is for the edit that moves a status ALONGSIDE\n            other fields in one transaction, which is the thing two calls cannot\n            do. `ApplyPatchBody.status` has meant exactly this since\n            `issues:batchApply` landed.\n        assignee:\n          type: string\n          maxLength: 255\n          description: >-\n            The assignee. A transfer away from a live foreign in-progress owner\n            is refused with `409 already_claimed` unless\n            `force_assignee_transfer` is set or `expected_assignee` matched.\n            Setting it to the empty string unassigns.\n\n\n            `{id}:claim` remains the operation that ACQUIRES work: it carries\n            its own eligibility rules and sets the status with the assignee in\n            one act. This member is the raw write, fenced.\n        parent_id:\n          type: string\n          maxLength: 255\n          description: >-\n            Replaces the issue's parents atomically: a nonempty value makes THAT\n            issue the only parent, and an EMPTY STRING removes every parent-child\n            edge the issue has. Labels are not inherited — that is a create-time\n            choice (`CreateIssueRequest.inherit_labels_from_parent`) and a\n            reparent does not re-run it.\n\n\n            IT IS A GRAPH EDIT, and it earns the graph's refusals: a new parent\n            this workspace holds no row for is a `400`, a pair that already\n            carries an edge of another type is `409 dependency_exists`, and a\n            move under the issue's own descendant is `409 dependency_cycle` —\n            the PLAIN one, carrying no `issue_id`/`blocker_id`/\n            `blocker_is_ancestor`, because the hierarchy refusal answers only to\n            blocking edges and this member writes a `parent-child` edge.\n            Naming the issue itself is a `400`. One call rather than a\n            remove-then-add pair, which is the whole reason it is here: the\n            two-call spelling leaves the issue parentless if the second call\n            fails.\n        labels:\n          type: array\n          items:\n            type: string\n            maxLength: 255\n          description: >-\n            COMPLETE REPLACEMENT of the label set. An empty array clears every\n            label.\n\n\n            It is the REPLACE half of the same ordered edit `ApplyPatchBody`\n            spells as `labels.replace`, and `add_labels`/`remove_labels` are the\n            other two. All three may travel together and are applied in that\n            order — replace, then add, then remove — so REMOVAL WINS when one\n            label appears in more than one of them. That is the role's own\n            algebra, not this operation's arrangement of it.\n\n\n            THE SHAPE DIFFERS FROM `ApplyPatchBody`'s, which nests the three\n            under one `labels` object, and the difference is historical rather\n            than meaningful. This member shipped as a bare array; nesting it now\n            would RE-TYPE a published member, which is the one kind of change\n            this document has no additive route for. Two flat siblings is the\n            shape that could be added — and it is the shape `notes` and\n            `append_notes` already use for the same replace/increment pair.\n        add_labels:\n          type: array\n          items:\n            type: string\n            maxLength: 255\n          description: >-\n            Labels to add, applied AFTER any `labels` replacement.\n\n\n            IT IS NOT MUTUALLY EXCLUSIVE WITH `labels`, and that is the\n            difference from `append_notes`, which is. The role defines an order\n            over all three label edits, so sending a replacement and an addition\n            together has a defined result; notes have no such algebra, so there\n            the two are a contradiction and are refused.\n\n\n            IT IS WHY THIS PAIR EXISTS. A caller that reads a row, adds one\n            label and writes the whole set back silently drops any label another\n            writer added in between — and `bd label add` and every agent that\n            tags work concurrently are exactly that caller. A replacement can\n            only be composed safely by a writer that knows it is alone.\n\n\n            Repetition is free: a label named twice is applied once, and adding\n            one the issue already carries changes no labels. (Whether the\n            RESPONSE reports `changed: false` is a fact about the whole patch —\n            see `remove_labels`.) An EMPTY-STRING entry is DROPPED rather than\n            refused — a label row carrying `\"\"` renders as nothing and matches\n            nothing, so writing one would only store junk, and refusing the\n            whole update would let one stray entry fail an otherwise-good edit.\n        remove_labels:\n          type: array\n          items:\n            type: string\n            maxLength: 255\n          description: >-\n            Labels to remove, applied AFTER `labels` and `add_labels`, so\n            REMOVAL WINS over both.\n\n\n            Removing a label the issue does not carry CHANGES NO LABELS; it is\n            not a `404` and not a conflict. Whether the RESPONSE reports\n            `changed: false` is a fact about the whole patch, not about this\n            member — a request that also moved a title changed the row. The same\n            repetition and empty-string rules as `add_labels` apply, and a value\n            longer than the column is refused here as it is there — the length\n            rule is about what a label may BE, not about whether this particular\n            row happens to carry one.\n        estimated_minutes:\n          type: integer\n          nullable: true\n          description: 'Explicit `null` CLEARS the estimate.'\n        external_ref:\n          type: string\n          nullable: true\n          maxLength: 255\n          description: 'Explicit `null` CLEARS the reference.'\n        due_at:\n          type: string\n          format: date-time\n          nullable: true\n          description: 'RFC 3339. Explicit `null` CLEARS the due date.'\n        defer_until:\n          type: string\n          format: date-time\n          nullable: true\n          description: 'RFC 3339. Explicit `null` CLEARS the deferral.'\n        metadata:\n          $ref: '#/components/schemas/ApplyMetadataPatch'\n\n    UpdateIssueResponse:\n      type: object\n      required: [issue, changed, revision]\n      properties:\n        issue:\n          $ref: '#/components/schemas/Issue'\n        changed:\n          type: boolean\n          description: >-\n            Whether the request persisted a semantic mutation. A same-value\n            patch is a 200 with `changed: false` rather than an error —\n            idempotent, like every replay answer on this surface.\n        revision:\n          type: integer\n          format: int64\n          description: >-\n            The row's optimistic-concurrency token AFTER this write, spelled the\n            way `ApplyItemResult.revision` spells it.\n\n\n            It is here because `expected_version` is: a guard whose token no\n            response carries is a guard a caller cannot fill. A read-modify-write\n            loop composes its next expectation from THIS value and never from a\n            number it incremented itself, for the reason\n            `compareAndSetMetadata` gives about a value the store renormalizes.\n            `GET /v0/beads/issues/{id}`'s `revision` is the read that publishes\n            the same token, and this member agrees with it.\n\n\n            DECODE IT AS A 64-BIT INTEGER, for the reason\n            `UpdateIssueRequest.expected_version` spells out: an IEEE-754-double\n            parser corrupts it silently, and the corruption only shows up as a\n            `precondition_failed` on the NEXT request.\n\n    CreateIssueRequest:\n      type: object\n      additionalProperties: false\n      required: [actor, title]\n      description: >-\n        One issue, its parent, its explicit edges and its waits-for gate,\n        created as one act.\n\n\n        It is FLAT rather than nesting the issue's fields under an `issue`\n        member, unlike `UpdateIssueRequest`'s `patch`: a patch has to distinguish\n        a member that is absent from one set to its zero value, and a create has\n        no such distinction to make — an absent member is the workspace default,\n        which is the same answer a nested object would have given.\n\n\n        The issue members mirror `ApplyCreateItem` exactly, minus that schema's\n        two plan-only members (`key` and `metadata_refs`, which name items of a\n        request this operation has only one of). What this adds is the edge\n        vocabulary that operation moves into `dep_add` items: `parent_id`,\n        `inherit_labels_from_parent`, `dependencies` and `waits_for`.\n      properties:\n        actor:\n          type: string\n          minLength: 1\n          maxLength: 256\n          pattern: '^[^\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029]+$'\n          description: >-\n            Who is creating the issue. `ClaimRequest.actor`'s rules exactly: the\n            server trims it, then refuses an empty result, anything longer than\n            256 BYTES (the `maxLength` above counts characters — the byte limit\n            is the binding one), and any control character including newline.\n            The value reaches the created edges' author column, the history\n            entry's attribution and the storage commit message, so an\n            unvalidated newline would forge audit-trail lines.\n\n\n            It is NOT the issue's `created_by`, which this operation does not\n            publish: this is the caller-asserted provenance of the ACT, and the\n            row's own author column is left to the implementation.\n        id:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            An explicit id for the new row, CREATE-ONLY: an id that already\n            names a stored row is a `409` `already_exists` and nothing is\n            written — never an adoption and never an overwrite. It is checked\n            against the workspace's configured issue prefix unless\n            `force_id_prefix` is set. Absent is the ordinary case and the server\n            mints one.\n        title:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: The issue's title. Must not be blank after trimming.\n        description:\n          type: string\n        design:\n          type: string\n        acceptance_criteria:\n          type: string\n        notes:\n          type: string\n        issue_type:\n          type: string\n          maxLength: 255\n          description: >-\n            Issue type. Spelled `issue_type` rather than `type`, matching the\n            member `Issue` carries, and validated against the built-ins plus the\n            workspace's configured custom types by the ROLE — this server cannot\n            read that vocabulary without a transaction, so it checks only what\n            this schema declares and an unknown one arrives as a `400`.\n\n\n            SEND ONE. The member is optional in this schema and the role\n            validates the EMPTY type against the same vocabulary as any other,\n            where it is neither a built-in nor a configured type — so an omitted\n            `issue_type` is refused with everything else the request asked for.\n            It stays optional because the vocabulary belongs to the workspace and\n            a deployment may configure a default this server cannot read, but it\n            is not optional in practice on any workspace shipped today.\n            `POST /v0/beads/issues:batchCreate` has the same property and does\n            not say so, which is why this member does.\n        status:\n          type: string\n          maxLength: 255\n          description: >-\n            The status the issue is created in, from this workspace's own\n            configured vocabulary. Absent means the workspace's own default,\n            which is `open` today — unlike `issue_type`, the role fills this one\n            in before it validates.\n        priority:\n          type: integer\n          minimum: 0\n          maximum: 4\n          description: 0 is P0/critical. Absent means the workspace default.\n        assignee:\n          type: string\n          maxLength: 255\n        owner:\n          type: string\n          maxLength: 255\n          description: >-\n            The human owner, which is a different member from `assignee`: the\n            assignee is who is working it now, the owner is who it is attributed\n            to.\n        labels:\n          type: array\n          items:\n            type: string\n            maxLength: 255\n          description: >-\n            The complete label set the issue is created with. Authoritative, not\n            a patch — a create has nothing to add to. `inherit_labels_from_parent`\n            adds the parent's labels on top of it.\n        estimated_minutes:\n          type: integer\n          description: >-\n            An estimate in minutes. Absent leaves it unset. NOT nullable, unlike\n            `IssuePatchBody.estimated_minutes`: a create has nothing to clear, so\n            `null` here would be a second spelling of omission and is a `400`.\n        external_ref:\n          type: string\n          maxLength: 255\n          description: 'e.g. `gh-9`. Not nullable, for `estimated_minutes`'' reason.'\n        due_at:\n          type: string\n          format: date-time\n          description: 'RFC 3339. Not nullable, for `estimated_minutes`'' reason.'\n        defer_until:\n          type: string\n          format: date-time\n          description: >-\n            RFC 3339. The issue is hidden from ready work until then. Not\n            nullable, for `estimated_minutes`' reason.\n        sender:\n          type: string\n          maxLength: 255\n          description: >-\n            Who sent this, for the message-shaped rows an orchestrator creates.\n            Stored verbatim and interpreted by nothing on this surface.\n        metadata:\n          $ref: '#/components/schemas/MetadataValue'\n        ephemeral:\n          type: boolean\n          default: false\n          description: >-\n            Creates the issue on the EPHEMERAL plane rather than the durable one,\n            exactly as it does for `POST /v0/beads/issues:batchApply`. Mutually\n            exclusive with `no_history`.\n        no_history:\n          type: boolean\n          default: false\n          description: >-\n            Creates the issue on the ephemeral plane WITHOUT history, and without\n            the garbage collection an ordinary ephemeral row is eligible for.\n            Mutually exclusive with `ephemeral`.\n        parent_id:\n          type: string\n          maxLength: 255\n          description: >-\n            Creates a typed `parent-child` edge from the new issue to this\n            target. It must not duplicate an edge `dependencies` already spells;\n            naming the same pair twice with two types is a `400`.\n        inherit_labels_from_parent:\n          type: boolean\n          default: false\n          description: >-\n            Copies the parent's labels onto the new issue at creation, on top of\n            `labels`. It has no effect without `parent_id`.\n\n\n            The DEFAULT IS FALSE and diverges from `bd create --parent`, whose\n            default is to inherit. A wire caller sends what it means: this\n            operation has no `--no-inherit-labels` to turn off, and a create that\n            silently acquired labels the request never named would be a set the\n            caller has to read back to learn.\n        dependencies:\n          type: array\n          maxItems: 100\n          items:\n            $ref: '#/components/schemas/CreateIssueDependency'\n          description: >-\n            The complete set of explicit edges created with the issue.\n            Authoritative, not a patch. Every edge is written in the same\n            transaction as the row, so an edge this request cannot write means no\n            issue either.\n\n\n            A TARGET NEED NOT BE A ROW THIS DATABASE HOLDS: an `external:`\n            reference and an id belonging to another repository are legitimate\n            targets, so only an absence this database can SEE is refused —\n            `ApplyDepAddItem`'s rule, unchanged.\n        waits_for:\n          $ref: '#/components/schemas/CreateIssueWaitsFor'\n        force_id_prefix:\n          type: boolean\n          default: false\n          description: >-\n            Permits an explicit `id` outside the workspace's configured issue\n            prefix. It bypasses ONLY that check: it is not a force on the\n            create-only guard, so an occupied id is still a `409`.\n\n    CreateIssueDependency:\n      type: object\n      additionalProperties: false\n      required: [target_id, type]\n      description: >-\n        One edge created with the issue. It carries `reverse` where\n        `BatchCreateDependency` does not, because that operation's items have no\n        id a target could point back at and this one's issue does.\n      properties:\n        target_id:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: The other endpoint of the edge.\n        type:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            The edge type, from the same OPEN vocabulary `Dependency.type`\n            carries: checked for BEING a storable value, never for membership of\n            a known-types list, so a workspace's own type passes.\n        reverse:\n          type: boolean\n          default: false\n          description: >-\n            Writes the edge from `target_id` TO the new issue rather than from\n            it. It is what lets a create declare an edge that points INTO the row\n            being minted — the id no caller could have spelled beforehand — and\n            it is the member that makes `dependency_cycle` reachable on this\n            operation at all.\n        metadata:\n          $ref: '#/components/schemas/MetadataValue'\n\n    CreateIssueWaitsFor:\n      type: object\n      additionalProperties: false\n      required: [spawner_id]\n      description: >-\n        A typed `waits-for` edge from the new issue to a spawner whose children\n        gate it. It records a readiness primitive; it does not define scheduling\n        or execution policy.\n\n\n        IT IS A TYPED MEMBER HERE AND A METADATA BLOB ON\n        `POST /v0/beads/issues:batchApply`, and the difference follows the ROLE\n        rather than taste: `CreateRequest.WaitsFor` is a typed field that gets\n        the gate defaulted and the \"must not duplicate an explicit edge\" check,\n        while that operation's `dep_add` item is one generic edge with no typed\n        field to reach. One spelling per operation, and each is its role's.\n      properties:\n        spawner_id:\n          type: string\n          minLength: 1\n          maxLength: 255\n          description: >-\n            The dependency target whose children are observed. It must not\n            duplicate an edge `dependencies` or `parent_id` already spells.\n        gate:\n          type: string\n          maxLength: 255\n          description: >-\n            The readiness condition: `all-children` or `any-children`. Absent or\n            empty defaults to `all-children`. A value that is neither is refused\n            by the ROLE and reaches the client as a `400`.\n\n    Problem:\n      type: object\n      description: >-\n        RFC 9457 problem detail. This is the only error shape on this surface.\n        The core declares `type`; this server never emits it, so `about:blank`\n        is implied throughout.\n      required: [status, title, code, request_id]\n      properties:\n        type:\n          type: string\n          format: uri\n          description: >-\n            RFC 9457 problem type. This server never emits it, so `about:blank`\n            is implied. A deployment that hosts problem documentation MAY supply\n            it: one stable URI per status+code pair, dereferencing to\n            documentation for that pair. It restates identity that `code`\n            already carries, so a client MUST NOT dispatch on it and a server\n            MUST NOT use it to subdivide a code.\n        status:\n          type: integer\n          description: The HTTP status code, repeated in the body.\n        title:\n          type: string\n          description: The status phrase. Human-facing; never dispatch on it.\n        code:\n          type: string\n          description: >-\n            The stable machine-readable reason, and the ONLY member a client\n            may dispatch on. v0's vocabulary: `invalid_argument` (400, also\n            emitted by the Host-header middleware on any route),\n            `invalid_cursor` (400), `unauthenticated` (401, only on a server\n            configured with a token file), `not_found` (404),\n            `already_claimed` (409), `not_claimable` (409),\n            `not_closable` (409), `not_releasable` (409),\n            `dependency_cycle` (409), `dependency_exists` (409),\n            `already_exists` (409), `precondition_failed` (409),\n            `events_journal_disabled` (409), `events_journal_truncated` (410),\n            `busy` (503),\n            `db_unavailable` (503), `events_watch_saturated` (503),\n            `internal` (500). Renaming or removing a status+code pair is a\n            breaking change; ADDING one is not, so clients MUST default-branch\n            on unknown values and fall back to the status class (unknown 4xx →\n            client bug, fail loud; unknown 503 → retry per `Retry-After`; other\n            unknown 5xx → server fault).\n        detail:\n          type: string\n          description: >-\n            Optional prose, never load-bearing. For 5xx codes it is a FIXED\n            string per code and carries nothing about the underlying failure:\n            driver and dial errors routinely embed the DSN, database user and\n            host:port, and this API supports binding beyond loopback. It is\n            fixed for `unauthenticated` too, and for the mirror-image reason:\n            the caller's own input there is a credential, so echoing it would\n            write the token into every client log and proxy trace on the way\n            back. Other 4xx details reflect the caller's own input back and are\n            specific.\n        param:\n          type: string\n          description: >-\n            With `invalid_argument`: the offending query parameter, body member\n            or header name. Present on every 400 except a body that fails to\n            parse at all.\n\n\n            With `precondition_failed`: the body member carrying the guard that\n            missed. It is the same spelling a 400 on the same operation would\n            use, so a client reads one member to find the offending input\n            whichever way the request was refused.\n        reason:\n          type: string\n          description: >-\n            With `invalid_argument`: `unknown_parameter` (this server does not\n            know that parameter — version skew; degrade or fall back),\n            `invalid_value` (the value is not one this server will act on:\n            malformed, out of vocabulary, or — for `limit=0` under\n            `--allow-non-loopback` — legal but refused in this server's\n            configuration; `detail` says which), or `project_mismatch` (the\n            `Bd-Project-Id` header named a project this server does not serve —\n            a document-level refusal like the Host-header 400, raised on every\n            enforced route, and the one that carries `server_project_id`; see\n            the document-level rule). Either way the recovery is to send\n            something different, never to retry the same request. The set may\n            grow; default-branch on unknown values.\n        assignee:\n          type: string\n          description: >-\n            With `already_claimed`: the actor currently holding the issue, read\n            inside the transaction that refused.\n\n\n            IT IS OPTIONAL ON EVERY OPERATION BUT THE CLAIM. `POST\n            /v0/beads/issues/{id}:claim` always carries it, because its\n            conflict path reads the row it lost to. `PATCH\n            /v0/beads/issues/{id}` and `POST /v0/beads/issues:batchApply` carry\n            it only when the refusing transaction reported a holder, and `POST\n            /v0/beads/issues/{id}:release` never does — the ownership fence\n            refuses without naming anyone. An absent member means \"this refusal\n            could not name the holder\", never \"nobody holds it\"; re-read the\n            row.\n        issue_status:\n          type: string\n          description: >-\n            With `already_claimed` or `not_claimable`: the issue's status at\n            the moment of refusal.\n        open_children:\n          type: integer\n          description: >-\n            With `not_closable`: how many open children the transaction that\n            refused the close observed, read inside that transaction rather\n            than parsed out of `detail`.\n\n\n            PRESENT ONLY for the open-children refusal. The other\n            `not_closable` refusal is a live blocker and carries no such\n            member, so member presence — not prose — is how a client tells the\n            two apart. Both are bypassed by `force`.\n        existing_type:\n          type: string\n          description: >-\n            With `dependency_exists`: the type of the edge the pair already\n            carries, read inside the refusing transaction.\n        requested_type:\n          type: string\n          description: >-\n            With `dependency_exists`: the type the request asked for. Together\n            with `existing_type` it is the whole refusal, so a client never\n            parses either out of `detail`.\n        issue_id:\n          type: string\n          description: >-\n            With `dependency_cycle`, and ONLY on the hierarchy refusal: the\n            issue the requested blocking edge would have gated. Its PRESENCE is\n            the discriminator — absent means a plain scheduling cycle, present\n            means the edge pointed at the issue's own ancestor or descendant.\n\n\n            The conflicting hierarchy may exist only inside the rolled-back\n            batch, so no read after the fact can recover it: the refusing\n            transaction is the only place this member can come from.\n        blocker_id:\n          type: string\n          description: >-\n            With `dependency_cycle`, hierarchy refusal only: the ancestor or\n            descendant the edge named as blocker. See `issue_id`.\n        blocker_is_ancestor:\n          type: boolean\n          description: >-\n            With `dependency_cycle`, hierarchy refusal only: true when\n            `blocker_id` is an ANCESTOR of `issue_id` (which cannot close until\n            its descendants finish, so the gate would never clear), false when\n            it is a DESCENDANT (blocked status cascades, so it would inherit the\n            block and never close). Both polarities are reported; this member is\n            never omitted to mean false. See `issue_id`.\n        expected_version:\n          type: integer\n          format: int64\n          description: >-\n            With `precondition_failed`: the row `revision` the request guarded\n            on, echoed from the request itself.\n\n\n            THE EXPECTED/ACTUAL PAIRS ARE SPLIT BY TYPE rather than carried as\n            one polymorphic `expected`/`actual`, and the reason is this\n            document's: a member that is \"a version or a status or an assignee\"\n            is a schema alternation, and no composition keyword is available to\n            spell one here (see `ApplyItem`). Three typed pairs cost three\n            member names and are readable by a generated client without a cast.\n        actual_version:\n          type: integer\n          format: int64\n          description: >-\n            With `precondition_failed`: the `revision` the row was found\n            holding, read inside the transaction that refused the guard.\n\n\n            PRESENT ONLY WHERE THE REFUSING OPERATION CAN REPORT IT. An\n            all-or-nothing operation rolls its transaction back, so a value read\n            after the fact would describe a row the refusal never saw; where the\n            role behind an operation does not carry the observed value, this\n            member is omitted rather than reconstructed. Its absence therefore\n            means \"this server cannot tell you what it found\", never \"it found\n            zero\".\n\n\n            NO v0 OPERATION EMITS IT TODAY, nor `actual_status` or\n            `actual_assignee`. Every operation that publishes a guard refuses\n            all-or-nothing, and none of the roles behind them carries the\n            observed value out of the rolled-back transaction. The three\n            members are declared so that an operation whose role CAN report\n            what it found is an addition rather than a wire change — a client\n            must not wait for them, and must never read their absence as a\n            value.\n        expected_status:\n          type: string\n          description: >-\n            With `precondition_failed`: the status the request guarded on,\n            echoed from the request. See `expected_version`.\n        actual_status:\n          type: string\n          description: >-\n            With `precondition_failed`: the status the row was found holding.\n            Present under `actual_version`'s rule.\n        expected_assignee:\n          type: string\n          description: >-\n            With `precondition_failed`: the assignee the request guarded on,\n            echoed from the request. See `expected_version`.\n        actual_assignee:\n          type: string\n          description: >-\n            With `precondition_failed`: the assignee the row was found holding.\n            Present under `actual_version`'s rule.\n        item_index:\n          type: integer\n          description: >-\n            On a batch operation whose items are heterogeneous: the position in\n            `items` of the item that earned the refusal, read from the role's own\n            typed error rather than parsed out of `detail`.\n\n\n            The request is all or nothing, so there is no per-item result array\n            for a client to find the offender in — these four `item_*` members\n            are the only place it exists.\n        item_kind:\n          type: string\n          description: >-\n            The `kind` of the item at `item_index`, so a client can dispatch on\n            what the item was doing without walking its own request back.\n        item_key:\n          type: string\n          description: >-\n            The refused item's own `key`, or the key its target ref named.\n            ABSENT when the item named nothing symbolically, which is a real\n            state rather than a gap: not every item has a key.\n        item_issue_id:\n          type: string\n          description: >-\n            The id the refused item was acting on, where one had been resolved\n            before the refusal. ABSENT when the refusal happened before\n            resolution — a create whose id was never minted, or a ref that\n            resolved to nothing.\n\n\n            IT IS NOT `issue_id`, and the divergence is load-bearing rather than\n            verbose: `issue_id` is a PRESENCE-DISCRIMINATING member of the\n            `dependency_cycle` hierarchy refusal, so a batch operation reusing it\n            would make that discriminator fire on refusals it says nothing about.\n        declared_later:\n          type: boolean\n          description: >-\n            With `invalid_argument` on a batch operation whose items may name\n            each other: whether the unresolvable key IS declared by the request,\n            at a LATER index.\n\n\n            True is an ORDERING mistake — a key reaches backward only — and false\n            is a key nothing in the request declares, which is a typo or a\n            missing item. A client acts differently on each. Both polarities are\n            emitted and the member is never omitted to mean false: an absent\n            member says the refusal was not about a key at all.\n        since:\n          type: integer\n          format: int64\n          description: >-\n            With `events_journal_truncated`: the checkpoint the reported window\n            begins after.\n\n\n            It is NOT always the value the request sent. In the ordinary case —\n            the prefix you asked for was pruned — it IS your checkpoint. When\n            the prefix is intact but the retained window has an interior hole,\n            it is instead the last seq the server could serve contiguously from\n            your checkpoint, and `floor` is where the next intact stretch\n            begins. It never reports a value BELOW what you sent, so echoing it\n            back can never re-deliver records you already hold.\n        floor:\n          type: integer\n          format: int64\n          description: >-\n            With `events_journal_truncated`: the lowest seq still retained, or\n            `head + 1` when the journal retains nothing at all. Resuming from\n            `floor - 1` continues with a known, explicit gap.\n        head:\n          type: integer\n          format: int64\n          description: >-\n            With `events_journal_truncated`: the highest seq this journal has\n            ever assigned. It never decreases under a prune, so `floor > head`\n            means the journal was pruned empty and the caller is at the end of\n            its history. Emitted even when zero.\n        request_id:\n          type: string\n          description: >-\n            Opaque correlation id for this request, echoed in the server's\n            request log line. Never a dispatch key and never a retry key.\n            (This server mints per-process ids that do not survive a restart;\n            a deployment may substitute any identifier with the same\n            log-correlation property, such as an edge trace id.)\n        server_project_id:\n          type: string\n          description: >-\n            With `invalid_argument` / `reason: \"project_mismatch\"` ONLY: the\n            project id this server actually serves, so a client that stamped a\n            `Bd-Project-Id` can tell a wrong-server refusal from a malformed one\n            without parsing `detail`. It is set on that refusal and on no other,\n            and never on a refusal raised before the stamp is checked — the Host\n            gate, or a deployment's authentication layer — so its PRESENCE is the\n            signal that this specific check fired.\n"}