API REFERENCE

API reference

Rendered from canonical revision 2ccb1710c71144e239606b5a43536f70f2582714.

openapi: 3.1.0
info:
  title: Remudero Daemon Service Surface
  version: 0.1.0
  description: >
    The daemon's ONE tailnet service surface (MASTER-PLAN §7A): REST + SSE,
    single port, bearer-scoped (read vs. write). No client gets a private
    backdoor -- packages/api-client is GENERATED from this document (see
    scripts/generate-api-client.mjs) and is the ONLY way any client talks to
    the daemon.


    v0 (W3-T1a, src/lib/service.ts) shipped the generic mechanism only: bearer
    read/write scope enforcement, a shared JSON error envelope, and SSE
    framing, with `paths` intentionally empty -- no business route yet.

    W3-T2 (MASTER-PLAN §7, WS-5a -- the read-only live board) adds the first
    two: GET /v1/status (task/ledger-state snapshot) and GET /v1/status/stream
    (its SSE live-update companion), built on that same mechanism via
    src/lib/board.ts.

    W3-T5 (MASTER-PLAN §7 -- human-in-the-loop panel actions) adds the first
    write routes: POST /v1/control/{pause,resume,stop},
    POST /v1/questions/answer, and POST /v1/manual/approve, all write-scoped
    and built via src/lib/panel-actions.ts.

    W3-T6 (MASTER-PLAN §7B -- the plan->task->PR graph + interactive plan
    adjustment) adds GET /v1/feedback (the inbox), POST /v1/feedback (submit
    feedback / answer a grill, origin=ui), GET /v1/trace (the plan->task->PR
    provenance graph, W1-T43), and POST /v1/feedback/decision (accept/reject a
    proposal), all built via src/lib/panel-graph.ts.

    W3-T8 (MASTER-PLAN §5B/§7 -- panel skill actions) adds GET /v1/skills: the
    panel's action-button set, resolved fresh from `.remudero/skills/<name>.yaml`
    (the SAME registry `rmd skill list` reads, W1-T44) via src/lib/panel-skills.ts
    -- one button per registry entry, generated, never hard-coded. POST
    /v1/skills/run (src/lib/panel-skill-run.ts) invokes one: today `{ skill:
    "plan", mode: "clarify" }` (Refine) is wired, grounding via the real §5C task
    linter and parking a `grilling` feedback entry the existing GET/POST
    /v1/feedback (W3-T6) already renders inline and answers.

    Every route the daemon's OTHER surfaces still need (re-prioritize, ...) is
    registered by later tasks the same way -- this document is additive, never
    rewritten wholesale.

servers:
  - url: "https://{tailnetHost}"
    description: The daemon over Tailscale, one port for REST and SSE alike.
    variables:
      tailnetHost:
        default: localhost:4317

components:
  securitySchemes:
    bearerRead:
      type: http
      scheme: bearer
      description: >
        Read-scoped bearer token. Grants GET access to read-scoped routes and
        SSE streams. A write-scoped token also satisfies this scope (write is
        a superset of read).
    bearerWrite:
      type: http
      scheme: bearer
      description: >
        Write-scoped bearer token. Required for any route whose `scope` is
        `write` (src/lib/service.ts's `Scope`).

  schemas:
    Error:
      type: object
      description: >
        The JSON error envelope every non-2xx response on the surface
        returns (src/lib/service.ts's `sendJson` error paths).
      required:
        - error
      properties:
        error:
          type: string
          description: >
            `unauthorized` (401, no/unrecognized bearer token), `forbidden`
            (403, recognized token missing the required scope), `not_found`
            (404, no route registered for this method + path),
            `invalid_request` (400, a write route's JSON body failed
            validation -- W3-T5's panel-action routes fail loud BEFORE any
            side effect, src/lib/panel-actions.ts's `jsonAction`), or
            `internal_error` (500, the route handler threw).
          enum:
            - unauthorized
            - forbidden
            - not_found
            - invalid_request
            - internal_error
        required_scope:
          type: string
          description: Present only on a 403 -- the scope the caller's token was missing.
          enum:
            - read
            - write
        required_tier:
          type: string
          description: >
            W1-T404 -- present only on a 403 refused for an insufficient WRITE TIER (once
            src/lib/service.ts's `enforceWriteTiers` is turned on; not yet set by `rmd serve`'s
            own production wiring). `low` (bookkeeping), `middle` (reversible but disruptive,
            or a spend force multiplier) or `high` (spends money or moves code) -- the tier the
            caller's token was missing, alongside `required_scope: write`.
          enum:
            - low
            - middle
            - high

    StatusProjection:
      type: object
      description: >
        One task's projected merge-state, derived from GitHub (src/lib/status.ts's
        `StatusProjection` -- never written back to plan/tasks.yaml). This is the
        per-task "live state" the read-only board (W3-T2) renders.
      required:
        - taskId
        - status
        - merged
        - source
      properties:
        taskId:
          type: string
          description: The plan task id (plan/tasks.yaml's `id`).
        status:
          type: string
          description: Derived status label in the plan's vocabulary (src/lib/plan.ts's TaskStatus).
          enum:
            - queued
            - recon
            - prompted
            - running
            - review
            - fixing
            - diagnosing
            - blocked
            - merged
            - done
        merged:
          type: boolean
          description: The single fact dependency-gating cares about -- has this task landed?
        source:
          type: string
          description: Which precedence source resolved this projection (`none` if GitHub was read but had no evidence; `throttled` if GitHub could not be read at all).
          enum:
            - ledger
            - pr-field
            - trailer
            - correction
            - none
            - throttled
        prNumber:
          type: number
        prUrl:
          type: string
        prState:
          type: string
        rejected_candidates:
          type: array
          description: >
            Trailer search hits rejected by the ownership/anchor asserts, each with a
            machine-readable reason. Present only when a candidate was actually rejected.
          items:
            type: object
            required:
              - pr
              - reason
            properties:
              pr:
                type: string
              reason:
                type: string
        phase:
          type: string
          enum: [recon, implement, review, fix-rung]
        startedAt:
          type: string
          format: date-time
        elapsedMs:
          type: number
          minimum: 0
        workerState:
          type: string
          enum: [working, tool-executing, quiet]
        workerStateSince:
          type: string
          format: date-time
        workerTelemetry:
          type: object
          description: >
            Bounded worker observability. It names the active role/provider/model and the
            current or last tool, but never carries prompts, tool arguments, or tool output.
          properties:
            role:
              type: string
              enum: [recon, implementer, reviewer, fixer, triage, retro, unknown]
            provider:
              type: string
            requestedModel:
              type: string
            servedModel:
              type: string
            currentTool:
              type: string
            currentToolReason:
              type: string
            currentToolStartedAt:
              type: string
              format: date-time
            lastTool:
              type: object
              required: [name, durationMs, completedAt]
              properties:
                name:
                  type: string
                durationMs:
                  type: number
                  minimum: 0
                completedAt:
                  type: string
                  format: date-time
                outcome:
                  type: string
                  enum: [success, error]
            lastEventAt:
              type: string
              format: date-time
            lastEventKind:
              type: string
              enum: [working, tool-executing, message]
            firstSignalAt:
              type: string
              format: date-time
            firstSignalLatencyMs:
              type: number
              minimum: 0

    StatusSnapshot:
      type: object
      description: >
        GET /v1/status's body -- one StatusProjection per plan task, as of `generated_at`, plus
        (W1-T163, when the daemon's per-token last-seen marker store is wired) the calling
        token's own "since you last checked" recap.
      required:
        - generated_at
        - tasks
      properties:
        generated_at:
          type: string
        tasks:
          type: array
          items:
            $ref: "#/components/schemas/StatusProjection"
        recap:
          type: array
          description: >
            W1-T163: every recap-worthy event (src/lib/recap.ts) after this token's PRIOR
            marker -- absent entirely on a daemon with no last-seen store wired; `[]` on this
            token's first-ever view (nothing to recap FROM).
          items:
            $ref: "#/components/schemas/RecapEvent"
        sinceCheckpoint:
          type: string
          description: >
            W1-T163: this token's marker value BEFORE this request advanced it -- the timestamp
            `recap` was computed as-of. Absent alongside `recap` for the same two reasons.

    RepoDashboardHealth:
      type: object
      description: >
        The health projection for one managed repository. The current daemon has no per-repo
        health source, so every measurement is explicitly unknown rather than rendered as zero or
        healthy.
      required:
        - status
        - queuedtasks
        - errorrate
        - last_run
        - alerts
      properties:
        status:
          type: string
          enum:
            - unknown
        queuedtasks:
          type: [integer, "null"]
        errorrate:
          type: [number, "null"]
        last_run:
          type: [string, "null"]
        alerts:
          type: [array, "null"]
          items:
            type: string

    RepoDashboardTelemetry:
      type: object
      description: Per-repository telemetry, unavailable until a durable per-repo aggregation source exists.
      required:
        - tokens7d
        - modelsused
        - cost_7d
      properties:
        tokens7d:
          type: [number, "null"]
        modelsused:
          type: array
          items:
            type: string
        cost_7d:
          type: [number, "null"]

    RepoDashboardSettings:
      type: object
      description: Per-repository settings, unavailable until durable settings persistence exists.
      required:
        - proofpolicy
        - workerpoolsize
        - alertthreshold
      properties:
        proofpolicy:
          type: [string, "null"]
        workerpoolsize:
          type: [integer, "null"]
        alertthreshold:
          type: [number, "null"]

    RepoDashboardEntry:
      type: object
      description: >
        One repository from the validated `.remudero/managed-repos.json` set. Managed membership
        is not evidence of OAuth connection, activation, health, telemetry, or settings.
      required:
        - id
        - reponame
        - repourl
        - connected_at
        - active
        - managed
        - source
        - health
        - telemetry
        - settings
      properties:
        id:
          type: string
          description: Canonical `owner/repo` identity from the managed-repo set.
        reponame:
          type: string
          description: Repository name from the canonical identity.
        repourl:
          type: string
          description: Deterministic GitHub URL for the canonical identity; no GitHub read is implied.
        connected_at:
          type: [string, "null"]
        active:
          type: [boolean, "null"]
        managed:
          type: boolean
        source:
          type: string
          enum:
            - managed-repos
        health:
          $ref: "#/components/schemas/RepoDashboardHealth"
        telemetry:
          $ref: "#/components/schemas/RepoDashboardTelemetry"
        settings:
          $ref: "#/components/schemas/RepoDashboardSettings"

    RepoDashboardResult:
      type: object
      description: >
        GET /v1/repos's read-only managed-repo portfolio. An empty `repos` array is a measured
        empty managed set; it is not an unavailable response.
      required:
        - generated_at
        - source
        - repos
      properties:
        generated_at:
          type: string
        source:
          type: string
          enum:
            - managed-repos
        repos:
          type: array
          items:
            $ref: "#/components/schemas/RepoDashboardEntry"

    RecapEvent:
      type: object
      description: >
        W1-T163 -- one "since you last checked" row (src/lib/recap.ts): a single ledger event
        after the caller's per-token marker.
      required:
        - kind
        - taskId
        - ts
      properties:
        kind:
          type: string
          enum:
            - merged
            - blocked
            - escalated
            - question_answered
            - retro
        taskId:
          type: string
          description: The originating ledger line's `task_id` verbatim -- `RETRO` for a fleet-wide retro event.
        ts:
          type: string
        detail:
          type: string
          description: A short human-readable detail (verdict string, escalation class, answer text, retro stats).
        title:
          type: string
          description: The plan task's own title -- present only when `taskId` names a real plan task.
        taskCardLink:
          type: string
          description: >
            A same-page `#task=<id>` hash link into the console's task card -- present ONLY
            when `taskId` names a real plan task (a fleet-wide event like `retro` has none).

    # ── W3-T5 (MASTER-PLAN §7, human-in-the-loop panel actions) -- request/result shapes for
    # the six write routes src/lib/panel-actions.ts registers. Every one is write-scoped and
    # ledgered with the caller's bearer as `origin` (src/lib/panel-actions.ts's `bearerTokenId`).
    PauseRequest:
      type: object
      description: POST /v1/control/pause's body -- drain-and-hold, an optional human-readable reason.
      properties:
        reason:
          type: string

    PauseResult:
      type: object
      required:
        - paused
      properties:
        paused:
          type: boolean
        reason:
          type:
            - string
            - "null"

    ResumeResult:
      type: object
      description: POST /v1/control/resume's body -- clears BOTH STOP and PAUSE; reports what it cleared.
      required:
        - clearedStop
        - clearedPause
      properties:
        clearedStop:
          type: boolean
        clearedPause:
          type: boolean

    StopRequest:
      type: object
      description: POST /v1/control/stop's body -- the hard kill, an optional human-readable reason.
      properties:
        reason:
          type: string

    StopResult:
      type: object
      required:
        - stopped
      properties:
        stopped:
          type: boolean
        reason:
          type:
            - string
            - "null"

    AnswerQuestionRequest:
      type: object
      description: >
        POST /v1/questions/answer's body -- an operator's answer to a QUESTION-contract entry
        (worker.ts's plan/questions.ndjson), addressed by the task it was raised on (v0 routing
        has no path params, src/lib/service.ts).
      required:
        - taskId
        - answer
      properties:
        taskId:
          type: string
        answer:
          type: string

    AnswerQuestionResult:
      type: object
      required:
        - ok
        - taskId
        - answer
      properties:
        ok:
          type: boolean
        taskId:
          type: string
        answer:
          type: string

    ApproveManualRequest:
      type: object
      description: >
        POST /v1/manual/approve's body -- check off a MANUAL-queue item (MASTER-PLAN §4): closes
        the named `escalation-manual`-labeled GitHub issue (src/lib/escalate.ts).
      required:
        - taskId
        - issueUrl
      properties:
        taskId:
          type: string
        issueUrl:
          type: string

    ApproveManualResult:
      type: object
      required:
        - ok
        - taskId
        - issueUrl
      properties:
        ok:
          type: boolean
        taskId:
          type: string
        issueUrl:
          type: string

    MarkEscalationHandledRequest:
      type: object
      description: >
        POST /v1/escalation/mark-handled's body (W1-T182) -- the NEEDS ME affordance an
        ESCALATION row (any class: BLOCKED/MANUAL/HARD_STOP/GRILL) actually supports, distinct
        from ApproveManualRequest's MANUAL-queue check-off: "approve" has no defined verb for an
        escalation. Closes the named `needs-human`-labeled GitHub issue (src/lib/escalate.ts);
        the name is deliberately "mark handled", not "approve" or "resolve" -- closing the issue
        does not, by itself, imply the underlying block is fixed.
      required:
        - taskId
        - issueUrl
      properties:
        taskId:
          type: string
        issueUrl:
          type: string

    MarkEscalationHandledResult:
      type: object
      required:
        - ok
        - taskId
        - issueUrl
      properties:
        ok:
          type: boolean
        taskId:
          type: string
        issueUrl:
          type: string

    # ── W3-T6 (MASTER-PLAN §7B) -- the plan->task->PR graph + interactive plan adjustment:
    # submit feedback, answer grills, accept/reject a proposal. Built over the SAME feedback
    # inbox (src/lib/feedback.ts) and trace chain (src/lib/trace.ts, W1-T43) already documented
    # there; the four routes below are src/lib/panel-graph.ts's registrations.
    FeedbackEntry:
      type: object
      description: >
        One `plan/feedback/<id>.yaml` entry (src/lib/feedback.ts's `FeedbackEntry` -- the
        §7B schema shape: capture -> triage -> gate).
      required:
        - id
        - ts
        - raw
        - attachments
        - origin
        - status
        - proposal_pr
      properties:
        id:
          type: string
        ts:
          type: string
        raw:
          type: string
        attachments:
          type: array
          items:
            type: string
        origin:
          type: string
          enum:
            - cli
            - ui
            - issue
        status:
          type: string
          enum:
            - new
            - grilling
            - proposed
            - accepted
            - rejected
        proposal_pr:
          type:
            - string
            - "null"
          description: Set once `rmd triage` opens a proposal PR for this entry; null until then.
        unverified:
          type: boolean
          description: >
            GET /v1/feedback only (W1-T257): true when this `proposed` entry's proposal_pr merge
            state could not be read (GitHub outage) -- the row is kept, never dropped. Never
            written to plan/feedback/<id>.yaml; a read-time decoration only.
        discharged:
          type: boolean
          description: >
            GET /v1/feedback only (W1-T1257): true when every task this entry filed
            (`origin: feedback#<id>`) is credited MERGED -- the work the proposal produced has
            shipped, even though `status` still names a decision about the proposal itself.
            Derived fresh on every read, like `unverified` above; never written to
            plan/feedback/<id>.yaml and never auto-advances `status` -- whether a discharged
            entry should advance stays a human call.
        dischargeUndecidable:
          type: boolean
          description: >
            GET /v1/feedback only (W1-T1257): true when `discharged` could not be determined
            because the merged-set read failed or was truncated -- a partial read, never
            mistaken for "not discharged". Mutually exclusive with `discharged`; a read-time
            decoration only, exactly like `unverified`.

    FeedbackInboxResult:
      type: object
      description: GET /v1/feedback's body -- every captured feedback entry, oldest first.
      required:
        - entries
      properties:
        entries:
          type: array
          items:
            $ref: "#/components/schemas/FeedbackEntry"

    SubmitFeedbackRequest:
      type: object
      description: >
        POST /v1/feedback's body -- submit feedback from the panel (ALWAYS captured with
        origin: ui, never taken from this body). `replyTo`, if given, must name an existing
        entry parked `grilling` -- this is "answer a grill" v1 (src/lib/panel-graph.ts's header
        explains why): the answer is captured as a fresh feedback entry that re-enters triage,
        rather than a second, parallel answer-delivery primitive ahead of the still-unbuilt
        W1-T42 grill mechanics.
      required:
        - text
      properties:
        text:
          type: string
        attachments:
          type: array
          description: http(s) links ONLY -- a local file path would resolve against the daemon's own filesystem, not the operator's.
          items:
            type: string
        replyTo:
          type: string
          description: The `grilling` feedback id this submission answers, if any.

    SubmitFeedbackResult:
      type: object
      required:
        - ok
        - entry
      properties:
        ok:
          type: boolean
        entry:
          $ref: "#/components/schemas/FeedbackEntry"

    OperatorActivityItem:
      type: object
      description: One bounded activity, workstream, or authoritative artifact row.
      required:
        - id
        - kind
        - summary
        - source
        - observedAt
        - freshness
      properties:
        id:
          type: string
        kind:
          type: string
          enum:
            - activity
            - workstream
            - artifact
        summary:
          type: string
        source:
          type: string
        observedAt:
          type: string
          format: date-time
        freshness:
          type: string
          enum:
            - verified
            - stale
            - unavailable
            - unknown
            - not-collected
        taskId:
          type: string
        repository:
          type: string
        state:
          type: string
          enum:
            - active
            - blocked
            - queued
            - completed
            - unknown
        reason:
          type: string
        href:
          type: string

    OperatorActivityResult:
      type: object
      description: GET /v1/operator-activity's bounded, source-labeled read projection.
      required:
        - version
        - state
        - source
        - observedAt
      properties:
        version:
          type: string
          enum:
            - operator-activity-v1
        state:
          type: string
          enum:
            - verified
            - stale
            - unavailable
            - unknown
            - not-collected
        source:
          type: string
        observedAt:
          type: string
          format: date-time
        cursor:
          type: string
        items:
          type: array
          maxItems: 200
          items:
            $ref: "#/components/schemas/OperatorActivityItem"
        truncated:
          type: boolean
        reason:
          type: string
        detail:
          type: string

    TraceRun:
      type: object
      description: One run named on a task's ledger lines (src/lib/trace.ts's `TraceRun`).
      required:
        - runId
      properties:
        runId:
          type: string
        verdict:
          type: string
        prUrl:
          type: string
        prState:
          type: string
        mergeSha:
          type: string

    TraceTaskNode:
      type: object
      required:
        - id
        - title
        - runs
      properties:
        id:
          type: string
        title:
          type: string
        origin:
          type: string
        runs:
          type: array
          items:
            $ref: "#/components/schemas/TraceRun"

    TraceFeedbackNode:
      type: object
      required:
        - id
        - raw
        - ts
        - origin
        - status
      properties:
        id:
          type: string
        raw:
          type: string
        ts:
          type: string
        origin:
          type: string
        status:
          type: string
        proposalPr:
          type: string
        proposalPrState:
          type: string
        proposalMergeSha:
          type: string

    TraceChain:
      type: object
      description: >
        The plan->task->PR provenance chain the panel renders as a graph (src/lib/trace.ts's
        `TraceChain`, W1-T43): a feedback -> proposal PR -> task(s) -> run(s) -> PR(s) -> sha,
        entered either FORWARD (from a feedback id) or REVERSE (from a task id).
      required:
        - direction
        - tasks
      properties:
        direction:
          type: string
          enum:
            - forward
            - reverse
        feedback:
          $ref: "#/components/schemas/TraceFeedbackNode"
        tasks:
          type: array
          items:
            $ref: "#/components/schemas/TraceTaskNode"

    TraceResult:
      type: object
      description: GET /v1/trace's body -- the structured chain (for the graph render) plus the pre-rendered plain-text tree (`rmd trace`'s own output).
      required:
        - chain
        - rendered
      properties:
        chain:
          $ref: "#/components/schemas/TraceChain"
        rendered:
          type: string

    ProposalDecisionRequest:
      type: object
      description: POST /v1/feedback/decision's body -- accept or reject a `proposed` entry.
      required:
        - id
        - decision
      properties:
        id:
          type: string
        decision:
          type: string
          enum:
            - accept
            - reject

    ProposalDecisionResult:
      type: object
      required:
        - ok
        - id
        - status
        - proposalPr
      properties:
        ok:
          type: boolean
        id:
          type: string
        status:
          type: string
        proposalPr:
          type:
            - string
            - "null"

    OperatorAgentEvidence:
      type: object
      required:
        - label
        - value
        - source
        - observedAt
        - freshness
      properties:
        label:
          type: string
        value:
          type: string
        source:
          type: string
        observedAt:
          type: string
          format: date-time
        freshness:
          type: string
          enum: [verified, stale, unavailable]

    OperatorAgentProposal:
      type: object
      required:
        - proposalId
        - repo
        - proposalText
        - confidence
        - reasoning
        - category
        - status
        - createdAt
        - evidence
      properties:
        proposalId:
          type: string
        repo:
          type: string
        proposalText:
          type: string
        confidence:
          type: number
          minimum: 0
          maximum: 1
        reasoning:
          type: string
        category:
          type: string
          enum: [optimize, fix, scale]
        status:
          type: string
          enum: [pending, accepted, rejected, expired]
        createdAt:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time
        evidence:
          type: array
          items:
            $ref: "#/components/schemas/OperatorAgentEvidence"

    OperatorAgentHistory:
      type: object
      required:
        - proposalId
        - repo
        - proposalText
        - confidence
        - reasoning
        - category
        - status
        - createdAt
        - evidence
        - decisionHistory
      properties:
        proposalId:
          type: string
        repo:
          type: string
        proposalText:
          type: string
        confidence:
          type: number
        reasoning:
          type: string
        category:
          type: string
          enum: [optimize, fix, scale]
        status:
          type: string
          enum: [pending, accepted, rejected, expired]
        createdAt:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time
        evidence:
          type: array
          items:
            $ref: "#/components/schemas/OperatorAgentEvidence"
        decisionHistory:
          type: array
          items:
            type: object
            required: [decision, at]
            properties:
              decision:
                type: string
                enum: [accepted, rejected, more-info]
              at:
                type: string
                format: date-time
              note:
                type: string
        outcome:
          type: object
          required: [summary, observedAt]
          properties:
            summary:
              type: string
            helped:
              type: boolean
            observedAt:
              type: string
              format: date-time
            evidence:
              type: array
              items:
                type: string

    OperatorAgentProposalList:
      type: object
      required: [proposals, source]
      properties:
        proposals:
          type: array
          items:
            $ref: "#/components/schemas/OperatorAgentHistory"
        source:
          type: string
          enum: [ledger]

    ContextRetention:
      type: object
      required: [policy, expiresAt]
      properties:
        policy:
          type: string
          maxLength: 320
        expiresAt:
          type: string
          format: date-time

    ContextRevocation:
      type: object
      required: [state]
      properties:
        state:
          type: string
          enum: [active, revoked]
        revokedAt:
          type: string
          format: date-time
        reason:
          type: string
          maxLength: 500

    ContextItem:
      type: object
      description: >
        Bounded context-item-v1 memory. Content is accepted only for the assistant's internal
        reader and is never returned by the metadata inventory route.
      required: [version, contextId, source, principal, purpose, sensitivity, authorityRef, observedAt, freshness, retention, visibility, derivationLinks, revocation, content]
      properties:
        version:
          type: string
          enum: [context-item-v1]
        contextId:
          type: string
          maxLength: 160
        source:
          type: string
          maxLength: 320
        principal:
          type: string
          maxLength: 320
        purpose:
          type: string
          maxLength: 320
        sensitivity:
          type: string
          enum: [low, moderate, high, restricted]
        authorityRef:
          type: string
          maxLength: 320
        observedAt:
          type: string
          format: date-time
        freshness:
          type: string
          enum: [fresh, stale, unavailable]
        retention:
          $ref: "#/components/schemas/ContextRetention"
        visibility:
          type: string
          enum: [private, operator, shared]
        derivationLinks:
          type: array
          maxItems: 20
          items:
            type: string
            maxLength: 160
        revocation:
          $ref: "#/components/schemas/ContextRevocation"
        content:
          type: string
          maxLength: 4000

    ContextInventoryItem:
      type: object
      required: [version, contextId, source, principal, purpose, sensitivity, authorityRef, observedAt, freshness, retention, visibility, derivationLinks, revocation, availability]
      properties:
        version:
          type: string
          enum: [context-item-v1]
        contextId:
          type: string
          maxLength: 160
        source:
          type: string
          maxLength: 320
        principal:
          type: string
          maxLength: 320
        purpose:
          type: string
          maxLength: 320
        sensitivity:
          type: string
          enum: [low, moderate, high, restricted]
        authorityRef:
          type: string
          maxLength: 320
        observedAt:
          type: string
          format: date-time
        freshness:
          type: string
          enum: [fresh, stale, unavailable]
        retention:
          $ref: "#/components/schemas/ContextRetention"
        visibility:
          type: string
          enum: [private, operator, shared]
        derivationLinks:
          type: array
          maxItems: 20
          items:
            type: string
            maxLength: 160
        revocation:
          $ref: "#/components/schemas/ContextRevocation"
        availability:
          type: string
          enum: [available, stale, unavailable, revoked, deleted]
        deletionReceipt:
          $ref: "#/components/schemas/ContextReceipt"

    ContextReceipt:
      type: object
      required: [receiptId, contextId, operation, at, authorityRef, affectedDerivations]
      properties:
        receiptId:
          type: string
          maxLength: 160
        contextId:
          type: string
          maxLength: 160
        operation:
          type: string
          enum: [revoke, delete]
        at:
          type: string
          format: date-time
        authorityRef:
          type: string
          maxLength: 320
        affectedDerivations:
          type: integer
          minimum: 0

    ContextReceiptResult:
      type: object
      required: [ok, existing, receipt]
      properties:
        ok:
          type: boolean
          enum: [true]
        existing:
          type: boolean
        receipt:
          $ref: "#/components/schemas/ContextReceipt"

    ContextList:
      type: object
      required: [items, stale, absent, source, asOf]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/ContextInventoryItem"
        stale:
          type: array
          items:
            type: string
        absent:
          type: boolean
        source:
          type: string
          enum: [ledger]
        asOf:
          type: string
          format: date-time

    ContextRegistration:
      type: object
      required: [context]
      properties:
        context:
          $ref: "#/components/schemas/ContextItem"

    ContextActionRequest:
      type: object
      required: [contextId, authorityRef]
      properties:
        contextId:
          type: string
          maxLength: 160
        authorityRef:
          type: string
          maxLength: 320
        reason:
          type: string
          maxLength: 500

    OperatorAgentProposalRegistration:
      type: object
      required: [proposal]
      properties:
        proposal:
          $ref: "#/components/schemas/OperatorAgentProposal"

    OperatorAgentDecisionRequest:
      type: object
      required: [proposalId, decision]
      properties:
        proposalId:
          type: string
        decision:
          type: string
          enum: [accepted, rejected, more-info]
        note:
          type: string

    OperatorAgentOutcomeRequest:
      type: object
      required: [proposalId, outcome]
      properties:
        proposalId:
          type: string
        outcome:
          type: object
          required: [summary, observedAt]
          properties:
            summary:
              type: string
            helped:
              type: boolean
            observedAt:
              type: string
              format: date-time
            evidence:
              type: array
              items:
                type: string

    OperatorAgentSettings:
      type: object
      required: [enabled, confidenceThreshold]
      properties:
        enabled:
          type: boolean
        confidenceThreshold:
          type: number
          minimum: 0.9
          maximum: 0.99

    OperatorAgentSettingsScope:
      type: object
      description: Settings are durable per repository; the console must select the repository explicitly.
      required: [kind, repository]
      properties:
        kind:
          type: string
          enum: [repository]
        repository:
          type: string
          minLength: 1

    OperatorAgentSettingsRequest:
      type: object
      required: [settings]
      properties:
        settings:
          $ref: "#/components/schemas/OperatorAgentSettings"
        scope:
          $ref: "#/components/schemas/OperatorAgentSettingsScope"

    OperatorAgentSettingsResult:
      type: object
      required: [settings, source]
      properties:
        settings:
          $ref: "#/components/schemas/OperatorAgentSettings"
        source:
          type: string
          enum: [ledger, default]
        scope:
          $ref: "#/components/schemas/OperatorAgentSettingsScope"
        updatedAt:
          type: string
          format: date-time

    OperatorAgentExperimentScope:
      type: object
      required: [repo]
      properties:
        repo:
          type: string
          minLength: 1
        taskType:
          type: string
        lane:
          type: string
        provider:
          type: string
        modelPolicy:
          type: string
        evidenceAnchors:
          type: array
          maxItems: 12
          items:
            type: string

    OperatorAgentExperimentBaseline:
      type: object
      required: [metricName, value, unit, denominator, comparisonPopulation, windowStart, windowEnd, source, freshness]
      properties:
        metricName:
          type: string
        value:
          type: number
        unit:
          type: string
        denominator:
          type: integer
          minimum: 1
        comparisonPopulation:
          type: string
        windowStart:
          type: string
          format: date-time
        windowEnd:
          type: string
          format: date-time
        source:
          type: string
        freshness:
          type: string
          enum: [verified, stale, unavailable]

    OperatorAgentExperimentIntervention:
      type: object
      required: [summary, plan]
      properties:
        summary:
          type: string
        plan:
          type: string
        taskId:
          type: string
        prUrl:
          type: string
          format: uri

    OperatorAgentExperimentRollback:
      type: object
      required: [plan, reason]
      properties:
        plan:
          type: string
        reason:
          type: string
        receipt:
          type: string

    OperatorAgentExperiment:
      type: object
      required: [version, experimentId, hypothesis, intervention, scope, baseline, rollback, createdAt, state]
      properties:
        version:
          type: string
          enum: [experiment-v1]
        experimentId:
          type: string
        proposalId:
          type: string
        hypothesis:
          type: string
        intervention:
          $ref: "#/components/schemas/OperatorAgentExperimentIntervention"
        scope:
          $ref: "#/components/schemas/OperatorAgentExperimentScope"
        baseline:
          $ref: "#/components/schemas/OperatorAgentExperimentBaseline"
        rollback:
          $ref: "#/components/schemas/OperatorAgentExperimentRollback"
        createdAt:
          type: string
          format: date-time
        state:
          type: string
          enum: [proposed]

    OperatorAgentExperimentOutcome:
      type: object
      required: [state, summary, observedAt]
      properties:
        state:
          type: string
          enum: [observing, succeeded, neutral, regressed, unmeasurable]
        summary:
          type: string
        observedAt:
          type: string
          format: date-time
        source:
          type: string
        freshness:
          type: string
          enum: [verified, stale, unavailable]
        attribution:
          type: string
          enum: [complete, missing, mixed]
        denominator:
          type: integer
          minimum: 0
        comparisonPopulation:
          type: string
        metricName:
          type: string
        value:
          type: number
        reason:
          type: string

    OperatorAgentExperimentHistory:
      type: object
      required: [version, experimentId, hypothesis, intervention, scope, baseline, rollback, createdAt, state, events]
      properties:
        version:
          type: string
          enum: [experiment-v1]
        experimentId:
          type: string
        proposalId:
          type: string
        hypothesis:
          type: string
        intervention:
          $ref: "#/components/schemas/OperatorAgentExperimentIntervention"
        scope:
          $ref: "#/components/schemas/OperatorAgentExperimentScope"
        baseline:
          $ref: "#/components/schemas/OperatorAgentExperimentBaseline"
        rollback:
          $ref: "#/components/schemas/OperatorAgentExperimentRollback"
        createdAt:
          type: string
          format: date-time
        state:
          type: string
          enum: [proposed, approved, observing, succeeded, neutral, regressed, rolled_back, expired, unmeasurable, rejected]
        events:
          type: array
          items:
            type: object
            required: [kind, at]
            properties:
              kind:
                type: string
                enum: [decision, outcome, rollback]
              at:
                type: string
                format: date-time
              decision:
                type: string
                enum: [approved, rejected]
              outcome:
                $ref: "#/components/schemas/OperatorAgentExperimentOutcome"
              rollback:
                $ref: "#/components/schemas/OperatorAgentExperimentRollback"
              note:
                type: string
        outcome:
          $ref: "#/components/schemas/OperatorAgentExperimentOutcome"

    OperatorAgentExperimentList:
      type: object
      required: [experiments, source]
      properties:
        experiments:
          type: array
          items:
            $ref: "#/components/schemas/OperatorAgentExperimentHistory"
        source:
          type: string
          enum: [ledger]

    OperatorAgentExperimentRegistration:
      type: object
      required: [experiment]
      properties:
        experiment:
          $ref: "#/components/schemas/OperatorAgentExperiment"

    OperatorAgentExperimentDecisionRequest:
      type: object
      required: [experimentId, decision]
      properties:
        experimentId:
          type: string
        decision:
          type: string
          enum: [approved, rejected]
        note:
          type: string

    OperatorAgentExperimentOutcomeRequest:
      type: object
      required: [experimentId, outcome]
      properties:
        experimentId:
          type: string
        outcome:
          $ref: "#/components/schemas/OperatorAgentExperimentOutcome"

    OperatorAgentExperimentRollbackRequest:
      type: object
      required: [experimentId, rollback]
      properties:
        experimentId:
          type: string
        rollback:
          $ref: "#/components/schemas/OperatorAgentExperimentRollback"

    # ── W1-T3856 -- experiment-promotion-v1: the guarded flow from a candidate policy to measured
    # production exposure (deterministic replay, shadow observation, canary promotion, stop
    # conditions, automatic rollback) that a later-approved experiment-v1 record may take.
    PromotionScope:
      type: object
      required: [repo, policyScope]
      properties:
        repo:
          type: string
          minLength: 1
        policyScope:
          type: string
          minLength: 1
          description: The unit canary exposure is serialized against; at most one active promotion may hold a given (repo, policyScope) pair.
        taskType:
          type: string
        lane:
          type: string

    PromotionGuardMetric:
      type: object
      required: [metricName, unit, direction, abortThreshold]
      properties:
        metricName:
          type: string
        unit:
          type: string
        direction:
          type: string
          enum: [max, min]
        abortThreshold:
          type: number

    PromotionRollback:
      type: object
      required: [plan, reason]
      properties:
        plan:
          type: string
        reason:
          type: string
        receipt:
          type: string

    PromotionRecord:
      type: object
      required:
        - version
        - promotionId
        - candidate
        - baseline
        - scope
        - comparisonPopulation
        - denominatorFloor
        - observationWindow
        - guardMetrics
        - maxExposure
        - owner
        - expiresAt
        - rollback
        - createdAt
        - state
      properties:
        version:
          type: string
          enum: [experiment-promotion-v1]
        promotionId:
          type: string
        experimentId:
          type: string
          description: Links to the experiment-v1 record (W1-T3853) this promotion progresses.
        candidate:
          type: string
        baseline:
          type: string
        scope:
          $ref: "#/components/schemas/PromotionScope"
        comparisonPopulation:
          type: string
        denominatorFloor:
          type: integer
          minimum: 5
        observationWindow:
          type: object
          required: [start, end]
          properties:
            start:
              type: string
              format: date-time
            end:
              type: string
              format: date-time
        guardMetrics:
          type: array
          minItems: 1
          maxItems: 12
          items:
            $ref: "#/components/schemas/PromotionGuardMetric"
        maxExposure:
          type: number
          minimum: 0
          maximum: 1
          description: The bounded maximum fraction of traffic the canary may take.
        owner:
          type: string
        expiresAt:
          type: string
          format: date-time
        rollback:
          $ref: "#/components/schemas/PromotionRollback"
        createdAt:
          type: string
          format: date-time
        state:
          type: string
          enum: [proposed]

    PromotionReplayResult:
      type: object
      required: [caseId, matched]
      description: >
        The bounded per-case receipt submitted and persisted: whether the candidate and baseline
        agreed. The raw candidate/baseline outputs replayPromotion() computes are not part of the
        durable wire contract -- they may be arbitrarily large or unbounded -- so only the
        comparison result is recorded.
      properties:
        caseId:
          type: string
        matched:
          type: boolean

    PromotionReplaySummary:
      type: object
      required: [version, corpusSize, matched, mismatched, deterministic, sideEffectFree, results]
      properties:
        version:
          type: string
          enum: [experiment-promotion-v1]
        corpusSize:
          type: integer
          minimum: 1
        matched:
          type: integer
          minimum: 0
        mismatched:
          type: integer
          minimum: 0
        deterministic:
          type: boolean
        sideEffectFree:
          type: boolean
          enum: [true]
        results:
          type: array
          items:
            $ref: "#/components/schemas/PromotionReplayResult"

    PromotionGuardObservation:
      type: object
      required: [metricName, value, denominator, freshness, comparisonPopulation, observedAt]
      properties:
        metricName:
          type: string
        value:
          type: number
        denominator:
          type: integer
          minimum: 0
        freshness:
          type: string
          enum: [verified, stale, unavailable]
        comparisonPopulation:
          type: string
        observedAt:
          type: string
          format: date-time

    PromotionGuardEvaluation:
      type: object
      required: [state, reasons, breachedMetrics]
      properties:
        state:
          type: string
          enum: [ready, unmeasurable, regressed]
        reasons:
          type: array
          items:
            type: string
        breachedMetrics:
          type: array
          items:
            type: string

    AssistantTrustControlResult:
      type: object
      description: >
        One control case (W1-T3882): a `positive` control proves the corpus is visible (a
        known-good case the candidate must pass); a `negative` control proves a restraint
        failure is detectable (a known-bad case the candidate must flag, never pass silently).
      required: [caseId, metricName, controlType, expectedOutcome, observedOutcome]
      properties:
        caseId:
          type: string
        metricName:
          type: string
          enum:
            - proactivity_precision
            - dropped_thread_recovery
            - clarification_burden
            - intervention_rate
            - unauthorized_side_effect_rate
            - stale_context_use
            - receipt_completeness
            - rollback_success
            - time_to_human_attention
        controlType:
          type: string
          enum: [positive, negative]
        expectedOutcome:
          type: string
          enum: [pass, flagged]
        observedOutcome:
          type: string
          enum: [pass, flagged]

    AssistantTrustEvidence:
      type: object
      description: >
        Restraint and recovery evidence (W1-T3882) that gates a promotion above the base
        guardrail evaluation: any unauthorized side effect, stale-context use, incomplete
        receipt, or failed rollback blocks promotion outright.
      required: [unauthorizedSideEffects, staleContextUses, receiptsComplete, rollbackAttempted, rollbackSucceeded]
      properties:
        unauthorizedSideEffects:
          type: integer
          minimum: 0
        staleContextUses:
          type: integer
          minimum: 0
        receiptsComplete:
          type: boolean
        rollbackAttempted:
          type: boolean
        rollbackSucceeded:
          type: boolean

    RawAssistantTrustContext:
      type: object
      description: >
        Raw material an evaluation MIGHT be handed — never persisted or trained on. Only bounded
        counts and a secret-scrubbed, length-capped note ever leave this boundary; see
        `AssistantTrustEvaluation.evidence`.
      properties:
        prompts:
          type: array
          maxItems: 200
          items:
            type: string
        transcripts:
          type: array
          maxItems: 200
          items:
            type: string
        credentials:
          type: array
          maxItems: 200
          items:
            type: string
        note:
          type: string

    RedactedAssistantTrustEvidence:
      type: object
      required: [promptCount, transcriptCount, credentialCount, note, redacted]
      properties:
        promptCount:
          type: integer
          minimum: 0
        transcriptCount:
          type: integer
          minimum: 0
        credentialCount:
          type: integer
          minimum: 0
        note:
          type: string
        redacted:
          type: boolean
          enum: [true]

    AssistantTrustEvaluation:
      type: object
      description: >
        The evaluation layer above `PromotionGuardEvaluation` (W1-T3882): a candidate may reach
        `ready` only when replay was deterministic and side-effect-free, the corpus carried
        working positive and negative controls, and no unauthorized side effect, stale-context
        use, incomplete receipt, or failed rollback was observed.
      required: [state, reasons, evidence]
      properties:
        state:
          type: string
          enum: [ready, unmeasurable, blocked]
        reasons:
          type: array
          items:
            type: string
        evidence:
          $ref: "#/components/schemas/RedactedAssistantTrustEvidence"

    OperatorAgentPromotionHistory:
      type: object
      required:
        - version
        - promotionId
        - candidate
        - baseline
        - scope
        - comparisonPopulation
        - denominatorFloor
        - observationWindow
        - guardMetrics
        - maxExposure
        - owner
        - expiresAt
        - rollback
        - createdAt
        - state
        - events
      properties:
        version:
          type: string
          enum: [experiment-promotion-v1]
        promotionId:
          type: string
        experimentId:
          type: string
        candidate:
          type: string
        baseline:
          type: string
        scope:
          $ref: "#/components/schemas/PromotionScope"
        comparisonPopulation:
          type: string
        denominatorFloor:
          type: integer
          minimum: 5
        observationWindow:
          type: object
          required: [start, end]
          properties:
            start:
              type: string
              format: date-time
            end:
              type: string
              format: date-time
        guardMetrics:
          type: array
          minItems: 1
          maxItems: 12
          items:
            $ref: "#/components/schemas/PromotionGuardMetric"
        maxExposure:
          type: number
          minimum: 0
          maximum: 1
        owner:
          type: string
        expiresAt:
          type: string
          format: date-time
        rollback:
          $ref: "#/components/schemas/PromotionRollback"
        createdAt:
          type: string
          format: date-time
        state:
          type: string
          enum:
            - proposed
            - replayed
            - approved
            - shadow
            - canary
            - observing
            - promoted
            - neutral
            - regressed
            - rolled_back
            - expired
            - unmeasurable
        events:
          type: array
          items:
            type: object
            required: [kind, at, state]
            properties:
              kind:
                type: string
                enum: [replay, decision, advance, rollback]
              at:
                type: string
                format: date-time
              state:
                type: string
              replay:
                $ref: "#/components/schemas/PromotionReplaySummary"
              decision:
                type: string
                enum: [approved]
              advance:
                type: object
                required: [target, guard]
                properties:
                  target:
                    type: string
                    enum: [shadow, canary, observing, promoted]
                  guard:
                    $ref: "#/components/schemas/PromotionGuardEvaluation"
                  exposure:
                    type: number
                  assistantTrust:
                    $ref: "#/components/schemas/AssistantTrustEvaluation"
              rollback:
                $ref: "#/components/schemas/PromotionRollback"
              note:
                type: string

    OperatorAgentPromotionList:
      type: object
      required: [promotions, source]
      properties:
        promotions:
          type: array
          items:
            $ref: "#/components/schemas/OperatorAgentPromotionHistory"
        source:
          type: string
          enum: [ledger]

    OperatorAgentPromotionRegistration:
      type: object
      required: [promotion]
      properties:
        promotion:
          $ref: "#/components/schemas/PromotionRecord"

    OperatorAgentPromotionReplayRequest:
      type: object
      required: [promotionId, replay]
      properties:
        promotionId:
          type: string
        replay:
          $ref: "#/components/schemas/PromotionReplaySummary"

    OperatorAgentPromotionDecisionRequest:
      type: object
      required: [promotionId, decision]
      properties:
        promotionId:
          type: string
        decision:
          type: string
          enum: [approved]
        note:
          type: string

    OperatorAgentPromotionAdvanceRequest:
      type: object
      required: [promotionId, target]
      properties:
        promotionId:
          type: string
        target:
          type: string
          enum: [shadow, canary, observing, promoted]
        observations:
          type: array
          maxItems: 12
          items:
            $ref: "#/components/schemas/PromotionGuardObservation"
        exposure:
          type: number
        assistantTrust:
          type: object
          description: >
            Optional assistant-trust evidence (W1-T3882). Its absence preserves the pre-W1-T3882
            base-guardrail-only advance behaviour exactly; when present, the response carries an
            `assistantTrust` evaluation and a `blocked`/`unmeasurable` result can demote an
            otherwise-ready advance to `regressed`/`unmeasurable`.
          required: [controls, evidence]
          properties:
            controls:
              type: array
              maxItems: 64
              items:
                $ref: "#/components/schemas/AssistantTrustControlResult"
            evidence:
              $ref: "#/components/schemas/AssistantTrustEvidence"
            rawContext:
              $ref: "#/components/schemas/RawAssistantTrustContext"

    OperatorAgentPromotionRollbackRequest:
      type: object
      required: [promotionId, rollback]
      properties:
        promotionId:
          type: string
        rollback:
          $ref: "#/components/schemas/PromotionRollback"

    OperatorAgentConsequenceAction:
      type: object
      required: [consequenceClass, target, requiredApprovers]
      properties:
        id:
          type: string
        consequenceClass:
          type: string
          enum: [reversible, disruptive, irreversible, financial]
        target:
          type: object
          required: [identity, source]
          properties:
            identity:
              type: string
            source:
              type: string
              enum: [trusted, external]
            ambiguous:
              type: boolean
        scope:
          type: object
          properties:
            repo:
              type: string
            instance:
              type: string
        evidence:
          type: array
          items:
            type: object
            required: [label, observedAt, maxAgeSeconds]
            properties:
              label:
                type: string
              observedAt:
                type: string
                format: date-time
              maxAgeSeconds:
                type: number
        financial:
          type: object
          properties:
            amount:
              type: number
            currency:
              type: string
            perActionCeiling:
              type: number
            aggregateCeiling:
              type: number
            aggregateSpentBefore:
              type: number
            quoteExpiresAt:
              type: string
              format: date-time
            coolingOffSeconds:
              type: integer
            coolingOffStartedAt:
              type: string
              format: date-time
        irreversible:
          type: object
          properties:
            affectedResource:
              type: string
            recoveryAvailable:
              type: boolean
            recoveryStatement:
              type: string
            rollbackUnavailableReason:
              type: string
            confirmationNonce:
              type: string
            confirmationExpiresAt:
              type: string
              format: date-time
        requiredApprovers:
          type: integer
        approvals:
          type: array
          items:
            type: object
            required: [approverId, approvedAt, source]
            properties:
              approverId:
                type: string
              approvedAt:
                type: string
                format: date-time
              source:
                type: string
                enum: [trusted, external]
        capabilityGrant:
          type: object
          required: [grantId]
          properties:
            grantId:
              type: string

    OperatorAgentConsequencePreflightRequest:
      type: object
      required: [action]
      properties:
        action:
          $ref: "#/components/schemas/OperatorAgentConsequenceAction"

    FollowUpQuietHours:
      type: object
      required: [timezone, start, end]
      properties:
        timezone:
          type: string
        start:
          type: string
          pattern: '^([01]\\d|2[0-3]):[0-5]\\d$'
        end:
          type: string
          pattern: '^([01]\\d|2[0-3]):[0-5]\\d$'

    FollowUpNotificationPolicy:
      type: object
      required: [enabled]
      properties:
        enabled:
          type: boolean
        quietHours:
          $ref: "#/components/schemas/FollowUpQuietHours"

    FollowUpCandidate:
      type: object
      required: [version, candidateId, sourceEvent, workstream, reason, freshness, deduplicationKey, maxAttempts, owner, createdAt]
      description: A bounded follow-up candidate. It must carry a deadline or dependency and exactly one next action or question.
      properties:
        version:
          type: string
          enum: [follow-up-policy-v1]
        candidateId:
          type: string
        sourceEvent:
          type: string
        workstream:
          type: string
        reason:
          type: string
        freshness:
          type: string
          enum: [verified, stale, unavailable]
        deadline:
          type: string
          format: date-time
        dependency:
          type: string
        quietHours:
          $ref: "#/components/schemas/FollowUpQuietHours"
        deduplicationKey:
          type: string
        maxAttempts:
          type: integer
          minimum: 1
          maximum: 20
        owner:
          type: string
        nextAction:
          type: string
        nextQuestion:
          type: string
        createdAt:
          type: string
          format: date-time

    FollowUpEvaluation:
      type: object
      required: [version, candidateId, deduplicationKey, state, reason, at, attempts]
      properties:
        version:
          type: string
          enum: [follow-up-policy-v1]
        candidateId:
          type: string
        deduplicationKey:
          type: string
        state:
          type: string
          enum: [scheduled, eligible, snoozed, suppressed, asked, accepted, rejected, expired, blocked]
        reason:
          type: string
        at:
          type: string
          format: date-time
        attempts:
          type: integer
        nextAction:
          type: string
        nextQuestion:
          type: string

    FollowUpReceipt:
      type: object
      required: [delivered, systemActed, completed, permissionToAct, at]
      properties:
        delivered:
          type: boolean
        answered:
          type: boolean
        systemActed:
          type: boolean
        authority:
          type: string
        completed:
          type: boolean
          enum: [false]
        permissionToAct:
          type: boolean
        at:
          type: string
          format: date-time

    FollowUpHistory:
      type: object
      required: [version, candidateId, sourceEvent, workstream, reason, freshness, deduplicationKey, maxAttempts, owner, createdAt, state, attempts, events]
      properties:
        version:
          type: string
          enum: [follow-up-policy-v1]
        candidateId:
          type: string
        sourceEvent:
          type: string
        workstream:
          type: string
        reason:
          type: string
        freshness:
          type: string
          enum: [verified, stale, unavailable]
        deadline:
          type: string
          format: date-time
        dependency:
          type: string
        quietHours:
          $ref: "#/components/schemas/FollowUpQuietHours"
        deduplicationKey:
          type: string
        maxAttempts:
          type: integer
        owner:
          type: string
        nextAction:
          type: string
        nextQuestion:
          type: string
        createdAt:
          type: string
          format: date-time
        state:
          type: string
          enum: [scheduled, eligible, snoozed, suppressed, asked, accepted, rejected, expired, blocked]
        attempts:
          type: integer
        events:
          type: array
          items:
            type: object
        receipt:
          $ref: "#/components/schemas/FollowUpReceipt"
        notificationPolicy:
          $ref: "#/components/schemas/FollowUpNotificationPolicy"
        snoozedUntil:
          type: string
          format: date-time

    FollowUpList:
      type: object
      required: [followUps, source]
      properties:
        followUps:
          type: array
          items:
            $ref: "#/components/schemas/FollowUpHistory"
        source:
          type: string
          enum: [ledger]

    FollowUpEvaluationRequest:
      type: object
      required: [candidate]
      properties:
        candidate:
          $ref: "#/components/schemas/FollowUpCandidate"
        sourceTerminal:
          type: boolean
        dependencyAvailable:
          type: boolean
        notificationPolicy:
          $ref: "#/components/schemas/FollowUpNotificationPolicy"

    FollowUpControlRequest:
      type: object
      required: [candidateId, control]
      properties:
        candidateId:
          type: string
        control:
          type: string
          enum: [snooze, reject, revoke, policy]
        until:
          type: string
          format: date-time
        notificationPolicy:
          $ref: "#/components/schemas/FollowUpNotificationPolicy"

    FollowUpDeliveryRequest:
      type: object
      required: [candidateId]
      properties:
        candidateId:
          type: string
        answered:
          type: boolean
        systemActed:
          type: boolean
        authority:
          type: string

    # ── W3-T8 (MASTER-PLAN §5B/§7 -- panel skill actions) -- the button set
    # src/lib/panel-skills.ts's ONE route returns, resolved from
    # `.remudero/skills/<name>.yaml` (lib/skill.ts's `Skill`, W1-T44) the SAME
    # way `rmd skill list` does.
    SkillEntry:
      type: object
      description: >
        One `.remudero/skills/<name>.yaml` entry (lib/skill.ts's `Skill`) -- the panel button
        IS this registry entry (MASTER-PLAN §5B). `name` is the file's basename, never a
        `name:` field inside the body, so it can never drift from what `rmd skill list`
        reports it under.
      required:
        - name
        - tools
        - permission_profile
        - output_contract
        - grounding_sources
        - gate
        - tier
      properties:
        name:
          type: string
          description: The skill's identity -- its filename minus `.yaml`.
        tools:
          type: array
          items:
            type: string
        permission_profile:
          type: string
        output_contract:
          type: string
        grounding_sources:
          type: array
          items:
            type: string
        gate:
          type: string
        tier:
          type: string

    SkillsListResult:
      type: object
      description: >
        GET /v1/skills's body -- one SkillEntry per `.remudero/skills/<name>.yaml`, resolved
        fresh on every request (src/lib/panel-skills.ts).
      required:
        - skills
      properties:
        skills:
          type: array
          items:
            $ref: "#/components/schemas/SkillEntry"

    # ── W3-T8 round 2 (MASTER-PLAN §5B/§7) -- invoking a registry skill button. Today ONE
    # skill+mode is wired: { skill: "plan", mode: "clarify" }, the panel's "Refine" button
    # (src/lib/panel-skill-run.ts).
    RunSkillRequest:
      type: object
      description: >
        POST /v1/skills/run's body -- which registry skill to invoke, an optional mode, and
        (for `plan`/`clarify`, i.e. Refine) the plan task id it targets.
      required:
        - skill
      properties:
        skill:
          type: string
          description: A name present in the `.remudero/skills/<name>.yaml` registry (validated against GET /v1/skills's own source).
        mode:
          type: string
          description: 'The skill mode, e.g. "clarify" for Refine (§5B: plan is ONE skill, THREE MODES).'
        taskId:
          type: string
          description: The plan/tasks.yaml task id this invocation targets. Required for `plan`/`clarify` (Refine).

    RunSkillResult:
      type: object
      description: >
        POST /v1/skills/run's body -- the invoked skill echoed back plus the grill it parked.
        Today always carries a `grilling` `feedback` entry: Refine is the only wired skill/mode,
        and Refine always grills (grounds via the real §5C linter, never proposes outright).
      required:
        - ok
        - skill
        - taskId
        - feedback
      properties:
        ok:
          type: boolean
        skill:
          type: string
        mode:
          type: string
        taskId:
          type: string
        feedback:
          $ref: "#/components/schemas/FeedbackEntry"

    # ── W1-T3899 (external-effect-v1) — connector truth is separate from an action receipt. ──
    ExternalEffectPostcondition:
      type: object
      required: [path, equals]
      properties:
        path:
          type: string
        equals:
          description: JSON value expected at the connector path.
          type: object
          additionalProperties: true
        description:
          type: string

    ExternalEffect:
      type: object
      required:
        - version
        - originatingActionId
        - originatingReceiptId
        - capabilityGrantId
        - connector
        - targetIdentity
        - requestedOperation
        - preconditionSnapshot
        - expectedPostconditions
        - observation
        - idempotencyKey
        - reconciliationState
        - retryPath
        - evidenceReference
        - safeToComplete
      properties:
        version:
          type: string
          enum: [external-effect-v1]
        originatingActionId:
          type: string
        originatingReceiptId:
          type: string
        capabilityGrantId:
          type: string
        connector:
          type: string
        targetIdentity:
          type: string
        requestedOperation:
          type: string
        preconditionSnapshot:
          type: object
          additionalProperties: true
        expectedPostconditions:
          type: array
          items:
            $ref: "#/components/schemas/ExternalEffectPostcondition"
        observedState:
          type: object
          additionalProperties: true
        observation:
          type: object
          required: [status, maxAgeMs]
          properties:
            status:
              type: string
              enum: [fresh, stale, unavailable]
            observedAt:
              type: string
              format: date-time
            ageMs:
              type: number
            maxAgeMs:
              type: number
        idempotencyKey:
          type: string
        reconciliationState:
          type: string
          enum: [applied, refused, pending, partially-applied, drifted, stale, unobservable]
        partialSuccess:
          type: object
          required: [satisfied, unsatisfied]
          properties:
            satisfied:
              type: array
              items: { type: string }
            unsatisfied:
              type: array
              items: { type: string }
        retryPath:
          type: object
          required: [kind, allowed, reason]
          properties:
            kind:
              type: string
              enum: [none, retry, compensation]
            allowed:
              type: boolean
            reason:
              type: string
            attemptNumber:
              type: integer
        evidenceReference:
          type: string
          description: Opaque digest of redacted connector evidence; raw provider output is never stored.
        safeToComplete:
          type: boolean
        reason:
          type: string

  responses:
    Unauthorized:
      description: No or unrecognized bearer token.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: A recognized token whose granted scope does not cover this route.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: No route registered for this method + path.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    InvalidRequest:
      description: The JSON body failed validation -- no side effect occurred (src/lib/panel-actions.ts's `jsonAction`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Conflict:
      description: The requested proposal transition conflicts with the durable ledger state.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

# W3-T2 (MASTER-PLAN §7, WS-5a): the read-only live board's first real business routes --
# a task/ledger-state snapshot (REST) and its live-update companion (SSE). Both are built on
# the v0 mechanism above (src/lib/service.ts) and existing projection logic (src/lib/status.ts)
# -- see src/lib/board.ts. Every OTHER route the design (plan render, worker stream tails,
# DECISIONS feed, escalation inbox, question backlog, cost meter) describes is explicit
# follow-on work, not added here.
paths:
  /v1/repos:
    get:
      summary: The read-only managed-repo portfolio for the console dashboard.
      description: >
        Reads and validates `.remudero/managed-repos.json` (src/lib/managed-repos.ts). The
        endpoint reports repository identity only; OAuth connection, activation, health,
        telemetry, settings, logs, and test-run state remain explicitly unknown until they have
        durable daemon sources.
      security:
        - bearerRead: []
      responses:
        "200":
          description: The validated managed-repo portfolio, including an honest empty result.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RepoDashboardResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/status:
    get:
      summary: The current board snapshot -- one StatusProjection per plan task.
      description: >
        Read-only live board (MASTER-PLAN §7, WS-5a -> W3-T2): the task-DAG live-state
        projection, computed via src/lib/status.ts's `projectPlan` over the current plan +
        ledger + GitHub (src/lib/board.ts's `computeBoardSnapshot`).
      security:
        - bearerRead: []
      responses:
        "200":
          description: The current board snapshot.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusSnapshot"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── W3-T5 (MASTER-PLAN §7, human-in-the-loop panel actions) -- write routes over the SAME
  # v0 mechanism (src/lib/service.ts), all write-scoped, all built by src/lib/panel-actions.ts.
  # Depends on W3-T2's board existing (the panel acts on what the board shows). Every write
  # is ledgered with the caller's bearer as `origin`, never the raw token (see
  # src/lib/panel-actions.ts's header). Real `rmd serve` CLI wiring is later work, same split
  # W3-T2's routes below already document.
  /v1/control/pause:
    post:
      summary: Pause the fleet (drain-and-hold) -- no new spawns; an in-flight task still finishes.
      description: >
        MASTER-PLAN §4A/§4B: writes the PAUSE flag (src/lib/fleet-control.ts's `requestPause`),
        checked at the top of every drain tick. Ledgers `panel.pause_requested`.
      security:
        - bearerWrite: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PauseRequest"
      responses:
        "200":
          description: PAUSE is now set.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PauseResult"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/control/resume:
    post:
      summary: Resume the fleet -- clears BOTH STOP and PAUSE.
      description: >
        MASTER-PLAN §4A: the one command that always means go (src/lib/fleet-control.ts's
        `resumeFleet`). Ledgers `panel.resume_requested`. No request body.
      security:
        - bearerWrite: []
      responses:
        "200":
          description: STOP and PAUSE are both cleared (whichever were set).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResumeResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/control/stop:
    post:
      summary: STOP the fleet -- the hard kill, checked first, every tick.
      description: >
        MASTER-PLAN §4A/§4B: writes the STOP flag (src/lib/fleet-control.ts's `requestStop`),
        checked before PAUSE and before picking up any new task. "STOP from the panel halts the
        fleet within one tick" (W3-T5 acceptance) -- the SAME predicate drain.ts's loop already
        checks first. Ledgers `panel.stop_requested`.
      security:
        - bearerWrite: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StopRequest"
      responses:
        "200":
          description: STOP is now set.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StopResult"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/questions/answer:
    post:
      summary: Answer a QUESTION-contract entry (MASTER-PLAN §2), attributed to the panel's bearer.
      description: >
        "An escalation answered FROM the panel appears in the ledger with the panel's bearer
        token as origin; the answer flows to the Architect" (W3-T5 acceptance) -- proof is the
        resulting `panel.question_answered` ledger line (src/lib/panel-actions.ts).
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AnswerQuestionRequest"
      responses:
        "200":
          description: The answer was recorded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnswerQuestionResult"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/manual/approve:
    post:
      summary: Approve (check off) a MANUAL-queue item -- closes its GitHub issue.
      description: >
        MASTER-PLAN §4: "the MANUAL queue doubles as the human's to-do list -- rendered in the
        control panel with check-off (= closing the issue)." Closes the named
        `escalation-manual`-labeled issue (src/lib/escalate.ts) FIRST, then ledgers
        `panel.manual_approved` -- a close that fails never produces a false-approved ledger line.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ApproveManualRequest"
      responses:
        "200":
          description: The issue was closed and the approval recorded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApproveManualResult"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/escalation/mark-handled:
    post:
      summary: Mark an ESCALATION row handled -- closes its GitHub issue.
      description: >
        W1-T182: the NEEDS ME affordance an ESCALATION row (any class) actually supports --
        distinct from /v1/manual/approve's MANUAL-queue check-off, since "approve" has no
        defined verb for an escalation. Closes the named `needs-human`-labeled issue
        (src/lib/escalate.ts) FIRST, then ledgers `panel.escalation_marked_handled` -- a close
        that fails never produces a false-handled ledger line. Closing the issue does not, by
        itself, resolve the underlying block (the issue body says so); the row clears on the
        NEXT poll once GET /v1/status's live join (src/lib/status.ts) reads it CLOSED.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MarkEscalationHandledRequest"
      responses:
        "200":
          description: The issue was closed and the action recorded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarkEscalationHandledResult"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── W3-T6 (MASTER-PLAN §7B) -- the plan->task->PR graph + interactive plan adjustment, over
  # the SAME v0 mechanism (src/lib/service.ts), built by src/lib/panel-graph.ts. Depends on
  # W3-T2's board (the panel this extends) and W1-T43's trace chain (src/lib/trace.ts). Every
  # write is ledgered with the caller's bearer as `origin`, via the SAME `appendPanelLedger`
  # primitive W3-T5's routes use. Real `rmd serve` CLI wiring is later work, same split every
  # route on this surface already documents.
  /v1/feedback:
    get:
      summary: The feedback inbox -- every captured entry, oldest first.
      description: >
        `plan/feedback/` (MASTER-PLAN §7B, W1-T40), listed via src/lib/feedback.ts's
        `listFeedback`. Optional `?status=` filters to one lifecycle status.
      security:
        - bearerRead: []
      responses:
        "200":
          description: The inbox.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedbackInboxResult"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      summary: Submit feedback from the panel -- ALWAYS captured with origin=ui.
      description: >
        "feedback submitted from the panel appears in the inbox with origin=ui and produces a
        proposal PR" (W3-T6 acceptance) -- captures via src/lib/feedback.ts's `captureFeedback`,
        ledgers `panel.feedback_submitted`. `replyTo` doubles this route as "answer a grill" v1
        (src/lib/panel-graph.ts's header).
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SubmitFeedbackRequest"
      responses:
        "200":
          description: The captured entry.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubmitFeedbackResult"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/trace:
    get:
      summary: The plan->task->PR provenance graph for one task or feedback id.
      description: >
        "the panel renders the plan→task→PR graph" (W3-T6 acceptance) -- the SAME two-entry-
        point chain `rmd trace <id>` renders (W1-T43, src/lib/trace.ts), addressed by
        `?id=<task-id-or-feedback-id>` (v0 routing has no path params, src/lib/service.ts).
      security:
        - bearerRead: []
      responses:
        "200":
          description: The provenance chain.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TraceResult"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/operator-activity:
    get:
      summary: Read the bounded operator activity, workstream, and artifact projection.
      description: >
        Read-only operator-activity-v1 composition over the existing ledger, plan projection,
        dispatcher frontier, and authoritative trace links. The response never contains raw
        prompts, transcripts, credentials, account identifiers, or browser-owned measurements.
        Unavailable and unknown source states remain explicit rather than becoming empty healthy data.
      security:
        - bearerRead: []
      responses:
        "200":
          description: Operator activity projection, including explicit non-verified states.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OperatorActivityResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /v1/feedback/decision:
    post:
      summary: Accept or reject a `proposed` feedback entry.
      description: >
        "accepting a proposal from the panel is ledgered with the panel bearer as origin"
        (W3-T6 acceptance) -- src/lib/feedback.ts's `setFeedbackStatus`, ledgers
        `panel.proposal_accepted`/`panel.proposal_rejected`. Only a `proposed` entry may be
        decided (400 otherwise).
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProposalDecisionRequest"
      responses:
        "200":
          description: The decision was recorded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProposalDecisionResult"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/operator-agent/context:
    get:
      summary: Read the metadata-only personal context inventory.
      description: >
        Reads context-item-v1 envelopes from the ledger union, including rotated records. The
        response excludes raw private content and reports stale or unavailable derived context.
      security:
        - bearerRead: []
      parameters:
        - name: purpose
          in: query
          required: false
          schema:
            type: string
        - name: authorityRef
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Metadata-only context inventory.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContextList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      summary: Register one bounded context-item-v1 envelope.
      description: Records provenance, purpose, authority, retention, visibility, and derivation links before use.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ContextRegistration"
      responses:
        "201":
          description: Context item registered; the response is metadata-only.
        "200":
          description: Identical context item already registered.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"

  /v1/operator-agent/context/revoke:
    post:
      summary: Revoke a context item and its derived dependants.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ContextActionRequest"
      responses:
        "200":
          description: Bounded revocation receipt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContextReceiptResult"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"

  /v1/operator-agent/context/delete:
    post:
      summary: Delete a context item and retain its bounded deletion receipt.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ContextActionRequest"
      responses:
        "200":
          description: Bounded deletion receipt; raw private content is not returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContextReceiptResult"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/operator-agent/proposals:
    get:
      summary: Read durable operator-agent proposals and decision history.
      description: >
        Returns only proposal, decision, and observed-outcome records reconstructed from the
        append-only ledger, including supported rotated ledger files. No raw analytics payload or
        bearer token is returned.
      security:
        - bearerRead: []
      responses:
        "200":
          description: Durable operator-agent history.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OperatorAgentProposalList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      summary: Register an evidence-backed operator-agent proposal.
      description: >
        Idempotently records the proposal identity and evidence in the ledger. A repeated
        proposalId with different repository or text is rejected as a conflict.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OperatorAgentProposalRegistration"
      responses:
        "201":
          description: Proposal registered.
        "200":
          description: Identical proposal already registered.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"

  /v1/operator-agent/proposals/decision:
    post:
      summary: Record an operator decision on an operator-agent proposal.
      description: >
        Records accepted, rejected, or more-info. More-info leaves the proposal pending and is
        retained as a learning signal. Accepted proposals have no outcome until a later outcome
        request supplies observed evidence.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OperatorAgentDecisionRequest"
      responses:
        "200":
          description: Decision recorded.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"

  /v1/operator-agent/proposals/outcome:
    post:
      summary: Record a later observed outcome for an accepted proposal.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OperatorAgentOutcomeRequest"
      responses:
        "200":
          description: Outcome recorded.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"

  /v1/operator-agent/settings:
    get:
      summary: Read durable operator-agent settings.
      description: Returns the last valid ledger settings row, or explicit conservative defaults when none exists.
      security:
        - bearerRead: []
      parameters:
        - name: repository
          in: query
          required: false
          description: Explicit repository scope. When present, only rows for this repository are read.
          schema:
            type: string
            minLength: 1
      responses:
        "200":
          description: Operator-agent settings.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OperatorAgentSettingsResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      summary: Persist bounded operator-agent settings.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OperatorAgentSettingsRequest"
      responses:
        "200":
          description: Settings persisted.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /v1/operator-agent/experiments:
    get:
      summary: Read durable self-improvement experiment-v1 history.
      description: >
        Returns the append-only experiment record, approval decision, measured outcome, and
        rollback events across the live ledger and supported rotated ledger files. Approval is
        never presented as measured success; weak attribution is returned as unmeasurable.
      security:
        - bearerRead: []
      responses:
        "200":
          description: Durable experiment history.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OperatorAgentExperimentList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      summary: Register a bounded, falsifiable self-improvement experiment.
      description: >
        Records the hypothesis, intervention, baseline, scope, observation window, and rollback
        path before any operator decision. The durable version is experiment-v1 and excludes raw
        credentials or unbounded model prose.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OperatorAgentExperimentRegistration"
      responses:
        "201":
          description: Experiment registered.
        "200":
          description: Identical experiment already registered.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"

  /v1/operator-agent/experiments/decision:
    post:
      summary: Approve or reject a self-improvement experiment.
      description: Approval records intent only; no outcome or success rate is created until a later observation.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OperatorAgentExperimentDecisionRequest"
      responses:
        "200":
          description: Experiment decision recorded.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"

  /v1/operator-agent/experiments/outcome:
    post:
      summary: Record a later self-improvement experiment observation.
      description: >
        A claimed success, neutral result, or regression is normalised to unmeasurable when its
        denominator, attribution, comparison population, metric, or freshness is insufficient.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OperatorAgentExperimentOutcomeRequest"
      responses:
        "200":
          description: Experiment observation recorded.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"

  /v1/operator-agent/experiments/rollback:
    post:
      summary: Record a rollback for a regressed self-improvement experiment.
      description: Rollback appends a linked event and leaves the proposal, approval, and regression history intact.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OperatorAgentExperimentRollbackRequest"
      responses:
        "200":
          description: Rollback recorded.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"

  /v1/operator-agent/promotions:
    get:
      summary: Read durable experiment-promotion-v1 replay/shadow/canary history.
      description: >
        Returns the append-only promotion record and its replay, decision, advance, and rollback
        events across the live ledger and supported rotated ledger files. The current state is
        the last event's resulting state, adjusted for expiry.
      security:
        - bearerRead: []
      responses:
        "200":
          description: Durable promotion history.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OperatorAgentPromotionList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      summary: Register a bounded promotion; canary exposure is serialized per policy scope.
      description: >
        Records the candidate, baseline, comparable population, denominator floor, observation
        window, guard metrics, maximum exposure, owner, expiry, and rollback path before any
        replay or approval. At most one active (non-terminal) promotion may hold a given
        (repo, policyScope) pair at a time.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OperatorAgentPromotionRegistration"
      responses:
        "201":
          description: Promotion registered.
        "200":
          description: Identical promotion already registered.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"

  /v1/operator-agent/promotions/replay:
    post:
      summary: Attach a deterministic, side-effect-free replay summary to a proposed promotion.
      description: >
        The submitted summary is a bounded receipt of a replay pass already run over a historical
        corpus by the caller; this route validates and persists it rather than re-executing
        anything, moving the promotion from proposed to replayed.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OperatorAgentPromotionReplayRequest"
      responses:
        "200":
          description: Replay summary recorded.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"

  /v1/operator-agent/promotions/decision:
    post:
      summary: Approve a replayed promotion candidate.
      description: Approval records intent only; only later guard-evaluated evidence can advance the promotion further.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OperatorAgentPromotionDecisionRequest"
      responses:
        "200":
          description: Promotion decision recorded.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"

  /v1/operator-agent/promotions/advance:
    post:
      summary: Advance a promotion one guarded step (shadow, canary, observing, or promoted).
      description: >
        `shadow` entry follows approval alone. Every later step evaluates freshly submitted guard
        observations against the promotion's declared metrics, denominator floor, comparison
        population, and window: a breach becomes `regressed`, insufficient evidence becomes
        `unmeasurable`, and only a fully-ready evaluation reaches the requested target. Requesting
        exposure above the promotion's declared maximum is refused.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OperatorAgentPromotionAdvanceRequest"
      responses:
        "200":
          description: Promotion advanced (or explicitly aborted to regressed/unmeasurable).
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"

  /v1/operator-agent/promotions/rollback:
    post:
      summary: Record a rollback for an approved, shadow, canary, observing, regressed, or unmeasurable promotion.
      description: Rollback appends a linked event and leaves the registration, replay, approval, and advance history intact.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OperatorAgentPromotionRollbackRequest"
      responses:
        "200":
          description: Rollback recorded.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"

  /v1/operator-agent/consequence/preflight:
    post:
      summary: Classify and evaluate a consequence-policy-v1 action before any capability it names may be used.
      description: >
        Every operator-agent action path that would go on to use a capability grant (see
        `capability-grant-v1`) evaluates its consequence policy here FIRST. Refuses an ambiguous or
        externally-sourced target, stale evidence, an expired quote/confirmation, an exceeded
        per-action/aggregate financial ceiling, an active cooling-off window, or a shortfall of
        distinct trusted approvers against the action's configured requirement. A `ready` response
        is a preflight verdict only — it never itself uses a capability or reports success.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OperatorAgentConsequencePreflightRequest"
      responses:
        "200":
          description: The action is ready — every consequence-policy check passed.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          description: The action was refused by consequence policy; the response names the machine-readable code and reason.

  /v1/operator-agent/follow-ups:
    get:
      summary: Read durable proactive follow-up candidates and receipts.
      description: Returns ledgered follow-up state after freshness, quiet-hours, deduplication, and human-stop evaluation.
      security:
        - bearerRead: []
      responses:
        "200":
          description: Durable follow-up history.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FollowUpList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /v1/operator-agent/follow-ups/evaluate:
    post:
      summary: Evaluate and ledger a proactive follow-up candidate.
      description: Refuses stale, unavailable, expired, terminal, blocked, quiet-hour, disabled, or duplicate candidates.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FollowUpEvaluationRequest"
      responses:
        "200":
          description: Follow-up policy decision recorded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FollowUpEvaluation"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /v1/operator-agent/follow-ups/control:
    post:
      summary: Apply a durable human follow-up stop or notification-policy change.
      description: Snooze, reject, revoke, and notification-policy changes are append-only and survive restart.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FollowUpControlRequest"
      responses:
        "200":
          description: Follow-up control recorded.
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"

  /v1/operator-agent/follow-ups/delivery:
    post:
      summary: Record a delivered follow-up receipt.
      description: Delivery records an asked receipt and never implies completion or permission to act.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FollowUpDeliveryRequest"
      responses:
        "200":
          description: Delivery receipt recorded.
          content:
            application/json:
              schema:
                type: object
                required: [ok, state, receipt]
                properties:
                  ok:
                    type: boolean
                  state:
                    type: string
                    enum: [asked]
                  receipt:
                    $ref: "#/components/schemas/FollowUpReceipt"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"

  /v1/skills:
    get:
      summary: The panel's skill action-button set, resolved from the registry.
      description: >
        "each v1 skill appears as a panel button resolved from the registry (not hard-coded
        UI)... adding a skill yaml adds a button with no UI code change" (W3-T8 acceptance) --
        the SAME `.remudero/skills/<name>.yaml` registry `rmd skill list` reads (W1-T44,
        src/lib/skill.ts), via src/lib/panel-skills.ts. Reloaded fresh on every request, so a
        skill yaml added after the daemon boots appears on the very next request.
      security:
        - bearerRead: []
      responses:
        "200":
          description: The button set.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SkillsListResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/skills/run:
    post:
      summary: Invoke a registry skill button -- today, Refine (plan/clarify) only.
      description: >
        "invoking Refine from the panel runs the plan --mode=clarify skill and shows the grill
        inline... the answer flows back" (W3-T8 acceptance) -- src/lib/panel-skill-run.ts.
        `{ skill: "plan", mode: "clarify" }` (Refine) grounds via the REAL, ALREADY-MERGED §5C
        linter (src/lib/task-linter.ts) and parks the result as a `grilling`
        plan/feedback/<id>.yaml entry (src/lib/feedback.ts) -- the SAME entry GET /v1/feedback
        already renders inline and POST /v1/feedback's `replyTo` already answers (W3-T6). Any
        other skill/mode is a 400 naming exactly what is not yet wired, never a silent no-op.
      security:
        - bearerWrite: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RunSkillRequest"
      responses:
        "200":
          description: The invoked skill, echoed back with the grill it parked.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RunSkillResult"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/status/stream:
    get:
      summary: SSE stream of task state flips -- one `status` event per changed StatusProjection.
      description: >
        Live companion to GET /v1/status (MASTER-PLAN §7, WS-5a -> W3-T2): the daemon tails
        state/ledger.ndjson (src/lib/board.ts's `buildStatusStream`) and, for every new line
        naming a task whose derived StatusProjection actually changed, emits an SSE `status`
        event carrying that StatusProjection (src/lib/service.ts's SSE framing). Requires a
        read-scoped bearer token, same as GET /v1/status.
      security:
        - bearerRead: []
      responses:
        "200":
          description: >
            An open text/event-stream connection. Event framing is outside this OpenAPI
            subset's response-body modeling -- each `status` event's `data` payload is a
            components.schemas.StatusProjection.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
Back to home