{
  "openapi": "3.1.0",
  "info": {
    "title": "Pathmode API",
    "version": "1.0.0",
    "description": "REST API for the Pathmode Intent Engineering platform. Connect AI agents and tooling to your Intent Layer — intents, evidence, workspace strategy, and verification. Authenticate with a pm_live_ API key as a Bearer token. See https://pathmode.io/developers for the full guide.",
    "contact": {
      "name": "Pathmode",
      "url": "https://pathmode.io/developers"
    }
  },
  "servers": [
    {
      "url": "https://pathmode.io",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "intents",
      "description": "Create, read, update, and transition intent specs through their lifecycle, plus AI grading and agent-ready prompts."
    },
    {
      "name": "notes",
      "description": "Record and read the technical decisions made while implementing an intent."
    },
    {
      "name": "findings",
      "description": "Record and read write-back findings: contradictions that building the spec surfaced, so the next agent reads where the spec may be stale."
    },
    {
      "name": "decisions",
      "description": "Record and read the decision audit trail on an intent: what was chosen, what was ruled out, why, and what would reopen each call."
    },
    {
      "name": "outcomes",
      "description": "Track measured values against an intent’s outcomes after shipping. Session-authenticated (dashboard) endpoints."
    },
    {
      "name": "evidence",
      "description": "Create and search evidence items, and link them to intents for traceability from user problem to shipped solution."
    },
    {
      "name": "analyze-codebase",
      "description": "Stream an AI analysis of the bound repository to synthesize Implementation Context for an intent. Beta, session-authenticated."
    },
    {
      "name": "workspace",
      "description": "Read workspace strategy, products, and the constitution rules enforced during verification."
    },
    {
      "name": "export",
      "description": "Export workspace and intent context as CLAUDE.md, AGENTS.md, .cursorrules, an intent spec, or an outcome rubric."
    },
    {
      "name": "api-keys",
      "description": "List, create, and revoke API keys. Session-authenticated (dashboard) endpoints — not callable with a pm_live_ key."
    },
    {
      "name": "connection",
      "description": "Confirm a key authenticates, and let an MCP client announce itself at launch. Neither counts as usage: these endpoints deliberately do not touch last_used_at."
    },
    {
      "name": "onboarding",
      "description": "Exchange a short-lived browser-issued code and complete adoption of an existing repository intent.md. Intended for the Pathmode CLI."
    }
  ],
  "components": {
    "schemas": {
      "OutcomeMeasurementDefinition": {
        "type": "object",
        "description": "Provider-neutral recipe for evaluating an outcome with a stable saved query.",
        "properties": {
          "source": {
            "type": "object",
            "properties": {
              "provider": {
                "type": "string",
                "example": "posthog"
              },
              "queryRef": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string",
                    "example": "saved_insight"
                  },
                  "id": {
                    "type": "string",
                    "description": "Stable provider id; never a mutable display name."
                  }
                },
                "required": [
                  "kind",
                  "id"
                ]
              }
            },
            "required": [
              "provider",
              "queryRef"
            ]
          },
          "resultSelector": {
            "type": "string",
            "description": "Optional path into a compound provider response."
          },
          "expectation": {
            "type": "object",
            "properties": {
              "operator": {
                "type": "string",
                "enum": [
                  "gt",
                  "gte",
                  "lt",
                  "lte",
                  "eq"
                ]
              },
              "target": {
                "type": "number"
              },
              "unit": {
                "type": "string"
              }
            },
            "required": [
              "operator",
              "target"
            ]
          },
          "window": {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "rolling",
                  "fixed",
                  "since_ship"
                ]
              },
              "duration": {
                "type": "string",
                "description": "Required for rolling windows, e.g. 28d."
              },
              "start": {
                "type": "string",
                "description": "Required with end for fixed windows."
              },
              "end": {
                "type": "string",
                "description": "Required with start for fixed windows."
              }
            },
            "required": [
              "kind"
            ]
          }
        },
        "required": [
          "source",
          "expectation",
          "window"
        ]
      },
      "OutcomeInput": {
        "oneOf": [
          {
            "type": "string",
            "minLength": 1
          },
          {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "text": {
                "type": "string",
                "minLength": 1
              },
              "priority": {
                "type": "string",
                "enum": [
                  "must",
                  "should",
                  "could"
                ]
              },
              "measurement": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/OutcomeMeasurementDefinition"
                  },
                  {
                    "type": "null",
                    "description": "On update, explicitly clears the stored definition."
                  }
                ]
              }
            },
            "required": [
              "text"
            ]
          }
        ]
      },
      "Error": {
        "type": "object",
        "description": "Standard error envelope returned by every 4xx/5xx response. Always includes an \"error\" string; validation errors (400) add a \"details\" object with flattened Zod field errors, and quota errors (403 plan_limit_reached) add metric/current/limit/plan fields.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message, or a machine-readable code such as \"plan_limit_reached\"."
          },
          "details": {
            "type": "object",
            "description": "Present on 400 validation errors: flattened Zod errors as { formErrors, fieldErrors }."
          }
        },
        "required": [
          "error"
        ],
        "additionalProperties": true
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key as a Bearer token, prefixed with pm_live_. Created in Settings → API Keys or via POST /api-keys."
      },
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "sb-access-token",
        "description": "Logged-in Supabase session cookie. Used by dashboard/admin endpoints that do not accept API keys."
      }
    }
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/api/v1/intents": {
      "get": {
        "operationId": "get_intents",
        "tags": [
          "intents"
        ],
        "summary": "List all intents for the authenticated workspace, optionally filtered by status, ordered by updated_at descending.",
        "x-stability": "stable",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "List path uses INTENT_LIST_SELECT which does NOT join edge cases or evidence: edgeCases and evidenceIds always come back as empty arrays [] here (populate them via the detail endpoint GET /api/v1/intents/:id). relations IS populated on the list path. Rows that fail apiIntentSchema.parse are silently skipped (logged), so count may be less than the raw row count. Default order: updated_at descending. No rate limiting on this GET.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Optional exact-match filter on intent status (e.g. draft, validated, approved, shipped, verified). Passed straight to an .eq('status', ...) filter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY when authenticating via session cookie (no pm_live_ Bearer). Identifies which workspace the cookie session targets; ignored for API-key auth (key carries its own workspace).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK — returns { intents, count }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "intents": [
                    {
                      "id": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                      "workspaceId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                      "specVersion": "a1b2c3d4e5f6a7b8",
                      "productId": "7d8e9f0a-1b2c-4d3e-9f8a-6b5c4d3e2f1a",
                      "stageName": null,
                      "status": "draft",
                      "version": 1,
                      "title": "Speed up onboarding",
                      "objective": "Cut time-to-first-value for new workspaces under 10 minutes.",
                      "problemSeverity": "high",
                      "outcomes": [
                        {
                          "id": "c4d3e2f1-a0b9-4c8d-7e6f-5a4b3c2d1e0f",
                          "text": "New user reaches first intent in under 10 min",
                          "priority": "must"
                        }
                      ],
                      "healthMetrics": [
                        "activation_rate"
                      ],
                      "scope": null,
                      "strategicAlignment": null,
                      "alignmentNotes": null,
                      "context": {},
                      "constraints": [
                        "No new third-party signup steps"
                      ],
                      "verification": {},
                      "externalLinks": [],
                      "evidenceAnchors": {},
                      "implementationContext": null,
                      "createdAt": "2026-06-01T12:00:00.000Z",
                      "updatedAt": "2026-06-05T09:30:00.000Z",
                      "shippedAt": null,
                      "verifiedAt": null,
                      "edgeCases": [],
                      "evidenceIds": [],
                      "relations": [
                        {
                          "targetId": "9e8d7c6b-5a4f-4e3d-2c1b-0a9f8e7d6c5b",
                          "type": "depends_on"
                        }
                      ]
                    }
                  ],
                  "count": 1
                }
              }
            }
          },
          "400": {
            "description": "Cookie-session auth only: workspace_id query param missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid/expired API key, or no Bearer key and no valid session cookie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated but lacks read scope (or, for cookie auth, not a member of the workspace)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error fetching intents or unexpected internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      },
      "post": {
        "operationId": "post_intents",
        "tags": [
          "intents"
        ],
        "summary": "Create a new intent spec (status defaults to draft) under a product in the authenticated workspace.",
        "x-stability": "stable",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "intents 200/h",
        "description": "Rate-limited by the 'intents' bucket (200/h sliding window, keyed user:<userId> or ip:<ip>). Billing-gated: when the workspace intent quota is reached, returns 403 with body { error: 'plan_limit_reached', metric: 'intents', current, limit, plan }. A v1 API create is agent-authored: it starts at status 'draft', version 1, origin 'agent', and authorization 'pending'; the response notice tells the caller not to implement until a signed-in human authorizes it. Fires a Slack 'intent_status_changed' notification and an 'intent_created' analytics event (both fire-and-forget). Response is built from a re-fetch with relations (INTENT_WITH_RELATIONS_SELECT) so edgeCases are populated; evidenceIds empty on a fresh create.",
        "parameters": [
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth; identifies the target workspace. Ignored for API-key auth.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Adopt this UUID instead of minting one. Exists so a spec keeps its identity when it moves from a local file into a workspace: a keyless author may already have stamped intent/<uuid> on a branch, and a changed id would silently stop the merge webhook resolving it. Rejected with 409 INTENT_ID_TAKEN if the id already exists."
                  },
                  "title": {
                    "type": "string",
                    "description": "Non-empty after trim. Stored as user_goal."
                  },
                  "objective": {
                    "type": "string",
                    "description": "Non-empty after trim."
                  },
                  "currentState": {
                    "type": "string",
                    "description": "Author-written as-is behavior: how this works today, before the change (max 4000 chars). Leave out for net-new capability. Distinct from implementationContext, which is analyzer-derived."
                  },
                  "productId": {
                    "type": "string",
                    "description": "Non-empty after trim. Must reference a product in this workspace or returns 404. Omit it and the server resolves it when the workspace has exactly one NON-example product; otherwise the request fails 400 with code PRODUCT_REQUIRED and the candidate list, so the caller can ask and retry with an explicit id. Deliberately strict: workspaces auto-load a demo product, and a per-product GitHub binding decides which repository the PR stamp and merge verification apply to, so a guessed product is a mis-wired verification loop rather than a tidy-up."
                  },
                  "outcomes": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/OutcomeInput"
                    },
                    "description": "Strings or structured objects; strings must be non-empty after trim. A structured outcome may carry measurement?: OutcomeMeasurementDefinition, a provider-neutral stable saved-query recipe (for PostHog, typically kind saved_insight plus the insight id). Normalized to StructuredOutcome[] at the boundary."
                  },
                  "constraints": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Coerced to trimmed non-empty strings."
                  },
                  "healthMetrics": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Coerced to trimmed non-empty strings."
                  },
                  "edgeCases": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Each field non-empty after trim. Synced via replaceIntentEdgeCasesRpc; on failure the just-created intent is deleted and 500 returned."
                  },
                  "verification": {
                    "type": "object",
                    "description": "Verification plan object; checks is canonical. The three older buckets are accepted only for compatibility. Defaults to {} if omitted."
                  },
                  "problemSeverity": {
                    "type": "string",
                    "enum": [
                      "low",
                      "medium",
                      "high",
                      "critical"
                    ],
                    "description": "Optional severity enum."
                  },
                  "scope": {
                    "type": "string",
                    "description": "Optional scope object; may be null."
                  }
                },
                "required": [
                  "title",
                  "objective"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created — returns the full mapped intent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "workspaceId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                  "specVersion": "a1b2c3d4e5f6a7b8",
                  "productId": "7d8e9f0a-1b2c-4d3e-9f8a-6b5c4d3e2f1a",
                  "stageName": null,
                  "status": "draft",
                  "version": 1,
                  "title": "Speed up onboarding",
                  "objective": "Cut time-to-first-value for new workspaces under 10 minutes.",
                  "currentState": null,
                  "problemSeverity": "high",
                  "outcomes": [
                    {
                      "id": "c4d3e2f1-a0b9-4c8d-7e6f-5a4b3c2d1e0f",
                      "text": "New user reaches first intent in under 10 min",
                      "priority": "must"
                    }
                  ],
                  "healthMetrics": [
                    "activation_rate"
                  ],
                  "scope": null,
                  "strategicAlignment": null,
                  "alignmentNotes": null,
                  "context": {},
                  "constraints": [
                    "No new third-party signup steps"
                  ],
                  "verification": {},
                  "externalLinks": [],
                  "evidenceAnchors": {},
                  "confirmations": [],
                  "decisions": [],
                  "implementationFindings": [],
                  "implementationContext": null,
                  "origin": "agent",
                  "authorization": "pending",
                  "authorizationNote": null,
                  "createdAt": "2026-08-31T10:15:00.000Z",
                  "updatedAt": "2026-08-31T10:15:00.000Z",
                  "shippedAt": null,
                  "verifiedAt": null,
                  "edgeCases": [
                    {
                      "id": "b2c1d0e9-f8a7-4b6c-5d4e-3f2a1b0c9d8e",
                      "scenario": "User has no products yet",
                      "expectedBehavior": "Show empty-state prompt"
                    }
                  ],
                  "evidenceIds": [],
                  "relations": [],
                  "notice": "This intent is agent-originated and PENDING HUMAN AUTHORIZATION. Do not implement it until its authorization field reads authorized."
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body (zod) — returns { error, details } with flattened zod errors; OR cookie-session auth missing workspace_id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid/expired API key, or no Bearer key and no valid session cookie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks write scope; OR plan_limit_reached (billing intents quota exceeded); OR cookie auth not a member of the workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Product not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "code INTENT_ID_TAKEN: the supplied id already exists. If it is yours, update it instead of creating it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (intents bucket, 200/h) — { error } with Retry-After header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Insert error, edge-case sync failure (intent rolled back/deleted), or unexpected internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}": {
      "get": {
        "operationId": "get_intents_id",
        "tags": [
          "intents"
        ],
        "summary": "Get a single intent with edge cases, linked evidence ids, and relations.",
        "x-stability": "stable",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "Detail path uses INTENT_WITH_RELATIONS_SELECT, so edgeCases, evidenceIds (from intent_spec_evidence.friction_id), and relations are all fully populated here (unlike the list endpoint). Scoped to the authenticated workspace via .eq('workspace_id', ...). No rate limiting on this GET.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth; identifies the target workspace. Ignored for API-key auth.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK — returns the full mapped intent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "workspaceId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                  "specVersion": "a1b2c3d4e5f6a7b8",
                  "productId": "7d8e9f0a-1b2c-4d3e-9f8a-6b5c4d3e2f1a",
                  "stageName": null,
                  "status": "shipped",
                  "version": 3,
                  "title": "Speed up onboarding",
                  "objective": "Cut time-to-first-value for new workspaces under 10 minutes.",
                  "currentState": "New workspaces require three disconnected setup steps.",
                  "problemSeverity": "high",
                  "outcomes": [
                    {
                      "id": "c4d3e2f1-a0b9-4c8d-7e6f-5a4b3c2d1e0f",
                      "text": "New user reaches first intent in under 10 min",
                      "priority": "must"
                    }
                  ],
                  "healthMetrics": [
                    "activation_rate"
                  ],
                  "scope": {
                    "inScope": [
                      "Signup flow"
                    ],
                    "outOfScope": [
                      "Billing"
                    ]
                  },
                  "strategicAlignment": null,
                  "alignmentNotes": null,
                  "context": {},
                  "constraints": [
                    "No new third-party signup steps"
                  ],
                  "verification": {
                    "checks": [
                      {
                        "id": "check-activation",
                        "kind": "shipped-signal",
                        "description": "Measure time to first intent for new workspaces",
                        "status": "unknown",
                        "verifies": "c4d3e2f1-a0b9-4c8d-7e6f-5a4b3c2d1e0f"
                      }
                    ]
                  },
                  "externalLinks": [],
                  "evidenceAnchors": {
                    "outcome:c4d3e2f1-a0b9-4c8d-7e6f-5a4b3c2d1e0f": [
                      "d3e2f1a0-b9c8-4d7e-6f5a-4b3c2d1e0f9a"
                    ]
                  },
                  "confirmations": [],
                  "decisions": [],
                  "implementationFindings": [],
                  "implementationContext": null,
                  "origin": "agent",
                  "authorization": "authorized",
                  "authorizationNote": null,
                  "createdAt": "2026-08-25T12:00:00.000Z",
                  "updatedAt": "2026-08-30T14:20:00.000Z",
                  "shippedAt": "2026-08-30T14:20:00.000Z",
                  "verifiedAt": null,
                  "edgeCases": [
                    {
                      "id": "b2c1d0e9-f8a7-4b6c-5d4e-3f2a1b0c9d8e",
                      "scenario": "User has no products yet",
                      "expectedBehavior": "Show empty-state prompt"
                    }
                  ],
                  "evidenceIds": [
                    "d3e2f1a0-b9c8-4d7e-6f5a-4b3c2d1e0f9a"
                  ],
                  "relations": [
                    {
                      "targetId": "9e8d7c6b-5a4f-4e3d-2c1b-0a9f8e7d6c5b",
                      "type": "depends_on"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Cookie-session auth only: workspace_id query param missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid/expired API key, or no Bearer key and no valid session cookie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks read scope; OR cookie auth not a member of the workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found (Supabase PGRST116) within this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error or unexpected internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      },
      "patch": {
        "operationId": "patch_intents_id",
        "tags": [
          "intents"
        ],
        "summary": "Update intent content fields (title, objective, outcomes, constraints, etc.). Does NOT change status — use the /status endpoint.",
        "x-stability": "stable",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "Content-only update; status/shipped_at/verified_at are NOT touched here. Always sets updated_at. Requires at least one updatable field (otherwise 400). When edgeCases is provided, the prior intent state is snapshotted first so a failed edge-case sync can roll back. Response is re-fetched with INTENT_WITH_RELATIONS_SELECT so edgeCases/evidenceIds/relations are fully populated. No rate limiting on this PATCH.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth; identifies the target workspace. Ignored for API-key auth.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expectedVersion": {
                    "type": "string",
                    "description": "The specVersion this edit was based on, from a previous read. When supplied and no longer current, the update is refused with 409 and code VERSION_CONFLICT rather than silently overwriting whoever moved it. Opaque: carry it, do not compute or compare it — the value is derived server-side and the algorithm can change without a client release."
                  },
                  "changeRequestId": {
                    "type": "string",
                    "description": "Open structured PM request this repository sync deliberately applies. Requires expectedRepoBodyRevision and an API-key principal."
                  },
                  "expectedRepoBodyRevision": {
                    "type": "string",
                    "description": "Exact base revision carried by changeRequestId. Both the request base and the current intent are compared atomically; a mismatch returns 409 CHANGE_REQUEST_BASE_CONFLICT."
                  },
                  "title": {
                    "type": "string",
                    "description": "Non-empty after trim if provided. Stored as user_goal."
                  },
                  "objective": {
                    "type": "string",
                    "description": "Non-empty after trim if provided."
                  },
                  "currentState": {
                    "type": "string",
                    "description": "Author-written as-is behavior (max 4000 chars). An empty string clears the field (net-new capability has no current state)."
                  },
                  "outcomes": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/OutcomeInput"
                    },
                    "description": "Replaces both outcomes_v2 and the flat outcomes text list. Omit measurement on an outcome to preserve its stored recipe; send measurement: null to clear it explicitly."
                  },
                  "constraints": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Replaces constraints (coerced to trimmed non-empty strings)."
                  },
                  "healthMetrics": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Replaces health_metrics (coerced to trimmed non-empty strings)."
                  },
                  "edgeCases": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Replace-all semantics via replaceIntentEdgeCasesRpc; on failure the intent is rolled back to its prior snapshot and 500 returned."
                  },
                  "verification": {
                    "type": "object",
                    "description": "Replaces the verification plan. checks is canonical; the three older buckets are compatibility inputs. Incoming check status is not trusted over the stored live status."
                  },
                  "problemSeverity": {
                    "type": "string",
                    "enum": [
                      "low",
                      "medium",
                      "high",
                      "critical"
                    ],
                    "description": "Nullable severity enum."
                  },
                  "evidenceAnchors": {
                    "type": "string",
                    "description": "Map of anchor key (e.g. 'objective', 'outcome:<stable-outcome-id>', 'edgeCase:<stable-edge-case-id>', 'constraint:0') to evidence id arrays."
                  },
                  "scope": {
                    "type": "string",
                    "description": "Nullable scope object."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK — returns the full updated intent (re-fetched with relations)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "workspaceId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                  "specVersion": "f8e7d6c5b4a39281",
                  "productId": "7d8e9f0a-1b2c-4d3e-9f8a-6b5c4d3e2f1a",
                  "stageName": null,
                  "status": "draft",
                  "version": 1,
                  "title": "Speed up onboarding (v2)",
                  "objective": "Cut time-to-first-value for new workspaces under 8 minutes.",
                  "currentState": "New workspaces require three disconnected setup steps.",
                  "problemSeverity": "critical",
                  "outcomes": [
                    {
                      "id": "c4d3e2f1-a0b9-4c8d-7e6f-5a4b3c2d1e0f",
                      "text": "New user reaches first intent in under 8 min",
                      "priority": "must"
                    }
                  ],
                  "healthMetrics": [
                    "activation_rate",
                    "time_to_first_intent"
                  ],
                  "scope": null,
                  "strategicAlignment": null,
                  "alignmentNotes": null,
                  "context": {},
                  "constraints": [
                    "No new third-party signup steps"
                  ],
                  "verification": {
                    "checks": [
                      {
                        "id": "check-activation",
                        "kind": "shipped-signal",
                        "description": "Measure time to first intent for new workspaces",
                        "status": "unknown"
                      }
                    ]
                  },
                  "externalLinks": [],
                  "evidenceAnchors": {
                    "objective": [
                      "d3e2f1a0-b9c8-4d7e-6f5a-4b3c2d1e0f9a"
                    ]
                  },
                  "confirmations": [],
                  "decisions": [],
                  "implementationFindings": [],
                  "implementationContext": null,
                  "origin": "agent",
                  "authorization": "authorized",
                  "authorizationNote": null,
                  "createdAt": "2026-08-25T12:00:00.000Z",
                  "updatedAt": "2026-08-31T11:00:00.000Z",
                  "shippedAt": null,
                  "verifiedAt": null,
                  "edgeCases": [
                    {
                      "id": "b2c1d0e9-f8a7-4b6c-5d4e-3f2a1b0c9d8e",
                      "scenario": "User has no products yet",
                      "expectedBehavior": "Show empty-state prompt"
                    }
                  ],
                  "evidenceIds": [
                    "d3e2f1a0-b9c8-4d7e-6f5a-4b3c2d1e0f9a"
                  ],
                  "relations": []
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body (zod) — { error, details }; OR no updatable field provided ('At least one field must be provided to update'); OR cookie-session auth missing workspace_id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid/expired API key, or no Bearer key and no valid session cookie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks write scope; OR cookie auth not a member of the workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found (PGRST116) within this workspace (on snapshot or update)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "code VERSION_CONFLICT: expectedVersion no longer matches. Re-read the intent, reapply the edit on top, and retry with the new specVersion.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error, edge-case sync failure (intent rolled back to prior snapshot), or unexpected internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/status": {
      "patch": {
        "operationId": "patch_intents_id_status",
        "tags": [
          "intents"
        ],
        "summary": "Update an intent's status (draft/validated/approved/shipped/verified), managing shipped_at/verified_at timestamps and returning a verification checklist for shipped/verified transitions.",
        "x-stability": "stable",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "Base response is always { id, status, updatedAt, shippedAt, verifiedAt }. ONLY when status is 'shipped' or 'verified' does it add verificationChecklist (items {text, category} drawn from outcomes [category 'outcome'], health_metrics ['health_metric'], verification.manualChecks ['manual_check'], and active constitution_rules ['constitution']) and verificationChecklistCount. Fires a Slack 'intent_status_changed' notification (fire-and-forget). No billing gate, no rate limiting on this endpoint.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth; identifies the target workspace. Ignored for API-key auth.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "draft",
                      "validated",
                      "approved",
                      "shipped",
                      "verified"
                    ],
                    "description": "Target status. draft/validated/approved clear shipped_at and verified_at; shipped sets shipped_at and clears verified_at; verified sets verified_at."
                  },
                  "verificationResult": {
                    "type": "object",
                    "description": "Optional. When present it is saved to verification_result and a summary is logged as an intent_implementation_notes row (source 'verification_checkpoint')."
                  }
                },
                "required": [
                  "status"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK — returns { id, status, updatedAt, shippedAt, verifiedAt }, plus verificationChecklist + verificationChecklistCount for shipped/verified transitions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "status": "shipped",
                  "updatedAt": "2026-06-07T12:00:00.000Z",
                  "shippedAt": "2026-06-07T12:00:00.000Z",
                  "verifiedAt": null,
                  "verificationChecklist": [
                    {
                      "text": "New user reaches first intent in under 10 min",
                      "category": "outcome"
                    },
                    {
                      "text": "activation_rate",
                      "category": "health_metric"
                    },
                    {
                      "text": "New user can create an intent in under 10 min",
                      "category": "manual_check"
                    },
                    {
                      "text": "Never ship a flow that adds a new required signup field",
                      "category": "constitution"
                    }
                  ],
                  "verificationChecklistCount": 4
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body (zod) — { error, details }; OR cookie-session auth missing workspace_id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid/expired API key, or no Bearer key and no valid session cookie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks write scope; OR cookie auth not a member of the workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found (PGRST116) within this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error updating status or unexpected internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/notes": {
      "post": {
        "operationId": "post_intents_id_notes",
        "tags": [
          "notes"
        ],
        "summary": "Log an implementation note against an intent.",
        "x-stability": "stable",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "general 300/h",
        "description": "Rate-limited by the 'general' bucket (300/h sliding window, keyed user:<userId> or ip:<ip>). The created row's created_by is set to the authenticated userId, but the POST response does NOT echo createdBy (only id, intentId, note, source, createdAt). The GET list response DOES include createdBy.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth; identifies the target workspace. Ignored for API-key auth.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expectedRepoBodyRevision": {
                    "type": "string",
                    "description": "Required for repository-authority intents. Opaque repoBodyRevision returned by the intent API; the server atomically refuses a verdict if the body moved."
                  },
                  "note": {
                    "type": "string",
                    "description": "Non-empty after trim."
                  },
                  "source": {
                    "type": "string",
                    "enum": [
                      "mcp",
                      "cli",
                      "github",
                      "manual",
                      "verification_checkpoint",
                      "linear",
                      "jira",
                      "execution_sync"
                    ],
                    "description": "Origin of the note; defaults to 'manual' when omitted."
                  }
                },
                "required": [
                  "note"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created — returns { id, intentId, note, source, createdAt }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
                  "intentId": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "note": "Shipped behind flag; rollout to 10% of workspaces.",
                  "source": "cli",
                  "createdAt": "2026-06-07T13:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body (zod) — { error, details }; OR cookie-session auth missing workspace_id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid/expired API key, or no Bearer key and no valid session cookie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks write scope; OR cookie auth not a member of the workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found within this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (general bucket, 300/h) — { error } with Retry-After header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error creating note or unexpected internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      },
      "get": {
        "operationId": "get_intents_id_notes",
        "tags": [
          "notes"
        ],
        "summary": "List implementation notes for an intent, newest first.",
        "x-stability": "stable",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "Default order: created_at descending (newest first). Each note includes createdBy (unlike the POST create response, which omits it). No rate limiting on this GET.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth; identifies the target workspace. Ignored for API-key auth.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK — returns { notes, count }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "notes": [
                    {
                      "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
                      "intentId": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                      "note": "Shipped behind flag; rollout to 10% of workspaces.",
                      "source": "cli",
                      "createdBy": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                      "createdAt": "2026-06-07T13:00:00.000Z"
                    }
                  ],
                  "count": 1
                }
              }
            }
          },
          "400": {
            "description": "Cookie-session auth only: workspace_id query param missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid/expired API key, or no Bearer key and no valid session cookie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks read scope; OR cookie auth not a member of the workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found within this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error fetching notes or unexpected internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/findings": {
      "post": {
        "operationId": "post_intents_id_findings",
        "tags": [
          "findings"
        ],
        "summary": "Record a write-back finding against an intent: a contradiction that building the spec surfaced. The inverse of a decision — the next agent that pulls this intent reads open findings as 'the spec may be stale here' so the refinement loop converges instead of repeating a dead premise.",
        "x-stability": "beta",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "general 300/h",
        "description": "Rate-limited by the 'general' bucket (300/h sliding window, keyed user:<userId> or ip:<ip>). Findings are stored as a read-modify-write append on the intent's implementation_findings JSONB array (prior findings are preserved); the merged set is sanitized (capped length, malformed legacy rows dropped) before persisting. Every new finding is created with status 'open' and a server-assigned id + recordedAt (epoch ms); target, correction, and source are echoed back only when supplied. openCount is the number of findings still in status 'open' after the write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth; identifies the target workspace. Ignored for API-key auth.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "assumption": {
                    "type": "string",
                    "description": "What the spec assumed or said before it was built. Non-empty after trim."
                  },
                  "finding": {
                    "type": "string",
                    "description": "What building it actually revealed — the fact that contradicts the assumption. Non-empty after trim."
                  },
                  "target": {
                    "type": "string",
                    "description": "Which part of the spec this contradicts: 'objective', 'outcome:<id>', 'constraint:<index>', 'edgeCase:<id>', 'check:<id>' (a verification check — recording this flips that check to failing), or plain prose. Non-empty after trim when provided."
                  },
                  "correction": {
                    "type": "string",
                    "description": "Proposed correction to the intent, if any. Non-empty after trim when provided."
                  },
                  "source": {
                    "type": "string",
                    "description": "Provenance: where this came from, e.g. 'claude-code @ owner/repo'. Non-empty after trim when provided."
                  }
                },
                "required": [
                  "assumption",
                  "finding"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created — returns { intentId, finding, openCount }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "intentId": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "finding": {
                    "id": "b7c8d9e0-1f2a-4b3c-8d4e-5f6a7b8c9d0e",
                    "assumption": "Onboarding completes in a single page load.",
                    "finding": "The OAuth callback forces a full reload, so step state is lost.",
                    "target": "outcome:c4d3e2f1-a0b9-4c8d-7e6f-5a4b3c2d1e0f",
                    "correction": "Persist step state in the session before the OAuth redirect.",
                    "source": "claude-code @ acme/web",
                    "status": "open",
                    "recordedAt": 1749300000000
                  },
                  "openCount": 1
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body (zod) — { error, details }; OR cookie-session auth missing workspace_id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid/expired API key, or no Bearer key and no valid session cookie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks write scope; OR cookie auth not a member of the workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found within this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (general bucket, 300/h) — { error } with Retry-After header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error recording the finding or unexpected internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      },
      "get": {
        "operationId": "get_intents_id_findings",
        "tags": [
          "findings"
        ],
        "summary": "List the write-back findings for an intent. Optionally filter by status.",
        "x-stability": "beta",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "Findings come from the intent's implementation_findings JSONB array, sanitized on read (capped length, malformed legacy rows dropped). The optional status query param filters in-memory after the sanitize pass; reconciled findings include a reconciledAt timestamp. count reflects the returned (post-filter) set. No rate limiting on this GET.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Optional exact-match filter on finding status. Omit to return findings of every status.",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "reconciled",
                "dismissed"
              ]
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth; identifies the target workspace. Ignored for API-key auth.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK — returns { intentId, findings, count }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "intentId": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "findings": [
                    {
                      "id": "b7c8d9e0-1f2a-4b3c-8d4e-5f6a7b8c9d0e",
                      "assumption": "Onboarding completes in a single page load.",
                      "finding": "The OAuth callback forces a full reload, so step state is lost.",
                      "target": "outcome:c4d3e2f1-a0b9-4c8d-7e6f-5a4b3c2d1e0f",
                      "correction": "Persist step state in the session before the OAuth redirect.",
                      "source": "claude-code @ acme/web",
                      "status": "open",
                      "recordedAt": 1749300000000
                    }
                  ],
                  "count": 1
                }
              }
            }
          },
          "401": {
            "description": "Invalid/expired API key, or no Bearer key and no valid session cookie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks read scope; OR cookie auth not a member of the workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found within this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error fetching findings or unexpected internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/authorization": {
      "post": {
        "operationId": "post_intents_id_authorization",
        "tags": [
          "intents"
        ],
        "summary": "The gate verb: a human authorizes or rejects an agent-originated spec. Cookie-session only by design — an agent can never authorize its own proposal.",
        "x-stability": "beta",
        "x-scope": "none",
        "x-auth": "cookie-session-only",
        "x-rateLimit": "none",
        "description": "Human-only by construction: the route never inspects the Authorization header, so agents cannot clear their own gate. Only meaningful for origin='agent' intents. Repository-authority verdicts are bound to the supplied repoBodyRevision in one transaction; any later material body revision clears the current verdict while preserving immutable history. Also stamps reviewed_at when unset (authorizing implies having seen). Agent-facing surfaces reflect the state: intents responses carry origin + authorization ('pending'/'authorized'/'rejected'; null for human-authored), the agent prompt opens with a DO-NOT-IMPLEMENT banner while pending or rejected, and intent.md frontmatter carries authorization. Emits agent_spec_authorized / agent_spec_rejected server-side.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string",
                    "description": "'authorize' or 'reject'."
                  },
                  "note": {
                    "type": "string",
                    "description": "Optional reason (max 500 chars), most useful on rejection — agents read it on their next pull."
                  },
                  "expectedBuild": {
                    "type": "boolean",
                    "description": "Repository-owned intents: whether the page told the reviewer the agent builds as soon as this is authorized. An authorization is refused with 409 BUILD_DECLARATION_CHANGED when the agent's declaration has changed since; when true and still true, the build is confirmed for the exact revision."
                  }
                },
                "required": [
                  "action"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verdict recorded — returns { intentId, authorizationStatus, authorizedAt }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "intentId": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "authorizationStatus": "authorized",
                  "authorizedAt": "2026-08-23T10:00:00.000Z",
                  "authorizedRepoBodyRevision": "a1b2c3d4e5f6",
                  "buildConfirmed": true
                }
              }
            }
          },
          "400": {
            "description": "Invalid action/note; OR the intent is not agent-originated (human-authored specs are implicitly authorized by authorship)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No valid Supabase session cookie — pm_live_ API keys are rejected by design",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Session user is not an owner/admin/editor of the intent's workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The repository body moved before the verdict settled; re-read and judge the current revision. Or BUILD_DECLARATION_CHANGED: the agent changed whether it builds as soon as this is authorized; look again and decide (buildOnAuthorization carries the current declaration)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "428": {
            "description": "expectedRepoBodyRevision is required for a repository-authority intent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error recording the verdict or unexpected internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/build-review": {
      "post": {
        "operationId": "post_intents_id_build-review",
        "tags": [
          "intents"
        ],
        "summary": "Build with these answers: a signed-in reviewer previews the proposal that answering the last open product choice would produce, then approves building exactly that result or saves the answer without building. Off unless PATHMODE_CHOICE_BUILD_ENABLED is set.",
        "x-stability": "beta",
        "x-scope": "write",
        "x-auth": "cookie-session-only",
        "x-rateLimit": "intents 200/h",
        "description": "Previewing sends no answer and authorizes nothing. Build records the answer and a conditional approval in one transaction; the repository agent still applies the answer. Authorization, and the watch's build confirmation, are recorded together only after the agent's applied result matches the stored preview exactly, and only while the agent still declares a build, the reviewer still has write access, and no other verdict, readiness judgment or change has come in. Anything else marks the approval as differing, and the review page shows what changed for a new decision. The result is graded with the same execution checks as the execution prompt.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": true,
            "description": "Workspace the session user is a member of.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string",
                    "description": "'preview' computes and stores the resulting proposal; 'build' records the answer and a conditional approval of that exact result; 'save' records only the answer."
                  },
                  "choiceId": {
                    "type": "string",
                    "description": "preview only: the last open product choice, answered with its recommendation."
                  },
                  "expectedRepoBodyRevision": {
                    "type": "string",
                    "description": "preview only: the revision the reviewer is looking at."
                  },
                  "previewId": {
                    "type": "string",
                    "description": "build and save: the preview returned earlier to this reviewer."
                  }
                },
                "required": [
                  "action"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "preview: the stored resulting proposal. build or save: the review and the answer it sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "review": {
                    "id": "22222222-2222-4222-8222-222222222222",
                    "status": "pending",
                    "baseRevision": "a1b2c3d4...",
                    "tradeoffs": []
                  },
                  "changeRequest": {
                    "id": "c601f811-c8fa-4bd2-8861-374a29823fb4",
                    "status": "open"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid intent ID or body; client-supplied results or actors are refused.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No valid session cookie.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "An API key, or a session without write access: only a signed-in workspace writer makes this decision.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found, or no such preview for this reviewer, intent and workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The proposal changed, the preview expired, other answers or changes are still open, the agent is not set to build next, it is not the last structured recommendation, or the result would not pass the execution checks.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The feature is off, or the decision could not be prepared or recorded; no approval was extended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/claim-confirmations": {
      "post": {
        "operationId": "post_intents_id_claim-confirmations",
        "tags": [
          "intents"
        ],
        "summary": "A signed-in reviewer confirms, rejects, or withdraws their judgment of ONE claim (objective, outcome, constraint or edge case) in an agent-originated proposal. The judgment is recorded under the reviewer's name and sent to the agent; it neither authorizes nor blocks implementation.",
        "x-stability": "beta",
        "x-scope": "write",
        "x-auth": "cookie-session-only",
        "x-rateLimit": "intents 200/h",
        "description": "Claim judgments are stored in the same column as readiness confirmations but are returned separately, as claimConfirmations on GET /api/v1/intents/:id; the confirmations field carries readiness records only. They never feed the readiness verdict and never change the repository body revision. A judgment is live while the claim still reads as judged; an edited claim shows its judgment as covering the earlier wording. Amending a claim is a change request, not a judgment.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": true,
            "description": "Workspace the session user is a member of.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "claim": {
                    "type": "string",
                    "description": "Claim key: 'objective', 'outcome:<id>', 'constraint:<index>', or 'edgeCase:<id>'."
                  },
                  "action": {
                    "type": "string",
                    "description": "'confirm', 'reject', or 'clear' (withdraw your judgment)."
                  },
                  "anchor": {
                    "type": "string",
                    "description": "Required to confirm or reject: the digest of the claim text you read. A mismatch with the stored text is a 409, never a judgment of text you did not see."
                  },
                  "reason": {
                    "type": "string",
                    "description": "Required to reject (max 600 chars): what is wrong with the claim. The agent receives it."
                  }
                },
                "required": [
                  "claim",
                  "action"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Judgment recorded (or nothing to withdraw) — returns the intent's claimConfirmations and the new record",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "claimConfirmations": [
                    {
                      "claim": "outcome:c4d3e2f1-a0b9-4c8d-7e6f-5a4b3c2d1e0f",
                      "kind": "rejected",
                      "by": "human",
                      "assurance": "authenticated",
                      "source": "browser",
                      "anchor": "9f86d081884c7d65",
                      "claimText": "New user reaches first intent in under 10 min",
                      "reason": "Ten minutes still loses trial users; we agreed on five",
                      "reviewerId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                      "reviewerName": "Pat Reviewer",
                      "at": "2026-09-23T10:00:00.000Z",
                      "repoBodyRevision": "4f2a..."
                    }
                  ],
                  "record": {
                    "claim": "outcome:c4d3e2f1-a0b9-4c8d-7e6f-5a4b3c2d1e0f",
                    "kind": "rejected"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid body; a rejection without a reason (REASON_REQUIRED); or the intent is not agent-originated (NOT_A_PROPOSAL)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No valid session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "An API key (SESSION_REQUIRED): an agent cannot judge its own proposal; or the member is not an owner/admin/editor",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found, or the proposal no longer makes that claim (CLAIM_NOT_FOUND)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The claim text changed after it was read (CLAIM_CHANGED, with currentText); or the row kept changing mid-write (VERSION_CONFLICT)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The intent already holds the maximum number of claim judgments (CLAIM_LIMIT)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error or unexpected internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/change-requests": {
      "get": {
        "operationId": "get_change-requests",
        "tags": [
          "change requests"
        ],
        "summary": "List structured change requests across the workspace for dashboard re-entry or agent discovery. API-key reads see consumable rows by default and stamp consumption only for those; unaccepted agent proposals are never handed to a different key.",
        "x-stability": "beta",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "This is the discovery projection used by Workspace/Product Home and by list_intent_change_requests when intentId is omitted. Session reads never count as repository-agent consumption.",
        "parameters": [
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "API-key reads only: consumable (default) = accepted PM judgment the agent may act on; proposals = the caller's own pending agent proposals; all = both. Session reads always see every row.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "open (default), applied, rejected, withdrawn, superseded, or all.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK — returns workspace-scoped { changeRequests, count }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "changeRequests": [
                    {
                      "id": "3f2a...",
                      "intentId": "9c1e...",
                      "status": "open"
                    }
                  ],
                  "count": 1
                }
              }
            }
          },
          "400": {
            "description": "Invalid status filter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key/session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error, or API-key consumption could not be recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/change-requests": {
      "get": {
        "operationId": "get_intents_id_change-requests",
        "tags": [
          "change requests"
        ],
        "summary": "List structured change requests for one repository-authority intent. Each row carries authorPrincipal, adjudication (pending | accepted | null) and consumable; API-key reads see consumable rows by default and stamp consumption without claiming application.",
        "x-stability": "beta",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "An API-key GET is the connected-agent consumption event: every returned open request is stamped with first/last consumed time and key id. Session reads do not stamp consumption. Reading never changes request status.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "API-key reads only: consumable (default) = accepted PM judgment the agent may act on; proposals = the caller's own pending agent proposals; all = both. Session reads always see every row.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "open (default), applied, rejected, withdrawn, superseded, or all.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK — returns { changeRequests, count }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "changeRequests": [
                    {
                      "id": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                      "intentId": "9c1e7a6b-5c4d-4e2a-8b4d-3f2a9c1e7a6b",
                      "baseRepoBodyRevision": "a1b2c3d4e5f6",
                      "targetField": "objective",
                      "expectedOldValue": "Old objective",
                      "operation": "replace",
                      "proposedValue": "New objective",
                      "reason": "Customer evidence changed",
                      "status": "open",
                      "createdAt": 1788249600000,
                      "updatedAt": 1788249600000
                    }
                  ],
                  "count": 1
                }
              }
            }
          },
          "400": {
            "description": "Invalid status filter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key/session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error, or agent consumption could not be recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      },
      "post": {
        "operationId": "post_intents_id_change-requests",
        "tags": [
          "change requests"
        ],
        "summary": "Session: propose a structured PM amendment against the exact current repository-body revision without mutating intent.md. API key: record an agent PROPOSAL against a recorded implementation finding; it is stored as a change request the repository agent cannot consume or apply until a signed-in writer accepts it, and one origin=agent evidence row is written and anchored to the target claim.",
        "x-stability": "beta",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "Creation runs under the intent row lock and validates the expected value against the immutable base snapshot for both principals. It inserts only intent_change_requests (plus, for agent proposals, one evidence_items row, an additive evidence anchor and, when the finding is recorded inline, the finding itself, all in the same transaction); the repo-owned intent body is never edited. sourceUrl must be http(s). Consumable = status open AND (authorPrincipal session OR accepted). Plan: .claude/plans/agent-proposed-corrections.md.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "findingId": {
                    "type": "string",
                    "description": "Agent proposals only: id of the implementation finding this proposal answers (required unless finding is given)."
                  },
                  "finding": {
                    "type": "object",
                    "description": "Agent proposals only: record the finding inline, { assumption, finding, target?, source? }."
                  },
                  "sourceUrl": {
                    "type": "string",
                    "description": "Agent proposals only: PR or commit link stored on the evidence row."
                  },
                  "supersedesRequestId": {
                    "type": "string",
                    "description": "Session only (counter-proposal): the open request this one replaces; it becomes superseded in the same transaction."
                  },
                  "baseRepoBodyRevision": {
                    "type": "string",
                    "description": "Exact 64-character revision shown to the PM. A stale base returns 409; it is never silently rebased."
                  },
                  "targetField": {
                    "type": "string",
                    "description": "Semantic field or collection: title, severity, objective, currentState, outcomes, scope.inScope, scope.outOfScope, constraints, healthMetrics, edgeCases, decisions, verification, or implementationContext."
                  },
                  "targetItemId": {
                    "type": "string",
                    "description": "Stable item id for outcome/verification items when one exists. Index-only targeting is forbidden."
                  },
                  "expectedOldValue": {
                    "type": "string",
                    "description": "Required for replace/remove when no stable item id exists; compared to the immutable base snapshot."
                  },
                  "operation": {
                    "type": "string",
                    "description": "replace, add, or remove. Scalar fields support replace only."
                  },
                  "proposedValue": {
                    "type": "string",
                    "description": "Required for add/replace; forbidden for remove."
                  },
                  "reason": {
                    "type": "string",
                    "description": "Why the product judgment requires this change (max 4000 chars)."
                  }
                },
                "required": [
                  "baseRepoBodyRevision",
                  "targetField",
                  "operation",
                  "reason"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Request stored against the exact base revision",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "changeRequest": {
                    "id": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                    "baseRepoBodyRevision": "a1b2c3d4e5f6",
                    "targetField": "objective",
                    "operation": "replace",
                    "status": "open"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid structured operation/value, or FINDING_REQUIRED (an API key proposed without findingId or finding)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key/session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Writer permission missing, or an API key attempted a counter-proposal (SESSION_REQUIRED)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "FINDING_NOT_FOUND: findingId is not on this intent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Base revision or expected old value moved, or the counter-proposal target is no longer open",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The intent is not repository-authority",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error creating the request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "SCHEMA_MIGRATION_PENDING: the change-request functions in the database are out of date",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/change-requests/{requestId}": {
      "get": {
        "operationId": "get_intents_id_change-requests_requestId",
        "tags": [
          "change requests"
        ],
        "summary": "Read one structured PM request. API-key reads record that the repository agent consumed an open request.",
        "x-stability": "beta",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "The response includes server-derived resolution metadata after application/supersession. No caller-supplied success flag or diff is accepted.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "description": "requestId path parameter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK — returns { changeRequest }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "changeRequest": {
                    "id": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                    "intentId": "9c1e7a6b-5c4d-4e2a-8b4d-3f2a9c1e7a6b",
                    "baseRepoBodyRevision": "a1b2c3d4e5f6",
                    "targetField": "objective",
                    "operation": "replace",
                    "status": "open"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key/session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Request not found for this intent/workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error, or agent consumption could not be recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      },
      "patch": {
        "operationId": "patch_intents_id_change-requests_requestId",
        "tags": [
          "change requests"
        ],
        "summary": "Reject an unworkable open request, withdraw it from a signed-in workspace session, accept a pending agent proposal (signed-in only) so the repository agent may consume and apply it, or ask the agent to retry a refused apply (signed-in only). Applied/superseded states remain server-owned.",
        "x-stability": "beta",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "accept stamps a pending agent proposal as human-accepted (status stays open; it becomes consumable). applied is set only by the revision settlement transaction after a request-scoped repo sync satisfies the operation; unrelated body movement supersedes the request.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "description": "requestId path parameter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string",
                    "description": "reject, withdraw, accept, or retry. API keys may reject; only a signed-in writer may withdraw, accept, or retry. Accept applies only to an open, unaccepted agent proposal. Retry applies only to an open request whose last apply was refused for a recoverable reason; it leaves the request open and unchanged and records apply_retry_requested_at, which makes it actionable for a listening agent again."
                  },
                  "reason": {
                    "type": "string",
                    "description": "Concrete resolution reason (max 4000 chars). Required for reject and withdraw."
                  }
                },
                "required": [
                  "action"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Open request closed with actor/principal and reason",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "changeRequest": {
                    "id": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                    "status": "rejected",
                    "resolutionPrincipal": "api-key",
                    "resolutionReason": "The requested invariant conflicts with the payment provider"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid action or missing reason",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key/session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks write permission, or an API key attempted withdraw/accept/retry (SESSION_REQUIRED)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Request not found for this intent/workspace, or an agent proposal this API key may not see (another key's pending proposal)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Request is no longer open (CHANGE_REQUEST_NOT_OPEN or, for retry, NOT_OPEN), no pending proposal to accept (PROPOSAL_NOT_PENDING), nothing to retry (NOT_REFUSED), or a refusal no retry can fix (TERMINAL_REFUSAL: intent.md moved past the request's base; answer again instead)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error closing the request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "SCHEMA_MIGRATION_PENDING: the change-request functions in the database are out of date",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/premerge-reviews": {
      "get": {
        "operationId": "get_intents_id_premerge-reviews",
        "tags": [
          "pre-merge review"
        ],
        "summary": "List the advisory pre-merge review runs recorded for this intent, newest first: the PR head SHA the grader read, the intent revision it graded against, the per-item verdict with citations, the human resolutions, and how the result was reported on the pull request.",
        "x-stability": "beta",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "A run is written by the GitHub App webhook on pull_request opened / synchronize / reopened for PRs that reference the intent (same grader and diff reader as the merge path). status is pending (claimed, grader not finished), completed (a verdict exists) or unavailable (quota, unreadable diff, grader error) with unavailableReason; neither pending nor unavailable is ever a pass. verdictLabel / verdictTone are the one description every surface renders (an unclear-only verdict reads as attention, not as resolved). The judgment identity is (intent, repo, pr, headSha, intentRevision, inputDigest): inputDigest hashes the exact grader input (spec + constitution rules), so a changed verification check or rule on the same head is a new judgment. intentRevisionKind is repo_body (intent.md revision, snapshot in intent_spec_revisions) or content_hash (the graded spec is stored with the run). itemKey is the identity a resolution attaches to: verification:<checkId> for verification items, <category>:<item text> otherwise. canWrite is true only for a signed-in writer; API keys always read canWrite=false because the agent never adjudicates its own grading. reportStatus is not_attempted | posted | permission_missing (the installation has not accepted the App's checks:write permission) | failed. Advisory throughout: the GitHub check run concludes success or neutral, never failure. Plan: .claude/plans/advisory-premerge-review.md.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pr",
            "in": "query",
            "required": false,
            "description": "Restrict to one pull request number.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Max runs to return (1-100, default 25).",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK — returns { premergeReviews, count, canWrite }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "premergeReviews": [
                    {
                      "id": "6b1f...",
                      "repo": "acme/web",
                      "prNumber": 42,
                      "headSha": "9f3c2a1b...",
                      "intentRevision": "a1b2c3d4...",
                      "intentRevisionKind": "repo_body",
                      "status": "completed",
                      "pass": false,
                      "score": 60,
                      "unresolvedFails": 1,
                      "items": [
                        {
                          "itemKey": "constraint:Reset links expire after 15 minutes",
                          "category": "constraint",
                          "status": "fail",
                          "reasoning": "No expiry is set.",
                          "evidence": [
                            {
                              "path": "src/auth/reset.ts"
                            }
                          ]
                        }
                      ],
                      "resolutions": [],
                      "reportStatus": "posted",
                      "checkRunId": 4242,
                      "mergedAt": null
                    }
                  ],
                  "count": 1,
                  "canWrite": true
                }
              }
            }
          },
          "400": {
            "description": "Invalid pr or limit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key/session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error listing runs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/premerge-reviews/{reviewId}/resolutions": {
      "post": {
        "operationId": "post_intents_id_premerge-reviews_reviewId_resolutions",
        "tags": [
          "pre-merge review"
        ],
        "summary": "A signed-in writer accepts, rejects or waives one graded item of one pre-merge run with a reason. The GitHub check run is re-posted so the decision shows on the pull request.",
        "x-stability": "beta",
        "x-scope": "write",
        "x-auth": "cookie-session-only",
        "x-rateLimit": "none",
        "description": "One resolution per itemKey per run; a later decision replaces the earlier one on the run (both remain in analytics). When the PR receives a new head, resolutions carry to the new run only for items whose judgment is unchanged (same key, same status), and the carried copy names the run where the person decided (carriedFrom). The merge path copies the run that graded the merged head, with its resolutions, into the intent's verification_result.premerge and the implementation note. Resolutions are deliberate human judgment and count toward the loop metric; automatic runs do not.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "reviewId",
            "in": "path",
            "required": true,
            "description": "reviewId path parameter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "itemKey": {
                    "type": "string",
                    "description": "The item's key as returned by GET (verification:<checkId> or <category>:<item text>)."
                  },
                  "action": {
                    "type": "string",
                    "description": "accept (the drift is real and will be fixed), reject (the grader is wrong), or waive (real, merging anyway)."
                  },
                  "reason": {
                    "type": "string",
                    "description": "Why (max 4000 chars). Required for all three actions."
                  }
                },
                "required": [
                  "itemKey",
                  "action",
                  "reason"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Decision stored; returns the updated run",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "premergeReview": {
                    "id": "6b1f...",
                    "unresolvedFails": 0,
                    "resolutions": [
                      {
                        "itemKey": "constraint:Reset links expire after 15 minutes",
                        "action": "waive",
                        "reason": "Expiry ships in the follow-up PR",
                        "userId": "...",
                        "at": 1756900000000
                      }
                    ],
                    "reportStatus": "posted"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid body (unknown action, missing reason)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks write permission, or an API key attempted to resolve (SESSION_REQUIRED)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Run not found for this intent/workspace, or no graded item with that itemKey (ITEM_NOT_FOUND)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The run was unavailable and has no verdict (REVIEW_UNAVAILABLE), or it changed while deciding (VERSION_CONFLICT)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error storing the decision",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/revision-diff": {
      "get": {
        "operationId": "get_intents_id_revision-diff",
        "tags": [
          "change requests"
        ],
        "summary": "Derive the semantic field delta from the last authorized repository revision to the exact current revision.",
        "x-stability": "beta",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "The client supplies no diff. The server compares persisted canonical snapshots and returns only semantic repo-owned fields.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Expected current 64-character repoBodyRevision. Returns 409 if it moved.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK — delta derived from immutable server snapshots",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "fromRepoBodyRevision": "a1b2...",
                  "toRepoBodyRevision": "c3d4...",
                  "changedFields": [
                    {
                      "field": "objective",
                      "before": "Old",
                      "after": "New"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid to revision",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key/session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The requested to revision is no longer current",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Intent is not repository-authority",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Revision or authorization history could not be read",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/decisions": {
      "post": {
        "operationId": "post_intents_id_decisions",
        "tags": [
          "decisions"
        ],
        "summary": "Record a decision on an intent from outside the editor conversation — a call made in a PR review, a Slack thread, or by an agent mid-build. Appends to the intent's decision audit trail (what was chosen, what was ruled out, why, and what would reopen it).",
        "x-stability": "beta",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "general 300/h",
        "description": "Rate-limited by the 'general' bucket (300/h sliding window, keyed user:<userId> or ip:<ip>). The append is an optimistic-concurrency compare-and-swap fenced on the intent's updated_at (up to 3 attempts), so two agents posting simultaneously cannot overwrite each other's decision; a persistently lost race returns 409. Appends beyond the 100-decision cap are rejected with 409 rather than silently discarded by the sanitizer. The merged set is sanitized (malformed legacy rows dropped) before persisting. The server assigns id (UUID) and resolvedAt (epoch ms). evidenceIds are validated against the workspace's evidence_items and non-resolving ids are dropped; the field is omitted when none resolve. Emits the server-side 'decision_recorded' analytics event with source 'api'.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth; identifies the target workspace. Ignored for API-key auth.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "choice": {
                    "type": "string",
                    "description": "What was chosen. Non-empty after trim, max 500 chars."
                  },
                  "reason": {
                    "type": "string",
                    "description": "Why — the evidence or reasoning behind the call. Non-empty after trim, max 1000 chars."
                  },
                  "ruledOut": {
                    "type": "string",
                    "description": "The alternative that was ruled out. Non-empty after trim, max 500 chars."
                  },
                  "reopenTrigger": {
                    "type": "string",
                    "description": "What new evidence or change would reopen this decision. Non-empty after trim, max 500 chars."
                  },
                  "evidenceIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Evidence item UUIDs backing the decision (max 20). Ids are verified against the workspace's evidence_items; ids that don't resolve are silently dropped so a caller cannot fabricate provenance."
                  }
                },
                "required": [
                  "choice",
                  "reason"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created — returns { intentId, decision, decisionCount }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "intentId": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "decision": {
                    "id": "b7c8d9e0-1f2a-4b3c-8d4e-5f6a7b8c9d0e",
                    "choice": "Collapse the six address fields to one type-ahead",
                    "reason": "35% drop at the address step across funnel data and 6 of 8 usability sessions.",
                    "ruledOut": "Progressive disclosure of the existing six fields",
                    "reopenTrigger": "Type-ahead coverage proves insufficient for rural addresses",
                    "evidenceIds": [
                      "d3e2f1a0-b9c8-4d7e-6f5a-4b3c2d1e0f9a"
                    ],
                    "resolvedAt": 1755800000000
                  },
                  "decisionCount": 3
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body (zod) — { error, details }; OR cookie-session auth missing workspace_id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid/expired API key, or no Bearer key and no valid session cookie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks write scope; OR cookie auth not a member of the workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found within this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Decision log at capacity (100) — remove or supersede an existing decision first; OR persistent concurrent updates to the intent (retry the request)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (general bucket, 300/h) — { error } with Retry-After header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error recording the decision or unexpected internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      },
      "get": {
        "operationId": "get_intents_id_decisions",
        "tags": [
          "decisions"
        ],
        "summary": "List the decisions recorded on an intent — the audit trail of what was chosen, ruled out, why, and what would reopen each call.",
        "x-stability": "beta",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "Decisions come from the intent's decisions JSONB array, sanitized on read (capped at 100, malformed legacy rows dropped; legacy rows may carry resolvedAt 0). ruledOut, reopenTrigger, and evidenceIds appear only when set. No rate limiting on this GET.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth; identifies the target workspace. Ignored for API-key auth.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK — returns { intentId, decisions, count }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "intentId": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "decisions": [
                    {
                      "id": "b7c8d9e0-1f2a-4b3c-8d4e-5f6a7b8c9d0e",
                      "choice": "Collapse the six address fields to one type-ahead",
                      "reason": "35% drop at the address step across funnel data and 6 of 8 usability sessions.",
                      "ruledOut": "Progressive disclosure of the existing six fields",
                      "reopenTrigger": "Type-ahead coverage proves insufficient for rural addresses",
                      "evidenceIds": [
                        "d3e2f1a0-b9c8-4d7e-6f5a-4b3c2d1e0f9a"
                      ],
                      "resolvedAt": 1755800000000
                    }
                  ],
                  "count": 1
                }
              }
            }
          },
          "401": {
            "description": "Invalid/expired API key, or no Bearer key and no valid session cookie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks read scope; OR cookie auth not a member of the workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found within this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error fetching decisions or unexpected internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/evidence": {
      "get": {
        "operationId": "get_intents_id_evidence",
        "tags": [
          "evidence"
        ],
        "summary": "The evidence an intent cites (direct links, section anchors, decision citations), resolved through the evidence trust contract: archived and dismissed items never appear, every item carries a trust tier, and evidenceTiers is computed over the same cited set. The MCP server uses this for get_current_intent include_evidence=true. Each call records an exposure event (tier counts only).",
        "x-stability": "beta",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "general 300/h",
        "description": "Read-only handoff endpoint introduced with the evidence trust contract (Sep 2026). Prefer it over GET /api/v1/evidence when you need exactly what an intent cites: it resolves every citation regardless of product size and reports per-tier counts that agree with GET /api/v1/intents/:id evidenceTiers.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cited evidence with trust tiers. tier is one of backing (human-reviewed), unreviewed (captured, not yet reviewed), assumption (AI-drafted hypothesis, not observed). Dismissed and archived citations are counted in evidenceTiers.excluded and never returned.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "intentId": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "evidence": [
                    {
                      "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "type": "friction",
                      "content": "Users abandon checkout at the payment step",
                      "severity": "high",
                      "origin": "manual",
                      "triageStatus": "triaged",
                      "tier": "backing"
                    },
                    {
                      "id": "b2c3d4e5-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                      "type": "observation",
                      "content": "Users may want a faster checkout",
                      "origin": "assumption",
                      "triageStatus": "inbox",
                      "tier": "assumption"
                    }
                  ],
                  "evidenceTiers": {
                    "backing": 1,
                    "unreviewed": 0,
                    "assumption": 1,
                    "excluded": 0
                  },
                  "unresolvedEvidenceIds": []
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key or session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Key lacks read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      },
      "post": {
        "operationId": "post_intents_id_evidence",
        "tags": [
          "evidence"
        ],
        "summary": "Link and/or unlink evidence items to/from an intent. Dual-writes the intent_spec_evidence join table and the linked_intent_ids array on evidence_items.",
        "x-stability": "stable",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "general 300/h",
        "description": "evidenceIds in the response is re-fetched from intent_spec_evidence after applying link/unlink, so it reflects ALL currently-linked evidence (not just what was changed). link/unlink counts only increment for IDs that exist in the workspace. Cookie-session auth requires ?workspace_id= and a workspace membership. Claim Trust Loop (slice A): `claims` lists every claim the intent makes (objective, each outcome, each constraint, each edge case) with `key` (the anchor key), `kind`, `claimText`, the deterministic `state` (backed when any anchored item is backing; else unreviewed; else assumption; else none), per-tier `counts`, and `items` (the exact anchored evidence with tier, origin, source, and `reviewedBy` / `reviewedAt`, which are populated from the latest attributed review; null for legacy rows reviewed before attribution existed, and a reviewer is never inferred). `general` lists intent-level links anchored to no claim; they never make a claim backed. `evidenceAnchors` is the current anchor map and `canWrite` is true only for a signed-in writer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "link": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Evidence item UUIDs to link. Validated against the workspace; unknown IDs silently skipped. At least one of link/unlink must be a non-empty array."
                  },
                  "unlink": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Evidence item UUIDs to unlink. Validated against the workspace; unknown IDs silently skipped. At least one of link/unlink must be a non-empty array."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Link/unlink applied; returns counts and the intent's full current evidence ID list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "intentId": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "evidenceIds": [
                    "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                    "b2c3d4e5-6f7a-4b8c-9d0e-1f2a3b4c5d6e"
                  ],
                  "linked": 2,
                  "unlinked": 0
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body (zod) — e.g. neither link nor unlink is a non-empty array",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key or session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated but lacks write scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "general rate limit exceeded (300/h per user)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/evidence/candidates": {
      "get": {
        "operationId": "get_intents_id_evidence_candidates",
        "tags": [
          "evidence"
        ],
        "summary": "Bounded candidate search behind the claim row's Link evidence picker. Returns up to 25 non-excluded evidence items from the intent's product, each with its trust tier and a short content excerpt, optionally excluding items already anchored to one claim. Never returns the product's full inventory.",
        "x-stability": "beta",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "general 300/h",
        "description": "Claim Trust Loop closeout (Sep 2026). Amendment 4 of the evidence trust contract forbids resolving claim backing from a browser-side copy of the product inventory, so the IntentSpec picker calls this route instead of listing all product evidence. Visibility is decided server-side: the intent must live in the caller's workspace and candidates are restricted to the intent's product; archived and dismissed rows never appear. Anchoring is not review: tier is reported so the picker can show what an item is, and turning an observed signal into backing remains the session-only atomic review action.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Free-text search over content and source (case-insensitive, max 200 chars). PostgREST filter characters are stripped before matching.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum candidates to return. Default 8; values above 25 are clamped to 25, never rejected.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "anchorKey",
            "in": "query",
            "required": false,
            "description": "A claim anchor key (objective, outcome:<id>, constraint:<n>, edgeCase:<id>). Items already anchored to that claim are excluded from the result.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required for cookie-session auth; ignored for API keys, whose workspace is fixed.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Candidates with trust tiers. tier is one of backing, unreviewed, assumption; excluded items are never returned.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "intentId": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "limit": 8,
                  "candidates": [
                    {
                      "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "type": "friction",
                      "severity": "high",
                      "source": "Support tickets",
                      "origin": "integration",
                      "triageStatus": "inbox",
                      "status": "active",
                      "tier": "unreviewed",
                      "excerpt": "Users abandon checkout at the payment step"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid query (zod)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key or session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Key lacks read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "INTENT_HAS_NO_PRODUCT: the intent has no product, and candidates require a product scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "general rate limit exceeded (300/h per user)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/evidence/anchors": {
      "post": {
        "operationId": "post_intents_id_evidence_anchors",
        "tags": [
          "evidence"
        ],
        "summary": "Anchor one evidence item to one exact claim of the intent (objective, outcome, constraint, or edge case). Changes where the item backs, never whether it is reviewed: the item's tier is untouched.",
        "x-stability": "beta",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "general",
        "description": "The row-locked anchor_evidence_to_intent RPC validates the live claim and commits its anchor, join row, and reverse link atomically, so concurrent writes cannot drop it or leave the relationship partially synchronized. Emits evidence_anchored (claim kind, tier, source claim_row) and, when newly linked, evidence_linked. Review with attribution (moving an unreviewed item to backing) is a separate, session-only action in the Claim Trust Loop; this endpoint never changes triage_status. Plan: .claude/plans/evidence-trust-contract.md (Claim Trust Loop, slice A).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "evidenceId": {
                    "type": "string",
                    "description": "UUID of an evidence item in this workspace (same product as the intent when both are set)."
                  },
                  "anchorKey": {
                    "type": "string",
                    "description": "One of the claim keys returned by GET /evidence: objective, outcome:<id>, constraint:<index>, edgeCase:<id>."
                  }
                },
                "required": [
                  "evidenceId",
                  "anchorKey"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Anchored (idempotent: an existing pair returns alreadyAnchored=true)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "intentId": "9c1e...",
                  "evidenceId": "3f2a...",
                  "anchorKey": "outcome:o1",
                  "tier": "unreviewed",
                  "evidenceAnchors": {
                    "outcome:o1": [
                      "3f2a..."
                    ]
                  },
                  "alreadyAnchored": false
                }
              }
            }
          },
          "400": {
            "description": "Invalid body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key/session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks write scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found, CLAIM_NOT_FOUND (anchorKey names no claim on this intent), or EVIDENCE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "EVIDENCE_EXCLUDED (archived or dismissed) or PRODUCT_MISMATCH",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Anchor merge failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/verify": {
      "post": {
        "operationId": "post_intents_id_verify",
        "tags": [
          "intents"
        ],
        "summary": "Grade an implementation against the intent spec by reading the pull request. With prUrl, Pathmode fetches the PR's changed files from GitHub and grades each outcome, constraint, constitution rule, edge case and verification check against the diff. Without it, the run is graded on the caller's own description and reported as basis 'self_report'. Stores the result on the intent and logs a verification_checkpoint implementation note.",
        "x-stability": "stable",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "ai",
        "description": "category enum is outcome|constraint|constitution|edge_case|verification|decision; status enum is pass|fail|unclear|not_applicable. not_applicable means the change does not touch what the item governs; such items leave both the score and the pass decision entirely, so a workspace's standing constitution rules only count against changes that engage them. Outcomes are never not_applicable (coerced to unclear): an outcome the change does not reach is one not achieved yet. A run where every item was inapplicable fails, since engaging nothing verifies nothing. basis is 'artifact' when a pull request diff was read and 'self_report' when it was not; artifact records what was actually read (repo, prNumber, headSha, filesRead, filesOmitted, truncated) or why nothing was, via artifact.reason (no_pointer|not_connected|permission_denied|not_found|repo_not_installed|fetch_failed). prUrl must name a repository the workspace's GitHub installation actually covers: a GitHub App installation token can read any public repository, so readability alone is not provenance, and a PR from an unconnected repo degrades to self_report with reason repo_not_installed rather than being graded as evidence. Each result item carries basis ('artifact' = it cites the diff, 'self_report' = the run had no diff, 'none' = a diff existed but this verdict points at nothing in it) and evidence (validated citations: a cited path must be a file the grader was shown or it is discarded, and a cited hunk must appear in that file's patch or it is dropped while the path survives). On a diff-graded run, a pass with no valid citation is downgraded to unclear with basis 'none' BEFORE pass/score are computed, so an unbacked green feeds the outcome rule and the non-negotiable blocking like any other unclear. Uncited fails are not downgraded: weakening the strict direction would make missing citations lenient. A prUrl that cannot be read degrades the run to self_report rather than failing it, because acceptance of the GitHub App's pull_requests permission is per installation. Diffs are budgeted: files are ranked against the spec's wording and patches are included up to 60000 characters, with the remainder counted in filesOmitted and flagged by truncated. pass and score are computed server-side from the per-item statuses, not returned by the model. A run passes only when three things hold: no item failed; no constraint, constitution, or decision item is unclear, since an unconfirmed non-negotiable is not a pass; and at least one outcome item passed, since a verdict that cannot point to a single achieved outcome has established nothing. The outcome rule applies only when the spec states outcomes. Otherwise outcomes stay forgiving: unclear on an outcome, edge_case or verification item lowers the score without failing the run, so a spec with several outcomes passes on one confirmed. score is 0-100 with pass=1, unclear=0.5, fail=0 credit per item; a run with no items scores 0 and does not pass. Side effects: writes verification_result + updated_at on intent_specs, and inserts an intent_implementation_notes row with source 'verification_checkpoint'. Rate limited on the 'ai' bucket and counted against the workspace ai_conversations quota (403 when the monthly allowance is spent).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "prUrl": {
                    "type": "string",
                    "description": "Pull request URL (https://github.com/owner/repo/pull/123, the api.github.com equivalent, or the owner/repo#123 shorthand). Preferred input: Pathmode reads the diff from GitHub, so the evidence is not authored by the party being graded. Requires an active GitHub connection whose installation has accepted pull_requests:read."
                  },
                  "summary": {
                    "type": "string",
                    "description": "What was implemented and how. Context for the grader, not evidence. Required only when prUrl is absent. At least one of prUrl or summary must be present."
                  },
                  "codeChanges": {
                    "type": "string",
                    "description": "Deprecated, superseded by prUrl. Used as the description when no summary is given. A diff supplied by the caller about its own work is not independent evidence."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verification graded; returns the VerificationResult object",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "pass": false,
                  "score": 75,
                  "basis": "artifact",
                  "results": [
                    {
                      "item": "Users can reset password from the login screen",
                      "category": "outcome",
                      "status": "pass",
                      "reasoning": "components/Login.tsx adds a reset link wired to the existing reset flow.",
                      "basis": "artifact",
                      "evidence": [
                        {
                          "path": "components/Login.tsx",
                          "hunk": "+  <ResetLink href=\"/reset\" />"
                        }
                      ]
                    },
                    {
                      "item": "No PII is written to logs",
                      "category": "constitution",
                      "status": "unclear",
                      "reasoning": "No logging change appears in the diff, so compliance cannot be settled either way.",
                      "basis": "none"
                    }
                  ],
                  "summary": "The reset flow is present in the diff and matches the primary outcome. Nothing in the changed files touches logging, so the no-PII rule could not be confirmed.",
                  "artifact": {
                    "kind": "pr_diff",
                    "basis": "artifact",
                    "repo": "acme/web",
                    "prNumber": 412,
                    "headSha": "9f2c1ab4",
                    "htmlUrl": "https://github.com/acme/web/pull/412",
                    "filesRead": 6,
                    "filesOmitted": 0,
                    "truncated": false
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body (zod) — e.g. missing/empty summary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key or session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated but lacks write scope, or the workspace's monthly AI conversation limit is reached",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited ('ai' bucket)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "AI verification service temporarily unavailable (Gemini API error / 429 / 503)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/prompt": {
      "get": {
        "operationId": "get_intents_id_prompt",
        "tags": [
          "intents"
        ],
        "summary": "Generate the formatted agent prompt for an intent. Assembles spec + workspace strategy + product + constitution rules + linked evidence and returns both a markdown/string prompt and a structured JSON payload.",
        "x-stability": "stable",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "json is an opaque object whose exact shape is produced by generateAgentPrompt (varies by agent_type/format) — fields above are illustrative. The 422 body is { error, readiness: { isSubstantive, isReadyForExecution, score, passed, total, verdict, blockers[], warnings[] }, suggestion }; blockers are the exact unresolved six-gate Preflight failures and evidence remains advisory. implementation_context is stripped from the export unless the workspace flag implementation_context_enabled is on. Constitution rules are prefixed [BLOCKING]/[advisory] by enforcement level. Date fields (createdAt/updatedAt/shippedAt/verifiedAt) are converted to epoch ms internally before prompt generation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "agent_type",
            "in": "query",
            "required": false,
            "description": "Target agent: cursor|windsurf|claude-code|codex|generic. Default 'claude-code'.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mode",
            "in": "query",
            "required": false,
            "description": "draft|execute. Default 'execute'. In 'execute' mode the intent must pass the canonical six-gate Preflight (title, objective, outcomes, constraints, edge cases, verification), including valid confirmations or waivers, or 422.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "json|markdown. Default 'markdown'. Shapes the prompt string.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required only when authenticating via session cookie (ignored for API-key auth).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns { prompt, json }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "prompt": "# Intent: Add password reset\n\n## Objective\nLet users recover access without contacting support...",
                  "json": {
                    "title": "Add password reset",
                    "objective": "Let users recover access without contacting support.",
                    "outcomes": [
                      "Users can reset password from the login screen"
                    ],
                    "constraints": [],
                    "edgeCases": [],
                    "constitutionRules": [
                      "[BLOCKING] No PII in logs"
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key or session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated but lacks read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "mode=execute but intent is not ready for execution; body includes { error, readiness, suggestion }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/share": {
      "post": {
        "operationId": "post_intents_id_share",
        "tags": [
          "intents"
        ],
        "summary": "Generate (or return existing) a public review token and set whether the link includes evidence content. Existing tokens are preserved while their disclosure mode is updated.",
        "x-stability": "stable",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "token is a raw crypto.randomUUID() (v4 UUID, no prefix). Token and includeEvidence are persisted in one intent_specs UPDATE. A bodyless request is supported and resets the link to metadata-only. No 201 — created and pre-existing tokens both return 200. No rate limiter on this route.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required only when authenticating via session cookie (ignored for API-key auth).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "includeEvidence": {
                    "type": "boolean",
                    "description": "Include linked evidence content and source in the public response. Defaults to false; metadata (id, type, severity) remains visible."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Returns { token, includeEvidence } after atomically saving both settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "token": "7a6b5c4d-3e2f-4a1b-9c8d-2e1f0a9b8c7d",
                  "includeEvidence": false
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON body or includeEvidence is not a boolean",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key or session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated but lacks write scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Failed to generate share link",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      },
      "delete": {
        "operationId": "delete_intents_id_share",
        "tags": [
          "intents"
        ],
        "summary": "Revoke the intent's share token and reset evidence disclosure to metadata-only.",
        "x-stability": "stable",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "Revocation atomically sets share_token to null and share_include_evidence to false. It is unconditional once ownership is verified and succeeds even if no token currently exists. No rate limiter on this route.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required only when authenticating via session cookie (ignored for API-key auth).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Share token revoked; returns { success: true }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "success": true
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key or session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authenticated but lacks write scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Failed to revoke share link",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/outcomes": {
      "get": {
        "operationId": "get_intents_id_outcomes",
        "tags": [
          "outcomes"
        ],
        "summary": "Fetch all outcome measurements recorded for an intent.",
        "x-stability": "stable",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "Accepts a pm_live_ Bearer key with read scope OR a session cookie. On the key path the query is scoped to the key's workspace as well as the intent, because RLS is bypassed there. Ordered by outcome_index ascending then measured_at descending, so the newest measurement for each outcome comes first within its group. Returns the full provenance of each row: provider, queryRef, queryVersion, window, evaluatedAt, expectation, rawResponse, numericValue, and metBasis ('computed' when Pathmode evaluated the expectation itself, 'self_report' when the caller asserted the verdict, null when there is no verdict). Legacy rows predate provenance and read back with those fields null.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns { measurements } ordered by outcome_index asc, then measured_at desc",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "measurements": [
                    {
                      "id": "9c1e7a6b-5c4d-4e2a-8b4d-3f2a9c1e7a6b",
                      "intentId": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                      "outcomeId": "c4d3e2f1-a0b9-4c8d-7e6f-5a4b3c2d1e0f",
                      "outcomeIndex": 0,
                      "outcomeText": "Password reset completion rate",
                      "baselineValue": "42%",
                      "actualValue": "68%",
                      "unit": "%",
                      "met": true,
                      "source": "manual",
                      "recordedBy": "d3e2f1a0-9b8c-4d7e-8f6a-5b4c3d2e1f0a",
                      "measuredAt": "2026-06-07T12:34:56.000Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "No valid Supabase session cookie (Unauthorized)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Failed to fetch measurements / internal server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      },
      "post": {
        "operationId": "post_intents_id_outcomes",
        "tags": [
          "outcomes"
        ],
        "summary": "Record a new outcome measurement for an intent.",
        "x-stability": "stable",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "Accepts a pm_live_ Bearer key with write scope OR a session cookie. The two paths differ on purpose: a key authenticates through the API-key layer, reads with the admin client, and scopes the intent lookup to the key's workspace — RLS is bypassed there, so that filter is what prevents cross-workspace writes. A session keeps the original anon+RLS client and its owner|admin|editor role gate, and needs no workspace_id query param, so the in-app caller is unaffected. Body is zod-validated (lib/outcomeMeasurementRecord.ts). met is the SERVER's conclusion, not the caller's claim: when numericValue and measurement.expectation are both present the comparator is evaluated here, met_basis is 'computed', and a contradicting caller-supplied met is discarded rather than blended. Otherwise a caller-supplied met is stored with met_basis 'self_report', and with neither, met and met_basis are null. Prefer outcomeId: the outcome's text, index, and id are derived server-side from the intent row, so client input can never mislabel a measurement; the legacy outcomeIndex/outcomeText path remains for compatibility and still derives text/id when the index resolves. recorded_by is the authenticated user (the key's owner on the key path); source defaults to 'api' for key writes and 'manual' for session writes; evaluatedAt defaults to now when a measurement block omits it, since a measurement with no evaluation time cannot be placed against the change it measures. workspace_id derives from the intent row. No rate limiting on this endpoint.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "outcomeId": {
                    "type": "string",
                    "description": "Preferred: stable StructuredOutcome UUID of the outcome being measured. Must match an outcome on the intent (400 otherwise); outcome text and index are derived server-side from the intent row."
                  },
                  "outcomeIndex": {
                    "type": "number",
                    "description": "Legacy positional path, used only when outcomeId is absent (one of the two is required; 0 is valid, must be a non-negative integer). When the index resolves against the intent's outcomes, text and id are derived server-side."
                  },
                  "outcomeText": {
                    "type": "string",
                    "description": "Legacy outcome text snapshot. Only consulted when outcomeIndex does not resolve against the intent's current outcomes; required non-empty in that case."
                  },
                  "actualValue": {
                    "type": "string",
                    "description": "Measured value. Required non-empty string; trimmed before insert."
                  },
                  "baselineValue": {
                    "type": "string",
                    "description": "Baseline value before the change. Stored null if falsy."
                  },
                  "unit": {
                    "type": "string",
                    "description": "Unit of measure. Stored null if falsy."
                  },
                  "numericValue": {
                    "type": "number",
                    "description": "The measured value as a finite number, alongside the displayable actualValue. Required for the server to evaluate measurement.expectation — without it a verdict can only be recorded as a self-report."
                  },
                  "measurement": {
                    "type": "object",
                    "description": "Provenance for the number: { provider, queryRef: { kind, id }, queryVersion?, window: { kind: rolling|fixed|since_ship, duration?, start?, end? }, evaluatedAt?, expectation?: { operator: gt|gte|lt|lte|eq, target, unit? }, rawResponse?, recordedByTool? }. queryRef.id must be a STABLE id (for PostHog, a saved insight id), never a metric name: names are mutable, so a renamed query would orphan the record. A rolling window requires duration; a fixed window requires start and end. rawResponse must be JSON-serialisable and under 20000 characters."
                  },
                  "met": {
                    "type": "boolean",
                    "description": "Whether the outcome target was met. Stored null if not provided (uses ?? null)."
                  },
                  "source": {
                    "type": "string",
                    "description": "Provenance label. Defaults to 'manual' when falsy."
                  }
                },
                "required": [
                  "actualValue"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Measurement recorded; returns the created measurement",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "intentId": "9e8d7c6b-5a4f-4e3d-2c1b-0a9f8e7d6c5b",
                  "outcomeId": "c4d3e2f1-a0b9-4c8d-7e6f-5a4b3c2d1e0f",
                  "outcomeIndex": 0,
                  "outcomeText": "Activation over 10%",
                  "baselineValue": "8.1%",
                  "actualValue": "12.4%",
                  "numericValue": 12.4,
                  "unit": "%",
                  "met": true,
                  "metBasis": "computed",
                  "source": "posthog",
                  "recordedBy": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                  "recordedByTool": "claude-code",
                  "measuredAt": "2026-08-25T12:00:00.000Z",
                  "provider": "posthog",
                  "queryRef": {
                    "kind": "saved_insight",
                    "id": "4HeoGZ7y"
                  },
                  "queryVersion": "sha256:abc123",
                  "window": {
                    "kind": "rolling",
                    "duration": "28d"
                  },
                  "evaluatedAt": "2026-08-25T11:59:00.000Z",
                  "expectation": {
                    "operator": "gte",
                    "target": 10,
                    "unit": "%"
                  },
                  "rawResponse": {
                    "result": [
                      {
                        "aggregated_value": 12.4
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failure — neither outcomeId nor outcomeIndex given; outcomeId not on this intent; malformed field (actualValue empty/over 500 chars, met non-boolean, etc.)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No valid Supabase session cookie (Unauthorized)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Session user is not an owner/admin/editor of the intent's workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Failed to record measurement / internal server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/analyze-codebase": {
      "post": {
        "operationId": "post_intents_id_analyze-codebase",
        "tags": [
          "analyze-codebase"
        ],
        "summary": "Spin up a Vercel Sandbox, shallow-clone the bound GitHub repo, gather a spec-targeted codebase slice, and synthesize an Implementation Context via Gemini. Streams progress as NDJSON. Sole writer of intent_specs.implementation_context.",
        "x-stability": "beta",
        "x-scope": "admin",
        "x-auth": "cookie-session-only",
        "x-rateLimit": "analyzeCodebase 30/24h (per workspace)",
        "description": "Cookie-session only — uses createClient() from @/lib/supabase/server + auth.getUser(); does NOT accept pm_live_ API keys. Role gate requires owner|admin|editor membership (treated as admin scope). Double-gated: paid plan (pro/team — free is 403) AND workspace flag implementation_context_enabled. Response is streamed NDJSON, Content-Type 'application/x-ndjson; charset=utf-8', Cache-Control no-store. Each line is a ProgressEvent { phase, message, partialResult?, updatedSpec?, error? }; errors after the 200 surface as a {phase:'error'} line, not an HTTP error. Rate limit keyed by workspace:<id>, NOT user; example products (is_example) are exempt from the cap. analyzedSpecVersion is a 16-char sha256 of canonical spec content. updatedSpec is the full mapIntentRowToApiIntent shape.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Streaming NDJSON of ProgressEvent lines (cloning, analyzing, writing, done; or a final error line). HTTP 200 returned immediately before work completes.",
            "content": {
              "application/x-ndjson": {
                "schema": {
                  "type": "string"
                },
                "example": "{\"phase\":\"cloning\",\"message\":\"Cloning acme/payroll-web (default branch)\"}\n{\"phase\":\"analyzing\",\"message\":\"Synthesizing implementation context from 37 files\"}\n{\"phase\":\"writing\",\"message\":\"Persisting implementation context\",\"partialResult\":{\"relevantAreas\":[{\"path\":\"src/auth/reset.ts\",\"reason\":\"Holds the existing reset-token flow this intent extends\"}],\"currentBehavior\":\"Reset is admin-only via a CLI script.\",\"liftEstimate\":\"medium\",\"risks\":[\"Token reuse if expiry is not enforced\"],\"verificationSuggestions\":[\"Add an e2e test for the reset link flow\"],\"analyzedAt\":\"2026-06-07T12:34:56.000Z\",\"analyzedRepo\":{\"provider\":\"github\",\"owner\":\"acme\",\"name\":\"payroll-web\",\"ref\":\"main\"},\"analyzedSpecVersion\":\"a1b2c3d4e5f6a7b8\"}}\n{\"phase\":\"done\",\"message\":\"Analysis complete\",\"updatedSpec\":{\"id\":\"3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f\",\"workspaceId\":\"7a6b5c4d-3e2f-4a1b-9c8d-2e1f0a9b8c7d\",\"productId\":\"b2c3d4e5-6f7a-4b8c-9d0e-1f2a3b4c5d6e\",\"status\":\"draft\",\"version\":3,\"title\":\"Add password reset\",\"objective\":\"Let users recover access without contacting support.\",\"outcomes\":[{\"id\":\"o1\",\"text\":\"Users can reset password from the login screen\"}],\"healthMetrics\":[],\"constraints\":[],\"verification\":{},\"externalLinks\":[],\"evidenceAnchors\":{},\"implementationContext\":{\"relevantAreas\":[{\"path\":\"src/auth/reset.ts\",\"reason\":\"Holds the existing reset-token flow\"}],\"currentBehavior\":\"Reset is admin-only via a CLI script.\",\"liftEstimate\":\"medium\",\"risks\":[\"Token reuse if expiry is not enforced\"],\"verificationSuggestions\":[\"Add an e2e test for the reset link flow\"],\"analyzedAt\":\"2026-06-07T12:34:56.000Z\",\"analyzedRepo\":{\"provider\":\"github\",\"owner\":\"acme\",\"name\":\"payroll-web\",\"ref\":\"main\"},\"analyzedSpecVersion\":\"a1b2c3d4e5f6a7b8\"},\"createdAt\":\"2026-06-01T09:00:00.000Z\",\"updatedAt\":\"2026-06-07T12:34:56.000Z\",\"shippedAt\":null,\"verifiedAt\":null,\"edgeCases\":[],\"evidenceIds\":[],\"relations\":[]}}"
              }
            }
          },
          "400": {
            "description": "No GitHub repo/binding configured, missing installation, or no default repo for the binding",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No valid Supabase session (Authentication required)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not owner/admin/editor of the workspace; OR plan is free; OR implementation_context_enabled flag is off",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Daily analysis limit reached for this workspace (30/24h); body { error, retryAfterSeconds } + Retry-After header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Service not configured (Supabase service role key / URL missing)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/evidence": {
      "get": {
        "operationId": "get_evidence",
        "tags": [
          "evidence"
        ],
        "summary": "Query active evidence items for the authenticated workspace, with filtering and pagination.",
        "x-stability": "stable",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "List path. Filters on status='active' only. Default sort created_at DESC. 'count' is the exact total matching the filters (count: 'exact'), falling back to the returned page length if null. Every field is always present in the object (nullable columns serialize as null; tags/linkedIntentIds coalesce to []).",
        "parameters": [
          {
            "name": "tier",
            "in": "query",
            "required": false,
            "description": "Filter to one trust tier: backing (human-reviewed), unreviewed (captured, not reviewed), assumption (AI-drafted hypothesis). Applied in SQL, so count stays exact.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "triageStatus",
            "in": "query",
            "required": false,
            "description": "inbox | triaged | dismissed. Dismissed rows are hidden unless this is set to dismissed explicitly (inventory endpoint only; the MCP query_evidence tool never returns them).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "productId",
            "in": "query",
            "required": false,
            "description": "Filter to a single product.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Filter by evidence type: friction | quote | observation | metric | request. 400 if invalid.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "severity",
            "in": "query",
            "required": false,
            "description": "Filter by severity: low | medium | high | critical. 400 if invalid.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Case-insensitive ILIKE substring match on content.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "required": false,
            "description": "Comma-separated tag list; matches rows whose tags array contains ALL given tags.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, default 50, capped at 200.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Pagination offset, default 0.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth (no Bearer key); ignored for API-key auth. Without it, cookie auth returns 400.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Evidence list returned.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "evidence": [
                    {
                      "id": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                      "productId": "b1c2d3e4-f5a6-4789-b0c1-2d3e4f5a6b7c",
                      "workspaceId": "7a6b5c4d-3e2f-4a1b-9c8d-0e1f2a3b4c5d",
                      "type": "friction",
                      "content": "Users abandon checkout when the address form rejects valid postal codes.",
                      "source": "Support ticket #4821",
                      "sourceUrl": "https://support.example.com/t/4821",
                      "severity": "high",
                      "sentiment": "negative",
                      "tags": [
                        "checkout",
                        "forms"
                      ],
                      "stage": "Checkout",
                      "linkedIntentIds": [
                        "c9d8e7f6-a5b4-4321-9876-543210fedcba"
                      ],
                      "createdAt": "2026-06-01T10:15:30.000Z",
                      "updatedAt": "2026-06-02T08:00:00.000Z"
                    }
                  ],
                  "count": 1,
                  "limit": 50,
                  "offset": 0
                }
              }
            }
          },
          "400": {
            "description": "Invalid type/severity enum value, or (cookie auth) missing workspace_id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key or no valid session cookie.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API key lacks read scope; or (cookie auth) not a member of the workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB or internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      },
      "post": {
        "operationId": "post_evidence",
        "tags": [
          "evidence"
        ],
        "summary": "Create a new evidence item under a product in the authenticated workspace.",
        "x-stability": "stable",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "general 300/h",
        "description": "Billing-gated: a 403 with body { error:'plan_limit_reached', metric:'evidence_items', current, limit, plan } is returned when the evidence_items quota is hit (distinct from the scope 403 which has body { error: 'Insufficient permissions...' }). Row stored with status='active', triage_status='inbox', created_by=auth.userId. Fires a Slack notify + PostHog evidence_created event (fire-and-forget). linkedIntentIds is [] on a fresh create.",
        "parameters": [
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth (no Bearer key); ignored for API-key auth.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "content": {
                    "type": "string",
                    "description": "Non-empty evidence text; trimmed before storage."
                  },
                  "type": {
                    "type": "string",
                    "description": "One of friction | quote | observation | metric | request."
                  },
                  "productId": {
                    "type": "string",
                    "description": "Product must belong to the workspace (else 404)."
                  },
                  "source": {
                    "type": "string",
                    "description": "Free-text source label; stored as null if omitted."
                  },
                  "sourceUrl": {
                    "type": "string",
                    "description": "Source URL; stored as null if omitted."
                  },
                  "severity": {
                    "type": "string",
                    "description": "low | medium | high | critical. 400 if invalid."
                  },
                  "sentiment": {
                    "type": "string",
                    "description": "positive | negative | neutral | mixed. 400 if invalid."
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Tag array; defaults to []."
                  },
                  "stage": {
                    "type": "string",
                    "description": "Free-text journey stage; stored as null if omitted."
                  }
                },
                "required": [
                  "content",
                  "type",
                  "productId"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Evidence created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "d4e5f6a7-b8c9-4012-a345-6789bcdef012",
                  "productId": "b1c2d3e4-f5a6-4789-b0c1-2d3e4f5a6b7c",
                  "workspaceId": "7a6b5c4d-3e2f-4a1b-9c8d-0e1f2a3b4c5d",
                  "type": "friction",
                  "content": "Users abandon checkout when the address form rejects valid postal codes.",
                  "source": "Support ticket #4821",
                  "sourceUrl": "https://support.example.com/t/4821",
                  "severity": "high",
                  "sentiment": "negative",
                  "tags": [
                    "checkout",
                    "forms"
                  ],
                  "stage": "Checkout",
                  "linkedIntentIds": [],
                  "createdAt": "2026-06-07T12:00:00.000Z",
                  "updatedAt": "2026-06-07T12:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Missing/empty content, missing/invalid type, missing productId, or invalid severity/sentiment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key or no valid session cookie.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API key lacks write scope; cookie auth not a workspace member; OR billing limit reached (body { error: 'plan_limit_reached', metric: 'evidence_items', current, limit, plan }).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "productId not found in this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (general bucket, 300/h).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB or internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/evidence/ingest": {
      "post": {
        "operationId": "post_evidence_ingest",
        "tags": [
          "evidence"
        ],
        "summary": "Ingest one or many evidence items from an external source in a single call. Built for webhook senders: PostHog destinations, Zapier, Intercom, or anything that can POST JSON.",
        "x-stability": "stable",
        "x-scope": "evidence:ingest",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "evidenceIngest 1000 items/h (per workspace)",
        "description": "Accepts two shapes against the same path: simple { productId, source, content, ... } for one item, and batch { productId, source, items: [...] } for up to 50. Validation is fail-fast and all-or-nothing: the first invalid item rejects the whole request with 400 and nothing is inserted, and the error names the path that failed (e.g. items[2].content). A body that is not a JSON object, a null batch member, or a non-string sourceUrl, stage or externalId is a 400 rather than a 500. productId must be a UUID: a malformed one is a 400, not the 404 it used to surface as. tags stays deliberately lenient — a non-array becomes [] and non-string members are dropped, since refusing them would break senders that work today. The billing check counts the entire batch, so a request that would cross the evidence_items quota is refused whole rather than partially written. Rows land with status='active', triage_status='inbox', origin='integration' (POST /api/v1/evidence leaves origin unset), and created_by=auth.userId. Fire-and-forget side effects: an activity-log row, a Slack 'evidence_created' notification carrying the first 5 items, and the PostHog 'webhook_evidence_ingested' event. Rate-limited per workspace on the evidenceIngest bucket, charged per item rather than per request: a 50-item batch spends 50 of the 1000/hour budget and a single-item post spends 1, so batching is neither penalised nor a way around the ceiling. The budget is spent after the payload is parsed and before anything is written, and the sliding window admits the request that crosses the ceiling, so a workspace can overshoot by at most one batch. Scope: use evidence:ingest, a scope that opens this endpoint and nothing else, so a credential stored in a third-party tool cannot mutate intents, statuses, share tokens, or verification if that tool leaks it. A write-scoped key is still accepted, because senders configured before the narrow scope existed hold one; prefer evidence:ingest for anything new. Only owners, admins, and editors can mint it — ingest is a write, so it is not available to viewers or commenters. Idempotency: items carrying externalId are deduplicated on (workspace, source, externalId), both within a single batch (first occurrence wins) and against everything the workspace already stored for that source. duplicates counts what was skipped. A request in which every item was a redelivery returns 200 with created 0 and writes nothing — not a 4xx, which would make a well-behaved sender retry forever. Skipped items cost no evidence_items quota, though they do spend the rate-limit budget: the limiter protects the service from volume, the quota bills for stored rows. A unique index settles the case where two identical deliveries race past the lookup. If the duplicate lookup itself fails the request is refused with 500 rather than inserting, so a retry gets a clean second chance. POST /api/webhooks/ingest is a legacy alias that delegates to this handler with an identical contract, kept live for senders already configured against it.",
        "parameters": [
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth (no Bearer key); ignored for API-key auth.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "productId": {
                    "type": "string",
                    "description": "Product the evidence lands under. Must belong to the workspace (else 404)."
                  },
                  "source": {
                    "type": "string",
                    "description": "Non-empty label for where the evidence came from, e.g. 'posthog', 'zapier', 'intercom'. Trimmed, then applied to every row in the request. Required here, unlike POST /api/v1/evidence where it is optional."
                  },
                  "content": {
                    "type": "string",
                    "description": "Evidence text for the simple single-item shape. Required unless items is given. Max 10000 characters."
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Batch shape: up to 50 objects, each accepting content (required) plus the same optional type, severity, sentiment, sourceUrl, tags, and stage fields. Required unless content is given; must be a non-empty array."
                  },
                  "type": {
                    "type": "string",
                    "description": "One of friction | quote | observation | metric | request. Defaults to 'observation' when omitted, unlike POST /api/v1/evidence where type is required."
                  },
                  "severity": {
                    "type": "string",
                    "description": "low | medium | high | critical. 400 if invalid."
                  },
                  "sentiment": {
                    "type": "string",
                    "description": "positive | negative | neutral | mixed. 400 if invalid."
                  },
                  "sourceUrl": {
                    "type": "string",
                    "description": "Link back to the originating ticket, survey response, or issue; stored as null if omitted."
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Tag array; non-string members are dropped. Defaults to []."
                  },
                  "externalId": {
                    "type": "string",
                    "description": "The sending system's own id for this event (PostHog: {event.uuid}). Supplying it makes redelivery a no-op: the same externalId under the same workspace and source is stored once, and a retry returns 200 with created 0 and duplicates 1 instead of a second row. Max 255 chars, trimmed. Omit it and the item inserts unconditionally, as before."
                  },
                  "stage": {
                    "type": "string",
                    "description": "Free-text journey stage; stored as null if omitted."
                  }
                },
                "required": [
                  "productId",
                  "source"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Evidence items created; body carries the created count and the created rows.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "created": 2,
                  "duplicates": 1,
                  "items": [
                    {
                      "id": "d4e5f6a7-b8c9-4012-a345-6789bcdef012",
                      "type": "friction",
                      "content": "Survey: 'I could not tell which plan I was on'",
                      "source": "posthog",
                      "severity": "high",
                      "triageStatus": "inbox",
                      "createdAt": "2026-06-07T12:00:00.000Z"
                    },
                    {
                      "id": "e5f6a7b8-c9d0-4123-b456-789cdef01234",
                      "type": "observation",
                      "content": "Error tracking: checkout_submit threw 41 times in 24h",
                      "source": "posthog",
                      "severity": null,
                      "triageStatus": "inbox",
                      "createdAt": "2026-06-07T12:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON body; missing productId or source; items not an array, empty, or over 50; neither items nor content given; or a per-item failure (empty content, content over 10000 chars, invalid type/severity/sentiment) reported as items[n]: <reason>.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key or no valid session cookie.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API key lacks write scope; OR billing limit reached (body { error: 'plan_limit_reached', metric: 'evidence_items', current, limit, plan }).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "productId not found in this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Workspace ingest budget exhausted (1000 items/hour, sliding window). Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset are set on the response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB or internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/workspace": {
      "get": {
        "operationId": "get_workspace",
        "tags": [
          "workspace"
        ],
        "summary": "Get workspace details: name, url key, tags, strategy, active products, and active constitution rules.",
        "x-stability": "stable",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "strategy is read from workspaces.settings.strategy (null when unset). products only includes status='active' rows, sorted created_at ASC. Optional product columns (description/productVision/northStar/targetAudience/manifest/icon/color) are omitted from JSON when falsy (mapped to undefined); defaultContext defaults to {}. constitutionRules only includes is_active=true rows, sorted created_at DESC; rationale/scope/examples omitted when null/non-array; enforcement defaults to 'required'.",
        "parameters": [
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth (no Bearer key); ignored for API-key auth.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Workspace returned.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "7a6b5c4d-3e2f-4a1b-9c8d-0e1f2a3b4c5d",
                  "name": "Acme Product Team",
                  "urlKey": "acme-product",
                  "tags": [
                    "beta"
                  ],
                  "strategy": null,
                  "products": [
                    {
                      "id": "b1c2d3e4-f5a6-4789-b0c1-2d3e4f5a6b7c",
                      "workspaceId": "7a6b5c4d-3e2f-4a1b-9c8d-0e1f2a3b4c5d",
                      "name": "Checkout",
                      "slug": "checkout",
                      "description": "The purchase flow.",
                      "productVision": "Frictionless checkout in under 30 seconds.",
                      "northStar": "Completed checkouts per week",
                      "targetAudience": "Returning shoppers",
                      "defaultContext": {},
                      "manifest": null,
                      "icon": "cart",
                      "color": "#3A8A8C",
                      "status": "active",
                      "createdBy": "e1f2a3b4-c5d6-4789-a012-3b4c5d6e7f80",
                      "createdAt": "2026-05-01T09:00:00.000Z",
                      "updatedAt": "2026-06-01T09:00:00.000Z"
                    }
                  ],
                  "constitutionRules": [
                    {
                      "id": "a1b2c3d4-e5f6-4789-9012-3456789abcde",
                      "category": "privacy",
                      "text": "Never log raw PII.",
                      "rationale": "Compliance with GDPR.",
                      "scope": "all-products",
                      "enforcement": "required",
                      "examples": [
                        "Mask email addresses in logs."
                      ],
                      "isActive": true,
                      "createdAt": "2026-04-10T11:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Cookie auth missing workspace_id query param.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key or no valid session cookie.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API key lacks read scope; or cookie auth not a workspace member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Workspace not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Products fetch failure or internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/workspace/constitution": {
      "get": {
        "operationId": "get_workspace_constitution",
        "tags": [
          "workspace"
        ],
        "summary": "Get the active constitution rules for the authenticated workspace.",
        "x-stability": "stable",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "Only returns is_active=true rules, sorted created_at DESC. rationale/scope omitted when null; examples omitted when not an array; enforcement defaults to 'required'. count is rules.length.",
        "parameters": [
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth (no Bearer key); ignored for API-key auth.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rules returned.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "rules": [
                    {
                      "id": "a1b2c3d4-e5f6-4789-9012-3456789abcde",
                      "category": "privacy",
                      "text": "Never log raw PII.",
                      "rationale": "Compliance with GDPR.",
                      "scope": "all-products",
                      "enforcement": "required",
                      "examples": [
                        "Mask email addresses in logs."
                      ],
                      "isActive": true,
                      "createdAt": "2026-04-10T11:00:00.000Z"
                    }
                  ],
                  "count": 1
                }
              }
            }
          },
          "400": {
            "description": "Cookie auth missing workspace_id query param.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key or no valid session cookie.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API key lacks read scope; or cookie auth not a workspace member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB or internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/export": {
      "get": {
        "operationId": "get_export",
        "tags": [
          "export"
        ],
        "summary": "Export workspace data as agent context files: CLAUDE.md, AGENTS.md, .cursorrules, or intent.md.",
        "x-stability": "stable",
        "x-scope": "read",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "SUCCESS RESPONSE IS A FILE DOWNLOAD, NOT JSON. claude-md sets Content-Type text/markdown + filename CLAUDE.md, filters out non-substantive intents (evaluateIntentReadiness.isSubstantive) and prepends a <!-- PATHMODE:WARNINGS --> block via getContextWarnings (plus a note when stub intents were filtered). agents-md is identical to claude-md (same agent-agnostic '# Pathmode Intent Context' body) but sets filename AGENTS.md — the native instruction file for OpenAI Codex and modern Cursor. cursorrules and intent-md require the same canonical six-gate Preflight used by the browser and MCP server; evidence remains advisory. cursorrules sets text/plain + filename .cursorrules. intent-md sets text/markdown + filename intent.md. Intents fetched via INTENT_WITH_RELATIONS_SELECT, sorted updated_at DESC; createdAt/updatedAt/shippedAt/verifiedAt are converted to epoch-millis numbers before generation. 422 and 404 bodies are JSON.",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "claude-md (default) | agents-md | cursorrules | intent-md. Any other value yields 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "intent_id",
            "in": "query",
            "required": false,
            "description": "For cursorrules/intent-md: target a specific intent; if omitted, pickBestIntent chooses one. For claude-md it influences product resolution.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "product_id",
            "in": "query",
            "required": false,
            "description": "For claude-md: include this product's manifest context (explicit override; else resolved from intent, else first active product).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth (no Bearer key); ignored for API-key auth.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "File generated; body is markdown/plaintext (see Content-Type), not JSON.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "# CLAUDE.md\n# (success responses are NOT JSON — body is the generated markdown/plaintext file)\n# Content-Type: text/markdown; charset=utf-8 (claude-md, agents-md, intent-md)\n# Content-Type: text/plain; charset=utf-8 (cursorrules)\n# Content-Disposition: attachment; filename=\"CLAUDE.md\" | \"AGENTS.md\" | \".cursorrules\" | \"intent.md\"\n#\n# JSON is only returned on error/422, e.g.:\n# {\n#   \"error\": \"Intent is not ready to export as agent context.\",\n#   \"readiness\": { \"isReadyForExecution\": false, \"passed\": 4, \"total\": 6, \"verdict\": \"failed 4/6 — blocking: edge-cases, verification\" },\n#   \"suggestion\": \"Resolve the named preflight blockers before exporting agent rules.\"\n# }"
              }
            }
          },
          "400": {
            "description": "Invalid format value; or cookie auth missing workspace_id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key or no valid session cookie.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API key lacks read scope; or cookie auth not a workspace member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "cursorrules/intent-md: requested intent_id not found, or no intents exist in the workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "cursorrules/intent-md: target intent is not ready for execution (returns JSON { error, readiness, suggestion }).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/api-keys": {
      "get": {
        "operationId": "get_api-keys",
        "tags": [
          "api-keys"
        ],
        "summary": "List the current user's API keys (key value never returned).",
        "x-stability": "stable",
        "x-scope": "none",
        "x-auth": "cookie-session-only",
        "x-rateLimit": "apiKeysRead 120/h",
        "description": "Cookie session only; uses createClient from @/lib/supabase/server + getUser, never inspects the Authorization header, so a pm_live_ Bearer token is NOT accepted. Lists keys for the authenticated user (eq user_id), sorted created_at DESC. The raw key is never returned — only keyPrefix. If admin client unavailable or api_keys table missing (code 42P01), returns 200 with { keys: [], count: 0, message }.",
        "responses": {
          "200": {
            "description": "Keys returned (or empty list with a 'message' field if SUPABASE_SERVICE_ROLE_KEY/the api_keys table is not configured).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "keys": [
                    {
                      "id": "f1e2d3c4-b5a6-4978-8a90-1b2c3d4e5f60",
                      "workspaceId": "7a6b5c4d-3e2f-4a1b-9c8d-0e1f2a3b4c5d",
                      "name": "CI deploy key",
                      "keyPrefix": "pm_live_3f2a9c1e",
                      "scopes": [
                        "read",
                        "write"
                      ],
                      "lastUsedAt": "2026-06-06T18:22:10.000Z",
                      "expiresAt": null,
                      "createdAt": "2026-05-20T14:00:00.000Z"
                    }
                  ],
                  "count": 1
                }
              }
            }
          },
          "401": {
            "description": "No valid session cookie (rejects API keys — this route does NOT call authenticateApiRoute).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (apiKeysRead bucket, 120/h).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB or internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ]
      },
      "post": {
        "operationId": "post_api-keys",
        "tags": [
          "api-keys"
        ],
        "summary": "Create a new API key for a workspace the user owns/admins; returns the secret key exactly once.",
        "x-stability": "stable",
        "x-scope": "admin",
        "x-auth": "cookie-session-only",
        "x-rateLimit": "apiKeysWrite 20/h",
        "description": "Cookie session only — never inspects Authorization header; pm_live_ Bearer NOT accepted. Caller must be admin or owner of workspaceId (workspace_members.role). Requested scopes are capped to deriveScopesFromRole(role); exceeding them is a 403. Billing-gated on the api_keys metric (403 with plan_limit_reached body). The secret 'key' (format pm_live_<64 hex chars>) is returned ONLY in this 201 response and can never be retrieved again — afterward only keyPrefix is exposed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Human label for the key."
                  },
                  "workspaceId": {
                    "type": "string",
                    "description": "Workspace the key belongs to; caller must be admin/owner."
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Subset of read | write | admin. Defaults to ['read','write']. Must be non-empty and within the scopes the caller's role permits."
                  }
                },
                "required": [
                  "name",
                  "workspaceId"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Key created; secret 'key' returned once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "f1e2d3c4-b5a6-4978-8a90-1b2c3d4e5f60",
                  "key": "pm_live_3f2a9c1e8b4d4e2a9c1e7a6b5c4d3e2f0a1b2c3d4e5f60718293a4b5c6d7e8f9",
                  "name": "CI deploy key",
                  "keyPrefix": "pm_live_3f2a9c1e",
                  "scopes": [
                    "read",
                    "write"
                  ],
                  "createdAt": "2026-06-07T12:00:00.000Z",
                  "message": "Save this key now. It cannot be retrieved again."
                }
              }
            }
          },
          "400": {
            "description": "Missing name, missing workspaceId, empty/non-array scopes, or unrecognized scope values.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No valid session cookie (API keys not accepted).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Caller is not workspace admin/owner; requested scopes exceed role-derived scopes; OR billing limit reached (body { error:'plan_limit_reached', metric:'api_keys', current, limit, plan }).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (apiKeysWrite bucket, 20/h).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Membership lookup failure, insert failure, or internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "SUPABASE_SERVICE_ROLE_KEY not configured (admin client unavailable).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ]
      },
      "delete": {
        "operationId": "delete_api-keys",
        "tags": [
          "api-keys"
        ],
        "summary": "Revoke (delete) one of the current user's API keys by id.",
        "x-stability": "stable",
        "x-scope": "none",
        "x-auth": "cookie-session-only",
        "x-rateLimit": "apiKeysWrite 20/h",
        "description": "Cookie session only — never inspects Authorization header; pm_live_ Bearer NOT accepted. Delete filters on both id=keyId and user_id=current user, so a user can only revoke their own keys. No workspace role check (unlike POST). Returns a fixed { success:true, message } body regardless of whether a row actually matched.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "keyId": {
                    "type": "string",
                    "description": "Id of the api_keys row to delete; scoped to the authenticated user (eq user_id)."
                  }
                },
                "required": [
                  "keyId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Key revoked (delete is scoped to eq id + eq user_id; succeeds even if no row matched).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "success": true,
                  "message": "API key revoked"
                }
              }
            }
          },
          "400": {
            "description": "Missing keyId.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No valid session cookie (API keys not accepted).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (apiKeysWrite bucket, 20/h).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Delete failure or internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "SUPABASE_SERVICE_ROLE_KEY not configured (admin client unavailable).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/connection/verify": {
      "post": {
        "operationId": "post_connection_verify",
        "tags": [
          "connection"
        ],
        "summary": "Confirm a key authenticates and return the workspace it belongs to. Records verification, not usage.",
        "x-stability": "stable",
        "x-scope": "none",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "Requires no scope — any valid key can verify itself. Stamps api_keys.verified_at and emits mcp_connection_verified; deliberately does NOT touch last_used_at or emit api_call, so checking a key can never make it look like an agent used it. Prefer this over calling a data endpoint to test credentials.",
        "responses": {
          "200": {
            "description": "OK — the key authenticates; returns { ok, workspaceId, name, scopes }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "workspaceId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                  "specVersion": "a1b2c3d4e5f6a7b8",
                  "name": "Acme Product",
                  "scopes": [
                    "read",
                    "write"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Invalid/expired API key, or no Bearer key and no valid session cookie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/connection/handshake": {
      "post": {
        "operationId": "post_connection_handshake",
        "tags": [
          "connection"
        ],
        "summary": "Announce that an MCP client has launched with this key. Fire-and-forget; records presence, not usage.",
        "x-stability": "beta",
        "x-scope": "none",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "none",
        "description": "Called automatically by @pathmode/mcp-server 1.9.0+ once per launch, on the MCP oninitialized callback. Stamps client_first_seen_at (first time) and client_seen_at, and emits exactly one event, mcp_client_launched; never touches last_used_at, because a client starting up is not an agent doing work. Body is optional and all fields are untrusted client-reported strings. Clients must treat failures as non-fatal.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "client": {
                    "type": "string",
                    "description": "Client name reported by the MCP client (e.g. claude-code). Truncated to 80 chars; used only as an analytics dimension."
                  },
                  "clientVersion": {
                    "type": "string",
                    "description": "Client version reported by the MCP client. Truncated to 80 chars."
                  },
                  "serverVersion": {
                    "type": "string",
                    "description": "Version of @pathmode/mcp-server making the call. Truncated to 80 chars."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK — handshake recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Invalid/expired API key, or no Bearer key and no valid session cookie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/connection/tool-call": {
      "post": {
        "operationId": "post_connection_tool-call",
        "tags": [
          "connection"
        ],
        "summary": "Report that one MCP tool fired. Fire-and-forget; records which tool ran, whether it succeeded, failed, or refused by design, and an opaque digest of the repository it ran in.",
        "x-stability": "beta",
        "x-scope": "none",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "mcpTelemetry 2000/h",
        "description": "Called automatically by @pathmode/mcp-server once per tool invocation, in connected (cloud) mode ONLY — a keyless local-mode server makes no network request at all. Emits exactly one canonical PostHog MCP Analytics event, $mcp_tool_call, carrying the allowlisted tool name and outcome (also projected as $mcp_tool_name and $mcp_is_error); deliberately touches NO api_keys column, so a tool firing never moves last_used_at and the activation series stays comparable across the day this shipped. Accepts only tool, outcome, an optional refusal kind from a closed list, and an optional opaque repository digest; arguments, responses, agent intent, spec content, and paths are never accepted. Clients must treat failures, including 429, as non-fatal.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tool": {
                    "type": "string",
                    "description": "Name of the tool that ran, e.g. check_intent_readiness. Checked against the server's registered tool list; an unrecognised name is recorded as \"unknown\" rather than stored as given, so this field cannot carry spec content, paths, or anything else from the caller's repo."
                  },
                  "outcome": {
                    "type": "string",
                    "description": "\"ok\", \"error\", or \"refused\". \"refused\" is a designed refusal, such as a preflight that cannot identify which repository it is about, and is kept out of the error rate. Anything else is recorded as \"unknown\"."
                  },
                  "refusal": {
                    "type": "string",
                    "description": "Optional, and kept only when outcome is \"refused\": which designed refusal it was, one of \"repo_unresolved\", \"no_repo_intent\", \"ambiguous_roots\", or \"ambiguous_intent_files\". Checked against that closed list, so this field cannot carry text from the caller's repo either."
                  },
                  "repo": {
                    "type": "string",
                    "description": "Optional. A 32-character lowercase hex digest of the git remote the call happened in, used to count distinct repositories. Validated by shape, so nothing resembling a path or URL is accepted, and salted with your workspace before it is stored, so the same repository is one identity across every key in the workspace. Omit it when there is no network remote; the call is still recorded, just without repository granularity."
                  }
                },
                "required": [
                  "tool",
                  "outcome"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK — invocation recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Invalid/expired API key, or no Bearer key and no valid session cookie",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (2000 pings per user per hour). Clients drop the datapoint and carry on.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/implementation-context": {
      "post": {
        "operationId": "post_intents_id_implementation-context",
        "tags": [
          "intents"
        ],
        "summary": "Hand back what the repo actually looks like where this intent lands. Written by an agent that is already in the working tree, which sees more than the sandbox analyzer can: no file cap, no truncation, and uncommitted work included.",
        "x-stability": "beta",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "implementation-context 120/h per workspace",
        "description": "Provenance is stamped by the server and never accepted from the caller: source is always 'agent', analyzedAt is server time, and analyzedSpecVersion is computed from the spec as read. Sending those fields does nothing. Two distinct tokens appear in the response: specVersion is the optimistic-concurrency token the intent read path returns (the one to hand back as expectedSpecVersion), while implementationContext.analyzedSpecVersion is the narrower analysis-freshness hash shared with the repo analyzer; they are not expected to be equal. No analyzedRepo is recorded, because an agent in a local checkout may have no GitHub installation and naming a repo we never read would be a fabricated provenance fact. Agent-supplied risks and verificationSuggestions reach the implementing agent as prose but do NOT become grading criteria in the Outcomes rubric until a human promotes the context — otherwise the party being graded would be drafting its own rubric. A write always lands unpromoted, including when it overwrites context a human had already promoted.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "relevantAreas": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Files or modules this change touches and why. Max 100 entries; path max 512 chars, reason max 2000."
                  },
                  "currentBehavior": {
                    "type": "string",
                    "description": "How the code behaves today where this lands. Max 20000 chars."
                  },
                  "liftEstimate": {
                    "type": "string",
                    "enum": [
                      "low",
                      "medium",
                      "high"
                    ],
                    "description": "Rough size of the change. Defaults to 'medium'."
                  },
                  "risks": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "What could break. Max 50 entries, 2000 chars each. Advisory only while the context is agent-supplied: see notes."
                  },
                  "verificationSuggestions": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Checks worth running. Max 50 entries, 2000 chars each. Advisory only while agent-supplied: see notes."
                  },
                  "expectedSpecVersion": {
                    "type": "string",
                    "description": "The specVersion returned by the intent read (GET /intents/{id}, or get_intent) that this context was gathered against. Carry it back verbatim, do not compute it. When supplied and no longer current, the write is refused with 409 rather than attaching a description of a spec that has since moved."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored. Returns the context as persisted, plus the current spec version.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "implementationContext": {
                    "source": "agent",
                    "relevantAreas": [
                      {
                        "path": "src/auth/reset.ts",
                        "reason": "holds the reset-token flow"
                      }
                    ],
                    "currentBehavior": "Reset is admin-only via a CLI script.",
                    "liftEstimate": "medium",
                    "risks": [
                      "Token reuse if expiry is not enforced"
                    ],
                    "verificationSuggestions": [
                      "Add an e2e test for the reset link flow"
                    ],
                    "analyzedAt": "2026-08-26T18:44:00.000Z",
                    "analyzedSpecVersion": "a1b2c3d4e5f6a7b8"
                  },
                  "specVersion": "5f6a7b8c9d0e1f2a",
                  "bindsGradingCriteria": false
                }
              }
            }
          },
          "400": {
            "description": "Body failed validation, including any field over its size cap.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Key lacks the write scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such intent in this key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "expectedSpecVersion no longer matches the spec's current specVersion. Re-read the spec and gather again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (120 writes per workspace per hour).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/onboarding/repo/exchange": {
      "post": {
        "operationId": "post_onboarding_repo_exchange",
        "tags": [
          "onboarding"
        ],
        "summary": "Exchange a short-lived repository-adoption code for a project-scoped API credential and its selected workspace/Product binding.",
        "x-stability": "beta",
        "x-scope": "none",
        "x-auth": "one-time-code",
        "x-rateLimit": "repoAdoptionExchange 30/15m per IP + general 300/h per code",
        "description": "The endpoint first applies a 30-per-15-minute caller-IP limit before parsing the body, then a separate 300-per-hour bucket keyed by the submitted code digest. The raw long-lived key is returned only to the CLI and is never sent to the browser or embedded in the copied shell command. A returning CLI may send its existing pm_live_ Bearer key: if that exact user/workspace write credential is valid, the endpoint binds and reuses it. After a code is consumed, only the exact bound key may resume the same exchanging session; possession of the code alone is insufficient. Success responses set Cache-Control: no-store.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "The 32-character pm_adopt_ code generated by the authenticated onboarding page. It expires after 15 minutes and is consumed once."
                  }
                },
                "required": [
                  "code"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Code claimed; returns the selected binding and either a newly minted apiKey or reusedCredential:true.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "apiKey": "pm_live_...",
                  "apiUrl": "https://pathmode.io",
                  "sessionId": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "workspaceId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                  "workspaceName": "Acme",
                  "productId": "7d8e9f0a-1b2c-4d3e-9f8a-6b5c4d3e2f1a",
                  "productName": "Checkout"
                }
              }
            }
          },
          "400": {
            "description": "Malformed adoption code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "A credential supplied for reuse/resume is invalid or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Workspace access changed, or the workspace API-key plan limit prevents minting a credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Code is invalid, already used, or a supplied existing key belongs to another workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "The unconsumed code expired; generate a new command in Pathmode.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Per-IP exchange limit or per-code retry limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Credential creation or binding failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/v1/onboarding/repo/connect/start": {
      "post": {
        "operationId": "post_onboarding_repo_connect_start",
        "tags": [
          "onboarding"
        ],
        "summary": "Start an agent-initiated repository connection. Returns a short user code for a person to approve in the browser and a device secret the agent uses to collect the approval.",
        "x-stability": "beta",
        "x-scope": "none",
        "x-auth": "one-time-code",
        "x-rateLimit": "repoConnectStart 10/15m per IP; at most 1000 live requests in total",
        "description": "No credential is required and no repository name or spec content is sent. A person opens verificationUrl, signs in, checks that the page shows the same userCode, chooses the workspace and Product, and approves. The request expires after 15 minutes. The device secret is returned once and stored only as a hash; keep it on the requesting machine. A caller may send an existing pm_live_ Bearer key: it is recorded for reuse, and reused only if it belongs to the approving person and the workspace they choose. An invalid key does not fail the start. Responses set Cache-Control: no-store.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "client": {
                    "type": "string",
                    "description": "The requesting client. Only \"mcp-server\" is accepted."
                  },
                  "clientVersion": {
                    "type": "string",
                    "description": "The client's semantic version, for example \"1.36.0\"."
                  }
                },
                "required": [
                  "client",
                  "clientVersion"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Request created; returns the user code, the device secret, the approval link, and its expiry.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "deviceSecret": "pm_pair_...",
                  "userCode": "BCDF-GHJK",
                  "expiresAt": "2026-09-27T10:15:00.000Z",
                  "interval": 5,
                  "verificationUrl": "https://pathmode.io/onboarding/repo?user_code=BCDF-GHJK"
                }
              }
            }
          },
          "400": {
            "description": "The client or clientVersion is missing or not accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Per-IP start limit exceeded, or CAPACITY_REACHED when too many requests are waiting.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The request could not be created or no free user code was found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/v1/onboarding/repo/connect/token": {
      "post": {
        "operationId": "post_onboarding_repo_connect_token",
        "tags": [
          "onboarding"
        ],
        "summary": "Poll an agent-started connection. After browser approval, collects the repository credential and the chosen workspace/Product binding.",
        "x-stability": "beta",
        "x-scope": "none",
        "x-auth": "one-time-code",
        "x-rateLimit": "repoConnectToken 30/1m per IP and per device secret; one poll per 5 seconds per request, enforced in the database",
        "description": "Request states (AUTHORIZATION_PENDING, SLOW_DOWN, EXPIRED, ACCESS_DENIED, FORBIDDEN, PRODUCT_REQUIRED, KEY_CHOICE_CHANGED, ALREADY_REDEEMED, INVALID_REQUEST) come back as a machine-readable code with an error message and interval. The terminal ones close the request, so the next attempt starts a new one. Approval never creates a key; the key is created or reused, and any approved replacement is revoked, in one transaction when the agent collects. After collection, the device secret alone can never retrieve a credential again: resuming needs the exact bound key, as Bearer or as redemptionKey. Responses set Cache-Control: no-store.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "deviceSecret": {
                    "type": "string",
                    "description": "The pm_pair_ secret returned by connect/start."
                  },
                  "redemptionKey": {
                    "type": "string",
                    "description": "A pm_live_ key the client generated for this request (64 hex characters after the prefix). When a new key is needed, the server stores only its hash. Sending the same value again resumes a collection whose response was lost."
                  },
                  "sessionId": {
                    "type": "string",
                    "description": "The session returned by an earlier collection. With the exact bound key as Bearer, it resumes after the request itself has been cleaned up."
                  }
                },
                "required": [
                  "deviceSecret"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Approved and collected; returns the binding and either the new apiKey or reusedCredential:true.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "apiKey": "pm_live_...",
                  "apiUrl": "https://pathmode.io",
                  "sessionId": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "workspaceId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                  "workspaceName": "Acme",
                  "productId": "7d8e9f0a-1b2c-4d3e-9f8a-6b5c4d3e2f1a",
                  "productName": "Checkout",
                  "keyAction": "create",
                  "reusedCredential": false
                }
              }
            }
          },
          "202": {
            "description": "AUTHORIZATION_PENDING: not approved yet. Poll again after interval seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "apiKey": "pm_live_...",
                  "apiUrl": "https://pathmode.io",
                  "sessionId": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "workspaceId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                  "workspaceName": "Acme",
                  "productId": "7d8e9f0a-1b2c-4d3e-9f8a-6b5c4d3e2f1a",
                  "productName": "Checkout",
                  "keyAction": "create",
                  "reusedCredential": false
                }
              }
            }
          },
          "400": {
            "description": "The device secret, redemptionKey, or sessionId is malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "A key supplied to resume a collected request is invalid or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "ACCESS_DENIED (denied or discarded in the browser) or FORBIDDEN (the approver's workspace access changed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "ALREADY_REDEEMED, INVALID_REQUEST, PRODUCT_REQUIRED, or KEY_CHOICE_CHANGED (the approved key choice no longer holds; start a new request).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "EXPIRED: the request expired before it was collected. Start a new one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "SLOW_DOWN: polled faster than the interval or over the rate limit. Add 5 seconds to the interval.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The request or its session could not be checked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/v1/onboarding/repo/fail": {
      "post": {
        "operationId": "post_onboarding_repo_fail",
        "tags": [
          "onboarding"
        ],
        "summary": "Report a failed repository adoption so the waiting browser can show the failure.",
        "x-stability": "beta",
        "x-scope": "write",
        "x-auth": "api-key-only",
        "x-rateLimit": "general 300/h",
        "description": "The CLI calls this endpoint when its local push fails. Only an exchanging session matching the authenticated API-key row id, workspace, and user is marked failed. Cookie sessions are rejected. Repeated reports, unmatched sessions, and sessions already completed are acknowledged without changing their state, so a completion that wins the race is preserved.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "sessionId": {
                    "type": "string",
                    "description": "The session returned by the code exchange."
                  }
                },
                "required": [
                  "sessionId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Failure recorded, or no matching exchanging session remains; returns ok:true.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "description": "sessionId is not a UUID, or the JSON body is malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or expired credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The caller is not an API key with write scope, or workspace access is denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Failed to record repository adoption failure.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/onboarding/repo/complete": {
      "post": {
        "operationId": "post_onboarding_repo_complete",
        "tags": [
          "onboarding"
        ],
        "summary": "Complete a repository-adoption session by binding the canonical intent that the CLI pushed.",
        "x-stability": "beta",
        "x-scope": "write",
        "x-auth": "api-key-only",
        "x-rateLimit": "general 300/h",
        "description": "Authentication is intentionally stricter than a normal workspace write: the API-key row id must exactly match the credential bound during exchange. Cookie sessions are rejected. Repeating completion for an already-adopted session is idempotent. On success the server emits onboarding_repo_spec_landed and the waiting browser observes the authoritative session row.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "sessionId": {
                    "type": "string",
                    "description": "The session returned by the code exchange."
                  },
                  "intentId": {
                    "type": "string",
                    "description": "The canonical intent id returned after the existing intent.md was created or updated. It must belong to the session's selected Product."
                  }
                },
                "required": [
                  "sessionId",
                  "intentId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session marked adopted; returns the canonical intent and in-app URL.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "intent": {
                    "id": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                    "title": "Speed up onboarding"
                  },
                  "url": "/intent/3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f?onboarding=repo"
                }
              }
            }
          },
          "400": {
            "description": "sessionId or intentId is not a UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The caller is not the exact write credential bound to this adoption session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Session not found, or intent does not belong to the selected workspace/Product.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Session completion failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/review-sources": {
      "get": {
        "operationId": "get_intents_id_review-sources",
        "tags": [
          "proposal review"
        ],
        "summary": "The saved source text this intent cites, returned verbatim so a signed-in reviewer can compare the original request against the agent's proposal.",
        "x-stability": "beta",
        "x-scope": "read",
        "x-auth": "cookie-session-only",
        "x-rateLimit": "general",
        "description": "Reached through this intent's citations: direct evidence links, evidence anchors and decision evidence, then each cited evidence item's source document. Evidence hidden from handoff is excluded, and a document is dropped unless the citing evidence shares its product, so a forged association cannot cross product ownership. extractedText is the saved text, never a summary or a paraphrase; unavailableSourceCount reports cited documents withheld or empty rather than hiding them. Direct links are paged explicitly because PostgREST silently caps embedded relations. Responses are private, no-store.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": true,
            "description": "Identifies which workspace the cookie session targets. This endpoint is cookie-only, so it is always required.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns only the documents cited by this intent, never the product's whole source library",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "sources": [
                    {
                      "id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
                      "title": "ENG-412",
                      "extractedText": "Let customers export their table. Should respect whatever they are looking at.",
                      "updatedAt": "2026-09-10T08:12:00.000Z"
                    }
                  ],
                  "unavailableSourceCount": 0,
                  "repoBodyRevision": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
                }
              }
            }
          },
          "400": {
            "description": "Invalid intent ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "An API key attempted the call, or the session lacks read scope. Agents receive source text through their own tools, not here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "general bucket rate limit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "DB error reading source text",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/review-focus": {
      "post": {
        "operationId": "post_intents_id_review-focus",
        "tags": [
          "proposal review"
        ],
        "summary": "Choose the one proposed behavior whose interpretation would most change what gets built, and return it as a product question with a recommendation, bound to an exact repository-body revision.",
        "x-stability": "beta",
        "x-scope": "read",
        "x-auth": "cookie-session-only",
        "x-rateLimit": "ai",
        "description": "The model sees only server-loaded intent and source data, and its output is revalidated before it is returned: claimKey must name a real behavior on this revision, and a sourceQuote is dropped unless those exact bytes occur in the workspace-owned document. Source text is untrusted product input, never an instruction. analysis reads generated or fallback, and a fallback is honest rather than a guess dressed as analysis. The question belongs to the revision, not the viewer: it is cached against the intent, the revision and the exact source text, so only a miss consumes AI conversation allowance and a second reviewer of the same proposal reads the same question. A fallback is never cached, so one model outage cannot become the stored question. Nothing here authorizes implementation or changes the repository.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": true,
            "description": "Identifies which workspace the cookie session targets. This endpoint is cookie-only, so it is always required.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expectedRepoBodyRevision": {
                    "type": "string",
                    "description": "The 64-character repository-body revision the reviewer is looking at. A mismatch is refused rather than answered against newer wording."
                  }
                },
                "required": [
                  "expectedRepoBodyRevision"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Returns one question. analysisUnavailable: true accompanies a fallback focus when analysis could not run.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "focus": {
                    "claimKey": "outcome:c4d3e2f1-a0b9-4c8d-7e6f-5a4b3c2d1e0f",
                    "question": "Does the export cover the current page, or every row matching the active filter?",
                    "recommendation": "Export every row matching the active filter, keeping the selected columns.",
                    "reason": "A page-only export would return 25 of tens of thousands of rows and still look complete.",
                    "suggestedCorrection": "Export every row matching the active filter as CSV.",
                    "sourceId": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
                    "sourceQuote": "Should respect whatever they are looking at.",
                    "analysis": "generated"
                  },
                  "repoBodyRevision": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
                }
              }
            }
          },
          "400": {
            "description": "Invalid intent ID or revision payload, checked before any query",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "An API key attempted the call, or the session lacks read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The intent is not repository-owned, or code REPO_BODY_REVISION_CONFLICT: the proposal changed before review and currentRepoBodyRevision is returned",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "ai bucket rate limit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Failed to prepare the review",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/original-request": {
      "post": {
        "operationId": "post_intents_id_original-request",
        "tags": [
          "proposal review"
        ],
        "summary": "Attach the request an intent was written from, stored verbatim, so the first review can show the original ask beside the agent's interpretation.",
        "x-stability": "beta",
        "x-scope": "write",
        "x-auth": "api-key-or-cookie",
        "x-rateLimit": "general",
        "description": "Fills the gap that left the first review's original-request column empty: create_evidence takes a free-text source LABEL, not a document, so a ticket handed to an agent could never reach the column built to show it. One call writes a source document holding the text verbatim, one evidence item of type request that carries source_document_id, and the link to the intent. Attaching is deliberately not reviewing: the evidence lands status active, triage_status inbox and origin agent, which classifies as unreviewed — visible to the review, never counted as backing for a claim, so an agent cannot manufacture backing from its own input. The document is written with type import and file_type agent, which keeps it clear of the tracker-import undo path (that path requires a provider file_type). Same title and identical bytes in the same product is treated as the same request. Attaching a different request adds a second source rather than replacing the first; the review offers them as a picker.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workspace_id",
            "in": "query",
            "required": false,
            "description": "Required ONLY for cookie-session auth; identifies the target workspace. Ignored for API-key auth.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "How a reviewer recognizes it: a ticket key, a document name, a thread subject. 1-200 chars."
                  },
                  "text": {
                    "type": "string",
                    "description": "The request itself, 1-100000 chars. Stored exactly as sent: never summarized, tidied or translated."
                  },
                  "sourceUrl": {
                    "type": "string",
                    "description": "Where it lives, when it has an address a person can open."
                  }
                },
                "required": [
                  "title",
                  "text"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "This exact request was already attached. created is false and the existing ids are returned, so a retry does not stack duplicates behind the review's source picker.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "sourceDocumentId": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
                  "evidenceId": "d3e2f1a0-b9c8-4d7e-6f5a-4b3c2d1e0f9a",
                  "title": "ENG-412",
                  "characterCount": 182,
                  "created": true
                }
              }
            }
          },
          "201": {
            "description": "Attached; created is true",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "sourceDocumentId": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
                  "evidenceId": "d3e2f1a0-b9c8-4d7e-6f5a-4b3c2d1e0f9a",
                  "title": "ENG-412",
                  "characterCount": 182,
                  "created": true
                }
              }
            }
          },
          "400": {
            "description": "Invalid intent ID, or invalid body (zod) — returns { error, details }",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Lacks write scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Intent not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "code PRODUCT_REQUIRED: the intent has no product, so there is nowhere to file the request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "general bucket rate limit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Could not attach the request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/agent-watch": {
      "post": {
        "operationId": "post_intents_id_agent-watch",
        "tags": [
          "proposal review"
        ],
        "summary": "Start one run of a repository agent listening for review answers on a repository-owned intent. `npx @pathmode/mcp-server watch-review` calls this.",
        "x-stability": "beta",
        "x-scope": "read",
        "x-auth": "api-key-only",
        "x-rateLimit": "agentWatch 30/1m per user",
        "description": "One watch per intent and key holds the total listening time, four hours from when it opened. Restarting the command joins the same watch and never extends it. Once the time is used up the watch stays expired: a plain start is refused with WATCH_DEADLINE_PASSED, and only renew opens a new allowance. A watch that ended on a verdict (authorized or rejected) is a finished review, so the next review starts without renew. Starting records a check-in, so the review page can say an agent is listening.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "renew": {
                    "type": "boolean",
                    "description": "Open a new listening allowance after the previous one ran out. Only when the person asks the agent to listen again; a restart without it is refused once the allowance is used up."
                  },
                  "build": {
                    "type": "boolean",
                    "description": "The agent's declaration of the person's current instruction: true when they asked for the change to be implemented, so the agent builds as soon as the reviewer authorizes a revision while told so; false when they asked only for a proposal, or later said to hold off. Omit it on restarts after applying answers to keep what the review already holds. False unless declared. The server checks who declared it and for which review, not what the person asked; the reviewer seeing it before authorizing is the human check."
                  },
                  "declare": {
                    "type": "boolean",
                    "description": "With build: change the declaration of this key's latest review of the intent and start nothing (watch-review --declare). It reaches a review that has already ended, so a hold-off still stops an authorization whose delivery was lost. Answers 200 with the stored value, 409 NO_REVIEW when no review exists."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The run joined the open watch for this intent and key, or opened one.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "watchId": "3f2a9c1e-8b4d-4e2a-9c1e-7a6b5c4d3e2f",
                  "startedAt": "2026-10-09T10:00:00.000Z",
                  "deadlineAt": "2026-10-09T14:00:00.000Z",
                  "pollIntervalSeconds": 10,
                  "buildOnAuthorization": true
                }
              }
            }
          },
          "400": {
            "description": "The intent ID is not a UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or expired credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API_KEY_REQUIRED: a signed-in browser session cannot listen; or the key lacks read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such intent in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "WATCH_DEADLINE_PASSED: the listening time for this review is used up; send renew only when the person asks to listen again. Or NO_REVIEW, for declare with no review to change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "REPO_AUTHORITY_REQUIRED: the intent is not repository-owned.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "BUILD_DECLARATION_NOT_SAVED: an explicit build declaration could not be stored. It did not take effect; do not rely on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "SCHEMA_MIGRATION_PENDING: this server cannot listen yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/intents/{id}/agent-watch/{watchId}": {
      "get": {
        "operationId": "get_intents_id_agent-watch_watchId",
        "tags": [
          "proposal review"
        ],
        "summary": "One poll of a listening repository agent: records the check-in and says whether there is anything to wake the agent for.",
        "x-stability": "beta",
        "x-scope": "read",
        "x-auth": "api-key-only",
        "x-rateLimit": "agentWatch 30/1m per user",
        "description": "Work is an open change request this key may act on that has not been refused and that the agent has not acknowledged in this watch. The poll records the handoff before it responds, but a handoff never suppresses a later poll: a lost response or a crashed command would otherwise strand the answer. The agent reading a request through GET change-requests (list or single) records an acknowledgment, which is a ten-minute lease rather than a receipt: the response can be lost, or the agent can stop before applying, so a request still open after the lease is offered again. Applying or rejecting it ends the offer for good. A refused request is never offered again, because the agent learned of the refusal from its own apply call. With no work, a verdict on the current revision ends the watch (authorized or rejected), and so does the deadline. An authorization carries build: true only when the reviewer authorized this exact revision while the page said the agent builds next (see expectedBuild on the authorization route) AND the agent's declaration still holds, so a declaration changed after the reviewer looked can stop a build but never start one, AND the revision passes the execution checks the execution prompt applies. A confirmed build that fails them carries build: false with buildBlockers naming why, and the agent is told not to implement. Polls never write to the intent itself, so they do not disturb its version token.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "id path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "watchId",
            "in": "path",
            "required": true,
            "description": "watchId path parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "final",
            "in": "query",
            "required": false,
            "description": "Marks the last poll of a run that timed out, for the watch's record.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "outcome is work (with requestIds), waiting, authorized (with build, and buildBlockers when a confirmed build fails the execution checks), rejected, deadline, or ended (with endReason, and build when it ended on an authorization).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "outcome": "work",
                  "requestIds": [
                    "c601f811-c8fa-4bd2-8861-374a29823fb4"
                  ],
                  "deadlineAt": "2026-10-09T14:00:00.000Z",
                  "repoBodyRevision": "45e6ef1d..."
                }
              }
            }
          },
          "400": {
            "description": "The intent or watch ID is not a UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or expired credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API_KEY_REQUIRED, or the key lacks read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such watch for this intent and key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The review state could not be read or the poll could not be recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "SCHEMA_MIGRATION_PENDING: this server cannot listen yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    }
  }
}
