# Nano Workforce control API — OpenAPI (authoring source, ADR 0059).
#
# This YAML is the single source of truth for the app HTTP surface. The Urban toolkit
# derives typed request/response contracts + runtime validators from it (urban gen), and
# each operationId is implemented by a delegate in operations/<operationId>.ts. Mounted
# under /app/api (off the framework-reserved /app page-runtime namespace). Swagger UI is
# served at /app/api-docs. Prefer editing this file over the generated artifacts.
openapi: 3.0.3
info:
  title: Nano Workforce control API
  version: 1.0.0
  description: "The externally-facing control surface an operator, an automation harness, or an LLM
    uses to observe and steer PR-convergence and planning runs. Contract-first (ADR 0058/0059): the
    toolkit derives typed request/response contracts + runtime validators from this document and
    each `operationId` is implemented by a delegate module in `operations/`. Mounted under base
    `/app/api`, kept off the framework-reserved `/app` page-runtime namespace (which owns
    `/app/runtime.js`, `/app/pages/*`, `/app/data/*`). The webhook endpoints are ordinary operations
    too (ADR 0059 — the `actions[]` array is retired): their `/hooks/*` paths in this document are
    relative to the `/app/api` base, so they are served at `/app/api/hooks/*`. Shared-secret and
    capability-token side-channels enforce their credential in the delegate."
components:
  securitySchemes:
    hookSecret:
      type: apiKey
      in: header
      name: x-hook-secret
      x-nano-secret-env: NANO_PR_WEBHOOK_SECRET
      description: Shared secret. Enforced by the delegate (NOT the runtime) on this app's own
        operations only when NANO_PR_WEBHOOK_SECRET is set; unset means those endpoints are open.
        The `x-nano-secret-env` extension additionally makes this the app's canonical shared-secret
        scheme for the Urban runtime's framework mutation guard (`authorizeMutation`), so the
        framework-owned mutating `urban_debug_*` MCP tools (set_variables, retry_job,
        resolve_incident, cancel_instance) are authorizable on a remote-exposed instance by
        presenting the `x-hook-secret` header set to NANO_PR_WEBHOOK_SECRET. One shared secret, one
        header, for both app-operation reads/mutations and framework mutations. Must remain the sole
        apiKey scheme carrying `x-nano-secret-env` — the runtime throws on ambiguity.
  schemas:
    ErrorBody:
      type: object
      required:
        - error
      properties:
        error:
          type: string
    ValidationError:
      description: >-
        The uniform validation-error contract: a human-readable `error` plus a path-qualified
        `issues[]` naming each offending input. Returned by endpoints that reject a bad query/body
        param (e.g. `GET /agent/guide` with an unknown `section` id).
      type: object
      additionalProperties: false
      required:
        - error
        - issues
      properties:
        error:
          type: string
          description: A human-readable summary of the rejection.
        issues:
          type: array
          minItems: 1
          items:
            type: object
            additionalProperties: false
            required:
              - path
              - message
            properties:
              path:
                type: string
                description: JSON-path pointer at the offending input (e.g. `section`).
              message:
                type: string
                description: Human-actionable description of the failure.
          description: The path-qualified `{ path, message }` failures (at least one).
    ActivePr:
      type: object
      description: A tracked PR that is not in a terminal (converged/abandoned) state.
      additionalProperties: false
      required:
        - prKey
        - repo
        - number
        - url
        - title
        - status
        - round
        - processKey
        - waitingSince
        - openEscalation
        - updatedAt
        - activeWorker
        - leaseUntil
      properties:
        prKey:
          type: string
        repo:
          type: string
        number:
          type: integer
        url:
          type: string
        title:
          type: string
          nullable: true
        status:
          type: string
        round:
          type: integer
        processKey:
          type: string
          nullable: true
          description: The engine process instance key; also the keyField the pages processExplorer link uses.
        waitingSince:
          type: string
          nullable: true
        openEscalation:
          type: object
          nullable: true
          additionalProperties: false
          description: >-
            The structured pointer to this PR's OPEN escalation when it is parked awaiting a human/agent
            answer (issue #666), else null. Derived from the same `user_tasks` read model as
            `listEscalations`, so `userTaskKey` is the completable key an agent answers via
            `completeUserTask` / `agentCompleteEscalation` (no `/tasks/api/tasks` curl needed). `kind`
            is the BPMN escalation elementId (e.g. `wait-answer`, `wait-merge-answer`); `summary` is the
            raised question/findings (null when none was recorded).
          required:
            - userTaskKey
            - kind
            - summary
          properties:
            userTaskKey:
              type: string
              description: The completable engine user-task key (answer via completeUserTask / agentCompleteEscalation).
            kind:
              type: string
              description: The BPMN escalation elementId (the escalation kind).
            summary:
              type: string
              nullable: true
              description: The raised question / findings, denormalised for display; null when none.
        updatedAt:
          type: string
        activeWorker:
          type: string
          nullable: true
        leaseUntil:
          type: string
          nullable: true
    ActivePrList:
      type: object
      required:
        - count
        - prs
      properties:
        count:
          type: integer
        prs:
          type: array
          items:
            $ref: "#/components/schemas/ActivePr"
    Escalation:
      type: object
      description: >-
        One OPEN native user-task escalation awaiting a human/agent decision (issue #666), projected
        from the `user_tasks` read model. `userTaskKey` is the completable key an agent answers via
        `completeUserTask` / `agentCompleteEscalation`; `kind` is the BPMN escalation elementId.
      additionalProperties: false
      required:
        - userTaskKey
        - kind
        - kindLabel
        - prKey
        - subjectType
        - subjectKey
        - subjectTitle
        - subjectUrl
        - question
        - formKey
        - processKey
        - formVariables
      properties:
        userTaskKey:
          type: string
          description: The completable engine user-task key (answer via completeUserTask / agentCompleteEscalation).
        kind:
          type: string
          description: The BPMN escalation elementId (e.g. wait-answer, wait-merge-answer, plan-review-decision, trial-merge-decision, feature-escalation).
        kindLabel:
          type: string
          description: Human-readable kind label (e.g. "PR review", "Plan review", "Trial merge").
        prKey:
          type: string
          nullable: true
          description: The PR key when this escalation belongs to a PR (review/merge loop); null for feature / plan / delivery / agent subjects.
        subjectType:
          type: string
          description: The domain subject kind — feature | plan | pr | delivery | agent.
        subjectKey:
          type: string
          description: The subject aggregate key (feature_key / plan_key / pr_key).
        subjectTitle:
          type: string
          description: The subject's human-readable title (coalesced to subjectKey when unknown).
        subjectUrl:
          type: string
          nullable: true
          description: An optional external link (the issue/PR URL); null when none.
        question:
          type: string
          nullable: true
          description: The raised question / findings / task text the loop or agent recorded; null when none.
        formKey:
          type: string
          nullable: true
          description: The deployed `.form` key of the parked user task, for rendering/answering; null when unresolved.
        processKey:
          type: string
          nullable: true
          description: The owning engine process-instance key; null when unknown.
        formVariables:
          type: object
          additionalProperties: true
          description: >-
            The denormalised decision/form context the Tasks inbox renders for this task (the same
            question + subject the deployed form is seeded with). The typed answer fields an agent
            submits depend on `kind` (e.g. a PR `{ answer }`, a plan-review `{ directive, notes }`).
    EscalationList:
      type: object
      additionalProperties: false
      required:
        - count
        - escalations
      properties:
        count:
          type: integer
        escalations:
          type: array
          items:
            $ref: "#/components/schemas/Escalation"
    LineagePrView:
      type: object
      description: A member PR of a lineage thread (issue #245).
      additionalProperties: false
      required:
        - prKey
        - title
        - url
        - status
        - round
        - processKey
        - outcome
      properties:
        prKey:
          type: string
        title:
          type: string
          nullable: true
        url:
          type: string
        status:
          type: string
        round:
          type: integer
        processKey:
          type: string
          nullable: true
        outcome:
          type: string
          nullable: true
    LineageThreadView:
      type: object
      description: One stitched intent → progress arc, keyed by its origin request (issue #245).
      additionalProperties: false
      required:
        - rootRequestKey
        - kind
        - title
        - issueUrl
        - stage
        - stageLabel
        - processKey
        - prKeys
        - prCount
        - active
        - prs
      properties:
        rootRequestKey:
          type: string
          description: The origin issue key (feature_key/plan_key), a self-rooted pr_key, or a delivery-graph run_key.
        kind:
          type: string
          enum: [feature, epic, pr, delivery]
        title:
          type: string
          nullable: true
        issueUrl:
          type: string
          nullable: true
        stage:
          type: string
          description: The active-frontier stage (implementing|converging|merged|…).
        stageLabel:
          type: string
        processKey:
          type: string
          nullable: true
          description: The active-frontier process instance key (for the processExplorer link).
        prKeys:
          type: array
          items:
            type: string
        prCount:
          type: integer
        active:
          type: boolean
          description: True while the arc has an active frontier; false once every stage has settled.
        prs:
          type: array
          items:
            $ref: "#/components/schemas/LineagePrView"
    LineageList:
      type: object
      required:
        - count
        - threads
      properties:
        count:
          type: integer
        threads:
          type: array
          items:
            $ref: "#/components/schemas/LineageThreadView"
    PrHistoryRound:
      type: object
      description: One convergence round in a PR's timeline (issue #668) — its status transition/outcome, owning worker, and timestamps. Sourced from the `rounds` table the Convergence page reads.
      additionalProperties: false
      required:
        - roundNo
        - status
        - worker
        - summary
        - startedAt
        - endedAt
      properties:
        roundNo:
          type: integer
        status:
          type: string
          nullable: true
          description: The round's result/status transition (converged|addressed|waiting|needs_input|blocked).
        worker:
          type: string
          nullable: true
          description: The worker that ran this round.
        summary:
          type: string
          nullable: true
          description: The round outcome summary.
        startedAt:
          type: string
        endedAt:
          type: string
          nullable: true
    PrHistoryEscalation:
      type: object
      description: One escalation in a PR's history (issue #668) — its kind, question/answer, status, and timestamps. Sourced from the `escalations` table the Convergence page reads.
      additionalProperties: false
      required:
        - roundNo
        - kind
        - worker
        - question
        - answer
        - status
        - askedAt
        - answeredAt
      properties:
        roundNo:
          type: integer
        kind:
          type: string
          description: The escalation kind (question|blocker).
        worker:
          type: string
          nullable: true
          description: The worker that raised this escalation.
        question:
          type: string
        answer:
          type: string
          nullable: true
          description: The human's answer; null while the escalation is still open.
        status:
          type: string
          description: open | answered | stale.
        askedAt:
          type: string
        answeredAt:
          type: string
          nullable: true
    PrHistory:
      type: object
      description: A PR's full escalation + round history (issue #668), as surfaced read-only on the Convergence page's PR detail. Empty arrays when the PR has no history (or is unknown).
      additionalProperties: false
      required:
        - prKey
        - rounds
        - escalations
      properties:
        prKey:
          type: string
        rounds:
          type: array
          items:
            $ref: "#/components/schemas/PrHistoryRound"
        escalations:
          type: array
          items:
            $ref: "#/components/schemas/PrHistoryEscalation"
    AgenticSupplyWorker:
      type: object
      description: One connected worker in the supply mirror (H5 cockpit; sourced from the H1 presence registry).
      required:
        - instance
        - identity
        - stream
        - jobKeys
        - live
        - staleMs
        - harnessStale
      properties:
        instance:
          type: string
          description: The worker instance id.
        identity:
          type: string
          description: The ADR 0028 leaf token the worker registered under.
        stream:
          type: string
          description: The relay stream id to subscribe when drilling into this worker's terminal.
        family:
          type: string
          description: Declared family (enrolment attribute), if any.
        host:
          type: string
          description: Declared host (where the worker runs), if any.
        jobKeys:
          type: array
          description: The jobKeys this worker is currently processing (populated by the H6 correlation registry).
          items:
            type: string
        live:
          type: boolean
          description: Whether the worker's channel connection is still open.
        staleMs:
          type: integer
          description: Milliseconds since the last liveness refresh (0 when fresh).
        harnessProtocol:
          type: integer
          minimum: 0
          description: The worker-harness protocol version this worker advertised at enrolment (issue #802), when a numeric one is known.
        harnessStale:
          type: boolean
          description: >-
            Whether this worker's harness is STALE (issue #802) — below the configured minimum protocol
            or advertising no version at all — so it may silently swallow AgentInstance / transcript /
            result-envelope artifacts. Surfaced so the operator can drain it. Distinct from the
            liveness `staleMs` heartbeat grade.
    AgenticSupplyLeaf:
      type: object
      description: The supply for one leaf token — the workers registered under it.
      required:
        - token
        - workers
      properties:
        token:
          type: string
        workers:
          type: array
          items:
            $ref: "#/components/schemas/AgenticSupplyWorker"
    AgenticJobCorrelation:
      type: object
      description: One current job's engine context (H6) — lines a worker's terminal up with its process
        instance / plan. The relay stream a job's terminal is on is the jobKey-scoped stream `job:<jobKey>`.
      required:
        - jobKey
        - stream
      properties:
        jobKey:
          type: string
          description: The Camunda-8 job key the worker activated.
        stream:
          type: string
          description: The relay stream id the job's terminal is relayed on (`job:<jobKey>`).
        processInstanceKey:
          type: string
          description: The owning process instance key, if known.
        bpmnProcessId:
          type: string
          description: The BPMN process id the job belongs to, if known.
        elementId:
          type: string
          description: The BPMN element id (activity/task) the job is for, if known.
        planKey:
          type: string
          description: The plan / epic key this job is part of (e.g. owner/repo#142), if known.
    AgenticSupplyReport:
      type: object
      description: The SUPPLY-ONLY visibility report — the live worker list grouped by leaf. No demand-side
        fields (the demand×supply matrix / diversity SLO are deferred to enrolment epic #152).
      required:
        - count
        - workers
        - leaves
      properties:
        count:
          type: integer
          description: The number of connected workers.
        generatedAt:
          type: string
          description: When this snapshot was taken, ISO-8601.
        workers:
          type: array
          items:
            $ref: "#/components/schemas/AgenticSupplyWorker"
        leaves:
          type: array
          items:
            $ref: "#/components/schemas/AgenticSupplyLeaf"
        correlations:
          type: array
          description: The engine context (process instance / plan) for every jobKey currently being
            processed, so the cockpit can line each worker's terminal up with its process instance / plan (H6).
          items:
            $ref: "#/components/schemas/AgenticJobCorrelation"
    VocabRequirement:
      type: object
      description: One crew-vocab leaf token's flattened enrolment gate — the capability `requires`
        predicates, seat sizing, and diversity flag a worker sees without re-deriving the tree.
      required:
        - token
        - role
        - requires
        - seats
        - seatsDistinctFamily
      properties:
        token:
          type: string
          description: The leaf routing token (`network[.subnetwork…].role`, or a bare role).
        network:
          type: string
          description: The network segment, absent for a bare role.
        role:
          type: string
          description: The role segment.
        weight:
          type: number
          description: The role's cognition weight, if declared.
        requires:
          type: array
          description: The enrolment `requires` predicates (source form, e.g. `cognition=planning`).
          items:
            type: string
        seats:
          description: Either a seat count (integer ≥ 0) or the explicit named-seat list.
          oneOf:
            - type: integer
              minimum: 0
            - type: array
              items:
                type: string
        seatsDistinctFamily:
          type: boolean
          description: When true, seats must be filled by distinct families (the diversity SLO opt-in).
    VocabView:
      type: object
      description: The published crew vocabulary artifact — the ONE capability→token map (ADR 0059).
      required:
        - version
        - networks
        - requirements
      properties:
        version:
          type: integer
          description: The crew-vocab artifact version.
        networks:
          type: object
          additionalProperties: true
          description: The raw networks/roles tree, in the `@nanobpm/agentic` VocabDocument schema.
        requirements:
          type: array
          description: The flattened per-token enrolment requirements, sorted by token.
          items:
            $ref: "#/components/schemas/VocabRequirement"
    Capability:
      type: object
      description: A worker's declared enrolment capability. NEVER a routing token — it gates enrolment.
      properties:
        cognition:
          type: string
          description: The worker's cognition class (e.g. planning / implementation / qa / ci / decide).
        weight:
          type: number
          description: The cognition weight (the one numeric capability field).
        family:
          type: string
          description: The model family (the diversity-SLO seat filler, e.g. frontier / kimi / qwen).
        host:
          type: string
          description: Where the worker runs.
    EnrolRequest:
      type: object
      description: A worker's enrol request — its declared capability (ADR 0059 revised, per-worker).
      required:
        - capability
      properties:
        capability:
          $ref: "#/components/schemas/Capability"
        host:
          type: string
          description: Where the worker runs. Folded into `capability.host` when the latter is absent.
        instance:
          type: string
          description: The worker instance id, echoed back for provenance (optional).
        durableResume:
          type: boolean
          description: >-
            Whether this worker's harness advertises durable-resume (issue #325, ADR 0062 Slice 5/5)
            — an ENROLMENT attribute, never a routing token. Recorded per instance so the app emits
            the world-restore marker only to a fleet with a participant; a harness that omits it (or
            sets false) redrives a re-leased round from scratch. Recorded only when `instance` is
            a non-blank string — a missing, empty, or whitespace-only `instance` is echoed back for
            provenance but the flag is not persisted.
        harnessProtocol:
          type: integer
          minimum: 0
          description: >-
            The worker-harness protocol version (issue #802) — a non-negative integer declaring which
            machine-readable artifacts the harness emits (AgentInstance, transcript flush, result
            envelope). An ENROLMENT attribute, never a routing token. Recorded per instance so the app
            can flag a stale harness in getAgenticSupply / the registry and — under
            NANO_AGENTIC_STALE_HARNESS_POLICY=refuse — refuse it agent-job routing. A missing version is
            treated as stale.
    EnrolledRole:
      type: object
      description: One matched role in an enrolment resolution — provenance for the resolved SERVE set.
      required:
        - token
        - seatsDistinctFamily
      properties:
        token:
          type: string
        weight:
          type: number
        seatsDistinctFamily:
          type: boolean
    EnrolResult:
      type: object
      description: The resolved SERVE set for an enrolled capability (ADR 0059 REGISTER→SERVE).
      required:
        - serve
        - roles
        - demandVersion
        - leaseTtl
        - harnessStale
      properties:
        instance:
          type: string
          description: The worker instance id, echoed from the request when supplied.
        durableResume:
          type: boolean
          description: >-
            Echo of the request's durable-resume declaration (issue #325, ADR 0062 Slice 5/5).
            Present only when the request supplied it. This reflects the value the worker sent, not a
            guarantee of durable persistence — recording into the durable-resume registry is
            best-effort (skipped when `instance` is absent/blank, and a registry write hiccup is
            logged without failing enrolment).
        harnessProtocol:
          type: integer
          minimum: 0
          description: >-
            Echo of the request's advertised harness protocol version (issue #802). Present only when
            the request supplied it.
        harnessStale:
          type: boolean
          description: >-
            Whether this worker's harness is STALE (issue #802) — below the configured minimum protocol
            (NANO_AGENTIC_MIN_HARNESS_PROTOCOL) or advertising no version at all. Always present. Under
            NANO_AGENTIC_STALE_HARNESS_POLICY=refuse a stale harness is handed an EMPTY `serve` set so
            it wins no job leases; under the default `flag` policy `serve` is unchanged and the worker
            is only flagged for observability/drain.
        serve:
          type: array
          description: The SERVE token set — sorted, de-duplicated leaf tokens the worker may serve.
          items:
            type: string
        roles:
          type: array
          description: The matched roles (sorted by token) the SERVE tokens came from.
          items:
            $ref: "#/components/schemas/EnrolledRole"
        demandVersion:
          type: integer
          description: The crew-vocab version the capability was resolved against.
        leaseTtl:
          type: integer
          description: The liveness lease TTL in ms — the worker must heartbeat within this window.
    TokenDemand:
      type: object
      description: One demanded routing token and the live supply against it.
      required:
        - token
        - supply
        - instances
        - satisfied
      properties:
        token:
          type: string
          description: The demanded routing token (a deployed `taskDefinition` leaf's type).
        supply:
          type: integer
          description: How many registered workers currently serve this token.
        instances:
          type: array
          description: The instances serving it, sorted.
          items:
            type: string
        satisfied:
          type: boolean
          description: False when no registered worker serves it — a missing agent type.
    NetworkDemand:
      type: object
      description: The demand×supply picture for one network prefix.
      required:
        - network
        - tokens
        - missing
      properties:
        network:
          type: string
          description: The network prefix bucket (the `network` segment, or a bare token's own name).
        tokens:
          type: array
          description: Every demanded token in the bucket, sorted by token.
          items:
            $ref: "#/components/schemas/TokenDemand"
        missing:
          type: array
          description: The demanded tokens in this bucket with zero supply, sorted.
          items:
            type: string
    SeatAssignment:
      type: object
      description: One seat of a role filled by a worker of a given family.
      required:
        - seat
        - family
      properties:
        seat:
          type: string
        family:
          type: string
        instance:
          type: string
    RoleDiversity:
      type: object
      description: The diversity grade for a single role.
      required:
        - token
        - seatsDistinctFamily
        - assignments
        - collidingFamilies
        - status
      properties:
        token:
          type: string
        seatsDistinctFamily:
          type: boolean
        assignments:
          type: array
          items:
            $ref: "#/components/schemas/SeatAssignment"
        collidingFamilies:
          type: array
          items:
            type: string
        status:
          type: string
          enum: [green, amber, red]
    DiversityReport:
      type: object
      description: The diversity SLO over the correlated live registry (ADR 0056 §10).
      required:
        - status
        - roles
      properties:
        status:
          type: string
          enum: [green, amber, red]
          description: The worst grade across all roles (red > amber > green).
        roles:
          type: array
          description: Per-role grades, sorted by token.
          items:
            $ref: "#/components/schemas/RoleDiversity"
    RegistryReport:
      type: object
      description: The demand×supply report — deployed demand diffed against live supply, per network,
        with missing-agent-type reds and the diversity SLO (enrolment epic 152 slice N1).
      required:
        - version
        - generatedAt
        - demandUnavailable
        - networks
        - missing
        - nonAgentic
        - diversity
        - status
      properties:
        version:
          type: integer
          description: The crew-vocab version the report was resolved against.
        generatedAt:
          type: string
          description: When the report was computed, ISO-8601.
        demandUnavailable:
          type: boolean
          description: True when the deployed demand could not be read from the engine (supply-only report).
        networks:
          type: array
          description: Per-network demand×supply, sorted by network.
          items:
            $ref: "#/components/schemas/NetworkDemand"
        missing:
          type: array
          description: Every missing agent type across all networks, sorted and de-duplicated.
          items:
            type: string
        nonAgentic:
          type: array
          description: Deployed taskDefinition types that are NOT valid routing tokens (ordinary C8 jobs).
          items:
            type: string
        diversity:
          $ref: "#/components/schemas/DiversityReport"
        status:
          type: string
          enum: [green, amber, red]
          description: >-
            The overall SLO — worst of the missing-agent signal and the diversity SLO, and folded to
            `red` when any enrolled harness is stale (`staleWorkers` non-empty, issue #802), since the
            board renders only this pill as its overall signal.
        staleWorkers:
          type: array
          description: >-
            The enrolled workers whose harness is STALE (issue #802) — below the configured minimum
            protocol or advertising no version at all — so they may silently swallow AgentInstance /
            transcript / result-envelope artifacts and should be drained. Present (possibly empty) when
            the app's harness-protocol registry is available.
          items:
            $ref: "#/components/schemas/StaleWorker"
    StaleWorker:
      type: object
      description: One enrolled worker flagged as running a stale harness (issue #802).
      required:
        - instance
        - stale
      properties:
        instance:
          type: string
          description: The worker instance id.
        harnessProtocol:
          type: integer
          minimum: 0
          description: The advertised harness protocol version, when a numeric one is known (omitted when none was advertised).
        stale:
          type: boolean
          description: Whether the worker's harness is stale (always true for entries in this list).
    AgenticTranscript:
      type: object
      description: One captured agent session's transcript metadata (H3/#146 transcript store). A durable
        record of an ephemeral agent's terminal stream, readable AFTER the agent has exited. jobKey and
        the process-instance / plan correlation are derived from the stream id (`job:<jobKey>`) and, when
        the H6 correlation registry still holds the (live) job, its engine context (advisory, best-effort).
      required:
        - stream
        - lifecycle
        - status
        - createdAt
        - nextOffset
        - byteLength
        - chunkCount
      properties:
        stream:
          type: string
          description: The relay stream id the transcript was captured under (`job:<jobKey>` for a job stream).
        lifecycle:
          type: string
          enum: [ephemeral, long-lived]
          description: Retention lifecycle — ephemeral (flushed once on job completion, swept after retention)
            or long-lived (checkpointed, bounded by a rolling offset window).
        status:
          type: string
          enum: [open, completed]
          description: open (still capturing / reattachable) or completed (the ephemeral run flushed & sealed).
        createdAt:
          type: string
          description: When the stream was first opened, ISO-8601.
        completedAt:
          type: string
          description: When an ephemeral run was flushed & completed, ISO-8601 (absent while open).
        firstOffset:
          type: integer
          description: The oldest retained chunk offset, or absent when the transcript holds no chunks.
        nextOffset:
          type: integer
          description: One past the highest offset ever recorded (the resume high-water mark).
        byteLength:
          type: integer
          description: Total captured bytes across every retained chunk.
        chunkCount:
          type: integer
          description: The number of retained chunks.
        jobKey:
          type: string
          description: The Camunda-8 job key, decoded from a `job:<jobKey>` stream id (absent for other streams).
        processInstanceKey:
          type: string
          description: The owning process instance key, when the correlation is still known (advisory).
        bpmnProcessId:
          type: string
          description: The BPMN process id the job belonged to, when still known (advisory).
        elementId:
          type: string
          description: The BPMN element id (activity/task) the job was for, when still known (advisory).
        elementInstanceKey:
          type: string
          description: The engine element-instance key the job's token occupied (#544) — the per-occupancy
            handle, unambiguous across a looping / retried activity where many instances share one elementId.
            Resolved from the job's element-instance wait-state; advisory, present when resolution landed.
        planKey:
          type: string
          description: The plan / epic key this job was part of (e.g. owner/repo#142), when still known (advisory).
        instance:
          type: string
          description: The worker instance that ran the session, recovered from durable attribution — present even
            after the worker exited or the process restarted (advisory).
        identity:
          type: string
          description: The worker's durable identity (presence identity), when recorded (advisory).
        host:
          type: string
          description: The worker's host, when recorded (advisory).
    AgenticTranscriptList:
      type: object
      description: The list of captured agent sessions (past + open) — the cockpit "past sessions" feed.
      required:
        - count
        - transcripts
      properties:
        count:
          type: integer
          description: The number of transcripts returned (after any filters).
        generatedAt:
          type: string
          description: When this snapshot was taken, ISO-8601.
        retentionMs:
          type: integer
          description: The completed-ephemeral retention window in ms (how long a finished session is kept
            before a retention sweep may drop it). Absent when no transcript store is mounted.
        transcripts:
          type: array
          items:
            $ref: "#/components/schemas/AgenticTranscript"
    AgenticTranscriptChunk:
      type: object
      description: One durable transcript chunk and the offset it was recorded at.
      required:
        - offset
        - chunk
      properties:
        offset:
          type: integer
          description: The chunk's offset (resume key) — the terminal renderer resumes-from-offset off it.
        chunk:
          type: string
          description: The captured terminal bytes for this offset.
    AgenticTranscriptData:
      type: object
      description: A stored transcript's bytes, range/offset-based so the cockpit terminal replays it through
        the SAME resume-from-offset renderer it uses for a live stream (static playback of a closed stream).
      required:
        - stream
        - lifecycle
        - status
        - createdAt
        - nextOffset
        - byteLength
        - chunkCount
        - from
        - gap
        - entries
      properties:
        stream:
          type: string
          description: The relay stream id.
        lifecycle:
          type: string
          enum: [ephemeral, long-lived]
        status:
          type: string
          enum: [open, completed]
        createdAt:
          type: string
          description: When the stream was first opened, ISO-8601.
        completedAt:
          type: string
          description: When an ephemeral run was flushed & completed, ISO-8601 (absent while open).
        nextOffset:
          type: integer
          description: One past the highest recorded offset (where a live stream would continue).
        byteLength:
          type: integer
          description: Total captured bytes across the returned chunks.
        chunkCount:
          type: integer
          description: The number of returned chunks.
        from:
          type: integer
          description: The requested resume offset (inclusive) this page starts at.
        gap:
          type: boolean
          description: True when `from` predates the oldest retained offset — earlier chunks were dropped by
            retention, so the replay is a best-effort resume, not gap-free from `from`.
        jobKey:
          type: string
          description: The Camunda-8 job key, decoded from a `job:<jobKey>` stream id (absent otherwise).
        processInstanceKey:
          type: string
          description: The owning process instance key, when still known (advisory).
        bpmnProcessId:
          type: string
          description: The BPMN process id, when still known (advisory).
        elementId:
          type: string
          description: The BPMN element id, when still known (advisory).
        elementInstanceKey:
          type: string
          description: The engine element-instance key the job's token occupied (#544) — per-occupancy,
            unambiguous across a looping / retried activity; advisory, present when resolution landed.
        planKey:
          type: string
          description: The plan / epic key, when still known (advisory).
        instance:
          type: string
          description: The worker instance that ran the session, from durable attribution (advisory).
        identity:
          type: string
          description: The worker's durable identity, when recorded (advisory).
        host:
          type: string
          description: The worker's host, when recorded (advisory).
        entries:
          type: array
          description: The retained chunks with `offset >= from`, in offset order.
          items:
            $ref: "#/components/schemas/AgenticTranscriptChunk"
    AgentInstanceMetrics:
      type: object
      description: >-
        Aggregated metrics the engine rolls up on an AgentInstance across all of its model
        calls (issue #745/#747 — the CONSUMER half). Total tokens consumed, and the count of model /
        tool calls made. Distinct from the per-turn metrics carried on an AgentHistoryRecord.
      required:
        - inputTokens
        - outputTokens
        - modelCalls
        - toolCalls
      properties:
        inputTokens:
          type: integer
          description: Total input (prompt) tokens consumed across the instance's model calls.
        outputTokens:
          type: integer
          description: Total output (completion) tokens produced across the instance's model calls.
        modelCalls:
          type: integer
          description: The number of LLM model calls the agent made.
        toolCalls:
          type: integer
          description: The number of tool calls the agent dispatched.
    AgentInstance:
      type: object
      description: >-
        One engine-native AgentInstance as `searchAgentInstances` / `getAgentInstance` report
        it (issue #745/#747, umbrella #746 — Camunda 8.10 parity). The durable projection of an agent
        task's run, minted by the worker harness against the `<zeebe:agentDefinition agentType="external"/>`
        marker (#748). Keyed by `agentInstanceKey` (passed to the history read) and correlated by
        process / element-instance keys — NEVER the slash-bearing `job:<jobKey>` relay stream id.
      required:
        - agentInstanceKey
        - status
        - processInstanceKey
      properties:
        agentInstanceKey:
          type: string
          description: The engine-unique agent-instance key — the identity a caller passes to the history read.
        status:
          type: string
          description: The lifecycle status (the engine's broad `AgentInstanceStatusEnum` — e.g.
            INITIALIZING / THINKING / TOOL_CALLING / IDLE / COMPLETED), a bare string.
        processInstanceKey:
          type: string
          description: The owning process-instance key.
        elementId:
          type: string
          description: The BPMN element id (the AI-agent task) that owns the instance, when reported.
        elementInstanceKeys:
          type: array
          description: The engine element-instance keys the instance's token(s) occupied (#544) — per-occupancy
            handles, unambiguous across a looping / retried activity. Omitted when the engine reports none.
          items:
            type: string
        rootProcessInstanceKey:
          type: string
          description: The root process-instance key of the owning hierarchy, when reported.
        processDefinitionKey:
          type: string
          description: The owning process-definition key, when reported.
        processDefinitionId:
          type: string
          description: The owning process-definition id, when reported.
        metrics:
          $ref: "#/components/schemas/AgentInstanceMetrics"
        creationDate:
          type: string
          description: When the instance was created, ISO-8601, when reported.
        lastUpdatedDate:
          type: string
          description: When the instance was last updated, ISO-8601, when reported.
        completionDate:
          type: string
          description: When the instance completed, ISO-8601 (absent while still running).
    AgentInstanceList:
      type: object
      description: >-
        The list of engine-native agent instances (issue #745/#747), newest-created first —
        the cockpit "historical sessions" feed sourced from engine history (not the relay store).
      required:
        - count
        - instances
      properties:
        count:
          type: integer
          description: The number of instances returned (after any filters).
        generatedAt:
          type: string
          description: When this snapshot was taken, ISO-8601.
        instances:
          type: array
          items:
            $ref: "#/components/schemas/AgentInstance"
    AgentHistoryContentBlock:
      type: object
      description: One typed content block in a turn's message (Camunda `AgentHistoryMessageContentValue`
        parity). Exactly one payload is populated per `contentType`.
      required:
        - contentType
      properties:
        contentType:
          type: string
          enum: [TEXT, DOCUMENT, OBJECT, UNSPECIFIED]
          description: The content type; selects which payload field is populated.
        text:
          type: string
          description: Text payload; populated when `contentType` is TEXT.
        documentReference:
          type: string
          description: Document reference; populated when `contentType` is DOCUMENT.
        object:
          description: JSON value payload (any JSON type); populated when `contentType` is OBJECT.
    AgentHistoryToolCall:
      type: object
      description: A tool call embedded in a turn (Camunda `AgentHistoryEmbeddedToolCallValue` parity).
      required:
        - toolCallId
        - toolName
        - arguments
      properties:
        toolCallId:
          type: string
          description: The stable tool-call id (pairs a call to its result).
        toolName:
          type: string
          description: The tool that was called.
        elementId:
          type: string
          description: The tool task's BPMN element id, when reported.
        arguments:
          type: object
          description: The arguments passed to the tool (an arbitrary JSON object).
          additionalProperties: true
    AgentHistoryTurnMetrics:
      type: object
      description: Per-turn metrics (Camunda `AgentHistoryMetricsValue` parity) — the token counts a
        single turn's LLM call consumed/produced and its wall-clock duration. Distinct from the
        instance-level `AgentInstanceMetrics`.
      required:
        - inputTokens
        - outputTokens
        - reasoningTokenCount
        - cacheCreationTokenCount
        - cacheReadTokenCount
        - durationMs
      properties:
        inputTokens:
          type: integer
        outputTokens:
          type: integer
        reasoningTokenCount:
          type: integer
        cacheCreationTokenCount:
          type: integer
        cacheReadTokenCount:
          type: integer
        durationMs:
          type: integer
    AgentHistoryRecord:
      type: object
      description: >-
        One agent-instance history item (a turn) as `searchAgentInstanceHistory` reports it
        (issue #745/#747) — the projection of the engine's `AgentInstanceHistoryItemResult`, i.e. one
        Camunda `AgentHistoryRecordValue`. Its conversation grammar reuses the transcript parity types,
        so the engine-read seam and the relay transcript store project the SAME shape (No Drift Surfaces).
      required:
        - historyItemKey
        - agentInstanceKey
        - loopIteration
        - role
        - content
        - toolCalls
        - commitStatus
      properties:
        historyItemKey:
          type: string
          description: The stable, creation-ordered identity of the history item.
        agentInstanceKey:
          type: string
          description: The owning agent-instance key.
        loopIteration:
          type: integer
          description: The agent-loop counter (one LLM call + its tool dispatches + results share an iteration).
        role:
          type: string
          enum: [USER, ASSISTANT, TOOL_RESULT, CONFIGURATION, UNSPECIFIED]
          description: The conversation role of the turn.
        content:
          type: array
          description: The turn's typed content blocks.
          items:
            $ref: "#/components/schemas/AgentHistoryContentBlock"
        toolCalls:
          type: array
          description: The tool calls embedded in the turn.
          items:
            $ref: "#/components/schemas/AgentHistoryToolCall"
        metrics:
          $ref: "#/components/schemas/AgentHistoryTurnMetrics"
        commitStatus:
          type: string
          description: The engine's COMMITTED / PENDING / DISCARDED commit flag (a bare string).
        elementInstanceKey:
          type: string
          description: The element instance the item was produced under, when reported (best-effort).
        jobKey:
          type: string
          description: The job key the item was produced under, when reported (best-effort).
        producedAt:
          type: string
          description: When the item was produced, ISO-8601, when reported (best-effort).
    AgentHistory:
      type: object
      description: >-
        One agent instance's durable conversation history (turns + per-turn metrics) sourced
        from engine `searchAgentInstanceHistory` (issue #745/#747), in conversational order. The cockpit
        renders the HISTORICAL transcript + metrics from this; the token-granular relay stays the LIVE
        overlay only. Keyed by `agentInstanceKey` — never the slash-bearing relay stream id (#744 moot).
      required:
        - agentInstanceKey
        - count
        - records
      properties:
        agentInstanceKey:
          type: string
          description: The agent-instance key this history belongs to.
        count:
          type: integer
          description: The number of history records (turns) returned.
        generatedAt:
          type: string
          description: When this snapshot was taken, ISO-8601.
        instance:
          allOf:
            - $ref: "#/components/schemas/AgentInstance"
          description: The owning instance summary (rolled-up metrics + lifecycle), when the engine still
            reports it. Absent when the instance is unknown/aged out.
        records:
          type: array
          description: The history records (turns), ordered by loopIteration then creation-ordered key.
          items:
            $ref: "#/components/schemas/AgentHistoryRecord"
    VersionInfo:
      type: object
      description: The running app's identity (which code is actually live).
      additionalProperties: false
      required:
        - name
        - version
        - urbanVersion
        - gitSha
        - gitBranch
        - runtime
        - pid
        - startedAt
        - uptimeSeconds
      properties:
        name:
          type: string
        version:
          type: string
          nullable: true
        urbanVersion:
          type: string
          nullable: true
        gitSha:
          type: string
          nullable: true
        gitBranch:
          type: string
          nullable: true
        runtime:
          type: string
        pid:
          type: integer
          nullable: true
        startedAt:
          type: string
        uptimeSeconds:
          type: integer
    ReconcileReport:
      type: object
      description: "The merged result of the engine-reconcile invocation — an aggregate of both the
        epoch-regression (engine reset/rewind) pass and the vanished-instance pass (issues #622 and
        #630). `runId` is the epoch pass's run id; the vanished pass records its own provenance under
        the derived id `<runId>-vanished`."
      additionalProperties: false
      required:
        - runId
        - reason
        - observedEpoch
        - recordedEpoch
        - orphanedCount
        - orphaned
      properties:
        runId:
          type: string
          description: The reconcile run id the epoch pass's orphaned-transition provenance is stamped
            with. The vanished-instance pass's provenance is stamped with the derived, deterministic id
            `<runId>-vanished`, so operators can locate the provenance rows for either pass from this id.
        reason:
          type: string
          description: Why this pass acted (or did not). `instance-vanished` — a run whose engine
            instance is absent/unknown in the read model was orphaned (issue #630).
          enum:
            - epoch-regression
            - seed-epoch
            - no-op
            - engine-unreachable
            - instance-vanished
        observedEpoch:
          type: integer
          nullable: true
          description: The engine incarnation epoch observed on this pass (null when the engine exposes
            none, or was unreachable).
        recordedEpoch:
          type: integer
          nullable: true
          description: The previously-recorded epoch this pass compared against (null on the first run).
        orphanedCount:
          type: integer
          description: How many engine-backed inflight rows were driven to `orphaned`.
        orphaned:
          type: array
          description: The rows orphaned by this pass.
          items:
            type: object
            additionalProperties: false
            required:
              - table
              - pk
              - key
              - fromStatus
            properties:
              table:
                type: string
              pk:
                type: string
              key:
                type: string
                nullable: true
                description: The engine instance key (e.g. process_key) the row projected.
              fromStatus:
                type: string
                description: The non-terminal status the row carried before it was orphaned.
    AgentInstructions:
      type: object
      description: The agent operator guide — how to drive (submit PRs/epics, answer escalations)
        and debug (find engine instances, relate them to PRs, inspect models/prompts, unstick stuck
        processes, raise issues/PRs) this Nano Workforce instance. The `instructions` markdown has
        its example commands keyed to this instance's `baseUrl`/`engineBase`.
      additionalProperties: false
      required:
        - format
        - appVersion
        - generatedAt
        - baseUrl
        - engineBase
        - instructions
      properties:
        format:
          type: string
          description: The `instructions` media format. Always "markdown".
          enum:
            - markdown
        appVersion:
          type: string
          nullable: true
          description: The running app version this guide matches (null when unreadable).
        generatedAt:
          type: string
          description: When this response was rendered (ISO-8601).
        baseUrl:
          type: string
          description: The app control-API base the examples target (e.g. https://host/app/api).
        engineBase:
          type: string
          description: The engine's Camunda-8 v2 REST base this app talks to, for debugging queries.
        instructions:
          type: string
          description: The full operator guide as markdown.
    AgentSkill:
      type: object
      description: The portable Nano Workforce operator skill (SKILL.md) — a thin bootstrap an agent
        runtime loads on demand. It resolves which instance to drive and then fetches the live
        operator guide. The `skill` markdown has any embedded example keyed to this instance's
        `baseUrl`.
      additionalProperties: false
      required:
        - format
        - appVersion
        - generatedAt
        - baseUrl
        - skill
      properties:
        format:
          type: string
          description: The `skill` media format. Always "markdown".
          enum:
            - markdown
        appVersion:
          type: string
          nullable: true
          description: The running app version this skill was served from (null when unreadable).
        generatedAt:
          type: string
          description: When this response was rendered (ISO-8601).
        baseUrl:
          type: string
          description: The app control-API base the skill was fetched from (e.g. https://host/app/api).
        skill:
          type: string
          description: The full operator skill (SKILL.md) as markdown, including its YAML frontmatter.
    AgentGuideResponse:
      type: object
      description: The addressable operator-guide response served by `getAgentGuide`. Two shapes,
        discriminated by `kind`. `kind:"toc"` (no `section` argument) carries `sections` — the compact
        table of contents, one entry per stable section id. `kind:"section"` (a `section` id given)
        carries `section` — that one section's markdown, with its examples keyed to this instance.
      additionalProperties: false
      required:
        - kind
        - appVersion
        - generatedAt
        - baseUrl
      properties:
        kind:
          type: string
          description: '"toc" when listing sections (no `section` argument); "section" when returning one.'
          enum:
            - toc
            - section
        appVersion:
          type: string
          nullable: true
          description: The running app version this guide matches (null when unreadable).
        generatedAt:
          type: string
          description: When this response was rendered (ISO-8601).
        baseUrl:
          type: string
          description: The app control-API base the examples target (e.g. https://host/app/api).
        engineBase:
          type: string
          description: The engine's Camunda-8 v2 REST base this app talks to (present on a section response).
        sections:
          type: array
          description: The table of contents — present when `kind` is "toc". One entry per addressable section.
          items:
            type: object
            additionalProperties: false
            required:
              - id
              - title
              - summary
            properties:
              id:
                type: string
                description: The stable section id to pass back as `getAgentGuide(section)`.
              title:
                type: string
                description: The section's heading text (e.g. "9. Author and run a delivery graph (ADR 0005)").
              summary:
                type: string
                description: A one-line summary of what the section covers.
        section:
          type: object
          description: The requested section — present when `kind` is "section".
          additionalProperties: false
          required:
            - id
            - title
            - format
            - instructions
          properties:
            id:
              type: string
              description: The stable section id that was requested.
            title:
              type: string
              description: The section's heading text.
            format:
              type: string
              description: The `instructions` media format. Always "markdown".
              enum:
                - markdown
            instructions:
              type: string
              description: The section's markdown (or a bounded page of it when paginating), with example commands keyed to this instance.
            start:
              type: integer
              description: The CHARACTER offset this page starts at (present only when paginating, issue #740).
            length:
              type: integer
              description: The number of characters returned in this page (present only when paginating).
            totalLength:
              type: integer
              description: The total number of characters in the fully-rendered section (present only when paginating).
            nextStart:
              type: integer
              nullable: true
              description: The `start` to pass for the next page, or null when this is the last page (present only when paginating).
    SubmitResult:
      type: object
      required:
        - prKey
      properties:
        prKey:
          type: string
        processKey:
          type: string
          nullable: true
          description: The started convergence-loop instance key (null if the engine did not return one).
        alreadyRunning:
          type: boolean
          description: True when a non-terminal convergence loop for this PR already exists; the aggregate was
            refreshed and no new instance was started.
    StartPlanResult:
      type: object
      required:
        - planKey
      properties:
        planKey:
          type: string
        processKey:
          type: string
          nullable: true
        alreadyRunning:
          type: boolean
          description: True when a non-terminal plan for this issue already exists; no new instance was started.
    StartFeatureResult:
      type: object
      required:
        - featureKey
        - outcome
      properties:
        featureKey:
          type: string
        outcome:
          type: string
          enum:
            - started
            - already-active
            - noop-terminal
          description: >-
            Discriminated intake outcome (issue #704). `started` — a fresh engine instance was
            dispatched (`processKey` set). `already-active` — a non-terminal prior run for this issue is
            still live, so the start short-circuited (no new instance; `processKey` is the live run's).
            `noop-terminal` — the engine returned no instance key, so NOTHING was dispatched; the
            operation surfaces this as a 502 (never on a 202 body), so a submit that dispatched no
            instance is never reported as success.
        processKey:
          type: string
          nullable: true
        alreadyRunning:
          type: boolean
          description: True iff a non-terminal feature run for this issue already existed and short-circuited the start (outcome `already-active`); no new instance was started.
    EpicSetStart:
      description: >-
        The set/batch admission request body (issue #292, slice S2). Submits a SET of epics plus the
        inter-epic dependency edges between them. `epics` is admitted all-or-nothing through the same
        `admitPlan` gate as the single-issue door; `deps` declares that a `consumer` epic waits for a
        `producer` epic's published `{ package, capabilityRef }` capability. Every edge must connect
        two epics named in `epics`, and the edge set must be an acyclic DAG — otherwise the whole set
        is rejected with a 4xx and nothing is persisted.
      type: object
      additionalProperties: false
      required:
        - epics
      properties:
        epics:
          type: array
          minItems: 1
          description: The epics to admit as one set. Each is admitted through the `admitPlan` gate.
          items:
            $ref: "#/components/schemas/EpicSetMember"
        deps:
          type: array
          description: >-
            The inter-epic dependency edges. Each declares `consumer` waits for `producer` to publish
            the `{ package, capabilityRef }` capability. Both endpoints must name epics in `epics`.
            Omit or pass `[]` for a set of independent (root) epics.
          items:
            $ref: "#/components/schemas/EpicSetDep"
    EpicSetMember:
      description: >-
        One epic in a submitted set. Names the target issue by EXACTLY ONE of `issue`
        (`owner/repo#123`) or `url` (a bare issue URL), plus a REQUIRED `baseBranch` and the optional
        admission acknowledgements — the same per-epic admission inputs as `PlanStart`.
      oneOf:
        - $ref: "#/components/schemas/EpicSetMemberByIssue"
        - $ref: "#/components/schemas/EpicSetMemberByUrl"
    EpicSetMemberByIssue:
      type: object
      additionalProperties: false
      required:
        - issue
        - baseBranch
      properties:
        issue:
          type: string
          description: "Issue reference: owner/repo#123."
        baseBranch:
          type: string
          minLength: 1
          maxLength: 255
          pattern: '\S'
          description: >-
            REQUIRED integration branch this epic branches off and opens its PRs against, admitted
            through the same ADR 0003 policy as the single-issue door. See `PlanStartByIssue.baseBranch`.
        allowSharedBase:
          type: boolean
          description: Opt in to sharing a custom integration base with another active epic. See `PlanStartByIssue.allowSharedBase`.
        confirmDefaultBase:
          type: boolean
          description: Acknowledge that `baseBranch` names the repository default branch. See `PlanStartByIssue.confirmDefaultBase`.
    EpicSetMemberByUrl:
      type: object
      additionalProperties: false
      required:
        - url
        - baseBranch
      properties:
        url:
          type: string
          description: A bare issue URL, when no `owner/repo#123` reference is supplied.
        baseBranch:
          type: string
          minLength: 1
          maxLength: 255
          pattern: '\S'
          description: REQUIRED integration branch this epic branches off. See `EpicSetMemberByIssue.baseBranch`.
        allowSharedBase:
          type: boolean
          description: Share a custom integration base with another active epic. See `PlanStartByIssue.allowSharedBase`.
        confirmDefaultBase:
          type: boolean
          description: Acknowledge landing on the default branch. See `PlanStartByIssue.confirmDefaultBase`.
    EpicSetDep:
      description: >-
        One inter-epic dependency edge: the `consumer` epic waits for the `producer` epic to publish
        the `{ package, capabilityRef }` capability. `consumer`/`producer` are epic references
        (`owner/repo#123` or an issue URL) that MUST both appear in the set's `epics`.
      type: object
      additionalProperties: false
      required:
        - consumer
        - producer
        - package
        - capabilityRef
      properties:
        consumer:
          type: string
          description: The dependent epic (waits). An `owner/repo#123` reference or issue URL in the set.
        producer:
          type: string
          description: The producer epic it waits for. An `owner/repo#123` reference or issue URL in the set.
        package:
          type: string
          minLength: 1
          description: The producer epic's published package name — the capability probe's target (S3).
        capabilityRef:
          type: string
          minLength: 1
          description: The producer epic's issue handle, used to resolve which published pkg@version first carries the capability (S3).
    StartEpicSetResult:
      description: The result of a successful set admission + lowering (issue #292, slice S3) — the epics admitted, the roots started immediately, the dependents started behind their leading capability readiness-gate, and the inter-epic edges materialized into `plan_deps`.
      type: object
      required:
        - epics
        - roots
        - dependents
        - edges
      properties:
        epics:
          type: array
          description: Every epic admitted, with its normalized base branch.
          items:
            type: object
            required:
              - planKey
              - baseBranch
            properties:
              planKey:
                type: string
              baseBranch:
                type: string
        roots:
          type: array
          description: The plan keys of epics with NO inbound edge — the roots started immediately (fan out right away).
          items:
            type: string
        dependents:
          type: array
          description: The epics with ≥1 inbound edge — started behind a leading capability readiness-gate that holds wave 0 until every listed producer publishes its capability.
          items:
            type: object
            required:
              - planKey
              - producers
            properties:
              planKey:
                type: string
              producers:
                type: array
                description: The producer plan keys whose capabilities this dependent waits for (ALL must be green before it fans out).
                items:
                  type: string
        edges:
          type: array
          description: The inter-epic edges materialized into the durable `plan_deps` graph (endpoints resolved to plan keys).
          items:
            type: object
            required:
              - consumer
              - producer
              - package
              - capabilityRef
            properties:
              consumer:
                type: string
              producer:
                type: string
              package:
                type: string
              capabilityRef:
                type: string
    ConvergenceStart:
      description: The start-convergence request body. Names the target PR by EXACTLY ONE of `pr`
        (an `owner/repo#123` reference) or `url` (a bare PR URL) — never both, never neither — with
        the optional convergence knobs. Modeled as `oneOf` named variants (ADR — Camunda REST v2
        pattern) so the runtime rejects an ambiguous or empty target at the edge with a 400 that
        names the allowed shapes, rather than the delegate silently coalescing `pr ?? url`.
      oneOf:
        - $ref: "#/components/schemas/ConvergenceStartByPr"
        - $ref: "#/components/schemas/ConvergenceStartByUrl"
    ConvergenceStartByPr:
      type: object
      additionalProperties: false
      required:
        - pr
      properties:
        pr:
          type: string
          description: "PR reference: owner/repo#123."
        dependsOn:
          type: array
          items:
            type: string
        maxRounds:
          type: integer
          minimum: 1
          description: Values above 100 are accepted and clamped to 100 by the delegate.
        autoMerge:
          type: boolean
          description: Preferred per-request setting. When false, stop at `converged`; when true, run the merge-loop after convergence only if `NANO_PR_AUTO_MERGE` is enabled. When omitted, the global default applies.
        convergeOnly:
          type: boolean
          deprecated: true
          description: "Deprecated negative alias. If `autoMerge` is omitted, `true` means `autoMerge: false`; `false` means `autoMerge: true` (still gated by `NANO_PR_AUTO_MERGE`). Prefer `autoMerge`."
    ConvergenceStartByUrl:
      type: object
      additionalProperties: false
      required:
        - url
      properties:
        url:
          type: string
          description: A bare PR URL, when no `owner/repo#123` reference is supplied.
        dependsOn:
          type: array
          items:
            type: string
        maxRounds:
          type: integer
          minimum: 1
          description: Values above 100 are accepted and clamped to 100 by the delegate.
        autoMerge:
          type: boolean
          description: Preferred per-request setting. When false, stop at `converged`; when true, run the merge-loop after convergence only if `NANO_PR_AUTO_MERGE` is enabled. When omitted, the global default applies.
        convergeOnly:
          type: boolean
          deprecated: true
          description: "Deprecated negative alias. If `autoMerge` is omitted, `true` means `autoMerge: false`; `false` means `autoMerge: true` (still gated by `NANO_PR_AUTO_MERGE`). Prefer `autoMerge`."
    PlanStart:
      description: The start-plan-fanout request body. Names the target issue by EXACTLY ONE of
        `issue` (an `owner/repo#123` reference) or `url` (a bare issue URL), plus a REQUIRED
        `baseBranch` (ADR 0003) the fleet branches off — a blank/absent base is a 400. Modeled as
        `oneOf` named variants (Camunda REST v2 pattern) so an ambiguous or empty target is a 400 at
        the edge, not a silent `issue ?? url` coalesce in the delegate.
      oneOf:
        - $ref: "#/components/schemas/PlanStartByIssue"
        - $ref: "#/components/schemas/PlanStartByUrl"
    PlanStartByIssue:
      type: object
      additionalProperties: false
      required:
        - issue
        - baseBranch
      properties:
        issue:
          type: string
          description: "Issue reference: owner/repo#123."
        baseBranch:
          type: string
          minLength: 1
          maxLength: 255
          pattern: '\S'
          description: >-
            REQUIRED target branch the fleet branches off and opens every PR against. Every epic
            launch must name its base explicitly (ADR 0003): a blank/absent value is rejected with a
            400, not silently coalesced to the repository default branch. Use it to land an entire
            epic on a long-lived integration branch (e.g. `epic/agent-protocol`) so nothing reaches
            the default branch — and any merge-to-default side effect, such as auto-publishing a
            package — until you deliberately merge the integration branch. NOTE: this slice (B0)
            only enforces that `baseBranch` is present and a plausible branch name; branch-existence
            admission (auto-creating a missing `epic/*` base off the default branch HEAD, and
            rejecting a missing non-`epic/*` base with a 400) is specified by ADR 0003 but NOT yet
            enforced here — it lands in a later admission slice.
        allowSharedBase:
          type: boolean
          description: >-
            Reserved for a later ADR 0003 admission slice — NOT yet enforced in this slice (B0). When
            implemented it will opt in to sharing a custom integration base branch with another
            already-active plan: admission will otherwise reject (409) when another active plan already
            targets the same repo + same custom base branch, to stop two epics interleaving commits on
            one integration branch (the repository default branch is exempt from that guard). Accepted
            by the schema today but currently has no runtime effect.
        confirmDefaultBase:
          type: boolean
          description: >-
            Reserved for a later ADR 0003 admission slice — NOT yet enforced in this slice (B0). When
            implemented it will be the required acknowledgement when `baseBranch` names the repository
            default branch: targeting the default lands every task directly on it with no integration
            buffer — and fires any merge-to-default side effect per task — so admission will reject
            (400) unless confirmed with true (no effect for a non-default base). Accepted by the schema
            today but currently has no runtime effect.
    PlanStartByUrl:
      type: object
      additionalProperties: false
      required:
        - url
        - baseBranch
      properties:
        url:
          type: string
          description: A bare issue URL, when no `owner/repo#123` reference is supplied.
        baseBranch:
          type: string
          minLength: 1
          maxLength: 255
          pattern: '\S'
          description: >-
            REQUIRED target branch the fleet branches off and opens every PR against. See
            `PlanStartByIssue.baseBranch`.
        allowSharedBase:
          type: boolean
          description: >-
            Reserved for a later ADR 0003 admission slice — NOT yet enforced in this slice (B0).
            Accepted by the schema today but currently has no runtime effect. When implemented it
            will opt in to sharing a custom integration base branch with another already-active plan.
            See `PlanStartByIssue.allowSharedBase`.
        confirmDefaultBase:
          type: boolean
          description: >-
            Reserved for a later ADR 0003 admission slice — NOT yet enforced in this slice (B0).
            Accepted by the schema today but currently has no runtime effect. When implemented it
            will be the required acknowledgement when `baseBranch` names the repository default
            branch. See `PlanStartByIssue.confirmDefaultBase`.
    ReadinessProbe:
      description: >-
        A single durable readiness probe (issue #258, #295) the feature run must satisfy before its
        implementation agent is dispatched. `kind` selects the source; `target` + `match` are the
        per-kind predicate. The `capability` kind resolves "which published `pkg@version` first
        carries capability C?" from publish provenance and late-binds it into the run. See
        `app/readiness.ts` for the full per-kind semantics.
      type: object
      additionalProperties: false
      required:
        - kind
        - target
      properties:
        kind:
          type: string
          enum: [http, command, npm, github-check, capability, pr, epic]
          description: The readiness source. `command` is the escape hatch; `capability` resolves a cross-repo published-artifact edge; `pr` watches an in-flight PR's merge state (ADR 0005 §2); `epic` gates on an nwf plan-fanout epic reaching "fully merged", keyed by its `planKey` (issue #568).
        target:
          type: string
          minLength: 1
          description: The kind-specific target (a URL, a shell command, a `pkg@version`, an `owner/repo@ref`, `github-releases:owner/repo`, an `owner/repo#123` PR reference for the `pr` kind, or an `owner/repo#NN` planKey — the epic issue — for the `epic` kind). For a `pr`/`epic` target used in a **delivery-graph `wait` node** it may instead be a `<nodeId>.<fact>` late-binding reference the delivery-graph compiler resolves at dispatch (issue #548/#570); this rewrite exists ONLY on the delivery-graph dispatch path — other surfaces (e.g. feature-intake readiness) have no such resolver, so they must supply a literal handle.
        onTimeout:
          type: string
          enum: [escalate, fail, continue]
          description: What the gate does when the bounded wait elapses (default `escalate`).
        credentialEnv:
          type: string
          description: A declared env-contract key supplying a credential (http kind only). Names a key, never a secret value.
        match:
          type: object
          additionalProperties: false
          description: The per-kind readiness predicate; every field is optional and read only by the kinds that understand it.
          properties:
            status: { type: integer, description: "http: the exact status that means ready (default any 2xx)." }
            bodyIncludes: { type: string, description: "http: a substring the response body must contain." }
            exitCode: { type: integer, description: "command: the exit code that means ready (default 0)." }
            stdoutIncludes: { type: string, description: "command/npm: a substring stdout must contain." }
            version: { type: string, description: "npm: the version that must be published." }
            conclusion: { type: string, description: "github-check: the conclusion that means ready (default success)." }
            checkName: { type: string, description: "github-check: restrict to the named check run." }
            capabilityRef: { type: string, description: "capability: the upstream issue/PR handle the resolved version must carry." }
            package: { type: string, description: "capability: the package whose releases are scanned for provenance." }
            verifyCommand: { type: string, description: "capability: optional empirical verifier run once at the gate boundary." }
            prState: { type: string, enum: [ready, merged, mergeable, checks-green], description: "pr: the declared PR state to wait for (default merged)." }
            epicState: { type: string, enum: [merged, done], description: "epic: the declared plan-fanout aggregate state to wait for (default merged) — both mean 'fully merged' (issue #568)." }
        poll:
          type: object
          additionalProperties: false
          description: The poll cadence (how often to re-probe, how long to keep trying, and the backoff shape).
          properties:
            everyMs: { type: integer, description: Interval between poll attempts (ms). }
            timeoutMs: { type: integer, description: Bounded budget (ms) before the gate escalates. }
            backoff: { type: string, enum: [fixed, exponential], description: Backoff shape between attempts. }
    DeliveryGraph:
      description: >-
        An agent-authored delivery graph (ADR 0005) — the SINGLE agent-facing artifact for a
        heterogeneous, partly-human, cross-repo delivery runbook. It is DATA, never an executable
        artifact: a JSON DAG whose nodes each name a `kind` from a CLOSED allowlist
        (`agent`/`wait`/`human`/`connector` — Decision 1/2, the trust boundary) and whose `edges`
        name DISCOVERED facts (Decision 3). Ingest validates the SHAPE here and the SEMANTICS
        (acyclicity, edge integrity, fact resolution) in the pure `validateDeliveryGraph`
        (`app/deliveryGraph.ts`). This slice (S0) defines the vocabulary + validation surface ONLY —
        no compiler, dispatch, or execution (those land in later slices).
      type: object
      additionalProperties: false
      # Canonical worked example — the §9.5 `wait[epic]`-gated graph from the operator guide
      # (docs/agent-guide.md). `compileDeliveryGraph` takes THIS structured object directly. The
      # generator (scripts/inline-mcp-bodies.ts) carries this example into the projected MCP tool
      # schema so an agent can call the tool from the surface alone.
      example:
        name: start #567 once epic #488 has fully merged
        nodes:
          - id: gate-epic
            kind: wait
            wait:
              kind: epic
              target: nanobpm/nano-ide#488
              match: { epicState: merged }
              poll: { everyMs: 300000, timeoutMs: 259200000 }
              onTimeout: escalate
            emits:
              - { name: prCount, type: number }
          - id: start-b
            kind: agent
            agent:
              jobType: senior:feature
              prompt: Implement nanobpm/nano-workforce#567 and open a PR.
        edges:
          - { from: gate-epic, to: start-b }
      required:
        - nodes
      properties:
        name:
          type: string
          maxLength: 255
          description: OPTIONAL human-readable label for the graph (shown in the rendered preview).
        nodes:
          type: array
          minItems: 1
          maxItems: 256
          items:
            $ref: "#/components/schemas/DeliveryNode"
          description: >-
            The graph's nodes. Each carries a unique `id` and a `kind` from the closed allowlist,
            plus its per-kind config and its typed `emits[]` declaration. Node ids must be unique
            across the graph (enforced by `validateDeliveryGraph`).
        edges:
          type: array
          maxItems: 1024
          items:
            $ref: "#/components/schemas/DeliveryEdge"
          description: >-
            The dependency edges — the graph's discovered-fact topology (Decision 3). Each edge means
            "`to` proceeds once fact `from` about the upstream node is observable". `from` is either a
            bare `<nodeId>` (the degenerate "wait for the upstream node's completion" fact) or a
            qualified `<nodeId>.<fact>` referencing one of that node's declared `emits`. Omit/`[]` for
            a set of independent (root) nodes. The edge set must be a DAG.
    DeliveryNode:
      description: >-
        One node in a delivery graph. A discriminated union on `kind` over the CLOSED allowlist; the
        matching per-kind config object (`agent`/`wait`/`connector`) is REQUIRED and names the
        engine-native body the node delegates to (Decision 2 — the graph schedules, it does not
        re-implement execution). The `human` config is the sole exception — it is OPTIONAL (a bare
        `human` node resolves to a generic emit-capturing form fallback in S3).
      oneOf:
        - $ref: "#/components/schemas/DeliveryNodeAgent"
        - $ref: "#/components/schemas/DeliveryNodeWait"
        - $ref: "#/components/schemas/DeliveryNodeHuman"
        - $ref: "#/components/schemas/DeliveryNodeConnector"
      discriminator:
        propertyName: kind
        mapping:
          agent: "#/components/schemas/DeliveryNodeAgent"
          wait: "#/components/schemas/DeliveryNodeWait"
          human: "#/components/schemas/DeliveryNodeHuman"
          connector: "#/components/schemas/DeliveryNodeConnector"
    DeliveryFact:
      description: >-
        A typed output a node declares it will EMIT (ADR 0005 Decision 3/4 — emitted-fact typing).
        A downstream edge references it as `from: "<nodeId>.<fact>"`, so a bind is validated against
        this declaration, not stringly. A "click done" human node or a pass-through node declares no
        facts (`emits` absent/empty) — the degenerate no-emit case.
      type: object
      additionalProperties: false
      required:
        - name
        - type
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 128
          pattern: '^[A-Za-z_][A-Za-z0-9_]*$'
          description: The fact's identifier, referenced downstream as `<nodeId>.<name>`. Must be unique within the node.
        type:
          type: string
          enum: [string, number, boolean, artifact, version, url, pr]
          description: >-
            The fact's declared type. `artifact` is a `pkg@version` handle, `version` a bare version
            string, `url` a location — mirrors the values `capability`/`pr` probes late-bind. `pr`
            (issue #548) is a PR reference (`owner/repo#N`) an `agent` node emits for the PR it opened,
            so a downstream `connector[converge*]`/`wait[pr]` node LATE-BINDS its target PR from the
            fact instead of a hardcoded literal.
        description:
          type: string
          maxLength: 512
          description: OPTIONAL human note describing what the fact carries.
    DeliveryNodeCommon:
      type: object
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 128
          pattern: '^[A-Za-z_][A-Za-z0-9_.-]*$'
          description: The node's identifier, unique within the graph and referenced by edges.
        emits:
          type: array
          maxItems: 32
          items:
            $ref: "#/components/schemas/DeliveryFact"
          description: >-
            The typed facts this node hands forward when it completes (Decision 3/4). Absent/empty for
            a node that emits nothing. Downstream edges bind these via `from: "<nodeId>.<fact>"`.
    DeliveryNodeAgent:
      description: >-
        An `agent` node — a worker executes an agent job type (the existing fan-out body). Bounded
        (timeout → escalate) and resumable like every node.
      allOf:
        - $ref: "#/components/schemas/DeliveryNodeCommon"
        - type: object
          additionalProperties: false
          required:
            - id
            - kind
            - agent
          properties:
            id: { type: string }
            kind: { type: string, enum: [agent] }
            emits:
              type: array
              items: { $ref: "#/components/schemas/DeliveryFact" }
              description: >-
                The typed facts this agent node hands forward (issue #506 — the classifier-emit
                contract). Each declared fact is appended to the node's dispatch prompt as an
                instruction the servicing `senior:*` agent MUST honour: return the fact as an extra
                TOP-LEVEL field of its result JSON (the same `AGENT_RESULT_FILE` envelope that carries
                `status`/`summary`/`pr`). The delivery output-mapping publishes that completion variable
                (named exactly after the fact) so a downstream guarded edge (`when: "<node>.<fact>"` +
                `equals`) routes on it; an omitted fact takes the split's `default` branch.
            agent:
              type: object
              additionalProperties: false
              required:
                - jobType
              properties:
                jobType:
                  type: string
                  minLength: 1
                  description: The agent job type a worker executes for this node (e.g. `senior:feature`).
                prompt:
                  type: string
                  maxLength: 20000
                  description: OPTIONAL steering prompt appended to the node's job brief.
                repository:
                  type: string
                  maxLength: 255
                  pattern: '^[A-Za-z0-9-]+/(?!.*\.[Gg][Ii][Tt]$)[A-Za-z0-9._-]+$'
                  description: >-
                    OPTIONAL per-node `owner/repo` this agent node implements against (#739). A delivery
                    graph provisions the `io.nanobpm.agentTask.repository` isolation envelope PER agent
                    cell from THIS field, so a genuinely cross-repo graph (each node a different repo)
                    isolates correctly without the operator ticking `repoless`. Absent → the node falls
                    back to the run-level dispatch `repository`. When set it must be exactly `owner/repo`
                    (a trailing `.git` and any non-`owner/repo` shape are rejected at submit), the same
                    allowlist `repoEnvelopeVars`/the dispatch door apply. The `sequenceIssues` generator
                    populates this automatically from the `owner/repo#N` each node implements.
                baseBranch:
                  type: string
                  maxLength: 255
                  pattern: '^(?![/.-])(?!.*[/.]$)(?!.*\.\.)(?!.*//)(?!.*/\.)(?!.*\.lock(?:/|$))[A-Za-z0-9._/-]+$'
                  description: >-
                    OPTIONAL per-node base branch this agent node branches off (#739) — the `ref` the
                    harness checks out in the isolated clone (the pre-PR shape: the harness cuts the
                    deterministic `feat/<node.id>` branch off this base itself, per #776, so a forgetful
                    agent can never be left committing on the base branch). Absent → the run-level dispatch `baseBranch`
                    (else the node's repository default branch). A value that is not a plausible git
                    branch name is rejected at submit; the pattern mirrors the authoritative server-side
                    gate (`isPlausibleBranchName`, app/baseBranch.ts).
                converge:
                  type: boolean
                  description: >-
                    OPTIONAL first-class CONVERGE policy (ADR 0006 §3 / S5) — a DECLARED, compiler-
                    validated completion-policy flag on this cell node. It declares that the node's
                    opened PR is to be driven through the review-convergence loop to green as an
                    edge-gated completion policy; this slice adds and validates the flag, with the
                    delivery-graph execution wiring that consumes it landing in a follow-up slice.
                    It supersedes (in intent) the emergent `feature.bpmn` `gw-converge` gateway and
                    the "un-draft + merge #B" prompt prose a delivery-graph `agent` node used to
                    smuggle. Converge and merge are SEPARABLE phases; a node may converge without
                    merging (stop at green and gate the landing behind a downstream node).
                merge:
                  type: boolean
                  description: >-
                    OPTIONAL first-class MERGE (land) policy (ADR 0006 §3 / S5) — a DECLARED,
                    compiler-validated flag. When set it declares that the cell lands its PR.
                    REQUIRES `converge: true` — you cannot land a PR you have not driven to green
                    (the validator rejects `merge` without `converge`). This slice adds and validates
                    the flag; the execution wiring that consumes it lands in a follow-up slice.
                    TWO-LEVEL (ADR 0003 base-branch admission): a UNIT node lands onto its epic/graph
                    base branch, never `main` directly; the graph's final merge-to-`main` is a
                    separate top-level step.
                timeout:
                  type: string
                  pattern: '^[Pp](?!$)(\d+[Yy])?(\d+[Mm])?(\d+[Ww])?(\d+[Dd])?([Tt](?=\d)(\d+[Hh])?(\d+[Mm])?(\d+[Ss])?)?$'
                  maxLength: 64
                  description: >-
                    OPTIONAL per-node ISO-8601 SLA timeout (#505). Overrides the run-level `nodeTimeout`
                    (and the `PT1H` default) for THIS node's bounded-timeout → escalate boundary timer, so
                    a legitimately-long node (e.g. a full `senior:feature` implementation) can outlast a
                    quick gate without a spurious escalation. Absent → the run/default value.
    DeliveryNodeWait:
      description: >-
        A `wait` node — a durable `ReadinessProbe` (ADR 0001 §2) watching an external fact. Reuses the
        existing `ReadinessProbe` shape verbatim (Decision 3 — never a second wait loop); the `pr`
        merge-state kind is added to that shape by slice S2 and flows in here automatically. The
        probe's `poll.timeoutMs` sets THIS node's escalation boundary (how long the gate waits before
        it acts on `onTimeout`), falling back to the run/default when absent (#462). `onTimeout`
        `escalate` (default) parks the elapsed gate on a human-completable task; `continue` proceeds
        past the gate as not-ready with NO human stop (a sharp edge — the downstream side-effecting
        node then runs without the awaited fact); `fail` is NOT yet supported on a delivery `wait`
        node (blocked on engine terminate-end execution, Magikcraft/nano-bpm#978) and is rejected at
        compile with a path-qualified error rather than silently degrading.
      allOf:
        - $ref: "#/components/schemas/DeliveryNodeCommon"
        - type: object
          additionalProperties: false
          required:
            - id
            - kind
            - wait
          properties:
            id: { type: string }
            kind: { type: string, enum: [wait] }
            emits: { type: array, items: { $ref: "#/components/schemas/DeliveryFact" } }
            wait:
              $ref: "#/components/schemas/ReadinessProbe"
    DeliveryNodeHuman:
      description: >-
        A `human` node — a scheduled user task + form (ADR 0002 machinery promoted from exception to
        node, Decision 4). Surfaces "now do X" on the Tasks inbox, blocks dependents, is answerable by
        a human OR an agent, is SLA-bounded, and can EMIT a typed fact its form captures.
      allOf:
        - $ref: "#/components/schemas/DeliveryNodeCommon"
        - type: object
          additionalProperties: false
          required:
            - id
            - kind
          properties:
            id: { type: string }
            kind: { type: string, enum: [human] }
            emits: { type: array, items: { $ref: "#/components/schemas/DeliveryFact" } }
            human:
              type: object
              additionalProperties: false
              description: >-
                OPTIONAL human-node config. `formKey` explicitly attaches a form (else a form is
                selected by node category, else a generic emit-capturing fallback — resolved in S3).
                The node's typed output is declared via the node-level `emits[]`.
              properties:
                formKey:
                  type: string
                  minLength: 1
                  description: OPTIONAL explicit form to attach at authoring time (specific-else-generic resolution, S3).
                prompt:
                  type: string
                  maxLength: 20000
                  description: OPTIONAL instruction shown to the human/agent completing the task ("now do X").
    DeliveryNodeConnector:
      description: >-
        A `connector` node — an automated, side-effecting outbound action (the connector I/O surface).
        Side-effecting, so it carries a `dedupeKey` and tolerates at-least-once execution. The
        `payload` schema is a minimal forward-declared stub in this slice (ADR 0005 non-goal — the
        concrete connector I/O lands later).
      allOf:
        - $ref: "#/components/schemas/DeliveryNodeCommon"
        - type: object
          additionalProperties: false
          required:
            - id
            - kind
            - connector
          properties:
            id: { type: string }
            kind: { type: string, enum: [connector] }
            emits: { type: array, items: { $ref: "#/components/schemas/DeliveryFact" } }
            connector:
              type: object
              additionalProperties: false
              required:
                - target
              properties:
                target:
                  type: string
                  minLength: 1
                  description: The connector action target (forward-declared — the concrete scheme lands in a later slice).
                dedupeKey:
                  type: string
                  minLength: 1
                  description: >-
                    OPTIONAL idempotency key so an at-least-once resume cannot double-fire this
                    side-effecting node (ADR 0005 Decision 7). Author-supplied or graph-derived.
                payload:
                  type: object
                  additionalProperties: true
                  description: Minimal forward-declared payload stub — the concrete connector payload schema is deferred (ADR non-goal).
                timeout:
                  type: string
                  pattern: '^[Pp](?!$)(\d+[Yy])?(\d+[Mm])?(\d+[Ww])?(\d+[Dd])?([Tt](?=\d)(\d+[Hh])?(\d+[Mm])?(\d+[Ss])?)?$'
                  maxLength: 64
                  description: >-
                    OPTIONAL per-node ISO-8601 SLA timeout (#505). Overrides the run-level `nodeTimeout`
                    (and the `PT1H` default) for THIS connector node's bounded-timeout → escalate boundary
                    timer. Absent → the run/default value.
    DeliveryEdge:
      description: >-
        A dependency edge — "`to` proceeds once fact `from` is observable" (ADR 0005 Decision 3).
        `from` is either a bare `<nodeId>` (wait for the upstream node's completion fact) or a
        qualified `<nodeId>.<fact>` referencing a declared `emits` fact of that node. Both endpoints
        must resolve to a node in the graph, the referenced fact must be declared, and the whole edge
        set must be a DAG — all enforced by `validateDeliveryGraph`. An OPTIONAL `when`/`equals` guard
        (or a `default` else-branch) makes the edge CONDITIONAL, turning its producer into an
        exclusive split (ADR 0005 S7) — a node's out-edges are then ALL guarded or ALL unconditional.
      type: object
      additionalProperties: false
      required:
        - from
        - to
      properties:
        from:
          type: string
          minLength: 1
          description: The upstream endpoint — `<nodeId>` (completion) or `<nodeId>.<fact>` (a declared emitted fact).
        to:
          type: string
          minLength: 1
          description: The dependent node's id — proceeds once `from` is observed.
        when:
          type: string
          minLength: 1
          description: >-
            OPTIONAL guard reference `<nodeId>.<fact>` naming a SCALAR emitted fact (`string`,
            `number`, or `boolean`) of the `from`-adjacent producer (ADR 0005 S7). Its presence makes
            this a GUARDED edge and turns the producer into an exclusive-split point: the edge is taken
            only when that runtime fact `equals` the literal below. Equality-only — no arbitrary
            expressions (the trust boundary). Mutually exclusive with `default`.
        equals:
          description: >-
            The literal value `when`'s fact must equal for this guarded edge to be taken (ADR 0005 S7).
            REQUIRED iff `when` is present, and its JSON type must match the referenced fact's declared
            type (`string`/`number`/`boolean`).
          oneOf:
            - type: string
            - type: number
            - type: boolean
        default:
          type: boolean
          enum: [true]
          description: >-
            OPTIONAL — marks this edge as the ELSE branch of the exclusive split (taken when no guarded
            edge matches at runtime). A FLAG: only `true` is meaningful, so it is constrained to `true`
            (omit the field entirely for a non-default edge — `default: false` is not a valid wire
            value). At most one `default` edge per split node. Mutually exclusive with `when`/`equals`
            (ADR 0005 S7).
    DeliveryCompileError:
      description: >-
        One semantic-validation or compile failure, path-qualified at the offending input
        (`nodes[2].kind`, `edges[1].from`, …). Mirrors a `validateDeliveryGraph` (`app/deliveryGraph.ts`)
        error stripped to the wire pair `{ path, message }` (the stable `code` stays server-side).
      type: object
      additionalProperties: false
      required:
        - path
        - message
      properties:
        path:
          type: string
          description: JSON-path pointer at the offending node/edge/fact.
        message:
          type: string
          description: Human-actionable description of the failure.
    DeliveryGraphPreviewSubmit:
      description: >-
        The human-facing UI JSON-paste PREVIEW request (issues #386 + #516). The Delivery Graphs page's
        "Preview" action cannot submit a structured object, so the operator's pasted delivery-graph is
        carried as a raw JSON STRING (`graphJson`), parsed server-side and handed to the SAME pure
        `compileDeliveryGraph` compiler the agent-facing door uses. Preview compiles WITHOUT persisting.
        Per-operation schema (not shared with the stage door) so each door's request stays independently
        evolvable.
      type: object
      additionalProperties: false
      # Worked example — the SAME canonical §9.5 `wait[epic]`-gated graph as `DeliveryGraph.example`,
      # but serialised to a JSON STRING (the text-door convention this operation shares with the
      # cockpit paste). `previewDeliveryGraph` takes `{ "graphJson": "<serialized DeliveryGraph>" }`.
      example:
        graphJson: >-
          {"name":"start #567 once epic #488 has fully merged","nodes":[{"id":"gate-epic","kind":"wait","wait":{"kind":"epic","target":"nanobpm/nano-ide#488","match":{"epicState":"merged"},"poll":{"everyMs":300000,"timeoutMs":259200000},"onTimeout":"escalate"},"emits":[{"name":"prCount","type":"number"}]},{"id":"start-b","kind":"agent","agent":{"jobType":"senior:feature","prompt":"Implement nanobpm/nano-workforce#567 and open a PR."}}],"edges":[{"from":"gate-epic","to":"start-b"}]}
      required:
        - graphJson
      properties:
        graphJson:
          type: string
          description: The pasted delivery-graph JSON (a serialised `DeliveryGraph`), parsed server-side.
    DeliveryGraphStageSubmit:
      description: >-
        The human-facing UI JSON-paste STAGE request (issue #516) — the commit half of the preview/stage
        split. The Delivery Graphs page's "Stage" action carries the operator's pasted delivery-graph as
        a raw JSON STRING (`graphJson`), parsed server-side and handed to the SAME pure
        `compileDeliveryGraph` compiler the preview/agent doors use, then persisted as a `staged`
        proposal. Per-operation schema (not shared with the preview door) so each door's request stays
        independently evolvable.
      type: object
      additionalProperties: false
      required:
        - graphJson
      properties:
        graphJson:
          type: string
          description: The pasted delivery-graph JSON (a serialised `DeliveryGraph`), parsed server-side.
    DeliveryGraphDispatchRequest:
      description: >-
        The OPERATOR dispatch request (ADR 0005 Decision 7, issue #460). The cockpit's staged-proposals
        grid posts the content `digest` of the proposal the operator picked; the door loads that staged
        proposal and launches the retained runner for its previewed graph. Dispatch is an operator
        action — this request carries NO graph and NO token (the graph is already staged; the operator's
        click is the approval).

        Repository provisioning is REQUIRED by default (#729) unless the graph is FULLY node-provisioned
        (#739). The run EITHER names the run-level `repository` + `baseBranch` its `agent` nodes fall back
        to, OR opts out with `repoless: true` for a genuinely checkout-less graph, OR — when EVERY `agent`
        node declares its OWN `repository` (a cross-repo graph, #739) — supplies NEITHER (the node-
        provisioned shape). Modeled as `oneOf` named variants (Camunda REST v2 pattern, as the
        convergence/plan/feature start bodies are) so the runtime rejects the "both" shape AT THE EDGE
        with a 400 that names the allowed shapes. The remaining invariant — that no `agent` node resolves
        to NO repository on a non-`repoless` run — is enforced after the staged graph is loaded (the door
        has the graph; the request alone does not), so a graph with an unprovisioned node still fails
        loudly rather than silently sharing the worker's launch dir (the issue #684 field failure).
      oneOf:
        - $ref: "#/components/schemas/DeliveryGraphDispatchWithRepository"
        - $ref: "#/components/schemas/DeliveryGraphDispatchRepoless"
        - $ref: "#/components/schemas/DeliveryGraphDispatchNodeProvisioned"
    DeliveryGraphDispatchWithRepository:
      description: >-
        The repository-provisioned dispatch shape (#729): names the `repository` + `baseBranch` the run's
        `agent` nodes implement against, so the runner seeds the canonical
        `io.nanobpm.agentTask.repository` isolation envelope onto every agent job. `repoless` is not a
        member of this variant (`additionalProperties: false`), so supplying it alongside a repository is
        rejected — the mutual exclusivity the `oneOf` enforces.
      type: object
      additionalProperties: false
      required:
        - digest
        - repository
        - baseBranch
      properties:
        digest:
          type: string
          description: The staged proposal's content digest (its primary key) — the proposal to dispatch.
        idempotencyKey:
          type: string
          maxLength: 255
          description: OPTIONAL idempotency key. A re-dispatch with the same key (or, when omitted, the same digest) does not double-launch. Blank/whitespace is treated as absent.
        nodeTimeout:
          type: string
          pattern: '^[Pp](?!$)(\d+[Yy])?(\d+[Mm])?(\d+[Ww])?(\d+[Dd])?([Tt](?=\d)(\d+[Hh])?(\d+[Mm])?(\d+[Ss])?)?$'
          maxLength: 64
          description: >-
            OPTIONAL run-level ISO-8601 SLA timeout for `agent`/`connector` nodes (#505) — the
            bounded-timeout → escalate boundary bound every such node inherits unless it declares its own
            per-node `timeout`. Absent → the `PT1H` default. An invalid duration is rejected at submit.
        probeTimeout:
          type: string
          pattern: '^[Pp](?!$)(\d+[Yy])?(\d+[Mm])?(\d+[Ww])?(\d+[Dd])?([Tt](?=\d)(\d+[Hh])?(\d+[Mm])?(\d+[Ss])?)?$'
          maxLength: 64
          description: >-
            OPTIONAL run-level ISO-8601 poll budget for `wait` gates (#505) before they escalate. Absent →
            the `PT30M` default. An invalid duration is rejected at submit.
        escalationSlaTimeout:
          type: string
          pattern: '^[Pp](?!$)(\d+[Yy])?(\d+[Mm])?(\d+[Ww])?(\d+[Dd])?([Tt](?=\d)(\d+[Hh])?(\d+[Mm])?(\d+[Ss])?)?$'
          maxLength: 64
          description: >-
            OPTIONAL run-level ISO-8601 SLA for `human` nodes (#505) before they record an `escalated`
            outcome. Absent → the `P1D` default. An invalid duration is rejected at submit.
        repository:
          type: string
          maxLength: 255
          pattern: '^[A-Za-z0-9-]+/(?!.*\.[Gg][Ii][Tt]$)[A-Za-z0-9._-]+$'
          description: >-
            The `owner/repo` the run's `agent` nodes implement against (#684/#686). REQUIRED together with
            `baseBranch` on this variant: the runner seeds the canonical
            `io.nanobpm.agentTask.repository` provisioning envelope (`repoEnvelopeVars`) as a run-root
            process variable so every agent node's servicing `senior:*` job gets an ISOLATED throwaway
            clone instead of inheriting — and clobbering — the worker's launch dir. A value that is not
            exactly `owner/repo` is rejected at submit.
        baseBranch:
          type: string
          maxLength: 255
          pattern: '^(?![/.-])(?!.*[/.]$)(?!.*\.\.)(?!.*//)(?!.*/\.)(?!.*\.lock(?:/|$))[A-Za-z0-9._/-]+$'
          description: >-
            The base branch the run's `agent` nodes branch off (#684/#686) — the `ref` the harness checks
            out in the isolated clone (the PRE-PR shape: each agent cuts its own `feat/<node.id>` branch
            off this base). REQUIRED together with `repository` on this variant. A value that is not a
            plausible git branch name (whitespace, shell metacharacters, a leading `-`, `..`/`//`, a path
            segment starting with `.` or ending in `.lock`, etc.) is rejected at submit. This pattern
            mirrors the authoritative server-side gate (`isPlausibleBranchName`, app/baseBranch.ts) so the
            documented contract and the door agree.
    DeliveryGraphDispatchRepoless:
      description: >-
        The checkout-less dispatch shape (#729): an EXPLICIT `repoless: true` opt-out of repository
        provisioning for a genuinely repo-less graph (e.g. one whose nodes touch no repository). NO
        isolation envelope is seeded — the legacy launch-dir behaviour, but now a CONSCIOUS choice rather
        than a silent fallback. `repository`/`baseBranch` are not members of this variant
        (`additionalProperties: false`), so supplying either alongside `repoless: true` is rejected — the
        mutual exclusivity the `oneOf` enforces.
      type: object
      additionalProperties: false
      required:
        - digest
        - repoless
      properties:
        digest:
          type: string
          description: The staged proposal's content digest (its primary key) — the proposal to dispatch.
        idempotencyKey:
          type: string
          maxLength: 255
          description: OPTIONAL idempotency key. A re-dispatch with the same key (or, when omitted, the same digest) does not double-launch. Blank/whitespace is treated as absent.
        nodeTimeout:
          type: string
          pattern: '^[Pp](?!$)(\d+[Yy])?(\d+[Mm])?(\d+[Ww])?(\d+[Dd])?([Tt](?=\d)(\d+[Hh])?(\d+[Mm])?(\d+[Ss])?)?$'
          maxLength: 64
          description: >-
            OPTIONAL run-level ISO-8601 SLA timeout for `agent`/`connector` nodes (#505) — the
            bounded-timeout → escalate boundary bound every such node inherits unless it declares its own
            per-node `timeout`. Absent → the `PT1H` default. An invalid duration is rejected at submit.
        probeTimeout:
          type: string
          pattern: '^[Pp](?!$)(\d+[Yy])?(\d+[Mm])?(\d+[Ww])?(\d+[Dd])?([Tt](?=\d)(\d+[Hh])?(\d+[Mm])?(\d+[Ss])?)?$'
          maxLength: 64
          description: >-
            OPTIONAL run-level ISO-8601 poll budget for `wait` gates (#505) before they escalate. Absent →
            the `PT30M` default. An invalid duration is rejected at submit.
        escalationSlaTimeout:
          type: string
          pattern: '^[Pp](?!$)(\d+[Yy])?(\d+[Mm])?(\d+[Ww])?(\d+[Dd])?([Tt](?=\d)(\d+[Hh])?(\d+[Mm])?(\d+[Ss])?)?$'
          maxLength: 64
          description: >-
            OPTIONAL run-level ISO-8601 SLA for `human` nodes (#505) before they record an `escalated`
            outcome. Absent → the `P1D` default. An invalid duration is rejected at submit.
        repoless:
          type: boolean
          enum: [true]
          description: >-
            EXPLICIT opt-out of repository provisioning (#729). MUST be `true` on this variant — it
            dispatches a genuinely checkout-less graph with NO isolation envelope (the legacy launch-dir
            behaviour, now a CONSCIOUS choice). Mutually exclusive with `repository`/`baseBranch`: those
            fields are not members of this variant, so supplying either alongside `repoless: true` fails
            `oneOf` matching and is a 400. To provision a repository, use the repository variant (supply
            `repository` + `baseBranch`) and omit `repoless` entirely.
    DeliveryGraphDispatchNodeProvisioned:
      description: >-
        The FULLY node-provisioned dispatch shape (#739): supplies NEITHER a run-level `repository`/
        `baseBranch` NOR `repoless`, for a cross-repo graph in which EVERY `agent` node declares its own
        `repository` (so the run needs no uniform fallback and no checkout-less opt-out). The runner
        seeds each agent cell's `io.nanobpm.agentTask.repository` isolation envelope from that node's own
        declared repository. The post-load invariant still holds: if ANY `agent` node resolves to no
        repository under this shape (it declared none and there is no run-level fallback), the dispatch
        fails loudly with a 400 rather than silently sharing the launch dir — so this shape is only valid
        for a graph whose every agent node is self-provisioned. `repository`/`baseBranch`/`repoless` are
        not members of this variant (`additionalProperties: false`), so supplying any of them selects a
        different variant instead.
      type: object
      additionalProperties: false
      required:
        - digest
      properties:
        digest:
          type: string
          description: The staged proposal's content digest (its primary key) — the proposal to dispatch.
        idempotencyKey:
          type: string
          maxLength: 255
          description: OPTIONAL idempotency key. A re-dispatch with the same key (or, when omitted, the same digest) does not double-launch. Blank/whitespace is treated as absent.
        nodeTimeout:
          type: string
          pattern: '^[Pp](?!$)(\d+[Yy])?(\d+[Mm])?(\d+[Ww])?(\d+[Dd])?([Tt](?=\d)(\d+[Hh])?(\d+[Mm])?(\d+[Ss])?)?$'
          maxLength: 64
          description: >-
            OPTIONAL run-level ISO-8601 SLA timeout for `agent`/`connector` nodes (#505) — the
            bounded-timeout → escalate boundary bound every such node inherits unless it declares its own
            per-node `timeout`. Absent → the `PT1H` default. An invalid duration is rejected at submit.
        probeTimeout:
          type: string
          pattern: '^[Pp](?!$)(\d+[Yy])?(\d+[Mm])?(\d+[Ww])?(\d+[Dd])?([Tt](?=\d)(\d+[Hh])?(\d+[Mm])?(\d+[Ss])?)?$'
          maxLength: 64
          description: >-
            OPTIONAL run-level ISO-8601 poll budget for `wait` gates (#505) before they escalate. Absent →
            the `PT30M` default. An invalid duration is rejected at submit.
        escalationSlaTimeout:
          type: string
          pattern: '^[Pp](?!$)(\d+[Yy])?(\d+[Mm])?(\d+[Ww])?(\d+[Dd])?([Tt](?=\d)(\d+[Hh])?(\d+[Mm])?(\d+[Ss])?)?$'
          maxLength: 64
          description: >-
            OPTIONAL run-level ISO-8601 SLA for `human` nodes (#505) before they record an `escalated`
            outcome. Absent → the `P1D` default. An invalid duration is rejected at submit.
    DeliveryGraphDismissRequest:
      description: >-
        The OPERATOR dismiss request (#520). The cockpit's staged-proposals grid posts the content
        `digest` of the proposal the operator wants to discard as noise; the door loads that staged
        proposal and flips it to the terminal `dismissed` status. Dismiss is an operator action — this
        request carries NO graph and NO token (the graph is already staged; the operator's click is the
        approval). It launches nothing.
      type: object
      additionalProperties: false
      required:
        - digest
      properties:
        digest:
          type: string
          description: The staged proposal's content digest (its primary key) — the proposal to dismiss.
    DeliveryGraphProposalBpmnRequest:
      description: >-
        Request the compiled BPMN of a staged delivery-graph proposal for read-only DI PREVIEW. Carries
        only the proposal's content `digest`; the door recompiles the staged graph deterministically. No
        deploy, no dispatch — this reads, it does not launch anything.
      type: object
      additionalProperties: false
      required:
        - digest
      properties:
        digest:
          type: string
          description: The staged proposal's content digest — the proposal whose compiled BPMN (with DI) to render.
    DeliveryGraphProposalBpmnResult:
      description: >-
        The compiled BPMN of a staged proposal, for read-only DI preview. `ok` discriminates success; a
        failure (unknown/expired/superseded/dispatched digest, corrupt or no-longer-compiling graph)
        carries a human `error`.
      type: object
      additionalProperties: false
      required:
        - ok
      properties:
        ok:
          type: boolean
          description: True when the staged graph recompiled and its BPMN is returned; false otherwise.
        error:
          type: string
          description: A human-readable failure message.
        digest:
          type: string
          description: The proposal's content digest, echoed on success.
        bpmn:
          type: string
          description: >-
            The compiled BPMN 2.0 XML INCLUDING diagram interchange (`bpmndi:BPMNDiagram`), recompiled
            deterministically from the staged graph — byte-identical to what a dispatch would deploy.
            Rendered read-only in the host explorer's definition preview. Nothing is deployed.
    StagedProposalSummary:
      description: >-
        One LIVE staged delivery-graph proposal (issue #511) — the metadata the staged App-View renders
        as a Preview-DI + Dispatch row. A projection of the durable `delivery_graph_proposals` row; the
        `graph`/`preview` payloads are omitted (the App-View recompiles by `digest` for the DI preview).
      type: object
      additionalProperties: false
      required:
        - digest
        - title
        - nodeCount
        - humanNodeCount
        - sideEffectCount
        - sideEffecting
        - createdAt
        - expiresAt
      properties:
        digest:
          type: string
          description: The proposal's content digest — the handle the Preview-DI and Dispatch doors take.
        title:
          type: string
          nullable: true
          description: The graph's name, when it carried one.
        nodeCount:
          type: integer
          description: Total nodes in the compiled graph.
        humanNodeCount:
          type: integer
          description: How many nodes park on a person.
        sideEffectCount:
          type: integer
          description: How many nodes perform a side effect (merge/publish) once dispatched.
        sideEffecting:
          type: boolean
          description: True when the graph has any side-effecting node — dispatching it authorises those actions.
        createdAt:
          type: string
          description: When the proposal was staged (ISO-8601).
        expiresAt:
          type: string
          description: When the proposal ages out of its TTL if never dispatched (ISO-8601).
    StagedProposalList:
      description: The live staged delivery-graph proposals awaiting dispatch (issue #511), newest first.
      type: object
      additionalProperties: false
      required:
        - count
        - proposals
      properties:
        count:
          type: integer
        proposals:
          type: array
          items:
            $ref: "#/components/schemas/StagedProposalSummary"
    DeliveryGraphLibraryEntry:
      description: >-
        One saved reusable delivery-graph LIBRARY entry (issue #522, epic #519 S3) — the durable base
        the Library App-View (S4/#523), filesystem import (S5/#524), and export (S6/#525) build on.
        Unlike a `staged` proposal (content-digest-keyed, TTL-swept), a library entry is keyed by a
        slug + short-hash of its NAME — the name *is* the identity — so its graph can be edited in
        place without moving the row, and it never ages out. (A rename is not an in-place update:
        because the id is derived from the name, renaming derives a new id, i.e. a new entry; the old
        row remains until explicitly deleted.) The full `graph` JSON is carried so the export
        affordance (S6/#525) can build a client-side download from the list payload without a second
        fetch.
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - description
        - graph
        - source
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          description: The entry's stable id — `<slug>-<sha256(name)[:8]>`, derived from the name (not the content).
        name:
          type: string
          description: The saved graph's human name.
        description:
          type: string
          nullable: true
          description: An optional human note, or null when none was given.
        graph:
          type: string
          description: The `DeliveryGraph` JSON (serialised) — validated/compiled before it was ever persisted.
        source:
          type: string
          enum: [composed, imported, from-staged, from-dispatched]
          description: How the entry entered the library (raw compose, filesystem import, or a staged/dispatched proposal's digest).
        createdAt:
          type: string
          description: When the entry was first saved (ISO-8601); preserved across an idempotent re-save of the same name.
        updatedAt:
          type: string
          description: When the entry was last saved/edited (ISO-8601).
    DeliveryGraphLibraryList:
      description: Every saved library entry (issue #522), newest first.
      type: object
      additionalProperties: false
      required:
        - count
        - entries
      properties:
        count:
          type: integer
        entries:
          type: array
          items:
            $ref: "#/components/schemas/DeliveryGraphLibraryEntry"
    SaveToLibrarySubmit:
      description: >-
        Save a delivery graph to the reusable library (issue #522). Carries the entry `name` (its
        slug/hash derive the id) and an optional `description`, PLUS exactly one graph source: either a
        raw `graphJson` STRING (validated + compiled before persisting, `source: composed`), or the
        `digest` of an existing staged/dispatched proposal whose already-stored graph is reused
        (`source: from-staged` / `from-dispatched`). A graph that fails to compile is a clean 400 and
        nothing is persisted.
      type: object
      additionalProperties: false
      required:
        - name
      properties:
        name:
          type: string
          description: The entry's human name — its slug + short-hash derive the stable library id (re-saving the same name upserts).
        description:
          type: string
          description: An optional human note stored alongside the entry.
        graphJson:
          type: string
          description: >-
            A raw `DeliveryGraph` JSON string to validate, compile and save (`source: composed`).
            Mutually exclusive with `digest`.
        digest:
          type: string
          description: The content digest of an existing staged/dispatched proposal whose stored graph is reused. Mutually exclusive with `graphJson`.
    ImportToLibrarySubmit:
      description: >-
        Import a delivery graph into the reusable library FROM A FILE (issue #524, epic #519 S5). The
        compose App-View's `<input type=file accept=.json>` reads the selected file's text client-side
        and POSTs it here as the raw `graphJson` string. The door validates + compiles it via the SAME
        `parseAndCompileText` pipeline the preview/stage/save doors use, then persists it with
        `source: imported`. A file that is not valid JSON, or a graph that fails to compile, is a clean
        400 with path-qualified errors and NOTHING is persisted. The entry `name` defaults to the
        imported graph's own `name`; an explicit `name` overrides it (an unnamed graph with no override
        is a clean 400).
      type: object
      additionalProperties: false
      required:
        - graphJson
      properties:
        graphJson:
          type: string
          description: The raw `DeliveryGraph` JSON text read from the imported file (validated + compiled before persisting).
        name:
          type: string
          description: Optional override for the entry name — its slug + short-hash derive the library id. Defaults to the imported graph's own `name`.
        description:
          type: string
          description: An optional human note stored alongside the imported entry.
    SaveToLibraryResult:
      description: >-
        The save-to-library outcome (issue #522). `ok` discriminates success; success carries the
        persisted `entry`, a failure carries a human `error` (and, for a compile failure, path-qualified
        `errors`). Nothing is persisted on a failure.
      type: object
      additionalProperties: false
      required:
        - ok
      properties:
        ok:
          type: boolean
          description: True when the graph validated and the entry was saved; false otherwise.
        error:
          type: string
          description: A human-readable failure message.
        errors:
          type: array
          items:
            $ref: "#/components/schemas/DeliveryCompileError"
          description: Path-qualified validation/compile failures, when the submitted or referenced graph was malformed.
        entry:
          $ref: "#/components/schemas/DeliveryGraphLibraryEntry"
    DeleteLibraryEntryResult:
      description: The delete-library-entry outcome (issue #522). `deleted` is true when a row was removed, false when the id named nothing (idempotent).
      type: object
      additionalProperties: false
      required:
        - ok
        - deleted
      properties:
        ok:
          type: boolean
          description: True — the request was well-formed and processed.
        deleted:
          type: boolean
          description: True when an entry was removed; false when the id named no entry (a re-delete is a clean no-op).
    DeliveryGraphTextResult:
      description: >-
        The delivery-graph text-ingress outcome (issue #460) — a single shape covering the JSON-paste
        PREVIEW+STAGE summary, the by-`digest` OPERATOR DISPATCH outcome (dispatch takes only a staged
        proposal's `digest`, not pasted JSON), and any parse/validation error. `ok` discriminates
        success; a failure carries a human `error` (and, for a compile failure, path-qualified `errors`).
      type: object
      additionalProperties: false
      required:
        - ok
      properties:
        ok:
          type: boolean
          description: True on a successful preview+stage / dispatch; false on a parse/validation/dispatch failure.
        error:
          type: string
          description: A human-readable failure message (surfaced by the page's action banner).
        errors:
          type: array
          items:
            $ref: "#/components/schemas/DeliveryCompileError"
          description: Path-qualified validation/compile failures, when the submitted or staged graph was malformed.
        status:
          type: string
          description: The dispatch run's lifecycle position (`running`), when dispatched.
        runKey:
          type: string
          description: The dispatch run's idempotency key, when dispatched.
        digest:
          type: string
          description: The graph's content digest — the content-address of the staged proposal / dispatched run.
        sideEffecting:
          type: boolean
          description: Whether the graph has any side-effecting (`agent`/`connector`) node.
        alreadyRunning:
          type: boolean
          description: True when a dispatch short-circuited onto an already-running run.
        processInstanceKey:
          type: string
          description: The started engine instance key, when dispatched.
        processDefinitionId:
          type: string
          description: The started process definition id, when dispatched.
        message:
          type: string
          description: Additional human-readable detail from the door (e.g. the staged/dispatched summary).
        reviewUrl:
          type: string
          description: A navigational cockpit deep-link to the staged proposal (a pointer only — NOT a dispatch handle).
        staged:
          type: boolean
          description: True when a valid preview persisted the compiled graph as a staged proposal (issue #460).
        title:
          type: string
          description: The graph's human-readable name, echoed on a successful preview.
        nodeCount:
          type: integer
          description: The compiled graph's node count (preview).
        humanNodeCount:
          type: integer
          description: The compiled graph's human stop-point count (preview).
        sideEffectCount:
          type: integer
          description: The compiled graph's side-effecting node count (preview).
        diagram:
          type: string
          description: The mermaid flowchart of the compiled graph (preview).
        humanNodes:
          type: array
          items:
            $ref: "#/components/schemas/DeliveryHumanStop"
          description: >-
            The human stop-points the compiled graph parks on (preview) — where it waits for a person
            (or an agent answering on their behalf), rendered by the Delivery Graphs page (#441).
        sideEffects:
          type: array
          items:
            $ref: "#/components/schemas/DeliverySideEffect"
          description: >-
            The side-effecting (`agent`/`connector`) actions the compiled graph WILL perform (preview)
            — what an approval authorises (Decision 7), rendered by the Delivery Graphs page (#441).
        bpmn:
          type: string
          description: >-
            The compiled BPMN 2.0 XML INCLUDING diagram interchange (`bpmndi:BPMNDiagram`) — returned by
            the PURE preview door (`previewDeliveryGraph`) only, so the Delivery Graphs page can render
            the laid-out BPMN in the host explorer WITHOUT staging (#516). Byte-identical to what a
            dispatch would deploy. Omitted by the stage/dispatch outcomes.
    ResolvedDeliveryNode:
      description: >-
        A normalised node in the compiled graph (ADR 0005 slice S1) — its `id`, `kind`, the
        deterministic BPMN `element` id it compiled to, the engine-native `calledElement` body it
        delegates to (Decision 2 — absent for a `human` user task), its typed `emits[]`, and the
        upstream node ids it `dependsOn` (sorted).
      type: object
      additionalProperties: false
      required:
        - id
        - kind
        - element
        - emits
        - dependsOn
      properties:
        id:
          type: string
          description: The author's node id (unique across the graph).
        kind:
          type: string
          enum: [agent, wait, human, connector]
          description: The node's kind from the closed allowlist (the trust boundary).
        element:
          type: string
          description: The deterministic BPMN element id this node compiled to (e.g. `n0`).
        calledElement:
          type: string
          description: >-
            The engine-native sub-process/call-activity target this node delegates to (Decision 2).
            Absent for a `human` node (a native user task, not a call activity).
        emits:
          type: array
          items:
            $ref: "#/components/schemas/DeliveryFact"
          description: The node's typed emitted facts (empty when it emits nothing).
        dependsOn:
          type: array
          items:
            type: string
          description: The ids of the upstream nodes this node depends on, sorted for determinism.
    ResolvedDeliveryEdge:
      description: >-
        A resolved dependency edge — the author's `from`/`to` plus the resolved upstream `fromNode`
        and, when the `from` was qualified (`<nodeId>.<fact>`), the referenced `fromFact`.
      type: object
      additionalProperties: false
      required:
        - from
        - to
        - fromNode
      properties:
        from:
          type: string
          description: The author's `from` endpoint verbatim (`<nodeId>` or `<nodeId>.<fact>`).
        to:
          type: string
          description: The dependent node's id.
        fromNode:
          type: string
          description: The resolved upstream node id.
        fromFact:
          type: string
          description: The referenced emitted fact, when the edge `from` was qualified.
        when:
          type: string
          description: The guard reference (`<nodeId>.<fact>`) verbatim, present only on a guarded edge (ADR 0005 S7).
        equals:
          description: The literal the guard fact must equal, present only on a guarded edge (ADR 0005 S7).
          oneOf:
            - type: string
            - type: number
            - type: boolean
        default:
          type: boolean
          enum: [true]
          description: True when this is the exclusive split's default (else) branch; a FLAG, only ever `true` and omitted otherwise (ADR 0005 S7).
    ResolvedDeliveryGraph:
      description: >-
        The normalised graph the compiler resolved from the input (ADR 0005 slice S1) — nodes and
        edges sorted deterministically so the same JSON always yields the same preview.
      type: object
      additionalProperties: false
      required:
        - nodes
        - edges
      properties:
        name:
          type: string
          description: The graph's optional human-readable label, echoed from the input.
        nodes:
          type: array
          items:
            $ref: "#/components/schemas/ResolvedDeliveryNode"
          description: The normalised nodes, sorted by id.
        edges:
          type: array
          items:
            $ref: "#/components/schemas/ResolvedDeliveryEdge"
          description: The resolved edges, sorted deterministically.
    DeliveryHumanStop:
      description: >-
        A `human` node extracted for the preview — a point where the graph STOPS for a person (or an
        agent answering on their behalf). Carries the instruction, the optional attached form, and the
        typed facts the node will emit on completion.
      type: object
      additionalProperties: false
      required:
        - nodeId
        - emits
      properties:
        nodeId:
          type: string
          description: The human node's id.
        prompt:
          type: string
          description: The instruction shown to the human/agent ("now do X"), when declared.
        formKey:
          type: string
          description: The explicitly-attached form key, when declared.
        emits:
          type: array
          items:
            $ref: "#/components/schemas/DeliveryFact"
          description: The typed facts this human node will hand forward (empty for a "click done" stop).
    DeliverySideEffect:
      description: >-
        A side-effecting action the compiled graph WILL perform (ADR 0005 slice S1 preview) — an
        `agent` job run or a `connector` outbound action. Read-only `wait` gates and `human` stops are
        NOT side effects (they are surfaced separately). Lets a human see "what it will do" before
        approving (Decision 7).
      type: object
      additionalProperties: false
      required:
        - nodeId
        - kind
        - description
      properties:
        nodeId:
          type: string
          description: The id of the node that performs the side effect.
        kind:
          type: string
          enum: [agent, connector]
          description: The side-effecting node kind.
        description:
          type: string
          description: Human-readable summary of the effect (e.g. "runs agent job `senior:feature`").
        dedupeKey:
          type: string
          description: The connector's idempotency key, when declared (at-least-once safety, Decision 7).
    CompileDeliveryGraphResult:
      description: >-
        A successful compile (ADR 0005 slice S1) — the PURE, side-effect-free preview a co-designing
        agent iterates against. Carries the compiled one-shot `bpmn` (compile-to-native artifact), a
        human-readable `diagram` (mermaid), the `resolved` normalised graph, and the extracted
        `humanNodes[]` (where it stops for a person) and `sideEffects[]` (what it will do). NOTHING is
        deployed — `compile` and `start` are separate doors (Decision 5/7).
      type: object
      additionalProperties: false
      required:
        - ok
        - diagram
        - bpmn
        - resolved
        - humanNodes
        - sideEffects
      properties:
        ok:
          type: boolean
          enum: [true]
          description: Discriminant — `true` for a successful compile.
        diagram:
          type: string
          description: A human-readable mermaid `flowchart` of the resolved graph.
        bpmn:
          type: string
          description: >-
            The compiled one-shot BPMN process definition (compile-to-native). Deterministic — the same
            input graph always produces byte-identical XML. Not deployed here (S4 owns deployment).
        resolved:
          $ref: "#/components/schemas/ResolvedDeliveryGraph"
        humanNodes:
          type: array
          items:
            $ref: "#/components/schemas/DeliveryHumanStop"
          description: The human stop-points, sorted by node id.
        sideEffects:
          type: array
          items:
            $ref: "#/components/schemas/DeliverySideEffect"
          description: The side-effecting actions the graph will perform, sorted by node id.
    CompileDeliveryGraphErrors:
      description: >-
        A rejected compile (ADR 0005 slice S1) — the graph failed shape or semantic validation. Every
        error is path-qualified so the co-designing agent can fix the exact offending input and
        re-compile. Nothing was compiled or deployed.
      type: object
      additionalProperties: false
      required:
        - ok
        - errors
      properties:
        ok:
          type: boolean
          enum: [false]
          description: Discriminant — `false` for a rejected compile.
        errors:
          type: array
          items:
            $ref: "#/components/schemas/DeliveryCompileError"
          description: The path-qualified validation/compile failures (at least one).
    DeliveryProposalPreview:
      description: >-
        The operator-facing preview a staged proposal carries (ADR 0005 Decision 7, issue #460) — WHAT
        the graph does. Carries the mermaid `diagram`, the `sideEffects[]` a dispatch authorises, and
        the `humanNodes[]` where it parks on a person. It carries NO dispatch handle by construction —
        a preview, not an affordance to start a run.
      type: object
      additionalProperties: false
      required:
        - diagram
        - sideEffects
        - humanNodes
      properties:
        diagram:
          type: string
          description: A human-readable mermaid `flowchart` of the resolved graph.
        sideEffects:
          type: array
          items:
            $ref: "#/components/schemas/DeliverySideEffect"
          description: The side-effecting actions the graph WILL perform once an operator dispatches it.
        humanNodes:
          type: array
          items:
            $ref: "#/components/schemas/DeliveryHumanStop"
          description: The human stop-points where the graph parks on a person.
    CompileDeliveryGraphStaged:
      description: >-
        A successful agent-facing compile (ADR 0005 Decision 7, issue #460) — the graph validated and
        compiled, and was persisted as a `staged` proposal for operator review. The response is
        deliberately a PREVIEW plus a navigational pointer and NOTHING that can trigger a run: no run
        key, no token, no process-instance key. This is capability-by-absence — the agent's surface ends
        at propose → compile → stage; a HUMAN dispatches the staged proposal in the cockpit.
      type: object
      additionalProperties: false
      required:
        - status
        - message
        - digest
        - preview
        - reviewUrl
      properties:
        status:
          type: string
          enum: [ready]
          description: Discriminant — `ready` means the graph compiled and is staged for operator review.
        message:
          type: string
          description: >-
            A human-readable instruction telling the agent its role ends here — ask the operator to
            preview and approve (or request modifications) in the cockpit; dispatch is an operator
            action and there is no start endpoint.
        digest:
          type: string
          description: The compiled graph's content digest — so the agent can NAME the proposal to the operator.
        preview:
          $ref: "#/components/schemas/DeliveryProposalPreview"
        reviewUrl:
          type: string
          description: >-
            A NAVIGATIONAL cockpit deep-link to the staged proposal (helps the agent hand the human a
            link). It is a pointer only — NOT a dispatch handle; nothing in this response can start a run.
        superseded:
          type: array
          items:
            type: string
          description: >-
            Digests of prior staged proposals sharing this graph's LOGICAL key (derived from its `name`)
            that this stage replaced — flipped to `superseded` (issue #740). Empty on a first stage. Lets
            the agent tell the operator precisely which proposals it retired.
        siblingsStaged:
          type: integer
          description: >-
            How many OTHER live staged proposals remain after this stage (a DIFFERENT logical key from
            this one) — i.e. proposals this stage did NOT supersede (issue #740). A non-zero count flags
            potential ORPHANED SIBLINGS (e.g. an earlier stage of the "same" runbook under a different
            `name`, which supersede does not catch) cluttering the operator's Delivery Graphs list, so
            the agent can name them for cleanup.
    SequenceGate:
      description: >-
        An interleaved `wait` GATE (issue #740) that must go green before a given issue's agent starts
        — reusing the exact probe schema `wait` nodes accept (`npm`/`github-check`/`http`/`command`/
        `capability`/`pr`/`epic`). It lets an author splice e.g. "wait for `@nanobpm/agentic@0.13.0` to
        publish" between two sequence steps without hand-authoring raw node/edge JSON.
      type: object
      additionalProperties: false
      required:
        - kind
        - target
      properties:
        kind:
          type: string
          minLength: 1
          description: >-
            The wait-probe kind — one of the S3 vocabulary kinds (`npm`, `github-check`, `http`,
            `command`, `capability`, `pr`, `epic`). An unknown kind is a 400.
        target:
          type: string
          minLength: 1
          description: >-
            The probe target — kind-specific: `pkg@version` for `npm`, `owner/repo@ref` for
            `github-check`, a URL for `http`, an `owner/repo#N` PR for `pr`, etc.
        match:
          type: object
          description: >-
            OPTIONAL kind-specific readiness match fields (e.g. `{ version }` for `npm`,
            `{ conclusion, checkName }` for `github-check`).
        poll:
          type: object
          additionalProperties: false
          description: >-
            OPTIONAL poll budget. Defaults to the bounded merge-gate budget (re-probe every 5 minutes,
            budget 3 days) so a gate never falls into the 30-minute default trap.
          properties:
            everyMs:
              type: integer
              minimum: 1
              description: How often to re-probe (milliseconds).
            timeoutMs:
              type: integer
              minimum: 1
              description: The total budget before the gate escalates/continues (milliseconds).
        onTimeout:
          type: string
          enum:
            - escalate
            - continue
          description: >-
            What to do when the gate never goes green within its budget — `escalate` (default) parks on
            a human; `continue` proceeds anyway. `fail` is not supported on a `wait` node.
        credentialEnv:
          type: string
          minLength: 1
          description: OPTIONAL env-key name supplying a credential for the probe (`http`/`capability`).
    SequenceIssueEntry:
      description: >-
        One `issues[]` entry with an OPTIONAL leading gate (issue #740) — an object form of a sequence
        step. The `issue` is implemented + converged + merged like a bare-string entry; the optional
        `gate` must go green (in addition to the prior issue merging) before this issue's agent starts.
      type: object
      additionalProperties: false
      required:
        - issue
      properties:
        gate:
          $ref: "#/components/schemas/SequenceGate"
        issue:
          type: string
          minLength: 1
          maxLength: 255
          description: An `owner/repo#N` issue reference to implement + converge + merge, in sequence.
    SequenceIssuesIntent:
      description: >-
        The `sequenceIssues` INTENT (epic nano-workforce#605, S4) — a high-level shape that GENERATES
        the canonical "implement issue → converge → merge" delivery graph (operator-guide §9.4) instead
        of making an agent hand-author its node/edge JSON. It names an ordered list of `issues` to
        sequence (each issue's implementation starts once the PRIOR issue has merged) and an OPTIONAL
        leading `behind` gate (wait for that issue/epic/feature to be fully merged first, §9.5). The
        door GENERATES, then STAGES the graph through the same compile+stage flow as
        `compileDeliveryGraph` — it never dispatches (dispatch is an operator-only cockpit action, ADR
        0005 Decision 7). For each issue it emits `agent` (`senior:feature`, emits a `pr` fact) →
        `connector` (`converge-merge`, late-binding that `pr`) → `wait[pr, merged]` (a realistic
        `poll.timeoutMs`), threading the `pr` fact per §9.4. An `issues[]` entry may be a bare
        `owner/repo#N` string OR a `{ gate?, issue }` object that interleaves a `wait` gate (the
        `npm`/`github-check`/… vocabulary) before that issue's agent (issue #740). Invalid input (empty
        `issues`, an unparseable ref, an unknown gate kind) is a 400 carrying `issues: [{ path, message
        }]`; nothing is staged.
      type: object
      additionalProperties: false
      example:
        issues:
          - nanobpm/nano-ide#557
          - gate:
              kind: npm
              target: "@nanobpm/agentic@0.13.0"
            issue: jwulf/c8ctl-plugin-nano#186
          - nanobpm/nano-workforce#738
      required:
        - issues
      properties:
        behind:
          type: string
          minLength: 1
          maxLength: 255
          description: >-
            OPTIONAL gate — an `owner/repo#NN` issue/epic/feature reference. When present, a leading
            `wait[epic]` node gates the whole sequence on that reference reaching "fully merged" (every
            opened slice/PR landed, §9.5) before the first issue's implementation starts.
        issues:
          type: array
          minItems: 1
          maxItems: 64
          items:
            oneOf:
              - type: string
                minLength: 1
                maxLength: 255
                description: A bare `owner/repo#N` issue reference (no gate) — today's behaviour, byte-for-byte.
              - $ref: "#/components/schemas/SequenceIssueEntry"
            description: >-
              A sequence step — a bare `owner/repo#N` string, or a `{ gate?, issue }` object that
              interleaves a `wait` gate before that issue's agent (issue #740).
          description: >-
            The ordered issues to sequence — each is implemented by a `senior:feature` agent that opens
            a PR, driven to convergence + merge, and the NEXT issue's implementation starts only once
            the prior issue has merged (and any interleaved gate has gone green). At least one; at most
            64. The compiler's node ceiling is enforced SEPARATELY by a node-budget guard (interleaved
            gates each add nodes, so a fully-gated 64-issue sequence can exceed the ceiling and is
            rejected there) — `maxItems: 64` bounds the entry count, not the generated node count.
    SequenceIssuesRejected:
      description: >-
        A rejected `sequenceIssues` intent — the input failed validation (empty/oversized `issues`, an
        unparseable `owner/repo#N` reference, or an unknown target/probe per the delivery-graph
        vocabulary). Every failure is path-qualified so the caller can fix the exact offending input.
        Nothing was generated or staged.
      type: object
      additionalProperties: false
      required:
        - error
        - issues
      properties:
        error:
          type: string
          description: A human-readable summary of why the intent was rejected.
        issues:
          type: array
          items:
            $ref: "#/components/schemas/DeliveryCompileError"
          description: The path-qualified validation failures (at least one).
    FeatureStart:
      description: The start-feature request body — a SINGLE-issue feature run. Names the target issue
        by EXACTLY ONE of `issue` (an `owner/repo#123` reference) or `url` (a bare issue URL), plus a
        REQUIRED `baseBranch` (ADR 0003, same admission as the epic path), and the two optional
        follow-on knobs `converge` / `autoMerge`. May also carry an intake-time readiness gate
        (`readiness` and/or `blockedOn` + `consumerPackage`, per issue 295) that parks the run until
        the declared upstreams land. Modeled as `oneOf` named variants (Camunda REST v2 pattern) so an
        ambiguous or empty target is a 400 at the edge.
      oneOf:
        - $ref: "#/components/schemas/FeatureStartByIssue"
        - $ref: "#/components/schemas/FeatureStartByUrl"
    FeatureStartByIssue:
      type: object
      additionalProperties: false
      required:
        - issue
        - baseBranch
      properties:
        issue:
          type: string
          description: "Issue reference: owner/repo#123."
        baseBranch:
          type: string
          minLength: 1
          maxLength: 255
          pattern: '\S'
          description: >-
            REQUIRED target branch the agent branches off and opens its PR against. Admitted through
            the same ADR 0003 policy as the epic path (`admitPlan`): a missing `epic/*` base is
            auto-created off default HEAD; a non-`epic/*` base must already exist; naming the default
            branch requires `confirmDefaultBase`.
        converge:
          type: boolean
          description: >-
            When true, hand the opened PR to the convergence loop (review rounds) as a follow-on step.
            When false (default) the run ends at "PR raised, not reviewed".
        autoMerge:
          type: boolean
          description: >-
            When true (and `converge` is true), drive the merge-loop after convergence only when
            `NANO_PR_AUTO_MERGE` is enabled. When false the run stops at `converged`.
            Moot when `converge` is false.
        confirmDefaultBase:
          type: boolean
          description: >-
            Acknowledge that `baseBranch` is the repository default branch (the PR would target it
            directly). See `PlanStartByIssue.confirmDefaultBase`.
        allowSharedBase:
          type: boolean
          description: >-
            Opt in to sharing a custom integration base branch with another already-active epic. See
            `PlanStartByIssue.allowSharedBase`.
        customInstructions:
          type: string
          maxLength: 8000
          description: >-
            OPTIONAL free-text steering appended to the implementation agent's prompt for this run
            (via the implement task's `appendPrompt`). Blank/whitespace is treated as absent. Persists
            on the instance, so it also applies to the agent's answer-loop redispatch.
        readiness:
          type: array
          maxItems: 32
          items:
            $ref: "#/components/schemas/ReadinessProbe"
          description: >-
            OPTIONAL intake-time readiness gate (issue #295): one or more durable probes the run must
            ALL satisfy before its implementation agent is dispatched. The run parks (durably, bounded
            by the gate's escalating timer) at the leading readiness preflight until every probe goes
            green. Absent/empty ⇒ the run implements immediately, unchanged.
        blockedOn:
          type: array
          maxItems: 32
          items:
            type: string
            minLength: 1
          description: >-
            OPTIONAL ergonomic shorthand for `readiness` (issue #295): a list of upstream
            `owner/repo#123` issue/PR handles the run waits to land. With `consumerPackage` each
            desugars to a `capability` probe (resolve which published `pkg@version` first carries the
            handle, and late-bind it into the run); without it, to a `command` probe that goes green
            once the referenced issue/PR is closed/merged.
        consumerPackage:
          type: string
          minLength: 1
          description: >-
            OPTIONAL npm package name (e.g. `@nanobpm/engine-wasm`) the `blockedOn` shorthand resolves
            its handles against — the consumer dependency whose published provenance must carry each
            awaited upstream. When present, `blockedOn` desugars to `capability` probes and the
            resolved `pkg@version` is late-bound into the implementation agent's brief.
    FeatureStartByUrl:
      type: object
      additionalProperties: false
      required:
        - url
        - baseBranch
      properties:
        url:
          type: string
          description: A bare issue URL, when no `owner/repo#123` reference is supplied.
        baseBranch:
          type: string
          minLength: 1
          maxLength: 255
          pattern: '\S'
          description: >-
            REQUIRED target branch the agent branches off and opens its PR against. See
            `FeatureStartByIssue.baseBranch`.
        converge:
          type: boolean
          description: When true, hand the opened PR to the convergence loop. See `FeatureStartByIssue.converge`.
        autoMerge:
          type: boolean
          description: When true (with `converge`), drive the merge-loop. See `FeatureStartByIssue.autoMerge`.
        confirmDefaultBase:
          type: boolean
          description: Acknowledge landing on the default branch. See `PlanStartByIssue.confirmDefaultBase`.
        allowSharedBase:
          type: boolean
          description: Share a custom integration base with another active epic. See `PlanStartByIssue.allowSharedBase`.
        customInstructions:
          type: string
          maxLength: 8000
          description: >-
            OPTIONAL free-text steering appended to the implementation agent's prompt for this run.
            See `FeatureStartByIssue.customInstructions`.
        readiness:
          type: array
          maxItems: 32
          items:
            $ref: "#/components/schemas/ReadinessProbe"
          description: OPTIONAL intake-time readiness gate. See `FeatureStartByIssue.readiness`.
        blockedOn:
          type: array
          maxItems: 32
          items:
            type: string
            minLength: 1
          description: OPTIONAL readiness shorthand — upstream `owner/repo#123` handles to wait on. See `FeatureStartByIssue.blockedOn`.
        consumerPackage:
          type: string
          minLength: 1
          description: OPTIONAL package the `blockedOn` handles resolve against. See `FeatureStartByIssue.consumerPackage`.
    MessageResult:
      type: object
      description: The result of publishing a message / answering an escalation. Shape varies by message
        name; `ok` is always present.
      additionalProperties: true
      required:
        - ok
      properties:
        ok:
          type: boolean
    AgentCompleteRequest:
      type: object
      additionalProperties: false
      description: >-
        Complete an escalation user task AS AN AGENT (epic #156 / ADR 0046). The agent submits the
        SAME typed form variables a human would through the task inbox; the host routes them through
        the one canonical attributed completer, records the agent's identity for the audit trail, and
        resumes the process. Only the migrated escalation user tasks may be completed this way.
      required:
        - userTaskKey
        - agentId
        - variables
      properties:
        userTaskKey:
          type: string
          minLength: 1
          description: The engine user-task key of the parked escalation task (from GET /tasks/api/tasks).
        agentId:
          type: string
          minLength: 1
          description: The completing agent's identity (ADR 0046), recorded for attribution.
        variables:
          type: object
          additionalProperties: true
          description: >-
            The typed form completion variables — the SAME shape a human submits (e.g.
            `{ resolution, answer }`, `{ directive, notes }`, `{ action, notes }`, or `{ answer }`).
    RevertCompletionRequest:
      type: object
      additionalProperties: false
      description: >-
        Revert/override an agent escalation completion (the reversibility guarantee of ADR 0046). A
        human marks a reversible agent completion reverted so the agent's answer is no longer treated
        as authoritative; the reverter's identity + timestamp are recorded.
      required:
        - completionId
        - reverterId
      properties:
        completionId:
          type: integer
          description: The `task_completions` id returned when the agent completion was recorded.
        reverterId:
          type: string
          minLength: 1
          description: The human identity overriding the agent completion, recorded for the audit trail.
        note:
          type: string
          description: Optional corrective guidance from the human that overrides the agent's answer.
    BlackboardEntry:
      type: object
      additionalProperties: false
      required:
        - id
        - author_task
        - kind
        - files
        - body
        - wave
        - created_at
      properties:
        id:
          type: integer
        author_task:
          type: string
        kind:
          type: string
        files:
          type: array
          items:
            type: string
        body:
          type: string
        wave:
          type: integer
          nullable: true
        created_at:
          type: string
    BlackboardPage:
      type: object
      required:
        - planKey
        - entries
        - cursor
      properties:
        planKey:
          type: string
        entries:
          type: array
          items:
            $ref: "#/components/schemas/BlackboardEntry"
        cursor:
          type: integer
          nullable: true
          description: The plan's current head id; pass it back as `since` on the next poll (Tier 2).
    BlackboardAppendRequest:
      type: object
      description: Append one blackboard entry. Idempotent on (plan, dedupe_key).
      required:
        - body
      properties:
        author_task:
          type: string
          description: The authoring task id; defaults to "system" when omitted/blank.
        kind:
          type: string
          description: Entry kind (e.g. note, file-claim); normalized by the delegate.
        files:
          type: array
          items:
            type: string
          description: Files this entry concerns (used by advisory file-claim conflict detection).
        body:
          type: string
          minLength: 1
          description: The note text (required, non-blank).
        wave:
          type: integer
          description: Optional plan wave the entry belongs to.
        dedupe_key:
          type: string
          description: "Idempotency key: a retry with the same (plan, dedupe_key) returns the existing entry."
    BlackboardAppendResult:
      type: object
      required:
        - id
        - inserted
        - conflicts
      properties:
        id:
          type: integer
        inserted:
          type: boolean
          description: False when a prior entry with the same dedupe_key already existed (idempotent retry).
        conflicts:
          type: array
          description: Prior sibling file-claims on the same file(s) (advisory first-writer-wins; never a lock).
          items:
            type: object
            additionalProperties: false
            required:
              - file
              - author_task
              - id
              - body
              - created_at
            properties:
              file:
                type: string
              author_task:
                type: string
              id:
                type: integer
              body:
                type: string
              created_at:
                type: string
        contractConflicts:
          type: array
          description: >-
            Near-duplicate contract-DECLARATION conflicts on a `contract` POST (issue #227): a
            synonym, contradiction, or rejected synonym vs. the durable contract registry. Advisory —
            surfaced so the writer reconciles a divergent contract at authoring time; never a lock.
          items:
            type: object
            additionalProperties: false
            required:
              - kind
              - proposedName
              - existingName
              - detail
            properties:
              kind:
                type: string
                enum: [synonym, contradiction, rejected-synonym]
              proposedName:
                type: string
              existingName:
                type: string
              detail:
                type: string
    AbandonStatus:
      type: object
      required:
        - prKey
        - status
        - abandoned
      properties:
        prKey:
          type: string
        status:
          type: string
        abandoned:
          type: boolean
          description: Derived from pull_requests.status; true ⇒ the run was cancelled and the agent must stop.
    CancelInstanceIntent:
      type: object
      additionalProperties: false
      required:
        - processInstanceKey
      properties:
        processInstanceKey:
          type: string
          description: The engine process-instance key of the run to cancel (the `process_key` column surfaced by `listActivePrs`/the Convergence page). Must be a string — engine keys are 64-bit and a numeric value risks silent JS precision loss.
    CancelInstanceResult:
      type: object
      required:
        - ok
        - processInstanceKey
        - state
        - reconciled
      properties:
        ok:
          type: boolean
          description: True when the engine confirmed the instance is terminated AND its tracked record reconciled to a terminal edge (the record is now — or derives as — `abandoned`). False ⇒ either the engine did NOT stop the instance, or it stopped it but the tracked record did not settle to a terminal edge (both surfaced as a 502), or no tracked record owns the key (404). NOTE — record reconciliation is only verified when the deployment has a readable default source; in a no-default-source deployment (e.g. `mount.data:false`) there is no derived view to reconcile against, so `ok:true` reflects ENGINE TERMINATION ONLY (the primitive-only path) and does not by itself guarantee a tracked record reconciled.
        processInstanceKey:
          type: string
          description: The cancelled instance key, echoed back.
        state:
          type: string
          enum: [ACTIVE, COMPLETED, TERMINATED, gone]
          description: The instance state after the cancel attempt. For a tracked key, this is read back from the engine — `ACTIVE`/`COMPLETED`/`TERMINATED` reflect that post-cancel engine read. NOTE — this readback can LAG — a committed cancel is acknowledged asynchronously, so the engine may still report `ACTIVE` for a short window even though the cancel has been accepted. Therefore `ok:true` can coincide with `state:ACTIVE` (see the "accepted cancel whose read model lags at ACTIVE is still trusted → 200 ok" case); do NOT treat `ok:true` + `state:ACTIVE` as contradictory — `ok`/`reconciled` are the authoritative signal that the cancel took, not `state`. `gone` ⇒ EITHER the engine has no record of the key (already cleaned up / never existed) OR no tracked record owns the key (the 404 no-op, in which case the engine is never contacted, so `gone` is not an engine read).
        reconciled:
          type: integer
          description: 1 when the tracked record's derived status has left its active set (become terminal → the tracked PR/plan/feature-run row derives as `abandoned`; for a PR that additionally means it drops out of `listActivePrs`, which lists PRs only — plans and feature runs are not surfaced there), else 0. NOTE — with no readable default source there is no derived record to re-read, so this carries the primitive's own reconciliation count rather than a record-derived terminal edge.
        error:
          type: string
          description: Present on a non-terminal failure — the reason the cancel did not take, the tracked record was not reconciled, or no tracked record owns the key.
# ─────────────────────────────────────────────────────────────────────────────────────────────
# MCP tool-schema convention (epic nano-workforce#605, S0 — the shared invariant every later slice
# inherits). The Urban runtime projects THIS document into MCP tools (ADR 0067 — zero MCP server
# code in nwf). The projector copies each operation's request-body schema VERBATIM into the tool's
# `inputSchema.properties.body`; it does NOT resolve `$ref`s. So EVERY projected (non-`x-mcp`)
# request-body operation MUST present a self-contained tool schema:
#   • `type: object` with inline `properties` (never a bare `$ref` — it is unresolvable in an MCP
#     client, and the object gets stringified and rejected at the door: nano-ide#501/#502/#503);
#   • NO `$ref` anywhere in the projected body schema;
#   • the graph doors (`compileDeliveryGraph`/`previewDeliveryGraph`) carry a worked `example` — the
#     only example the invariant enforces (`test/mcp-tool-schemas.test.ts`); other doors MAY add one;
#   • a `description` that carries the contract (input shape, side effects, idempotency, next call).
# You do NOT hand-write the inline body. Author the shape ONCE as a `components.schemas` entry and
# reference it here as usual (`schema: { $ref: … }` OR keep the generated block); the generator
# scripts/inline-mcp-bodies.ts DERIVES the `$ref`-free inline body from that component (single source
# of truth) into a `# BEGIN/END generated:mcp-body` region. Run `npm run gen:mcp-bodies` after
# changing a source component; `npm run check:mcp-bodies` (CI) + test/mcp-tool-schemas.test.ts (the
# real projector) fail the build on any drift or re-leaked `$ref`. Operator-only doors opt OUT with
# `x-mcp: { exclude: true }` (stage/dispatch/dismiss) and are not projected.
# ─────────────────────────────────────────────────────────────────────────────────────────────
paths:
  /status:
    get:
      operationId: listActivePrs
      summary: List every tracked PR currently in flight (not converged/abandoned), newest-updated first.
      security:
        - hookSecret: []
        - {}
      responses:
        "200":
          description: The active PRs.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActivePrList"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /actions/cancel:
    post:
      operationId: cancelInstance
      summary: "Cancel a wedged run the app-owned, record-consistent way: terminate the engine instance AND flip its PR/plan record to `abandoned` (issue #667). The correct unstick — go through the app so its record state stays consistent, rather than engine-level `urban_debug_cancel_instance`, which cancels out from under the app and leaves the PR row inconsistent."
      security:
        - hookSecret: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              # BEGIN generated:mcp-body source=#/components/schemas/CancelInstanceIntent (scripts/inline-mcp-bodies.ts — do not hand-edit)
              type: object
              additionalProperties: false
              required:
                - processInstanceKey
              properties:
                processInstanceKey:
                  type: string
                  description: The engine process-instance key of the run to cancel (the `process_key` column surfaced by `listActivePrs`/the Convergence page). Must be a string — engine keys are 64-bit and a numeric value risks silent JS precision loss.
              # END generated:mcp-body
      responses:
        "200":
          description: The instance was terminated; its record derives/flips to `abandoned` and drops out of `listActivePrs`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CancelInstanceResult"
        "400":
          description: Missing/invalid `processInstanceKey`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "404":
          description: "No tracked record owns the key (a clean no-op — the engine is never touched). Body is a CancelInstanceResult with `ok:false`, `state:\"gone\"`, `reconciled:0`."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CancelInstanceResult"
        "502":
          description: "The cancel did not fully take: EITHER the engine did NOT stop the instance (the cancel was not committed; the run may still be live), OR the engine stopped it but the tracked record did not settle to a terminal edge (`ok:false`, `reconciled:0`) — a wedged record the caller must not read as done."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CancelInstanceResult"
        "503":
          description: The app has no data source configured, so the cancel cannot be reconciled into the record.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /escalations:
    get:
      operationId: listEscalations
      summary: List every OPEN escalation awaiting a human/agent decision, with the completable userTaskKey.
      description: >-
        Discovery for the escalation-answer path (epic #664, issue #666): across every surfaced
        escalation kind (PR review/merge loop, plan-review, empty-plan, trial-merge,
        conformance-review, delivery human-step, feature/blocked, agent-permission and the shared
        human-escalation cell), list each currently-open native user-task escalation with the
        completable `userTaskKey` an agent then answers via `completeUserTask` /
        `agentCompleteEscalation` — so a tool-aware agent never has to curl the un-projected
        `/tasks/api/tasks` inbox to find keys. Read-only projection over the ONE `user_tasks` read
        model the Tasks inbox and Convergence page consume (no second source of truth); a row is
        present iff its task is open, so the list reflects live pending work.
      security:
        - hookSecret: []
        - {}
      responses:
        "200":
          description: The open escalations.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EscalationList"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /lineage:
    get:
      operationId: getLineage
      summary: "Intent to progress lineage projection (issue #245): every request stitched into one arc (request, implementation, PRs, convergence, merge, outcome), active frontier first. Pass root to fetch a single origin's thread."
      security:
        - hookSecret: []
        - {}
      parameters:
        - name: root
          in: query
          required: false
          schema:
            type: string
          description: "An origin request key (feature_key/plan_key) or a self-rooted pr_key. When set, the response contains just that thread (empty if unknown)."
      responses:
        "200":
          description: The stitched lineage threads.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LineageList"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /prs/history:
    get:
      operationId: getPrHistory
      summary: "A PR's escalation + round history (issue #668): per-round status transitions and outcome, plus each escalation's kind, question/answer, and timestamps — the read that retires the DB-over-ssh fallback. Identify the PR by prKey, or by processKey. Reads the same rounds/escalations tables the Convergence page surfaces."
      security:
        - hookSecret: []
        - {}
      parameters:
        - name: prKey
          in: query
          required: false
          schema:
            type: string
          description: The PR key ("<owner>/<repo>#<number>"). Provide this or processKey.
        - name: processKey
          in: query
          required: false
          schema:
            type: string
          description: The engine process-instance key driving the PR; resolved to its prKey. Provide this or prKey.
      responses:
        "200":
          description: The PR's round + escalation history (empty arrays when there is none).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrHistory"
        "400":
          description: Neither prKey nor processKey supplied.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /agentic/supply:
    get:
      operationId: getAgenticSupply
      summary: The SUPPLY-ONLY agentic visibility report — the live worker list (family, host, current jobs,
        liveness) grouped by leaf, sourced from the H1 presence registry. Feeds the H5 cockpit page.
      security:
        - hookSecret: []
        - {}
      responses:
        "200":
          description: The supply report.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgenticSupplyReport"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /agentic/vocab:
    get:
      operationId: getAgenticVocab
      summary: The crew vocabulary artifact (enrolment epic 152 slice N1) — the ONE capability→token map a
        worker resolves its SERVE set against. Returns the networks/roles tree plus the flattened
        per-token enrolment requirements and the artifact version. Read-only; advisory.
      security:
        - hookSecret: []
        - {}
      responses:
        "200":
          description: The published vocab view.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VocabView"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /agentic/enrol:
    post:
      operationId: enrolAgenticWorker
      summary: Resolve a worker's declared enrolment capability to its SERVE token set (ADR 0059 revised
        REGISTER→SERVE, per-worker). Returns the SERVE leaf tokens, the vocab version resolved against,
        and the liveness lease TTL. Pure/deterministic — idempotent per (app, worker).
      security:
        - hookSecret: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              # BEGIN generated:mcp-body source=#/components/schemas/EnrolRequest (scripts/inline-mcp-bodies.ts — do not hand-edit)
              type: object
              description: A worker's enrol request — its declared capability (ADR 0059 revised, per-worker).
              required:
                - capability
              properties:
                capability:
                  type: object
                  description: A worker's declared enrolment capability. NEVER a routing token — it gates enrolment.
                  properties:
                    cognition:
                      type: string
                      description: The worker's cognition class (e.g. planning / implementation / qa / ci / decide).
                    weight:
                      type: number
                      description: The cognition weight (the one numeric capability field).
                    family:
                      type: string
                      description: The model family (the diversity-SLO seat filler, e.g. frontier / kimi / qwen).
                    host:
                      type: string
                      description: Where the worker runs.
                host:
                  type: string
                  description: Where the worker runs. Folded into `capability.host` when the latter is absent.
                instance:
                  type: string
                  description: The worker instance id, echoed back for provenance (optional).
                durableResume:
                  type: boolean
                  description: "Whether this worker's harness advertises durable-resume (issue #325, ADR 0062 Slice 5/5) — an ENROLMENT attribute, never a routing token. Recorded per instance so the app emits the world-restore marker only to a fleet with a participant; a harness that omits it (or sets false) redrives a re-leased round from scratch. Recorded only when `instance` is a non-blank string — a missing, empty, or whitespace-only `instance` is echoed back for provenance but the flag is not persisted."
                harnessProtocol:
                  type: integer
                  minimum: 0
                  description: 'The worker-harness protocol version (issue #802) — a non-negative integer declaring which machine-readable artifacts the harness emits (AgentInstance, transcript flush, result envelope). An ENROLMENT attribute, never a routing token. Recorded per instance so the app can flag a stale harness in getAgenticSupply / the registry and — under NANO_AGENTIC_STALE_HARNESS_POLICY=refuse — refuse it agent-job routing. A missing version is treated as stale.'
              # END generated:mcp-body
      responses:
        "200":
          description: The resolved SERVE set for the declared capability.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnrolResult"
        "400":
          description: A malformed enrol body (missing/invalid capability).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /agentic/registry:
    get:
      operationId: getAgenticRegistry
      summary: The demand×supply report (enrolment epic 152 slice N1) — deployed demand (the models'
        taskDefinition leaves) diffed against live supply (the presence registry resolved through the
        crew vocab), per network, with missing-agent-type reds and the diversity SLO. Read-only; advisory.
      security:
        - hookSecret: []
        - {}
      responses:
        "200":
          description: The demand×supply report.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RegistryReport"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /agentic/transcripts:
    get:
      operationId: listAgenticTranscripts
      summary: List captured agent sessions (H3/#146) — the durable transcripts an ephemeral agent flushed
        on job completion, readable AFTER it exited. Optional filters by jobKey / process instance / plan /
        time. Also serves the proxy-safe single-stream READ form `?stream=` (#744). Advisory read-only;
        never gates control flow. Feeds the cockpit "past sessions" view.
      security:
        - hookSecret: []
        - {}
      parameters:
        - name: stream
          in: query
          required: false
          schema:
            type: string
          description: >-
            Single-stream READ form (#744, proxy-safe): when present, the response is that ONE
            stream's transcript bytes (`AgenticTranscriptData` — the payload
            `GET /agentic/transcripts/{stream}` serves), NOT the list, and the filters below are
            ignored. The id rides a query value, so slash-bearing worker-instance ids
            (`34:<instance>/<jobKey>`) survive gateways that decode %2F in path segments before
            routing. Prefer this form over the path form behind any proxy.
        - name: from
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
          description: Only with `stream` — resume from this offset (inclusive). Default 0. Rejected with 400
            when supplied without `stream`.
        - name: jobKey
          in: query
          required: false
          schema:
            type: string
          description: Return only the transcript for this Camunda-8 job key (its `job:<jobKey>` stream).
        - name: processInstanceKey
          in: query
          required: false
          schema:
            type: string
          description: Return only transcripts whose (still-known) correlation names this process instance.
        - name: elementInstanceKey
          in: query
          required: false
          schema:
            type: string
          description: Return only transcripts whose correlation names this engine element-instance key (#544) —
            resolves a session to one occupancy of a looping / retried activity, unlike the static elementId.
        - name: planKey
          in: query
          required: false
          schema:
            type: string
          description: Return only transcripts whose (still-known) correlation names this plan / epic key.
        - name: instance
          in: query
          required: false
          schema:
            type: string
          description: Return only sessions run by this worker instance (durable attribution) — powers the
            per-worker history view. Survives worker exit / process restart.
        - name: since
          in: query
          required: false
          schema:
            type: string
          description: Return only sessions created at or after this ISO-8601 instant.
        - name: until
          in: query
          required: false
          schema:
            type: string
          description: Return only sessions created at or before this ISO-8601 instant.
      responses:
        "200":
          description: The captured session list — or, in the single-stream form (`?stream=`), that
            stream's stored transcript bytes (#744).
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/AgenticTranscriptList"
                  - $ref: "#/components/schemas/AgenticTranscriptData"
        "400":
          description: A malformed filter (e.g. an unparseable since/until), or — in the single-stream
            form — a malformed `from` offset.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "404":
          description: Single-stream form only — no transcript exists for the given `stream`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /agentic/transcripts/{stream}:
    get:
      operationId: getAgenticTranscript
      summary: Fetch a stored transcript's bytes (H3/#146), range/offset-based so the cockpit terminal
        replays it through the same resume-from-offset renderer it uses for a live stream. PATH form
        of the single-stream read (back-compat; a slash-bearing id 404s behind a decoding proxy —
        prefer the `?stream=` query form there, #744). Advisory read-only; never gates control flow.
      security:
        - hookSecret: []
        - {}
      parameters:
        - name: stream
          in: path
          required: true
          schema:
            type: string
          description: The relay stream id to fetch (`job:<jobKey>` for a job stream). CAVEAT (#744) —
            a gateway that decodes %2F in a path segment splits a slash-bearing id and this route
            404s; such ids must use the `?stream=` query form. Slash-free ids are safe in either.
        - name: from
          in: query
          required: false
          schema:
            type: integer
          description: Resume from this offset (inclusive). Default 0 (the whole retained transcript).
      responses:
        "200":
          description: The stored transcript bytes from `from`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgenticTranscriptData"
        "400":
          description: A malformed `from` offset.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "404":
          description: No transcript exists for the given stream.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /agentic/agent-instances:
    get:
      operationId: listAgentInstances
      summary: >-
        List engine-native AgentInstances (issue #745/#747, umbrella #746) — the durable agent-task
        runs the worker harness minted against the `<zeebe:agentDefinition agentType="external"/>` marker,
        read back from the engine read model (`searchAgentInstances`), newest-created first. Keyed/filtered
        by process / element / status — NOT the slash-bearing relay stream id. Advisory read-only; never
        gates control flow. Feeds the cockpit "historical sessions" view (settled history = engine; live
        tail = relay overlay). Read-as-absence — an engine with no AgentInstance channel returns an empty list.
      security:
        - hookSecret: []
        - {}
      parameters:
        - name: processInstanceKey
          in: query
          required: false
          schema:
            type: string
          description: Return only agent instances owned by this process instance.
        - name: rootProcessInstanceKey
          in: query
          required: false
          schema:
            type: string
          description: Return only agent instances in this root process-instance hierarchy.
        - name: elementId
          in: query
          required: false
          schema:
            type: string
          description: Return only agent instances owned by this BPMN element (the AI-agent task).
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: Return only agent instances in this lifecycle status (the engine's `AgentInstanceStatusEnum`).
      responses:
        "200":
          description: The engine-native agent instances matching the filters.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentInstanceList"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "503":
          description: No engine read path available (no engine client configured).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /agentic/agent-instances/{agentInstanceKey}/history:
    get:
      operationId: getAgentInstanceHistory
      summary: >-
        Fetch one AgentInstance's durable conversation history (turns + per-turn metrics) from
        engine `searchAgentInstanceHistory` (issue #745/#747), in conversational order, with the owning
        instance's rolled-up metrics when still reported. The cockpit renders the HISTORICAL transcript +
        metrics from this — keyed by `agentInstanceKey`, never the slash-bearing relay stream id (#744
        moot). Advisory read-only; never gates control flow. Read-as-absence — an unknown key / an engine
        with no AgentHistory channel returns an empty history.
      security:
        - hookSecret: []
        - {}
      parameters:
        - name: agentInstanceKey
          in: path
          required: true
          schema:
            type: string
          description: The engine agent-instance key whose history to read (from `listAgentInstances`).
        - name: role
          in: query
          required: false
          schema:
            type: string
            enum: [USER, ASSISTANT, TOOL_RESULT, CONFIGURATION, UNSPECIFIED]
          description: Only turns with this conversation role. Any value from the enum is
            forwarded verbatim to the engine as the filter.
        - name: loopIteration
          in: query
          required: false
          schema:
            type: integer
          description: Only turns produced in this agent-loop iteration.
        - name: elementInstanceKey
          in: query
          required: false
          schema:
            type: string
          description: Only turns produced under this element instance.
      responses:
        "200":
          description: The agent instance's history (turns + metrics), possibly empty.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentHistory"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "503":
          description: No engine read path available (no engine client configured).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /reconcile:
    post:
      operationId: reconcileEngineState
      summary: Reconcile engine-backed inflight projections after an engine reset/rewind (issue #622).
      description: >-
        The explicit operator command for the app-side reconciliation surface. Probes the engine's
        incarnation epoch (`/v2/topology`) and compares it to the last-seen value: on a REGRESSION (the
        #1065 reset/rewind signature) every NON-terminal engine-backed app row (an instanceTracking
        binding whose status is still active and whose engine key is populated) is driven to the
        defined `orphaned` terminal WITH PROVENANCE. Terminal history and non-engine-backed surfaces
        (presence, audit) are never touched. Idempotent — a second call with a matching epoch is a
        no-op — and safe: an unreachable engine orphans nothing. Runs automatically on startup too.
      security:
        - hookSecret: []
        - {}
      responses:
        "200":
          description: The reconcile pass result (what it observed and orphaned).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReconcileReport"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "503":
          description: The app has no data source configured, so there is nothing to reconcile.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /version:
    get:
      operationId: getVersion
      summary: The running app's identity (app/urban versions, git sha/branch, runtime, pid, uptime).
      security:
        - hookSecret: []
        - {}
      responses:
        "200":
          description: The version/identity payload.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VersionInfo"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /agent:
    get:
      operationId: getAgentInstructions
      summary: The agent operator guide (markdown) — how to submit PRs/epics, answer escalations, and
        debug this instance. Point a coding agent at this URL to drive and debug the workforce.
      security:
        - hookSecret: []
        - {}
      responses:
        "200":
          description: The operator guide, with examples keyed to this instance.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentInstructions"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /agent/skill:
    get:
      operationId: getAgentSkill
      summary: The portable operator skill (markdown SKILL.md) an agent runtime loads on demand — a
        thin bootstrap that resolves which instance to drive, then fetches the live operator guide
        (GET /agent). Surfaced from the Overview tab's "Agent Instructions" prompt.
      security:
        - hookSecret: []
        - {}
      responses:
        "200":
          description: The operator skill, with any example keyed to this instance.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentSkill"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /agent/guide:
    get:
      operationId: getAgentGuide
      summary: The operator guide, ADDRESSABLE — fetch one section instead of the whole ~43KB blob.
        Read-only, pure, idempotent. Call with NO `section` to get a compact table of contents (every
        stable section id + a one-line summary); call with `section` set to a TOC id (e.g.
        `delivery-graphs`) to get ONLY that section's markdown, small enough to fit a typical
        tool-result limit. This is the MCP-friendly companion to `getAgentInstructions`, which still
        returns the full guide unchanged for non-MCP callers. Typical flow — first call
        `getAgentGuide` (no arg) to see the ids, then `getAgentGuide(section=<id>)` for the one you
        need. An unknown id is rejected with `issues[{path,message}]` that lists the valid ids.
      security:
        - hookSecret: []
        - {}
      parameters:
        # Self-contained tool input (epic #605 S0 convention): a single inline `type: string` query
        # param — no `$ref`, an explicit type and example — so the projected MCP tool schema is
        # client-usable as-is (no request body; the inline-mcp-bodies generator does not apply here).
        - name: section
          in: query
          required: false
          schema:
            type: string
            example: delivery-graphs
          description: OPTIONAL stable section id (from the table of contents `getAgentGuide` returns
            with no argument), e.g. `orient`, `submit-pr`, `submit-epic`, `escalations`, `lifecycle`,
            `debug`, `debug-models`, `unstick`, `raise-issue`, `delivery-graphs`, `tool-crosswalk`. Omit it to get the
            table of contents. An unknown id yields a 400 listing the valid ids.
        - name: start
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            example: 0
          description: >-
            OPTIONAL pagination cursor (issue #740) — the CHARACTER offset into a `section`'s markdown to
            start at. Passing `start` and/or `length` engages bounded-chunk retrieval so a large section
            (e.g. `delivery-graphs`) can be paged through without overflowing a tool-result limit; the
            response echoes `start`, `length`, `totalLength`, and a `nextStart` cursor (null on the last
            page). Ignored when no `section` is given. Omit both `start` and `length` to get the whole
            section unchanged.
        - name: length
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            example: 12000
          description: >-
            OPTIONAL pagination window (issue #740) — the maximum number of CHARACTERS to return for a
            `section`. Defaults to a bounded page size when `start` is given without `length`. Pair with
            `start`/`nextStart` to page through an oversized section.
      responses:
        "200":
          description: Either the table of contents (no `section`) or a single section's markdown.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentGuideResponse"
        "400":
          description: The `section` id is not a known section; `issues` lists the valid ids.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ValidationError"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /actions/start/convergence-loop:
    post:
      operationId: startConvergenceLoop
      summary: Register/refresh a PR aggregate (idempotent on prKey) and start its convergence loop.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              # BEGIN generated:mcp-body source=#/components/schemas/ConvergenceStart (scripts/inline-mcp-bodies.ts — do not hand-edit)
              description: The start-convergence request body. Names the target PR by EXACTLY ONE of `pr` (an `owner/repo#123` reference) or `url` (a bare PR URL) — never both, never neither — with the optional convergence knobs. Modeled as `oneOf` named variants (ADR — Camunda REST v2 pattern) so the runtime rejects an ambiguous or empty target at the edge with a 400 that names the allowed shapes, rather than the delegate silently coalescing `pr ?? url`.
              oneOf:
                - type: object
                  additionalProperties: false
                  required:
                    - pr
                  properties:
                    pr:
                      type: string
                      description: 'PR reference: owner/repo#123.'
                    dependsOn:
                      type: array
                      items:
                        type: string
                    maxRounds:
                      type: integer
                      minimum: 1
                      description: Values above 100 are accepted and clamped to 100 by the delegate.
                    autoMerge:
                      type: boolean
                      description: Preferred per-request setting. When false, stop at `converged`; when true, run the merge-loop after convergence only if `NANO_PR_AUTO_MERGE` is enabled. When omitted, the global default applies.
                    convergeOnly:
                      type: boolean
                      deprecated: true
                      description: 'Deprecated negative alias. If `autoMerge` is omitted, `true` means `autoMerge: false`; `false` means `autoMerge: true` (still gated by `NANO_PR_AUTO_MERGE`). Prefer `autoMerge`.'
                - type: object
                  additionalProperties: false
                  required:
                    - url
                  properties:
                    url:
                      type: string
                      description: A bare PR URL, when no `owner/repo#123` reference is supplied.
                    dependsOn:
                      type: array
                      items:
                        type: string
                    maxRounds:
                      type: integer
                      minimum: 1
                      description: Values above 100 are accepted and clamped to 100 by the delegate.
                    autoMerge:
                      type: boolean
                      description: Preferred per-request setting. When false, stop at `converged`; when true, run the merge-loop after convergence only if `NANO_PR_AUTO_MERGE` is enabled. When omitted, the global default applies.
                    convergeOnly:
                      type: boolean
                      deprecated: true
                      description: 'Deprecated negative alias. If `autoMerge` is omitted, `true` means `autoMerge: false`; `false` means `autoMerge: true` (still gated by `NANO_PR_AUTO_MERGE`). Prefer `autoMerge`.'
              type: object
              # END generated:mcp-body
      responses:
        "202":
          description: The loop was started (or refreshed).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubmitResult"
        "400":
          description: The PR reference could not be parsed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /actions/start/plan-fanout:
    post:
      operationId: startPlanFanout
      summary: Register/refresh a plan aggregate (idempotent on planKey) and start the planning fan-out.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              # BEGIN generated:mcp-body source=#/components/schemas/PlanStart (scripts/inline-mcp-bodies.ts — do not hand-edit)
              description: The start-plan-fanout request body. Names the target issue by EXACTLY ONE of `issue` (an `owner/repo#123` reference) or `url` (a bare issue URL), plus a REQUIRED `baseBranch` (ADR 0003) the fleet branches off — a blank/absent base is a 400. Modeled as `oneOf` named variants (Camunda REST v2 pattern) so an ambiguous or empty target is a 400 at the edge, not a silent `issue ?? url` coalesce in the delegate.
              oneOf:
                - type: object
                  additionalProperties: false
                  required:
                    - issue
                    - baseBranch
                  properties:
                    issue:
                      type: string
                      description: 'Issue reference: owner/repo#123.'
                    baseBranch:
                      type: string
                      minLength: 1
                      maxLength: 255
                      pattern: \S
                      description: 'REQUIRED target branch the fleet branches off and opens every PR against. Every epic launch must name its base explicitly (ADR 0003): a blank/absent value is rejected with a 400, not silently coalesced to the repository default branch. Use it to land an entire epic on a long-lived integration branch (e.g. `epic/agent-protocol`) so nothing reaches the default branch — and any merge-to-default side effect, such as auto-publishing a package — until you deliberately merge the integration branch. NOTE: this slice (B0) only enforces that `baseBranch` is present and a plausible branch name; branch-existence admission (auto-creating a missing `epic/*` base off the default branch HEAD, and rejecting a missing non-`epic/*` base with a 400) is specified by ADR 0003 but NOT yet enforced here — it lands in a later admission slice.'
                    allowSharedBase:
                      type: boolean
                      description: 'Reserved for a later ADR 0003 admission slice — NOT yet enforced in this slice (B0). When implemented it will opt in to sharing a custom integration base branch with another already-active plan: admission will otherwise reject (409) when another active plan already targets the same repo + same custom base branch, to stop two epics interleaving commits on one integration branch (the repository default branch is exempt from that guard). Accepted by the schema today but currently has no runtime effect.'
                    confirmDefaultBase:
                      type: boolean
                      description: 'Reserved for a later ADR 0003 admission slice — NOT yet enforced in this slice (B0). When implemented it will be the required acknowledgement when `baseBranch` names the repository default branch: targeting the default lands every task directly on it with no integration buffer — and fires any merge-to-default side effect per task — so admission will reject (400) unless confirmed with true (no effect for a non-default base). Accepted by the schema today but currently has no runtime effect.'
                - type: object
                  additionalProperties: false
                  required:
                    - url
                    - baseBranch
                  properties:
                    url:
                      type: string
                      description: A bare issue URL, when no `owner/repo#123` reference is supplied.
                    baseBranch:
                      type: string
                      minLength: 1
                      maxLength: 255
                      pattern: \S
                      description: REQUIRED target branch the fleet branches off and opens every PR against. See `PlanStartByIssue.baseBranch`.
                    allowSharedBase:
                      type: boolean
                      description: Reserved for a later ADR 0003 admission slice — NOT yet enforced in this slice (B0). Accepted by the schema today but currently has no runtime effect. When implemented it will opt in to sharing a custom integration base branch with another already-active plan. See `PlanStartByIssue.allowSharedBase`.
                    confirmDefaultBase:
                      type: boolean
                      description: Reserved for a later ADR 0003 admission slice — NOT yet enforced in this slice (B0). Accepted by the schema today but currently has no runtime effect. When implemented it will be the required acknowledgement when `baseBranch` names the repository default branch. See `PlanStartByIssue.confirmDefaultBase`.
              type: object
              # END generated:mcp-body
      responses:
        "202":
          description: The plan fan-out was started (or was already running).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StartPlanResult"
        "400":
          description: The issue reference could not be parsed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /actions/start/epic-set:
    post:
      operationId: startEpicSet
      summary: Admit a SET of epics plus their inter-epic dependency edges in one all-or-nothing call (issue #292).
      description: >-
        The set/batch admission door (slice S2). Validates the WHOLE submission before persisting
        anything: each epic passes the same `admitPlan` gate as the single-issue door, every edge must
        connect two epics in the submitted set, and the edge set must be an acyclic DAG. A malformed
        set fails as one clean 4xx at the offending epic/edge with nothing half-started — no edge is
        persisted. On success the admitted epics and validated edges are staged FK-free into
        `admitted_epics` / `admitted_plan_deps`; materializing them into `plans` / `plan_deps` and
        scheduling/lowering (starting roots, seeding the capability readiness-gate, version binding)
        is a later slice (S3).
        Idempotent on the set: re-submitting the identical set records no duplicate edge.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              # BEGIN generated:mcp-body source=#/components/schemas/EpicSetStart (scripts/inline-mcp-bodies.ts — do not hand-edit)
              description: "The set/batch admission request body (issue #292, slice S2). Submits a SET of epics plus the inter-epic dependency edges between them. `epics` is admitted all-or-nothing through the same `admitPlan` gate as the single-issue door; `deps` declares that a `consumer` epic waits for a `producer` epic's published `{ package, capabilityRef }` capability. Every edge must connect two epics named in `epics`, and the edge set must be an acyclic DAG — otherwise the whole set is rejected with a 4xx and nothing is persisted."
              type: object
              additionalProperties: false
              required:
                - epics
              properties:
                epics:
                  type: array
                  minItems: 1
                  description: The epics to admit as one set. Each is admitted through the `admitPlan` gate.
                  items:
                    description: One epic in a submitted set. Names the target issue by EXACTLY ONE of `issue` (`owner/repo#123`) or `url` (a bare issue URL), plus a REQUIRED `baseBranch` and the optional admission acknowledgements — the same per-epic admission inputs as `PlanStart`.
                    oneOf:
                      - type: object
                        additionalProperties: false
                        required:
                          - issue
                          - baseBranch
                        properties:
                          issue:
                            type: string
                            description: 'Issue reference: owner/repo#123.'
                          baseBranch:
                            type: string
                            minLength: 1
                            maxLength: 255
                            pattern: \S
                            description: REQUIRED integration branch this epic branches off and opens its PRs against, admitted through the same ADR 0003 policy as the single-issue door. See `PlanStartByIssue.baseBranch`.
                          allowSharedBase:
                            type: boolean
                            description: Opt in to sharing a custom integration base with another active epic. See `PlanStartByIssue.allowSharedBase`.
                          confirmDefaultBase:
                            type: boolean
                            description: Acknowledge that `baseBranch` names the repository default branch. See `PlanStartByIssue.confirmDefaultBase`.
                      - type: object
                        additionalProperties: false
                        required:
                          - url
                          - baseBranch
                        properties:
                          url:
                            type: string
                            description: A bare issue URL, when no `owner/repo#123` reference is supplied.
                          baseBranch:
                            type: string
                            minLength: 1
                            maxLength: 255
                            pattern: \S
                            description: REQUIRED integration branch this epic branches off. See `EpicSetMemberByIssue.baseBranch`.
                          allowSharedBase:
                            type: boolean
                            description: Share a custom integration base with another active epic. See `PlanStartByIssue.allowSharedBase`.
                          confirmDefaultBase:
                            type: boolean
                            description: Acknowledge landing on the default branch. See `PlanStartByIssue.confirmDefaultBase`.
                deps:
                  type: array
                  description: The inter-epic dependency edges. Each declares `consumer` waits for `producer` to publish the `{ package, capabilityRef }` capability. Both endpoints must name epics in `epics`. Omit or pass `[]` for a set of independent (root) epics.
                  items:
                    description: "One inter-epic dependency edge: the `consumer` epic waits for the `producer` epic to publish the `{ package, capabilityRef }` capability. `consumer`/`producer` are epic references (`owner/repo#123` or an issue URL) that MUST both appear in the set's `epics`."
                    type: object
                    additionalProperties: false
                    required:
                      - consumer
                      - producer
                      - package
                      - capabilityRef
                    properties:
                      consumer:
                        type: string
                        description: The dependent epic (waits). An `owner/repo#123` reference or issue URL in the set.
                      producer:
                        type: string
                        description: The producer epic it waits for. An `owner/repo#123` reference or issue URL in the set.
                      package:
                        type: string
                        minLength: 1
                        description: The producer epic's published package name — the capability probe's target (S3).
                      capabilityRef:
                        type: string
                        minLength: 1
                        description: The producer epic's issue handle, used to resolve which published pkg@version first carries the capability (S3).
              # END generated:mcp-body
      responses:
        "202":
          description: The whole set validated; every epic was admitted and every edge staged FK-free (in `admitted_plan_deps`) for S3 to materialize.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StartEpicSetResult"
        "400":
          description: >-
            The set is malformed — an unparseable epic/edge reference, a rejected base branch, an edge
            naming an epic outside the set, or a dependency cycle.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "409":
          description: An epic's base branch is already in use by another active epic (shared-base guard).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /actions/start/sequence-issues:
    post:
      operationId: sequenceIssues
      summary: Generate + STAGE the canonical "implement issue → converge → merge" delivery graph from a high-level intent (never dispatches). (epic #605 / S4)
      description: >-
        An INTENT-SHAPED door (epic nano-workforce#605, S4). ADR 0005's delivery graph is a closed
        vocabulary and operator-guide §9.4 already names the canonical shape for "implement issue →
        converge → merge", but an agent still had to hand-author the full node/edge JSON — sequencing
        four issues behind a gate meant constructing 13 nodes and 12 edges by hand. This door takes the
        high-level intent `{ behind?, issues[] }` and GENERATES that canonical graph, then STAGES it
        through the SAME compile+stage flow the raw `compileDeliveryGraph` door uses (one compiler, one
        staging path — idempotency + digest inherited, not re-implemented). It returns a preview and a
        navigational `reviewUrl` and NOTHING that can trigger a run: dispatch is an OPERATOR action in
        the cockpit (ADR 0005 Decision 7 / issue #460).


        For each issue it emits the canonical chain — `agent` (`senior:feature`, emits a typed `pr`
        fact) → `connector` (`converge-merge`, late-binding that `pr`) → `wait[pr, merged]` (a realistic
        `poll.timeoutMs`) — and threads the `pr` fact along fact-qualified edges (§9.4). The issues run
        in SEQUENCE: each issue's implementation starts once the PRIOR issue has merged. When `behind`
        is given, a leading `wait[epic]` gate (§9.5) makes the whole sequence wait for that reference to
        be fully merged first.


        INPUT — the intent OBJECT `{ "issues": ["owner/repo#A", …] }` with an OPTIONAL
        `"behind": "owner/repo#NN"` gate (this is the object-body door). SIDE EFFECTS — impure: a valid intent is STAGED as a proposal.
        IDEMPOTENCY — content-addressed by the compiled `digest` (an identical intent re-stages the same
        digest). VALIDATION — invalid input (empty `issues`, an unparseable `owner/repo#N` reference, an
        unknown target/probe per the S3 vocabulary) is a `400` with `issues: [{ path, message }]`;
        nothing is staged. NEXT — surface the returned `reviewUrl` to the operator; poll
        `listStagedProposals` to see the staged digest.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              # BEGIN generated:mcp-body source=#/components/schemas/SequenceIssuesIntent (scripts/inline-mcp-bodies.ts — do not hand-edit)
              description: 'The `sequenceIssues` INTENT (epic nano-workforce#605, S4) — a high-level shape that GENERATES the canonical "implement issue → converge → merge" delivery graph (operator-guide §9.4) instead of making an agent hand-author its node/edge JSON. It names an ordered list of `issues` to sequence (each issue''s implementation starts once the PRIOR issue has merged) and an OPTIONAL leading `behind` gate (wait for that issue/epic/feature to be fully merged first, §9.5). The door GENERATES, then STAGES the graph through the same compile+stage flow as `compileDeliveryGraph` — it never dispatches (dispatch is an operator-only cockpit action, ADR 0005 Decision 7). For each issue it emits `agent` (`senior:feature`, emits a `pr` fact) → `connector` (`converge-merge`, late-binding that `pr`) → `wait[pr, merged]` (a realistic `poll.timeoutMs`), threading the `pr` fact per §9.4. An `issues[]` entry may be a bare `owner/repo#N` string OR a `{ gate?, issue }` object that interleaves a `wait` gate (the `npm`/`github-check`/… vocabulary) before that issue''s agent (issue #740). Invalid input (empty `issues`, an unparseable ref, an unknown gate kind) is a 400 carrying `issues: [{ path, message }]`; nothing is staged.'
              type: object
              additionalProperties: false
              example:
                issues:
                  - nanobpm/nano-ide#557
                  - gate:
                      kind: npm
                      target: '@nanobpm/agentic@0.13.0'
                    issue: jwulf/c8ctl-plugin-nano#186
                  - nanobpm/nano-workforce#738
              required:
                - issues
              properties:
                behind:
                  type: string
                  minLength: 1
                  maxLength: 255
                  description: OPTIONAL gate — an `owner/repo#NN` issue/epic/feature reference. When present, a leading `wait[epic]` node gates the whole sequence on that reference reaching "fully merged" (every opened slice/PR landed, §9.5) before the first issue's implementation starts.
                issues:
                  type: array
                  minItems: 1
                  maxItems: 64
                  items:
                    oneOf:
                      - type: string
                        minLength: 1
                        maxLength: 255
                        description: A bare `owner/repo#N` issue reference (no gate) — today's behaviour, byte-for-byte.
                      - description: "One `issues[]` entry with an OPTIONAL leading gate (issue #740) — an object form of a sequence step. The `issue` is implemented + converged + merged like a bare-string entry; the optional `gate` must go green (in addition to the prior issue merging) before this issue's agent starts."
                        type: object
                        additionalProperties: false
                        required:
                          - issue
                        properties:
                          gate:
                            description: 'An interleaved `wait` GATE (issue #740) that must go green before a given issue''s agent starts — reusing the exact probe schema `wait` nodes accept (`npm`/`github-check`/`http`/`command`/ `capability`/`pr`/`epic`). It lets an author splice e.g. "wait for `@nanobpm/agentic@0.13.0` to publish" between two sequence steps without hand-authoring raw node/edge JSON.'
                            type: object
                            additionalProperties: false
                            required:
                              - kind
                              - target
                            properties:
                              kind:
                                type: string
                                minLength: 1
                                description: The wait-probe kind — one of the S3 vocabulary kinds (`npm`, `github-check`, `http`, `command`, `capability`, `pr`, `epic`). An unknown kind is a 400.
                              target:
                                type: string
                                minLength: 1
                                description: 'The probe target — kind-specific: `pkg@version` for `npm`, `owner/repo@ref` for `github-check`, a URL for `http`, an `owner/repo#N` PR for `pr`, etc.'
                              match:
                                type: object
                                description: OPTIONAL kind-specific readiness match fields (e.g. `{ version }` for `npm`, `{ conclusion, checkName }` for `github-check`).
                              poll:
                                type: object
                                additionalProperties: false
                                description: OPTIONAL poll budget. Defaults to the bounded merge-gate budget (re-probe every 5 minutes, budget 3 days) so a gate never falls into the 30-minute default trap.
                                properties:
                                  everyMs:
                                    type: integer
                                    minimum: 1
                                    description: How often to re-probe (milliseconds).
                                  timeoutMs:
                                    type: integer
                                    minimum: 1
                                    description: The total budget before the gate escalates/continues (milliseconds).
                              onTimeout:
                                type: string
                                enum:
                                  - escalate
                                  - continue
                                description: What to do when the gate never goes green within its budget — `escalate` (default) parks on a human; `continue` proceeds anyway. `fail` is not supported on a `wait` node.
                              credentialEnv:
                                type: string
                                minLength: 1
                                description: OPTIONAL env-key name supplying a credential for the probe (`http`/`capability`).
                          issue:
                            type: string
                            minLength: 1
                            maxLength: 255
                            description: An `owner/repo#N` issue reference to implement + converge + merge, in sequence.
                    description: "A sequence step — a bare `owner/repo#N` string, or a `{ gate?, issue }` object that interleaves a `wait` gate before that issue's agent (issue #740)."
                  description: "The ordered issues to sequence — each is implemented by a `senior:feature` agent that opens a PR, driven to convergence + merge, and the NEXT issue's implementation starts only once the prior issue has merged (and any interleaved gate has gone green). At least one; at most 64. The compiler's node ceiling is enforced SEPARATELY by a node-budget guard (interleaved gates each add nodes, so a fully-gated 64-issue sequence can exceed the ceiling and is rejected there) — `maxItems: 64` bounds the entry count, not the generated node count."
              # END generated:mcp-body
      responses:
        "200":
          description: The intent generated a valid delivery graph — it compiled and is STAGED for operator review; the response carries a preview and a navigational reviewUrl (no dispatch handle).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CompileDeliveryGraphStaged"
        "400":
          description: The intent was invalid (empty/oversized issues, an unparseable reference, or an unknown target/probe) — path-qualified issues, nothing generated or staged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SequenceIssuesRejected"
  /actions/compile-delivery-graph:
    post:
      operationId: compileDeliveryGraph
      summary: Validate + compile a delivery graph and STAGE it for operator review (never dispatches). (ADR 0005 Decision 7 / #460)
      description: >-
        The agent-facing delivery-graph door — the END of the agent's surface (ADR 0005 Decision 7,
        issue #460). Given an agent-authored `DeliveryGraph` (the closed
        `agent`/`wait`/`human`/`connector` node vocabulary — the trust boundary), it runs the pure
        `validateDeliveryGraph` semantic check and the deterministic, human-written compiler, and — when
        the graph is valid — persists the compiled graph as a `staged` proposal (content-addressed by
        its `digest`). It returns a PREVIEW and a navigational `reviewUrl` and NOTHING that can trigger a
        run: no run key, no token, no process-instance key. Dispatch is an OPERATOR action performed in
        the cockpit — there is no `start` endpoint. This is capability-by-absence: if the agent never
        receives a dispatch handle, there is nothing to replay (closing the self-approval hole the old
        replayable `approvalToken` left open). A malformed graph is a 400 carrying path-qualified errors;
        nothing is staged.


        INPUT — the structured `DeliveryGraph` OBJECT directly (`{ nodes[], edges[] }`), NOT a string:
        this is the object-body door. Its sibling `previewDeliveryGraph` instead takes the
        text-door shape `{ "graphJson": "<serialized DeliveryGraph>" }` (the same paste the cockpit
        uses). SIDE EFFECTS — impure: a valid graph is STAGED (persisted as a `staged` proposal),
        unlike the pure `previewDeliveryGraph` which never persists. IDEMPOTENCY — content-addressed
        by `digest`: re-compiling an identical graph re-stages the same digest, not a duplicate.
        VALIDATION — a failure returns `400` with `errors: [{ path, message }]`. NEXT — surface the
        returned `reviewUrl` to the operator (only a human's cockpit Dispatch runs it); poll
        `listStagedProposals` to see the staged digest.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              # BEGIN generated:mcp-body source=#/components/schemas/DeliveryGraph (scripts/inline-mcp-bodies.ts — do not hand-edit)
              description: 'An agent-authored delivery graph (ADR 0005) — the SINGLE agent-facing artifact for a heterogeneous, partly-human, cross-repo delivery runbook. It is DATA, never an executable artifact: a JSON DAG whose nodes each name a `kind` from a CLOSED allowlist (`agent`/`wait`/`human`/`connector` — Decision 1/2, the trust boundary) and whose `edges` name DISCOVERED facts (Decision 3). Ingest validates the SHAPE here and the SEMANTICS (acyclicity, edge integrity, fact resolution) in the pure `validateDeliveryGraph` (`app/deliveryGraph.ts`). This slice (S0) defines the vocabulary + validation surface ONLY — no compiler, dispatch, or execution (those land in later slices).'
              type: object
              additionalProperties: false
              example:
                name: start
                nodes:
                  - id: gate-epic
                    kind: wait
                    wait:
                      kind: epic
                      target: nanobpm/nano-ide#488
                      match:
                        epicState: merged
                      poll:
                        everyMs: 300000
                        timeoutMs: 259200000
                      onTimeout: escalate
                    emits:
                      - name: prCount
                        type: number
                  - id: start-b
                    kind: agent
                    agent:
                      jobType: senior:feature
                      prompt: Implement nanobpm/nano-workforce#567 and open a PR.
                edges:
                  - from: gate-epic
                    to: start-b
              required:
                - nodes
              properties:
                name:
                  type: string
                  maxLength: 255
                  description: OPTIONAL human-readable label for the graph (shown in the rendered preview).
                nodes:
                  type: array
                  minItems: 1
                  maxItems: 256
                  items:
                    description: One node in a delivery graph. A discriminated union on `kind` over the CLOSED allowlist; the matching per-kind config object (`agent`/`wait`/`connector`) is REQUIRED and names the engine-native body the node delegates to (Decision 2 — the graph schedules, it does not re-implement execution). The `human` config is the sole exception — it is OPTIONAL (a bare `human` node resolves to a generic emit-capturing form fallback in S3).
                    oneOf:
                      - type: object
                        properties:
                          id:
                            type: string
                          emits:
                            type: array
                            items:
                              description: 'A typed output a node declares it will EMIT (ADR 0005 Decision 3/4 — emitted-fact typing). A downstream edge references it as `from: "<nodeId>.<fact>"`, so a bind is validated against this declaration, not stringly. A "click done" human node or a pass-through node declares no facts (`emits` absent/empty) — the degenerate no-emit case.'
                              type: object
                              additionalProperties: false
                              required:
                                - name
                                - type
                              properties:
                                name:
                                  type: string
                                  minLength: 1
                                  maxLength: 128
                                  pattern: ^[A-Za-z_][A-Za-z0-9_]*$
                                  description: The fact's identifier, referenced downstream as `<nodeId>.<name>`. Must be unique within the node.
                                type:
                                  type: string
                                  enum:
                                    - string
                                    - number
                                    - boolean
                                    - artifact
                                    - version
                                    - url
                                    - pr
                                  description: "The fact's declared type. `artifact` is a `pkg@version` handle, `version` a bare version string, `url` a location — mirrors the values `capability`/`pr` probes late-bind. `pr` (issue #548) is a PR reference (`owner/repo#N`) an `agent` node emits for the PR it opened, so a downstream `connector[converge*]`/`wait[pr]` node LATE-BINDS its target PR from the fact instead of a hardcoded literal."
                                description:
                                  type: string
                                  maxLength: 512
                                  description: OPTIONAL human note describing what the fact carries.
                            description: 'The typed facts this agent node hands forward (issue #506 — the classifier-emit contract). Each declared fact is appended to the node''s dispatch prompt as an instruction the servicing `senior:*` agent MUST honour: return the fact as an extra TOP-LEVEL field of its result JSON (the same `AGENT_RESULT_FILE` envelope that carries `status`/`summary`/`pr`). The delivery output-mapping publishes that completion variable (named exactly after the fact) so a downstream guarded edge (`when: "<node>.<fact>"` + `equals`) routes on it; an omitted fact takes the split''s `default` branch.'
                          kind:
                            type: string
                            enum:
                              - agent
                          agent:
                            type: object
                            additionalProperties: false
                            required:
                              - jobType
                            properties:
                              jobType:
                                type: string
                                minLength: 1
                                description: The agent job type a worker executes for this node (e.g. `senior:feature`).
                              prompt:
                                type: string
                                maxLength: 20000
                                description: OPTIONAL steering prompt appended to the node's job brief.
                              repository:
                                type: string
                                maxLength: 255
                                pattern: ^[A-Za-z0-9-]+/(?!.*\.[Gg][Ii][Tt]$)[A-Za-z0-9._-]+$
                                description: OPTIONAL per-node `owner/repo` this agent node implements against (#739). A delivery graph provisions the `io.nanobpm.agentTask.repository` isolation envelope PER agent cell from THIS field, so a genuinely cross-repo graph (each node a different repo) isolates correctly without the operator ticking `repoless`. Absent → the node falls back to the run-level dispatch `repository`. When set it must be exactly `owner/repo` (a trailing `.git` and any non-`owner/repo` shape are rejected at submit), the same allowlist `repoEnvelopeVars`/the dispatch door apply. The `sequenceIssues` generator populates this automatically from the `owner/repo#N` each node implements.
                              baseBranch:
                                type: string
                                maxLength: 255
                                pattern: ^(?![/.-])(?!.*[/.]$)(?!.*\.\.)(?!.*//)(?!.*/\.)(?!.*\.lock(?:/|$))[A-Za-z0-9._/-]+$
                                description: "OPTIONAL per-node base branch this agent node branches off (#739) — the `ref` the harness checks out in the isolated clone (the pre-PR shape: the harness cuts the deterministic `feat/<node.id>` branch off this base itself, per #776, so a forgetful agent can never be left committing on the base branch). Absent → the run-level dispatch `baseBranch` (else the node's repository default branch). A value that is not a plausible git branch name is rejected at submit; the pattern mirrors the authoritative server-side gate (`isPlausibleBranchName`, app/baseBranch.ts)."
                              converge:
                                type: boolean
                                description: 'OPTIONAL first-class CONVERGE policy (ADR 0006 §3 / S5) — a DECLARED, compiler- validated completion-policy flag on this cell node. It declares that the node''s opened PR is to be driven through the review-convergence loop to green as an edge-gated completion policy; this slice adds and validates the flag, with the delivery-graph execution wiring that consumes it landing in a follow-up slice. It supersedes (in intent) the emergent `feature.bpmn` `gw-converge` gateway and the "un-draft + merge #B" prompt prose a delivery-graph `agent` node used to smuggle. Converge and merge are SEPARABLE phases; a node may converge without merging (stop at green and gate the landing behind a downstream node).'
                              merge:
                                type: boolean
                                description: "OPTIONAL first-class MERGE (land) policy (ADR 0006 §3 / S5) — a DECLARED, compiler-validated flag. When set it declares that the cell lands its PR. REQUIRES `converge: true` — you cannot land a PR you have not driven to green (the validator rejects `merge` without `converge`). This slice adds and validates the flag; the execution wiring that consumes it lands in a follow-up slice. TWO-LEVEL (ADR 0003 base-branch admission): a UNIT node lands onto its epic/graph base branch, never `main` directly; the graph's final merge-to-`main` is a separate top-level step."
                              timeout:
                                type: string
                                pattern: ^[Pp](?!$)(\d+[Yy])?(\d+[Mm])?(\d+[Ww])?(\d+[Dd])?([Tt](?=\d)(\d+[Hh])?(\d+[Mm])?(\d+[Ss])?)?$
                                maxLength: 64
                                description: OPTIONAL per-node ISO-8601 SLA timeout (#505). Overrides the run-level `nodeTimeout` (and the `PT1H` default) for THIS node's bounded-timeout → escalate boundary timer, so a legitimately-long node (e.g. a full `senior:feature` implementation) can outlast a quick gate without a spurious escalation. Absent → the run/default value.
                        required:
                          - id
                          - kind
                          - agent
                        description: An `agent` node — a worker executes an agent job type (the existing fan-out body). Bounded (timeout → escalate) and resumable like every node.
                        additionalProperties: false
                      - type: object
                        properties:
                          id:
                            type: string
                          emits:
                            type: array
                            items:
                              description: 'A typed output a node declares it will EMIT (ADR 0005 Decision 3/4 — emitted-fact typing). A downstream edge references it as `from: "<nodeId>.<fact>"`, so a bind is validated against this declaration, not stringly. A "click done" human node or a pass-through node declares no facts (`emits` absent/empty) — the degenerate no-emit case.'
                              type: object
                              additionalProperties: false
                              required:
                                - name
                                - type
                              properties:
                                name:
                                  type: string
                                  minLength: 1
                                  maxLength: 128
                                  pattern: ^[A-Za-z_][A-Za-z0-9_]*$
                                  description: The fact's identifier, referenced downstream as `<nodeId>.<name>`. Must be unique within the node.
                                type:
                                  type: string
                                  enum:
                                    - string
                                    - number
                                    - boolean
                                    - artifact
                                    - version
                                    - url
                                    - pr
                                  description: "The fact's declared type. `artifact` is a `pkg@version` handle, `version` a bare version string, `url` a location — mirrors the values `capability`/`pr` probes late-bind. `pr` (issue #548) is a PR reference (`owner/repo#N`) an `agent` node emits for the PR it opened, so a downstream `connector[converge*]`/`wait[pr]` node LATE-BINDS its target PR from the fact instead of a hardcoded literal."
                                description:
                                  type: string
                                  maxLength: 512
                                  description: OPTIONAL human note describing what the fact carries.
                          kind:
                            type: string
                            enum:
                              - wait
                          wait:
                            description: 'A single durable readiness probe (issue #258, #295) the feature run must satisfy before its implementation agent is dispatched. `kind` selects the source; `target` + `match` are the per-kind predicate. The `capability` kind resolves "which published `pkg@version` first carries capability C?" from publish provenance and late-binds it into the run. See `app/readiness.ts` for the full per-kind semantics.'
                            type: object
                            additionalProperties: false
                            required:
                              - kind
                              - target
                            properties:
                              kind:
                                type: string
                                enum:
                                  - http
                                  - command
                                  - npm
                                  - github-check
                                  - capability
                                  - pr
                                  - epic
                                description: The readiness source. `command` is the escape hatch; `capability` resolves a cross-repo published-artifact edge; `pr` watches an in-flight PR's merge state (ADR 0005 §2); `epic` gates on an nwf plan-fanout epic reaching "fully merged", keyed by its `planKey` (issue
                              target:
                                type: string
                                minLength: 1
                                description: The kind-specific target (a URL, a shell command, a `pkg@version`, an `owner/repo@ref`, `github-releases:owner/repo`, an `owner/repo#123` PR reference for the `pr` kind, or an `owner/repo#NN` planKey — the epic issue — for the `epic` kind). For a `pr`/`epic` target used in a **delivery-graph `wait` node** it may instead be a `<nodeId>.<fact>` late-binding reference the delivery-graph compiler resolves at dispatch (issue
                              onTimeout:
                                type: string
                                enum:
                                  - escalate
                                  - fail
                                  - continue
                                description: What the gate does when the bounded wait elapses (default `escalate`).
                              credentialEnv:
                                type: string
                                description: A declared env-contract key supplying a credential (http kind only). Names a key, never a secret value.
                              match:
                                type: object
                                additionalProperties: false
                                description: The per-kind readiness predicate; every field is optional and read only by the kinds that understand it.
                                properties:
                                  status:
                                    type: integer
                                    description: 'http: the exact status that means ready (default any 2xx).'
                                  bodyIncludes:
                                    type: string
                                    description: 'http: a substring the response body must contain.'
                                  exitCode:
                                    type: integer
                                    description: 'command: the exit code that means ready (default 0).'
                                  stdoutIncludes:
                                    type: string
                                    description: 'command/npm: a substring stdout must contain.'
                                  version:
                                    type: string
                                    description: 'npm: the version that must be published.'
                                  conclusion:
                                    type: string
                                    description: 'github-check: the conclusion that means ready (default success).'
                                  checkName:
                                    type: string
                                    description: 'github-check: restrict to the named check run.'
                                  capabilityRef:
                                    type: string
                                    description: 'capability: the upstream issue/PR handle the resolved version must carry.'
                                  package:
                                    type: string
                                    description: 'capability: the package whose releases are scanned for provenance.'
                                  verifyCommand:
                                    type: string
                                    description: 'capability: optional empirical verifier run once at the gate boundary.'
                                  prState:
                                    type: string
                                    enum:
                                      - ready
                                      - merged
                                      - mergeable
                                      - checks-green
                                    description: 'pr: the declared PR state to wait for (default merged).'
                                  epicState:
                                    type: string
                                    enum:
                                      - merged
                                      - done
                                    description: "epic: the declared plan-fanout aggregate state to wait for (default merged) — both mean 'fully merged' (issue #568)."
                              poll:
                                type: object
                                additionalProperties: false
                                description: The poll cadence (how often to re-probe, how long to keep trying, and the backoff shape).
                                properties:
                                  everyMs:
                                    type: integer
                                    description: Interval between poll attempts (ms).
                                  timeoutMs:
                                    type: integer
                                    description: Bounded budget (ms) before the gate escalates.
                                  backoff:
                                    type: string
                                    enum:
                                      - fixed
                                      - exponential
                                    description: Backoff shape between attempts.
                        required:
                          - id
                          - kind
                          - wait
                        description: A `wait` node — a durable `ReadinessProbe` (ADR 0001 §2) watching an external fact. Reuses the existing `ReadinessProbe` shape verbatim (Decision 3 — never a second wait loop); the `pr` merge-state kind is added to that shape by slice S2 and flows in here automatically. The probe's `poll.timeoutMs` sets THIS node's escalation boundary (how long the gate waits before it acts on `onTimeout`), falling back to the run/default when absent (#462). `onTimeout` `escalate` (default) parks the elapsed gate on a human-completable task; `continue` proceeds past the gate as not-ready with NO human stop (a sharp edge — the downstream side-effecting node then runs without the awaited fact); `fail` is NOT yet supported on a delivery `wait` node (blocked on engine terminate-end execution, Magikcraft/nano-bpm#978) and is rejected at compile with a path-qualified error rather than silently degrading.
                        additionalProperties: false
                      - type: object
                        properties:
                          id:
                            type: string
                          emits:
                            type: array
                            items:
                              description: 'A typed output a node declares it will EMIT (ADR 0005 Decision 3/4 — emitted-fact typing). A downstream edge references it as `from: "<nodeId>.<fact>"`, so a bind is validated against this declaration, not stringly. A "click done" human node or a pass-through node declares no facts (`emits` absent/empty) — the degenerate no-emit case.'
                              type: object
                              additionalProperties: false
                              required:
                                - name
                                - type
                              properties:
                                name:
                                  type: string
                                  minLength: 1
                                  maxLength: 128
                                  pattern: ^[A-Za-z_][A-Za-z0-9_]*$
                                  description: The fact's identifier, referenced downstream as `<nodeId>.<name>`. Must be unique within the node.
                                type:
                                  type: string
                                  enum:
                                    - string
                                    - number
                                    - boolean
                                    - artifact
                                    - version
                                    - url
                                    - pr
                                  description: "The fact's declared type. `artifact` is a `pkg@version` handle, `version` a bare version string, `url` a location — mirrors the values `capability`/`pr` probes late-bind. `pr` (issue #548) is a PR reference (`owner/repo#N`) an `agent` node emits for the PR it opened, so a downstream `connector[converge*]`/`wait[pr]` node LATE-BINDS its target PR from the fact instead of a hardcoded literal."
                                description:
                                  type: string
                                  maxLength: 512
                                  description: OPTIONAL human note describing what the fact carries.
                          kind:
                            type: string
                            enum:
                              - human
                          human:
                            type: object
                            additionalProperties: false
                            description: OPTIONAL human-node config. `formKey` explicitly attaches a form (else a form is selected by node category, else a generic emit-capturing fallback — resolved in S3). The node's typed output is declared via the node-level `emits[]`.
                            properties:
                              formKey:
                                type: string
                                minLength: 1
                                description: OPTIONAL explicit form to attach at authoring time (specific-else-generic resolution, S3).
                              prompt:
                                type: string
                                maxLength: 20000
                                description: OPTIONAL instruction shown to the human/agent completing the task ("now do X").
                        required:
                          - id
                          - kind
                        description: A `human` node — a scheduled user task + form (ADR 0002 machinery promoted from exception to node, Decision 4). Surfaces "now do X" on the Tasks inbox, blocks dependents, is answerable by a human OR an agent, is SLA-bounded, and can EMIT a typed fact its form captures.
                        additionalProperties: false
                      - type: object
                        properties:
                          id:
                            type: string
                          emits:
                            type: array
                            items:
                              description: 'A typed output a node declares it will EMIT (ADR 0005 Decision 3/4 — emitted-fact typing). A downstream edge references it as `from: "<nodeId>.<fact>"`, so a bind is validated against this declaration, not stringly. A "click done" human node or a pass-through node declares no facts (`emits` absent/empty) — the degenerate no-emit case.'
                              type: object
                              additionalProperties: false
                              required:
                                - name
                                - type
                              properties:
                                name:
                                  type: string
                                  minLength: 1
                                  maxLength: 128
                                  pattern: ^[A-Za-z_][A-Za-z0-9_]*$
                                  description: The fact's identifier, referenced downstream as `<nodeId>.<name>`. Must be unique within the node.
                                type:
                                  type: string
                                  enum:
                                    - string
                                    - number
                                    - boolean
                                    - artifact
                                    - version
                                    - url
                                    - pr
                                  description: "The fact's declared type. `artifact` is a `pkg@version` handle, `version` a bare version string, `url` a location — mirrors the values `capability`/`pr` probes late-bind. `pr` (issue #548) is a PR reference (`owner/repo#N`) an `agent` node emits for the PR it opened, so a downstream `connector[converge*]`/`wait[pr]` node LATE-BINDS its target PR from the fact instead of a hardcoded literal."
                                description:
                                  type: string
                                  maxLength: 512
                                  description: OPTIONAL human note describing what the fact carries.
                          kind:
                            type: string
                            enum:
                              - connector
                          connector:
                            type: object
                            additionalProperties: false
                            required:
                              - target
                            properties:
                              target:
                                type: string
                                minLength: 1
                                description: The connector action target (forward-declared — the concrete scheme lands in a later slice).
                              dedupeKey:
                                type: string
                                minLength: 1
                                description: OPTIONAL idempotency key so an at-least-once resume cannot double-fire this side-effecting node (ADR 0005 Decision 7). Author-supplied or graph-derived.
                              payload:
                                type: object
                                additionalProperties: true
                                description: Minimal forward-declared payload stub — the concrete connector payload schema is deferred (ADR non-goal).
                              timeout:
                                type: string
                                pattern: ^[Pp](?!$)(\d+[Yy])?(\d+[Mm])?(\d+[Ww])?(\d+[Dd])?([Tt](?=\d)(\d+[Hh])?(\d+[Mm])?(\d+[Ss])?)?$
                                maxLength: 64
                                description: OPTIONAL per-node ISO-8601 SLA timeout (#505). Overrides the run-level `nodeTimeout` (and the `PT1H` default) for THIS connector node's bounded-timeout → escalate boundary timer. Absent → the run/default value.
                        required:
                          - id
                          - kind
                          - connector
                        description: A `connector` node — an automated, side-effecting outbound action (the connector I/O surface). Side-effecting, so it carries a `dedupeKey` and tolerates at-least-once execution. The `payload` schema is a minimal forward-declared stub in this slice (ADR 0005 non-goal — the concrete connector I/O lands later).
                        additionalProperties: false
                  description: The graph's nodes. Each carries a unique `id` and a `kind` from the closed allowlist, plus its per-kind config and its typed `emits[]` declaration. Node ids must be unique across the graph (enforced by `validateDeliveryGraph`).
                edges:
                  type: array
                  maxItems: 1024
                  items:
                    description: A dependency edge — "`to` proceeds once fact `from` is observable" (ADR 0005 Decision 3). `from` is either a bare `<nodeId>` (wait for the upstream node's completion fact) or a qualified `<nodeId>.<fact>` referencing a declared `emits` fact of that node. Both endpoints must resolve to a node in the graph, the referenced fact must be declared, and the whole edge set must be a DAG — all enforced by `validateDeliveryGraph`. An OPTIONAL `when`/`equals` guard (or a `default` else-branch) makes the edge CONDITIONAL, turning its producer into an exclusive split (ADR 0005 S7) — a node's out-edges are then ALL guarded or ALL unconditional.
                    type: object
                    additionalProperties: false
                    required:
                      - from
                      - to
                    properties:
                      from:
                        type: string
                        minLength: 1
                        description: The upstream endpoint — `<nodeId>` (completion) or `<nodeId>.<fact>` (a declared emitted fact).
                      to:
                        type: string
                        minLength: 1
                        description: The dependent node's id — proceeds once `from` is observed.
                      when:
                        type: string
                        minLength: 1
                        description: 'OPTIONAL guard reference `<nodeId>.<fact>` naming a SCALAR emitted fact (`string`, `number`, or `boolean`) of the `from`-adjacent producer (ADR 0005 S7). Its presence makes this a GUARDED edge and turns the producer into an exclusive-split point: the edge is taken only when that runtime fact `equals` the literal below. Equality-only — no arbitrary expressions (the trust boundary). Mutually exclusive with `default`.'
                      equals:
                        description: The literal value `when`'s fact must equal for this guarded edge to be taken (ADR 0005 S7). REQUIRED iff `when` is present, and its JSON type must match the referenced fact's declared type (`string`/`number`/`boolean`).
                        oneOf:
                          - type: string
                          - type: number
                          - type: boolean
                      default:
                        type: boolean
                        enum:
                          - true
                        description: 'OPTIONAL — marks this edge as the ELSE branch of the exclusive split (taken when no guarded edge matches at runtime). A FLAG: only `true` is meaningful, so it is constrained to `true` (omit the field entirely for a non-default edge — `default: false` is not a valid wire value). At most one `default` edge per split node. Mutually exclusive with `when`/`equals` (ADR 0005 S7).'
                  description: The dependency edges — the graph's discovered-fact topology (Decision 3). Each edge means "`to` proceeds once fact `from` about the upstream node is observable". `from` is either a bare `<nodeId>` (the degenerate "wait for the upstream node's completion" fact) or a qualified `<nodeId>.<fact>` referencing one of that node's declared `emits`. Omit/`[]` for a set of independent (root) nodes. The edge set must be a DAG.
              # END generated:mcp-body
      responses:
        "200":
          description: The graph validated and compiled — it is STAGED for operator review; the response carries a preview and a navigational reviewUrl (no dispatch handle).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CompileDeliveryGraphStaged"
        "400":
          description: The graph failed shape or semantic validation — path-qualified errors, nothing compiled or staged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CompileDeliveryGraphErrors"
  /actions/delivery-graph/preview:
    post:
      operationId: previewDeliveryGraph
      summary: UI JSON-paste PURE PREVIEW — parse a pasted delivery-graph JSON string and compile it, without staging. (ADR 0005 Decision 7 / #460 / #516)
      description: >-
        The human-facing UI JSON-paste PURE PREVIEW ingress. The Delivery Graphs page's "Preview"
        action posts the operator's pasted JSON as a STRING; this door parses it and runs the SAME
        `compileDeliveryGraph` compiler the agent-facing door uses, but — unlike the compile/stage doors
        — it does NOT persist anything (#516: preview and staging are separate operator actions). It
        returns a compact preview summary (`staged:false`, the `digest`, node/human/side-effect counts,
        the mermaid `diagram`, the human stops and side effects) PLUS the compiled `bpmn` (with diagram
        interchange) so the page can render the laid-out BPMN in the host explorer without staging. It
        never deploys, stages or dispatches. A blank/invalid JSON string, or a graph that fails
        validation, is a 400 carrying a human `error` (and path-qualified `errors` for a compile failure).


        INPUT — the text-door shape `{ "graphJson": "<serialized DeliveryGraph>" }` (a STRING, the
        same paste the cockpit uses), NOT the structured object; its sibling `compileDeliveryGraph`
        takes the `DeliveryGraph` OBJECT directly. SIDE EFFECTS — none: PURE preview, nothing is
        persisted or dispatched (`staged: false`). IDEMPOTENCY — total (a pure function of the input).
        VALIDATION — a bad string or invalid graph returns `400` with a human `error` and
        `errors: [{ path, message }]`. NEXT — to actually stage, send the SAME graph as an object to
        `compileDeliveryGraph`, then have an operator dispatch it from the `reviewUrl`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              # BEGIN generated:mcp-body source=#/components/schemas/DeliveryGraphPreviewSubmit (scripts/inline-mcp-bodies.ts — do not hand-edit)
              description: 'The human-facing UI JSON-paste PREVIEW request (issues #386 + #516). The Delivery Graphs page''s "Preview" action cannot submit a structured object, so the operator''s pasted delivery-graph is carried as a raw JSON STRING (`graphJson`), parsed server-side and handed to the SAME pure `compileDeliveryGraph` compiler the agent-facing door uses. Preview compiles WITHOUT persisting. Per-operation schema (not shared with the stage door) so each door''s request stays independently evolvable.'
              type: object
              additionalProperties: false
              example:
                graphJson: '{"name":"start #567 once epic #488 has fully merged","nodes":[{"id":"gate-epic","kind":"wait","wait":{"kind":"epic","target":"nanobpm/nano-ide#488","match":{"epicState":"merged"},"poll":{"everyMs":300000,"timeoutMs":259200000},"onTimeout":"escalate"},"emits":[{"name":"prCount","type":"number"}]},{"id":"start-b","kind":"agent","agent":{"jobType":"senior:feature","prompt":"Implement nanobpm/nano-workforce#567 and open a PR."}}],"edges":[{"from":"gate-epic","to":"start-b"}]}'
              required:
                - graphJson
              properties:
                graphJson:
                  type: string
                  description: The pasted delivery-graph JSON (a serialised `DeliveryGraph`), parsed server-side.
              # END generated:mcp-body
      responses:
        "200":
          description: The pasted graph parsed, validated and compiled — the preview summary and compiled BPMN are returned; nothing is staged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeliveryGraphTextResult"
        "400":
          description: The pasted text was not valid JSON, or the graph failed validation/compilation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeliveryGraphTextResult"
  /actions/delivery-graph/stage:
    post:
      operationId: stageDeliveryGraph
      # x-mcp exclusion (ADR 0067 §2 / nano-ide#488): operator-only cockpit door, kept OFF the
      # runtime-projected MCP tool surface. The staged-proposal lifecycle (stage -> dispatch ->
      # dismiss) is the human approval path (ADR 0005 Decision 7 — the operator's click IS the
      # approval); agents author graphs through the pure compile/preview doors only, never stage.
      x-mcp:
        exclude: true
      summary: UI JSON-paste STAGE — parse a pasted delivery-graph JSON string, compile it and stage it for operator dispatch. (ADR 0005 Decision 7 / #460 / #516)
      description: >-
        The human-facing UI JSON-paste STAGE ingress — the deliberate commit half of the preview/stage
        split (#516). The Delivery Graphs page's "Stage" action posts the operator's pasted JSON as a
        STRING; this door parses it, runs the SAME `compileDeliveryGraph` compiler the preview/agent
        doors use, and — on success — persists the compiled graph as a `staged` proposal
        (content-addressed by its `digest`). It returns the same preview summary as the preview door but
        with `staged:true`. It never deploys or dispatches — dispatch is a separate operator action on
        the staged proposal (the Dispatch button on the staged-proposals grid). A blank/invalid JSON
        string, or a graph that fails validation, is a 400 carrying a human `error` (and path-qualified
        `errors` for a compile failure); nothing is staged.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DeliveryGraphStageSubmit"
      responses:
        "200":
          description: The pasted graph parsed, validated and compiled — staged for operator dispatch; the preview summary is returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeliveryGraphTextResult"
        "400":
          description: The pasted text was not valid JSON, or the graph failed validation/compilation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeliveryGraphTextResult"
  /actions/delivery-graph/dispatch:
    post:
      operationId: dispatchDeliveryGraph
      # x-mcp exclusion (ADR 0067 §2 / nano-ide#488): the canonical operator-only door. Dispatch
      # approval IS a human clicking Dispatch in the cockpit (ADR 0005 Decision 7) — there is no
      # replayable token and no agent-facing dispatch handle, so it is never a projected MCP tool.
      x-mcp:
        exclude: true
      summary: OPERATOR DISPATCH — launch a staged delivery-graph proposal by its digest (idempotent). (ADR 0005 Decision 7 / #460)
      description: >-
        The OPERATOR-ONLY dispatch door (ADR 0005 Decision 7, issue #460). The cockpit's staged-proposals
        grid posts the `digest` of the proposal the operator picked; this door loads that `staged`
        proposal, runs the retained S4 runner for its previewed graph, and marks the proposal
        `dispatched`. The operator clicking Dispatch IS the approval — there is no replayable token. It
        is NOT part of the agent surface: the agent compile door returns no digest-as-dispatch-handle
        (only a navigational preview), so an agent cannot reach a run through the documented surface.
        Idempotent: a re-dispatch of an already-running run short-circuits with `alreadyRunning` instead
        of double-launching. An unknown/expired/superseded/already-dispatched digest is a 400. If the
        supplied `idempotencyKey` is already bound to a DIFFERENT running graph, the door refuses with a
        409 and leaves the proposal staged (it was never launched).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DeliveryGraphDispatchRequest"
      responses:
        "202":
          description: The staged proposal dispatched (or a re-dispatch short-circuited an already-running run).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeliveryGraphTextResult"
        "400":
          description: >-
            The digest was missing, or named no live staged proposal (unknown / expired / superseded /
            already dispatched), or the stored graph failed to compile (the body carries a human `error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeliveryGraphTextResult"
        "409":
          description: >-
            The supplied `idempotencyKey` is already bound to a DIFFERENT running delivery graph, so this
            proposal's graph was NOT launched and the staged proposal was left untouched (not marked
            dispatched). Retry with a fresh `idempotencyKey` (or none). The body carries a human `error`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeliveryGraphTextResult"
  /actions/delivery-graph/dismiss:
    post:
      operationId: dismissProposal
      # x-mcp exclusion (ADR 0067 §2 / nano-ide#488): operator-only cockpit door — discarding a
      # staged proposal as noise is a human decision in the same approval class as dispatch, so it
      # stays off the projected MCP tool surface.
      x-mcp:
        exclude: true
      summary: OPERATOR DISMISS — discard a staged delivery-graph proposal by its digest as noise (idempotent). (#520)
      description: >-
        The OPERATOR-ONLY dismiss door (#520). The cockpit's staged-proposals grid posts the `digest` of
        the proposal the operator wants to discard as noise; this door loads that live `staged` proposal
        and flips it to the terminal `dismissed` status, so it drops out of the staged list — exactly like
        `superseded`/`expired`, but recording a deliberate operator discard rather than a TTL sweep or a
        newer digest landing. It launches nothing. Idempotent: a re-dismiss of an already-terminal
        (dismissed / dispatched / superseded / expired) or unknown digest is a clean 400, leaving state
        untouched.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DeliveryGraphDismissRequest"
      responses:
        "200":
          description: The staged proposal was dismissed; it drops out of the staged list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeliveryGraphTextResult"
        "400":
          description: >-
            The digest was missing, or named no live staged proposal (unknown / already dismissed /
            dispatched / superseded / expired). The body carries a human `error`. Nothing changed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeliveryGraphTextResult"
  /actions/delivery-graph/proposal-bpmn:
    post:
      operationId: previewProposalBpmn
      summary: Recompile a staged delivery-graph proposal's BPMN (with DI) for read-only preview — no deploy, no dispatch.
      description: >-
        Return the compiled BPMN 2.0 XML (including diagram interchange) of a staged delivery-graph
        proposal, so its generated DI can be previewed BEFORE dispatch. The cockpit posts the proposal's
        `digest`; this door loads that `staged` proposal and recompiles its stored graph deterministically
        (the recompiled digest must equal the requested one), returning the BPMN. It NEVER deploys a
        definition or launches an instance — it is a pure read. An unknown / expired / superseded /
        already-dispatched digest, or a graph that no longer compiles, is a clean 400.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              # BEGIN generated:mcp-body source=#/components/schemas/DeliveryGraphProposalBpmnRequest (scripts/inline-mcp-bodies.ts — do not hand-edit)
              description: Request the compiled BPMN of a staged delivery-graph proposal for read-only DI PREVIEW. Carries only the proposal's content `digest`; the door recompiles the staged graph deterministically. No deploy, no dispatch — this reads, it does not launch anything.
              type: object
              additionalProperties: false
              required:
                - digest
              properties:
                digest:
                  type: string
                  description: The staged proposal's content digest — the proposal whose compiled BPMN (with DI) to render.
              # END generated:mcp-body
      responses:
        "200":
          description: The staged proposal recompiled; the body carries its BPMN (with DI).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeliveryGraphProposalBpmnResult"
        "400":
          description: The digest was missing, named no live staged proposal, or the stored graph failed to recompile.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeliveryGraphProposalBpmnResult"
  /delivery-graph/staged:
    get:
      operationId: listStagedProposals
      summary: List the LIVE staged delivery-graph proposals awaiting dispatch (issue #511), newest first.
      description: >-
        The read behind the staged-proposals App-View AND the MCP `listStagedProposals` tool: every
        `staged` delivery-graph proposal that has not aged out of its TTL, newest first, projected to the
        Preview-DI + Dispatch metadata (the `graph`/`preview` payloads are omitted — the App-View
        recompiles by `digest` for the DI preview). Mirrors the
        `previewProposalBpmn`/`dispatchDeliveryGraph` freshness guard so an expired-but-not-yet-swept row
        is never listed. Read-only.


        FRESHNESS / CONSISTENCY (issue #608). This read is READ-AFTER-WRITE consistent with
        `compileDeliveryGraph`: it is served by a fresh query against the SAME durable
        `delivery_graph_proposals` store, in the SAME scope, that the compile/stage write commits to (the
        app's single default data source) — there is no projection, read-model, or cache between the
        write and this read. So a `digest` that `compileDeliveryGraph` just returned as `status: "ready"`
        is guaranteed to appear here on the very next call, with NO intervening delay and no polling
        needed (the row is committed before the compile door responds). The one caveat is scope: the MCP
        endpoint and the cockpit must resolve the SAME app deployment — point the MCP client at the same
        instance/mount whose `reviewUrl` the compile door returned, or the read and the write address
        different databases and disagree. Returns an empty list only when nothing is genuinely staged
        (or every staged row has aged out of its TTL).
      security:
        - hookSecret: []
        - {}
      responses:
        "200":
          description: The live staged proposals.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StagedProposalList"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /delivery-graph/vocabulary:
    get:
      operationId: getDeliveryGraphVocabulary
      summary: The closed delivery-graph vocabulary + wait-probe semantics as structured JSON (ADR 0005).
      description: >-
        Read tool (projected onto the MCP surface like `getAgentInstructions`). Returns the CLOSED
        delivery-graph vocabulary and the non-obvious wait/poll/fact-threading semantics as structured
        JSON, so you can discover them from the surface instead of reading source. Covers: the four node
        kinds (`agent`/`wait`/`human`/`connector`) with their per-kind body contracts; every wait probe
        kind with its `match` fields and — crucially — WHAT it OBSERVES (e.g. `epic` resolves a lineage
        thread by `rootRequestKey` REGARDLESS of the thread's kind, so it gates plan-fanout epics AND
        single-PR feature runs alike, ready on `stage:"merged" && active:false`); which connector targets
        are real (`converge`/`converge-merge`/`merge-main`) vs. forward-declared stubs; the `onTimeout`
        options; the poll-budget rule (always set a realistic `poll.timeoutMs` on merge/epic gates — the
        30-minute default is a trap); and the edge/fact-threading rules (a `node.fact` must be threaded by
        an edge to every consumer, else `unbound-pr`). Derived from the implementing code (a drift test
        fails the build if a probe kind / connector target is added without a vocabulary entry). Pure,
        read-only, idempotent — no side effects. Pairs with `compileDeliveryGraph`/`previewDeliveryGraph`:
        call this first to learn the vocabulary, then author a `DeliveryGraph` and compile it.
      security:
        - hookSecret: []
        - {}
      responses:
        "200":
          description: The full delivery-graph vocabulary.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                description: The closed delivery-graph vocabulary + wait-probe semantics, derived from the compiler/runner code.
                required:
                  - adr
                  - summary
                  - nodeKinds
                  - factTypes
                  - guardScalarTypes
                  - waitProbeKinds
                  - connectorTargets
                  - onTimeout
                  - pollBudget
                  - factThreading
                properties:
                  adr:
                    type: string
                    description: The governing ADR (agent-authored delivery graphs).
                  summary:
                    type: string
                    description: One-paragraph orientation on the graph shape and the propose→compile→stage surface.
                  nodeKinds:
                    type: array
                    description: The closed node-kind allowlist with each kind's config key and body contract.
                    items:
                      type: object
                      additionalProperties: false
                      required: [kind, configKey, requiredFields, optionalFields, sideEffecting, mayEmit, summary]
                      properties:
                        kind:
                          type: string
                          description: The node kind (one of agent | wait | human | connector).
                        configKey:
                          type: string
                          description: The per-kind config object key the node must carry.
                        requiredFields:
                          type: array
                          items: { type: string }
                          description: Required non-empty fields inside the per-kind config.
                        optionalFields:
                          type: array
                          items: { type: string }
                          description: Optional fields inside the per-kind config.
                        sideEffecting:
                          type: boolean
                          description: Whether the node performs a side effect (agent/connector) vs. read-only (wait/human).
                        mayEmit:
                          type: boolean
                          description: Whether the node may declare typed emits.
                        summary:
                          type: string
                          description: The body contract / semantics of the kind.
                  factTypes:
                    type: array
                    items: { type: string }
                    description: The closed emitted-fact type allowlist.
                  guardScalarTypes:
                    type: array
                    items: { type: string }
                    description: The scalar fact types an edge `when` guard may reference.
                  waitProbeKinds:
                    type: array
                    description: Every wait probe kind, its match fields, and what it observes / when it is ready.
                    items:
                      type: object
                      additionalProperties: false
                      required: [kind, target, matchFields, observes, ready]
                      properties:
                        kind:
                          type: string
                          description: The probe kind (http | command | npm | github-check | capability | pr | epic).
                        target:
                          type: string
                          description: What the probe's `target` names.
                        matchFields:
                          type: array
                          items: { type: string }
                          description: The `match` fields this kind reads.
                        conditions:
                          type: array
                          items: { type: string }
                          description: The closed condition set for pr/epic kinds (else absent).
                        observes:
                          type: string
                          description: The read that decides readiness (what the probe actually observes).
                        ready:
                          type: string
                          description: The condition under which the probe reports ready.
                        binds:
                          type: array
                          items: { type: string }
                          description: Output facts the probe binds on a ready match.
                  connectorTargets:
                    type: array
                    description: Which connector targets are real (converge-enrollment) vs. forward-declared stubs.
                    items:
                      type: object
                      additionalProperties: false
                      required: [target, status, summary]
                      properties:
                        target:
                          type: string
                          description: The connector target literal (or a sentinel for any other target).
                        status:
                          type: string
                          enum: [real, forward-declared]
                          description: real ⇒ dispatches a real side effect; forward-declared ⇒ a no-op stub.
                        autoMergeDefault:
                          type: boolean
                          description: The default `autoMerge` for a real converge target.
                        summary:
                          type: string
                          description: What the target does.
                  onTimeout:
                    type: array
                    description: The `onTimeout` options for a bounded wait and what each does.
                    items:
                      type: object
                      additionalProperties: false
                      required: [value, meaning]
                      properties:
                        value: { type: string }
                        meaning: { type: string }
                  pollBudget:
                    type: object
                    additionalProperties: false
                    required: [defaultTimeoutMs, defaultTimeoutIso, defaultEveryMs, rule]
                    description: The poll-budget defaults and the "always set poll.timeoutMs on merge/epic gates" rule.
                    properties:
                      defaultTimeoutMs: { type: number }
                      defaultTimeoutIso: { type: string }
                      defaultEveryMs: { type: number }
                      rule: { type: string }
                  factThreading:
                    type: object
                    additionalProperties: false
                    required: [rule, details]
                    description: The edge/fact-threading rules — a node.fact reaches a consumer only via an edge.
                    properties:
                      rule: { type: string }
                      details:
                        type: array
                        items: { type: string }
                  guideSection:
                    type: string
                    description: The operator-guide section this data mirrors (docs/agent-guide.md §9).
                example:
                  adr: "ADR 0005 — agent-authored delivery graphs"
                  summary: "A delivery graph is a JSON DAG an agent authors as DATA; the surface ends at propose → compile → stage."
                  nodeKinds:
                    - kind: agent
                      configKey: agent
                      requiredFields: [jobType]
                      optionalFields: [prompt, converge, merge]
                      sideEffecting: true
                      mayEmit: true
                      summary: "A worker runs an agent job type; an agent that opens a PR emits it as a `pr` fact."
                    - kind: wait
                      configKey: wait
                      requiredFields: [kind, target]
                      optionalFields: [match, poll, onTimeout, credentialEnv]
                      sideEffecting: false
                      mayEmit: true
                      summary: "A durable, bounded, read-only readiness probe (a ReadinessProbe verbatim)."
                    - kind: connector
                      configKey: connector
                      requiredFields: [target]
                      optionalFields: [dedupeKey, payload]
                      sideEffecting: true
                      mayEmit: true
                      summary: "An automated outbound action; payload for a converge target is { pr, autoMerge?, dependsOn? }."
                  factTypes: [string, number, boolean, artifact, version, url, pr]
                  guardScalarTypes: [string, number, boolean]
                  waitProbeKinds:
                    - kind: pr
                      target: "an owner/repo#N PR (or a <node>.pr fact reference)"
                      matchFields: [prState]
                      conditions: [ready, merged, mergeable, checks-green]
                      observes: "the live GitHub state of one in-flight PR; only OBSERVES, level-triggered."
                      ready: "the PR reaches match.prState (default merged)."
                      binds: [mergedSha]
                    - kind: epic
                      target: "the epic's durable planKey (owner/repo#NN, the epic issue)"
                      matchFields: [epicState]
                      conditions: [merged, done]
                      observes: "the app's lineage read-model, resolved by rootRequestKey REGARDLESS of thread kind (feature | epic | pr | delivery) — so it gates a single-PR FEATURE RUN just as well as a plan-fanout epic."
                      ready: 'the lineage thread reaches stage:"merged" && active:false (every opened slice/PR landed).'
                      binds: [prCount]
                  connectorTargets:
                    - target: converge
                      status: real
                      autoMergeDefault: false
                      summary: "Converge-only: drive review convergence and stop at converged."
                    - target: converge-merge
                      status: real
                      autoMergeDefault: true
                      summary: "Unit-level land: converge AND merge onto the PR's own base branch."
                    - target: merge-main
                      status: real
                      autoMergeDefault: true
                      summary: "Graph-level top-level land onto main (two-level merge)."
                    - target: "<any other target>"
                      status: forward-declared
                      summary: "Forward-declared stub — returns a deterministic acknowledgement, fires no real I/O."
                  onTimeout:
                    - value: escalate
                      meaning: "park a human escalation when the bounded wait elapses."
                    - value: fail
                      meaning: "terminate the gate as failed (NOT yet supported on a wait node — rejected by the compiler)."
                    - value: continue
                      meaning: "proceed as if ready when the wait elapses (a soft gate)."
                  pollBudget:
                    defaultTimeoutMs: 1800000
                    defaultTimeoutIso: PT30M
                    defaultEveryMs: 15000
                    rule: "An omitted poll.timeoutMs inherits the 30-minute default — a trap for wait[pr, merged]/wait[epic] which wait hours/days. Always set poll.timeoutMs explicitly on a merge/epic gate."
                  factThreading:
                    rule: "A node's emitted fact reaches a consumer ONLY via an edge (<nodeId>.<fact>); an unthreaded reference is rejected (unbound-pr)."
                    details:
                      - "The referenced fact must be declared in the producer's emits[] with the right type."
                      - "A connector payload may omit pr to auto-bind the single incoming pr fact."
                  guideSection: "docs/agent-guide.md §9 (Author and run a delivery graph)"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /actions/delivery-graph/library/save:
    post:
      operationId: saveToLibrary
      summary: Save a delivery graph to the reusable library — from a raw graph JSON or an existing proposal digest (issue #522, #519 S3).
      description: >-
        Persist a delivery graph to the reusable LIBRARY (issue #522, epic #519 S3) — the durable base
        S4/S5/S6 build on. The request carries the entry `name` (its slug + short-hash derive the stable
        library id, so re-saving the same name upserts) plus EITHER a raw `graphJson` STRING (validated
        and compiled via the SAME `parseAndCompileText` pipeline the preview/stage doors use, then saved
        with `source: composed`) OR the `digest` of an existing staged/dispatched proposal whose
        already-stored graph is reused (`source: from-staged` / `from-dispatched`). A graph that is not
        valid JSON or fails to compile is a clean 400 and NOTHING is persisted (an uncompilable graph can
        never enter the library). Unlike a staged proposal, a library entry has no TTL. This door is
        INTENTIONALLY UNGUARDED (no shared-secret requirement), unlike the get/delete/import library
        doors: it is also invoked by a DECLARATIVE page row action ("Save to library" on the
        In-flight/History grid) that structurally cannot attach the `x-hook-secret` header, so a guard
        here would make the door unreachable by its own UI. See PR #533 review.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              # BEGIN generated:mcp-body source=#/components/schemas/SaveToLibrarySubmit (scripts/inline-mcp-bodies.ts — do not hand-edit)
              description: 'Save a delivery graph to the reusable library (issue #522). Carries the entry `name` (its slug/hash derive the id) and an optional `description`, PLUS exactly one graph source: either a raw `graphJson` STRING (validated + compiled before persisting, `source: composed`), or the `digest` of an existing staged/dispatched proposal whose already-stored graph is reused (`source: from-staged` / `from-dispatched`). A graph that fails to compile is a clean 400 and nothing is persisted.'
              type: object
              additionalProperties: false
              required:
                - name
              properties:
                name:
                  type: string
                  description: The entry's human name — its slug + short-hash derive the stable library id (re-saving the same name upserts).
                description:
                  type: string
                  description: An optional human note stored alongside the entry.
                graphJson:
                  type: string
                  description: 'A raw `DeliveryGraph` JSON string to validate, compile and save (`source: composed`). Mutually exclusive with `digest`.'
                digest:
                  type: string
                  description: The content digest of an existing staged/dispatched proposal whose stored graph is reused. Mutually exclusive with `graphJson`.
              # END generated:mcp-body
      responses:
        "200":
          description: The graph validated and the entry was saved (upserted on its name-derived id).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SaveToLibraryResult"
        "400":
          description: The request was malformed, the referenced digest named no stored graph, or the graph failed to compile — nothing persisted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SaveToLibraryResult"
  /actions/delivery-graph/library/import:
    post:
      operationId: importToLibrary
      summary: Import a delivery graph FROM A FILE into the reusable library (issue #524, #519 S5).
      description: >-
        Import a delivery graph into the LIBRARY from a filesystem file (issue #524, epic #519 S5). The
        compose App-View's `<input type=file accept=.json>` reads the chosen file's text client-side and
        POSTs it here as the raw `graphJson`. The door validates + compiles it via the SAME
        `parseAndCompileText` pipeline the preview/stage/save doors use, then persists it with
        `source: imported`. A file that is not valid JSON, or a graph that fails to compile, is a clean
        400 with path-qualified errors and NOTHING is persisted. The entry name defaults to the imported
        graph's own `name` (an explicit `name` overrides it). The optional shared-secret guard mirrors the
        get/delete library doors: when NANO_PR_WEBHOOK_SECRET is set, callers must present it via the
        x-hook-secret header (unset → open). (The `save` door is unguarded — it is also reached by a
        declarative page row action that cannot carry the header.)
      security:
        - hookSecret: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              # BEGIN generated:mcp-body source=#/components/schemas/ImportToLibrarySubmit (scripts/inline-mcp-bodies.ts — do not hand-edit)
              description: "Import a delivery graph into the reusable library FROM A FILE (issue #524, epic #519 S5). The compose App-View's `<input type=file accept=.json>` reads the selected file's text client-side and POSTs it here as the raw `graphJson` string. The door validates + compiles it via the SAME `parseAndCompileText` pipeline the preview/stage/save doors use, then persists it with `source: imported`. A file that is not valid JSON, or a graph that fails to compile, is a clean 400 with path-qualified errors and NOTHING is persisted. The entry `name` defaults to the imported graph's own `name`; an explicit `name` overrides it (an unnamed graph with no override is a clean 400)."
              type: object
              additionalProperties: false
              required:
                - graphJson
              properties:
                graphJson:
                  type: string
                  description: The raw `DeliveryGraph` JSON text read from the imported file (validated + compiled before persisting).
                name:
                  type: string
                  description: Optional override for the entry name — its slug + short-hash derive the library id. Defaults to the imported graph's own `name`.
                description:
                  type: string
                  description: An optional human note stored alongside the imported entry.
              # END generated:mcp-body
      responses:
        "200":
          description: The imported graph validated and the entry was saved (source=imported).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SaveToLibraryResult"
        "400":
          description: The file was not valid JSON, carried no usable name, or the graph failed to compile — nothing persisted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SaveToLibraryResult"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SaveToLibraryResult"
  /delivery-graph/library:
    get:
      operationId: listLibrary
      summary: List the saved reusable delivery-graph library entries (issue #522, #519 S3), newest first.
      description: >-
        The read behind the Library App-View (S4/#523): every saved library entry, newest first, with
        its full `graph` JSON so the export affordance (S6/#525) can build a client-side download from
        the list payload. Read-only. The optional shared-secret guard mirrors the other read doors
        (getLineage / listActivePrs / listStagedProposals): when NANO_PR_WEBHOOK_SECRET is set, callers
        must present it via the x-hook-secret header.
      security:
        - hookSecret: []
        - {}
      responses:
        "200":
          description: The saved library entries, newest first.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeliveryGraphLibraryList"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /delivery-graph/library/{id}:
    get:
      operationId: getLibraryEntry
      summary: Fetch one saved library entry by id (issue #522, #519 S3).
      description: >-
        Return one saved library entry (issue #522) by its `id`, including its full `graph` JSON. An
        unknown id is a clean 404. The optional shared-secret guard mirrors the other read doors.
      security:
        - hookSecret: []
        - {}
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The library entry's id (`<slug>-<sha256(name)[:8]>`).
      responses:
        "200":
          description: The library entry.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeliveryGraphLibraryEntry"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "404":
          description: No library entry exists for the given id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
    delete:
      operationId: deleteLibraryEntry
      summary: Delete one saved library entry by id (issue #522, #519 S3).
      description: >-
        Delete one saved library entry (issue #522) by its `id`. Idempotent — deleting an id that names
        no entry returns `deleted: false` (a clean no-op), not an error. The optional shared-secret guard
        mirrors the other write doors.
      security:
        - hookSecret: []
        - {}
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The library entry's id to delete.
      responses:
        "200":
          description: The delete was processed (see `deleted` for whether a row was actually removed).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteLibraryEntryResult"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /actions/start/feature:
    post:
      operationId: startFeature
      summary: Register/refresh a feature-run aggregate (idempotent on featureKey) and start the single-issue run.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              # BEGIN generated:mcp-body source=#/components/schemas/FeatureStart (scripts/inline-mcp-bodies.ts — do not hand-edit)
              description: The start-feature request body — a SINGLE-issue feature run. Names the target issue by EXACTLY ONE of `issue` (an `owner/repo#123` reference) or `url` (a bare issue URL), plus a REQUIRED `baseBranch` (ADR 0003, same admission as the epic path), and the two optional follow-on knobs `converge` / `autoMerge`. May also carry an intake-time readiness gate (`readiness` and/or `blockedOn` + `consumerPackage`, per issue 295) that parks the run until the declared upstreams land. Modeled as `oneOf` named variants (Camunda REST v2 pattern) so an ambiguous or empty target is a 400 at the edge.
              oneOf:
                - type: object
                  additionalProperties: false
                  required:
                    - issue
                    - baseBranch
                  properties:
                    issue:
                      type: string
                      description: 'Issue reference: owner/repo#123.'
                    baseBranch:
                      type: string
                      minLength: 1
                      maxLength: 255
                      pattern: \S
                      description: 'REQUIRED target branch the agent branches off and opens its PR against. Admitted through the same ADR 0003 policy as the epic path (`admitPlan`): a missing `epic/*` base is auto-created off default HEAD; a non-`epic/*` base must already exist; naming the default branch requires `confirmDefaultBase`.'
                    converge:
                      type: boolean
                      description: When true, hand the opened PR to the convergence loop (review rounds) as a follow-on step. When false (default) the run ends at "PR raised, not reviewed".
                    autoMerge:
                      type: boolean
                      description: When true (and `converge` is true), drive the merge-loop after convergence only when `NANO_PR_AUTO_MERGE` is enabled. When false the run stops at `converged`. Moot when `converge` is false.
                    confirmDefaultBase:
                      type: boolean
                      description: Acknowledge that `baseBranch` is the repository default branch (the PR would target it directly). See `PlanStartByIssue.confirmDefaultBase`.
                    allowSharedBase:
                      type: boolean
                      description: Opt in to sharing a custom integration base branch with another already-active epic. See `PlanStartByIssue.allowSharedBase`.
                    customInstructions:
                      type: string
                      maxLength: 8000
                      description: OPTIONAL free-text steering appended to the implementation agent's prompt for this run (via the implement task's `appendPrompt`). Blank/whitespace is treated as absent. Persists on the instance, so it also applies to the agent's answer-loop redispatch.
                    readiness:
                      type: array
                      maxItems: 32
                      items:
                        description: 'A single durable readiness probe (issue #258, #295) the feature run must satisfy before its implementation agent is dispatched. `kind` selects the source; `target` + `match` are the per-kind predicate. The `capability` kind resolves "which published `pkg@version` first carries capability C?" from publish provenance and late-binds it into the run. See `app/readiness.ts` for the full per-kind semantics.'
                        type: object
                        additionalProperties: false
                        required:
                          - kind
                          - target
                        properties:
                          kind:
                            type: string
                            enum:
                              - http
                              - command
                              - npm
                              - github-check
                              - capability
                              - pr
                              - epic
                            description: The readiness source. `command` is the escape hatch; `capability` resolves a cross-repo published-artifact edge; `pr` watches an in-flight PR's merge state (ADR 0005 §2); `epic` gates on an nwf plan-fanout epic reaching "fully merged", keyed by its `planKey` (issue
                          target:
                            type: string
                            minLength: 1
                            description: The kind-specific target (a URL, a shell command, a `pkg@version`, an `owner/repo@ref`, `github-releases:owner/repo`, an `owner/repo#123` PR reference for the `pr` kind, or an `owner/repo#NN` planKey — the epic issue — for the `epic` kind). For a `pr`/`epic` target used in a **delivery-graph `wait` node** it may instead be a `<nodeId>.<fact>` late-binding reference the delivery-graph compiler resolves at dispatch (issue
                          onTimeout:
                            type: string
                            enum:
                              - escalate
                              - fail
                              - continue
                            description: What the gate does when the bounded wait elapses (default `escalate`).
                          credentialEnv:
                            type: string
                            description: A declared env-contract key supplying a credential (http kind only). Names a key, never a secret value.
                          match:
                            type: object
                            additionalProperties: false
                            description: The per-kind readiness predicate; every field is optional and read only by the kinds that understand it.
                            properties:
                              status:
                                type: integer
                                description: 'http: the exact status that means ready (default any 2xx).'
                              bodyIncludes:
                                type: string
                                description: 'http: a substring the response body must contain.'
                              exitCode:
                                type: integer
                                description: 'command: the exit code that means ready (default 0).'
                              stdoutIncludes:
                                type: string
                                description: 'command/npm: a substring stdout must contain.'
                              version:
                                type: string
                                description: 'npm: the version that must be published.'
                              conclusion:
                                type: string
                                description: 'github-check: the conclusion that means ready (default success).'
                              checkName:
                                type: string
                                description: 'github-check: restrict to the named check run.'
                              capabilityRef:
                                type: string
                                description: 'capability: the upstream issue/PR handle the resolved version must carry.'
                              package:
                                type: string
                                description: 'capability: the package whose releases are scanned for provenance.'
                              verifyCommand:
                                type: string
                                description: 'capability: optional empirical verifier run once at the gate boundary.'
                              prState:
                                type: string
                                enum:
                                  - ready
                                  - merged
                                  - mergeable
                                  - checks-green
                                description: 'pr: the declared PR state to wait for (default merged).'
                              epicState:
                                type: string
                                enum:
                                  - merged
                                  - done
                                description: "epic: the declared plan-fanout aggregate state to wait for (default merged) — both mean 'fully merged' (issue #568)."
                          poll:
                            type: object
                            additionalProperties: false
                            description: The poll cadence (how often to re-probe, how long to keep trying, and the backoff shape).
                            properties:
                              everyMs:
                                type: integer
                                description: Interval between poll attempts (ms).
                              timeoutMs:
                                type: integer
                                description: Bounded budget (ms) before the gate escalates.
                              backoff:
                                type: string
                                enum:
                                  - fixed
                                  - exponential
                                description: Backoff shape between attempts.
                      description: "OPTIONAL intake-time readiness gate (issue #295): one or more durable probes the run must ALL satisfy before its implementation agent is dispatched. The run parks (durably, bounded by the gate's escalating timer) at the leading readiness preflight until every probe goes green. Absent/empty ⇒ the run implements immediately, unchanged."
                    blockedOn:
                      type: array
                      maxItems: 32
                      items:
                        type: string
                        minLength: 1
                      description: 'OPTIONAL ergonomic shorthand for `readiness` (issue #295): a list of upstream `owner/repo#123` issue/PR handles the run waits to land. With `consumerPackage` each desugars to a `capability` probe (resolve which published `pkg@version` first carries the handle, and late-bind it into the run); without it, to a `command` probe that goes green once the referenced issue/PR is closed/merged.'
                    consumerPackage:
                      type: string
                      minLength: 1
                      description: OPTIONAL npm package name (e.g. `@nanobpm/engine-wasm`) the `blockedOn` shorthand resolves its handles against — the consumer dependency whose published provenance must carry each awaited upstream. When present, `blockedOn` desugars to `capability` probes and the resolved `pkg@version` is late-bound into the implementation agent's brief.
                - type: object
                  additionalProperties: false
                  required:
                    - url
                    - baseBranch
                  properties:
                    url:
                      type: string
                      description: A bare issue URL, when no `owner/repo#123` reference is supplied.
                    baseBranch:
                      type: string
                      minLength: 1
                      maxLength: 255
                      pattern: \S
                      description: REQUIRED target branch the agent branches off and opens its PR against. See `FeatureStartByIssue.baseBranch`.
                    converge:
                      type: boolean
                      description: When true, hand the opened PR to the convergence loop. See `FeatureStartByIssue.converge`.
                    autoMerge:
                      type: boolean
                      description: When true (with `converge`), drive the merge-loop. See `FeatureStartByIssue.autoMerge`.
                    confirmDefaultBase:
                      type: boolean
                      description: Acknowledge landing on the default branch. See `PlanStartByIssue.confirmDefaultBase`.
                    allowSharedBase:
                      type: boolean
                      description: Share a custom integration base with another active epic. See `PlanStartByIssue.allowSharedBase`.
                    customInstructions:
                      type: string
                      maxLength: 8000
                      description: OPTIONAL free-text steering appended to the implementation agent's prompt for this run. See `FeatureStartByIssue.customInstructions`.
                    readiness:
                      type: array
                      maxItems: 32
                      items:
                        description: 'A single durable readiness probe (issue #258, #295) the feature run must satisfy before its implementation agent is dispatched. `kind` selects the source; `target` + `match` are the per-kind predicate. The `capability` kind resolves "which published `pkg@version` first carries capability C?" from publish provenance and late-binds it into the run. See `app/readiness.ts` for the full per-kind semantics.'
                        type: object
                        additionalProperties: false
                        required:
                          - kind
                          - target
                        properties:
                          kind:
                            type: string
                            enum:
                              - http
                              - command
                              - npm
                              - github-check
                              - capability
                              - pr
                              - epic
                            description: The readiness source. `command` is the escape hatch; `capability` resolves a cross-repo published-artifact edge; `pr` watches an in-flight PR's merge state (ADR 0005 §2); `epic` gates on an nwf plan-fanout epic reaching "fully merged", keyed by its `planKey` (issue
                          target:
                            type: string
                            minLength: 1
                            description: The kind-specific target (a URL, a shell command, a `pkg@version`, an `owner/repo@ref`, `github-releases:owner/repo`, an `owner/repo#123` PR reference for the `pr` kind, or an `owner/repo#NN` planKey — the epic issue — for the `epic` kind). For a `pr`/`epic` target used in a **delivery-graph `wait` node** it may instead be a `<nodeId>.<fact>` late-binding reference the delivery-graph compiler resolves at dispatch (issue
                          onTimeout:
                            type: string
                            enum:
                              - escalate
                              - fail
                              - continue
                            description: What the gate does when the bounded wait elapses (default `escalate`).
                          credentialEnv:
                            type: string
                            description: A declared env-contract key supplying a credential (http kind only). Names a key, never a secret value.
                          match:
                            type: object
                            additionalProperties: false
                            description: The per-kind readiness predicate; every field is optional and read only by the kinds that understand it.
                            properties:
                              status:
                                type: integer
                                description: 'http: the exact status that means ready (default any 2xx).'
                              bodyIncludes:
                                type: string
                                description: 'http: a substring the response body must contain.'
                              exitCode:
                                type: integer
                                description: 'command: the exit code that means ready (default 0).'
                              stdoutIncludes:
                                type: string
                                description: 'command/npm: a substring stdout must contain.'
                              version:
                                type: string
                                description: 'npm: the version that must be published.'
                              conclusion:
                                type: string
                                description: 'github-check: the conclusion that means ready (default success).'
                              checkName:
                                type: string
                                description: 'github-check: restrict to the named check run.'
                              capabilityRef:
                                type: string
                                description: 'capability: the upstream issue/PR handle the resolved version must carry.'
                              package:
                                type: string
                                description: 'capability: the package whose releases are scanned for provenance.'
                              verifyCommand:
                                type: string
                                description: 'capability: optional empirical verifier run once at the gate boundary.'
                              prState:
                                type: string
                                enum:
                                  - ready
                                  - merged
                                  - mergeable
                                  - checks-green
                                description: 'pr: the declared PR state to wait for (default merged).'
                              epicState:
                                type: string
                                enum:
                                  - merged
                                  - done
                                description: "epic: the declared plan-fanout aggregate state to wait for (default merged) — both mean 'fully merged' (issue #568)."
                          poll:
                            type: object
                            additionalProperties: false
                            description: The poll cadence (how often to re-probe, how long to keep trying, and the backoff shape).
                            properties:
                              everyMs:
                                type: integer
                                description: Interval between poll attempts (ms).
                              timeoutMs:
                                type: integer
                                description: Bounded budget (ms) before the gate escalates.
                              backoff:
                                type: string
                                enum:
                                  - fixed
                                  - exponential
                                description: Backoff shape between attempts.
                      description: OPTIONAL intake-time readiness gate. See `FeatureStartByIssue.readiness`.
                    blockedOn:
                      type: array
                      maxItems: 32
                      items:
                        type: string
                        minLength: 1
                      description: OPTIONAL readiness shorthand — upstream `owner/repo#123` handles to wait on. See `FeatureStartByIssue.blockedOn`.
                    consumerPackage:
                      type: string
                      minLength: 1
                      description: OPTIONAL package the `blockedOn` handles resolve against. See `FeatureStartByIssue.consumerPackage`.
              type: object
              # END generated:mcp-body
      responses:
        "202":
          description: The feature run was started (or was already running).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StartFeatureResult"
        "400":
          description: The issue reference could not be parsed, or the base branch was rejected.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "409":
          description: The base branch is already in use by another active epic.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "502":
          description: >-
            The engine accepted the start but returned no instance key — nothing was dispatched (issue
            #704). A submit that starts no instance is surfaced here as a distinct non-success, never a
            green 202.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /actions/message:
    post:
      operationId: postMessage
      summary: "Publish a BPMN message (optionally correlated) into the engine. Every escalation kind
        (task, plan-review, trial-merge, PR review-loop, PR merge-loop) is a native user task answered
        via the Tasks inbox / POST /actions/complete-user-task, not this route."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              required:
                - name
              properties:
                name:
                  type: string
                  minLength: 1
                  description: The message name (correlates a waiting event).
                correlationKey:
                  type: string
                variables:
                  type: object
                  additionalProperties: true
                  properties:
                    answer:
                      type: string
                      description: The operator's answer that resumes a parked merge-loop escalation.
      responses:
        "200":
          description: The message was published (or the escalation answered).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "400":
          description: A required field was missing/blank.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "404":
          description: No matching open escalation / parked token to answer.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
  /actions/complete-user-task:
    post:
      operationId: completeUserTask
      summary: "Complete an open native user-task escalation from the nwf Tasks inbox (issue #236).
        Submits the parked task's typed `.form` variables (e.g. a plan-review `{ directive, notes }`, a
        trial-merge `{ action, notes }`, a PR `{ answer }`, a feature escalation `{ resolution, answer }`,
        or a blocked-run `{ note }`) to the canonical human completer (completeEscalationAsHuman →
        completeUserTaskAttributed) — the same resume path the task inbox uses, recording who answered.
        The completer refuses any user task that is not one of the human-completable elements. Issue #332
        retired the bespoke feature-run doors (answer-escalation / acknowledge-blocked) and folded
        `feature-escalation` / `feature-blocked` onto this one canonical door. On success the answered
        task's read-model row is dropped so the Tasks grid stops offering a decision for it."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - userTaskKey
                - variables
              properties:
                userTaskKey:
                  type: string
                  minLength: 1
                  description: The parked escalation user-task key (user_tasks.user_task_key).
                variables:
                  type: object
                  additionalProperties: true
                  description: The typed form variables the parked task's `.form` expects (kind-specific).
                operator:
                  type: string
                  description: Optional operator handle recorded in the attribution ledger; defaults to "operator".
      responses:
        "200":
          description: The escalation user task was completed and the process resumed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "400":
          description: A required field was missing/invalid, or the target is not an escalation task.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "404":
          description: No open escalation user task matches the userTaskKey.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
  /actions/acknowledge-done:
    post:
      operationId: acknowledgeDone
      summary: "Tick off a TERMINAL feature run (issue #254 §5). Stamps `acknowledged_at` on the run so
        the gateway recomputes its `list_bucket` to 'history', dropping the finished run out of the
        primary Active list into History. The Done twin of the blocked acknowledgement, but a terminal run is
        not parked at a user task, so this completes no user task and only writes the row. Keyed on the
        run's `feature_key`; idempotent-safe (re-acknowledging keeps it in History)."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - feature_key
              properties:
                feature_key:
                  type: string
                  minLength: 1
                  description: The feature run's key (feature_runs.feature_key, `<owner>/<repo>#<n>`).
      responses:
        "200":
          description: The terminal run was acknowledged and moved to History.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "400":
          description: A required field (feature_key) was missing or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "404":
          description: No feature run matches the feature_key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "409":
          description: The feature run is not terminal, so it cannot be ticked off yet.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
  /actions/acknowledge-epic:
    post:
      operationId: acknowledgeEpic
      summary: "Dismiss a RESOLVED epic (issue #298). Stamps `acknowledged_at` on the `plans` row so
        the gateway recomputes its `list_bucket` to 'history' (and `ack_open` to 0), dropping the
        resolved epic out of the Active epic list into History. The epic twin of acknowledge-done, but
        a resolved epic is not parked at a user task, so this completes no user task and only writes the
        row. Keyed on the epic's `plan_key`. Rejects (409) an epic that is not yet resolved (still
        `planning`/`dispatched`, or `done` but still `converging`), so a converging epic stays visible
        in Active. Idempotent-safe (re-acknowledging keeps it in History)."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - plan_key
              properties:
                plan_key:
                  type: string
                  minLength: 1
                  description: The epic's key (plans.plan_key, `<owner>/<repo>#<n>`).
      responses:
        "200":
          description: The resolved epic was acknowledged and moved to History. Applies to any
            resolved epic — whether all slices merged (`delivery=landed`) or it resolved-not-landed
            (`delivery=null`), not only landed epics.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "400":
          description: A required field (plan_key) was missing or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "404":
          description: No epic matches the plan_key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "409":
          description: The epic is not yet resolved (still planning/dispatched, or converging), so it cannot be dismissed yet.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
  /actions/acknowledge-pr:
    post:
      operationId: acknowledgePr
      summary: "Dismiss a TERMINAL pull request (issue #641). Stamps `acknowledged_at` on the
        `pull_requests` row so the `pull_requests_read_model` VIEW recomputes its `list_bucket` to
        'history' (and `ack_open` to 0), dropping the finished PR out of the Active convergence list
        into History. The PR twin of acknowledge-done/acknowledge-epic: a PR is not parked at a user
        task, so this completes no user task and touches no engine/ledger — it only stamps the row.
        Keyed on the PR's `pr_key`. Rejects (409) a PR that is not yet terminal (still converging), so a
        live PR stays visible in Active. Idempotent-safe (re-acknowledging keeps it in History)."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - pr_key
              properties:
                pr_key:
                  type: string
                  minLength: 1
                  description: The pull request's key (pull_requests.pr_key).
      responses:
        "200":
          description: The terminal PR was acknowledged and moved to History.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "400":
          description: A required field (pr_key) was missing or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "404":
          description: No pull request matches the pr_key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "409":
          description: The pull request is not terminal, so it cannot be dismissed yet.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
  /actions/acknowledge-delivery-graph:
    post:
      operationId: acknowledgeDeliveryGraph
      summary: "Dismiss a TERMINAL delivery-graph run (issue #641). Stamps `acknowledged_at` on the
        `delivery_graph_runs` row so the `delivery_graph_read_model` VIEW recomputes its `list_bucket`
        to 'history' (and `ack_open` to 0), dropping the finished run out of the Active delivery-graph
        list into History. The delivery-graph twin of acknowledge-done/acknowledge-epic/acknowledge-pr:
        it completes no user task and touches no engine/ledger — it only stamps the row. Keyed on the
        run's `run_key`. Rejects (409) a run that is not yet terminal (still awaiting-approval/running),
        so a live run stays visible in Active. Idempotent-safe (re-acknowledging keeps it in History)."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - run_key
              properties:
                run_key:
                  type: string
                  minLength: 1
                  description: The delivery-graph run's key (delivery_graph_runs.run_key).
      responses:
        "200":
          description: The terminal delivery-graph run was acknowledged and moved to History.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "400":
          description: A required field (run_key) was missing or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "404":
          description: No delivery-graph run matches the run_key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "409":
          description: The delivery-graph run is not terminal, so it cannot be dismissed yet.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
  /hooks/agent-complete:
    post:
      operationId: agentCompleteEscalation
      summary: "Complete an escalation user task as an agent assignee (ADR 0046). Reuses the same
        `.form` contract + engine resume path a human uses; records the agent identity for
        attribution. Optional shared-secret guard (x-hook-secret), enforced only when
        NANO_PR_WEBHOOK_SECRET is set."
      security:
        - hookSecret: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              # BEGIN generated:mcp-body source=#/components/schemas/AgentCompleteRequest (scripts/inline-mcp-bodies.ts — do not hand-edit)
              type: object
              additionalProperties: false
              description: "Complete an escalation user task AS AN AGENT (epic #156 / ADR 0046). The agent submits the SAME typed form variables a human would through the task inbox; the host routes them through the one canonical attributed completer, records the agent's identity for the audit trail, and resumes the process. Only the migrated escalation user tasks may be completed this way."
              required:
                - userTaskKey
                - agentId
                - variables
              properties:
                userTaskKey:
                  type: string
                  minLength: 1
                  description: The engine user-task key of the parked escalation task (from GET /tasks/api/tasks).
                agentId:
                  type: string
                  minLength: 1
                  description: The completing agent's identity (ADR 0046), recorded for attribution.
                variables:
                  type: object
                  additionalProperties: true
                  description: The typed form completion variables — the SAME shape a human submits (e.g. `{ resolution, answer }`, `{ directive, notes }`, `{ action, notes }`, or `{ answer }`).
              # END generated:mcp-body
      responses:
        "200":
          description: The agent completed the escalation task and the process resumed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "400":
          description: A required field was missing (userTaskKey, agentId, variables), or the target is not an escalation task.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "404":
          description: No open escalation user task matches the userTaskKey.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
  /hooks/revert-completion:
    post:
      operationId: revertEscalationCompletion
      summary: "Revert/override an agent escalation completion (ADR 0046 reversibility). A human marks
        a reversible agent completion reverted so the agent's answer is no longer authoritative.
        Optional shared-secret guard (x-hook-secret), enforced only when NANO_PR_WEBHOOK_SECRET is
        set."
      security:
        - hookSecret: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              # BEGIN generated:mcp-body source=#/components/schemas/RevertCompletionRequest (scripts/inline-mcp-bodies.ts — do not hand-edit)
              type: object
              additionalProperties: false
              description: Revert/override an agent escalation completion (the reversibility guarantee of ADR 0046). A human marks a reversible agent completion reverted so the agent's answer is no longer treated as authoritative; the reverter's identity + timestamp are recorded.
              required:
                - completionId
                - reverterId
              properties:
                completionId:
                  type: integer
                  description: The `task_completions` id returned when the agent completion was recorded.
                reverterId:
                  type: string
                  minLength: 1
                  description: The human identity overriding the agent completion, recorded for the audit trail.
                note:
                  type: string
                  description: Optional corrective guidance from the human that overrides the agent's answer.
              # END generated:mcp-body
      responses:
        "200":
          description: The agent completion was reverted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "400":
          description: A required field was missing, or the completion is not reversible / already reverted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "401":
          description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
        "404":
          description: No completion matches the completionId.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResult"
  /hooks/blackboard:
    get:
      operationId: readBlackboard
      summary: Read a plan's blackboard, scoped by the capability token (Tier 1). Optional ?since=<id>
        returns only newer entries.
      parameters:
        - name: token
          in: query
          required: false
          schema:
            type: string
          description: "Per-plan capability token (issues #51 / #49) — the token IS the credential and
            scopes the read to exactly one plan. Required in practice, but declared optional here
            because it may instead be sent as the `x-blackboard-token` header; the delegate enforces
            it (400 when absent from both, 404 when unknown). Not an OpenAPI `security` scheme: it is
            a per-plan DB-issued capability, not a fixed shared secret the runtime could verify."
        - name: since
          in: query
          required: false
          schema:
            type: integer
          description: Return only entries with id > since (incremental poll).
      responses:
        "200":
          description: The blackboard page for this plan.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BlackboardPage"
        "400":
          description: Missing blackboard token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "404":
          description: Unknown blackboard token (never reveals which plans exist).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
    post:
      operationId: appendBlackboard
      summary: Append one entry to a plan's blackboard, scoped by the capability token (Tier 1).
        Idempotent on (plan, dedupe_key).
      parameters:
        - name: token
          in: query
          required: false
          schema:
            type: string
          description: "Per-plan capability token (issues #51 / #49) — the token IS the credential and
            scopes the write to exactly one plan. Required in practice, but declared optional here
            because it may instead be sent as the `x-blackboard-token` header; the delegate enforces
            it (400 when absent from both, 404 when unknown). Not an OpenAPI `security` scheme: it is
            a per-plan DB-issued capability, not a fixed shared secret the runtime could verify."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              # BEGIN generated:mcp-body source=#/components/schemas/BlackboardAppendRequest (scripts/inline-mcp-bodies.ts — do not hand-edit)
              type: object
              description: Append one blackboard entry. Idempotent on (plan, dedupe_key).
              required:
                - body
              properties:
                author_task:
                  type: string
                  description: The authoring task id; defaults to "system" when omitted/blank.
                kind:
                  type: string
                  description: Entry kind (e.g. note, file-claim); normalized by the delegate.
                files:
                  type: array
                  items:
                    type: string
                  description: Files this entry concerns (used by advisory file-claim conflict detection).
                body:
                  type: string
                  minLength: 1
                  description: The note text (required, non-blank).
                wave:
                  type: integer
                  description: Optional plan wave the entry belongs to.
                dedupe_key:
                  type: string
                  description: 'Idempotency key: a retry with the same (plan, dedupe_key) returns the existing entry.'
              # END generated:mcp-body
      responses:
        "200":
          description: "An idempotent retry: the entry already existed (inserted=false)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BlackboardAppendResult"
        "201":
          description: The entry was appended.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BlackboardAppendResult"
        "400":
          description: Missing blackboard token or blank body.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "404":
          description: Unknown blackboard token (never reveals which plans exist).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /hooks/abandon:
    get:
      operationId: checkAbandon
      summary: "Cooperative abandon check (issue #76): tell a running agent whether its run was cancelled.
        Scoped by the per-PR capability token."
      parameters:
        - name: token
          in: query
          required: false
          schema:
            type: string
          description: "Per-PR capability token (issue #76) — the token IS the credential and scopes the
            read to exactly one PR. Required in practice, but declared optional here because it may
            instead be sent as the `x-abandon-token` header; the delegate enforces it (400 when absent
            from both, 404 when unknown). Not an OpenAPI `security` scheme: it is a per-PR DB-issued
            capability, not a fixed shared secret the runtime could verify."
      responses:
        "200":
          description: The PR's abandon status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AbandonStatus"
        "400":
          description: Missing abandon token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "404":
          description: Unknown abandon token (never reveals which PRs exist).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
