From 8f951ab2497c0abebabc97351447739ae61a624f Mon Sep 17 00:00:00 2001 From: Cedric Vidal Date: Mon, 12 Oct 2026 18:16:36 +0200 Subject: [PATCH 01/15] feat(taxonomy): restack readout handler on DAG foundation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cbbd5eb-61c6-4e6a-bb38-c377be5b2163 --- AGENTS.md | 2 + ENV_VARIABLES.md | 28 ++ .../openapi-snapshot.test.ts.snap | 63 +++++ apps/api/src/archive-har.test.ts | 42 ++- apps/api/src/archive-har.ts | 29 +- apps/api/src/index.ts | 2 + apps/api/src/routes/requests/taxonomy.test.ts | 107 ++++++++ apps/api/src/routes/requests/taxonomy.ts | 30 +++ apps/portal/src/components/TaxonomyTab.tsx | 248 ++++++++++++++++++ apps/portal/src/lib/api.ts | 10 + apps/portal/src/pages/RunDetail.tsx | 10 + apps/portal/src/types.ts | 1 + apps/scheduler/src/handler-dispatcher.test.ts | 22 +- apps/scheduler/src/notify-routes.test.ts | 8 +- apps/workers/taxonomy/Dockerfile | 50 ++++ apps/workers/taxonomy/handler.yaml | 11 + apps/workers/taxonomy/package.json | 24 ++ apps/workers/taxonomy/src/cli.ts | 206 +++++++++++++++ apps/workers/taxonomy/src/index.ts | 30 +++ apps/workers/taxonomy/src/prompt.ts | 88 +++++++ .../taxonomy/src/taxonomy-generator.ts | 202 ++++++++++++++ .../taxonomy/src/taxonomy-queue-processor.ts | 190 ++++++++++++++ apps/workers/taxonomy/src/tools.ts | 154 +++++++++++ apps/workers/taxonomy/tsconfig.json | 8 + docker-compose.dev.yml | 16 ++ docker-compose.yml | 46 ++++ docs/architecture/db-migrations.md | 2 +- docs/architecture/post-processing.md | 85 ++++-- package.json | 5 +- .../031-migrate-services-to-handler-dag.ts | 2 +- .../032-add-handler-status-index.ts | 8 + packages/shared/src/index.ts | 1 + packages/shared/src/schemas/index.ts | 1 + packages/shared/src/schemas/request.ts | 1 + packages/shared/src/schemas/taxonomy.test.ts | 180 +++++++++++++ packages/shared/src/schemas/taxonomy.ts | 104 ++++++++ .../src/scripts/register-handler.test.ts | 6 +- packages/shared/src/types/types.ts | 4 +- pnpm-lock.yaml | 40 ++- 39 files changed, 2025 insertions(+), 41 deletions(-) create mode 100644 apps/api/src/routes/requests/taxonomy.test.ts create mode 100644 apps/api/src/routes/requests/taxonomy.ts create mode 100644 apps/portal/src/components/TaxonomyTab.tsx create mode 100644 apps/workers/taxonomy/Dockerfile create mode 100644 apps/workers/taxonomy/handler.yaml create mode 100644 apps/workers/taxonomy/package.json create mode 100644 apps/workers/taxonomy/src/cli.ts create mode 100644 apps/workers/taxonomy/src/index.ts create mode 100644 apps/workers/taxonomy/src/prompt.ts create mode 100644 apps/workers/taxonomy/src/taxonomy-generator.ts create mode 100644 apps/workers/taxonomy/src/taxonomy-queue-processor.ts create mode 100644 apps/workers/taxonomy/src/tools.ts create mode 100644 apps/workers/taxonomy/tsconfig.json create mode 100644 packages/shared/src/schemas/taxonomy.test.ts create mode 100644 packages/shared/src/schemas/taxonomy.ts diff --git a/AGENTS.md b/AGENTS.md index 409016367..d51fc5f64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,7 @@ apps/ coder-acp-copilot/ # GitHub Copilot worker (ACP SDK) coder-acp-claude-code/ # Claude Code worker (ACP SDK) report-generator/ # Post-run report generation + taxonomy/ # Post-run taxonomy generation model-scanners/ # Feature detection for Copilot and Anthropic models version-checkers/ # Poll for new agent/tool releases key-updaters/ # GitHub auth cookie management for VS Code Web @@ -69,6 +70,7 @@ Each worker implements the same queue-processor interface but adapts a different | `coder-acp-copilot` | GitHub Copilot | ACP SDK v0.14.1 | | `coder-acp-claude-code` | Claude Code | ACP SDK v0.13.1 | | `report-generator` | — | Copilot SDK | +| `taxonomy` | — | Copilot SDK | Workers consume tasks from Azure Storage Queues (named `queue-`) and write results to MongoDB and Blob Storage. Each has its own Dockerfile and docker-compose profile. diff --git a/ENV_VARIABLES.md b/ENV_VARIABLES.md index 4f20cc04b..503f16ff3 100644 --- a/ENV_VARIABLES.md +++ b/ENV_VARIABLES.md @@ -889,6 +889,34 @@ Timeout for the Copilot SDK session used by the report-generator worker. If the Git commit hash embedded in reporter metadata. Automatically set during CI/CD builds. Used to track which version of the report-generator produced a given report. +## Taxonomy Handler Configuration + +### AZURE_STORAGE_QUEUE_TAXONOMY +**Default:** `pp-taxonomy-queue` +**Type:** string + +Azure Storage Queue name for taxonomy-generation jobs. The scheduler enqueues `{ type: "taxonomy", requestId, runId }` messages here after the `pp-atif` dependency completes, and the taxonomy worker polls this queue. + +### TAXONOMY_MODEL +**Default:** `gpt-5.4` +**Type:** string + +The LLM model used by the taxonomy worker (via the Copilot SDK) to generate structured taxonomy JSON from run metadata and ATIF trajectories. + +> **Model choice matters for taxonomy quality.** A/B testing across nine models on a multi-iteration run with a real defect (agent lost context in iteration 2) showed the verdict ranged from 72 to 100 depending on the model. `gpt-4.1` (the previous default) missed action items and, on harder runs, produced the thinnest output; weaker models rubber-stamped a buggy run with a perfect score. `gpt-5.4` was the best value: it correctly caught the critical defect while using the fewest tokens and lowest cost of the models tested. Stronger alternatives (e.g. `claude-opus-4-7`) yield richer evidence at higher token cost; avoid `gpt-5.3-codex` as a default (≈1M tokens / ~7 min per run). + +### SCOPE_MT_API_URL +**Default:** `http://localhost:3001` (local), `http://api:80` (Docker) +**Type:** URL string + +Base URL of the Scope API. The taxonomy worker calls this to fetch the request document and active run state through its Copilot tools. + +### SESSION_TIMEOUT_MS +**Default:** `300000` (5 minutes) +**Type:** integer (milliseconds) + +Timeout for the Copilot SDK session used by the taxonomy worker. If the model does not finish generating a valid taxonomy JSON document within this window, the worker marks `run.handlerStatus.pp-taxonomy` as failed. + ## Scheduler Configuration ### SCHEDULER_POLL_INTERVAL_MS diff --git a/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap b/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap index 317154c30..6d28ff754 100644 --- a/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap +++ b/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap @@ -3358,6 +3358,9 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], "type": "string", }, + "taxonomyUrl": { + "type": "string", + }, "tokenUsage": { "$ref": "#/components/schemas/TokenUsage", }, @@ -10436,6 +10439,40 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], }, }, + "/api/v1/requests/{id}/runs/{runId}/taxonomy": { + "get": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + }, + }, + { + "in": "path", + "name": "runId", + "required": true, + "schema": { + "type": "string", + }, + }, + ], + "responses": { + "200": { + "description": "Taxonomy v1 JSON document (scorecard, behavior analysis, action list)", + }, + "404": { + "description": "Not found — taxonomy not yet generated", + }, + }, + "summary": "Download taxonomy JSON for a specific attempt", + "tags": [ + "Requests", + ], + }, + }, "/api/v1/requests/{id}/runs/{runId}/tool-calls": { "get": { "parameters": [ @@ -10538,6 +10575,32 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], }, }, + "/api/v1/requests/{id}/taxonomy": { + "get": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + }, + }, + ], + "responses": { + "200": { + "description": "Taxonomy v1 JSON document (scorecard, behavior analysis, action list)", + }, + "404": { + "description": "Not found — taxonomy not yet generated", + }, + }, + "summary": "Download taxonomy JSON for the run", + "tags": [ + "Requests", + ], + }, + }, "/api/v1/requests/{id}/tool-calls": { "get": { "parameters": [ diff --git a/apps/api/src/archive-har.test.ts b/apps/api/src/archive-har.test.ts index 9d01fc261..af8205f2b 100644 --- a/apps/api/src/archive-har.test.ts +++ b/apps/api/src/archive-har.test.ts @@ -99,6 +99,17 @@ describe("rewriteHarUrlsForArchive", () => { expect(result.run!.turns![1].toolCallsUrl).toBe("iteration-2.tool-calls.jsonl"); expect(result.run!.turns![2].toolCallsUrl).toBeUndefined(); }); + + it("rewrites run.taxonomyUrl to taxonomy.json", () => { + const resource = { + run: { + taxonomyUrl: "https://storage.blob.core.windows.net/snapshots/abc123/runs/attempt-1/taxonomy.json", + turns: [], + }, + }; + const result = rewriteHarUrlsForArchive(resource); + expect(result.run!.taxonomyUrl).toBe("taxonomy.json"); + }); }); // --- blobNameFromSnapshotsUrl --- @@ -902,7 +913,36 @@ describe("packRunIntoTar", () => { await packRunIntoTar(p, run, container, "run-011", () => true); p.finalize(); await entriesPromise; - expect(run.codebase!.archiveUrl).toBe(blobUrl); }); + + it("bundles run-level taxonomy.json and rewrites taxonomyUrl", async () => { + const { pack } = await import("tar-stream"); + const p = pack(); + const taxonomyData = Buffer.from('{"topics":["agent"]}'); + const container = makeMockBlobContainer({ + "run-012/runs/attempt-1/taxonomy.json": { body: taxonomyData, length: taxonomyData.length }, + }); + + const run: ArchivableRun = { + _id: "run-012", + run: { + taxonomyUrl: "https://storage.blob.core.windows.net/snapshots/run-012/runs/attempt-1/taxonomy.json", + turns: [], + }, + }; + + const entriesPromise = collectPackEntries(p); + await packRunIntoTar(p, run, container, "run-012", () => true); + p.finalize(); + + const entries = await entriesPromise; + const names = entries.map(e => e.name); + expect(names).toContain("run-012/taxonomy.json"); + expect(entries.find(e => e.name === "run-012/taxonomy.json")!.data).toEqual(taxonomyData); + + const yaml = entries.find(e => e.name === "run-012/run.yaml")!.data.toString(); + expect(yaml).toContain("taxonomy.json"); + expect(yaml).not.toContain("blob.core.windows.net"); + }); }); diff --git a/apps/api/src/archive-har.ts b/apps/api/src/archive-har.ts index 204eaf124..c28658aea 100644 --- a/apps/api/src/archive-har.ts +++ b/apps/api/src/archive-har.ts @@ -41,11 +41,12 @@ export function blobNameFromLogsUrl(url: string): string | null { /** * Deep-clone a run resource and rewrite `harUrl`, `rawChatUrl`, - * `chatResultUrl`, and `toolCallsUrl` fields to relative archive paths. + * `taxonomyUrl`, `chatResultUrl`, and `toolCallsUrl` fields to relative archive paths. * * - `run.harUrl` → `"run.har"` * - Per-turn `harUrl` → `"iteration-{N}.har"` * - `run.rawChatUrl` → `"run.chat-export.json"` + * - `run.taxonomyUrl` → `"taxonomy.json"` * - Per-turn `rawChatUrl` → `"iteration-{N}.chat-export.json"` * - Per-turn `chatResultUrl` → `"iteration-{N}.chat-result.json"` * - Per-turn `toolCallsUrl` → `"iteration-{N}.tool-calls.jsonl"` @@ -54,6 +55,7 @@ export function rewriteHarUrlsForArchive; [key: string]: unknown; }; @@ -66,6 +68,9 @@ export function rewriteHarUrlsForArchive ({ + checkMigrations: vi.fn().mockResolvedValue({ ready: true, applied: ["001", "002"], pending: [] }), +})); + +vi.hoisted(() => { + process.env.STORAGE_CONNECTION_STRING = "DefaultEndpointsProtocol=https;AccountName=mockaccount;AccountKey=bW9jaw==;EndpointSuffix=core.windows.net"; +}); + +vi.mock("../../llm.js", () => ({ isLlmAvailable: vi.fn().mockReturnValue(false) })); +vi.mock("../../prompt-feature-llm.js", () => ({ isLlmAvailable: vi.fn().mockReturnValue(false) })); +vi.mock("../../task-prompt-llm.js", () => ({ isTaskPromptLlmAvailable: vi.fn().mockReturnValue(false) })); + +const { mockDownload, mockGetProperties, mockGetBlockBlobClient, mockGetBlobClient, mockGetContainerClient } = vi.hoisted(() => { + const mockDownload = vi.fn(); + const mockGetProperties = vi.fn(); + const mockGetBlockBlobClient = vi.fn().mockReturnValue({ download: mockDownload, getProperties: mockGetProperties }); + const mockGetBlobClient = vi.fn().mockReturnValue({ download: mockDownload, getProperties: mockGetProperties }); + const mockGetContainerClient = vi.fn().mockReturnValue({ getBlockBlobClient: mockGetBlockBlobClient, getBlobClient: mockGetBlobClient }); + return { mockDownload, mockGetProperties, mockGetBlockBlobClient, mockGetBlobClient, mockGetContainerClient }; +}); + +vi.mock("@azure/storage-blob", async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + BlobServiceClient: { fromConnectionString: vi.fn().mockReturnValue({ getContainerClient: mockGetContainerClient }) }, + }; +}); + +describe("taxonomy endpoints", () => { + let mocks: ReturnType; + + beforeAll(() => { + mocks = createAllMockDependencies(); + _injectTestDependencies(mocks); + }); + + beforeEach(() => { + vi.clearAllMocks(); + mocks = createAllMockDependencies(); + _injectTestDependencies(mocks); + rewireBlobMocks({ mockDownload, mockGetProperties, mockGetBlockBlobClient, mockGetBlobClient, mockGetContainerClient }); + mockDownload.mockResolvedValue({ readableStreamBody: readableFrom('{"status":"ok"}'), contentLength: 15 }); + mockGetProperties.mockResolvedValue({ contentLength: 15 }); + }); + + describe.each(VARIANTS)("$label", (variant) => { + const taxonomyUrl = (id: string) => variant.url(id) + "taxonomy"; + + it("returns 404 when request not found", async () => { + (mocks.collection.findOne as ReturnType).mockResolvedValue(null); + const res = await supertest(app).get(taxonomyUrl("missing")); + expect(res.status).toBe(404); + expect(res.body).toMatchObject({ error: "Request not found" }); + }); + + it("returns 404 when taxonomyUrl is not set", async () => { + variant.mockRequest(mocks, "req-1", { _id: "run-1", attemptNumber: 1, status: "done" }); + const res = await supertest(app).get(taxonomyUrl("req-1")); + expect(res.status).toBe(404); + expect(res.body).toMatchObject({ error: "Taxonomy not yet generated for this run" }); + }); + + it("returns taxonomy JSON when taxonomyUrl is set", async () => { + variant.mockRequest(mocks, "req-1", { + _id: "run-1", + attemptNumber: 1, + status: "done", + taxonomyUrl: blobUrl("req-1/taxonomy.json"), + }); + + const res = await supertest(app).get(taxonomyUrl("req-1")).buffer(true).parse(rawParser); + expect(res.status).toBe(200); + expect(res.headers["content-type"]).toMatch("application/json"); + expect(res.headers["content-disposition"]).toContain("req-1-taxonomy.json"); + expect(mockGetBlockBlobClient).toHaveBeenCalledWith("req-1/taxonomy.json"); + }); + }); + + it("works with per-run variant", async () => { + RUN_LEVEL.mockRequest(mocks, "req-1", { + _id: "run-1", + attemptNumber: 1, + status: "done", + taxonomyUrl: blobUrl("req-1/runs/run-1/taxonomy.json"), + }); + + const res = await supertest(app).get(RUN_LEVEL.url("req-1") + "taxonomy").buffer(true).parse(rawParser); + expect(res.status).toBe(200); + expect(res.headers["content-disposition"]).toContain("req-1-taxonomy.json"); + expect(mockGetBlockBlobClient).toHaveBeenCalledWith("req-1/runs/run-1/taxonomy.json"); + }); +}); diff --git a/apps/api/src/routes/requests/taxonomy.ts b/apps/api/src/routes/requests/taxonomy.ts new file mode 100644 index 000000000..e74d303f6 --- /dev/null +++ b/apps/api/src/routes/requests/taxonomy.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { RouteContext } from "../../route-context.js"; +import { downloadBlobToResponse } from "./blob-helpers.js"; +import { registerArtifactRoutes } from "./artifact-route.js"; + +export function registerRequestsTaxonomyRoutes(ctx: RouteContext): void { + registerArtifactRoutes(ctx, { + path: "taxonomy", + summary: "Download taxonomy JSON for the run", + perRunSummary: "Download taxonomy JSON for a specific attempt", + responseDescription: "Taxonomy v1 JSON document (scorecard, behavior analysis, action list)", + errorResponses: { 404: { description: "Not found — taxonomy not yet generated" } }, + blobNotFoundMessage: "Taxonomy file not found — the blob may have been deleted or is no longer available", + handler: async (ctx, _req, res, targetRun, id) => { + const taxonomyUrl = targetRun.taxonomyUrl; + if (!taxonomyUrl) { + res.status(404).json({ error: "Taxonomy not yet generated for this run" }); + return; + } + + await downloadBlobToResponse(ctx, res, taxonomyUrl, { + contentType: "application/json", + filename: `${id}-taxonomy.json`, + label: "Taxonomy", + }); + }, + }); +} diff --git a/apps/portal/src/components/TaxonomyTab.tsx b/apps/portal/src/components/TaxonomyTab.tsx new file mode 100644 index 000000000..e97c8c8c4 --- /dev/null +++ b/apps/portal/src/components/TaxonomyTab.tsx @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useQuery } from "@tanstack/react-query"; +import { api } from "@/lib/api"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Loader2, Download, ChevronDown, ChevronRight } from "lucide-react"; +import { useState } from "react"; + +interface TaxonomyDimension { + name: string; + score: number; + maxScore: number; + justification: string; +} + +interface BehaviorPattern { + id: string; + category: string; + observation: string; + evidence: string[]; + impact: "positive" | "negative" | "neutral"; +} + +interface ActionItem { + id: string; + priority: "high" | "medium" | "low"; + title: string; + description: string; + category: string; +} + +interface TaxonomyData { + meta?: { + schemaVersion?: string; + generatedAt?: string; + requestId?: string; + runId?: string; + model?: string; + }; + scorecard?: { + overall?: number; + dimensions?: TaxonomyDimension[]; + }; + behaviorAnalysis?: { + patterns?: BehaviorPattern[]; + }; + actionList?: { + items?: ActionItem[]; + }; +} + +function ScoreBar({ score, maxScore }: { score: number; maxScore: number }) { + const pct = Math.round((score / maxScore) * 100); + const color = pct >= 80 ? "bg-green-500" : pct >= 50 ? "bg-yellow-500" : "bg-red-500"; + return ( +
+
+
+
+ {score}/{maxScore} +
+ ); +} + +function ImpactBadge({ impact }: { impact: string }) { + const variant = impact === "positive" ? "default" : impact === "negative" ? "destructive" : "secondary"; + return {impact}; +} + +function PriorityBadge({ priority }: { priority: string }) { + const colors: Record = { + high: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300", + medium: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300", + low: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300", + }; + return {priority}; +} + +function CollapsibleSection({ title, defaultOpen = true, children }: { title: string; defaultOpen?: boolean; children: React.ReactNode }) { + const [open, setOpen] = useState(defaultOpen); + return ( +
+ + {open &&
{children}
} +
+ ); +} + +export function TaxonomyTab({ runId, attemptRunId }: { runId: string; attemptRunId?: string }) { + const url = attemptRunId ? api.runTaxonomyUrl(runId, attemptRunId) : api.taxonomyUrl(runId); + + const { data: taxonomy, isLoading, error } = useQuery({ + queryKey: ["run-taxonomy", runId, attemptRunId], + queryFn: async () => { + const res = await fetch(url); + if (!res.ok) { + if (res.status === 404) return null; + throw new Error(`Failed to fetch taxonomy: ${res.statusText}`); + } + return res.json(); + }, + }); + + if (isLoading) { + return ( +
+ + Loading taxonomy… +
+ ); + } + + if (error) { + return ( +
+

Failed to load taxonomy: {(error as Error).message}

+
+ ); + } + + if (!taxonomy) { + return ( +
+

Taxonomy not yet generated for this run.

+

It will appear here once post-processing completes.

+
+ ); + } + + const { meta, scorecard, behaviorAnalysis, actionList } = taxonomy; + + return ( +
+ {/* Header with download */} +
+
+

Run Taxonomy

+ {meta?.generatedAt && ( +

+ Generated {new Date(meta.generatedAt).toLocaleString()} + {meta.model && <> · Model: {meta.model}} +

+ )} +
+ +
+ + {/* Scorecard */} + {scorecard && ( + + + + Scorecard + {scorecard.overall !== undefined && ( + {scorecard.overall}% + )} + + + + {scorecard.dimensions?.map((dim) => ( +
+
+ {dim.name} +
+ +

{dim.justification}

+
+ ))} +
+
+ )} + + {/* Behavior Analysis */} + {behaviorAnalysis?.patterns && behaviorAnalysis.patterns.length > 0 && ( + + + + Behavior Analysis ({behaviorAnalysis.patterns.length}) + + + + +
+ {behaviorAnalysis.patterns.map((pattern) => ( +
+
+ {pattern.category} + +
+

{pattern.observation}

+ {pattern.evidence.length > 0 && ( +
    + {pattern.evidence.map((e, i) =>
  • {e}
  • )} +
+ )} +
+ ))} +
+
+
+
+ )} + + {/* Action List */} + {actionList?.items && actionList.items.length > 0 && ( + + + + Action Items ({actionList.items.length}) + + + + +
+ {actionList.items.map((item) => ( +
+ +
+

{item.title}

+

{item.description}

+ {item.category} +
+
+ ))} +
+
+
+
+ )} + + {/* Raw JSON fallback — collapsed by default */} + +
+          {JSON.stringify(taxonomy, null, 2)}
+        
+
+
+ ); +} diff --git a/apps/portal/src/lib/api.ts b/apps/portal/src/lib/api.ts index 1258ac38e..1673dfb0a 100644 --- a/apps/portal/src/lib/api.ts +++ b/apps/portal/src/lib/api.ts @@ -377,6 +377,16 @@ export const api = { return `${BASE}/requests/${id}/atif${qs}`; }, + /** Get taxonomy JSON URL for a request (latest run) */ + taxonomyUrl: (id: string): string => { + return `${BASE}/requests/${id}/taxonomy`; + }, + + /** Get taxonomy JSON URL for a specific attempt */ + runTaxonomyUrl: (requestId: string, runId: string): string => { + return `${BASE}/requests/${requestId}/runs/${runId}/taxonomy`; + }, + /** Download a batch archive of multiple runs as a single .tar.gz */ batchArchive: async (ids: string[]): Promise => { const resp = await apiClient(`${BASE}/requests/archive`, { diff --git a/apps/portal/src/pages/RunDetail.tsx b/apps/portal/src/pages/RunDetail.tsx index 9eaed350f..224372225 100644 --- a/apps/portal/src/pages/RunDetail.tsx +++ b/apps/portal/src/pages/RunDetail.tsx @@ -24,6 +24,7 @@ import { CriteriaGraphView } from "@/components/CriteriaGraphView"; import { HarNetworkViewer } from "@/components/HarNetworkViewer"; import { ConversationView } from "@/components/ConversationView"; import { VideoPlayer } from "@/components/VideoPlayer"; +import { TaxonomyTab } from "@/components/TaxonomyTab"; import { useLogStream } from "@/hooks/use-log-stream"; import { useAllTurnsToolCalls } from "@/hooks/useHarExtraction"; import { MarkdownRenderer } from "@/components/MarkdownRenderer"; @@ -718,6 +719,7 @@ export function RunDetail() { } const hasHarData = !!(activeRun?.harUrl || activeRun?.turns?.some(t => t.harUrl)); + const hasTaxonomy = !!activeRun?.taxonomyUrl; const hasVideoData = !!(activeRun?.videoUrls?.length || activeRun?.setupVideoUrls?.length || activeRun?.turns?.some(t => t.videoUrls?.length)); const videoCount = (activeRun?.setupVideoUrls?.length ?? 0) + (activeRun?.videoUrls?.length ?? 0) @@ -1166,6 +1168,7 @@ export function RunDetail() { )} {hasHarData && Network} {hasHarData && Tool Calls} + {hasTaxonomy && Taxonomy} {hasVideoData && } {(hasConfiguredObservations || hasObservationResults) && ( @@ -1245,6 +1248,13 @@ export function RunDetail() { )} + {/* Taxonomy tab — structured scorecard + behavior analysis + action items */} + {hasTaxonomy && ( + + + + )} + {/* Video tab — session recording player */} {hasVideoData && ( diff --git a/apps/portal/src/types.ts b/apps/portal/src/types.ts index 1ee0d61f0..4767e887e 100644 --- a/apps/portal/src/types.ts +++ b/apps/portal/src/types.ts @@ -187,6 +187,7 @@ export interface RunState { * instead. Empty/absent when no run-subject observations were selected. */ observationResults?: CriterionResult[]; + taxonomyUrl?: string; } export interface Run { diff --git a/apps/scheduler/src/handler-dispatcher.test.ts b/apps/scheduler/src/handler-dispatcher.test.ts index 32d954243..847e6be90 100644 --- a/apps/scheduler/src/handler-dispatcher.test.ts +++ b/apps/scheduler/src/handler-dispatcher.test.ts @@ -16,11 +16,11 @@ const HANDLERS: HandlerServiceDocument[] = [ dependsOn: [], }, { - _id: "pp-example", + _id: "pp-taxonomy", type: "post-process-handler", version: 1, - queue: "pp-example-queue", - selector: "example", + queue: "pp-taxonomy-queue", + selector: "taxonomy", autoBackfill: false, dependsOn: ["pp-atif"], }, @@ -87,7 +87,7 @@ describe("HandlerDispatcher — drain detection", () => { it("is drained when all handlers are done", () => { expect( - isDrained(HANDLERS, { "pp-atif": { status: "done" }, "pp-example": { status: "done" } }), + isDrained(HANDLERS, { "pp-atif": { status: "done" }, "pp-taxonomy": { status: "done" } }), ).toBe(true); }); @@ -99,13 +99,13 @@ describe("HandlerDispatcher — drain detection", () => { expect( isDrained(HANDLERS, { "pp-atif": { status: "done" }, - "pp-example": { status: "processing" }, + "pp-taxonomy": { status: "processing" }, }), ).toBe(false); }); it("is drained when a failed dependency blocks its descendant", () => { - // pp-atif failed → pp-example can never run (blocked) → terminal. + // pp-atif failed → pp-taxonomy can never run (blocked) → terminal. expect(isDrained(HANDLERS, { "pp-atif": { status: "failed" } })).toBe(true); }); @@ -113,7 +113,7 @@ describe("HandlerDispatcher — drain detection", () => { expect( isDrained(HANDLERS, { "pp-atif": { status: "done" }, - "pp-example": { status: "failed" }, + "pp-taxonomy": { status: "failed" }, }), ).toBe(true); }); @@ -143,7 +143,7 @@ describe("HandlerDispatcher — maybeTriggerReports", () => { run: { _id: "run-1", status: "done", - handlerStatus: { "pp-atif": { status: "done" }, "pp-example": { status: "done" } }, + handlerStatus: { "pp-atif": { status: "done" }, "pp-taxonomy": { status: "done" } }, }, }); collection.findOneAndUpdate.mockResolvedValue({ _id: "req-1" }); // claim succeeds @@ -203,7 +203,7 @@ describe("HandlerDispatcher — maybeTriggerReports", () => { run: { _id: "run-2", status: "done", - handlerStatus: { "pp-atif": { status: "done" }, "pp-example": { status: "done" } }, + handlerStatus: { "pp-atif": { status: "done" }, "pp-taxonomy": { status: "done" } }, }, }); @@ -220,7 +220,7 @@ describe("HandlerDispatcher — maybeTriggerReports", () => { run: { _id: "run-1", status: "done", - handlerStatus: { "pp-atif": { status: "done" }, "pp-example": { status: "done" } }, + handlerStatus: { "pp-atif": { status: "done" }, "pp-taxonomy": { status: "done" } }, }, }); collection.findOneAndUpdate.mockResolvedValue(null); // another path claimed it first @@ -237,7 +237,7 @@ describe("HandlerDispatcher — maybeTriggerReports", () => { run: { _id: "run-1", status: "done", - handlerStatus: { "pp-atif": { status: "done" }, "pp-example": { status: "done" } }, + handlerStatus: { "pp-atif": { status: "done" }, "pp-taxonomy": { status: "done" } }, }, }); collection.findOneAndUpdate.mockResolvedValue({ _id: "req-1" }); diff --git a/apps/scheduler/src/notify-routes.test.ts b/apps/scheduler/src/notify-routes.test.ts index 2d7ef741b..b942faeee 100644 --- a/apps/scheduler/src/notify-routes.test.ts +++ b/apps/scheduler/src/notify-routes.test.ts @@ -173,11 +173,11 @@ describe("createHttpServer", () => { servers.push(server); const doc = { - _id: "pp-example", + _id: "pp-taxonomy", type: "post-process-handler", version: 1, - queue: "pp-example-queue", - selector: "example", + queue: "pp-taxonomy-queue", + selector: "taxonomy", autoBackfill: false, dependsOn: ["pp-atif"], }; @@ -189,7 +189,7 @@ describe("createHttpServer", () => { }); expect(response.status).toBe(200); - expect(await response.json()).toEqual({ ok: true, handlerId: "pp-example" }); + expect(await response.json()).toEqual({ ok: true, handlerId: "pp-taxonomy" }); expect(handler.registerHandler).toHaveBeenCalledWith(doc); }); diff --git a/apps/workers/taxonomy/Dockerfile b/apps/workers/taxonomy/Dockerfile new file mode 100644 index 000000000..b6be8f2ad --- /dev/null +++ b/apps/workers/taxonomy/Dockerfile @@ -0,0 +1,50 @@ +FROM node:22.22.0-slim AS builder + +RUN corepack enable && corepack prepare pnpm@10.29.1 --activate + +WORKDIR /app + +COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ +COPY tsconfig.base.json ./ +COPY packages/shared/package.json ./packages/shared/ +COPY apps/workers/taxonomy/package.json ./apps/workers/taxonomy/ + +RUN pnpm install --frozen-lockfile + +COPY packages/shared/tsconfig.json ./packages/shared/ +COPY packages/shared/src ./packages/shared/src/ +COPY apps/workers/taxonomy/tsconfig.json ./apps/workers/taxonomy/ +COPY apps/workers/taxonomy/src ./apps/workers/taxonomy/src/ +COPY apps/workers/taxonomy/handler.yaml ./apps/workers/taxonomy/ + +RUN pnpm --filter shared build && pnpm --filter taxonomy-handler build + +# --- Dev stage: hot reload with tsx watch --- +FROM builder AS dev +ENV NODE_ENV=development +ENV SERVICE_DIR=workers/taxonomy +COPY scripts/dev-entrypoint.sh /app/scripts/dev-entrypoint.sh +EXPOSE 80 +ENV PORT=80 +CMD ["/app/scripts/dev-entrypoint.sh"] + +# --- Production stage --- +FROM node:22.22.0-slim + +RUN corepack enable && corepack prepare pnpm@10.29.1 --activate + +WORKDIR /app + +COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ +COPY packages/shared/package.json ./packages/shared/ +COPY apps/workers/taxonomy/package.json ./apps/workers/taxonomy/ + +RUN pnpm install --frozen-lockfile --prod + +COPY --from=builder /app/packages/shared/dist ./packages/shared/dist/ +COPY --from=builder /app/apps/workers/taxonomy/dist ./apps/workers/taxonomy/dist/ +COPY --from=builder /app/apps/workers/taxonomy/handler.yaml ./apps/workers/taxonomy/handler.yaml + +WORKDIR /app/apps/workers/taxonomy + +CMD ["node", "dist/index.js"] diff --git a/apps/workers/taxonomy/handler.yaml b/apps/workers/taxonomy/handler.yaml new file mode 100644 index 000000000..b479f905e --- /dev/null +++ b/apps/workers/taxonomy/handler.yaml @@ -0,0 +1,11 @@ +# Post-process handler registration metadata. +# Read by the generic register-handler script and POSTed to the +# scheduler's POST /handlers/register endpoint on deploy. +_id: pp-taxonomy +type: post-process-handler +version: 1 +queue: pp-taxonomy-queue +selector: taxonomy +autoBackfill: false +dependsOn: + - pp-atif diff --git a/apps/workers/taxonomy/package.json b/apps/workers/taxonomy/package.json new file mode 100644 index 000000000..52cdd6b2f --- /dev/null +++ b/apps/workers/taxonomy/package.json @@ -0,0 +1,24 @@ +{ + "name": "taxonomy-handler", + "version": "1.0.0", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "dev": "tsx watch src/index.ts", + "start": "node dist/index.js", + "taxonomy": "tsx src/cli.ts", + "lint": "eslint src/" + }, + "dependencies": { + "shared": "workspace:*", + "@azure/storage-queue": "^12.25.0", + "@github/copilot-sdk": "^0.1.17", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "@types/node": "^22.7.0", + "tsx": "^4.19.0", + "typescript": "^5.6.2" + } +} diff --git a/apps/workers/taxonomy/src/cli.ts b/apps/workers/taxonomy/src/cli.ts new file mode 100644 index 000000000..a5f51f600 --- /dev/null +++ b/apps/workers/taxonomy/src/cli.ts @@ -0,0 +1,206 @@ +#!/usr/bin/env npx tsx +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Taxonomy post-processor developer CLI. + * + * Invokes the taxonomy generation loop directly against an existing requestId, + * streams the Copilot SDK session events live, and saves the validated taxonomy + * to a local JSON file. This is a developer/troubleshooting tool only — it never + * writes to Mongo (handlerStatus) or blob storage (taxonomy.json) and does not + * notify the scheduler. + * + * Usage: + * npx tsx src/cli.ts [options] + * pnpm taxonomy [options] + * + * Options: + * --run, -r Run id for the prompt/logging (default: active run from API). + * --model Override TAXONOMY_MODEL (e.g. a reasoning model). + * --timeout Override SESSION_TIMEOUT_MS (per-attempt session timeout). + * --output, -o Output file path (default: taxonomy-.json). + * --stdout Also print the taxonomy JSON to stdout. + * --quiet Suppress live session-event streaming on stderr. + * --help, -h Show this help. + */ +import "dotenv/config"; +import { writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { SessionEvent } from "@github/copilot-sdk"; +import { TokenManagerClient, type LogEvent, type RequestDocument } from "shared"; +import { generateTaxonomy } from "./taxonomy-generator.js"; + +interface CliArgs { + requestId: string; + runId?: string; + model?: string; + timeoutMs?: number; + output?: string; + stdout: boolean; + quiet: boolean; +} + +function printHelp(): void { + process.stderr.write( + [ + "Taxonomy post-processor developer CLI", + "", + "Usage: npx tsx src/cli.ts [options]", + "", + "Options:", + " --run, -r Run id for the prompt/logging (default: active run from API)", + " --model Override TAXONOMY_MODEL", + " --timeout Override SESSION_TIMEOUT_MS (per-attempt session timeout)", + " --output, -o Output file path (default: taxonomy-.json)", + " --stdout Also print the taxonomy JSON to stdout", + " --quiet Suppress live session-event streaming on stderr", + " --help, -h Show this help", + "", + ].join("\n"), + ); +} + +function parseArgs(argv: string[]): CliArgs | undefined { + const args: Partial & { stdout: boolean; quiet: boolean } = { stdout: false, quiet: false }; + const positionals: string[] = []; + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + switch (arg) { + case "--help": + case "-h": + printHelp(); + return undefined; + case "--run": + case "-r": + args.runId = argv[++i]; + break; + case "--model": + args.model = argv[++i]; + break; + case "--timeout": + args.timeoutMs = Number(argv[++i]); + break; + case "--output": + case "-o": + args.output = argv[++i]; + break; + case "--stdout": + args.stdout = true; + break; + case "--quiet": + args.quiet = true; + break; + default: + if (arg.startsWith("-")) { + throw new Error(`Unknown option: ${arg}`); + } + positionals.push(arg); + } + } + + const requestId = positionals[0]; + if (!requestId) { + throw new Error("Missing required argument"); + } + if (args.timeoutMs !== undefined && Number.isNaN(args.timeoutMs)) { + throw new Error("--timeout must be a number (milliseconds)"); + } + + return { ...args, requestId }; +} + +async function resolveRunId(apiBaseUrl: string, requestId: string, explicit?: string): Promise { + if (explicit) return explicit; + try { + const response = await fetch(`${apiBaseUrl}/api/v1/requests/${requestId}`); + if (response.ok) { + const request: RequestDocument = await response.json(); + if (request.run?._id) return request.run._id; + } + } catch { + // fall through to requestId fallback + } + return requestId; +} + +function summarizeEvent(event: SessionEvent): string | undefined { + switch (event.type) { + case "session.start": + return `session.start model=${event.data.selectedModel}`; + case "session.error": + return `session.error ${event.data.errorType}: ${event.data.message}`; + case "tool.execution_start": + return `tool.start ${event.data.toolName} ${JSON.stringify(event.data.arguments ?? {})}`; + case "tool.execution_complete": + return `tool.done ${event.data.success ? "ok" : "FAILED"}${event.data.error ? ` ${event.data.error.message}` : ""}`; + case "assistant.message": + return `assistant.message (${event.data.content.length} chars)`; + default: + return undefined; + } +} + +async function main(): Promise { + let parsed: CliArgs | undefined; + try { + parsed = parseArgs(process.argv.slice(2)); + } catch (error) { + process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n\n`); + printHelp(); + process.exit(2); + } + if (!parsed) return; // help shown + + const apiBaseUrl = process.env.SCOPE_MT_API_URL || "http://localhost:3001"; + const taxonomyModel = parsed.model || process.env.TAXONOMY_MODEL || "gpt-5.4"; + const sessionTimeoutMs = parsed.timeoutMs ?? (Number(process.env.SESSION_TIMEOUT_MS) || 5 * 60 * 1000); + const outputPath = resolve(parsed.output ?? `taxonomy-${parsed.requestId}.json`); + + const githubToken = process.env.GITHUB_TOKEN + ? process.env.GITHUB_TOKEN + : await new TokenManagerClient(process.env.TOKEN_MANAGER_URL).acquireToken("copilot-sdk"); + + const runId = await resolveRunId(apiBaseUrl, parsed.requestId, parsed.runId); + + const log = (level: LogEvent["level"], msg: string, data?: Record): void => { + if (parsed.quiet) return; + const suffix = data && Object.keys(data).length > 0 ? ` ${JSON.stringify(data)}` : ""; + process.stderr.write(`[${level}] ${msg}${suffix}\n`); + }; + + process.stderr.write( + `Generating taxonomy: requestId=${parsed.requestId} runId=${runId} model=${taxonomyModel} timeout=${sessionTimeoutMs}ms\n`, + ); + + const onEvent = parsed.quiet + ? undefined + : (event: SessionEvent): void => { + const summary = summarizeEvent(event); + if (summary) process.stderr.write(` · ${summary}\n`); + }; + + const taxonomy = await generateTaxonomy({ + requestId: parsed.requestId, + runId, + apiBaseUrl, + taxonomyModel, + githubToken, + sessionTimeoutMs, + log, + onEvent, + }); + + const json = JSON.stringify(taxonomy, null, 2); + await writeFile(outputPath, `${json}\n`, "utf-8"); + process.stderr.write(`\nTaxonomy written to ${outputPath}\n`); + if (parsed.stdout) { + process.stdout.write(`${json}\n`); + } +} + +main().catch((error) => { + process.stderr.write(`\nTaxonomy generation failed: ${error instanceof Error ? error.message : String(error)}\n`); + process.exit(1); +}); diff --git a/apps/workers/taxonomy/src/index.ts b/apps/workers/taxonomy/src/index.ts new file mode 100644 index 000000000..b1a4f322b --- /dev/null +++ b/apps/workers/taxonomy/src/index.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import "dotenv/config"; +import { TaxonomyQueueProcessor, type TaxonomyQueueProcessorConfig } from "./taxonomy-queue-processor.js"; + +const config: TaxonomyQueueProcessorConfig = { + mongoUri: process.env.MONGO_CONNECTION_STRING || process.env.AZURE_COSMOS_CONNECTION_STRING || "mongodb://localhost:27017", + mongoDatabase: process.env.MONGO_DATABASE || "requests-db", + mongoCollection: "requests", + storageAccountName: process.env.AZURE_STORAGE_ACCOUNT_NAME || "devstoreaccount1", + storageConnectionString: process.env.STORAGE_CONNECTION_STRING || process.env.AZURE_STORAGE_CONNECTION_STRING || undefined, + queueName: process.env.AZURE_STORAGE_QUEUE_TAXONOMY || "pp-taxonomy-queue", + batchSize: Number(process.env.BATCH_SIZE) || 1, + pollIntervalMs: Number(process.env.POLL_INTERVAL_MS) || 5000, + redisHost: process.env.REDIS_HOST || "localhost", + redisPort: Number(process.env.REDIS_PORT) || 6379, + redisPassword: process.env.REDIS_PASSWORD || "", + taxonomyModel: process.env.TAXONOMY_MODEL || "gpt-5.4", + apiBaseUrl: process.env.SCOPE_MT_API_URL || "http://localhost:3001", + sessionTimeoutMs: Number(process.env.SESSION_TIMEOUT_MS) || 5 * 60 * 1000, + schedulerUrl: process.env.SCHEDULER_URL, + tokenManagerUrl: process.env.TOKEN_MANAGER_URL, +}; + +const processor = new TaxonomyQueueProcessor(config); +processor.start().catch((err) => { + console.error("[taxonomy-handler] Fatal error:", err); + process.exit(1); +}); diff --git a/apps/workers/taxonomy/src/prompt.ts b/apps/workers/taxonomy/src/prompt.ts new file mode 100644 index 000000000..47ea28e4d --- /dev/null +++ b/apps/workers/taxonomy/src/prompt.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export const TAXONOMY_SYSTEM_PROMPT = `You generate benchmark taxonomy documents for completed Scope runs. + +Your job is to inspect the run metadata and ATIF trajectories, then return a taxonomy JSON document that conforms exactly to the taxonomy Zod schema enforced by the worker. + +Required JSON shape: +{ + "meta": { + "generatedAt": "ISO-8601 datetime with offset", + "schemaVersion": "string", + "runId": "string", + "requestId": "string" + }, + "scorecard": { + "summary": "string", + "profileResults": [ + { + "profileId": "string", + "select": "best|median|worst", + "taskOutcome": "success|partial|failure", + "overallScore": 0-100, + "dimensions": [ + { + "name": "string", + "score": 0-100, + "rationale": "string", + "evidence": ["string"] + } + ] + } + ], + "crossProfileInsights": ["string"] + }, + "behaviorAnalysis": { + "patterns": [ + { + "id": "string", + "category": "strength|weakness|inconsistency|regression", + "title": "string", + "description": "string", + "frequency": 0-1, + "impact": "critical|high|medium|low", + "evidence": [ + { + "runId": "string", + "iteration": 0-or-greater integer, + "excerpt": "string" + } + ] + } + ], + "agentPersonality": { + "communicationStyle": "string", + "problemSolvingApproach": "string", + "errorRecoveryBehavior": "string" + } + }, + "actionList": { + "fix": [actionItem], + "improve": [actionItem], + "investigate": [actionItem] + } +} + +Each actionItem must include: +- id +- title +- priority: critical|high|medium|low +- description +- ownership: agent-team|platform-team|prompt-engineer|scenario-author +- relatedPatterns: string[] +- suggestedApproach + +Output rules: +- Return raw JSON only. +- Do not wrap the JSON in markdown fences. +- Do not include commentary before or after the JSON. +- Every field must be valid for the schema. +- Preserve the 3-layer structure: scorecard, behaviorAnalysis, actionList. +- If a value is unknown, use the schema-compatible empty/null/default value rather than inventing facts. + +Use the available tools to gather evidence before answering: +- get_run_data for the request, scenario, run, turns, and criteria outcomes. +- get_atif_trajectory for ATIF evidence from individual iterations. + +Ground every classification in the actual run evidence. Prefer precise, stable labels over verbose prose. If validation feedback is provided later in the conversation, correct the JSON and return a full replacement document.`; diff --git a/apps/workers/taxonomy/src/taxonomy-generator.ts b/apps/workers/taxonomy/src/taxonomy-generator.ts new file mode 100644 index 000000000..ada150f2c --- /dev/null +++ b/apps/workers/taxonomy/src/taxonomy-generator.ts @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { CopilotClient, type PermissionHandler, SessionEvent } from "@github/copilot-sdk"; +import { + type LogEvent, + taxonomySchema, + type TaxonomyDocument, +} from "shared"; +import { TAXONOMY_SYSTEM_PROMPT } from "./prompt.js"; +import { createTaxonomyTools } from "./tools.js"; + +export const DEFAULT_SESSION_TIMEOUT_MS = 5 * 60 * 1000; +const DELTA_LOG_INTERVAL = 2_000; +const approveTaxonomyTools: PermissionHandler = request => + request.kind === "custom-tool" + ? { kind: "approved" } + : { kind: "denied-by-rules", rules: [] }; + +type TaxonomyLogFn = ( + level: LogEvent["level"], + msg: string, + data?: Record, +) => Promise | void; + +export interface GenerateTaxonomyParams { + requestId: string; + runId: string; + apiBaseUrl: string; + taxonomyModel: string; + githubToken: string; + sessionTimeoutMs?: number; + log: TaxonomyLogFn; + /** Optional raw session-event hook (used by the dev CLI to render live progress). */ + onEvent?: (event: SessionEvent) => void; +} + +/** + * Runs the Copilot SDK taxonomy generation loop and returns a schema-validated + * taxonomy document. This is side-effect-free with respect to Mongo and blob + * storage (it only reads run data and ATIF trajectories through the API); + * persistence is the caller's responsibility. + */ +export async function generateTaxonomy(params: GenerateTaxonomyParams): Promise { + const { requestId, runId, apiBaseUrl, taxonomyModel, githubToken, log, onEvent } = params; + const client = new CopilotClient({ githubToken }); + const tools = createTaxonomyTools(apiBaseUrl, requestId); + const timeoutMs = params.sessionTimeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS; + + let streamedResponse = ""; + let latestMessage = ""; + let lastLoggedCharCount = 0; + const toolNames = new Map(); + + try { + const session = await client.createSession({ + model: taxonomyModel, + streaming: true, + tools, + availableTools: [], + excludedTools: ["builtin:*", "mcp:*"], + onPermissionRequest: approveTaxonomyTools, + systemMessage: { mode: "replace", content: TAXONOMY_SYSTEM_PROMPT }, + }); + + session.on((event: SessionEvent) => { + onEvent?.(event); + switch (event.type) { + case "session.start": + void log("info", "Copilot session started", { + sessionId: event.data.sessionId, + model: event.data.selectedModel, + }); + break; + case "session.error": + void log("error", `Session error: ${event.data.message}`, { errorType: event.data.errorType }); + break; + case "tool.execution_start": + toolNames.set(event.data.toolCallId, event.data.toolName); + void log("info", `Tool call: ${event.data.toolName}`, { + toolCallId: event.data.toolCallId, + arguments: event.data.arguments as Record | undefined, + }); + break; + case "tool.execution_complete": { + const toolName = toolNames.get(event.data.toolCallId) ?? "unknown"; + toolNames.delete(event.data.toolCallId); + void log("info", `Tool result: ${toolName} (${event.data.success ? "success" : "failed"})`, { + toolCallId: event.data.toolCallId, + error: event.data.error?.message, + }); + break; + } + case "assistant.message_delta": + streamedResponse += event.data.deltaContent; + if (streamedResponse.length - lastLoggedCharCount >= DELTA_LOG_INTERVAL) { + lastLoggedCharCount = streamedResponse.length; + void log("info", `Generating taxonomy… (${streamedResponse.length} chars so far)`); + } + break; + case "assistant.message": + latestMessage = event.data.content; + break; + } + }); + + let attempts = 0; + const maxAttempts = 3; + let prompt = [ + `Generate taxonomy JSON for request ${requestId} and run ${runId}.`, + "Use get_run_data first, then inspect ATIF trajectories that materially affect the classification.", + "Return only raw JSON that satisfies the taxonomy schema.", + ].join(" "); + let lastJson: string | undefined; + + while (attempts < maxAttempts) { + attempts += 1; + streamedResponse = ""; + latestMessage = ""; + lastLoggedCharCount = 0; + + await log("info", `Sending taxonomy prompt to Copilot SDK (attempt ${attempts}/${maxAttempts})`, { + requestId, + runId, + timeoutMs, + }); + + const response = await session.sendAndWait({ prompt }, timeoutMs); + const responseText = response?.data.content?.trim() || latestMessage.trim() || streamedResponse.trim(); + + if (!responseText) { + throw new Error("Copilot SDK returned an empty taxonomy response"); + } + + const jsonText = extractJson(responseText); + lastJson = jsonText; + + let parsedJson: unknown; + try { + parsedJson = JSON.parse(jsonText) as unknown; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + if (attempts >= maxAttempts) { + throw new Error(`Taxonomy response was not valid JSON after ${maxAttempts} attempts: ${errorMessage}`); + } + + prompt = [ + "Your previous response was not valid JSON.", + `JSON parse error: ${errorMessage}.`, + "Return the full taxonomy document again as raw JSON only, with no markdown fences or commentary.", + "Replace the entire response with corrected JSON.", + ].join(" "); + continue; + } + + const validation = taxonomySchema.safeParse(parsedJson); + if (validation.success) { + return validation.data; + } + + const validationErrors = formatValidationErrors(validation.error.issues); + await log("warn", `Taxonomy validation failed on attempt ${attempts}`, { + issues: validationErrors, + }); + + if (attempts >= maxAttempts) { + throw new Error( + `Taxonomy schema validation failed after ${maxAttempts} attempts: ${validationErrors.join(" | ")}`, + ); + } + + prompt = [ + "Your previous taxonomy JSON did not satisfy the schema.", + "Correct the document and return the entire replacement JSON object.", + "Do not include markdown fences or commentary.", + `Validation errors: ${validationErrors.join("; ")}`, + lastJson ? `Previous JSON: ${lastJson}` : "", + ].filter(Boolean).join(" "); + } + + throw new Error("Taxonomy generation ended without a valid document"); + } finally { + await client.stop().catch(() => undefined); + } +} + +function extractJson(responseText: string): string { + const trimmed = responseText.trim(); + if (!trimmed.startsWith("```")) { + return trimmed; + } + + const fenceMatch = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i); + return fenceMatch?.[1]?.trim() ?? trimmed; +} + +function formatValidationErrors(issues: Array<{ path: PropertyKey[]; message: string }>): string[] { + return issues.map((issue) => { + const path = issue.path.length > 0 ? issue.path.map((segment) => String(segment)).join(".") : ""; + return `${path}: ${issue.message}`; + }); +} diff --git a/apps/workers/taxonomy/src/taxonomy-queue-processor.ts b/apps/workers/taxonomy/src/taxonomy-queue-processor.ts new file mode 100644 index 000000000..1d8160db0 --- /dev/null +++ b/apps/workers/taxonomy/src/taxonomy-queue-processor.ts @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { DequeuedMessageItem } from "@azure/storage-queue"; +import { + BaseQueueProcessor, + BlobStorage, + type BaseQueueProcessorConfig, + type LogEvent, + type RequestDocument, + TokenManagerClient, + type TaxonomyDocument, + type VisibilityHeartbeat, + withRetry, +} from "shared"; +import { generateTaxonomy } from "./taxonomy-generator.js"; + +const HANDLER_ID = "pp-taxonomy"; +const HANDLER_VERSION = 1; +const DEFAULT_SESSION_TIMEOUT_MS = 5 * 60 * 1000; + +interface TaxonomyQueueMessage { + type: "taxonomy"; + requestId: string; + runId?: string; +} + +export interface TaxonomyQueueProcessorConfig extends BaseQueueProcessorConfig { + taxonomyModel: string; + apiBaseUrl: string; + sessionTimeoutMs?: number; + schedulerUrl?: string; + tokenManagerUrl?: string; +} + +export class TaxonomyQueueProcessor extends BaseQueueProcessor { + private taxonomyConfig: TaxonomyQueueProcessorConfig; + private tokenClient: TokenManagerClient; + private blobStorage: BlobStorage; + + constructor(config: TaxonomyQueueProcessorConfig) { + super(config, "taxonomy-handler"); + this.taxonomyConfig = config; + this.tokenClient = new TokenManagerClient(config.tokenManagerUrl); + this.blobStorage = new BlobStorage({ + storageAccountName: config.storageAccountName, + storageConnectionString: config.storageConnectionString, + }); + } + + protected override async handleRequest( + doc: RequestDocument, + message: DequeuedMessageItem, + heartbeat: VisibilityHeartbeat, + log: (level: LogEvent["level"], msg: string, data?: Record) => Promise, + payload?: Record, + ): Promise { + const queueMessage = payload as TaxonomyQueueMessage | undefined; + const requestId = doc._id; + const activeRunId = doc.run?._id; + const runId = queueMessage?.runId ?? activeRunId ?? requestId; + + if (!queueMessage || queueMessage.type !== "taxonomy") { + await log("warn", "Discarding message with unsupported taxonomy payload", { payload }); + await this.safeDeleteMessage(message.messageId, heartbeat.stop()); + return; + } + + if (queueMessage.runId && activeRunId && queueMessage.runId !== activeRunId) { + await log("warn", "Discarding stale taxonomy message for superseded run", { + requestId, + queuedRunId: queueMessage.runId, + activeRunId, + }); + await this.safeDeleteMessage(message.messageId, heartbeat.stop()); + return; + } + + if (!doc.run) { + throw new Error(`Request ${requestId} has no active run to classify`); + } + + await withRetry(() => this.collection.updateOne( + { _id: requestId } as never, + { + $set: { + [`run.handlerStatus.${HANDLER_ID}.status`]: "processing", + [`run.handlerStatus.${HANDLER_ID}.version`]: HANDLER_VERSION, + [`run.handlerStatus.${HANDLER_ID}.updatedAt`]: new Date(), + [`run.handlerStatus.${HANDLER_ID}.error`]: null, + }, + } as never, + )); + + await log("info", "Starting taxonomy generation", { requestId, runId, model: this.taxonomyConfig.taxonomyModel }); + + try { + const taxonomy = await this.generateTaxonomy(requestId, runId, log); + const blobName = `${requestId}/runs/${runId}/taxonomy.json`; + const taxonomyUrl = await this.blobStorage.uploadJson(blobName, taxonomy); + + await withRetry(() => this.collection.updateOne( + { _id: requestId } as never, + { + $set: { + [`run.taxonomyUrl`]: taxonomyUrl, + [`run.handlerStatus.${HANDLER_ID}.status`]: "done", + [`run.handlerStatus.${HANDLER_ID}.version`]: HANDLER_VERSION, + [`run.handlerStatus.${HANDLER_ID}.updatedAt`]: new Date(), + }, + $unset: { + [`run.handlerStatus.${HANDLER_ID}.error`]: "", + }, + } as never, + )); + + await log("info", "Taxonomy generation completed", { + requestId, + runId, + blobName, + taxonomyUrl, + }); + + await this.notifyHandlerComplete(requestId, runId, "done"); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + await withRetry(() => this.collection.updateOne( + { _id: requestId } as never, + { + $set: { + [`run.handlerStatus.${HANDLER_ID}.status`]: "failed", + [`run.handlerStatus.${HANDLER_ID}.version`]: HANDLER_VERSION, + [`run.handlerStatus.${HANDLER_ID}.updatedAt`]: new Date(), + [`run.handlerStatus.${HANDLER_ID}.error`]: errorMessage, + }, + } as never, + )); + + await log("error", `Taxonomy generation failed: ${errorMessage}`, { requestId, runId }); + await this.notifyHandlerComplete(requestId, runId, "failed"); + throw error; + } + + const finalPopReceipt = heartbeat.stop(); + await this.safeDeleteMessage(message.messageId, finalPopReceipt); + } + + private async generateTaxonomy( + requestId: string, + runId: string, + log: (level: LogEvent["level"], msg: string, data?: Record) => Promise, + ): Promise { + const githubToken = await this.tokenClient.acquireToken("copilot-sdk"); + await log("info", "Acquired Copilot SDK token"); + + return generateTaxonomy({ + requestId, + runId, + apiBaseUrl: this.taxonomyConfig.apiBaseUrl, + taxonomyModel: this.taxonomyConfig.taxonomyModel, + githubToken, + sessionTimeoutMs: this.taxonomyConfig.sessionTimeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS, + log, + }); + } + + private async notifyHandlerComplete( + requestId: string, + runId: string, + status: "done" | "failed", + ): Promise { + const schedulerUrl = this.taxonomyConfig.schedulerUrl; + if (!schedulerUrl) return; + + try { + const response = await fetch(`${schedulerUrl}/notify/handler-complete`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ requestId, runId, handlerId: HANDLER_ID, status }), + }); + + if (!response.ok) { + console.warn(`[taxonomy-handler] Scheduler notify failed: ${response.status}`); + } + } catch (error) { + console.warn(`[taxonomy-handler] Scheduler notify error for ${requestId}/${runId}:`, error); + } + } +} diff --git a/apps/workers/taxonomy/src/tools.ts b/apps/workers/taxonomy/src/tools.ts new file mode 100644 index 000000000..ea4eced32 --- /dev/null +++ b/apps/workers/taxonomy/src/tools.ts @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { defineTool } from "@github/copilot-sdk"; +import { type ConversationTurn, type RequestDocument } from "shared"; + +interface GetAtifTrajectoryArgs { + iteration?: number; +} + +function selectTurnWithAtif(turns: ConversationTurn[] | undefined, requestedIteration?: number): ConversationTurn | undefined { + if (!turns?.length) return undefined; + + if (requestedIteration !== undefined) { + return turns.find((turn) => turn.iteration === requestedIteration && typeof turn.atifUrl === "string"); + } + + return [...turns] + .filter((turn) => typeof turn.atifUrl === "string") + .sort((a, b) => b.iteration - a.iteration)[0]; +} + +export function createTaxonomyTools( + apiBaseUrl: string, + requestId: string, +) { + const getRunData = defineTool("get_run_data", { + description: + "Fetch the full request document plus the active run state, including scenario, persona, turns, criteria results, and handler status.", + parameters: { + type: "object", + properties: {}, + }, + handler: async () => { + try { + const response = await fetch(`${apiBaseUrl}/api/v1/requests/${requestId}`); + if (!response.ok) { + return { error: `Failed to fetch request: ${response.status} ${response.statusText}` }; + } + + const request: RequestDocument = await response.json(); + const run = request.run; + + return { + request: { + id: request._id, + workerType: request.workerType, + model: request.model, + reasoningEffort: request.reasoningEffort, + maxIterations: request.maxIterations, + createdAt: request.createdAt, + updatedAt: request.updatedAt, + persona: request.persona, + personaInstructions: request.personaInstructions, + mcpServers: request.mcpServers, + skillRevisions: request.skillRevisions, + extensions: request.extensions, + priority: request.priority, + scenario: request.scenario, + }, + run: run ? { + id: run._id, + attemptNumber: run.attemptNumber, + status: run.status, + outcome: run.outcome, + result: run.result, + error: run.error, + startedAt: run.startedAt, + finishedAt: run.finishedAt, + updatedAt: run.updatedAt, + aiCallCount: run.aiCallCount, + tokenUsage: run.tokenUsage, + handlerStatus: run.handlerStatus, + turns: run.turns?.map((turn) => ({ + iteration: turn.iteration, + passed: turn.passed, + timestamp: turn.timestamp, + startedAt: turn.startedAt, + durationMs: turn.durationMs, + judgeFeedback: turn.judgeFeedback, + codingAgentResponse: turn.codingAgentResponse, + aiCallCount: turn.aiCallCount, + tokenUsage: turn.tokenUsage, + snapshotUrl: turn.snapshotUrl, + rawChatUrl: turn.rawChatUrl, + rawChatFormat: turn.rawChatFormat, + toolCallsUrl: turn.toolCallsUrl, + toolCallCount: turn.toolCallCount, + atifUrl: turn.atifUrl, + criteriaResults: turn.criteriaResults, + })) ?? [], + } : null, + }; + } catch (err) { + return { error: `Failed to fetch run data: ${err}` }; + } + }, + }); + + const getAtifTrajectory = defineTool("get_atif_trajectory", { + description: + "Fetch an ATIF trajectory JSON document for a specific iteration, or the latest available iteration when omitted.", + parameters: { + type: "object", + properties: { + iteration: { + type: "number", + description: "Optional 1-based iteration number. If omitted, the latest available ATIF trajectory is returned.", + }, + }, + }, + handler: async (args: GetAtifTrajectoryArgs) => { + try { + const response = await fetch(`${apiBaseUrl}/api/v1/requests/${requestId}`); + if (!response.ok) { + return { error: `Failed to fetch request: ${response.status} ${response.statusText}` }; + } + + const request: RequestDocument = await response.json(); + const selectedTurn = selectTurnWithAtif(request.run?.turns, args.iteration); + + if (!selectedTurn?.atifUrl) { + return { + error: args.iteration !== undefined + ? `No ATIF trajectory available for iteration ${args.iteration}` + : "No ATIF trajectory is available for this run", + }; + } + + const runId = request.run?._id; + const iteration = selectedTurn.iteration; + const atifUrl = runId + ? `${apiBaseUrl}/api/v1/requests/${requestId}/runs/${runId}/atif?iteration=${iteration}` + : `${apiBaseUrl}/api/v1/requests/${requestId}/atif?iteration=${iteration}`; + + const atifResponse = await fetch(atifUrl); + if (!atifResponse.ok) { + return { error: `Failed to fetch ATIF trajectory: ${atifResponse.status} ${atifResponse.statusText}` }; + } + + const trajectory: unknown = await atifResponse.json(); + + return { + iteration, + trajectory, + }; + } catch (err) { + return { error: `Failed to fetch ATIF trajectory: ${err}` }; + } + }, + }); + + return [getRunData, getAtifTrajectory]; +} diff --git a/apps/workers/taxonomy/tsconfig.json b/apps/workers/taxonomy/tsconfig.json new file mode 100644 index 000000000..bf012ecbb --- /dev/null +++ b/apps/workers/taxonomy/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 411c8fc2e..f5e309b30 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -217,6 +217,22 @@ services: - action: rebuild path: ./pnpm-lock.yaml + taxonomy-handler: + build: + target: dev + develop: + watch: + - action: sync + path: ./apps/workers/taxonomy/src + target: /app/apps/workers/taxonomy/src + - action: sync + path: ./packages/shared/src + target: /app/packages/shared/src + - action: rebuild + path: ./apps/workers/taxonomy/package.json + - action: rebuild + path: ./pnpm-lock.yaml + register-handler-post-processor: build: target: dev diff --git a/docker-compose.yml b/docker-compose.yml index 221dd5c03..46df7cd4f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ # docker compose --profile vscode-web up # + VS Code Web worker # docker compose --profile claude-code --profile copilot up # multiple workers # docker compose --profile report up # + Report Generator worker +# docker compose --profile taxonomy up # + Taxonomy handler worker # docker compose up mongodb redis azurite # infra only (run app services natively) # # Host-native debugging: @@ -869,6 +870,51 @@ services: api: condition: service_started + taxonomy-handler: + build: + context: . + dockerfile: apps/workers/taxonomy/Dockerfile + profiles: [taxonomy, all] + environment: + <<: *common-env + AZURE_STORAGE_QUEUE_TAXONOMY: pp-taxonomy-queue + TAXONOMY_MODEL: ${TAXONOMY_MODEL:-gpt-5.4} + SCOPE_MT_API_URL: http://api:80 + SCHEDULER_URL: http://scheduler:8080 + GITHUB_TOKEN: ${GITHUB_TOKEN:-} + TOKEN_MANAGER_URL: http://token-manager:80 + GIT_COMMIT: ${GIT_COMMIT:-development} + SESSION_TIMEOUT_MS: ${SESSION_TIMEOUT_MS:-300000} + depends_on: + mongodb: + condition: service_healthy + required: false + redis: + condition: service_healthy + azurite: + condition: service_healthy + api: + condition: service_started + scheduler: + condition: service_started + register-handler-taxonomy: + condition: service_completed_successfully + + register-handler-taxonomy: + build: + context: . + dockerfile: apps/workers/taxonomy/Dockerfile + profiles: [taxonomy, all] + environment: + <<: *common-env + HANDLER_YAML_PATH: apps/workers/taxonomy/handler.yaml + SCHEDULER_URL: http://scheduler:8080 + depends_on: + scheduler: + condition: service_started + restart: "no" + entrypoint: ["npx", "tsx", "packages/shared/src/scripts/register-handler.ts"] + register-handler-post-processor: build: context: . diff --git a/docs/architecture/db-migrations.md b/docs/architecture/db-migrations.md index d01bcc5aa..ce8f2e673 100644 --- a/docs/architecture/db-migrations.md +++ b/docs/architecture/db-migrations.md @@ -116,7 +116,7 @@ When you run a command: | `029-create-users-collection` | Creates the database-enforced unique identity index for authentication; uses Cosmos collection-creation extensions for continuous-backup accounts and preserves native MongoDB index creation (see below) | | `030-create-resource-indexes` | Creates project-scoped resource indexes | | `031-migrate-services-to-handler-dag` | Replaces the legacy single `post-processor` service row with per-handler service registrations and dependencies | -| `032-add-handler-status-index` | Adds a handler-specific dispatch index for `pp-atif` polling | +| `032-add-handler-status-index` | Adds handler-specific dispatch indexes for `pp-atif`, `pp-taxonomy`, and `pp-report` polling | | `033-backfill-handler-status` | Backfills `run.handlerStatus.pp-atif` from deprecated `run.postProcessorStatus` / `run.postProcessorVersion` fields | ### Migration 029: users identity uniqueness diff --git a/docs/architecture/post-processing.md b/docs/architecture/post-processing.md index 4518ef391..81a59285b 100644 --- a/docs/architecture/post-processing.md +++ b/docs/architecture/post-processing.md @@ -34,7 +34,23 @@ Two paths trigger post-processing: - Updates `run.turns.$.atifUrl` in the request document 4. On success, stamps `run.handlerStatus["pp-atif"] = { status: "done", version: N }` (while legacy `run.postProcessorVersion` / `run.postProcessorStatus` may still be mirrored during the transition) -5. Triggers any downstream handlers once the scheduler sees the dependency graph is satisfied. (Reports are **not** a DAG handler — see [Report Triggering](#report-triggering-on-dag-drain).) +5. Triggers the downstream taxonomy handler once the scheduler sees the dependency graph is satisfied. (Reports are **not** a DAG handler — see [Report Triggering](#report-triggering-on-dag-drain).) + +## Taxonomy Generation + +The taxonomy handler (`apps/workers/taxonomy/`) consumes messages from the dedicated `pp-taxonomy-queue` and produces a structured JSON artifact for a completed run. It uses the GitHub Copilot SDK with a tool-backed session to fetch run metadata and ATIF trajectories from the API, then validates the generated JSON against the shared taxonomy Zod schema. + +### Flow + +1. Queue message arrives: `{ type: "taxonomy", requestId, runId }` +2. Worker fetches the request document from MongoDB and marks `run.handlerStatus["pp-taxonomy"].status = "processing"` +3. Copilot SDK session calls: + - `get_run_data` to fetch the request + active run from the API + - `get_atif_trajectory` to fetch per-iteration ATIF JSON from the API (`GET /api/v1/requests/:id/runs/:runId/atif?iteration=N`) +4. The worker validates the assistant response with `taxonomySchema.safeParse()` +5. If validation fails, the worker feeds the schema errors back into the same session and retries up to 3 total attempts +6. On success, the worker uploads `taxonomy.json` to `{requestId}/runs/{runId}/taxonomy.json` +7. The worker stamps `run.handlerStatus["pp-taxonomy"] = { status: "done", version: 1 }` and calls `POST /notify/handler-complete` ### Blob Storage Layout @@ -45,16 +61,52 @@ Two paths trigger post-processing: atif.trajectory.json iteration-2/ atif.trajectory.json + taxonomy.json ``` +The completed run stores the taxonomy blob URL on `run.taxonomyUrl`. + ### API Endpoints | Endpoint | Description | | --- | --- | | `GET /api/v1/requests/:id/atif?iteration=N` | Download ATIF for the latest run | | `GET /api/v1/requests/:id/runs/:runId/atif?iteration=N` | Download ATIF for a specific run | +| `GET /api/v1/requests/:id/taxonomy` | Download taxonomy JSON for the latest run | +| `GET /api/v1/requests/:id/runs/:runId/taxonomy` | Download taxonomy JSON for a specific run | + +The `iteration` query parameter is mandatory only for the ATIF endpoints. ATIF files are also included in archive exports as `iteration-{N}.atif.trajectory.json`. + +### Local troubleshooting: taxonomy CLI + +The Copilot SDK generation loop lives in `apps/workers/taxonomy/src/taxonomy-generator.ts` (`generateTaxonomy()`), shared by both the queue processor and a standalone developer CLI (`src/cli.ts`). The CLI invokes generation directly against an existing `requestId`, streams the session events live, and saves the validated taxonomy to a local file — **it never writes to MongoDB, blob storage, or the scheduler** (read-only). It is a developer tool and is intentionally not part of the Scope CLI. + +Because both taxonomy tools (`get_run_data` and `get_atif_trajectory`) read through the API, the CLI needs only an API URL and a Copilot token — no storage connection string. This lets it run against any environment (local or a remote deployment such as integration) given a reachable API. + +```bash +# Local stack (use your worktree's host-mapped API port): +SCOPE_MT_API_URL=http://localhost:3001 \ +GITHUB_TOKEN=$(gh auth token) \ +pnpm taxonomy [--run ] [--model ] [--timeout ] [-o out.json] [--stdout] [--quiet] + +# Integration (read-only against the deployed API): +SCOPE_MT_API_URL=https://msscope-int.azurewebsites.net \ +GITHUB_TOKEN=$(gh auth token) \ +pnpm taxonomy -o taxonomy-int.json +``` + +The root `pnpm taxonomy` script proxies to `pnpm --filter taxonomy-handler taxonomy` (i.e. `tsx src/cli.ts`). + +| Option | Description | +| --- | --- | +| `--run, -r ` | Run id used in the prompt/logging (default: active run resolved from the API) | +| `--model ` | Override `TAXONOMY_MODEL` (e.g. A/B a reasoning model vs the `gpt-5.4` default) | +| `--timeout ` | Override `SESSION_TIMEOUT_MS` (per-attempt session timeout) | +| `--output, -o ` | Output file path (default: `taxonomy-.json` in the cwd) | +| `--stdout` | Also print the taxonomy JSON to stdout | +| `--quiet` | Suppress live session-event streaming on stderr | -The `iteration` query parameter is mandatory for the ATIF endpoints. ATIF files are also included in archive exports as `iteration-{N}.atif.trajectory.json`. +Live progress (tool calls, errors, deltas) is written to stderr so the saved JSON file stays clean. The CLI exits non-zero on failure (empty response, JSON/schema validation failure after 3 attempts, timeout, or model rate limit) and surfaces the underlying error message — making it well suited to diagnosing the oversized-`get_atif_trajectory` paging loop and Copilot model rate limits. ## Taxonomy Observations (`pp-observations`) @@ -165,7 +217,7 @@ The `AtifHandler` has two complementary test suites: The post-processor uses a version-based re-processing scheme: -- Each handler is registered independently in the `services` collection using a `post-process-handler` document keyed by handler ID (for example `pp-atif`) +- Each handler is registered independently in the `services` collection using a `post-process-handler` document keyed by handler ID (for example `pp-atif`, `pp-taxonomy`) - Each registration stores the handler `version`, queue name, selector, `autoBackfill` behavior, and `dependsOn` DAG edges - The scheduler compares each request's per-handler status/version state (for example `run.handlerStatus["pp-atif"]`) against the registered handler entry for that selector - Requests with missing or outdated handler state are re-dispatched for that specific handler once its dependencies are satisfied @@ -174,17 +226,18 @@ This allows deploying handler improvements and having them automatically applied ## Handler Registration -Each post-process handler declares its DAG metadata in a `handler.yaml` file colocated with the worker (e.g. `apps/workers/post-processor/handler.yaml`). This mirrors the coding-agent `agent.yaml` registration pattern. +Each post-process handler declares its DAG metadata in a `handler.yaml` file colocated with the worker (e.g. `apps/workers/taxonomy/handler.yaml`). This mirrors the coding-agent `agent.yaml` registration pattern. ```yaml -# apps/workers/post-processor/handler.yaml -_id: pp-atif +# apps/workers/taxonomy/handler.yaml +_id: pp-taxonomy type: post-process-handler version: 1 -queue: post-processor-queue -selector: atif -autoBackfill: true -dependsOn: [] +queue: pp-taxonomy-queue +selector: taxonomy +autoBackfill: false +dependsOn: + - pp-atif ``` On deploy, a generic registration script — `packages/shared/src/scripts/register-handler.ts` — reads the `handler.yaml` pointed to by `HANDLER_YAML_PATH`, waits for the scheduler's `/health`, and POSTs the parsed document to `${SCHEDULER_URL}/handlers/register` (with retry). The scheduler validates the document, runs a cycle check against the existing topology, and upserts it into the `services` collection via `HandlerDispatcher.registerHandler`. **Workers never write to the `services` collection directly** — the scheduler is the sole owner. @@ -250,7 +303,7 @@ active template's trigger and creates the properly-templated `ReportDocument`(s) ```mermaid flowchart LR - RT["run done"] --> DAG["scheduler dispatches DAG
(pp-atif → …)"] + RT["run done"] --> DAG["scheduler dispatches DAG
(pp-atif → pp-taxonomy)"] DAG -->|"all handlers terminal"| DR{DAG drained?} DR -->|yes| TRIG["POST /api/v1/reports/trigger
{ requestId }"] TRIG --> API["API evaluates each template trigger
→ ReportDocument + report-queue {reportId}"] @@ -263,7 +316,7 @@ flowchart LR registered handler is terminal. A run with **zero** registered handlers is vacuously drained, so reports trigger immediately on run completion. -**Trigger regardless of handler success.** A flaky handler must not block report +**Trigger regardless of handler success.** A flaky taxonomy must not block report generation (reports analyze the run/ATIF), so the scheduler triggers reports whenever the DAG drains — not only when every handler succeeded. @@ -307,18 +360,20 @@ stateDiagram-v2 | Environment Variable | Default | Description | | --- | --- | --- | | `AZURE_STORAGE_QUEUE_POSTPROCESSOR` | `post-processor-queue` | Queue name for ATIF post-processor messages | +| `AZURE_STORAGE_QUEUE_TAXONOMY` | `pp-taxonomy-queue` | Queue name for taxonomy-generation jobs | | `SCHEDULER_PP_POLL_INTERVAL_MS` | `30000` | Scheduler backfill polling interval | | `BATCH_SIZE` | `1` | Messages to process per poll (worker-side) | | `POLL_INTERVAL_MS` | `5000` | Worker queue polling interval | -| `SCOPE_MT_API_URL` / `API_BASE_URL` | `http://localhost:3001` | API URL used by report Copilot tools | -| `SESSION_TIMEOUT_MS` | `300000` | Timeout for report Copilot SDK sessions | +| `SCOPE_MT_API_URL` / `API_BASE_URL` | `http://localhost:3001` | API URL used by report/taxonomy Copilot tools | +| `TAXONOMY_MODEL` | `gpt-5.4` | Copilot SDK model used by the taxonomy handler | +| `SESSION_TIMEOUT_MS` | `300000` | Timeout for report/taxonomy Copilot SDK sessions | | `SCHEDULER_URL` | _(unset)_ | Scheduler base URL — used by handlers to notify completion and by the register-handler script to reach `/handlers/register` | | `API_URL` | `http://api:80` | API base URL the **scheduler** uses to call `POST /api/v1/reports/trigger` when a run's handler DAG drains | | `HANDLER_YAML_PATH` | _(unset)_ | Path to a handler's `handler.yaml`, read by the generic `register-handler` script | ## Infrastructure -- **Queues:** Azure Storage Queues (`post-processor-queue` for ATIF, `report-queue` for reports) +- **Queues:** Azure Storage Queues (`post-processor-queue` for ATIF, `pp-taxonomy-queue` for taxonomy, `report-queue` for reports) - **Blob Storage:** `snapshots` container (shared with HAR, tool-calls, and other artifacts) - **Database:** MongoDB `requests` collection (`run.handlerStatus` map keyed by handler ID, with deprecated `run.postProcessorStatus` / `run.postProcessorVersion` compatibility fields during migration) - **KEDA:** ScaledObject scales the worker to zero when queue is empty diff --git a/package.json b/package.json index 635bb66ae..a98a960c8 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dev:coder-acp-claude-code": "pnpm --filter coder-acp-claude-code dev", "dev:coder-acp-copilot": "pnpm --filter coder-acp-copilot dev", "dev:report-generator": "pnpm --filter report-generator dev", + "taxonomy": "pnpm --filter taxonomy-handler taxonomy", "docker:up": "scripts/worktree-env.sh && scripts/dev-compose.sh up --build", "docker:up:claude-code": "scripts/worktree-env.sh && SCOPE_REGISTER_CLAUDE_CODE_AGENT=true scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-claude-code/versions.env --profile claude-code --profile report up --build", "docker:up:copilot": "scripts/worktree-env.sh && SCOPE_REGISTER_COPILOT_AGENT=true scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-copilot/versions.env --profile copilot --profile report up --build", @@ -26,9 +27,9 @@ "docker:up:portal": "scripts/worktree-env.sh && scripts/dev-compose.sh --profile auth --profile portal up --build", "docker:dev": "scripts/worktree-env.sh && COMPOSE_EXPERIMENTAL_WATCH_POLL=1 scripts/dev-compose.sh -f docker-compose.yml -f docker-compose.dev.yml up --build --watch", "docker:dev:claude-code": "scripts/worktree-env.sh && SCOPE_REGISTER_CLAUDE_CODE_AGENT=true GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) COMPOSE_EXPERIMENTAL_WATCH_POLL=1 scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-claude-code/versions.env -f docker-compose.yml -f docker-compose.dev.yml --profile claude-code --profile report --profile post-processor --profile auth --profile portal up --build --watch", - "docker:dev:copilot": "scripts/worktree-env.sh && SCOPE_REGISTER_COPILOT_AGENT=true GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) COMPOSE_EXPERIMENTAL_WATCH_POLL=1 scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-copilot/versions.env -f docker-compose.yml -f docker-compose.dev.yml --profile copilot --profile report --profile post-processor --profile auth --profile portal up --build --watch", + "docker:dev:copilot": "scripts/worktree-env.sh && SCOPE_REGISTER_COPILOT_AGENT=true GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) COMPOSE_EXPERIMENTAL_WATCH_POLL=1 scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-copilot/versions.env -f docker-compose.yml -f docker-compose.dev.yml --profile copilot --profile report --profile post-processor --profile taxonomy --profile auth --profile portal up --build --watch", "docker:dev:portal": "scripts/worktree-env.sh && GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) COMPOSE_EXPERIMENTAL_WATCH_POLL=1 scripts/dev-compose.sh -f docker-compose.yml -f docker-compose.dev.yml --profile auth --profile portal up --build --watch", - "docker:dev:all": "scripts/worktree-env.sh && SCOPE_REGISTER_COPILOT_AGENT=true SCOPE_REGISTER_CLAUDE_CODE_AGENT=true GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) COMPOSE_EXPERIMENTAL_WATCH_POLL=1 scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-copilot/versions.env --env-file apps/workers/coder-acp-claude-code/versions.env -f docker-compose.yml -f docker-compose.dev.yml --profile claude-code --profile copilot --profile report --profile post-processor --profile auth --profile portal up --build --watch", + "docker:dev:all": "scripts/worktree-env.sh && SCOPE_REGISTER_COPILOT_AGENT=true SCOPE_REGISTER_CLAUDE_CODE_AGENT=true GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) COMPOSE_EXPERIMENTAL_WATCH_POLL=1 scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-copilot/versions.env --env-file apps/workers/coder-acp-claude-code/versions.env -f docker-compose.yml -f docker-compose.dev.yml --profile claude-code --profile copilot --profile report --profile post-processor --profile taxonomy --profile auth --profile portal up --build --watch", "docker:dev:build": "scripts/worktree-env.sh && GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-copilot/versions.env --env-file apps/workers/coder-acp-claude-code/versions.env -f docker-compose.yml -f docker-compose.dev.yml --profile claude-code --profile copilot --profile report --profile auth --profile portal build", "docker:down": "scripts/worktree-env.sh && scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-copilot/versions.env --env-file apps/workers/coder-acp-claude-code/versions.env --profile claude-code --profile copilot --profile report --profile portal down --volumes", "docker:build": "scripts/worktree-env.sh && scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-copilot/versions.env --env-file apps/workers/coder-acp-claude-code/versions.env --profile claude-code --profile copilot --profile report build", diff --git a/packages/db-migrations/src/migrations/031-migrate-services-to-handler-dag.ts b/packages/db-migrations/src/migrations/031-migrate-services-to-handler-dag.ts index 570b12e40..87fba84c8 100644 --- a/packages/db-migrations/src/migrations/031-migrate-services-to-handler-dag.ts +++ b/packages/db-migrations/src/migrations/031-migrate-services-to-handler-dag.ts @@ -7,7 +7,7 @@ import type { MigrationInterface } from "mongo-migrate-ts"; /** * Migration 031: Remove the legacy "post-processor" service entry. * - * Handler service registration (e.g. pp-atif) is now owned + * Handler service registration (pp-atif, pp-taxonomy) is now owned * by each worker's handler.yaml + the generic register-handler script, * which POSTs to the scheduler's POST /handlers/register endpoint on deploy. * Reports are not a DAG handler — the scheduler triggers report generation diff --git a/packages/db-migrations/src/migrations/032-add-handler-status-index.ts b/packages/db-migrations/src/migrations/032-add-handler-status-index.ts index 44d7a448a..cf69fb7c0 100644 --- a/packages/db-migrations/src/migrations/032-add-handler-status-index.ts +++ b/packages/db-migrations/src/migrations/032-add-handler-status-index.ts @@ -13,6 +13,14 @@ const HANDLER_STATUS_INDEXES = [ updatedAt: -1 as const, }, }, + { + name: "idx_handler_dispatch_taxonomy", + key: { + "run.handlerStatus.pp-taxonomy.status": 1 as const, + "run.status": 1 as const, + updatedAt: -1 as const, + }, + }, ] as const; export class AddHandlerStatusIndex implements MigrationInterface { diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index fae65d02f..725dcb079 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -29,5 +29,6 @@ export * from "./har/index.js"; export * from "./utils/index.js"; export * from "./schemas/index.js"; export * from "./auth/index.js"; +export * from "./schemas/taxonomy.js"; export * from "./cursor.js"; export * from "./run-duration.js"; diff --git a/packages/shared/src/schemas/index.ts b/packages/shared/src/schemas/index.ts index 52a7bed3f..6ed9119dc 100644 --- a/packages/shared/src/schemas/index.ts +++ b/packages/shared/src/schemas/index.ts @@ -23,3 +23,4 @@ export * from "./feature-flag.js"; export * from "./profile.js"; export * from "./project.js"; export * from "./submission.js"; +export * from "./taxonomy.js"; diff --git a/packages/shared/src/schemas/request.ts b/packages/shared/src/schemas/request.ts index 5aefb2466..33fe18f89 100644 --- a/packages/shared/src/schemas/request.ts +++ b/packages/shared/src/schemas/request.ts @@ -240,6 +240,7 @@ export const RunStateSchema = z podName: z.string().optional(), }).optional(), harUrl: z.string().optional(), + taxonomyUrl: z.string().optional(), videoUrls: z.array(z.string()).optional(), setupVideoUrls: z.array(z.string()).optional(), tokenUsage: TokenUsageSchema.optional(), diff --git a/packages/shared/src/schemas/taxonomy.test.ts b/packages/shared/src/schemas/taxonomy.test.ts new file mode 100644 index 000000000..320f89ceb --- /dev/null +++ b/packages/shared/src/schemas/taxonomy.test.ts @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, expect, it } from "vitest"; +import { taxonomySchema } from "./taxonomy.js"; + +describe("taxonomySchema", () => { + const validFixture = { + meta: { + generatedAt: "2025-02-01T12:00:00Z", + schemaVersion: "1.0.0", + runId: "run-123", + requestId: "req-456", + }, + scorecard: { + summary: "The agent usually succeeds but struggles with recovery quality.", + profileResults: [ + { + profileId: "profile-best", + select: "best", + taskOutcome: "success", + overallScore: 92, + dimensions: [ + { + name: "Correctness", + score: 95, + rationale: "Implemented the requested behavior fully.", + evidence: ["All acceptance criteria passed", "No regressions observed"], + }, + ], + }, + ], + crossProfileInsights: [ + "The agent communicates clearly across profiles.", + "Stress conditions reduce recovery quality.", + ], + }, + behaviorAnalysis: { + patterns: [ + { + id: "pattern-1", + category: "strength", + title: "Consistent planning", + description: "The agent outlines a clear plan before making changes.", + frequency: 0.75, + impact: "high", + evidence: [ + { + runId: "run-123", + iteration: 1, + excerpt: "Plan: inspect the failing endpoint, then patch the handler.", + }, + ], + }, + ], + agentPersonality: { + communicationStyle: "Concise and structured", + problemSolvingApproach: "Hypothesis-driven debugging", + errorRecoveryBehavior: "Retries with targeted fixes after inspecting evidence", + }, + }, + actionList: { + fix: [ + { + id: "action-1", + title: "Stabilize recovery prompts", + priority: "critical", + description: "Reduce failures when the first attempt is rejected.", + ownership: "prompt-engineer", + relatedPatterns: ["pattern-1"], + suggestedApproach: "Add explicit fallback guidance for failed first passes.", + }, + ], + improve: [ + { + id: "action-2", + title: "Expand planning examples", + priority: "medium", + description: "Reinforce successful planning behavior.", + ownership: "agent-team", + relatedPatterns: ["pattern-1"], + suggestedApproach: "Fine-tune on traces with strong upfront decomposition.", + }, + ], + investigate: [ + { + id: "action-3", + title: "Review profile variance", + priority: "low", + description: "Understand why profile-specific regressions appear sporadically.", + ownership: "scenario-author", + relatedPatterns: ["pattern-1"], + suggestedApproach: "Compare scenario wording against affected profile runs.", + }, + ], + }, + }; + + it("accepts a valid taxonomy fixture", () => { + const result = taxonomySchema.safeParse(validFixture); + expect(result.success).toBe(true); + }); + + it("rejects fixtures missing required fields with precise paths", () => { + const invalidFixture = { + ...validFixture, + scorecard: { + ...validFixture.scorecard, + profileResults: [ + { + ...validFixture.scorecard.profileResults[0], + dimensions: [ + { + name: "Correctness", + score: 95, + evidence: ["All acceptance criteria passed"], + }, + ], + }, + ], + }, + behaviorAnalysis: { + ...validFixture.behaviorAnalysis, + agentPersonality: { + communicationStyle: "Concise and structured", + errorRecoveryBehavior: "Retries with targeted fixes after inspecting evidence", + }, + }, + }; + + const result = taxonomySchema.safeParse(invalidFixture); + expect(result.success).toBe(false); + if (result.success) { + throw new Error("Expected invalid taxonomy fixture to fail parsing"); + } + + const issuePaths = result.error.issues.map((issue) => issue.path); + expect(issuePaths).toContainEqual([ + "scorecard", + "profileResults", + 0, + "dimensions", + 0, + "rationale", + ]); + expect(issuePaths).toContainEqual([ + "behaviorAnalysis", + "agentPersonality", + "problemSolvingApproach", + ]); + }); + + it("enforces enum values", () => { + const invalidFixture = { + ...validFixture, + actionList: { + ...validFixture.actionList, + fix: [ + { + ...validFixture.actionList.fix[0], + priority: "urgent", + }, + ], + }, + }; + + const result = taxonomySchema.safeParse(invalidFixture); + expect(result.success).toBe(false); + if (result.success) { + throw new Error("Expected invalid enum fixture to fail parsing"); + } + + expect(result.error.issues.map((issue) => issue.path)).toContainEqual([ + "actionList", + "fix", + 0, + "priority", + ]); + }); +}); diff --git a/packages/shared/src/schemas/taxonomy.ts b/packages/shared/src/schemas/taxonomy.ts new file mode 100644 index 000000000..a837d4f91 --- /dev/null +++ b/packages/shared/src/schemas/taxonomy.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { z } from "zod"; + +const scoreSchema = z.number().min(0).max(100); +const frequencySchema = z.number().min(0).max(1); + +/** Taxonomy document metadata. */ +export const taxonomyMetaSchema = z.object({ + generatedAt: z.string().datetime({ offset: true }), + schemaVersion: z.string().min(1), + runId: z.string().min(1), + requestId: z.string().min(1), +}); + +/** Scorecard dimension result for a single profile. */ +export const scorecardDimensionSchema = z.object({ + name: z.string().min(1), + score: scoreSchema, + rationale: z.string().min(1), + evidence: z.array(z.string().min(1)), +}); + +/** Scorecard result for a specific profile slice. */ +export const profileResultSchema = z.object({ + profileId: z.string().min(1), + select: z.enum(["best", "median", "worst"]), + taskOutcome: z.enum(["success", "partial", "failure"]), + overallScore: scoreSchema, + dimensions: z.array(scorecardDimensionSchema), +}); + +/** Top-level scorecard section summarizing profile results. */ +export const scorecardSchema = z.object({ + summary: z.string().min(1), + profileResults: z.array(profileResultSchema), + crossProfileInsights: z.array(z.string().min(1)), +}); + +/** Evidence excerpt supporting an observed behavior pattern. */ +export const behaviorEvidenceSchema = z.object({ + runId: z.string().min(1), + iteration: z.number().int().min(0), + excerpt: z.string().min(1), +}); + +/** Cross-run behavioral pattern identified by the taxonomy analysis. */ +export const behaviorPatternSchema = z.object({ + id: z.string().min(1), + category: z.enum(["strength", "weakness", "inconsistency", "regression"]), + title: z.string().min(1), + description: z.string().min(1), + frequency: frequencySchema, + impact: z.enum(["critical", "high", "medium", "low"]), + evidence: z.array(behaviorEvidenceSchema), +}); + +/** Qualitative description of the agent's personality traits. */ +export const agentPersonalitySchema = z.object({ + communicationStyle: z.string().min(1), + problemSolvingApproach: z.string().min(1), + errorRecoveryBehavior: z.string().min(1), +}); + +/** Behavior analysis section describing patterns and personality. */ +export const behaviorAnalysisSchema = z.object({ + patterns: z.array(behaviorPatternSchema), + agentPersonality: agentPersonalitySchema, +}); + +/** Action item emitted by the taxonomy analysis. */ +export const actionItemSchema = z.object({ + id: z.string().min(1), + title: z.string().min(1), + priority: z.enum(["critical", "high", "medium", "low"]), + description: z.string().min(1), + ownership: z.enum([ + "agent-team", + "platform-team", + "prompt-engineer", + "scenario-author", + ]), + relatedPatterns: z.array(z.string().min(1)), + suggestedApproach: z.string().min(1), +}); + +/** Prioritized action lists grouped by remediation intent. */ +export const actionListSchema = z.object({ + fix: z.array(actionItemSchema), + improve: z.array(actionItemSchema), + investigate: z.array(actionItemSchema), +}); + +/** Complete taxonomy document produced by the taxonomy post-processor. */ +export const taxonomySchema = z.object({ + meta: taxonomyMetaSchema, + scorecard: scorecardSchema, + behaviorAnalysis: behaviorAnalysisSchema, + actionList: actionListSchema, +}); + +/** Inferred TypeScript type for the taxonomy document. */ +export type TaxonomyDocument = z.infer; diff --git a/packages/shared/src/scripts/register-handler.test.ts b/packages/shared/src/scripts/register-handler.test.ts index 162916404..0c3858b58 100644 --- a/packages/shared/src/scripts/register-handler.test.ts +++ b/packages/shared/src/scripts/register-handler.test.ts @@ -6,11 +6,11 @@ import { parseHandlerDocument } from "./register-handler.js"; describe("parseHandlerDocument", () => { const valid = { - _id: "pp-example", + _id: "pp-taxonomy", type: "post-process-handler", version: 1, - queue: "pp-example-queue", - selector: "example", + queue: "pp-taxonomy-queue", + selector: "taxonomy", autoBackfill: false, dependsOn: ["pp-atif"], }; diff --git a/packages/shared/src/types/types.ts b/packages/shared/src/types/types.ts index 5f8315307..dcd131259 100644 --- a/packages/shared/src/types/types.ts +++ b/packages/shared/src/types/types.ts @@ -534,13 +534,14 @@ export interface RunState { podName?: string; }; harUrl?: string; + taxonomyUrl?: string; videoUrls?: string[]; setupVideoUrls?: string[]; tokenUsage?: TokenUsage; aiCallCount?: number; rawChatUrl?: string; rawChatFormat?: string; - /** Per-handler post-processing status. Keys are handler IDs (e.g. "pp-atif"). */ + /** Per-handler post-processing status. Keys are handler IDs (e.g. "pp-atif", "pp-taxonomy"). */ handlerStatus?: Record; /** Whole-run observation results — one entry per selected `subject:"run"` * observation criterion, evaluated once against the final snapshot + the @@ -709,6 +710,7 @@ export const RUN_STATE_FIELD_NAMES = [ "lastHeartbeatAt", "worker", "harUrl", + "taxonomyUrl", "videoUrls", "setupVideoUrls", "tokenUsage", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4a29c36a..44a7b86bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -787,6 +787,31 @@ importers: specifier: ^5.6.2 version: 5.9.3 + apps/workers/taxonomy: + dependencies: + '@azure/storage-queue': + specifier: ^12.25.0 + version: 12.29.0 + '@github/copilot-sdk': + specifier: ^0.1.17 + version: 0.1.32(typescript@5.9.3) + dotenv: + specifier: 16.6.1 + version: 16.6.1 + shared: + specifier: workspace:* + version: link:../../../packages/shared + devDependencies: + '@types/node': + specifier: ^22.7.0 + version: 22.19.10 + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: ^5.6.2 + version: 5.9.3 + evaluations/static-prompts: dependencies: shared: @@ -798,6 +823,7 @@ importers: yaml: specifier: ^2.8.1 version: 2.8.3 + devDependencies: '@types/node': specifier: ^22.18.6 @@ -1558,6 +1584,10 @@ packages: libc: [musl] hasBin: true + '@github/copilot-sdk@0.1.32': + resolution: {integrity: sha1-mkVfe7Zkq80wJuGboVpfCcAwg4E=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@github/copilot-sdk/-/copilot-sdk-0.1.32.tgz} + engines: {node: '>=20.0.0'} + '@github/copilot-sdk@1.0.1': resolution: {integrity: sha512-w6AaS0WqqTE/3iyUrZznvgCLQhsUF7ZmEVCneacuHCfOzlH0r6ww9WUmyA0zgqmXO75V0IYrkIcnFke/qJkkDg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4782,7 +4812,7 @@ packages: engines: {node: '>=12'} dotenv@16.6.1: - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + resolution: {integrity: sha1-dz8OaVJ6gxXHKF1e5zxEWdIKgCA=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/dotenv/-/dotenv-16.6.1.tgz} engines: {node: '>=12'} dunder-proto@1.0.1: @@ -7699,6 +7729,14 @@ snapshots: '@github/copilot-linuxmusl-x64@1.0.63': optional: true + '@github/copilot-sdk@0.1.32(typescript@5.9.3)': + dependencies: + '@github/copilot': 1.0.63(typescript@5.9.3) + vscode-jsonrpc: 8.2.1 + zod: 4.3.6 + transitivePeerDependencies: + - typescript + '@github/copilot-sdk@1.0.1(typescript@5.9.3)': dependencies: '@github/copilot': 1.0.63(typescript@5.9.3) From cd71e3f87c544f2263ce65155d45ae879361c36c Mon Sep 17 00:00:00 2001 From: Cedric Vidal Date: Tue, 13 Oct 2026 16:47:46 +0200 Subject: [PATCH 02/15] feat(readout): generate submission-level reports Add the pp-readout submission handler, exact-vector evidence aggregation, structured artifacts, report APIs, and stale-publication safeguards. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cbbd5eb-61c6-4e6a-bb38-c377be5b2163 --- .../openapi-snapshot.test.ts.snap | 1070 ++++++++++++++--- apps/api/src/archive-har.test.ts | 40 - apps/api/src/archive-har.ts | 29 +- apps/api/src/index.ts | 2 - .../api/src/routes/reports-structured.test.ts | 100 ++ apps/api/src/routes/reports.ts | 61 +- apps/api/src/routes/requests/taxonomy.test.ts | 107 -- apps/api/src/routes/requests/taxonomy.ts | 30 - apps/api/src/routes/submissions.test.ts | 49 + apps/api/src/routes/submissions.ts | 29 + apps/scheduler/src/handler-dispatcher.test.ts | 22 +- apps/scheduler/src/notify-routes.test.ts | 8 +- apps/workers/{taxonomy => readout}/Dockerfile | 20 +- apps/workers/readout/handler.yaml | 12 + .../{taxonomy => readout}/package.json | 8 +- apps/workers/readout/src/index.ts | 39 + apps/workers/readout/src/prompt.ts | 63 + .../readout/src/readout-builder.test.ts | 191 +++ apps/workers/readout/src/readout-builder.ts | 303 +++++ apps/workers/readout/src/readout-generator.ts | 98 ++ .../src/readout-queue-processor.test.ts | 274 +++++ .../readout/src/readout-queue-processor.ts | 303 +++++ .../{taxonomy => readout}/tsconfig.json | 0 .../src/report-queue-processor.ts | 5 + apps/workers/taxonomy/handler.yaml | 11 - apps/workers/taxonomy/src/cli.ts | 206 ---- apps/workers/taxonomy/src/index.ts | 30 - apps/workers/taxonomy/src/prompt.ts | 88 -- .../taxonomy/src/taxonomy-generator.ts | 202 ---- .../taxonomy/src/taxonomy-queue-processor.ts | 190 --- apps/workers/taxonomy/src/tools.ts | 154 --- docker-compose.dev.yml | 10 +- docker-compose.yml | 28 +- package.json | 6 +- .../031-migrate-services-to-handler-dag.ts | 2 +- .../032-add-handler-status-index.ts | 8 - packages/shared/src/index.ts | 2 +- packages/shared/src/schemas/index.ts | 2 +- packages/shared/src/schemas/readout.test.ts | 48 + packages/shared/src/schemas/readout.ts | 84 ++ packages/shared/src/schemas/report.ts | 14 +- packages/shared/src/schemas/request.ts | 1 - packages/shared/src/schemas/taxonomy.test.ts | 180 --- packages/shared/src/schemas/taxonomy.ts | 104 -- .../src/scripts/register-handler.test.ts | 20 +- packages/shared/src/types/types.ts | 16 +- pnpm-lock.yaml | 30 +- 47 files changed, 2651 insertions(+), 1648 deletions(-) create mode 100644 apps/api/src/routes/reports-structured.test.ts delete mode 100644 apps/api/src/routes/requests/taxonomy.test.ts delete mode 100644 apps/api/src/routes/requests/taxonomy.ts rename apps/workers/{taxonomy => readout}/Dockerfile (60%) create mode 100644 apps/workers/readout/handler.yaml rename apps/workers/{taxonomy => readout}/package.json (76%) create mode 100644 apps/workers/readout/src/index.ts create mode 100644 apps/workers/readout/src/prompt.ts create mode 100644 apps/workers/readout/src/readout-builder.test.ts create mode 100644 apps/workers/readout/src/readout-builder.ts create mode 100644 apps/workers/readout/src/readout-generator.ts create mode 100644 apps/workers/readout/src/readout-queue-processor.test.ts create mode 100644 apps/workers/readout/src/readout-queue-processor.ts rename apps/workers/{taxonomy => readout}/tsconfig.json (100%) delete mode 100644 apps/workers/taxonomy/handler.yaml delete mode 100644 apps/workers/taxonomy/src/cli.ts delete mode 100644 apps/workers/taxonomy/src/index.ts delete mode 100644 apps/workers/taxonomy/src/prompt.ts delete mode 100644 apps/workers/taxonomy/src/taxonomy-generator.ts delete mode 100644 apps/workers/taxonomy/src/taxonomy-queue-processor.ts delete mode 100644 apps/workers/taxonomy/src/tools.ts create mode 100644 packages/shared/src/schemas/readout.test.ts create mode 100644 packages/shared/src/schemas/readout.ts delete mode 100644 packages/shared/src/schemas/taxonomy.test.ts delete mode 100644 packages/shared/src/schemas/taxonomy.ts diff --git a/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap b/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap index 6d28ff754..4f68fb14e 100644 --- a/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap +++ b/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap @@ -2450,6 +2450,15 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], "type": "string", }, + "structuredArtifact": { + "$ref": "#/components/schemas/StructuredReportArtifact", + }, + "submissionId": { + "type": "string", + }, + "submissionRunId": { + "type": "string", + }, "templateId": { "type": "string", }, @@ -2463,7 +2472,6 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` }, "required": [ "_id", - "requestId", "status", "createdAt", "projectId", @@ -2817,9 +2825,27 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` }, "type": "array", }, + "submissionExpectedRequestCount": { + "minimum": 1, + "type": "integer", + }, + "submissionFinalizedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, "submissionId": { "type": "string", }, + "submissionInitialRunId": { + "type": "string", + }, + "submissionRequestIndex": { + "minimum": 0, + "type": "integer", + }, "taskPromptId": { "type": "string", }, @@ -3358,9 +3384,6 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], "type": "string", }, - "taxonomyUrl": { - "type": "string", - }, "tokenUsage": { "$ref": "#/components/schemas/TokenUsage", }, @@ -3658,202 +3681,660 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], "type": "object", }, - "SuggestedPromptFeature": { + "StructuredReportArtifact": { "properties": { - "behavior": { + "contentType": { + "enum": [ + "application/json", + ], "type": "string", }, - "prompt": { + "schemaId": { "type": "string", }, - "suggestedId": { + "schemaVersion": { + "type": "string", + }, + "url": { "type": "string", }, }, "required": [ - "suggestedId", - "behavior", - "prompt", + "schemaId", + "schemaVersion", + "contentType", + "url", ], "type": "object", }, - "TaskPromptResponse": { + "SubmissionBlockingDetail": { "properties": { - "_id": { + "error": { "type": "string", }, - "contentBlobUrl": { + "handlerId": { "type": "string", }, - "createdAt": { - "format": "date-time", - "type": [ - "string", - "null", + "reason": { + "enum": [ + "request-missing", + "run-changed", + "dependency-not-ready", + "dependency-failed", + "dependency-blocked", ], + "type": "string", }, - "deletedAt": { + "requestId": { + "type": "string", + }, + "runId": { + "type": "string", + }, + }, + "required": [ + "requestId", + "runId", + "handlerId", + "reason", + ], + "type": "object", + }, + "SubmissionDependencyProjection": { + "properties": { + "error": { + "type": "string", + }, + "handlerId": { + "type": "string", + }, + "requestId": { + "type": "string", + }, + "runId": { + "type": "string", + }, + "state": { + "enum": [ + "waiting", + "ready", + "failed", + "blocked", + ], + "type": "string", + }, + "updatedAt": { "format": "date-time", "type": [ "string", "null", ], }, - "features": { - "items": { - "$ref": "#/components/schemas/PromptFeatureResult", - }, - "type": "array", + "version": { + "type": "number", }, - "featuresExtractedAt": { + }, + "required": [ + "requestId", + "runId", + "handlerId", + "state", + ], + "type": "object", + }, + "SubmissionHandlerStatus": { + "properties": { + "error": { + "type": "string", + }, + "status": { + "enum": [ + "queued", + "processing", + "done", + "failed", + ], + "type": "string", + }, + "updatedAt": { "format": "date-time", "type": [ "string", "null", ], }, - "keyId": { - "type": "string", - }, - "projectId": { - "type": "string", - }, - "text": { - "type": "string", + "version": { + "type": "number", }, - "type": { - "$ref": "#/components/schemas/PromptType", + }, + "required": [ + "status", + ], + "type": "object", + }, + "SubmissionListResponse": { + "properties": { + "submissions": { + "items": { + "$ref": "#/components/schemas/SubmissionResponse", + }, + "type": "array", }, }, "required": [ - "_id", - "keyId", - "createdAt", - "projectId", + "submissions", ], "type": "object", }, - "TokenUsage": { + "SubmissionReportPointer": { "properties": { - "completionTokens": { - "type": "number", + "reportId": { + "type": "string", }, - "promptTokens": { - "type": "number", + "updatedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], }, - "totalTokens": { - "type": "number", + "url": { + "type": "string", }, }, - "required": [ - "promptTokens", - "completionTokens", - "totalTokens", - ], "type": "object", }, - "TriggerReportsInput": { + "SubmissionRequestRef": { "properties": { "requestId": { "type": "string", }, + "runId": { + "type": "string", + }, }, "required": [ "requestId", + "runId", ], "type": "object", }, - "UpdateAgentInput": { + "SubmissionRequestSummary": { "properties": { - "available": { - "type": "boolean", - }, - "capabilities": { - "$ref": "#/components/schemas/AgentCapabilities", - }, - "defaultModel": { + "model": { "type": "string", }, - "description": { + "outcome": { "type": "string", }, - "modelProvider": { + "profileId": { "type": "string", }, - "name": { + "requestId": { "type": "string", }, - "supportedModels": { - "items": { - "type": "string", - }, - "type": "array", + "runId": { + "type": "string", }, - }, - "type": "object", - }, - "UpdateCodebaseInput": { - "properties": { - "defaultBranch": { + "status": { "type": "string", }, - "description": { + "task": { "type": "string", }, - "name": { - "minLength": 1, + "workerType": { "type": "string", }, }, + "required": [ + "requestId", + "runId", + "task", + "status", + "workerType", + ], "type": "object", }, - "UpdateCriteriaInput": { + "SubmissionResponse": { "properties": { - "dependsOn": { + "attemptNumber": { + "type": "integer", + }, + "blocking": { "items": { - "type": "string", + "$ref": "#/components/schemas/SubmissionBlockingDetail", }, "type": "array", }, - "gates": { + "createdAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "dependencyProjection": { "items": { - "enum": [ - "select", - "build", - "test", - "run", - "deploy", - ], - "type": "string", + "$ref": "#/components/schemas/SubmissionDependencyProjection", }, "type": "array", }, - "kind": { + "handlerProgress": { + "additionalProperties": { + "$ref": "#/components/schemas/SubmissionHandlerStatus", + }, + "type": "object", + }, + "projectId": { + "type": "string", + }, + "readiness": { "enum": [ - "gate", - "observation", + "waiting", + "ready", + "blocked", + "processing", + "done", + "failed", ], "type": "string", }, - "prompt": { + "report": { + "$ref": "#/components/schemas/SubmissionReportPointer", + }, + "requestIds": { + "items": { + "type": "string", + }, + "type": "array", + }, + "requests": { + "items": { + "$ref": "#/components/schemas/SubmissionRequestSummary", + }, + "type": "array", + }, + "submissionId": { "type": "string", }, - "subject": { - "enum": [ - "run", - "iteration", + "submissionRunId": { + "type": "string", + }, + "supersededAt": { + "format": "date-time", + "type": [ + "string", + "null", ], + }, + "supersededBySubmissionRunId": { "type": "string", }, - "taxonomyElementId": { + "supersededReason": { "enum": [ - "dimension:idiomatic-use", - "dimension:dependency-currency", - "dimension:configuration-correctness", + "request-vector-changed", + "manual-retry", ], "type": "string", }, - }, + "updatedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + }, + "required": [ + "submissionId", + "projectId", + "requestIds", + "createdAt", + "submissionRunId", + "attemptNumber", + "readiness", + "requests", + "dependencyProjection", + "blocking", + ], + "type": "object", + }, + "SubmissionRetryResponse": { + "properties": { + "attemptNumber": { + "type": "integer", + }, + "blocking": { + "items": { + "$ref": "#/components/schemas/SubmissionBlockingDetail", + }, + "type": "array", + }, + "readiness": { + "enum": [ + "waiting", + "ready", + "blocked", + "processing", + "done", + "failed", + ], + "type": "string", + }, + "submissionId": { + "type": "string", + }, + "submissionRunId": { + "type": "string", + }, + }, + "required": [ + "submissionId", + "submissionRunId", + "attemptNumber", + "readiness", + "blocking", + ], + "type": "object", + }, + "SubmissionRunResponse": { + "properties": { + "_id": { + "type": "string", + }, + "attemptNumber": { + "minimum": 1, + "type": "integer", + }, + "blocking": { + "default": [], + "items": { + "$ref": "#/components/schemas/SubmissionBlockingDetail", + }, + "type": "array", + }, + "createdAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "dependencyProjection": { + "default": [], + "items": { + "$ref": "#/components/schemas/SubmissionDependencyProjection", + }, + "type": "array", + }, + "finishedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "handlerStatus": { + "additionalProperties": { + "$ref": "#/components/schemas/SubmissionHandlerStatus", + }, + "type": "object", + }, + "projectId": { + "type": "string", + }, + "report": { + "$ref": "#/components/schemas/SubmissionReportPointer", + }, + "requests": { + "items": { + "$ref": "#/components/schemas/SubmissionRequestRef", + }, + "type": "array", + }, + "startedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "submissionId": { + "type": "string", + }, + "supersededAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "supersededBySubmissionRunId": { + "type": "string", + }, + "supersededReason": { + "enum": [ + "request-vector-changed", + "manual-retry", + ], + "type": "string", + }, + "updatedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "vectorFingerprint": { + "type": "string", + }, + }, + "required": [ + "_id", + "attemptNumber", + "vectorFingerprint", + "requests", + "createdAt", + ], + "type": "object", + }, + "SuggestedPromptFeature": { + "properties": { + "behavior": { + "type": "string", + }, + "prompt": { + "type": "string", + }, + "suggestedId": { + "type": "string", + }, + }, + "required": [ + "suggestedId", + "behavior", + "prompt", + ], + "type": "object", + }, + "TaskPromptResponse": { + "properties": { + "_id": { + "type": "string", + }, + "contentBlobUrl": { + "type": "string", + }, + "createdAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "deletedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "features": { + "items": { + "$ref": "#/components/schemas/PromptFeatureResult", + }, + "type": "array", + }, + "featuresExtractedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "keyId": { + "type": "string", + }, + "projectId": { + "type": "string", + }, + "text": { + "type": "string", + }, + "type": { + "$ref": "#/components/schemas/PromptType", + }, + }, + "required": [ + "_id", + "keyId", + "createdAt", + "projectId", + ], + "type": "object", + }, + "TokenUsage": { + "properties": { + "completionTokens": { + "type": "number", + }, + "promptTokens": { + "type": "number", + }, + "totalTokens": { + "type": "number", + }, + }, + "required": [ + "promptTokens", + "completionTokens", + "totalTokens", + ], + "type": "object", + }, + "TriggerReportsInput": { + "properties": { + "requestId": { + "type": "string", + }, + }, + "required": [ + "requestId", + ], + "type": "object", + }, + "UpdateAgentInput": { + "properties": { + "available": { + "type": "boolean", + }, + "capabilities": { + "$ref": "#/components/schemas/AgentCapabilities", + }, + "defaultModel": { + "type": "string", + }, + "description": { + "type": "string", + }, + "modelProvider": { + "type": "string", + }, + "name": { + "type": "string", + }, + "supportedModels": { + "items": { + "type": "string", + }, + "type": "array", + }, + }, + "type": "object", + }, + "UpdateCodebaseInput": { + "properties": { + "defaultBranch": { + "type": "string", + }, + "description": { + "type": "string", + }, + "name": { + "minLength": 1, + "type": "string", + }, + }, + "type": "object", + }, + "UpdateCriteriaInput": { + "properties": { + "dependsOn": { + "items": { + "type": "string", + }, + "type": "array", + }, + "gates": { + "items": { + "enum": [ + "select", + "build", + "test", + "run", + "deploy", + ], + "type": "string", + }, + "type": "array", + }, + "kind": { + "enum": [ + "gate", + "observation", + ], + "type": "string", + }, + "prompt": { + "type": "string", + }, + "subject": { + "enum": [ + "run", + "iteration", + ], + "type": "string", + }, + "taxonomyElementId": { + "enum": [ + "dimension:idiomatic-use", + "dimension:dependency-currency", + "dimension:configuration-correctness", + ], + "type": "string", + }, + }, "type": "object", }, "UpdateExtensionInput": { @@ -8350,6 +8831,22 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "type": "string", }, }, + { + "in": "query", + "name": "submissionId", + "required": false, + "schema": { + "type": "string", + }, + }, + { + "in": "query", + "name": "submissionRunId", + "required": false, + "schema": { + "type": "string", + }, + }, { "in": "query", "name": "projectId", @@ -8792,6 +9289,32 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], }, }, + "/api/v1/reports/{id}/structured": { + "get": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + }, + }, + ], + "responses": { + "200": { + "description": "Success", + }, + "404": { + "description": "Report or structured artifact not found", + }, + }, + "summary": "Download a report's structured JSON artifact", + "tags": [ + "Reports", + ], + }, + }, "/api/v1/requests": { "get": { "parameters": [ @@ -10368,49 +10891,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "parameters": [ { "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "string", - }, - }, - { - "in": "path", - "name": "runId", - "required": true, - "schema": { - "type": "string", - }, - }, - ], - "responses": { - "200": { - "description": "Server-sent event stream of log entries", - }, - "404": { - "description": "Not found", - }, - }, - "summary": "Stream logs for a specific attempt (SSE)", - "tags": [ - "Requests", - ], - }, - }, - "/api/v1/requests/{id}/runs/{runId}/snapshots/{iteration}": { - "get": { - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "string", - }, - }, - { - "in": "path", - "name": "runId", + "name": "id", "required": true, "schema": { "type": "string", @@ -10418,7 +10899,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` }, { "in": "path", - "name": "iteration", + "name": "runId", "required": true, "schema": { "type": "string", @@ -10427,19 +10908,19 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], "responses": { "200": { - "description": "Gzipped snapshot archive", + "description": "Server-sent event stream of log entries", }, "404": { "description": "Not found", }, }, - "summary": "Download iteration snapshot for a specific attempt", + "summary": "Stream logs for a specific attempt (SSE)", "tags": [ "Requests", ], }, }, - "/api/v1/requests/{id}/runs/{runId}/taxonomy": { + "/api/v1/requests/{id}/runs/{runId}/snapshots/{iteration}": { "get": { "parameters": [ { @@ -10458,16 +10939,24 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "type": "string", }, }, + { + "in": "path", + "name": "iteration", + "required": true, + "schema": { + "type": "string", + }, + }, ], "responses": { "200": { - "description": "Taxonomy v1 JSON document (scorecard, behavior analysis, action list)", + "description": "Gzipped snapshot archive", }, "404": { - "description": "Not found — taxonomy not yet generated", + "description": "Not found", }, }, - "summary": "Download taxonomy JSON for a specific attempt", + "summary": "Download iteration snapshot for a specific attempt", "tags": [ "Requests", ], @@ -10575,32 +11064,6 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], }, }, - "/api/v1/requests/{id}/taxonomy": { - "get": { - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "string", - }, - }, - ], - "responses": { - "200": { - "description": "Taxonomy v1 JSON document (scorecard, behavior analysis, action list)", - }, - "404": { - "description": "Not found — taxonomy not yet generated", - }, - }, - "summary": "Download taxonomy JSON for the run", - "tags": [ - "Requests", - ], - }, - }, "/api/v1/requests/{id}/tool-calls": { "get": { "parameters": [ @@ -11656,6 +12119,255 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], }, }, + "/api/v1/submissions": { + "get": { + "parameters": [ + { + "description": "Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.", + "in": "query", + "name": "projectId", + "required": true, + "schema": { + "description": "Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.", + "example": "00000000-0000-0000-0000-000000000000", + "minLength": 1, + "type": "string", + }, + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 50, + "maximum": 100, + "minimum": 1, + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmissionListResponse", + }, + }, + }, + "description": "Success", + }, + }, + "summary": "List submissions", + "tags": [ + "Submissions", + ], + }, + }, + "/api/v1/submissions/{id}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmissionResponse", + }, + }, + }, + "description": "Success", + }, + "404": { + "description": "Submission not found", + }, + }, + "summary": "Get a submission", + "tags": [ + "Submissions", + ], + }, + }, + "/api/v1/submissions/{id}/readout": { + "get": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReportResponse", + }, + }, + }, + "description": "Success", + }, + "404": { + "description": "Current readout not found", + }, + }, + "summary": "Get the current submission readout report", + "tags": [ + "Submissions", + ], + }, + }, + "/api/v1/submissions/{id}/retry": { + "post": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + }, + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "submissionRunId": { + "type": "string", + }, + }, + "required": [ + "submissionRunId", + ], + "type": "object", + }, + }, + }, + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmissionRetryResponse", + }, + }, + }, + "description": "Success", + }, + "404": { + "description": "Submission not found", + }, + "409": { + "description": "Submission attempt is stale or dependencies are blocked", + }, + }, + "summary": "Retry current submission processing", + "tags": [ + "Submissions", + ], + }, + }, + "/api/v1/submissions/{id}/runs": { + "get": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "runs": { + "items": { + "$ref": "#/components/schemas/SubmissionRunResponse", + }, + "type": "array", + }, + }, + "required": [ + "runs", + ], + "type": "object", + }, + }, + }, + "description": "Success", + }, + "404": { + "description": "Submission not found", + }, + }, + "summary": "List current and historical submission attempts", + "tags": [ + "Submissions", + ], + }, + }, + "/api/v1/submissions/{id}/runs/{submissionRunId}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + }, + }, + { + "in": "path", + "name": "submissionRunId", + "required": true, + "schema": { + "type": "string", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmissionRunResponse", + }, + }, + }, + "description": "Success", + }, + "404": { + "description": "Submission attempt not found", + }, + }, + "summary": "Get one submission attempt", + "tags": [ + "Submissions", + ], + }, + }, "/api/v1/task-prompts": { "get": { "parameters": [ diff --git a/apps/api/src/archive-har.test.ts b/apps/api/src/archive-har.test.ts index af8205f2b..f123b4b8c 100644 --- a/apps/api/src/archive-har.test.ts +++ b/apps/api/src/archive-har.test.ts @@ -100,16 +100,6 @@ describe("rewriteHarUrlsForArchive", () => { expect(result.run!.turns![2].toolCallsUrl).toBeUndefined(); }); - it("rewrites run.taxonomyUrl to taxonomy.json", () => { - const resource = { - run: { - taxonomyUrl: "https://storage.blob.core.windows.net/snapshots/abc123/runs/attempt-1/taxonomy.json", - turns: [], - }, - }; - const result = rewriteHarUrlsForArchive(resource); - expect(result.run!.taxonomyUrl).toBe("taxonomy.json"); - }); }); // --- blobNameFromSnapshotsUrl --- @@ -915,34 +905,4 @@ describe("packRunIntoTar", () => { await entriesPromise; expect(run.codebase!.archiveUrl).toBe(blobUrl); }); - - it("bundles run-level taxonomy.json and rewrites taxonomyUrl", async () => { - const { pack } = await import("tar-stream"); - const p = pack(); - const taxonomyData = Buffer.from('{"topics":["agent"]}'); - const container = makeMockBlobContainer({ - "run-012/runs/attempt-1/taxonomy.json": { body: taxonomyData, length: taxonomyData.length }, - }); - - const run: ArchivableRun = { - _id: "run-012", - run: { - taxonomyUrl: "https://storage.blob.core.windows.net/snapshots/run-012/runs/attempt-1/taxonomy.json", - turns: [], - }, - }; - - const entriesPromise = collectPackEntries(p); - await packRunIntoTar(p, run, container, "run-012", () => true); - p.finalize(); - - const entries = await entriesPromise; - const names = entries.map(e => e.name); - expect(names).toContain("run-012/taxonomy.json"); - expect(entries.find(e => e.name === "run-012/taxonomy.json")!.data).toEqual(taxonomyData); - - const yaml = entries.find(e => e.name === "run-012/run.yaml")!.data.toString(); - expect(yaml).toContain("taxonomy.json"); - expect(yaml).not.toContain("blob.core.windows.net"); - }); }); diff --git a/apps/api/src/archive-har.ts b/apps/api/src/archive-har.ts index c28658aea..204eaf124 100644 --- a/apps/api/src/archive-har.ts +++ b/apps/api/src/archive-har.ts @@ -41,12 +41,11 @@ export function blobNameFromLogsUrl(url: string): string | null { /** * Deep-clone a run resource and rewrite `harUrl`, `rawChatUrl`, - * `taxonomyUrl`, `chatResultUrl`, and `toolCallsUrl` fields to relative archive paths. + * `chatResultUrl`, and `toolCallsUrl` fields to relative archive paths. * * - `run.harUrl` → `"run.har"` * - Per-turn `harUrl` → `"iteration-{N}.har"` * - `run.rawChatUrl` → `"run.chat-export.json"` - * - `run.taxonomyUrl` → `"taxonomy.json"` * - Per-turn `rawChatUrl` → `"iteration-{N}.chat-export.json"` * - Per-turn `chatResultUrl` → `"iteration-{N}.chat-result.json"` * - Per-turn `toolCallsUrl` → `"iteration-{N}.tool-calls.jsonl"` @@ -55,7 +54,6 @@ export function rewriteHarUrlsForArchive; [key: string]: unknown; }; @@ -68,9 +66,6 @@ export function rewriteHarUrlsForArchive { + it("proxies the stored JSON artifact through the API", async () => { + const app = express(); + app.use(express.json()); + const downloadJson = vi.fn().mockResolvedValue({ + meta: { submissionRunId: "submission-run-1" }, + }); + const ctx = { + app, + registry: new OpenAPIRegistry(), + reportCollection: { + findOne: vi.fn().mockResolvedValue({ + _id: "report-1", + projectId: "project-1", + submissionId: "submission-1", + submissionRunId: "submission-run-1", + status: "completed", + structuredArtifact: { + schemaId: "scope.submission-readout", + schemaVersion: "1.0", + contentType: "application/json", + url: "https://storage.example/structured.json", + }, + createdAt: new Date(), + }), + }, + blobStorage: { downloadJson }, + } as unknown as RouteContext; + registerReportsRoutes(ctx); + app.use(( + error: Error, + _req: express.Request, + res: express.Response, + _next: express.NextFunction, + ) => res.status(500).json({ error: error.message })); + + const response = await request(app).get("/api/v1/reports/report-1/structured"); + + expect(response.status).toBe(200); + expect(response.headers["content-disposition"]).toContain( + "report-1.structured.json", + ); + expect(response.body).toEqual({ + meta: { submissionRunId: "submission-run-1" }, + }); + expect(downloadJson).toHaveBeenCalledWith( + "https://storage.example/structured.json", + ); + }); + + it("filters submission reports by the exact submission run", async () => { + const app = express(); + app.use(express.json()); + const toArray = vi.fn().mockResolvedValue([{ + _id: "report-1", + projectId: "project-1", + submissionId: "submission-1", + submissionRunId: "submission-run-1", + status: "completed", + createdAt: new Date("2026-10-13T12:00:00.000Z"), + }]); + const find = vi.fn(() => ({ + sort: vi.fn(() => ({ toArray })), + })); + const ctx = { + app, + registry: new OpenAPIRegistry(), + reportCollection: { find }, + requestCollection: { + find: vi.fn(() => ({ toArray: vi.fn().mockResolvedValue([]) })), + }, + } as unknown as RouteContext; + registerReportsRoutes(ctx); + + const response = await request(app).get( + "/api/v1/reports?submissionId=submission-1&submissionRunId=submission-run-1", + ); + + expect(response.status).toBe(200); + expect(find).toHaveBeenCalledWith({ + submissionId: "submission-1", + submissionRunId: "submission-run-1", + }); + expect(response.body[0]).toMatchObject({ + id: "report-1", + submissionId: "submission-1", + submissionRunId: "submission-run-1", + }); + }); +}); diff --git a/apps/api/src/routes/reports.ts b/apps/api/src/routes/reports.ts index d6dd7aa32..381600660 100644 --- a/apps/api/src/routes/reports.ts +++ b/apps/api/src/routes/reports.ts @@ -15,6 +15,7 @@ import { ReportResponseSchema, TriggerReportsInputSchema, evaluateTrigger, + withRetry, } from "shared"; import type { TaskPromptDocument } from "shared"; import { apiRoute } from "../openapi/api-route.js"; @@ -108,18 +109,29 @@ apiRoute(ctx.app, ctx.registry, { path: "/api/v1/reports", tags: ["Reports"], summary: "List reports", - query: z.object({ requestId: z.string().optional(), projectId: z.string().optional() }), + query: z.object({ + requestId: z.string().optional(), + submissionId: z.string().optional(), + submissionRunId: z.string().optional(), + projectId: z.string().optional(), + }), response: z.array(ReportResponseSchema), handler: async (req, res, next) => { try { const requestIdFilter = req.query.requestId as string; + const submissionIdFilter = req.query.submissionId as string; + const submissionRunIdFilter = req.query.submissionRunId as string; const filter: Record = {}; if (requestIdFilter) { // Parent-scoped: the run id already pins the project. filter.requestId = requestIdFilter; + } else if (submissionIdFilter) { + filter.submissionId = submissionIdFilter; + if (submissionRunIdFilter) filter.submissionRunId = submissionRunIdFilter; } else { // Top-level reports list requires an explicit project (400 if absent). filter.projectId = getQueryProjectId(req); + if (submissionRunIdFilter) filter.submissionRunId = submissionRunIdFilter; } const reports = await ctx.reportCollection @@ -128,13 +140,21 @@ apiRoute(ctx.app, ctx.registry, { .toArray(); // Enrich reports with the task from their associated run - const requestIds = [...new Set(reports.map(r => r.requestId))]; + const requestIds = [ + ...new Set(reports.flatMap((report) => report.requestId ? [report.requestId] : [])), + ]; const runs = requestIds.length > 0 ? await ctx.requestCollection.find({ _id: { $in: requestIds } as any }, { projection: { _id: 1, "scenario.task": 1 } }).toArray() : []; const taskByRequestId = new Map(runs.map(r => [r._id, r.scenario?.task])); - res.json(reports.map(r => ({ ...r, id: r._id, task: taskByRequestId.get(r.requestId) }))); + res.json(reports.map((report) => ({ + ...report, + id: report._id, + ...(report.requestId + ? { task: taskByRequestId.get(report.requestId) } + : {}), + }))); } catch (error) { next(error); } @@ -201,6 +221,39 @@ apiRoute(ctx.app, ctx.registry, { }, }); +apiRoute(ctx.app, ctx.registry, { + method: "get", + path: "/api/v1/reports/:id/structured", + tags: ["Reports"], + summary: "Download a report's structured JSON artifact", + params: z.object({ id: z.string() }), + response: z.any(), + rawResponse: true, + errorResponses: { + 404: { description: "Report or structured artifact not found" }, + }, + handler: async (req, res, next) => { + try { + const report = await ctx.reportCollection.findOne({ _id: req.params.id }); + if (!report?.structuredArtifact) { + res.status(404).json({ error: "Structured report artifact not found" }); + return; + } + const structured = await withRetry(() => + ctx.blobStorage.downloadJson(report.structuredArtifact!.url) + ); + res.setHeader("Content-Type", "application/json"); + res.setHeader( + "Content-Disposition", + `attachment; filename="${req.params.id}.structured.json"`, + ); + res.json(structured); + } catch (error) { + next(error); + } + }, +}); + apiRoute(ctx.app, ctx.registry, { method: "post", path: "/api/v1/reports/bulk-status", @@ -229,7 +282,7 @@ apiRoute(ctx.app, ctx.registry, { // Build a map of requestId → latest report status const statusMap: Record = {}; for (const report of reports) { - if (!statusMap[report.requestId]) { + if (report.requestId && !statusMap[report.requestId]) { statusMap[report.requestId] = { reportId: report._id, status: report.status, diff --git a/apps/api/src/routes/requests/taxonomy.test.ts b/apps/api/src/routes/requests/taxonomy.test.ts deleted file mode 100644 index 3909df826..000000000 --- a/apps/api/src/routes/requests/taxonomy.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** - * Unit tests for taxonomy download endpoints: - * - GET /api/v1/requests/:id/taxonomy - * - GET /api/v1/requests/:id/runs/:runId/taxonomy - */ -import { describe, it, expect, beforeAll, beforeEach, vi } from "vitest"; -import supertest from "supertest"; -import { app, _injectTestDependencies } from "../../index.js"; -import { createAllMockDependencies } from "../../test-helpers.js"; -import { readableFrom, rawParser, blobUrl, VARIANTS, RUN_LEVEL, rewireBlobMocks } from "./test-blob-helpers.js"; - -vi.mock("db-migrations/check-migrations", () => ({ - checkMigrations: vi.fn().mockResolvedValue({ ready: true, applied: ["001", "002"], pending: [] }), -})); - -vi.hoisted(() => { - process.env.STORAGE_CONNECTION_STRING = "DefaultEndpointsProtocol=https;AccountName=mockaccount;AccountKey=bW9jaw==;EndpointSuffix=core.windows.net"; -}); - -vi.mock("../../llm.js", () => ({ isLlmAvailable: vi.fn().mockReturnValue(false) })); -vi.mock("../../prompt-feature-llm.js", () => ({ isLlmAvailable: vi.fn().mockReturnValue(false) })); -vi.mock("../../task-prompt-llm.js", () => ({ isTaskPromptLlmAvailable: vi.fn().mockReturnValue(false) })); - -const { mockDownload, mockGetProperties, mockGetBlockBlobClient, mockGetBlobClient, mockGetContainerClient } = vi.hoisted(() => { - const mockDownload = vi.fn(); - const mockGetProperties = vi.fn(); - const mockGetBlockBlobClient = vi.fn().mockReturnValue({ download: mockDownload, getProperties: mockGetProperties }); - const mockGetBlobClient = vi.fn().mockReturnValue({ download: mockDownload, getProperties: mockGetProperties }); - const mockGetContainerClient = vi.fn().mockReturnValue({ getBlockBlobClient: mockGetBlockBlobClient, getBlobClient: mockGetBlobClient }); - return { mockDownload, mockGetProperties, mockGetBlockBlobClient, mockGetBlobClient, mockGetContainerClient }; -}); - -vi.mock("@azure/storage-blob", async (importOriginal) => { - const original = await importOriginal(); - return { - ...original, - BlobServiceClient: { fromConnectionString: vi.fn().mockReturnValue({ getContainerClient: mockGetContainerClient }) }, - }; -}); - -describe("taxonomy endpoints", () => { - let mocks: ReturnType; - - beforeAll(() => { - mocks = createAllMockDependencies(); - _injectTestDependencies(mocks); - }); - - beforeEach(() => { - vi.clearAllMocks(); - mocks = createAllMockDependencies(); - _injectTestDependencies(mocks); - rewireBlobMocks({ mockDownload, mockGetProperties, mockGetBlockBlobClient, mockGetBlobClient, mockGetContainerClient }); - mockDownload.mockResolvedValue({ readableStreamBody: readableFrom('{"status":"ok"}'), contentLength: 15 }); - mockGetProperties.mockResolvedValue({ contentLength: 15 }); - }); - - describe.each(VARIANTS)("$label", (variant) => { - const taxonomyUrl = (id: string) => variant.url(id) + "taxonomy"; - - it("returns 404 when request not found", async () => { - (mocks.collection.findOne as ReturnType).mockResolvedValue(null); - const res = await supertest(app).get(taxonomyUrl("missing")); - expect(res.status).toBe(404); - expect(res.body).toMatchObject({ error: "Request not found" }); - }); - - it("returns 404 when taxonomyUrl is not set", async () => { - variant.mockRequest(mocks, "req-1", { _id: "run-1", attemptNumber: 1, status: "done" }); - const res = await supertest(app).get(taxonomyUrl("req-1")); - expect(res.status).toBe(404); - expect(res.body).toMatchObject({ error: "Taxonomy not yet generated for this run" }); - }); - - it("returns taxonomy JSON when taxonomyUrl is set", async () => { - variant.mockRequest(mocks, "req-1", { - _id: "run-1", - attemptNumber: 1, - status: "done", - taxonomyUrl: blobUrl("req-1/taxonomy.json"), - }); - - const res = await supertest(app).get(taxonomyUrl("req-1")).buffer(true).parse(rawParser); - expect(res.status).toBe(200); - expect(res.headers["content-type"]).toMatch("application/json"); - expect(res.headers["content-disposition"]).toContain("req-1-taxonomy.json"); - expect(mockGetBlockBlobClient).toHaveBeenCalledWith("req-1/taxonomy.json"); - }); - }); - - it("works with per-run variant", async () => { - RUN_LEVEL.mockRequest(mocks, "req-1", { - _id: "run-1", - attemptNumber: 1, - status: "done", - taxonomyUrl: blobUrl("req-1/runs/run-1/taxonomy.json"), - }); - - const res = await supertest(app).get(RUN_LEVEL.url("req-1") + "taxonomy").buffer(true).parse(rawParser); - expect(res.status).toBe(200); - expect(res.headers["content-disposition"]).toContain("req-1-taxonomy.json"); - expect(mockGetBlockBlobClient).toHaveBeenCalledWith("req-1/runs/run-1/taxonomy.json"); - }); -}); diff --git a/apps/api/src/routes/requests/taxonomy.ts b/apps/api/src/routes/requests/taxonomy.ts deleted file mode 100644 index e74d303f6..000000000 --- a/apps/api/src/routes/requests/taxonomy.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import type { RouteContext } from "../../route-context.js"; -import { downloadBlobToResponse } from "./blob-helpers.js"; -import { registerArtifactRoutes } from "./artifact-route.js"; - -export function registerRequestsTaxonomyRoutes(ctx: RouteContext): void { - registerArtifactRoutes(ctx, { - path: "taxonomy", - summary: "Download taxonomy JSON for the run", - perRunSummary: "Download taxonomy JSON for a specific attempt", - responseDescription: "Taxonomy v1 JSON document (scorecard, behavior analysis, action list)", - errorResponses: { 404: { description: "Not found — taxonomy not yet generated" } }, - blobNotFoundMessage: "Taxonomy file not found — the blob may have been deleted or is no longer available", - handler: async (ctx, _req, res, targetRun, id) => { - const taxonomyUrl = targetRun.taxonomyUrl; - if (!taxonomyUrl) { - res.status(404).json({ error: "Taxonomy not yet generated for this run" }); - return; - } - - await downloadBlobToResponse(ctx, res, taxonomyUrl, { - contentType: "application/json", - filename: `${id}-taxonomy.json`, - label: "Taxonomy", - }); - }, - }); -} diff --git a/apps/api/src/routes/submissions.test.ts b/apps/api/src/routes/submissions.test.ts index 984bdd358..4b1779484 100644 --- a/apps/api/src/routes/submissions.test.ts +++ b/apps/api/src/routes/submissions.test.ts @@ -8,6 +8,7 @@ import { describe, expect, it, vi } from "vitest"; import type { Collection, Db } from "mongodb"; import type { RequestDocument, + ReportDocument, ServiceHandlerDocument, SubmissionDocument, SubmissionRunHistoryDocument, @@ -28,6 +29,7 @@ function buildApp(input: { requests: RequestDocument[]; submission: SubmissionDocument; history?: SubmissionRunHistoryDocument[]; + reports?: ReportDocument[]; }) { let submission = structuredClone(input.submission); const services: ServiceHandlerDocument[] = [ @@ -90,6 +92,15 @@ function buildApp(input: { requestCollection: { find: vi.fn(() => queryResult(input.requests)), } as unknown as Collection, + reportCollection: { + findOne: vi.fn(async (filter: Record) => + (input.reports ?? []).find((report) => + Object.entries(filter).every(([key, value]) => + report[key as keyof ReportDocument] === value + ) + ) ?? null + ), + } as unknown as Collection, submissionCollection, submissionRunsCollection: { find: vi.fn(() => queryResult(input.history ?? [])), @@ -250,4 +261,42 @@ describe("submission routes", () => { }, ]); }); + + it("selects only the current submission run readout", async () => { + const currentReport: ReportDocument = { + _id: "report-current", + projectId: "project-1", + submissionId: "submission-1", + submissionRunId: "submission-run-1", + status: "completed", + content: "# Current", + createdAt: new Date(), + }; + const response = await request( + buildApp({ + requests: [requestDocument], + submission: { + ...submissionDocument, + run: { + ...submissionDocument.run, + report: { reportId: "report-current" }, + }, + }, + reports: [ + { + ...currentReport, + _id: "report-historical", + submissionRunId: "submission-run-0", + }, + currentReport, + ], + }), + ).get("/api/v1/submissions/submission-1/readout"); + + expect(response.status).toBe(200); + expect(response.body).toMatchObject({ + _id: "report-current", + submissionRunId: "submission-run-1", + }); + }); }); diff --git a/apps/api/src/routes/submissions.ts b/apps/api/src/routes/submissions.ts index 201aae5bd..aca173167 100644 --- a/apps/api/src/routes/submissions.ts +++ b/apps/api/src/routes/submissions.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import { SubmissionCoordinator, SubmissionListResponseSchema, + ReportResponseSchema, SubmissionResponseSchema, SubmissionRetryResponseSchema, SubmissionRunResponseSchema, @@ -123,6 +124,34 @@ export function registerSubmissionsRoutes(ctx: RouteContext): void { }, }); + apiRoute(ctx.app, ctx.registry, { + method: "get", + path: "/api/v1/submissions/:id/readout", + tags: ["Submissions"], + summary: "Get the current submission readout report", + params: z.object({ id: z.string() }), + response: ReportResponseSchema, + errorResponses: { 404: { description: "Current readout not found" } }, + handler: async (req, res) => { + const submission = await ctx.submissionCollection.findOne({ _id: req.params.id }); + const reportId = submission?.run.report?.reportId; + if (!submission || !reportId) { + res.status(404).json({ error: "Current readout not found" }); + return; + } + const report = await ctx.reportCollection.findOne({ + _id: reportId, + submissionId: submission._id, + submissionRunId: submission.run._id, + }); + if (!report) { + res.status(404).json({ error: "Current readout not found" }); + return; + } + res.json({ ...report, id: report._id }); + }, + }); + apiRoute(ctx.app, ctx.registry, { method: "get", path: "/api/v1/submissions/:id", diff --git a/apps/scheduler/src/handler-dispatcher.test.ts b/apps/scheduler/src/handler-dispatcher.test.ts index 847e6be90..32d954243 100644 --- a/apps/scheduler/src/handler-dispatcher.test.ts +++ b/apps/scheduler/src/handler-dispatcher.test.ts @@ -16,11 +16,11 @@ const HANDLERS: HandlerServiceDocument[] = [ dependsOn: [], }, { - _id: "pp-taxonomy", + _id: "pp-example", type: "post-process-handler", version: 1, - queue: "pp-taxonomy-queue", - selector: "taxonomy", + queue: "pp-example-queue", + selector: "example", autoBackfill: false, dependsOn: ["pp-atif"], }, @@ -87,7 +87,7 @@ describe("HandlerDispatcher — drain detection", () => { it("is drained when all handlers are done", () => { expect( - isDrained(HANDLERS, { "pp-atif": { status: "done" }, "pp-taxonomy": { status: "done" } }), + isDrained(HANDLERS, { "pp-atif": { status: "done" }, "pp-example": { status: "done" } }), ).toBe(true); }); @@ -99,13 +99,13 @@ describe("HandlerDispatcher — drain detection", () => { expect( isDrained(HANDLERS, { "pp-atif": { status: "done" }, - "pp-taxonomy": { status: "processing" }, + "pp-example": { status: "processing" }, }), ).toBe(false); }); it("is drained when a failed dependency blocks its descendant", () => { - // pp-atif failed → pp-taxonomy can never run (blocked) → terminal. + // pp-atif failed → pp-example can never run (blocked) → terminal. expect(isDrained(HANDLERS, { "pp-atif": { status: "failed" } })).toBe(true); }); @@ -113,7 +113,7 @@ describe("HandlerDispatcher — drain detection", () => { expect( isDrained(HANDLERS, { "pp-atif": { status: "done" }, - "pp-taxonomy": { status: "failed" }, + "pp-example": { status: "failed" }, }), ).toBe(true); }); @@ -143,7 +143,7 @@ describe("HandlerDispatcher — maybeTriggerReports", () => { run: { _id: "run-1", status: "done", - handlerStatus: { "pp-atif": { status: "done" }, "pp-taxonomy": { status: "done" } }, + handlerStatus: { "pp-atif": { status: "done" }, "pp-example": { status: "done" } }, }, }); collection.findOneAndUpdate.mockResolvedValue({ _id: "req-1" }); // claim succeeds @@ -203,7 +203,7 @@ describe("HandlerDispatcher — maybeTriggerReports", () => { run: { _id: "run-2", status: "done", - handlerStatus: { "pp-atif": { status: "done" }, "pp-taxonomy": { status: "done" } }, + handlerStatus: { "pp-atif": { status: "done" }, "pp-example": { status: "done" } }, }, }); @@ -220,7 +220,7 @@ describe("HandlerDispatcher — maybeTriggerReports", () => { run: { _id: "run-1", status: "done", - handlerStatus: { "pp-atif": { status: "done" }, "pp-taxonomy": { status: "done" } }, + handlerStatus: { "pp-atif": { status: "done" }, "pp-example": { status: "done" } }, }, }); collection.findOneAndUpdate.mockResolvedValue(null); // another path claimed it first @@ -237,7 +237,7 @@ describe("HandlerDispatcher — maybeTriggerReports", () => { run: { _id: "run-1", status: "done", - handlerStatus: { "pp-atif": { status: "done" }, "pp-taxonomy": { status: "done" } }, + handlerStatus: { "pp-atif": { status: "done" }, "pp-example": { status: "done" } }, }, }); collection.findOneAndUpdate.mockResolvedValue({ _id: "req-1" }); diff --git a/apps/scheduler/src/notify-routes.test.ts b/apps/scheduler/src/notify-routes.test.ts index b942faeee..2d7ef741b 100644 --- a/apps/scheduler/src/notify-routes.test.ts +++ b/apps/scheduler/src/notify-routes.test.ts @@ -173,11 +173,11 @@ describe("createHttpServer", () => { servers.push(server); const doc = { - _id: "pp-taxonomy", + _id: "pp-example", type: "post-process-handler", version: 1, - queue: "pp-taxonomy-queue", - selector: "taxonomy", + queue: "pp-example-queue", + selector: "example", autoBackfill: false, dependsOn: ["pp-atif"], }; @@ -189,7 +189,7 @@ describe("createHttpServer", () => { }); expect(response.status).toBe(200); - expect(await response.json()).toEqual({ ok: true, handlerId: "pp-taxonomy" }); + expect(await response.json()).toEqual({ ok: true, handlerId: "pp-example" }); expect(handler.registerHandler).toHaveBeenCalledWith(doc); }); diff --git a/apps/workers/taxonomy/Dockerfile b/apps/workers/readout/Dockerfile similarity index 60% rename from apps/workers/taxonomy/Dockerfile rename to apps/workers/readout/Dockerfile index b6be8f2ad..dc615f72c 100644 --- a/apps/workers/taxonomy/Dockerfile +++ b/apps/workers/readout/Dockerfile @@ -7,22 +7,22 @@ WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ COPY tsconfig.base.json ./ COPY packages/shared/package.json ./packages/shared/ -COPY apps/workers/taxonomy/package.json ./apps/workers/taxonomy/ +COPY apps/workers/readout/package.json ./apps/workers/readout/ RUN pnpm install --frozen-lockfile COPY packages/shared/tsconfig.json ./packages/shared/ COPY packages/shared/src ./packages/shared/src/ -COPY apps/workers/taxonomy/tsconfig.json ./apps/workers/taxonomy/ -COPY apps/workers/taxonomy/src ./apps/workers/taxonomy/src/ -COPY apps/workers/taxonomy/handler.yaml ./apps/workers/taxonomy/ +COPY apps/workers/readout/tsconfig.json ./apps/workers/readout/ +COPY apps/workers/readout/src ./apps/workers/readout/src/ +COPY apps/workers/readout/handler.yaml ./apps/workers/readout/ -RUN pnpm --filter shared build && pnpm --filter taxonomy-handler build +RUN pnpm --filter shared build && pnpm --filter readout-handler build # --- Dev stage: hot reload with tsx watch --- FROM builder AS dev ENV NODE_ENV=development -ENV SERVICE_DIR=workers/taxonomy +ENV SERVICE_DIR=workers/readout COPY scripts/dev-entrypoint.sh /app/scripts/dev-entrypoint.sh EXPOSE 80 ENV PORT=80 @@ -37,14 +37,14 @@ WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ COPY packages/shared/package.json ./packages/shared/ -COPY apps/workers/taxonomy/package.json ./apps/workers/taxonomy/ +COPY apps/workers/readout/package.json ./apps/workers/readout/ RUN pnpm install --frozen-lockfile --prod COPY --from=builder /app/packages/shared/dist ./packages/shared/dist/ -COPY --from=builder /app/apps/workers/taxonomy/dist ./apps/workers/taxonomy/dist/ -COPY --from=builder /app/apps/workers/taxonomy/handler.yaml ./apps/workers/taxonomy/handler.yaml +COPY --from=builder /app/apps/workers/readout/dist ./apps/workers/readout/dist/ +COPY --from=builder /app/apps/workers/readout/handler.yaml ./apps/workers/readout/handler.yaml -WORKDIR /app/apps/workers/taxonomy +WORKDIR /app/apps/workers/readout CMD ["node", "dist/index.js"] diff --git a/apps/workers/readout/handler.yaml b/apps/workers/readout/handler.yaml new file mode 100644 index 000000000..967490565 --- /dev/null +++ b/apps/workers/readout/handler.yaml @@ -0,0 +1,12 @@ +# Submission handler registration metadata. +# Read by the generic register-handler script and POSTed to the +# scheduler's POST /handlers/register endpoint on deploy. +_id: pp-readout +type: submission-handler +version: 1 +queue: pp-readout-queue +selector: readout +autoBackfill: false +dependsOn: + - pp-observations +dependencyMode: all-runs diff --git a/apps/workers/taxonomy/package.json b/apps/workers/readout/package.json similarity index 76% rename from apps/workers/taxonomy/package.json rename to apps/workers/readout/package.json index 52cdd6b2f..8e4b87b7f 100644 --- a/apps/workers/taxonomy/package.json +++ b/apps/workers/readout/package.json @@ -1,5 +1,5 @@ { - "name": "taxonomy-handler", + "name": "readout-handler", "version": "1.0.0", "type": "module", "main": "dist/index.js", @@ -7,14 +7,14 @@ "build": "tsc", "dev": "tsx watch src/index.ts", "start": "node dist/index.js", - "taxonomy": "tsx src/cli.ts", "lint": "eslint src/" }, "dependencies": { "shared": "workspace:*", "@azure/storage-queue": "^12.25.0", - "@github/copilot-sdk": "^0.1.17", - "dotenv": "^16.4.5" + "@github/copilot-sdk": "1.0.1", + "dotenv": "^16.4.5", + "zod": "^4.3.6" }, "devDependencies": { "@types/node": "^22.7.0", diff --git a/apps/workers/readout/src/index.ts b/apps/workers/readout/src/index.ts new file mode 100644 index 000000000..e9ed516eb --- /dev/null +++ b/apps/workers/readout/src/index.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import "dotenv/config"; +import { + ReadoutQueueProcessor, + type ReadoutQueueProcessorConfig, +} from "./readout-queue-processor.js"; + +const config: ReadoutQueueProcessorConfig = { + mongoUri: + process.env.MONGO_CONNECTION_STRING || + process.env.AZURE_COSMOS_CONNECTION_STRING || + "mongodb://localhost:27017", + mongoDatabase: process.env.MONGO_DATABASE || "requests-db", + mongoCollection: "submissions", + storageAccountName: + process.env.AZURE_STORAGE_ACCOUNT_NAME || "devstoreaccount1", + storageConnectionString: + process.env.STORAGE_CONNECTION_STRING || + process.env.AZURE_STORAGE_CONNECTION_STRING || + undefined, + queueName: process.env.AZURE_STORAGE_QUEUE_READOUT || "pp-readout-queue", + batchSize: Number(process.env.BATCH_SIZE) || 1, + pollIntervalMs: Number(process.env.POLL_INTERVAL_MS) || 5000, + redisHost: process.env.REDIS_HOST || "localhost", + redisPort: Number(process.env.REDIS_PORT) || 6379, + redisPassword: process.env.REDIS_PASSWORD || "", + model: process.env.READOUT_MODEL || "gpt-5.4", + sessionTimeoutMs: + Number(process.env.READOUT_SESSION_TIMEOUT_MS) || 5 * 60 * 1000, + tokenManagerUrl: process.env.TOKEN_MANAGER_URL, +}; + +const processor = new ReadoutQueueProcessor(config); +processor.start().catch((error) => { + console.error("[readout-handler] Fatal error:", error); + process.exit(1); +}); diff --git a/apps/workers/readout/src/prompt.ts b/apps/workers/readout/src/prompt.ts new file mode 100644 index 000000000..0841817b8 --- /dev/null +++ b/apps/workers/readout/src/prompt.ts @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { SubmissionEvidence } from "./readout-builder.js"; + +export const READOUT_SYSTEM_PROMPT = `You synthesize a submission-level benchmark readout from an exact immutable vector of Scope runs. + +The user message contains JSON evidence for every request/run pair in that vector: request metadata, coding outcome, profile metadata, criteria definitions, run-level and per-iteration observations, and ATIF trajectories. + +Return raw JSON only with this shape: +{ + "title": "string", + "executiveSummary": "string", + "profiles": [ + { "profileId": "optional string", "requestIds": ["string"], "summary": "string" } + ], + "runs": [ + { "requestId": "string", "runId": "string", "trajectoryHighlights": ["string"] } + ], + "findings": [ + { + "id": "string", + "title": "string", + "description": "string", + "severity": "critical|high|medium|low|info", + "evidence": [ + { + "requestId": "string", + "runId": "string", + "iteration": 0, + "criterionId": "optional string", + "excerpt": "string" + } + ] + } + ], + "actions": [ + { + "id": "string", + "title": "string", + "priority": "critical|high|medium|low", + "description": "string", + "relatedFindingIds": ["string"] + } + ] +} + +Rules: +- Use only requestId, runId, iteration, criterionId, and profileId values present in the evidence. +- Treat failed coding outcomes as valid evidence. +- Treat observation results with passed:false as evidence, not as missing data. +- An empty selected-observation set is valid; summarize trajectory and outcome evidence without inventing observations. +- Include every exact-vector run once in runs and every request once in a profile group. +- Preserve disagreements and mixed results instead of averaging them away. +- Ground every finding in at least one evidence reference. +- Return no Markdown fences or commentary.`; + +export function composeReadoutUserPrompt(evidence: SubmissionEvidence): string { + return [ + "Generate the submission readout synthesis for this exact immutable evidence vector.", + JSON.stringify(evidence), + ].join("\n\n"); +} diff --git a/apps/workers/readout/src/readout-builder.test.ts b/apps/workers/readout/src/readout-builder.test.ts new file mode 100644 index 000000000..fa377becb --- /dev/null +++ b/apps/workers/readout/src/readout-builder.test.ts @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, expect, it } from "vitest"; +import type { + CriteriaDocument, + RequestDocument, + RunState, +} from "shared"; +import { + composeSubmissionReadout, + renderSubmissionReadoutMarkdown, + type SubmissionEvidence, +} from "./readout-builder.js"; + +function evidence(): SubmissionEvidence { + const criterion: CriteriaDocument = { + id: "uses-current-api", + projectId: "project-1", + prompt: "Uses the current API", + kind: "observation", + subject: "run", + taxonomyElementId: "dimension:dependency-currency", + createdAt: new Date(), + }; + const request = { + _id: "request-1", + projectId: "project-1", + workerType: "copilot", + profileId: "profile-1", + profileVersionId: "profile-version-1", + observations: [criterion.id], + scenario: { task: "Update the API", criteria: [] }, + createdAt: new Date(), + priority: 0, + } satisfies RequestDocument; + const run = { + _id: "run-1", + attemptNumber: 1, + status: "done", + outcome: "failed", + observationResults: [{ + criterionId: criterion.id, + passed: false, + feedback: "The deprecated API remained.", + evaluated: true, + }], + turns: [{ + iteration: 1, + judgeFeedback: "", + snapshotUrl: "https://example.test/snapshot", + passed: false, + timestamp: new Date("2026-10-13T12:00:00.000Z"), + observationResults: [{ + criterionId: criterion.id, + passed: true, + feedback: "The first iteration used the current API.", + evaluated: true, + }], + }], + } satisfies RunState; + return { + submissionId: "submission-1", + submissionRunId: "submission-run-1", + runs: [{ request, run, criteria: [criterion], trajectories: [] }], + }; +} + +describe("submission readout composition", () => { + it("preserves mixed run/iteration observations, profiles, and provenance", () => { + const readout = composeSubmissionReadout(evidence(), { + title: "Submission readout", + executiveSummary: "The run failed after mixed observation evidence.", + profiles: [{ + profileId: "profile-1", + requestIds: ["request-1"], + summary: "One profiled run.", + }], + runs: [{ + requestId: "request-1", + runId: "run-1", + trajectoryHighlights: ["The agent changed approach between iterations."], + }], + findings: [{ + id: "finding-1", + title: "Regression", + description: "The final state regressed.", + severity: "high", + evidence: [{ + requestId: "request-1", + runId: "run-1", + iteration: 1, + criterionId: "uses-current-api", + excerpt: "The deprecated API remained.", + }], + }], + actions: [{ + id: "action-1", + title: "Prevent regression", + priority: "high", + description: "Add a final-state check.", + relatedFindingIds: ["finding-1"], + }], + }, new Date("2026-10-13T12:00:00.000Z")); + + expect(readout.runs[0].observations).toEqual([ + expect.objectContaining({ subject: "run", passed: false }), + expect.objectContaining({ subject: "iteration", passed: true }), + ]); + expect(readout.findings[0].evidence[0]).toMatchObject({ + submissionRunId: "submission-run-1", + requestId: "request-1", + runId: "run-1", + }); + expect(readout.profiles).toEqual([{ + profileId: "profile-1", + requestIds: ["request-1"], + summary: "One profiled run.", + }]); + expect(readout.summary.failedRuns).toBe(1); + expect(readout.summary.passedObservations).toBe(1); + expect(readout.summary.failedObservations).toBe(1); + expect(renderSubmissionReadoutMarkdown(readout)).toContain( + "Schema: `scope.submission-readout@1.0`", + ); + }); + + it("rejects evidence outside the exact vector", () => { + expect(() => composeSubmissionReadout(evidence(), { + title: "Submission readout", + executiveSummary: "Invalid evidence.", + profiles: [{ requestIds: ["request-1"], summary: "One run." }], + runs: [{ + requestId: "request-1", + runId: "run-1", + trajectoryHighlights: [], + }], + findings: [{ + id: "finding-1", + title: "Invalid", + description: "References a newer run.", + severity: "high", + evidence: [{ + requestId: "request-1", + runId: "run-newer", + excerpt: "Not in the vector.", + }], + }], + actions: [], + })).toThrow("non-vector run"); + }); + + it("rejects findings that cite a missing iteration", () => { + expect(() => composeSubmissionReadout(evidence(), { + title: "Submission readout", + executiveSummary: "Invalid iteration evidence.", + profiles: [], + runs: [], + findings: [{ + id: "finding-1", + title: "Invalid", + description: "References a missing iteration.", + severity: "high", + evidence: [{ + requestId: "request-1", + runId: "run-1", + iteration: 2, + excerpt: "Not in the vector.", + }], + }], + actions: [], + })).toThrow("unknown iteration 2"); + }); + + it("canonically retains every exact-vector profile when synthesis omits it", () => { + const readout = composeSubmissionReadout(evidence(), { + title: "Submission readout", + executiveSummary: "Profile synthesis was omitted.", + profiles: [], + runs: [], + findings: [], + actions: [], + }); + + expect(readout.profiles).toEqual([{ + profileId: "profile-1", + requestIds: ["request-1"], + summary: "1 exact-vector run.", + }]); + }); +}); diff --git a/apps/workers/readout/src/readout-builder.ts b/apps/workers/readout/src/readout-builder.ts new file mode 100644 index 000000000..0143188b9 --- /dev/null +++ b/apps/workers/readout/src/readout-builder.ts @@ -0,0 +1,303 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { z } from "zod"; +import { + SUBMISSION_READOUT_SCHEMA_ID, + SUBMISSION_READOUT_SCHEMA_VERSION, + submissionReadoutSchema, + type CriteriaDocument, + type CriterionResult, + type RequestDocument, + type RunState, + type SubmissionReadout, +} from "shared"; + +export interface ExactRunEvidence { + request: RequestDocument; + run: RunState; + criteria: CriteriaDocument[]; + trajectories: Array<{ iteration: number; trajectory: unknown }>; +} + +export interface SubmissionEvidence { + submissionId: string; + submissionRunId: string; + runs: ExactRunEvidence[]; +} + +const evidenceReferenceSchema = z.object({ + requestId: z.string().min(1), + runId: z.string().min(1), + iteration: z.number().int().min(0).optional(), + criterionId: z.string().min(1).optional(), + excerpt: z.string().min(1), +}); + +export const readoutSynthesisSchema = z.object({ + title: z.string().min(1), + executiveSummary: z.string().min(1), + profiles: z.array(z.object({ + profileId: z.string().optional(), + requestIds: z.array(z.string().min(1)).min(1), + summary: z.string().min(1), + })), + runs: z.array(z.object({ + requestId: z.string().min(1), + runId: z.string().min(1), + trajectoryHighlights: z.array(z.string().min(1)), + })), + findings: z.array(z.object({ + id: z.string().min(1), + title: z.string().min(1), + description: z.string().min(1), + severity: z.enum(["critical", "high", "medium", "low", "info"]), + evidence: z.array(evidenceReferenceSchema).min(1), + })), + actions: z.array(z.object({ + id: z.string().min(1), + title: z.string().min(1), + priority: z.enum(["critical", "high", "medium", "low"]), + description: z.string().min(1), + relatedFindingIds: z.array(z.string().min(1)), + })), +}); + +export type ReadoutSynthesis = z.infer; + +function observationsForRun(evidence: ExactRunEvidence) { + const criteriaById = new Map(evidence.criteria.map((criterion) => [criterion.id, criterion])); + const mapResult = ( + result: CriterionResult, + subject: "run" | "iteration", + ) => { + const criterion = criteriaById.get(result.criterionId); + return { + criterionId: result.criterionId, + ...(criterion?.taxonomyElementId + ? { taxonomyElementId: criterion.taxonomyElementId } + : {}), + subject, + passed: result.passed, + feedback: result.feedback, + }; + }; + return [ + ...(evidence.run.observationResults ?? []).map((result) => + mapResult(result, "run") + ), + ...(evidence.run.turns ?? []).flatMap((turn) => + (turn.observationResults ?? []).map((result) => + mapResult(result, "iteration") + ) + ), + ]; +} + +export function composeSubmissionReadout( + evidence: SubmissionEvidence, + synthesis: ReadoutSynthesis, + generatedAt = new Date(), +): SubmissionReadout { + const exactRuns = new Map( + evidence.runs.map((entry) => [ + `${entry.request._id}:${entry.run._id}`, + entry, + ]), + ); + const highlights = new Map( + synthesis.runs.map((entry) => [ + `${entry.requestId}:${entry.runId}`, + entry.trajectoryHighlights, + ]), + ); + for (const key of highlights.keys()) { + if (!exactRuns.has(key)) { + throw new Error(`Readout synthesis referenced non-vector run ${key}`); + } + } + + const criterionIds = new Set( + evidence.runs.flatMap((entry) => entry.criteria.map((criterion) => criterion.id)), + ); + for (const finding of synthesis.findings) { + for (const reference of finding.evidence) { + const key = `${reference.requestId}:${reference.runId}`; + const exactRun = exactRuns.get(key); + if (!exactRun) { + throw new Error(`Readout finding ${finding.id} referenced non-vector run ${key}`); + } + if ( + reference.iteration !== undefined && + !(exactRun.run.turns ?? []).some( + (turn) => turn.iteration === reference.iteration, + ) + ) { + throw new Error( + `Readout finding ${finding.id} referenced unknown iteration ${reference.iteration} for ${key}`, + ); + } + if (reference.criterionId && !criterionIds.has(reference.criterionId)) { + throw new Error( + `Readout finding ${finding.id} referenced unknown criterion ${reference.criterionId}`, + ); + } + } + } + + const runs = evidence.runs.map((entry) => ({ + requestId: entry.request._id, + runId: entry.run._id, + task: entry.request.scenario.task, + workerType: entry.request.workerType, + ...(entry.request.model ? { model: entry.request.model } : {}), + ...(entry.request.profileId ? { profileId: entry.request.profileId } : {}), + ...(entry.request.profileVersionId + ? { profileVersionId: entry.request.profileVersionId } + : {}), + status: entry.run.status, + ...(entry.run.outcome ? { outcome: entry.run.outcome } : {}), + observations: observationsForRun(entry), + trajectoryHighlights: + highlights.get(`${entry.request._id}:${entry.run._id}`) ?? [], + })); + const allObservations = runs.flatMap((run) => run.observations); + const synthesisProfiles = new Map( + synthesis.profiles.map((profile) => [profile.profileId ?? "", profile]), + ); + const requestIdsByProfile = new Map(); + for (const entry of evidence.runs) { + const key = entry.request.profileId ?? ""; + requestIdsByProfile.set(key, [ + ...(requestIdsByProfile.get(key) ?? []), + entry.request._id, + ]); + } + const profiles = [...requestIdsByProfile].map(([profileId, requestIds]) => ({ + ...(profileId ? { profileId } : {}), + requestIds, + summary: + synthesisProfiles.get(profileId)?.summary ?? + `${requestIds.length} exact-vector run${requestIds.length === 1 ? "" : "s"}.`, + })); + const findingIds = new Set(synthesis.findings.map((finding) => finding.id)); + for (const action of synthesis.actions) { + const unknown = action.relatedFindingIds.find((id) => !findingIds.has(id)); + if (unknown) { + throw new Error(`Readout action ${action.id} referenced unknown finding ${unknown}`); + } + } + + return submissionReadoutSchema.parse({ + meta: { + schemaId: SUBMISSION_READOUT_SCHEMA_ID, + schemaVersion: SUBMISSION_READOUT_SCHEMA_VERSION, + generatedAt: generatedAt.toISOString(), + submissionId: evidence.submissionId, + submissionRunId: evidence.submissionRunId, + }, + summary: { + title: synthesis.title, + executiveSummary: synthesis.executiveSummary, + totalRuns: runs.length, + succeededRuns: runs.filter((run) => run.outcome === "succeeded").length, + failedRuns: runs.filter((run) => run.outcome === "failed").length, + passedObservations: allObservations.filter((result) => result.passed).length, + failedObservations: allObservations.filter((result) => !result.passed).length, + }, + profiles, + runs, + findings: synthesis.findings.map((finding) => ({ + ...finding, + evidence: finding.evidence.map((reference) => ({ + submissionRunId: evidence.submissionRunId, + ...reference, + })), + })), + actions: synthesis.actions, + }); +} + +export function renderSubmissionReadoutMarkdown(readout: SubmissionReadout): string { + const lines = [ + `# ${readout.summary.title}`, + "", + readout.summary.executiveSummary, + "", + "## Summary", + "", + `- Runs: ${readout.summary.totalRuns}`, + `- Succeeded: ${readout.summary.succeededRuns}`, + `- Failed: ${readout.summary.failedRuns}`, + `- Passed observations: ${readout.summary.passedObservations}`, + `- Failed observations: ${readout.summary.failedObservations}`, + "", + "## Runs", + "", + ]; + for (const run of readout.runs) { + lines.push( + `### ${run.task || run.requestId}`, + "", + `- Request: \`${run.requestId}\``, + `- Run: \`${run.runId}\``, + `- Status: ${run.status}${run.outcome ? ` / ${run.outcome}` : ""}`, + `- Profile: ${run.profileId ?? "none"}`, + "", + ); + if (run.observations.length > 0) { + lines.push("| Observation | Subject | Result | Evidence |", "| --- | --- | --- | --- |"); + for (const observation of run.observations) { + lines.push( + `| \`${observation.criterionId}\` | ${observation.subject} | ${observation.passed ? "Passed" : "Failed"} | ${observation.feedback.replaceAll("|", "\\|")} |`, + ); + } + lines.push(""); + } + if (run.trajectoryHighlights.length > 0) { + lines.push( + "**Trajectory highlights**", + "", + ...run.trajectoryHighlights.map((highlight) => `- ${highlight}`), + "", + ); + } + } + lines.push("## Findings", ""); + if (readout.findings.length === 0) { + lines.push("No cross-run findings were identified.", ""); + } else { + for (const finding of readout.findings) { + lines.push( + `### ${finding.title}`, + "", + `**Severity:** ${finding.severity}`, + "", + finding.description, + "", + ...finding.evidence.map( + (evidence) => + `- \`${evidence.requestId}/${evidence.runId}\`${evidence.iteration === undefined ? "" : ` iteration ${evidence.iteration}`}: ${evidence.excerpt}`, + ), + "", + ); + } + } + lines.push("## Actions", ""); + if (readout.actions.length === 0) { + lines.push("No follow-up actions were generated.", ""); + } else { + for (const action of readout.actions) { + lines.push( + `- **${action.title}** (${action.priority}): ${action.description}`, + ); + } + lines.push(""); + } + lines.push( + "---", + `Schema: \`${readout.meta.schemaId}@${readout.meta.schemaVersion}\``, + `Submission run: \`${readout.meta.submissionRunId}\``, + ); + return lines.join("\n"); +} diff --git a/apps/workers/readout/src/readout-generator.ts b/apps/workers/readout/src/readout-generator.ts new file mode 100644 index 000000000..51cc320d0 --- /dev/null +++ b/apps/workers/readout/src/readout-generator.ts @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { CopilotClient, type PermissionHandler, SessionEvent } from "@github/copilot-sdk"; +import type { LogEvent } from "shared"; +import { + composeSubmissionReadout, + readoutSynthesisSchema, + type SubmissionEvidence, +} from "./readout-builder.js"; +import { + READOUT_SYSTEM_PROMPT, + composeReadoutUserPrompt, +} from "./prompt.js"; + +const approveNoTools: PermissionHandler = () => ({ + kind: "denied-by-rules", + rules: [], +}); + +type ReadoutLog = ( + level: LogEvent["level"], + message: string, + data?: Record, +) => Promise | void; + +export async function generateSubmissionReadout(input: { + evidence: SubmissionEvidence; + model: string; + githubToken: string; + timeoutMs: number; + log: ReadoutLog; +}) { + const client = new CopilotClient({ gitHubToken: input.githubToken }); + let latestMessage = ""; + let streamedResponse = ""; + try { + const session = await client.createSession({ + model: input.model, + streaming: true, + availableTools: [], + excludedTools: ["builtin:*", "mcp:*"], + onPermissionRequest: approveNoTools, + systemMessage: { mode: "replace", content: READOUT_SYSTEM_PROMPT }, + }); + session.on((event: SessionEvent) => { + if (event.type === "assistant.message_delta") { + streamedResponse += event.data.deltaContent; + } else if (event.type === "assistant.message") { + latestMessage = event.data.content; + } else if (event.type === "session.error") { + void input.log("error", event.data.message, { + errorType: event.data.errorType, + }); + } + }); + + let prompt = composeReadoutUserPrompt(input.evidence); + for (let attempt = 1; attempt <= 3; attempt += 1) { + streamedResponse = ""; + latestMessage = ""; + await input.log("info", `Generating submission readout (${attempt}/3)`); + const response = await session.sendAndWait({ prompt }, input.timeoutMs); + const text = + response?.data.content?.trim() || + latestMessage.trim() || + streamedResponse.trim(); + if (!text) throw new Error("Copilot SDK returned an empty readout response"); + const jsonText = text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, ""); + let parsed: unknown; + try { + parsed = JSON.parse(jsonText) as unknown; + } catch (error) { + if (attempt === 3) { + throw new Error(`Readout response was not valid JSON: ${String(error)}`); + } + prompt = "Return the complete corrected readout synthesis as raw JSON only."; + continue; + } + const result = readoutSynthesisSchema.safeParse(parsed); + if (result.success) { + return composeSubmissionReadout(input.evidence, result.data); + } + if (attempt === 3) { + throw new Error( + `Readout synthesis failed schema validation: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`, + ); + } + prompt = [ + "Return the complete corrected readout synthesis as raw JSON only.", + `Validation errors: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`, + ].join("\n"); + } + throw new Error("Readout generation ended without a valid result"); + } finally { + await client.stop().catch(() => undefined); + } +} diff --git a/apps/workers/readout/src/readout-queue-processor.test.ts b/apps/workers/readout/src/readout-queue-processor.test.ts new file mode 100644 index 000000000..6d842073a --- /dev/null +++ b/apps/workers/readout/src/readout-queue-processor.test.ts @@ -0,0 +1,274 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { DequeuedMessageItem } from "@azure/storage-queue"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { SubmissionDocument, VisibilityHeartbeat } from "shared"; +import type { RequestDocument, RunHistoryDocument } from "shared"; + +const coordination = vi.hoisted(() => ({ + claim: vi.fn(), + complete: vi.fn(), + isCurrent: vi.fn(), +})); +const generate = vi.hoisted(() => vi.fn()); + +vi.mock("shared", async (importOriginal) => ({ + ...(await importOriginal()), + claimSubmissionHandler: coordination.claim, + completeSubmissionHandler: coordination.complete, + isCurrentSubmissionRun: coordination.isCurrent, +})); +vi.mock("./readout-generator.js", () => ({ + generateSubmissionReadout: generate, +})); + +import { ReadoutQueueProcessor } from "./readout-queue-processor.js"; + +const submission: SubmissionDocument = { + _id: "submission-1", + submissionId: "submission-1", + projectId: "project-1", + requestIds: ["request-1"], + run: { + _id: "submission-run-1", + attemptNumber: 1, + vectorFingerprint: "request-1:run-1", + requests: [{ requestId: "request-1", runId: "run-1" }], + dependencyProjection: [], + handlerStatus: { + "pp-readout": { + version: 1, + status: "processing", + updatedAt: new Date("2026-10-13T12:00:00.000Z"), + }, + }, + blocking: [], + createdAt: new Date("2026-10-13T12:00:00.000Z"), + startedAt: new Date("2026-10-13T12:00:00.000Z"), + updatedAt: new Date("2026-10-13T12:00:00.000Z"), + }, + createdAt: new Date("2026-10-13T12:00:00.000Z"), + updatedAt: new Date("2026-10-13T12:00:00.000Z"), +}; + +function processor() { + const instance = new ReadoutQueueProcessor({ + mongoUri: "mongodb://localhost:27017", + mongoDatabase: "scope", + mongoCollection: "submissions", + storageAccountName: "devstoreaccount1", + storageConnectionString: "UseDevelopmentStorage=true", + queueName: "pp-readout-queue", + batchSize: 1, + pollIntervalMs: 100, + redisHost: "localhost", + redisPort: 6379, + redisPassword: "", + model: "test-model", + sessionTimeoutMs: 1_000, + }); + const insertOne = vi.fn().mockResolvedValue({ acknowledged: true }); + Object.assign(instance as object, { + collection: {}, + db: { + collection: vi.fn((name: string) => + name === "reports" ? { insertOne } : {} + ), + }, + loadEvidence: vi.fn().mockResolvedValue({ + submissionId: submission._id, + submissionRunId: submission.run._id, + runs: [], + }), + tokenClient: { + acquireToken: vi.fn().mockResolvedValue("token"), + }, + blobStorage: { + uploadJson: vi.fn().mockImplementation((path: string) => + Promise.resolve(`https://storage.example/${path}`) + ), + }, + safeDeleteMessage: vi.fn().mockResolvedValue(undefined), + }); + return { instance, insertOne }; +} + +const message = { messageId: "message-1" } as DequeuedMessageItem; +const heartbeat = { + stop: vi.fn().mockResolvedValue(undefined), +} as unknown as VisibilityHeartbeat; +const payload = { + type: "readout", + submissionId: submission._id, + submissionRunId: submission.run._id, +}; +type LogFn = ReturnType; +type ReadoutPayload = typeof payload; + +async function handle(instance: ReadoutQueueProcessor, log: LogFn = vi.fn()) { + await (instance as unknown as { + handleRequest( + submission: SubmissionDocument, + message: DequeuedMessageItem, + heartbeat: VisibilityHeartbeat, + log: LogFn, + payload: ReadoutPayload, + ): Promise; + }).handleRequest(submission, message, heartbeat, log, payload); +} + +describe("readout queue orchestration", () => { + beforeEach(() => { + vi.clearAllMocks(); + coordination.isCurrent.mockResolvedValue(true); + generate.mockResolvedValue({ + meta: { + schemaId: "scope.submission-readout", + schemaVersion: "1.0", + generatedAt: "2026-10-13T12:00:00.000Z", + submissionId: submission._id, + submissionRunId: submission.run._id, + }, + summary: { + title: "Submission readout", + executiveSummary: "Summary", + totalRuns: 0, + succeededRuns: 0, + failedRuns: 0, + passedObservations: 0, + failedObservations: 0, + }, + profiles: [], + runs: [], + findings: [], + actions: [], + }); + }); + + it("discards stale or duplicate exact-run messages before generation", async () => { + coordination.claim.mockResolvedValue(null); + const { instance, insertOne } = processor(); + + await handle(instance); + + expect(generate).not.toHaveBeenCalled(); + expect(insertOne).not.toHaveBeenCalled(); + expect((instance as unknown as { safeDeleteMessage: ReturnType }) + .safeDeleteMessage).toHaveBeenCalledOnce(); + }); + + it("keeps superseded completions historical and uses new report ids on retry", async () => { + coordination.claim.mockResolvedValue(submission); + coordination.complete.mockResolvedValue(false); + const { instance, insertOne } = processor(); + const log = vi.fn().mockResolvedValue(undefined); + + await handle(instance, log); + await handle(instance, log); + + expect(insertOne).toHaveBeenCalledTimes(2); + const reports = insertOne.mock.calls.map(([report]) => report); + expect(reports[0]._id).not.toBe(reports[1]._id); + expect(reports).toEqual([ + expect.objectContaining({ + submissionId: submission._id, + submissionRunId: submission.run._id, + }), + expect.objectContaining({ + submissionId: submission._id, + submissionRunId: submission.run._id, + }), + ]); + expect(coordination.complete).toHaveBeenCalledTimes(2); + expect(log).toHaveBeenCalledWith( + "warn", + "Readout report remained historical after supersession", + expect.objectContaining({ + submissionId: submission._id, + submissionRunId: submission.run._id, + }), + ); + }); + + it("does not publish when the submission is superseded during upload", async () => { + coordination.claim.mockResolvedValue(submission); + coordination.isCurrent + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(false); + const { instance, insertOne } = processor(); + + await handle(instance); + + expect(generate).toHaveBeenCalledOnce(); + expect(insertOne).not.toHaveBeenCalled(); + expect(coordination.complete).not.toHaveBeenCalled(); + }); + + it("loads the archived run named by the immutable exact vector", async () => { + const { instance } = processor(); + delete (instance as unknown as Record).loadEvidence; + const currentRequest = { + _id: "request-1", + projectId: "project-1", + workerType: "coder-acp-copilot", + observations: [], + scenario: { task: "Task", criteria: [] }, + createdAt: new Date("2026-10-13T12:00:00.000Z"), + priority: 0, + run: { + _id: "run-newer", + attemptNumber: 2, + status: "done", + outcome: "succeeded", + }, + } satisfies RequestDocument; + const archivedRun = { + _id: "run-1", + requestId: "request-1", + projectId: "project-1", + attemptNumber: 1, + status: "done", + outcome: "failed", + } satisfies RunHistoryDocument; + const historyFindOne = vi.fn().mockResolvedValue(archivedRun); + Object.assign(instance as object, { + db: { + collection: vi.fn((name: string) => { + if (name === "requests") { + return { + find: vi.fn(() => ({ + toArray: vi.fn().mockResolvedValue([currentRequest]), + })), + }; + } + if (name === "runs") return { findOne: historyFindOne }; + if (name === "criteria") { + return { + find: vi.fn(() => ({ + toArray: vi.fn().mockResolvedValue([]), + })), + }; + } + return {}; + }), + }, + }); + + const evidence = await (instance as unknown as { + loadEvidence(value: SubmissionDocument): Promise<{ + runs: Array<{ run: RunHistoryDocument | RequestDocument["run"] }>; + }>; + }).loadEvidence(submission); + + expect(historyFindOne).toHaveBeenCalledWith({ + _id: "run-1", + requestId: "request-1", + projectId: "project-1", + }); + expect(evidence.runs[0].run?._id).toBe("run-1"); + }); +}); diff --git a/apps/workers/readout/src/readout-queue-processor.ts b/apps/workers/readout/src/readout-queue-processor.ts new file mode 100644 index 000000000..3d0f52fd6 --- /dev/null +++ b/apps/workers/readout/src/readout-queue-processor.ts @@ -0,0 +1,303 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { DequeuedMessageItem } from "@azure/storage-queue"; +import { randomUUID } from "node:crypto"; +import { + BaseQueueProcessor, + BlobStorage, + type BaseQueueProcessorConfig, + type CriteriaDocument, + type LogEvent, + type ReportDocument, + type RequestDocument, + type RunHistoryDocument, + type SubmissionDocument, + type SubmissionQueueMessagePayload, + TokenManagerClient, + claimSubmissionHandler, + completeSubmissionHandler, + isCurrentSubmissionRun, + SUBMISSION_READOUT_SCHEMA_ID, + SUBMISSION_READOUT_SCHEMA_VERSION, + type VisibilityHeartbeat, + withRetry, +} from "shared"; +import { + renderSubmissionReadoutMarkdown, + type ExactRunEvidence, + type SubmissionEvidence, +} from "./readout-builder.js"; +import { generateSubmissionReadout } from "./readout-generator.js"; + +const HANDLER_ID = "pp-readout"; +const HANDLER_VERSION = 1; + +export interface ReadoutQueueProcessorConfig extends BaseQueueProcessorConfig { + model: string; + sessionTimeoutMs: number; + tokenManagerUrl?: string; +} + +function isReadoutMessage( + payload: Record | undefined, +): payload is Record & SubmissionQueueMessagePayload { + return payload?.type === "readout" && + typeof payload.submissionId === "string" && + typeof payload.submissionRunId === "string"; +} + +export class ReadoutQueueProcessor extends BaseQueueProcessor { + private readonly readoutConfig: ReadoutQueueProcessorConfig; + private readonly tokenClient: TokenManagerClient; + private readonly blobStorage: BlobStorage; + + constructor(config: ReadoutQueueProcessorConfig) { + super(config, "readout-handler"); + this.readoutConfig = config; + this.tokenClient = new TokenManagerClient(config.tokenManagerUrl); + this.blobStorage = new BlobStorage({ + storageAccountName: config.storageAccountName, + storageConnectionString: config.storageConnectionString, + }); + } + + protected override extractDocumentId(payload: Record): string { + return typeof payload.submissionId === "string" ? payload.submissionId : ""; + } + + protected override async handleRequest( + _submission: SubmissionDocument, + message: DequeuedMessageItem, + heartbeat: VisibilityHeartbeat, + log: ( + level: LogEvent["level"], + message: string, + data?: Record, + ) => Promise, + payload?: Record, + ): Promise { + if (!isReadoutMessage(payload)) { + await log("warn", "Discarding unsupported readout queue payload", { payload }); + await this.safeDeleteMessage(message.messageId, heartbeat.stop()); + return; + } + const { submissionId, submissionRunId } = payload; + const claimed = await withRetry(() => + claimSubmissionHandler({ + submissions: this.collection, + submissionId, + submissionRunId, + handlerId: HANDLER_ID, + version: HANDLER_VERSION, + }) + ); + if (!claimed) { + await log("info", "Discarding stale or duplicate readout message", { + submissionId, + submissionRunId, + }); + await this.safeDeleteMessage(message.messageId, heartbeat.stop()); + return; + } + + try { + if (!(await this.isCurrent(submissionId, submissionRunId))) return; + const evidence = await this.loadEvidence(claimed); + + if (!(await this.isCurrent(submissionId, submissionRunId))) return; + const githubToken = await this.tokenClient.acquireToken("copilot-sdk"); + + if (!(await this.isCurrent(submissionId, submissionRunId))) return; + const readout = await generateSubmissionReadout({ + evidence, + model: this.readoutConfig.model, + githubToken, + timeoutMs: this.readoutConfig.sessionTimeoutMs, + log, + }); + const markdown = renderSubmissionReadoutMarkdown(readout); + + if (!(await this.isCurrent(submissionId, submissionRunId))) return; + const reportId = randomUUID(); + const structuredUrl = await this.blobStorage.uploadJson( + `reports/${reportId}/structured.json`, + readout, + ); + + if (!(await this.isCurrent(submissionId, submissionRunId))) return; + const now = new Date(); + const report: ReportDocument = { + _id: reportId, + projectId: claimed.projectId, + submissionId, + submissionRunId, + reporter: { + id: HANDLER_ID, + name: "Submission Readout", + gitHash: process.env.GIT_COMMIT || "unknown", + model: this.readoutConfig.model, + agentId: "copilot-sdk", + agentVersion: process.env.COPILOT_SDK_VERSION || "unknown", + }, + content: markdown, + structuredArtifact: { + schemaId: SUBMISSION_READOUT_SCHEMA_ID, + schemaVersion: SUBMISSION_READOUT_SCHEMA_VERSION, + contentType: "application/json", + url: structuredUrl, + }, + status: "completed", + createdAt: now, + updatedAt: now, + }; + await withRetry(() => + this.db.collection("reports").insertOne(report) + ); + + const completed = await withRetry(() => + completeSubmissionHandler({ + submissions: this.collection, + submissionId, + submissionRunId, + handlerId: HANDLER_ID, + version: HANDLER_VERSION, + status: "done", + report: { + reportId, + url: `/api/v1/reports/${reportId}`, + updatedAt: now, + }, + }) + ); + if (!completed) { + await log("warn", "Readout report remained historical after supersession", { + submissionId, + submissionRunId, + reportId, + }); + } else { + await log("info", "Submission readout completed", { + submissionId, + submissionRunId, + reportId, + }); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + if (await this.isCurrent(submissionId, submissionRunId)) { + await withRetry(() => + completeSubmissionHandler({ + submissions: this.collection, + submissionId, + submissionRunId, + handlerId: HANDLER_ID, + version: HANDLER_VERSION, + status: "failed", + error: errorMessage, + }) + ); + } + await log("error", `Submission readout failed: ${errorMessage}`, { + submissionId, + submissionRunId, + }); + } finally { + await this.safeDeleteMessage(message.messageId, heartbeat.stop()); + } + } + + private isCurrent( + submissionId: string, + submissionRunId: string, + ): Promise { + return withRetry(() => + isCurrentSubmissionRun({ + submissions: this.collection, + submissionId, + submissionRunId, + }) + ); + } + + private async loadEvidence( + submission: SubmissionDocument, + ): Promise { + const requests = await withRetry(() => + this.db.collection("requests").find({ + _id: { $in: submission.run.requests.map((ref) => ref.requestId) }, + projectId: submission.projectId, + }).toArray() + ); + const requestById = new Map(requests.map((request) => [request._id, request])); + const history = this.db.collection("runs"); + const exactRuns: Array<{ request: RequestDocument; run: RunHistoryDocument | RequestDocument["run"] }> = []; + for (const ref of submission.run.requests) { + const request = requestById.get(ref.requestId); + if (!request) throw new Error(`Exact-vector request ${ref.requestId} was not found`); + const run = request.run?._id === ref.runId + ? request.run + : await withRetry(() => history.findOne({ + _id: ref.runId, + requestId: ref.requestId, + projectId: submission.projectId, + })); + if (!run) { + throw new Error(`Exact-vector run ${ref.requestId}/${ref.runId} was not found`); + } + exactRuns.push({ request, run }); + } + + const criterionIds = [ + ...new Set( + exactRuns.flatMap(({ request, run }) => [ + ...(request.observations ?? []), + ...(run?.observationResults ?? []).map((result) => result.criterionId), + ...(run?.turns ?? []).flatMap((turn) => + (turn.observationResults ?? []).map((result) => result.criterionId) + ), + ]), + ), + ]; + const criteria = criterionIds.length === 0 + ? [] + : await withRetry(() => + this.db.collection("criteria").find({ + projectId: submission.projectId, + id: { $in: criterionIds }, + }).toArray() + ); + const criteriaById = new Map(criteria.map((criterion) => [criterion.id, criterion])); + + const runs: ExactRunEvidence[] = []; + for (const { request, run } of exactRuns) { + if (!run) continue; + const trajectories = await Promise.all( + (run.turns ?? []).flatMap((turn) => + turn.atifUrl + ? [withRetry(() => + this.blobStorage.downloadJson(turn.atifUrl!) + ).then((trajectory) => ({ + iteration: turn.iteration, + trajectory, + }))] + : [], + ), + ); + runs.push({ + request, + run, + criteria: (request.observations ?? []).flatMap((id) => { + const criterion = criteriaById.get(id); + return criterion ? [criterion] : []; + }), + trajectories, + }); + } + return { + submissionId: submission._id, + submissionRunId: submission.run._id, + runs, + }; + } +} diff --git a/apps/workers/taxonomy/tsconfig.json b/apps/workers/readout/tsconfig.json similarity index 100% rename from apps/workers/taxonomy/tsconfig.json rename to apps/workers/readout/tsconfig.json diff --git a/apps/workers/report-generator/src/report-queue-processor.ts b/apps/workers/report-generator/src/report-queue-processor.ts index 7ecaac8f8..1a0523a97 100644 --- a/apps/workers/report-generator/src/report-queue-processor.ts +++ b/apps/workers/report-generator/src/report-queue-processor.ts @@ -88,6 +88,11 @@ export class ReportQueueProcessor extends BaseQueueProcessor { const generationStart = Date.now(); const reportId = doc._id; const requestId = doc.requestId; + if (!requestId) { + throw new Error( + `Report ${reportId} has no requestId and cannot be processed by report-generator`, + ); + } if (!ReportQueueProcessor.coldStartTracked) { ReportQueueProcessor.coldStartTracked = true; diff --git a/apps/workers/taxonomy/handler.yaml b/apps/workers/taxonomy/handler.yaml deleted file mode 100644 index b479f905e..000000000 --- a/apps/workers/taxonomy/handler.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Post-process handler registration metadata. -# Read by the generic register-handler script and POSTed to the -# scheduler's POST /handlers/register endpoint on deploy. -_id: pp-taxonomy -type: post-process-handler -version: 1 -queue: pp-taxonomy-queue -selector: taxonomy -autoBackfill: false -dependsOn: - - pp-atif diff --git a/apps/workers/taxonomy/src/cli.ts b/apps/workers/taxonomy/src/cli.ts deleted file mode 100644 index a5f51f600..000000000 --- a/apps/workers/taxonomy/src/cli.ts +++ /dev/null @@ -1,206 +0,0 @@ -#!/usr/bin/env npx tsx -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** - * Taxonomy post-processor developer CLI. - * - * Invokes the taxonomy generation loop directly against an existing requestId, - * streams the Copilot SDK session events live, and saves the validated taxonomy - * to a local JSON file. This is a developer/troubleshooting tool only — it never - * writes to Mongo (handlerStatus) or blob storage (taxonomy.json) and does not - * notify the scheduler. - * - * Usage: - * npx tsx src/cli.ts [options] - * pnpm taxonomy [options] - * - * Options: - * --run, -r Run id for the prompt/logging (default: active run from API). - * --model Override TAXONOMY_MODEL (e.g. a reasoning model). - * --timeout Override SESSION_TIMEOUT_MS (per-attempt session timeout). - * --output, -o Output file path (default: taxonomy-.json). - * --stdout Also print the taxonomy JSON to stdout. - * --quiet Suppress live session-event streaming on stderr. - * --help, -h Show this help. - */ -import "dotenv/config"; -import { writeFile } from "node:fs/promises"; -import { resolve } from "node:path"; -import { SessionEvent } from "@github/copilot-sdk"; -import { TokenManagerClient, type LogEvent, type RequestDocument } from "shared"; -import { generateTaxonomy } from "./taxonomy-generator.js"; - -interface CliArgs { - requestId: string; - runId?: string; - model?: string; - timeoutMs?: number; - output?: string; - stdout: boolean; - quiet: boolean; -} - -function printHelp(): void { - process.stderr.write( - [ - "Taxonomy post-processor developer CLI", - "", - "Usage: npx tsx src/cli.ts [options]", - "", - "Options:", - " --run, -r Run id for the prompt/logging (default: active run from API)", - " --model Override TAXONOMY_MODEL", - " --timeout Override SESSION_TIMEOUT_MS (per-attempt session timeout)", - " --output, -o Output file path (default: taxonomy-.json)", - " --stdout Also print the taxonomy JSON to stdout", - " --quiet Suppress live session-event streaming on stderr", - " --help, -h Show this help", - "", - ].join("\n"), - ); -} - -function parseArgs(argv: string[]): CliArgs | undefined { - const args: Partial & { stdout: boolean; quiet: boolean } = { stdout: false, quiet: false }; - const positionals: string[] = []; - - for (let i = 0; i < argv.length; i += 1) { - const arg = argv[i]; - switch (arg) { - case "--help": - case "-h": - printHelp(); - return undefined; - case "--run": - case "-r": - args.runId = argv[++i]; - break; - case "--model": - args.model = argv[++i]; - break; - case "--timeout": - args.timeoutMs = Number(argv[++i]); - break; - case "--output": - case "-o": - args.output = argv[++i]; - break; - case "--stdout": - args.stdout = true; - break; - case "--quiet": - args.quiet = true; - break; - default: - if (arg.startsWith("-")) { - throw new Error(`Unknown option: ${arg}`); - } - positionals.push(arg); - } - } - - const requestId = positionals[0]; - if (!requestId) { - throw new Error("Missing required argument"); - } - if (args.timeoutMs !== undefined && Number.isNaN(args.timeoutMs)) { - throw new Error("--timeout must be a number (milliseconds)"); - } - - return { ...args, requestId }; -} - -async function resolveRunId(apiBaseUrl: string, requestId: string, explicit?: string): Promise { - if (explicit) return explicit; - try { - const response = await fetch(`${apiBaseUrl}/api/v1/requests/${requestId}`); - if (response.ok) { - const request: RequestDocument = await response.json(); - if (request.run?._id) return request.run._id; - } - } catch { - // fall through to requestId fallback - } - return requestId; -} - -function summarizeEvent(event: SessionEvent): string | undefined { - switch (event.type) { - case "session.start": - return `session.start model=${event.data.selectedModel}`; - case "session.error": - return `session.error ${event.data.errorType}: ${event.data.message}`; - case "tool.execution_start": - return `tool.start ${event.data.toolName} ${JSON.stringify(event.data.arguments ?? {})}`; - case "tool.execution_complete": - return `tool.done ${event.data.success ? "ok" : "FAILED"}${event.data.error ? ` ${event.data.error.message}` : ""}`; - case "assistant.message": - return `assistant.message (${event.data.content.length} chars)`; - default: - return undefined; - } -} - -async function main(): Promise { - let parsed: CliArgs | undefined; - try { - parsed = parseArgs(process.argv.slice(2)); - } catch (error) { - process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n\n`); - printHelp(); - process.exit(2); - } - if (!parsed) return; // help shown - - const apiBaseUrl = process.env.SCOPE_MT_API_URL || "http://localhost:3001"; - const taxonomyModel = parsed.model || process.env.TAXONOMY_MODEL || "gpt-5.4"; - const sessionTimeoutMs = parsed.timeoutMs ?? (Number(process.env.SESSION_TIMEOUT_MS) || 5 * 60 * 1000); - const outputPath = resolve(parsed.output ?? `taxonomy-${parsed.requestId}.json`); - - const githubToken = process.env.GITHUB_TOKEN - ? process.env.GITHUB_TOKEN - : await new TokenManagerClient(process.env.TOKEN_MANAGER_URL).acquireToken("copilot-sdk"); - - const runId = await resolveRunId(apiBaseUrl, parsed.requestId, parsed.runId); - - const log = (level: LogEvent["level"], msg: string, data?: Record): void => { - if (parsed.quiet) return; - const suffix = data && Object.keys(data).length > 0 ? ` ${JSON.stringify(data)}` : ""; - process.stderr.write(`[${level}] ${msg}${suffix}\n`); - }; - - process.stderr.write( - `Generating taxonomy: requestId=${parsed.requestId} runId=${runId} model=${taxonomyModel} timeout=${sessionTimeoutMs}ms\n`, - ); - - const onEvent = parsed.quiet - ? undefined - : (event: SessionEvent): void => { - const summary = summarizeEvent(event); - if (summary) process.stderr.write(` · ${summary}\n`); - }; - - const taxonomy = await generateTaxonomy({ - requestId: parsed.requestId, - runId, - apiBaseUrl, - taxonomyModel, - githubToken, - sessionTimeoutMs, - log, - onEvent, - }); - - const json = JSON.stringify(taxonomy, null, 2); - await writeFile(outputPath, `${json}\n`, "utf-8"); - process.stderr.write(`\nTaxonomy written to ${outputPath}\n`); - if (parsed.stdout) { - process.stdout.write(`${json}\n`); - } -} - -main().catch((error) => { - process.stderr.write(`\nTaxonomy generation failed: ${error instanceof Error ? error.message : String(error)}\n`); - process.exit(1); -}); diff --git a/apps/workers/taxonomy/src/index.ts b/apps/workers/taxonomy/src/index.ts deleted file mode 100644 index b1a4f322b..000000000 --- a/apps/workers/taxonomy/src/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import "dotenv/config"; -import { TaxonomyQueueProcessor, type TaxonomyQueueProcessorConfig } from "./taxonomy-queue-processor.js"; - -const config: TaxonomyQueueProcessorConfig = { - mongoUri: process.env.MONGO_CONNECTION_STRING || process.env.AZURE_COSMOS_CONNECTION_STRING || "mongodb://localhost:27017", - mongoDatabase: process.env.MONGO_DATABASE || "requests-db", - mongoCollection: "requests", - storageAccountName: process.env.AZURE_STORAGE_ACCOUNT_NAME || "devstoreaccount1", - storageConnectionString: process.env.STORAGE_CONNECTION_STRING || process.env.AZURE_STORAGE_CONNECTION_STRING || undefined, - queueName: process.env.AZURE_STORAGE_QUEUE_TAXONOMY || "pp-taxonomy-queue", - batchSize: Number(process.env.BATCH_SIZE) || 1, - pollIntervalMs: Number(process.env.POLL_INTERVAL_MS) || 5000, - redisHost: process.env.REDIS_HOST || "localhost", - redisPort: Number(process.env.REDIS_PORT) || 6379, - redisPassword: process.env.REDIS_PASSWORD || "", - taxonomyModel: process.env.TAXONOMY_MODEL || "gpt-5.4", - apiBaseUrl: process.env.SCOPE_MT_API_URL || "http://localhost:3001", - sessionTimeoutMs: Number(process.env.SESSION_TIMEOUT_MS) || 5 * 60 * 1000, - schedulerUrl: process.env.SCHEDULER_URL, - tokenManagerUrl: process.env.TOKEN_MANAGER_URL, -}; - -const processor = new TaxonomyQueueProcessor(config); -processor.start().catch((err) => { - console.error("[taxonomy-handler] Fatal error:", err); - process.exit(1); -}); diff --git a/apps/workers/taxonomy/src/prompt.ts b/apps/workers/taxonomy/src/prompt.ts deleted file mode 100644 index 47ea28e4d..000000000 --- a/apps/workers/taxonomy/src/prompt.ts +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -export const TAXONOMY_SYSTEM_PROMPT = `You generate benchmark taxonomy documents for completed Scope runs. - -Your job is to inspect the run metadata and ATIF trajectories, then return a taxonomy JSON document that conforms exactly to the taxonomy Zod schema enforced by the worker. - -Required JSON shape: -{ - "meta": { - "generatedAt": "ISO-8601 datetime with offset", - "schemaVersion": "string", - "runId": "string", - "requestId": "string" - }, - "scorecard": { - "summary": "string", - "profileResults": [ - { - "profileId": "string", - "select": "best|median|worst", - "taskOutcome": "success|partial|failure", - "overallScore": 0-100, - "dimensions": [ - { - "name": "string", - "score": 0-100, - "rationale": "string", - "evidence": ["string"] - } - ] - } - ], - "crossProfileInsights": ["string"] - }, - "behaviorAnalysis": { - "patterns": [ - { - "id": "string", - "category": "strength|weakness|inconsistency|regression", - "title": "string", - "description": "string", - "frequency": 0-1, - "impact": "critical|high|medium|low", - "evidence": [ - { - "runId": "string", - "iteration": 0-or-greater integer, - "excerpt": "string" - } - ] - } - ], - "agentPersonality": { - "communicationStyle": "string", - "problemSolvingApproach": "string", - "errorRecoveryBehavior": "string" - } - }, - "actionList": { - "fix": [actionItem], - "improve": [actionItem], - "investigate": [actionItem] - } -} - -Each actionItem must include: -- id -- title -- priority: critical|high|medium|low -- description -- ownership: agent-team|platform-team|prompt-engineer|scenario-author -- relatedPatterns: string[] -- suggestedApproach - -Output rules: -- Return raw JSON only. -- Do not wrap the JSON in markdown fences. -- Do not include commentary before or after the JSON. -- Every field must be valid for the schema. -- Preserve the 3-layer structure: scorecard, behaviorAnalysis, actionList. -- If a value is unknown, use the schema-compatible empty/null/default value rather than inventing facts. - -Use the available tools to gather evidence before answering: -- get_run_data for the request, scenario, run, turns, and criteria outcomes. -- get_atif_trajectory for ATIF evidence from individual iterations. - -Ground every classification in the actual run evidence. Prefer precise, stable labels over verbose prose. If validation feedback is provided later in the conversation, correct the JSON and return a full replacement document.`; diff --git a/apps/workers/taxonomy/src/taxonomy-generator.ts b/apps/workers/taxonomy/src/taxonomy-generator.ts deleted file mode 100644 index ada150f2c..000000000 --- a/apps/workers/taxonomy/src/taxonomy-generator.ts +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { CopilotClient, type PermissionHandler, SessionEvent } from "@github/copilot-sdk"; -import { - type LogEvent, - taxonomySchema, - type TaxonomyDocument, -} from "shared"; -import { TAXONOMY_SYSTEM_PROMPT } from "./prompt.js"; -import { createTaxonomyTools } from "./tools.js"; - -export const DEFAULT_SESSION_TIMEOUT_MS = 5 * 60 * 1000; -const DELTA_LOG_INTERVAL = 2_000; -const approveTaxonomyTools: PermissionHandler = request => - request.kind === "custom-tool" - ? { kind: "approved" } - : { kind: "denied-by-rules", rules: [] }; - -type TaxonomyLogFn = ( - level: LogEvent["level"], - msg: string, - data?: Record, -) => Promise | void; - -export interface GenerateTaxonomyParams { - requestId: string; - runId: string; - apiBaseUrl: string; - taxonomyModel: string; - githubToken: string; - sessionTimeoutMs?: number; - log: TaxonomyLogFn; - /** Optional raw session-event hook (used by the dev CLI to render live progress). */ - onEvent?: (event: SessionEvent) => void; -} - -/** - * Runs the Copilot SDK taxonomy generation loop and returns a schema-validated - * taxonomy document. This is side-effect-free with respect to Mongo and blob - * storage (it only reads run data and ATIF trajectories through the API); - * persistence is the caller's responsibility. - */ -export async function generateTaxonomy(params: GenerateTaxonomyParams): Promise { - const { requestId, runId, apiBaseUrl, taxonomyModel, githubToken, log, onEvent } = params; - const client = new CopilotClient({ githubToken }); - const tools = createTaxonomyTools(apiBaseUrl, requestId); - const timeoutMs = params.sessionTimeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS; - - let streamedResponse = ""; - let latestMessage = ""; - let lastLoggedCharCount = 0; - const toolNames = new Map(); - - try { - const session = await client.createSession({ - model: taxonomyModel, - streaming: true, - tools, - availableTools: [], - excludedTools: ["builtin:*", "mcp:*"], - onPermissionRequest: approveTaxonomyTools, - systemMessage: { mode: "replace", content: TAXONOMY_SYSTEM_PROMPT }, - }); - - session.on((event: SessionEvent) => { - onEvent?.(event); - switch (event.type) { - case "session.start": - void log("info", "Copilot session started", { - sessionId: event.data.sessionId, - model: event.data.selectedModel, - }); - break; - case "session.error": - void log("error", `Session error: ${event.data.message}`, { errorType: event.data.errorType }); - break; - case "tool.execution_start": - toolNames.set(event.data.toolCallId, event.data.toolName); - void log("info", `Tool call: ${event.data.toolName}`, { - toolCallId: event.data.toolCallId, - arguments: event.data.arguments as Record | undefined, - }); - break; - case "tool.execution_complete": { - const toolName = toolNames.get(event.data.toolCallId) ?? "unknown"; - toolNames.delete(event.data.toolCallId); - void log("info", `Tool result: ${toolName} (${event.data.success ? "success" : "failed"})`, { - toolCallId: event.data.toolCallId, - error: event.data.error?.message, - }); - break; - } - case "assistant.message_delta": - streamedResponse += event.data.deltaContent; - if (streamedResponse.length - lastLoggedCharCount >= DELTA_LOG_INTERVAL) { - lastLoggedCharCount = streamedResponse.length; - void log("info", `Generating taxonomy… (${streamedResponse.length} chars so far)`); - } - break; - case "assistant.message": - latestMessage = event.data.content; - break; - } - }); - - let attempts = 0; - const maxAttempts = 3; - let prompt = [ - `Generate taxonomy JSON for request ${requestId} and run ${runId}.`, - "Use get_run_data first, then inspect ATIF trajectories that materially affect the classification.", - "Return only raw JSON that satisfies the taxonomy schema.", - ].join(" "); - let lastJson: string | undefined; - - while (attempts < maxAttempts) { - attempts += 1; - streamedResponse = ""; - latestMessage = ""; - lastLoggedCharCount = 0; - - await log("info", `Sending taxonomy prompt to Copilot SDK (attempt ${attempts}/${maxAttempts})`, { - requestId, - runId, - timeoutMs, - }); - - const response = await session.sendAndWait({ prompt }, timeoutMs); - const responseText = response?.data.content?.trim() || latestMessage.trim() || streamedResponse.trim(); - - if (!responseText) { - throw new Error("Copilot SDK returned an empty taxonomy response"); - } - - const jsonText = extractJson(responseText); - lastJson = jsonText; - - let parsedJson: unknown; - try { - parsedJson = JSON.parse(jsonText) as unknown; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - if (attempts >= maxAttempts) { - throw new Error(`Taxonomy response was not valid JSON after ${maxAttempts} attempts: ${errorMessage}`); - } - - prompt = [ - "Your previous response was not valid JSON.", - `JSON parse error: ${errorMessage}.`, - "Return the full taxonomy document again as raw JSON only, with no markdown fences or commentary.", - "Replace the entire response with corrected JSON.", - ].join(" "); - continue; - } - - const validation = taxonomySchema.safeParse(parsedJson); - if (validation.success) { - return validation.data; - } - - const validationErrors = formatValidationErrors(validation.error.issues); - await log("warn", `Taxonomy validation failed on attempt ${attempts}`, { - issues: validationErrors, - }); - - if (attempts >= maxAttempts) { - throw new Error( - `Taxonomy schema validation failed after ${maxAttempts} attempts: ${validationErrors.join(" | ")}`, - ); - } - - prompt = [ - "Your previous taxonomy JSON did not satisfy the schema.", - "Correct the document and return the entire replacement JSON object.", - "Do not include markdown fences or commentary.", - `Validation errors: ${validationErrors.join("; ")}`, - lastJson ? `Previous JSON: ${lastJson}` : "", - ].filter(Boolean).join(" "); - } - - throw new Error("Taxonomy generation ended without a valid document"); - } finally { - await client.stop().catch(() => undefined); - } -} - -function extractJson(responseText: string): string { - const trimmed = responseText.trim(); - if (!trimmed.startsWith("```")) { - return trimmed; - } - - const fenceMatch = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i); - return fenceMatch?.[1]?.trim() ?? trimmed; -} - -function formatValidationErrors(issues: Array<{ path: PropertyKey[]; message: string }>): string[] { - return issues.map((issue) => { - const path = issue.path.length > 0 ? issue.path.map((segment) => String(segment)).join(".") : ""; - return `${path}: ${issue.message}`; - }); -} diff --git a/apps/workers/taxonomy/src/taxonomy-queue-processor.ts b/apps/workers/taxonomy/src/taxonomy-queue-processor.ts deleted file mode 100644 index 1d8160db0..000000000 --- a/apps/workers/taxonomy/src/taxonomy-queue-processor.ts +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { DequeuedMessageItem } from "@azure/storage-queue"; -import { - BaseQueueProcessor, - BlobStorage, - type BaseQueueProcessorConfig, - type LogEvent, - type RequestDocument, - TokenManagerClient, - type TaxonomyDocument, - type VisibilityHeartbeat, - withRetry, -} from "shared"; -import { generateTaxonomy } from "./taxonomy-generator.js"; - -const HANDLER_ID = "pp-taxonomy"; -const HANDLER_VERSION = 1; -const DEFAULT_SESSION_TIMEOUT_MS = 5 * 60 * 1000; - -interface TaxonomyQueueMessage { - type: "taxonomy"; - requestId: string; - runId?: string; -} - -export interface TaxonomyQueueProcessorConfig extends BaseQueueProcessorConfig { - taxonomyModel: string; - apiBaseUrl: string; - sessionTimeoutMs?: number; - schedulerUrl?: string; - tokenManagerUrl?: string; -} - -export class TaxonomyQueueProcessor extends BaseQueueProcessor { - private taxonomyConfig: TaxonomyQueueProcessorConfig; - private tokenClient: TokenManagerClient; - private blobStorage: BlobStorage; - - constructor(config: TaxonomyQueueProcessorConfig) { - super(config, "taxonomy-handler"); - this.taxonomyConfig = config; - this.tokenClient = new TokenManagerClient(config.tokenManagerUrl); - this.blobStorage = new BlobStorage({ - storageAccountName: config.storageAccountName, - storageConnectionString: config.storageConnectionString, - }); - } - - protected override async handleRequest( - doc: RequestDocument, - message: DequeuedMessageItem, - heartbeat: VisibilityHeartbeat, - log: (level: LogEvent["level"], msg: string, data?: Record) => Promise, - payload?: Record, - ): Promise { - const queueMessage = payload as TaxonomyQueueMessage | undefined; - const requestId = doc._id; - const activeRunId = doc.run?._id; - const runId = queueMessage?.runId ?? activeRunId ?? requestId; - - if (!queueMessage || queueMessage.type !== "taxonomy") { - await log("warn", "Discarding message with unsupported taxonomy payload", { payload }); - await this.safeDeleteMessage(message.messageId, heartbeat.stop()); - return; - } - - if (queueMessage.runId && activeRunId && queueMessage.runId !== activeRunId) { - await log("warn", "Discarding stale taxonomy message for superseded run", { - requestId, - queuedRunId: queueMessage.runId, - activeRunId, - }); - await this.safeDeleteMessage(message.messageId, heartbeat.stop()); - return; - } - - if (!doc.run) { - throw new Error(`Request ${requestId} has no active run to classify`); - } - - await withRetry(() => this.collection.updateOne( - { _id: requestId } as never, - { - $set: { - [`run.handlerStatus.${HANDLER_ID}.status`]: "processing", - [`run.handlerStatus.${HANDLER_ID}.version`]: HANDLER_VERSION, - [`run.handlerStatus.${HANDLER_ID}.updatedAt`]: new Date(), - [`run.handlerStatus.${HANDLER_ID}.error`]: null, - }, - } as never, - )); - - await log("info", "Starting taxonomy generation", { requestId, runId, model: this.taxonomyConfig.taxonomyModel }); - - try { - const taxonomy = await this.generateTaxonomy(requestId, runId, log); - const blobName = `${requestId}/runs/${runId}/taxonomy.json`; - const taxonomyUrl = await this.blobStorage.uploadJson(blobName, taxonomy); - - await withRetry(() => this.collection.updateOne( - { _id: requestId } as never, - { - $set: { - [`run.taxonomyUrl`]: taxonomyUrl, - [`run.handlerStatus.${HANDLER_ID}.status`]: "done", - [`run.handlerStatus.${HANDLER_ID}.version`]: HANDLER_VERSION, - [`run.handlerStatus.${HANDLER_ID}.updatedAt`]: new Date(), - }, - $unset: { - [`run.handlerStatus.${HANDLER_ID}.error`]: "", - }, - } as never, - )); - - await log("info", "Taxonomy generation completed", { - requestId, - runId, - blobName, - taxonomyUrl, - }); - - await this.notifyHandlerComplete(requestId, runId, "done"); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - - await withRetry(() => this.collection.updateOne( - { _id: requestId } as never, - { - $set: { - [`run.handlerStatus.${HANDLER_ID}.status`]: "failed", - [`run.handlerStatus.${HANDLER_ID}.version`]: HANDLER_VERSION, - [`run.handlerStatus.${HANDLER_ID}.updatedAt`]: new Date(), - [`run.handlerStatus.${HANDLER_ID}.error`]: errorMessage, - }, - } as never, - )); - - await log("error", `Taxonomy generation failed: ${errorMessage}`, { requestId, runId }); - await this.notifyHandlerComplete(requestId, runId, "failed"); - throw error; - } - - const finalPopReceipt = heartbeat.stop(); - await this.safeDeleteMessage(message.messageId, finalPopReceipt); - } - - private async generateTaxonomy( - requestId: string, - runId: string, - log: (level: LogEvent["level"], msg: string, data?: Record) => Promise, - ): Promise { - const githubToken = await this.tokenClient.acquireToken("copilot-sdk"); - await log("info", "Acquired Copilot SDK token"); - - return generateTaxonomy({ - requestId, - runId, - apiBaseUrl: this.taxonomyConfig.apiBaseUrl, - taxonomyModel: this.taxonomyConfig.taxonomyModel, - githubToken, - sessionTimeoutMs: this.taxonomyConfig.sessionTimeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS, - log, - }); - } - - private async notifyHandlerComplete( - requestId: string, - runId: string, - status: "done" | "failed", - ): Promise { - const schedulerUrl = this.taxonomyConfig.schedulerUrl; - if (!schedulerUrl) return; - - try { - const response = await fetch(`${schedulerUrl}/notify/handler-complete`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ requestId, runId, handlerId: HANDLER_ID, status }), - }); - - if (!response.ok) { - console.warn(`[taxonomy-handler] Scheduler notify failed: ${response.status}`); - } - } catch (error) { - console.warn(`[taxonomy-handler] Scheduler notify error for ${requestId}/${runId}:`, error); - } - } -} diff --git a/apps/workers/taxonomy/src/tools.ts b/apps/workers/taxonomy/src/tools.ts deleted file mode 100644 index ea4eced32..000000000 --- a/apps/workers/taxonomy/src/tools.ts +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { defineTool } from "@github/copilot-sdk"; -import { type ConversationTurn, type RequestDocument } from "shared"; - -interface GetAtifTrajectoryArgs { - iteration?: number; -} - -function selectTurnWithAtif(turns: ConversationTurn[] | undefined, requestedIteration?: number): ConversationTurn | undefined { - if (!turns?.length) return undefined; - - if (requestedIteration !== undefined) { - return turns.find((turn) => turn.iteration === requestedIteration && typeof turn.atifUrl === "string"); - } - - return [...turns] - .filter((turn) => typeof turn.atifUrl === "string") - .sort((a, b) => b.iteration - a.iteration)[0]; -} - -export function createTaxonomyTools( - apiBaseUrl: string, - requestId: string, -) { - const getRunData = defineTool("get_run_data", { - description: - "Fetch the full request document plus the active run state, including scenario, persona, turns, criteria results, and handler status.", - parameters: { - type: "object", - properties: {}, - }, - handler: async () => { - try { - const response = await fetch(`${apiBaseUrl}/api/v1/requests/${requestId}`); - if (!response.ok) { - return { error: `Failed to fetch request: ${response.status} ${response.statusText}` }; - } - - const request: RequestDocument = await response.json(); - const run = request.run; - - return { - request: { - id: request._id, - workerType: request.workerType, - model: request.model, - reasoningEffort: request.reasoningEffort, - maxIterations: request.maxIterations, - createdAt: request.createdAt, - updatedAt: request.updatedAt, - persona: request.persona, - personaInstructions: request.personaInstructions, - mcpServers: request.mcpServers, - skillRevisions: request.skillRevisions, - extensions: request.extensions, - priority: request.priority, - scenario: request.scenario, - }, - run: run ? { - id: run._id, - attemptNumber: run.attemptNumber, - status: run.status, - outcome: run.outcome, - result: run.result, - error: run.error, - startedAt: run.startedAt, - finishedAt: run.finishedAt, - updatedAt: run.updatedAt, - aiCallCount: run.aiCallCount, - tokenUsage: run.tokenUsage, - handlerStatus: run.handlerStatus, - turns: run.turns?.map((turn) => ({ - iteration: turn.iteration, - passed: turn.passed, - timestamp: turn.timestamp, - startedAt: turn.startedAt, - durationMs: turn.durationMs, - judgeFeedback: turn.judgeFeedback, - codingAgentResponse: turn.codingAgentResponse, - aiCallCount: turn.aiCallCount, - tokenUsage: turn.tokenUsage, - snapshotUrl: turn.snapshotUrl, - rawChatUrl: turn.rawChatUrl, - rawChatFormat: turn.rawChatFormat, - toolCallsUrl: turn.toolCallsUrl, - toolCallCount: turn.toolCallCount, - atifUrl: turn.atifUrl, - criteriaResults: turn.criteriaResults, - })) ?? [], - } : null, - }; - } catch (err) { - return { error: `Failed to fetch run data: ${err}` }; - } - }, - }); - - const getAtifTrajectory = defineTool("get_atif_trajectory", { - description: - "Fetch an ATIF trajectory JSON document for a specific iteration, or the latest available iteration when omitted.", - parameters: { - type: "object", - properties: { - iteration: { - type: "number", - description: "Optional 1-based iteration number. If omitted, the latest available ATIF trajectory is returned.", - }, - }, - }, - handler: async (args: GetAtifTrajectoryArgs) => { - try { - const response = await fetch(`${apiBaseUrl}/api/v1/requests/${requestId}`); - if (!response.ok) { - return { error: `Failed to fetch request: ${response.status} ${response.statusText}` }; - } - - const request: RequestDocument = await response.json(); - const selectedTurn = selectTurnWithAtif(request.run?.turns, args.iteration); - - if (!selectedTurn?.atifUrl) { - return { - error: args.iteration !== undefined - ? `No ATIF trajectory available for iteration ${args.iteration}` - : "No ATIF trajectory is available for this run", - }; - } - - const runId = request.run?._id; - const iteration = selectedTurn.iteration; - const atifUrl = runId - ? `${apiBaseUrl}/api/v1/requests/${requestId}/runs/${runId}/atif?iteration=${iteration}` - : `${apiBaseUrl}/api/v1/requests/${requestId}/atif?iteration=${iteration}`; - - const atifResponse = await fetch(atifUrl); - if (!atifResponse.ok) { - return { error: `Failed to fetch ATIF trajectory: ${atifResponse.status} ${atifResponse.statusText}` }; - } - - const trajectory: unknown = await atifResponse.json(); - - return { - iteration, - trajectory, - }; - } catch (err) { - return { error: `Failed to fetch ATIF trajectory: ${err}` }; - } - }, - }); - - return [getRunData, getAtifTrajectory]; -} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index f5e309b30..49c6443e8 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -217,19 +217,19 @@ services: - action: rebuild path: ./pnpm-lock.yaml - taxonomy-handler: + readout-handler: build: target: dev develop: watch: - action: sync - path: ./apps/workers/taxonomy/src - target: /app/apps/workers/taxonomy/src + path: ./apps/workers/readout/src + target: /app/apps/workers/readout/src - action: sync path: ./packages/shared/src target: /app/packages/shared/src - action: rebuild - path: ./apps/workers/taxonomy/package.json + path: ./apps/workers/readout/package.json - action: rebuild path: ./pnpm-lock.yaml @@ -237,7 +237,7 @@ services: build: target: dev - register-handler-taxonomy: + register-handler-readout: build: target: dev diff --git a/docker-compose.yml b/docker-compose.yml index 46df7cd4f..fabaea982 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ # docker compose --profile vscode-web up # + VS Code Web worker # docker compose --profile claude-code --profile copilot up # multiple workers # docker compose --profile report up # + Report Generator worker -# docker compose --profile taxonomy up # + Taxonomy handler worker +# docker compose --profile readout up # + Submission readout worker # docker compose up mongodb redis azurite # infra only (run app services natively) # # Host-native debugging: @@ -870,21 +870,19 @@ services: api: condition: service_started - taxonomy-handler: + readout-handler: build: context: . - dockerfile: apps/workers/taxonomy/Dockerfile - profiles: [taxonomy, all] + dockerfile: apps/workers/readout/Dockerfile + profiles: [readout, all] environment: <<: *common-env - AZURE_STORAGE_QUEUE_TAXONOMY: pp-taxonomy-queue - TAXONOMY_MODEL: ${TAXONOMY_MODEL:-gpt-5.4} - SCOPE_MT_API_URL: http://api:80 - SCHEDULER_URL: http://scheduler:8080 + AZURE_STORAGE_QUEUE_READOUT: pp-readout-queue + READOUT_MODEL: ${READOUT_MODEL:-gpt-5.4} GITHUB_TOKEN: ${GITHUB_TOKEN:-} TOKEN_MANAGER_URL: http://token-manager:80 GIT_COMMIT: ${GIT_COMMIT:-development} - SESSION_TIMEOUT_MS: ${SESSION_TIMEOUT_MS:-300000} + READOUT_SESSION_TIMEOUT_MS: ${READOUT_SESSION_TIMEOUT_MS:-300000} depends_on: mongodb: condition: service_healthy @@ -893,21 +891,19 @@ services: condition: service_healthy azurite: condition: service_healthy - api: - condition: service_started scheduler: condition: service_started - register-handler-taxonomy: + register-handler-readout: condition: service_completed_successfully - register-handler-taxonomy: + register-handler-readout: build: context: . - dockerfile: apps/workers/taxonomy/Dockerfile - profiles: [taxonomy, all] + dockerfile: apps/workers/readout/Dockerfile + profiles: [readout, all] environment: <<: *common-env - HANDLER_YAML_PATH: apps/workers/taxonomy/handler.yaml + HANDLER_YAML_PATH: apps/workers/readout/handler.yaml SCHEDULER_URL: http://scheduler:8080 depends_on: scheduler: diff --git a/package.json b/package.json index a98a960c8..d12b00825 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dev:coder-acp-claude-code": "pnpm --filter coder-acp-claude-code dev", "dev:coder-acp-copilot": "pnpm --filter coder-acp-copilot dev", "dev:report-generator": "pnpm --filter report-generator dev", - "taxonomy": "pnpm --filter taxonomy-handler taxonomy", + "dev:readout": "pnpm --filter readout-handler dev", "docker:up": "scripts/worktree-env.sh && scripts/dev-compose.sh up --build", "docker:up:claude-code": "scripts/worktree-env.sh && SCOPE_REGISTER_CLAUDE_CODE_AGENT=true scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-claude-code/versions.env --profile claude-code --profile report up --build", "docker:up:copilot": "scripts/worktree-env.sh && SCOPE_REGISTER_COPILOT_AGENT=true scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-copilot/versions.env --profile copilot --profile report up --build", @@ -27,9 +27,9 @@ "docker:up:portal": "scripts/worktree-env.sh && scripts/dev-compose.sh --profile auth --profile portal up --build", "docker:dev": "scripts/worktree-env.sh && COMPOSE_EXPERIMENTAL_WATCH_POLL=1 scripts/dev-compose.sh -f docker-compose.yml -f docker-compose.dev.yml up --build --watch", "docker:dev:claude-code": "scripts/worktree-env.sh && SCOPE_REGISTER_CLAUDE_CODE_AGENT=true GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) COMPOSE_EXPERIMENTAL_WATCH_POLL=1 scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-claude-code/versions.env -f docker-compose.yml -f docker-compose.dev.yml --profile claude-code --profile report --profile post-processor --profile auth --profile portal up --build --watch", - "docker:dev:copilot": "scripts/worktree-env.sh && SCOPE_REGISTER_COPILOT_AGENT=true GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) COMPOSE_EXPERIMENTAL_WATCH_POLL=1 scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-copilot/versions.env -f docker-compose.yml -f docker-compose.dev.yml --profile copilot --profile report --profile post-processor --profile taxonomy --profile auth --profile portal up --build --watch", + "docker:dev:copilot": "scripts/worktree-env.sh && SCOPE_REGISTER_COPILOT_AGENT=true GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) COMPOSE_EXPERIMENTAL_WATCH_POLL=1 scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-copilot/versions.env -f docker-compose.yml -f docker-compose.dev.yml --profile copilot --profile report --profile post-processor --profile readout --profile auth --profile portal up --build --watch", "docker:dev:portal": "scripts/worktree-env.sh && GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) COMPOSE_EXPERIMENTAL_WATCH_POLL=1 scripts/dev-compose.sh -f docker-compose.yml -f docker-compose.dev.yml --profile auth --profile portal up --build --watch", - "docker:dev:all": "scripts/worktree-env.sh && SCOPE_REGISTER_COPILOT_AGENT=true SCOPE_REGISTER_CLAUDE_CODE_AGENT=true GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) COMPOSE_EXPERIMENTAL_WATCH_POLL=1 scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-copilot/versions.env --env-file apps/workers/coder-acp-claude-code/versions.env -f docker-compose.yml -f docker-compose.dev.yml --profile claude-code --profile copilot --profile report --profile post-processor --profile taxonomy --profile auth --profile portal up --build --watch", + "docker:dev:all": "scripts/worktree-env.sh && SCOPE_REGISTER_COPILOT_AGENT=true SCOPE_REGISTER_CLAUDE_CODE_AGENT=true GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) COMPOSE_EXPERIMENTAL_WATCH_POLL=1 scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-copilot/versions.env --env-file apps/workers/coder-acp-claude-code/versions.env -f docker-compose.yml -f docker-compose.dev.yml --profile claude-code --profile copilot --profile report --profile post-processor --profile readout --profile auth --profile portal up --build --watch", "docker:dev:build": "scripts/worktree-env.sh && GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-copilot/versions.env --env-file apps/workers/coder-acp-claude-code/versions.env -f docker-compose.yml -f docker-compose.dev.yml --profile claude-code --profile copilot --profile report --profile auth --profile portal build", "docker:down": "scripts/worktree-env.sh && scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-copilot/versions.env --env-file apps/workers/coder-acp-claude-code/versions.env --profile claude-code --profile copilot --profile report --profile portal down --volumes", "docker:build": "scripts/worktree-env.sh && scripts/dev-compose.sh --env-file .env --env-file apps/workers/coder-acp-copilot/versions.env --env-file apps/workers/coder-acp-claude-code/versions.env --profile claude-code --profile copilot --profile report build", diff --git a/packages/db-migrations/src/migrations/031-migrate-services-to-handler-dag.ts b/packages/db-migrations/src/migrations/031-migrate-services-to-handler-dag.ts index 87fba84c8..570b12e40 100644 --- a/packages/db-migrations/src/migrations/031-migrate-services-to-handler-dag.ts +++ b/packages/db-migrations/src/migrations/031-migrate-services-to-handler-dag.ts @@ -7,7 +7,7 @@ import type { MigrationInterface } from "mongo-migrate-ts"; /** * Migration 031: Remove the legacy "post-processor" service entry. * - * Handler service registration (pp-atif, pp-taxonomy) is now owned + * Handler service registration (e.g. pp-atif) is now owned * by each worker's handler.yaml + the generic register-handler script, * which POSTs to the scheduler's POST /handlers/register endpoint on deploy. * Reports are not a DAG handler — the scheduler triggers report generation diff --git a/packages/db-migrations/src/migrations/032-add-handler-status-index.ts b/packages/db-migrations/src/migrations/032-add-handler-status-index.ts index cf69fb7c0..44d7a448a 100644 --- a/packages/db-migrations/src/migrations/032-add-handler-status-index.ts +++ b/packages/db-migrations/src/migrations/032-add-handler-status-index.ts @@ -13,14 +13,6 @@ const HANDLER_STATUS_INDEXES = [ updatedAt: -1 as const, }, }, - { - name: "idx_handler_dispatch_taxonomy", - key: { - "run.handlerStatus.pp-taxonomy.status": 1 as const, - "run.status": 1 as const, - updatedAt: -1 as const, - }, - }, ] as const; export class AddHandlerStatusIndex implements MigrationInterface { diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 725dcb079..4308a9a4d 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -29,6 +29,6 @@ export * from "./har/index.js"; export * from "./utils/index.js"; export * from "./schemas/index.js"; export * from "./auth/index.js"; -export * from "./schemas/taxonomy.js"; +export * from "./schemas/readout.js"; export * from "./cursor.js"; export * from "./run-duration.js"; diff --git a/packages/shared/src/schemas/index.ts b/packages/shared/src/schemas/index.ts index 6ed9119dc..036f45347 100644 --- a/packages/shared/src/schemas/index.ts +++ b/packages/shared/src/schemas/index.ts @@ -23,4 +23,4 @@ export * from "./feature-flag.js"; export * from "./profile.js"; export * from "./project.js"; export * from "./submission.js"; -export * from "./taxonomy.js"; +export * from "./readout.js"; diff --git a/packages/shared/src/schemas/readout.test.ts b/packages/shared/src/schemas/readout.test.ts new file mode 100644 index 000000000..503dcfc8c --- /dev/null +++ b/packages/shared/src/schemas/readout.test.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, expect, it } from "vitest"; +import { + SUBMISSION_READOUT_SCHEMA_ID, + SUBMISSION_READOUT_SCHEMA_VERSION, + submissionReadoutSchema, +} from "./readout.js"; + +describe("submissionReadoutSchema", () => { + it("accepts a one-request readout with empty selected observations", () => { + expect(submissionReadoutSchema.parse({ + meta: { + schemaId: SUBMISSION_READOUT_SCHEMA_ID, + schemaVersion: SUBMISSION_READOUT_SCHEMA_VERSION, + generatedAt: "2026-10-13T12:00:00.000Z", + submissionId: "submission-1", + submissionRunId: "submission-run-1", + }, + summary: { + title: "Submission readout", + executiveSummary: "The single run completed without selected observations.", + totalRuns: 1, + succeededRuns: 1, + failedRuns: 0, + passedObservations: 0, + failedObservations: 0, + }, + profiles: [{ + requestIds: ["request-1"], + summary: "One unprofiled request.", + }], + runs: [{ + requestId: "request-1", + runId: "run-1", + task: "Update the endpoint", + workerType: "copilot", + status: "done", + outcome: "succeeded", + observations: [], + trajectoryHighlights: [], + }], + findings: [], + actions: [], + })).toBeTruthy(); + }); +}); diff --git a/packages/shared/src/schemas/readout.ts b/packages/shared/src/schemas/readout.ts new file mode 100644 index 000000000..f8e85ca7e --- /dev/null +++ b/packages/shared/src/schemas/readout.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { z } from "zod"; + +export const SUBMISSION_READOUT_SCHEMA_ID = "scope.submission-readout"; +export const SUBMISSION_READOUT_SCHEMA_VERSION = "1.0"; + +export const readoutObservationSchema = z.object({ + criterionId: z.string().min(1), + taxonomyElementId: z.string().optional(), + subject: z.enum(["run", "iteration"]), + passed: z.boolean(), + feedback: z.string(), +}); + +export const readoutEvidenceSchema = z.object({ + submissionRunId: z.string().min(1), + requestId: z.string().min(1), + runId: z.string().min(1), + iteration: z.number().int().min(0).optional(), + criterionId: z.string().min(1).optional(), + excerpt: z.string().min(1), +}); + +export const readoutRunSchema = z.object({ + requestId: z.string().min(1), + runId: z.string().min(1), + task: z.string(), + workerType: z.string().min(1), + model: z.string().optional(), + profileId: z.string().optional(), + profileVersionId: z.string().optional(), + status: z.string(), + outcome: z.string().optional(), + observations: z.array(readoutObservationSchema), + trajectoryHighlights: z.array(z.string().min(1)), +}); + +export const readoutFindingSchema = z.object({ + id: z.string().min(1), + title: z.string().min(1), + description: z.string().min(1), + severity: z.enum(["critical", "high", "medium", "low", "info"]), + evidence: z.array(readoutEvidenceSchema).min(1), +}); + +export const readoutActionSchema = z.object({ + id: z.string().min(1), + title: z.string().min(1), + priority: z.enum(["critical", "high", "medium", "low"]), + description: z.string().min(1), + relatedFindingIds: z.array(z.string().min(1)), +}); + +export const submissionReadoutSchema = z.object({ + meta: z.object({ + schemaId: z.literal(SUBMISSION_READOUT_SCHEMA_ID), + schemaVersion: z.literal(SUBMISSION_READOUT_SCHEMA_VERSION), + generatedAt: z.string().datetime({ offset: true }), + submissionId: z.string().min(1), + submissionRunId: z.string().min(1), + }), + summary: z.object({ + title: z.string().min(1), + executiveSummary: z.string().min(1), + totalRuns: z.number().int().min(1), + succeededRuns: z.number().int().min(0), + failedRuns: z.number().int().min(0), + passedObservations: z.number().int().min(0), + failedObservations: z.number().int().min(0), + }), + profiles: z.array(z.object({ + profileId: z.string().optional(), + requestIds: z.array(z.string().min(1)).min(1), + summary: z.string().min(1), + })), + runs: z.array(readoutRunSchema).min(1), + findings: z.array(readoutFindingSchema), + actions: z.array(readoutActionSchema), +}); + +export type SubmissionReadout = z.infer; +export type ReadoutRun = z.infer; diff --git a/packages/shared/src/schemas/report.ts b/packages/shared/src/schemas/report.ts index c9aa697db..afeb0ac5d 100644 --- a/packages/shared/src/schemas/report.ts +++ b/packages/shared/src/schemas/report.ts @@ -33,13 +33,25 @@ export const ReportStatusSchema = z.enum([ "failed", ]); +export const StructuredReportArtifactSchema = z + .object({ + schemaId: z.string(), + schemaVersion: z.string(), + contentType: z.literal("application/json"), + url: z.string(), + }) + .openapi("StructuredReportArtifact"); + export const ReportResponseSchema = z .object({ _id: z.string(), - requestId: z.string(), + requestId: z.string().optional(), + submissionId: z.string().optional(), + submissionRunId: z.string().optional(), templateId: z.string().optional(), reporter: ReporterSchema.optional(), content: z.string().optional(), + structuredArtifact: StructuredReportArtifactSchema.optional(), status: ReportStatusSchema, error: z.string().optional(), insightReferences: z.array(InsightReferenceSchema).optional(), diff --git a/packages/shared/src/schemas/request.ts b/packages/shared/src/schemas/request.ts index 33fe18f89..5aefb2466 100644 --- a/packages/shared/src/schemas/request.ts +++ b/packages/shared/src/schemas/request.ts @@ -240,7 +240,6 @@ export const RunStateSchema = z podName: z.string().optional(), }).optional(), harUrl: z.string().optional(), - taxonomyUrl: z.string().optional(), videoUrls: z.array(z.string()).optional(), setupVideoUrls: z.array(z.string()).optional(), tokenUsage: TokenUsageSchema.optional(), diff --git a/packages/shared/src/schemas/taxonomy.test.ts b/packages/shared/src/schemas/taxonomy.test.ts deleted file mode 100644 index 320f89ceb..000000000 --- a/packages/shared/src/schemas/taxonomy.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { describe, expect, it } from "vitest"; -import { taxonomySchema } from "./taxonomy.js"; - -describe("taxonomySchema", () => { - const validFixture = { - meta: { - generatedAt: "2025-02-01T12:00:00Z", - schemaVersion: "1.0.0", - runId: "run-123", - requestId: "req-456", - }, - scorecard: { - summary: "The agent usually succeeds but struggles with recovery quality.", - profileResults: [ - { - profileId: "profile-best", - select: "best", - taskOutcome: "success", - overallScore: 92, - dimensions: [ - { - name: "Correctness", - score: 95, - rationale: "Implemented the requested behavior fully.", - evidence: ["All acceptance criteria passed", "No regressions observed"], - }, - ], - }, - ], - crossProfileInsights: [ - "The agent communicates clearly across profiles.", - "Stress conditions reduce recovery quality.", - ], - }, - behaviorAnalysis: { - patterns: [ - { - id: "pattern-1", - category: "strength", - title: "Consistent planning", - description: "The agent outlines a clear plan before making changes.", - frequency: 0.75, - impact: "high", - evidence: [ - { - runId: "run-123", - iteration: 1, - excerpt: "Plan: inspect the failing endpoint, then patch the handler.", - }, - ], - }, - ], - agentPersonality: { - communicationStyle: "Concise and structured", - problemSolvingApproach: "Hypothesis-driven debugging", - errorRecoveryBehavior: "Retries with targeted fixes after inspecting evidence", - }, - }, - actionList: { - fix: [ - { - id: "action-1", - title: "Stabilize recovery prompts", - priority: "critical", - description: "Reduce failures when the first attempt is rejected.", - ownership: "prompt-engineer", - relatedPatterns: ["pattern-1"], - suggestedApproach: "Add explicit fallback guidance for failed first passes.", - }, - ], - improve: [ - { - id: "action-2", - title: "Expand planning examples", - priority: "medium", - description: "Reinforce successful planning behavior.", - ownership: "agent-team", - relatedPatterns: ["pattern-1"], - suggestedApproach: "Fine-tune on traces with strong upfront decomposition.", - }, - ], - investigate: [ - { - id: "action-3", - title: "Review profile variance", - priority: "low", - description: "Understand why profile-specific regressions appear sporadically.", - ownership: "scenario-author", - relatedPatterns: ["pattern-1"], - suggestedApproach: "Compare scenario wording against affected profile runs.", - }, - ], - }, - }; - - it("accepts a valid taxonomy fixture", () => { - const result = taxonomySchema.safeParse(validFixture); - expect(result.success).toBe(true); - }); - - it("rejects fixtures missing required fields with precise paths", () => { - const invalidFixture = { - ...validFixture, - scorecard: { - ...validFixture.scorecard, - profileResults: [ - { - ...validFixture.scorecard.profileResults[0], - dimensions: [ - { - name: "Correctness", - score: 95, - evidence: ["All acceptance criteria passed"], - }, - ], - }, - ], - }, - behaviorAnalysis: { - ...validFixture.behaviorAnalysis, - agentPersonality: { - communicationStyle: "Concise and structured", - errorRecoveryBehavior: "Retries with targeted fixes after inspecting evidence", - }, - }, - }; - - const result = taxonomySchema.safeParse(invalidFixture); - expect(result.success).toBe(false); - if (result.success) { - throw new Error("Expected invalid taxonomy fixture to fail parsing"); - } - - const issuePaths = result.error.issues.map((issue) => issue.path); - expect(issuePaths).toContainEqual([ - "scorecard", - "profileResults", - 0, - "dimensions", - 0, - "rationale", - ]); - expect(issuePaths).toContainEqual([ - "behaviorAnalysis", - "agentPersonality", - "problemSolvingApproach", - ]); - }); - - it("enforces enum values", () => { - const invalidFixture = { - ...validFixture, - actionList: { - ...validFixture.actionList, - fix: [ - { - ...validFixture.actionList.fix[0], - priority: "urgent", - }, - ], - }, - }; - - const result = taxonomySchema.safeParse(invalidFixture); - expect(result.success).toBe(false); - if (result.success) { - throw new Error("Expected invalid enum fixture to fail parsing"); - } - - expect(result.error.issues.map((issue) => issue.path)).toContainEqual([ - "actionList", - "fix", - 0, - "priority", - ]); - }); -}); diff --git a/packages/shared/src/schemas/taxonomy.ts b/packages/shared/src/schemas/taxonomy.ts deleted file mode 100644 index a837d4f91..000000000 --- a/packages/shared/src/schemas/taxonomy.ts +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { z } from "zod"; - -const scoreSchema = z.number().min(0).max(100); -const frequencySchema = z.number().min(0).max(1); - -/** Taxonomy document metadata. */ -export const taxonomyMetaSchema = z.object({ - generatedAt: z.string().datetime({ offset: true }), - schemaVersion: z.string().min(1), - runId: z.string().min(1), - requestId: z.string().min(1), -}); - -/** Scorecard dimension result for a single profile. */ -export const scorecardDimensionSchema = z.object({ - name: z.string().min(1), - score: scoreSchema, - rationale: z.string().min(1), - evidence: z.array(z.string().min(1)), -}); - -/** Scorecard result for a specific profile slice. */ -export const profileResultSchema = z.object({ - profileId: z.string().min(1), - select: z.enum(["best", "median", "worst"]), - taskOutcome: z.enum(["success", "partial", "failure"]), - overallScore: scoreSchema, - dimensions: z.array(scorecardDimensionSchema), -}); - -/** Top-level scorecard section summarizing profile results. */ -export const scorecardSchema = z.object({ - summary: z.string().min(1), - profileResults: z.array(profileResultSchema), - crossProfileInsights: z.array(z.string().min(1)), -}); - -/** Evidence excerpt supporting an observed behavior pattern. */ -export const behaviorEvidenceSchema = z.object({ - runId: z.string().min(1), - iteration: z.number().int().min(0), - excerpt: z.string().min(1), -}); - -/** Cross-run behavioral pattern identified by the taxonomy analysis. */ -export const behaviorPatternSchema = z.object({ - id: z.string().min(1), - category: z.enum(["strength", "weakness", "inconsistency", "regression"]), - title: z.string().min(1), - description: z.string().min(1), - frequency: frequencySchema, - impact: z.enum(["critical", "high", "medium", "low"]), - evidence: z.array(behaviorEvidenceSchema), -}); - -/** Qualitative description of the agent's personality traits. */ -export const agentPersonalitySchema = z.object({ - communicationStyle: z.string().min(1), - problemSolvingApproach: z.string().min(1), - errorRecoveryBehavior: z.string().min(1), -}); - -/** Behavior analysis section describing patterns and personality. */ -export const behaviorAnalysisSchema = z.object({ - patterns: z.array(behaviorPatternSchema), - agentPersonality: agentPersonalitySchema, -}); - -/** Action item emitted by the taxonomy analysis. */ -export const actionItemSchema = z.object({ - id: z.string().min(1), - title: z.string().min(1), - priority: z.enum(["critical", "high", "medium", "low"]), - description: z.string().min(1), - ownership: z.enum([ - "agent-team", - "platform-team", - "prompt-engineer", - "scenario-author", - ]), - relatedPatterns: z.array(z.string().min(1)), - suggestedApproach: z.string().min(1), -}); - -/** Prioritized action lists grouped by remediation intent. */ -export const actionListSchema = z.object({ - fix: z.array(actionItemSchema), - improve: z.array(actionItemSchema), - investigate: z.array(actionItemSchema), -}); - -/** Complete taxonomy document produced by the taxonomy post-processor. */ -export const taxonomySchema = z.object({ - meta: taxonomyMetaSchema, - scorecard: scorecardSchema, - behaviorAnalysis: behaviorAnalysisSchema, - actionList: actionListSchema, -}); - -/** Inferred TypeScript type for the taxonomy document. */ -export type TaxonomyDocument = z.infer; diff --git a/packages/shared/src/scripts/register-handler.test.ts b/packages/shared/src/scripts/register-handler.test.ts index 0c3858b58..7d873e452 100644 --- a/packages/shared/src/scripts/register-handler.test.ts +++ b/packages/shared/src/scripts/register-handler.test.ts @@ -6,11 +6,11 @@ import { parseHandlerDocument } from "./register-handler.js"; describe("parseHandlerDocument", () => { const valid = { - _id: "pp-taxonomy", + _id: "pp-example", type: "post-process-handler", version: 1, - queue: "pp-taxonomy-queue", - selector: "taxonomy", + queue: "pp-example-queue", + selector: "example", autoBackfill: false, dependsOn: ["pp-atif"], }; @@ -51,4 +51,18 @@ describe("parseHandlerDocument", () => { it("rejects a non-object input", () => { expect(() => parseHandlerDocument("nope")).toThrow(/mapping/); }); + + it("accepts the pp-readout submission handler contract", () => { + const readout = { + _id: "pp-readout", + type: "submission-handler", + version: 1, + queue: "pp-readout-queue", + selector: "readout", + autoBackfill: false, + dependsOn: ["pp-observations"], + dependencyMode: "all-runs", + }; + expect(parseHandlerDocument(readout)).toEqual(readout); + }); }); diff --git a/packages/shared/src/types/types.ts b/packages/shared/src/types/types.ts index dcd131259..c921fe006 100644 --- a/packages/shared/src/types/types.ts +++ b/packages/shared/src/types/types.ts @@ -534,14 +534,13 @@ export interface RunState { podName?: string; }; harUrl?: string; - taxonomyUrl?: string; videoUrls?: string[]; setupVideoUrls?: string[]; tokenUsage?: TokenUsage; aiCallCount?: number; rawChatUrl?: string; rawChatFormat?: string; - /** Per-handler post-processing status. Keys are handler IDs (e.g. "pp-atif", "pp-taxonomy"). */ + /** Per-handler post-processing status. Keys are handler IDs (e.g. "pp-atif"). */ handlerStatus?: Record; /** Whole-run observation results — one entry per selected `subject:"run"` * observation criterion, evaluated once against the final snapshot + the @@ -710,7 +709,6 @@ export const RUN_STATE_FIELD_NAMES = [ "lastHeartbeatAt", "worker", "harUrl", - "taxonomyUrl", "videoUrls", "setupVideoUrls", "tokenUsage", @@ -941,14 +939,24 @@ export interface Reporter { agentVersion: string; // @github/copilot-sdk package version } +export interface StructuredReportArtifact { + schemaId: string; + schemaVersion: string; + contentType: "application/json"; + url: string; +} + /** Report document stored in MongoDB */ export interface ReportDocument { _id: string; // UUID - requestId: string; // FK → RequestDocument._id + requestId?: string; // FK → RequestDocument._id for run reports + submissionId?: string; // FK → SubmissionDocument._id for submission reports + submissionRunId?: string; // Exact immutable submission attempt projectId: string; // FK → ProjectDocument._id (denormalized from request) templateId?: string; // FK → ReportTemplateDocument.id (slug of template that generated this report) reporter?: Reporter; // Set by the worker when it picks up the job content?: string; // Generated markdown report + structuredArtifact?: StructuredReportArtifact; status: ReportStatus; error?: string; insightReferences?: InsightReference[]; // Insights discovered/referenced by this report diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44a7b86bc..75f306a3e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -759,7 +759,7 @@ importers: specifier: ^5.6.2 version: 5.9.3 - apps/workers/report-generator: + apps/workers/readout: dependencies: '@azure/storage-queue': specifier: ^12.25.0 @@ -773,9 +773,9 @@ importers: shared: specifier: workspace:* version: link:../../../packages/shared - telemetry: - specifier: workspace:* - version: link:../../../packages/telemetry + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@types/node': specifier: ^22.7.0 @@ -787,20 +787,23 @@ importers: specifier: ^5.6.2 version: 5.9.3 - apps/workers/taxonomy: + apps/workers/report-generator: dependencies: '@azure/storage-queue': specifier: ^12.25.0 version: 12.29.0 '@github/copilot-sdk': - specifier: ^0.1.17 - version: 0.1.32(typescript@5.9.3) + specifier: 1.0.1 + version: 1.0.1(typescript@5.9.3) dotenv: specifier: 16.6.1 version: 16.6.1 shared: specifier: workspace:* version: link:../../../packages/shared + telemetry: + specifier: workspace:* + version: link:../../../packages/telemetry devDependencies: '@types/node': specifier: ^22.7.0 @@ -823,7 +826,6 @@ importers: yaml: specifier: ^2.8.1 version: 2.8.3 - devDependencies: '@types/node': specifier: ^22.18.6 @@ -1584,10 +1586,6 @@ packages: libc: [musl] hasBin: true - '@github/copilot-sdk@0.1.32': - resolution: {integrity: sha1-mkVfe7Zkq80wJuGboVpfCcAwg4E=, tarball: https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@github/copilot-sdk/-/copilot-sdk-0.1.32.tgz} - engines: {node: '>=20.0.0'} - '@github/copilot-sdk@1.0.1': resolution: {integrity: sha512-w6AaS0WqqTE/3iyUrZznvgCLQhsUF7ZmEVCneacuHCfOzlH0r6ww9WUmyA0zgqmXO75V0IYrkIcnFke/qJkkDg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -7729,14 +7727,6 @@ snapshots: '@github/copilot-linuxmusl-x64@1.0.63': optional: true - '@github/copilot-sdk@0.1.32(typescript@5.9.3)': - dependencies: - '@github/copilot': 1.0.63(typescript@5.9.3) - vscode-jsonrpc: 8.2.1 - zod: 4.3.6 - transitivePeerDependencies: - - typescript - '@github/copilot-sdk@1.0.1(typescript@5.9.3)': dependencies: '@github/copilot': 1.0.63(typescript@5.9.3) From 51fd1a5b580441a9926794a6021bf14e4c771387 Mon Sep 17 00:00:00 2001 From: Cedric Vidal Date: Tue, 13 Oct 2026 16:47:49 +0200 Subject: [PATCH 03/15] feat(readout): expose submission reports in UI and CLI Render current and historical submission readouts, support structured downloads, and add CLI access while removing the duplicated run taxonomy surface. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cbbd5eb-61c6-4e6a-bb38-c377be5b2163 --- apps/cli/src/commands/submission.ts | 51 ++++ .../SubmissionStatusCard.stories.tsx | 25 ++ .../src/components/SubmissionStatusCard.tsx | 11 +- apps/portal/src/components/TaxonomyTab.tsx | 248 ------------------ apps/portal/src/lib/api.ts | 30 ++- apps/portal/src/pages/InsightDetail.tsx | 21 +- apps/portal/src/pages/ReportDetail.tsx | 61 +++-- apps/portal/src/pages/ReportPreviewPanel.tsx | 18 +- apps/portal/src/pages/ReportsList.tsx | 31 ++- apps/portal/src/pages/RunDetail.tsx | 10 - apps/portal/src/pages/SubmissionDetail.tsx | 83 +++++- apps/portal/src/types.ts | 12 +- 12 files changed, 295 insertions(+), 306 deletions(-) delete mode 100644 apps/portal/src/components/TaxonomyTab.tsx diff --git a/apps/cli/src/commands/submission.ts b/apps/cli/src/commands/submission.ts index 835e7f3ed..47e0db54c 100644 --- a/apps/cli/src/commands/submission.ts +++ b/apps/cli/src/commands/submission.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { Command } from "commander"; +import { writeFile } from "node:fs/promises"; import { configureHelp } from "../utils/helpFormatter.js"; import { formatData, isMachineReadable } from "../utils/formatters.js"; import type { DisplayField, OutputFormat } from "../utils/types.js"; @@ -33,6 +34,7 @@ interface SubmissionSummary { runId: string; reason: string; }>; + report?: { reportId?: string; url?: string; updatedAt?: string }; } async function responseJson(response: Response): Promise { return response.json() as Promise; @@ -108,6 +110,9 @@ export function registerSubmissionCommands(program: Command): void { console.log(`${label("Attempt:")} ${value(String(doc.attemptNumber))}`); console.log(`${label("Readiness:")} ${value(doc.readiness)}`); console.log(`${label("Submission run:")} ${value(doc.submissionRunId)}`); + if (doc.report?.reportId) { + console.log(`${label("Readout report:")} ${value(doc.report.reportId)}`); + } for (const request of doc.requests) { console.log( ` ${value(request.requestId)} ${request.status}` + @@ -168,6 +173,52 @@ export function registerSubmissionCommands(program: Command): void { } }); + submission + .command("readout") + .description("Get the current generated submission readout") + .argument("", "Submission ID") + .option("--structured", "Download structured JSON instead of Markdown") + .option("-o, --file ", "Write output to a file") + .option("-u, --url ", "API base URL", getDefaultApiUrl()) + .action(async (id: string, options) => { + try { + const response = await apiFetch( + options.url, + `/submissions/${encodeURIComponent(id)}/readout`, + ); + if (!response.ok) await throwApiError(response); + const report = await responseJson<{ + _id: string; + content?: string; + structuredArtifact?: { schemaId: string; schemaVersion: string }; + }>(response); + let output: string; + if (options.structured) { + const structuredResponse = await apiFetch( + options.url, + `/reports/${encodeURIComponent(report._id)}/structured`, + ); + if (!structuredResponse.ok) await throwApiError(structuredResponse); + output = JSON.stringify( + await responseJson(structuredResponse), + null, + 2, + ); + } else { + output = report.content ?? ""; + } + if (options.file) { + await writeFile(options.file, `${output}\n`, "utf8"); + console.log(successText(`Readout written to ${options.file}`)); + } else { + console.log(output); + } + } catch (error) { + console.error(errorText("Error:"), error instanceof Error ? error.message : error); + process.exitCode = 1; + } + }); + submission .command("retry") .description("Retry the current submission processing attempt") diff --git a/apps/portal/src/components/SubmissionStatusCard.stories.tsx b/apps/portal/src/components/SubmissionStatusCard.stories.tsx index 0c465e7e9..13c973d70 100644 --- a/apps/portal/src/components/SubmissionStatusCard.stories.tsx +++ b/apps/portal/src/components/SubmissionStatusCard.stories.tsx @@ -61,6 +61,31 @@ type Story = StoryObj; export const Processing: Story = {}; +export const Retryable: Story = { + args: { + submission: { + ...submission, + readiness: "ready", + handlerProgress: { + "pp-readout": { status: "failed", version: 1, error: "Model timeout" }, + }, + }, + }, +}; + +export const Completed: Story = { + args: { + submission: { + ...submission, + readiness: "done", + report: { reportId: "report-123" }, + handlerProgress: { + "pp-readout": { status: "done", version: 1 }, + }, + }, + }, +}; + export const Blocked: Story = { args: { submission: { diff --git a/apps/portal/src/components/SubmissionStatusCard.tsx b/apps/portal/src/components/SubmissionStatusCard.tsx index f711bdd1c..dd44a6792 100644 --- a/apps/portal/src/components/SubmissionStatusCard.tsx +++ b/apps/portal/src/components/SubmissionStatusCard.tsx @@ -17,6 +17,15 @@ const readinessVariant: Record< failed: "destructive", }; +const readinessLabel: Record = { + waiting: "waiting", + ready: "retryable", + blocked: "blocked", + processing: "processing", + done: "completed", + failed: "failed", +}; + export function SubmissionStatusCard({ submission, }: { @@ -30,7 +39,7 @@ export function SubmissionStatusCard({ Processing - {submission.readiness} + {readinessLabel[submission.readiness]} diff --git a/apps/portal/src/components/TaxonomyTab.tsx b/apps/portal/src/components/TaxonomyTab.tsx deleted file mode 100644 index e97c8c8c4..000000000 --- a/apps/portal/src/components/TaxonomyTab.tsx +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { useQuery } from "@tanstack/react-query"; -import { api } from "@/lib/api"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Loader2, Download, ChevronDown, ChevronRight } from "lucide-react"; -import { useState } from "react"; - -interface TaxonomyDimension { - name: string; - score: number; - maxScore: number; - justification: string; -} - -interface BehaviorPattern { - id: string; - category: string; - observation: string; - evidence: string[]; - impact: "positive" | "negative" | "neutral"; -} - -interface ActionItem { - id: string; - priority: "high" | "medium" | "low"; - title: string; - description: string; - category: string; -} - -interface TaxonomyData { - meta?: { - schemaVersion?: string; - generatedAt?: string; - requestId?: string; - runId?: string; - model?: string; - }; - scorecard?: { - overall?: number; - dimensions?: TaxonomyDimension[]; - }; - behaviorAnalysis?: { - patterns?: BehaviorPattern[]; - }; - actionList?: { - items?: ActionItem[]; - }; -} - -function ScoreBar({ score, maxScore }: { score: number; maxScore: number }) { - const pct = Math.round((score / maxScore) * 100); - const color = pct >= 80 ? "bg-green-500" : pct >= 50 ? "bg-yellow-500" : "bg-red-500"; - return ( -
-
-
-
- {score}/{maxScore} -
- ); -} - -function ImpactBadge({ impact }: { impact: string }) { - const variant = impact === "positive" ? "default" : impact === "negative" ? "destructive" : "secondary"; - return {impact}; -} - -function PriorityBadge({ priority }: { priority: string }) { - const colors: Record = { - high: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300", - medium: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300", - low: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300", - }; - return {priority}; -} - -function CollapsibleSection({ title, defaultOpen = true, children }: { title: string; defaultOpen?: boolean; children: React.ReactNode }) { - const [open, setOpen] = useState(defaultOpen); - return ( -
- - {open &&
{children}
} -
- ); -} - -export function TaxonomyTab({ runId, attemptRunId }: { runId: string; attemptRunId?: string }) { - const url = attemptRunId ? api.runTaxonomyUrl(runId, attemptRunId) : api.taxonomyUrl(runId); - - const { data: taxonomy, isLoading, error } = useQuery({ - queryKey: ["run-taxonomy", runId, attemptRunId], - queryFn: async () => { - const res = await fetch(url); - if (!res.ok) { - if (res.status === 404) return null; - throw new Error(`Failed to fetch taxonomy: ${res.statusText}`); - } - return res.json(); - }, - }); - - if (isLoading) { - return ( -
- - Loading taxonomy… -
- ); - } - - if (error) { - return ( -
-

Failed to load taxonomy: {(error as Error).message}

-
- ); - } - - if (!taxonomy) { - return ( -
-

Taxonomy not yet generated for this run.

-

It will appear here once post-processing completes.

-
- ); - } - - const { meta, scorecard, behaviorAnalysis, actionList } = taxonomy; - - return ( -
- {/* Header with download */} -
-
-

Run Taxonomy

- {meta?.generatedAt && ( -

- Generated {new Date(meta.generatedAt).toLocaleString()} - {meta.model && <> · Model: {meta.model}} -

- )} -
- -
- - {/* Scorecard */} - {scorecard && ( - - - - Scorecard - {scorecard.overall !== undefined && ( - {scorecard.overall}% - )} - - - - {scorecard.dimensions?.map((dim) => ( -
-
- {dim.name} -
- -

{dim.justification}

-
- ))} -
-
- )} - - {/* Behavior Analysis */} - {behaviorAnalysis?.patterns && behaviorAnalysis.patterns.length > 0 && ( - - - - Behavior Analysis ({behaviorAnalysis.patterns.length}) - - - - -
- {behaviorAnalysis.patterns.map((pattern) => ( -
-
- {pattern.category} - -
-

{pattern.observation}

- {pattern.evidence.length > 0 && ( -
    - {pattern.evidence.map((e, i) =>
  • {e}
  • )} -
- )} -
- ))} -
-
-
-
- )} - - {/* Action List */} - {actionList?.items && actionList.items.length > 0 && ( - - - - Action Items ({actionList.items.length}) - - - - -
- {actionList.items.map((item) => ( -
- -
-

{item.title}

-

{item.description}

- {item.category} -
-
- ))} -
-
-
-
- )} - - {/* Raw JSON fallback — collapsed by default */} - -
-          {JSON.stringify(taxonomy, null, 2)}
-        
-
-
- ); -} diff --git a/apps/portal/src/lib/api.ts b/apps/portal/src/lib/api.ts index 1673dfb0a..31559b018 100644 --- a/apps/portal/src/lib/api.ts +++ b/apps/portal/src/lib/api.ts @@ -246,6 +246,23 @@ export const api = { getSubmission: (id: string): Promise => request(`/submissions/${id}`), + getSubmissionRuns: (id: string): Promise<{ + runs: Array<{ + _id: string; + attemptNumber: number; + requests: Array<{ requestId: string; runId: string }>; + createdAt: string; + report?: { reportId?: string; url?: string; updatedAt?: string }; + handlerStatus?: Submission["handlerProgress"]; + supersededAt?: string; + supersededBySubmissionRunId?: string; + supersededReason?: "request-vector-changed" | "manual-retry"; + }>; + }> => request(`/submissions/${id}/runs`), + + getSubmissionReadout: (id: string): Promise => + request(`/submissions/${id}/readout`), + retrySubmission: ( id: string, submissionRunId: string, @@ -377,16 +394,6 @@ export const api = { return `${BASE}/requests/${id}/atif${qs}`; }, - /** Get taxonomy JSON URL for a request (latest run) */ - taxonomyUrl: (id: string): string => { - return `${BASE}/requests/${id}/taxonomy`; - }, - - /** Get taxonomy JSON URL for a specific attempt */ - runTaxonomyUrl: (requestId: string, runId: string): string => { - return `${BASE}/requests/${requestId}/runs/${runId}/taxonomy`; - }, - /** Download a batch archive of multiple runs as a single .tar.gz */ batchArchive: async (ids: string[]): Promise => { const resp = await apiClient(`${BASE}/requests/archive`, { @@ -792,6 +799,9 @@ export const api = { return request(`/reports/${id}`); }, + reportStructuredUrl: (id: string): string => + `${BASE}/reports/${encodeURIComponent(id)}/structured`, + /** Get reports for a specific run */ getRunReports: (requestId: string): Promise => { return request(`/requests/${requestId}/reports`); diff --git a/apps/portal/src/pages/InsightDetail.tsx b/apps/portal/src/pages/InsightDetail.tsx index 02792b53b..6c875853c 100644 --- a/apps/portal/src/pages/InsightDetail.tsx +++ b/apps/portal/src/pages/InsightDetail.tsx @@ -215,12 +215,21 @@ export function InsightDetail() { - - {formatId(report.requestId)} - + {report.submissionId ? ( + + {formatId(report.submissionId)} + + ) : report.requestId ? ( + + {formatId(report.requestId)} + + ) : "–"} diff --git a/apps/portal/src/pages/ReportDetail.tsx b/apps/portal/src/pages/ReportDetail.tsx index 685e69792..c65b9bbab 100644 --- a/apps/portal/src/pages/ReportDetail.tsx +++ b/apps/portal/src/pages/ReportDetail.tsx @@ -12,7 +12,7 @@ import { Badge } from "@/components/ui/badge"; import { ReportStatusBadge } from "@/components/ReportStatusBadge"; import { LogViewer } from "@/components/LogViewer"; import { useLogStream } from "@/hooks/use-log-stream"; -import { ArrowLeft, Copy, Check, ExternalLink, ClipboardCopy, Lightbulb } from "lucide-react"; +import { ArrowLeft, Copy, Check, Download, ExternalLink, ClipboardCopy, Lightbulb } from "lucide-react"; import { formatDate } from "@/lib/utils"; import { useEffect, useRef, useState } from "react"; import { MarkdownRenderer } from "@/components/MarkdownRenderer"; @@ -131,6 +131,14 @@ export function ReportDetail() {
+ {report.structuredArtifact && ( + + )} {report.templateId && ( )} - - Run: {report.requestId.slice(0, 8)}… - - + {report.submissionId ? ( + + + Submission: {report.submissionId.slice(0, 8)}… + + + + ) : report.requestId ? ( + + Run: {report.requestId.slice(0, 8)}… + + + ) : null} Created {formatDate(report.createdAt)}
@@ -330,13 +350,24 @@ export function ReportDetail() { {report._id}
- Run ID:{" "} - - {report.requestId} - + + {report.submissionId ? "Submission run:" : "Run ID:"} + {" "} + {report.submissionId ? ( + + {report.submissionRunId} + + ) : report.requestId ? ( + + {report.requestId} + + ) : "—"}
Created:{" "} diff --git a/apps/portal/src/pages/ReportPreviewPanel.tsx b/apps/portal/src/pages/ReportPreviewPanel.tsx index 477ca8331..f6ad08a20 100644 --- a/apps/portal/src/pages/ReportPreviewPanel.tsx +++ b/apps/portal/src/pages/ReportPreviewPanel.tsx @@ -57,11 +57,19 @@ export function ReportPreviewPanel() { onClose={closePanel} headerActions={
- - - + {report.submissionId ? ( + + + + ) : report.requestId ? ( + + + + ) : null} + )} + + + {readoutQuery.data?.content ? ( +
+ + {readoutQuery.data.content} + +
+ ) : submission.readiness === "blocked" ? ( +

+ Readout generation is blocked by observation dependencies. +

+ ) : submission.readiness === "failed" ? ( +

+ Readout generation failed and can be retried. +

+ ) : ( +

+ {submission.readiness === "done" + ? "The current report is not available." + : "Waiting for the exact run vector and observations to be ready."} +

+ )} +
+ + {submission.blocking.length > 0 && ( @@ -157,6 +205,39 @@ export function SubmissionDetail() { })} + + + + Submission history + + + {(historyQuery.data?.runs ?? []).map((run) => ( +
+
+
Attempt {run.attemptNumber}
+
+ {run._id} · {run.requests.length} run{run.requests.length === 1 ? "" : "s"} +
+
+
+ + {run.supersededAt ? "superseded" : "current"} + + {run.report?.reportId && ( + + )} +
+
+ ))} +
+
); } diff --git a/apps/portal/src/types.ts b/apps/portal/src/types.ts index 4767e887e..82ca84832 100644 --- a/apps/portal/src/types.ts +++ b/apps/portal/src/types.ts @@ -187,7 +187,6 @@ export interface RunState { * instead. Empty/absent when no run-subject observations were selected. */ observationResults?: CriterionResult[]; - taxonomyUrl?: string; } export interface Run { @@ -251,6 +250,7 @@ export interface SubmissionBlockingDetail { reason: | "request-missing" | "run-changed" + | "dependency-not-ready" | "dependency-failed" | "dependency-blocked"; error?: string; @@ -567,10 +567,18 @@ export interface Reporter { export interface Report { _id: string; id: string; - requestId: string; + requestId?: string; + submissionId?: string; + submissionRunId?: string; task?: string; reporter?: Reporter; content?: string; + structuredArtifact?: { + schemaId: string; + schemaVersion: string; + contentType: "application/json"; + url: string; + }; status: ReportStatus; error?: string; insightReferences?: InsightReference[]; From bd136be3c4bb0de8d2501393d4488010c69fb63e Mon Sep 17 00:00:00 2001 From: Cedric Vidal Date: Tue, 13 Oct 2026 16:47:55 +0200 Subject: [PATCH 04/15] docs(readout): document submission report generation Describe submission-scoped readouts, refresh API artifacts, and register the production prompt contract and reviewed smoke case. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cbbd5eb-61c6-4e6a-bb38-c377be5b2163 --- AGENTS.md | 4 +- ENV_VARIABLES.md | 35 +++-- docs/architecture/app-design.md | 10 ++ docs/architecture/db-migrations.md | 2 +- docs/architecture/overview.md | 1 + docs/architecture/post-processing.md | 127 ++++++------------ docs/architecture/prompt-evaluations.md | 2 +- .../static-prompts/adapter/adapter.test.ts | 41 +++++- .../static-prompts/adapter/targets/reports.ts | 42 +++++- .../static-prompts/datasets/manifest.json | 10 +- .../static-prompts/datasets/v1/readouts.jsonl | 1 + .../static-prompts/evaluation-manifest.yaml | 2 +- .../scripts/dataset-adapter-contract.test.ts | 11 ++ .../scripts/validate-coverage.test.ts | 2 +- .../scripts/validate-dataset.ts | 18 ++- website/src/openapi/scope-openapi.json | 2 +- 16 files changed, 191 insertions(+), 119 deletions(-) create mode 100644 evaluations/static-prompts/datasets/v1/readouts.jsonl diff --git a/AGENTS.md b/AGENTS.md index d51fc5f64..6ce56eb73 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ apps/ coder-acp-copilot/ # GitHub Copilot worker (ACP SDK) coder-acp-claude-code/ # Claude Code worker (ACP SDK) report-generator/ # Post-run report generation - taxonomy/ # Post-run taxonomy generation + readout/ # Submission-level generated readout model-scanners/ # Feature detection for Copilot and Anthropic models version-checkers/ # Poll for new agent/tool releases key-updaters/ # GitHub auth cookie management for VS Code Web @@ -70,7 +70,7 @@ Each worker implements the same queue-processor interface but adapts a different | `coder-acp-copilot` | GitHub Copilot | ACP SDK v0.14.1 | | `coder-acp-claude-code` | Claude Code | ACP SDK v0.13.1 | | `report-generator` | — | Copilot SDK | -| `taxonomy` | — | Copilot SDK | +| `readout` | — | Copilot SDK | Workers consume tasks from Azure Storage Queues (named `queue-`) and write results to MongoDB and Blob Storage. Each has its own Dockerfile and docker-compose profile. diff --git a/ENV_VARIABLES.md b/ENV_VARIABLES.md index 503f16ff3..444397731 100644 --- a/ENV_VARIABLES.md +++ b/ENV_VARIABLES.md @@ -889,33 +889,32 @@ Timeout for the Copilot SDK session used by the report-generator worker. If the Git commit hash embedded in reporter metadata. Automatically set during CI/CD builds. Used to track which version of the report-generator produced a given report. -## Taxonomy Handler Configuration +## Submission Readout Handler Configuration -### AZURE_STORAGE_QUEUE_TAXONOMY -**Default:** `pp-taxonomy-queue` +### AZURE_STORAGE_QUEUE_READOUT +**Default:** `pp-readout-queue` **Type:** string -Azure Storage Queue name for taxonomy-generation jobs. The scheduler enqueues `{ type: "taxonomy", requestId, runId }` messages here after the `pp-atif` dependency completes, and the taxonomy worker polls this queue. +Azure Storage Queue name for submission readout jobs. The scheduler enqueues +`{ type: "readout", submissionId, submissionRunId }` only after +`pp-observations` is done for every exact request/run pair in the current +submission attempt. -### TAXONOMY_MODEL +### READOUT_MODEL **Default:** `gpt-5.4` **Type:** string -The LLM model used by the taxonomy worker (via the Copilot SDK) to generate structured taxonomy JSON from run metadata and ATIF trajectories. +The Copilot SDK model used by the dedicated `pp-readout` worker to synthesize a +schema-validated structured submission readout. Markdown is derived from the +validated structured object. -> **Model choice matters for taxonomy quality.** A/B testing across nine models on a multi-iteration run with a real defect (agent lost context in iteration 2) showed the verdict ranged from 72 to 100 depending on the model. `gpt-4.1` (the previous default) missed action items and, on harder runs, produced the thinnest output; weaker models rubber-stamped a buggy run with a perfect score. `gpt-5.4` was the best value: it correctly caught the critical defect while using the fewest tokens and lowest cost of the models tested. Stronger alternatives (e.g. `claude-opus-4-7`) yield richer evidence at higher token cost; avoid `gpt-5.3-codex` as a default (≈1M tokens / ~7 min per run). - -### SCOPE_MT_API_URL -**Default:** `http://localhost:3001` (local), `http://api:80` (Docker) -**Type:** URL string - -Base URL of the Scope API. The taxonomy worker calls this to fetch the request document and active run state through its Copilot tools. - -### SESSION_TIMEOUT_MS +### READOUT_SESSION_TIMEOUT_MS **Default:** `300000` (5 minutes) **Type:** integer (milliseconds) -Timeout for the Copilot SDK session used by the taxonomy worker. If the model does not finish generating a valid taxonomy JSON document within this window, the worker marks `run.handlerStatus.pp-taxonomy` as failed. +Timeout for one readout-generation attempt. Exhausted generation or schema +validation marks the exact current submission attempt's `pp-readout` handler as +failed; historical and superseded attempts remain immutable. ## Scheduler Configuration @@ -964,13 +963,13 @@ TTL for the DAG dispatcher's in-memory cache of handler topology (the `services` **Default:** `post-processor-queue` **Type:** string -Azure Storage Queue name used by both the scheduler (to enqueue post-processing work) and the post-processor worker (to dequeue). Must match between the two services. The `pp-taxonomy` observation handler shares this queue. +Azure Storage Queue name used by both the scheduler (to enqueue post-processing work) and the post-processor worker (to dequeue). Must match between the two services. The `pp-observations` handler shares this queue. ### JUDGE_URL **Default:** falls back to `JUDGE_SERVICE_URL`, then `http://localhost:3002` **Type:** URL string -Base URL of the judge service used by the `pp-taxonomy` post-process handler to evaluate observation criteria per iteration (snapshot + ATIF trajectory → boolean + evidence). The handler reuses the shared `JudgeClient`, so `JUDGE_CLIENT_TIMEOUT` / `JUDGE_CLIENT_RETRIES` apply. In K8s the post-processor reads `JUDGE_SERVICE_URL` from the `worker-config` ConfigMap (`envFrom`); in docker-compose the `post-processor` service sets `JUDGE_SERVICE_URL: http://judge:80` explicitly (it uses the `common-env` anchor, which — unlike `worker-env` — does not carry it). See issue #1156 and [docs/architecture/post-processing.md](docs/architecture/post-processing.md). +Base URL of the judge service used by the `pp-observations` post-process handler to evaluate observation criteria per iteration (snapshot + ATIF trajectory → boolean + evidence). The handler reuses the shared `JudgeClient`, so `JUDGE_CLIENT_TIMEOUT` / `JUDGE_CLIENT_RETRIES` apply. In K8s the post-processor reads `JUDGE_SERVICE_URL` from the `worker-config` ConfigMap (`envFrom`); in docker-compose the `post-processor` service sets `JUDGE_SERVICE_URL: http://judge:80` explicitly (it uses the `common-env` anchor, which — unlike `worker-env` — does not carry it). See issue #1156 and [docs/architecture/post-processing.md](docs/architecture/post-processing.md). ### SCHEDULER_URL **Default:** (not set) diff --git a/docs/architecture/app-design.md b/docs/architecture/app-design.md index 9ff0c010f..ec449d69e 100644 --- a/docs/architecture/app-design.md +++ b/docs/architecture/app-design.md @@ -528,6 +528,16 @@ discard a message when `claimSubmissionHandler()` returns `null`. Final writes use `completeSubmissionHandler()`, whose compare-and-swap rejects stale publication after supersession. +`pp-readout` writes a normal append-only `ReportDocument` directly; it does not +invoke report templates or the report-generator worker. Run reports retain +`requestId`. Submission reports instead carry `submissionId` and the exact +`submissionRunId`, with no report-kind discriminator. Their optional structured +artifact reference identifies `scope.submission-readout@1.0`, +`application/json`, and the blob URL. The Markdown `content` is rendered from +that validated object. Each successful regeneration uses a new report id, while +the current embedded submission run points only to its latest successful +report. + The API exposes: - `GET /api/v1/submissions?projectId=...` diff --git a/docs/architecture/db-migrations.md b/docs/architecture/db-migrations.md index ce8f2e673..d01bcc5aa 100644 --- a/docs/architecture/db-migrations.md +++ b/docs/architecture/db-migrations.md @@ -116,7 +116,7 @@ When you run a command: | `029-create-users-collection` | Creates the database-enforced unique identity index for authentication; uses Cosmos collection-creation extensions for continuous-backup accounts and preserves native MongoDB index creation (see below) | | `030-create-resource-indexes` | Creates project-scoped resource indexes | | `031-migrate-services-to-handler-dag` | Replaces the legacy single `post-processor` service row with per-handler service registrations and dependencies | -| `032-add-handler-status-index` | Adds handler-specific dispatch indexes for `pp-atif`, `pp-taxonomy`, and `pp-report` polling | +| `032-add-handler-status-index` | Adds a handler-specific dispatch index for `pp-atif` polling | | `033-backfill-handler-status` | Backfills `run.handlerStatus.pp-atif` from deprecated `run.postProcessorStatus` / `run.postProcessorVersion` fields | ### Migration 029: users identity uniqueness diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index e6b179e56..a71c60123 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -80,6 +80,7 @@ flowchart TB | `workers/coder-acp-copilot` | GitHub Copilot agent via Agent Client Protocol (ACP) | | `workers/report-generator` | Copilot SDK worker that generates post-run markdown reports | | `workers/post-processor` | DAG-based post-run handlers: `pp-atif` (trajectory ATIF) and `pp-observations` (boolean observations via the judge, per-iteration or whole-run by criterion `subject`) | +| `workers/readout` | Copilot SDK submission handler that generates schema-validated structured JSON and derived Markdown for an exact submission attempt | | `gateway` | AI Gateway — shared Rust TLS-intercepting proxy with plugin architecture (HAR capture, future: token refresh, rate limiting) | | `evaluations/static-prompts` | Developer-run static prompt quality and user-controlled prompt red-team suites | diff --git a/docs/architecture/post-processing.md b/docs/architecture/post-processing.md index 81a59285b..6fbd5a305 100644 --- a/docs/architecture/post-processing.md +++ b/docs/architecture/post-processing.md @@ -34,79 +34,15 @@ Two paths trigger post-processing: - Updates `run.turns.$.atifUrl` in the request document 4. On success, stamps `run.handlerStatus["pp-atif"] = { status: "done", version: N }` (while legacy `run.postProcessorVersion` / `run.postProcessorStatus` may still be mirrored during the transition) -5. Triggers the downstream taxonomy handler once the scheduler sees the dependency graph is satisfied. (Reports are **not** a DAG handler — see [Report Triggering](#report-triggering-on-dag-drain).) +5. Triggers `pp-observations` once the scheduler sees the dependency graph is + satisfied. Reports are not run-level DAG handlers. -## Taxonomy Generation - -The taxonomy handler (`apps/workers/taxonomy/`) consumes messages from the dedicated `pp-taxonomy-queue` and produces a structured JSON artifact for a completed run. It uses the GitHub Copilot SDK with a tool-backed session to fetch run metadata and ATIF trajectories from the API, then validates the generated JSON against the shared taxonomy Zod schema. - -### Flow - -1. Queue message arrives: `{ type: "taxonomy", requestId, runId }` -2. Worker fetches the request document from MongoDB and marks `run.handlerStatus["pp-taxonomy"].status = "processing"` -3. Copilot SDK session calls: - - `get_run_data` to fetch the request + active run from the API - - `get_atif_trajectory` to fetch per-iteration ATIF JSON from the API (`GET /api/v1/requests/:id/runs/:runId/atif?iteration=N`) -4. The worker validates the assistant response with `taxonomySchema.safeParse()` -5. If validation fails, the worker feeds the schema errors back into the same session and retries up to 3 total attempts -6. On success, the worker uploads `taxonomy.json` to `{requestId}/runs/{runId}/taxonomy.json` -7. The worker stamps `run.handlerStatus["pp-taxonomy"] = { status: "done", version: 1 }` and calls `POST /notify/handler-complete` - -### Blob Storage Layout - -```javascript -{requestId}/ - runs/{runId}/ - iteration-1/ - atif.trajectory.json - iteration-2/ - atif.trajectory.json - taxonomy.json -``` - -The completed run stores the taxonomy blob URL on `run.taxonomyUrl`. - -### API Endpoints +ATIF downloads remain available for the latest or an exact historical attempt: | Endpoint | Description | | --- | --- | | `GET /api/v1/requests/:id/atif?iteration=N` | Download ATIF for the latest run | | `GET /api/v1/requests/:id/runs/:runId/atif?iteration=N` | Download ATIF for a specific run | -| `GET /api/v1/requests/:id/taxonomy` | Download taxonomy JSON for the latest run | -| `GET /api/v1/requests/:id/runs/:runId/taxonomy` | Download taxonomy JSON for a specific run | - -The `iteration` query parameter is mandatory only for the ATIF endpoints. ATIF files are also included in archive exports as `iteration-{N}.atif.trajectory.json`. - -### Local troubleshooting: taxonomy CLI - -The Copilot SDK generation loop lives in `apps/workers/taxonomy/src/taxonomy-generator.ts` (`generateTaxonomy()`), shared by both the queue processor and a standalone developer CLI (`src/cli.ts`). The CLI invokes generation directly against an existing `requestId`, streams the session events live, and saves the validated taxonomy to a local file — **it never writes to MongoDB, blob storage, or the scheduler** (read-only). It is a developer tool and is intentionally not part of the Scope CLI. - -Because both taxonomy tools (`get_run_data` and `get_atif_trajectory`) read through the API, the CLI needs only an API URL and a Copilot token — no storage connection string. This lets it run against any environment (local or a remote deployment such as integration) given a reachable API. - -```bash -# Local stack (use your worktree's host-mapped API port): -SCOPE_MT_API_URL=http://localhost:3001 \ -GITHUB_TOKEN=$(gh auth token) \ -pnpm taxonomy [--run ] [--model ] [--timeout ] [-o out.json] [--stdout] [--quiet] - -# Integration (read-only against the deployed API): -SCOPE_MT_API_URL=https://msscope-int.azurewebsites.net \ -GITHUB_TOKEN=$(gh auth token) \ -pnpm taxonomy -o taxonomy-int.json -``` - -The root `pnpm taxonomy` script proxies to `pnpm --filter taxonomy-handler taxonomy` (i.e. `tsx src/cli.ts`). - -| Option | Description | -| --- | --- | -| `--run, -r ` | Run id used in the prompt/logging (default: active run resolved from the API) | -| `--model ` | Override `TAXONOMY_MODEL` (e.g. A/B a reasoning model vs the `gpt-5.4` default) | -| `--timeout ` | Override `SESSION_TIMEOUT_MS` (per-attempt session timeout) | -| `--output, -o ` | Output file path (default: `taxonomy-.json` in the cwd) | -| `--stdout` | Also print the taxonomy JSON to stdout | -| `--quiet` | Suppress live session-event streaming on stderr | - -Live progress (tool calls, errors, deltas) is written to stderr so the saved JSON file stays clean. The CLI exits non-zero on failure (empty response, JSON/schema validation failure after 3 attempts, timeout, or model rate limit) and surfaces the underlying error message — making it well suited to diagnosing the oversized-`get_atif_trajectory` paging loop and Copilot model rate limits. ## Taxonomy Observations (`pp-observations`) @@ -140,7 +76,7 @@ Gate criteria are always `subject: "iteration"` (the API rejects `"run"` for gat 3. **Iteration-subject:** for each iteration with a `snapshotUrl`, calls the existing judge via the shared `JudgeClient` (`POST /api/v1/evaluate`) with `snapshotUrl` + `atifUrl` + the iteration-subject ids. Writes the returned `{criterionId, passed, feedback}` to that turn's **`turn.observationResults`**. 4. **Run-subject:** evaluates **once** with `snapshotUrl` = final turn's snapshot, `atifUrls` = every turn's `atifUrl` (the judge downloads + merges them into one `AgentTrajectory`), and the full `conversationHistory`. Writes the verdicts to **`run.observationResults`** (run level). - In all judge calls there is no persona, no gate, no taxonomyElementId (the classifier lives on the criterion). ATIF supersedes per-turn `toolCallsUrl`, so only ATIF is sent. - - Result shape/storage mirrors gate `criteriaResults`. No new blob, no `run.taxonomyUrl`. + - Result shape/storage mirrors gate `criteriaResults`; no separate run artifact is created. - **Runs exactly once per run:** the scheduler dispatches `pp-observations` a single time (it never sets `message.iteration`), so the run-subject branch needs no idempotency guard. 5. Stamps `run.handlerStatus["pp-observations"] = { status: "done", version: N }` and notifies the scheduler. @@ -217,7 +153,9 @@ The `AtifHandler` has two complementary test suites: The post-processor uses a version-based re-processing scheme: -- Each handler is registered independently in the `services` collection using a `post-process-handler` document keyed by handler ID (for example `pp-atif`, `pp-taxonomy`) +- Each run handler is registered independently in the `services` collection + using a `post-process-handler` document keyed by handler ID (for example + `pp-atif` or `pp-observations`) - Each registration stores the handler `version`, queue name, selector, `autoBackfill` behavior, and `dependsOn` DAG edges - The scheduler compares each request's per-handler status/version state (for example `run.handlerStatus["pp-atif"]`) against the registered handler entry for that selector - Requests with missing or outdated handler state are re-dispatched for that specific handler once its dependencies are satisfied @@ -226,15 +164,17 @@ This allows deploying handler improvements and having them automatically applied ## Handler Registration -Each post-process handler declares its DAG metadata in a `handler.yaml` file colocated with the worker (e.g. `apps/workers/taxonomy/handler.yaml`). This mirrors the coding-agent `agent.yaml` registration pattern. +Each post-process handler declares its DAG metadata in a `handler.yaml` file +colocated with the worker. This mirrors the coding-agent `agent.yaml` +registration pattern. ```yaml -# apps/workers/taxonomy/handler.yaml -_id: pp-taxonomy +# apps/workers/post-processor/handler.taxonomy.yaml +_id: pp-observations type: post-process-handler version: 1 -queue: pp-taxonomy-queue -selector: taxonomy +queue: post-processor-queue +selector: observations autoBackfill: false dependsOn: - pp-atif @@ -266,7 +206,7 @@ name run-level DAG handlers, not other submission handlers. The scheduler reconciles each submission's exact request/run vector, projects those named dependencies, and enqueues only the current embedded submission attempt. -The first consumer is `pp-readout`, which depends on `pp-observations` for every +The submission consumer is `pp-readout`, which depends on `pp-observations` for every run in the vector. Its queue contract is: ```json @@ -285,12 +225,28 @@ remains at-least-once; the embedded current attempt is the only dispatchable state, and the scheduler poll repairs missed notifications, stale projections, request-vector drift, and interrupted rollover/finalization. +The dedicated worker in `apps/workers/readout/` loads the exact embedded vector +from current or archived request attempts; it never substitutes a newer current +run. It aggregates ATIF trajectories, run- and iteration-level observations, +criteria definitions, taxonomy element IDs, outcomes, profiles, and request +metadata. The model returns schema-limited synthesis fields. Code then composes +canonical counts and provenance, validates the complete +`scope.submission-readout@1.0` object, and derives Markdown from that object. + +Each successful generation creates a new report document without a template or +report kind. The JSON artifact is uploaded to +`reports/{reportId}/structured.json`; Markdown stays inline on the report. +`SubmissionRunState.report` is compare-and-swap updated only for the exact +current attempt. Reports from previous generations and superseded attempts +remain append-only history. + To **add or change a handler**, edit its `handler.yaml` (bump `version`, adjust `dependsOn`, etc.) — no code or migration changes are needed for registration. ## Report Triggering on DAG Drain -Reports are intentionally **not** modeled as a DAG handler. The report-generator -worker is keyed by `reportId` and requires a `templateId`, whereas the DAG +Run reports are intentionally **not** modeled as a DAG handler. The +report-generator worker is keyed by `reportId` and requires a `templateId`, +whereas the run DAG dispatches a generic `{ type, requestId, runId }` message — so a report node could never process a DAG message. Reports are also fundamentally different from post-process handlers: they are parameterized (per `templateId`), N-per-run (one @@ -303,7 +259,7 @@ active template's trigger and creates the properly-templated `ReportDocument`(s) ```mermaid flowchart LR - RT["run done"] --> DAG["scheduler dispatches DAG
(pp-atif → pp-taxonomy)"] + RT["run done"] --> DAG["scheduler dispatches DAG
(pp-atif → pp-observations)"] DAG -->|"all handlers terminal"| DR{DAG drained?} DR -->|yes| TRIG["POST /api/v1/reports/trigger
{ requestId }"] TRIG --> API["API evaluates each template trigger
→ ReportDocument + report-queue {reportId}"] @@ -316,7 +272,7 @@ flowchart LR registered handler is terminal. A run with **zero** registered handlers is vacuously drained, so reports trigger immediately on run completion. -**Trigger regardless of handler success.** A flaky taxonomy must not block report +**Trigger regardless of handler success.** A flaky unrelated handler must not block report generation (reports analyze the run/ATIF), so the scheduler triggers reports whenever the DAG drains — not only when every handler succeeded. @@ -360,21 +316,20 @@ stateDiagram-v2 | Environment Variable | Default | Description | | --- | --- | --- | | `AZURE_STORAGE_QUEUE_POSTPROCESSOR` | `post-processor-queue` | Queue name for ATIF post-processor messages | -| `AZURE_STORAGE_QUEUE_TAXONOMY` | `pp-taxonomy-queue` | Queue name for taxonomy-generation jobs | +| `AZURE_STORAGE_QUEUE_READOUT` | `pp-readout-queue` | Queue name for exact-attempt submission readouts | | `SCHEDULER_PP_POLL_INTERVAL_MS` | `30000` | Scheduler backfill polling interval | | `BATCH_SIZE` | `1` | Messages to process per poll (worker-side) | | `POLL_INTERVAL_MS` | `5000` | Worker queue polling interval | -| `SCOPE_MT_API_URL` / `API_BASE_URL` | `http://localhost:3001` | API URL used by report/taxonomy Copilot tools | -| `TAXONOMY_MODEL` | `gpt-5.4` | Copilot SDK model used by the taxonomy handler | -| `SESSION_TIMEOUT_MS` | `300000` | Timeout for report/taxonomy Copilot SDK sessions | +| `READOUT_MODEL` | `gpt-5.4` | Copilot SDK model used by the submission readout worker | +| `READOUT_SESSION_TIMEOUT_MS` | `300000` | Timeout for one readout generation attempt | | `SCHEDULER_URL` | _(unset)_ | Scheduler base URL — used by handlers to notify completion and by the register-handler script to reach `/handlers/register` | | `API_URL` | `http://api:80` | API base URL the **scheduler** uses to call `POST /api/v1/reports/trigger` when a run's handler DAG drains | | `HANDLER_YAML_PATH` | _(unset)_ | Path to a handler's `handler.yaml`, read by the generic `register-handler` script | ## Infrastructure -- **Queues:** Azure Storage Queues (`post-processor-queue` for ATIF, `pp-taxonomy-queue` for taxonomy, `report-queue` for reports) -- **Blob Storage:** `snapshots` container (shared with HAR, tool-calls, and other artifacts) -- **Database:** MongoDB `requests` collection (`run.handlerStatus` map keyed by handler ID, with deprecated `run.postProcessorStatus` / `run.postProcessorVersion` compatibility fields during migration) +- **Queues:** Azure Storage Queues (`post-processor-queue` for ATIF and observations, `pp-readout-queue` for submission readouts, `report-queue` for templated run reports) +- **Blob Storage:** `snapshots` container, including `reports/{reportId}/structured.json` +- **Database:** MongoDB `requests`, `runs`, `submissions`, `submission-runs`, and append-only `reports` collections - **KEDA:** ScaledObject scales the worker to zero when queue is empty - **Handler registration:** declarative `handler.yaml` per worker → generic `register-handler` script → scheduler `POST /handlers/register` (sole writer to the `services` collection). A Kubernetes Job runs the script on each deploy; Docker Compose uses per-handler init services. See [Handler Registration](#handler-registration). \ No newline at end of file diff --git a/docs/architecture/prompt-evaluations.md b/docs/architecture/prompt-evaluations.md index 1bdc0636a..ba40805f4 100644 --- a/docs/architecture/prompt-evaluations.md +++ b/docs/architecture/prompt-evaluations.md @@ -30,7 +30,7 @@ families: | Prompt-feature extraction | Positive, negative, ambiguous, ancestor/descendant, and empty-feature cases | | Judge instructions | Bundled and independent strategies; file, tool-history, response, conflicting, missing, and prior-pass evidence | | Developer feedback | Default persona, custom persona, descendant guard on/off, and multiple root failures | -| Run reports | Default and appended system prompts; override as a control where the default prompt is intentionally absent | +| Run reports and submission readouts | Default and appended run-report system prompts; override as a control where the default prompt is intentionally absent; exact-vector submission readout synthesis | Judge and report tool descriptions are static AI-facing instructions, but they are evaluated through their owning end-to-end judge and report families rather diff --git a/evaluations/static-prompts/adapter/adapter.test.ts b/evaluations/static-prompts/adapter/adapter.test.ts index 4cc89ef9f..a5380d9f7 100644 --- a/evaluations/static-prompts/adapter/adapter.test.ts +++ b/evaluations/static-prompts/adapter/adapter.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; import { REPORT_SYSTEM_PROMPT } from "../../../packages/shared/src/index.js"; +import { READOUT_SYSTEM_PROMPT } from "../../../apps/workers/readout/src/prompt.js"; import { buildCriteriaAuthoringRequest, buildCriteriaDependencySuggestionRequest, @@ -66,7 +67,12 @@ describe("static prompt adapter registry", () => { }, { family: "run-report", - variants: ["default", "append", "override-control"], + variants: [ + "default", + "append", + "override-control", + "submission-readout", + ], }, ]); expect(listPromptTargets().map(({ family }) => family)).toEqual([ @@ -95,6 +101,11 @@ describe("static prompt adapter registry", () => { family: "run-report", variant: "override-control", }); + expect(targets).toContainEqual({ + adapterId: "run-report/submission-readout", + family: "run-report", + variant: "submission-readout", + }); expect(JSON.parse(JSON.stringify(targets))).toEqual(targets); }); }); @@ -422,4 +433,32 @@ describe("session prompt adapters", () => { ]), ); }); + + it("composes the exact submission-readout prompt without tools", async () => { + const evidence = { + submissionId: "submission-1", + submissionRunId: "submission-run-1", + runs: [], + }; + const result = await getPromptTarget( + "run-report", + "submission-readout", + ).run( + { evidence }, + "submission-readout", + fakeContext(() => "{}"), + ); + + expect(result.request.messages).toEqual([ + { role: "system", content: READOUT_SYSTEM_PROMPT }, + { + role: "user", + content: [ + "Generate the submission readout synthesis for this exact immutable evidence vector.", + JSON.stringify(evidence), + ].join("\n\n"), + }, + ]); + expect(result.request.tools).toEqual([]); + }); }); diff --git a/evaluations/static-prompts/adapter/targets/reports.ts b/evaluations/static-prompts/adapter/targets/reports.ts index 696ff31b3..bef13c8e5 100644 --- a/evaluations/static-prompts/adapter/targets/reports.ts +++ b/evaluations/static-prompts/adapter/targets/reports.ts @@ -4,6 +4,10 @@ import type { ReportTemplateSystemPrompt } from "../../../../packages/shared/src/index.js"; import { createReportTools } from "../../../../apps/workers/report-generator/src/tools.js"; import { resolveReportPrompts } from "../../../../apps/workers/report-generator/src/report-queue-processor.js"; +import { + READOUT_SYSTEM_PROMPT, + composeReadoutUserPrompt, +} from "../../../../apps/workers/readout/src/prompt.js"; import type { CompletionOptions, ComposedPromptRequest, @@ -213,6 +217,32 @@ export function composeReportRequest( variant: string, defaultModel: string, ): ComposedPromptRequest { + if (variant === "submission-readout") { + const value = record(input); + const evidence = record(value.evidence, "input.evidence"); + return { + messages: [ + { role: "system", content: READOUT_SYSTEM_PROMPT }, + { + role: "user", + content: composeReadoutUserPrompt( + evidence as unknown as Parameters[0], + ), + }, + ], + model: + optionalString(value.model, "input.model") ?? defaultModel, + tools: [], + metadata: { + adapterId: "run-report/submission-readout", + productionSources: [ + "apps/workers/readout/src/prompt.ts#READOUT_SYSTEM_PROMPT", + "apps/workers/readout/src/readout-generator.ts#composeReadoutUserPrompt", + ], + fidelity: "exact", + }, + }; + } return composeParsedReportRequest( parseReportInput(input, variant), variant, @@ -222,8 +252,18 @@ export function composeReportRequest( export const reportAdapter: PromptTargetAdapter = { family: "run-report", - variants: ["default", "append", "override-control"], + variants: ["default", "append", "override-control", "submission-readout"], async run(input, variant, context) { + if (variant === "submission-readout") { + const request = composeReportRequest(input, variant, context.model); + const completion = await context.complete(request); + return { + request, + rawResponse: completion.content, + output: completion.content.trim(), + invocationMetadata: completion.metadata, + }; + } const parsed = parseReportInput(input, variant); const request = composeParsedReportRequest(parsed, variant, context.model); const completion = await context.complete( diff --git a/evaluations/static-prompts/datasets/manifest.json b/evaluations/static-prompts/datasets/manifest.json index 06fcc0e6c..21a8b112b 100644 --- a/evaluations/static-prompts/datasets/manifest.json +++ b/evaluations/static-prompts/datasets/manifest.json @@ -36,7 +36,7 @@ "prompt-feature-extraction": 30, "judge-instructions": 30, "developer-feedback": 25, - "run-report": 30 + "run-report": 31 } }, "files": [ @@ -90,6 +90,14 @@ "run-report" ] }, + { + "path": "v1/readouts.jsonl", + "sha256": "a23f372bf8d4c4cb6668a2094facfe284bf8030685e6405b0c76a03a1f19f1e4", + "rows": 1, + "families": [ + "run-report" + ] + }, { "path": "v1/task-prompts.jsonl", "sha256": "58c8fd827afddcc197bef8013817d2aea0fd68667bcc0e71bb4ad6055c3aed06", diff --git a/evaluations/static-prompts/datasets/v1/readouts.jsonl b/evaluations/static-prompts/datasets/v1/readouts.jsonl new file mode 100644 index 000000000..9ba053ab2 --- /dev/null +++ b/evaluations/static-prompts/datasets/v1/readouts.jsonl @@ -0,0 +1 @@ +{"schemaVersion":1,"id":"run-report-0123456789abcdef","family":"run-report","variant":"submission-readout","smoke":true,"input":{"evidence":{"submissionId":"submission-1","submissionRunId":"submission-run-1","runs":[{"request":{"_id":"request-1","projectId":"project-1","workerType":"coder-acp-copilot","profileId":"profile-1","observations":["uses-current-api"],"scenario":{"task":"Update the API","criteria":[]},"createdAt":"2026-10-13T12:00:00.000Z","priority":0},"run":{"_id":"run-1","attemptNumber":1,"status":"done","outcome":"failed","observationResults":[{"criterionId":"uses-current-api","passed":false,"feedback":"The deprecated API remained.","evaluated":true}],"turns":[]},"criteria":[{"id":"uses-current-api","projectId":"project-1","prompt":"Use the current API","kind":"observation","subject":"run","taxonomyElementId":"dimension:dependency-currency","createdAt":"2026-10-13T12:00:00.000Z"}],"trajectories":[]}]}},"expected":{"referenceReport":"{\"title\":\"Submission readout\",\"executiveSummary\":\"The exact run failed and did not satisfy the selected observation.\",\"profiles\":[{\"profileId\":\"profile-1\",\"requestIds\":[\"request-1\"],\"summary\":\"One profiled run.\"}],\"runs\":[{\"requestId\":\"request-1\",\"runId\":\"run-1\",\"trajectoryHighlights\":[]}],\"findings\":[{\"id\":\"finding-1\",\"title\":\"Deprecated API remained\",\"description\":\"The final run evidence failed the selected observation.\",\"severity\":\"high\",\"evidence\":[{\"requestId\":\"request-1\",\"runId\":\"run-1\",\"criterionId\":\"uses-current-api\",\"excerpt\":\"The deprecated API remained.\"}]}],\"actions\":[{\"id\":\"action-1\",\"title\":\"Update the API usage\",\"priority\":\"high\",\"description\":\"Replace the deprecated API and verify the final state.\",\"relatedFindingIds\":[\"finding-1\"]}]}"},"evaluators":["generation_success","output_schema","non_empty_report","no_fabricated_references","relevance","coherence","groundedness","task_adherence","report_quality"],"tags":["submission-readout","failed"],"provenance":{"kind":"synthetic-reviewed","sourceEndpoints":[],"projectId":"synthetic-project","sourceEntityIds":["submission-1","submission-run-1","request-1","run-1"],"harvestedAt":"2026-10-13T12:00:00.000Z","selectionSeed":"scope-static-prompts-v1","sourceHash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","note":"Reviewed synthetic exact-vector case covering failed coding outcome and passed:false observation evidence."},"review":{"status":"approved","method":"manual-contract-review-v1"}} diff --git a/evaluations/static-prompts/evaluation-manifest.yaml b/evaluations/static-prompts/evaluation-manifest.yaml index e35c81865..975574108 100644 --- a/evaluations/static-prompts/evaluation-manifest.yaml +++ b/evaluations/static-prompts/evaluation-manifest.yaml @@ -19,4 +19,4 @@ qualityFamilies: - id: developer-feedback variants: [default, persona, descendant-guard] - id: run-report - variants: [default, append, override-control] + variants: [default, append, override-control, submission-readout] diff --git a/evaluations/static-prompts/scripts/dataset-adapter-contract.test.ts b/evaluations/static-prompts/scripts/dataset-adapter-contract.test.ts index 7a8771043..943edc312 100644 --- a/evaluations/static-prompts/scripts/dataset-adapter-contract.test.ts +++ b/evaluations/static-prompts/scripts/dataset-adapter-contract.test.ts @@ -57,6 +57,17 @@ function fakeResponse(row: QualityCaseRow, request: ComposedPromptRequest): stri case "developer-feedback": return "Implement the missing requirement and rerun the relevant check."; case "run-report": + if (row.variant === "submission-readout") { + expect(request.tools).toEqual([]); + return JSON.stringify({ + title: "Submission readout", + executiveSummary: "The exact vector was reviewed.", + profiles: [], + runs: [], + findings: [], + actions: [], + }); + } expect(request.tools?.length).toBeGreaterThan(0); return "# Run report\n\nThe run evidence was reviewed."; } diff --git a/evaluations/static-prompts/scripts/validate-coverage.test.ts b/evaluations/static-prompts/scripts/validate-coverage.test.ts index 66cd57fca..f525c4e76 100644 --- a/evaluations/static-prompts/scripts/validate-coverage.test.ts +++ b/evaluations/static-prompts/scripts/validate-coverage.test.ts @@ -6,6 +6,6 @@ import { validateCoverage } from "./validate-coverage.js"; it("covers every quality manifest family and variant", async () => { await expect(validateCoverage()).resolves.toEqual({ - qualityTargets: 15, + qualityTargets: 16, }); }); diff --git a/evaluations/static-prompts/scripts/validate-dataset.ts b/evaluations/static-prompts/scripts/validate-dataset.ts index 395c09cb8..608d5cdf9 100644 --- a/evaluations/static-prompts/scripts/validate-dataset.ts +++ b/evaluations/static-prompts/scripts/validate-dataset.ts @@ -181,9 +181,13 @@ function validateFamilyContract(row: DatasetCase, prefix: string, errors: string assert(typeof input.includeDescendantGuard === "boolean", `${prefix}: input.includeDescendantGuard must be boolean`, errors); break; case "run-report": - assert(stringField(input, "requestId"), `${prefix}: input.requestId is required`, errors); - assert(stringField(input, "reportId"), `${prefix}: input.reportId is required`, errors); - assert(stringField(input, "userPrompt"), `${prefix}: input.userPrompt is required`, errors); + if (row.variant === "submission-readout") { + assert(isRecord(input.evidence), `${prefix}: input.evidence is required`, errors); + } else { + assert(stringField(input, "requestId"), `${prefix}: input.requestId is required`, errors); + assert(stringField(input, "reportId"), `${prefix}: input.reportId is required`, errors); + assert(stringField(input, "userPrompt"), `${prefix}: input.userPrompt is required`, errors); + } if (row.variant === "append" || row.variant === "override-control") { assert(stringField(input, "systemPromptContent"), `${prefix}: input.systemPromptContent is required for ${row.variant}`, errors); } @@ -345,9 +349,13 @@ export async function validateDataset( assert( syntheticCases.every((row) => row.family === "judge-instructions" - || (row.family === "run-report" && (row.variant === "default" || row.variant === "override-control")) + || (row.family === "run-report" && ( + row.variant === "default" + || row.variant === "override-control" + || row.variant === "submission-readout" + )) || (row.family === "developer-feedback" && row.variant === "persona")), - "synthetic cases are only permitted for unavailable judge evidence, feedback persona, and report override controls", + "synthetic cases are only permitted for unavailable judge evidence, feedback persona, report override controls, and submission readouts", errors, ); const hasTag = (family: QualityFamily, tag: string): boolean => diff --git a/website/src/openapi/scope-openapi.json b/website/src/openapi/scope-openapi.json index eb0e8dfd8..9f963ac5e 100644 --- a/website/src/openapi/scope-openapi.json +++ b/website/src/openapi/scope-openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"Scope API","version":"1.0.0","description":"REST API for the Scope platform — benchmarking AI coding agents"},"servers":[{"url":"/","description":"Current server"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Use the unchanged IdP access token, without the Bearer prefix."}},"schemas":{"KeyResponse":{"type":"object","properties":{},"additionalProperties":{}},"KeyInput":{"type":"object","properties":{},"additionalProperties":{}},"ValidateKeyInput":{"type":"object","properties":{"token":{"type":"string"}},"required":["token"]},"AccountResponse":{"type":"object","properties":{},"additionalProperties":{}},"AccountInput":{"type":"object","properties":{},"additionalProperties":{}},"UserMeResponse":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string"},"email":{"type":"string"},"displayName":{"type":"string"},"idp":{"type":"string"},"idpTenant":{"type":"string"}},"required":["id"]},"ProjectResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","createdAt"]},"CreateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"creator":{"type":"string"}},"required":["name"]},"UpdateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"SubmissionListResponse":{"type":"object","properties":{"submissions":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionResponse"}}},"required":["submissions"]},"SubmissionResponse":{"type":"object","properties":{"submissionId":{"type":"string"},"projectId":{"type":"string"},"requestIds":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"submissionRunId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["waiting","ready","blocked","processing","done","failed"]},"requests":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRequestSummary"}},"dependencyProjection":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionDependencyProjection"}},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"}},"handlerProgress":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/SubmissionHandlerStatus"}},"report":{"$ref":"#/components/schemas/SubmissionReportPointer"},"supersededAt":{"type":["string","null"],"format":"date-time"},"supersededBySubmissionRunId":{"type":"string"},"supersededReason":{"type":"string","enum":["request-vector-changed","manual-retry"]}},"required":["submissionId","projectId","requestIds","createdAt","submissionRunId","attemptNumber","readiness","requests","dependencyProjection","blocking"]},"SubmissionRequestSummary":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"task":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"},"workerType":{"type":"string"},"model":{"type":"string"},"profileId":{"type":"string"}},"required":["requestId","runId","task","status","workerType"]},"SubmissionDependencyProjection":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"state":{"type":"string","enum":["waiting","ready","failed","blocked"]},"version":{"type":"number"},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["requestId","runId","handlerId","state"]},"SubmissionBlockingDetail":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"reason":{"type":"string","enum":["request-missing","run-changed","dependency-not-ready","dependency-failed","dependency-blocked"]},"error":{"type":"string"}},"required":["requestId","runId","handlerId","reason"]},"SubmissionHandlerStatus":{"type":"object","properties":{"status":{"type":"string","enum":["queued","processing","done","failed"]},"version":{"type":"number"},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["status"]},"SubmissionReportPointer":{"type":"object","properties":{"reportId":{"type":"string"},"url":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"}}},"SubmissionRunResponse":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"vectorFingerprint":{"type":"string"},"requests":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRequestRef"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"dependencyProjection":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionDependencyProjection"},"default":[]},"handlerStatus":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/SubmissionHandlerStatus"}},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"},"default":[]},"report":{"$ref":"#/components/schemas/SubmissionReportPointer"},"supersededAt":{"type":["string","null"],"format":"date-time"},"supersededBySubmissionRunId":{"type":"string"},"supersededReason":{"type":"string","enum":["request-vector-changed","manual-retry"]},"submissionId":{"type":"string"},"projectId":{"type":"string"}},"required":["_id","attemptNumber","vectorFingerprint","requests","createdAt"]},"SubmissionRequestRef":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"}},"required":["requestId","runId"]},"SubmissionRetryResponse":{"type":"object","properties":{"submissionId":{"type":"string"},"submissionRunId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["waiting","ready","blocked","processing","done","failed"]},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"}}},"required":["submissionId","submissionRunId","attemptNumber","readiness","blocking"]},"RequestResponse":{"type":"object","properties":{"_id":{"type":"string"},"scenario":{"$ref":"#/components/schemas/Scenario"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"maxIterations":{"type":"number"},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"deletedAt":{"type":["string","null"],"format":"date-time"},"taskPromptId":{"type":"string"},"agentsMdPromptId":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBinding"}},"extensions":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"profileId":{"type":"string"},"profileVersionId":{"type":"string"},"submissionId":{"type":"string"},"submissionExpectedRequestCount":{"type":"integer","minimum":1},"submissionRequestIndex":{"type":"integer","minimum":0},"submissionInitialRunId":{"type":"string"},"submissionFinalizedAt":{"type":["string","null"],"format":"date-time"},"priority":{"type":"integer","default":0},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"observations":{"type":"array","items":{"type":"string"}},"gateSummaries":{"type":"array","items":{"$ref":"#/components/schemas/GateRunSummary"}},"run":{"$ref":"#/components/schemas/RunState"},"projectId":{"type":"string"}},"required":["_id","scenario","workerType","createdAt","projectId"]},"Scenario":{"type":"object","properties":{"version":{"type":"string","enum":["v1","v2"]},"task":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}}},"required":["task","criteria"]},"Persona":{"type":"object","properties":{"personality":{"type":"string","enum":["demanding","friendly"]},"experience":{"type":"string","enum":["junior","senior"]},"verbosity":{"type":"string","enum":["brief","moderate"]},"type":{"type":"string","enum":["traditional","ai_assisted","vibe"]}},"required":["personality","experience","verbosity","type"]},"ResourceBinding":{"type":"object","properties":{"ref":{"type":"string"},"revisionId":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref","revisionId","params"]},"GateConfig":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"promptId":{"type":"string"},"promptText":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}},"maxIterations":{"type":"integer","minimum":1,"maximum":50}},"required":["gate","criteria"]},"GateRunSummary":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"status":{"type":"string","enum":["passed","failed","skipped"]},"iterations":{"type":"integer","minimum":0}},"required":["gate","status","iterations"]},"RunState":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"queuedQueueName":{"type":"string"},"outcome":{"type":"string","enum":["succeeded","failed","finished"]},"result":{"type":"string"},"error":{"type":"string"},"logsUrl":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"turns":{"type":"array","items":{"$ref":"#/components/schemas/ConversationTurn"}},"workerVersion":{"type":"string"},"os":{"type":"object","properties":{"platform":{"type":"string"},"release":{"type":"string"},"arch":{"type":"string"}},"required":["platform","release","arch"]},"lastHeartbeatAt":{"type":["string","null"],"format":"date-time"},"worker":{"type":"object","properties":{"instanceId":{"type":"string"},"podName":{"type":"string"}},"required":["instanceId"]},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"setupVideoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"aiCallCount":{"type":"number"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRunOutcome"}},"mcpRegistered":{"type":"boolean"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"pausedAt":{"type":["string","null"],"format":"date-time"},"resumedAt":{"type":["string","null"],"format":"date-time"},"observationResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}}},"required":["_id","attemptNumber","status"]},"ConversationTurn":{"type":"object","properties":{"iteration":{"type":"number"},"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"codingAgentResponse":{"type":"string"},"judgeFeedback":{"type":"string"},"snapshotUrl":{"type":"string"},"passed":{"type":"boolean"},"timestamp":{"type":["string","null"],"format":"date-time"},"criteriaResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"observationResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"startedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"toolCalls":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"arguments":{"type":"object","additionalProperties":{}},"response":{"type":"string"},"timestamp":{"type":"string"}},"required":["id","name","arguments"]}},"toolCallsUrl":{"type":"string"},"toolCallCount":{"type":"number"},"aiCallCount":{"type":"number"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"chatResultUrl":{"type":"string"},"chatResultFormat":{"type":"string"}},"required":["iteration","judgeFeedback","snapshotUrl","passed","timestamp"]},"CriterionResult":{"type":"object","properties":{"criterionId":{"type":"string"},"passed":{"type":"boolean"},"feedback":{"type":"string"},"evaluated":{"type":"boolean"}},"required":["criterionId","passed","feedback","evaluated"]},"TokenUsage":{"type":"object","properties":{"promptTokens":{"type":"number"},"completionTokens":{"type":"number"},"totalTokens":{"type":"number"}},"required":["promptTokens","completionTokens","totalTokens"]},"ResourceRunOutcome":{"type":"object","properties":{"ref":{"type":"string"},"slug":{"type":"string"},"revisionId":{"type":"string"},"setupSucceeded":{"type":"boolean"},"published":{"type":"array","items":{"type":"string"}},"params":{"type":"object","additionalProperties":{"type":"string"}},"setupDurationMs":{"type":"number"},"error":{"type":"string"},"teardownRan":{"type":"boolean"}},"required":["ref","slug","revisionId","setupSucceeded","published"]},"ResourceBindingSpec":{"anyOf":[{"type":"string"},{"type":"object","properties":{"ref":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref"]}]},"CreateRequestInput":{"type":"object","properties":{"scenario":{"$ref":"#/components/schemas/Scenario"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"maxIterations":{"type":"integer","minimum":1,"maximum":50},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"profileId":{"type":"string"},"profileVariations":{"type":"array","items":{"type":"string"}},"priority":{"type":"integer"},"agentsMd":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"observations":{"type":"array","items":{"type":"string"}}},"required":["scenario"]},"RunFacetsResponse":{"type":"object","properties":{"total":{"type":"number"},"facets":{"type":"object","properties":{"workerType":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"status":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"outcome":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"model":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"os":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"priority":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"agentVersion":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"profileId":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}}},"required":["workerType","status","outcome","model","os","priority","agentVersion","profileId"]}},"required":["total","facets"]},"RunFacetBucket":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"number"}},"required":["value","count"]},"PaginatedRunsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"Cursors":{"type":"object","properties":{"next":{"type":["string","null"]},"prev":{"type":["string","null"]}},"required":["next","prev"]},"PaginatedRunGroupsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RunGroup"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"RunGroup":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"runIds":{"type":"array","items":{"type":"string"}},"aggregates":{"$ref":"#/components/schemas/GroupAggregates"},"uniform":{"$ref":"#/components/schemas/GroupUniformValues"}},"required":["key","label","runIds","aggregates","uniform"]},"GroupAggregates":{"type":"object","properties":{"count":{"type":"number"},"turns":{"$ref":"#/components/schemas/AggregateStats"},"duration":{"$ref":"#/components/schemas/AggregateStats"},"promptTokens":{"$ref":"#/components/schemas/AggregateStats"},"completionTokens":{"$ref":"#/components/schemas/AggregateStats"},"statusCounts":{"type":"object","additionalProperties":{"type":"number"}},"outcomeCounts":{"type":"object","additionalProperties":{"type":"number"}}},"required":["count","turns","duration","promptTokens","completionTokens","statusCounts","outcomeCounts"]},"AggregateStats":{"type":["object","null"],"properties":{"min":{"type":"number"},"max":{"type":"number"},"mean":{"type":"number"},"stdDev":{"type":"number"}},"required":["min","max","mean","stdDev"]},"GroupUniformValues":{"type":"object","properties":{"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":"string"},"platform":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"extensions":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"submissionId":{"type":"string"},"task":{"type":"string"}}},"BulkResubmitInput":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"count":{"type":"integer","minimum":1,"maximum":10,"default":1},"overrides":{"type":"object","properties":{"profileId":{"type":["string","null"]},"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":["string","null"]},"reasoningEffort":{"type":["string","null"]},"maxIterations":{"type":["number","null"]},"mcpServers":{"type":["array","null"],"items":{"type":"string"}},"skillRevisions":{"type":["array","null"],"items":{"type":"string"}},"extensions":{"type":["array","null"],"items":{"type":"string"}}}}},"required":["ids"]},"ReportResponse":{"type":"object","properties":{"_id":{"type":"string"},"requestId":{"type":"string"},"templateId":{"type":"string"},"reporter":{"$ref":"#/components/schemas/Reporter"},"content":{"type":"string"},"status":{"type":"string","enum":["pending","generating","completed","failed"]},"error":{"type":"string"},"insightReferences":{"type":"array","items":{"$ref":"#/components/schemas/InsightReference"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","requestId","status","createdAt","projectId"]},"Reporter":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"gitHash":{"type":"string"},"model":{"type":"string"},"agentId":{"type":"string"},"agentVersion":{"type":"string"}},"required":["id","name","gitHash","model","agentId","agentVersion"]},"InsightReference":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]},"CreateCriteriaInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"],"default":"gate"},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}},"required":["id","prompt"]},"CriteriaResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"CriteriaGraph":{"type":"object","properties":{"nodes":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphNode"}},"edges":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphEdge"}}},"required":["nodes","edges"]},"CriteriaGraphNode":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}},"required":["id","prompt"]},"CriteriaGraphEdge":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"]},"UpdateCriteriaInput":{"type":"object","properties":{"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}}},"CreatePromptFeatureInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["id","prompt"]},"PromptType":{"type":"string","enum":["select","build","test","run","deploy","agents.md"]},"PromptFeatureResult":{"type":"object","properties":{"featureId":{"type":"string"},"detected":{"type":"boolean"},"evaluated":{"type":"boolean"}},"required":["featureId","detected","evaluated"]},"SuggestedPromptFeature":{"type":"object","properties":{"suggestedId":{"type":"string"},"behavior":{"type":"string"},"prompt":{"type":"string"}},"required":["suggestedId","behavior","prompt"]},"PromptFeatureResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"UpdatePromptFeatureInput":{"type":"object","properties":{"prompt":{"type":"string"}}},"TaskPromptResponse":{"type":"object","properties":{"_id":{"type":"string"},"keyId":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"text":{"type":"string"},"contentBlobUrl":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","keyId","createdAt","projectId"]},"CreateTaskPromptInput":{"type":"object","properties":{"text":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["text"]},"PatchTaskPromptFeatureInput":{"type":"object","properties":{"detected":{"type":"boolean"}},"required":["detected"]},"CreateReportInput":{"type":"object","properties":{"requestId":{"type":"string"},"templateId":{"type":"string"}},"required":["requestId"]},"BulkCreateReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}},"templateId":{"type":"string"}},"required":["requestIds"]},"BulkReportStatusInput":{"type":"object","properties":{"reportIds":{"type":"array","items":{"type":"string"}}},"required":["reportIds"]},"BulkReportSummaryResponse":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/ReportSummary"}},"ReportSummary":{"type":"object","properties":{"total":{"type":"number"},"pending":{"type":"number"},"generating":{"type":"number"},"completed":{"type":"number"},"failed":{"type":"number"}},"required":["total","pending","generating","completed","failed"]},"BulkReportSummaryInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"TriggerReportsInput":{"type":"object","properties":{"requestId":{"type":"string"}},"required":["requestId"]},"BulkTriggerReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"InsightResponse":{"type":"object","properties":{"_id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"upvotes":{"type":"number"},"downvotes":{"type":"number"},"blocked":{"type":"boolean"},"referenceCount":{"type":"number"},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","title","description","upvotes","downvotes","blocked","referenceCount","createdBy","createdAt","projectId"]},"ProfileWithVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"version":{"$ref":"#/components/schemas/ProfileVersionResponse"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","version","projectId"]},"ProfileVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"profileId":{"type":"string"},"version":{"type":"number"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","profileId","version","workerType","model","createdAt","projectId"]},"CreateProfileInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["name","workerType","model"]},"ProfileResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","projectId"]},"UpdateProfileIdentity":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512}}},"ReportTemplateResponse":{"type":"object","properties":{"_id":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","id","name","userPrompt","createdAt","projectId"]},"ReportTemplateSystemPrompt":{"type":"object","properties":{"mode":{"type":"string","enum":["append","override"]},"content":{"type":"string"}},"required":["mode","content"]},"ReportTrigger":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["always"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["criteria"]},"criteriaIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","criteriaIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["taskPrompt"]},"taskPromptIds":{"type":"array","items":{"type":"string"}}},"required":["type","taskPromptIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["promptFeature"]},"featureIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","featureIds"]}]},"CreateReportTemplateInput":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0}},"required":["id","name","userPrompt"]},"UpdateReportTemplateInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"allOf":[{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},{"type":["object","null"]}]},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":["string","null"]},"timeoutMs":{"type":["integer","null"],"exclusiveMinimum":0}}},"AgentResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"},"versions":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","supportedModels","createdAt"]},"AgentCapabilities":{"type":"object","properties":{"supportsReasoningEffort":{"type":"boolean"},"supportsMcpServers":{"type":"boolean"},"supportsSkills":{"type":"boolean"},"supportsExtensions":{"type":"boolean"},"supportsResources":{"type":"boolean"}}},"AgentVersion":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string"},"status":{"type":"string","enum":["active","retired"]},"createdAt":{"type":["string","null"],"format":"date-time"}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","status","createdAt"]},"CreateAgentInput":{"type":"object","properties":{"_id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}},"required":["_id","name"]},"UpdateAgentInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}}},"RegisterAgentVersionInput":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string","minLength":1}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","queueName"]},"PatchAgentVersionInput":{"type":"object","properties":{"status":{"type":"string","enum":["active","retired"]}},"required":["status"]},"ModelResponse":{"type":"object","properties":{"_id":{"type":"string"},"modelId":{"type":"string"},"provider":{"type":"string"},"agentId":{"type":"string"},"firstSeenAt":{"type":["string","null"],"format":"date-time"},"lastSeenAt":{"type":["string","null"],"format":"date-time"},"disappearedAt":{"type":["string","null"],"format":"date-time"},"providerAvailableFrom":{"type":["string","null"],"format":"date-time"},"providerEndOfLife":{"type":["string","null"],"format":"date-time"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"$ref":"#/components/schemas/ModelCapabilities"}},"required":["_id","modelId","provider","agentId","firstSeenAt","lastSeenAt"]},"ModelCapabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}},"McpServerResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","type","createdAt","projectId"]},"McpServerHeader":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}},"required":["name","value"]},"UpdateMcpServerInput":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}}},"SkillResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","source","skillName","name","origin","createdAt","projectId"]},"SkillSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"source":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"installs":{"type":"number"}},"required":["id","name","source","internal"]},"SkillDiscoveryResult":{"type":"object","properties":{"skillName":{"type":"string"},"skillPath":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"existsInLibrary":{"type":"boolean"},"currentRevisionCommitSha":{"type":"string"},"latestUpstreamCommitSha":{"type":"string"},"updateAvailable":{"type":"boolean"},"lastImportedAt":{"type":"string"}},"required":["skillName","skillPath"]},"SkillRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"ref":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"skillPath":{"type":"string"},"commitHash":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"name":{"type":"string"},"description":{"type":"string"},"license":{"type":"string"},"compatibility":{"type":"string"},"allowedTools":{"type":"string"},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"content":{"type":"string"},"archiveUrl":{"type":"string"},"validationWarnings":{"type":"array","items":{"type":"string"}},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","ref","source","skillName","skillPath","commitHash","commitTimestamp","name","description","content","archiveUrl","resolvedAt","createdAt","projectId"]},"CreateSkillInput":{"type":"object","properties":{"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]}},"required":["source","skillName","name","origin"]},"CodebaseResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"defaultBranch":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","sourceType","revisionCounter","createdAt","projectId"]},"UpdateCodebaseInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"defaultBranch":{"type":"string"}}},"CodebaseRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"codebaseId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"requestedRef":{"type":"string"},"resolvedCommitSha":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"originalFilename":{"type":"string"},"contentSha256":{"type":"string"},"archiveUrl":{"type":"string"},"sizeBytes":{"type":"number"},"fileCount":{"type":"number"},"creator":{"type":"string"},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","codebaseId","slug","revisionNumber","ref","sourceType","archiveUrl","resolvedAt","createdAt","projectId"]},"ResolveCodebaseRevisionInput":{"type":"object","properties":{"requestedRef":{"type":"string"},"creator":{"type":"string"}}},"ResourceResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"latestRevisionNumber":{"type":"number"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","revisionCounter","createdAt","projectId"]},"CreateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1},"description":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["name","setup"]},"ResourceScript":{"type":"object","properties":{"sh":{"type":"string","minLength":1}}},"ResourceParameter":{"type":"object","properties":{"name":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"description":{"type":"string"},"required":{"type":"boolean","default":false},"default":{"type":"string"},"example":{"type":"string"}},"required":["name"]},"UpdateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"ResourceRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"resourceId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"contentSha256":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","resourceId","slug","revisionNumber","ref","setup","exports","contentSha256","createdAt","projectId"]},"CreateResourceRevisionInput":{"type":"object","properties":{"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["setup"]},"ExtensionResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","publisher","name","origin","createdAt","projectId"]},"ExtensionSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"publisher":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"version":{"type":"string"}},"required":["id","name","publisher","internal"]},"CreateExtensionInput":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+$"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]}},"required":["_id","publisher","name","origin"]},"UpdateExtensionInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ExtensionVersionInfo":{"type":"object","properties":{"version":{"type":"string"},"preRelease":{"type":"boolean"},"lastUpdated":{"type":"string"}},"required":["version","preRelease","lastUpdated"]},"CreateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"}},"required":["title","description"]},"UpdateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}}}},"FeatureFlagResponse":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"enabled":{"type":"boolean"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["key","label","enabled","updatedAt"]},"UpdateFeatureFlagInput":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]}},"parameters":{}},"paths":{"/api/v1/keys/preview":{"post":{"tags":["Keys"],"summary":"Preview key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}}},"/api/v1/keys":{"post":{"tags":["Keys"],"summary":"Create key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"201":{"description":"Key created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"get":{"tags":["Keys"],"summary":"List keys","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KeyResponse"}}}}}}}},"/api/v1/keys/{id}":{"get":{"tags":["Keys"],"summary":"Get key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"put":{"tags":["Keys"],"summary":"Update key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"delete":{"tags":["Keys"],"summary":"Delete key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Key deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/keys/{id}/validate":{"post":{"tags":["Keys"],"summary":"Validate key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateKeyInput"}}}},"responses":{"200":{"description":"Validation result","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Key validation result"}}}}}}},"/api/v1/accounts":{"post":{"tags":["Accounts"],"summary":"Create account","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"201":{"description":"Account created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"get":{"tags":["Accounts"],"summary":"List accounts","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccountResponse"}}}}}}}},"/api/v1/accounts/{id}":{"get":{"tags":["Accounts"],"summary":"Get account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"put":{"tags":["Accounts"],"summary":"Update account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"200":{"description":"Account updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"delete":{"tags":["Accounts"],"summary":"Delete account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Account deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/users/me":{"get":{"tags":["Users"],"summary":"Get the authenticated user's identity","description":"Read the existing authenticated Scope identity without enrollment or profile writes. Responses must not be HTTP-cached.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is not enrolled or is disabled"},"503":{"description":"Authentication service unavailable"}}},"post":{"tags":["Users"],"summary":"Enroll the authenticated user","description":"JIT-enroll the authenticated user, refresh their profile and lastLoginAt, apply bootstrap-admin rules, and warm the access cache. Use this only after an explicit IdP login; do not prefetch, poll, or automatically retry transient failures.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is disabled"},"503":{"description":"Authentication service unavailable"}}}},"/health":{"get":{"tags":["Health"],"summary":"Liveness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"version":{"type":"string"}},"required":["status","version"]}}}}}}},"/ready":{"get":{"tags":["Health"],"summary":"Readiness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"migrations":{}},"required":["status"]}}}},"503":{"description":"Service is not ready"}}}},"/about":{"get":{"tags":["System"],"summary":"API metadata","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"version":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"description":{"type":"string"},"workers":{"type":"array","items":{"type":"string"}}},"required":["name","version","buildTime","environment","description","workers"]}}}}}}},"/api/v1/version":{"get":{"tags":["System"],"summary":"Version info","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"commit":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"strictAgentCapabilities":{"type":"boolean"}},"required":["commit","buildTime","environment","strictAgentCapabilities"]}}}}}}},"/api/v1/projects":{"get":{"tags":["Projects"],"summary":"List all projects","parameters":[{"schema":{"type":"string","enum":["true","false"],"description":"When true, include soft-deleted projects in the result"},"required":false,"description":"When true, include soft-deleted projects in the result","name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProjectResponse"}}}}}}},"post":{"tags":["Projects"],"summary":"Create a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/projects/{id}":{"get":{"tags":["Projects"],"summary":"Get a project","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"patch":{"tags":["Projects"],"summary":"Update a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProjectInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"delete":{"tags":["Projects"],"summary":"Soft-delete a project","responses":{"204":{"description":"Success"},"404":{"description":"Project not found"}}}},"/api/v1/projects/{id}/restore":{"post":{"tags":["Projects"],"summary":"Restore a soft-deleted project","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}}},"/api/v1/submissions":{"get":{"tags":["Submissions"],"summary":"List submissions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionListResponse"}}}}}}},"/api/v1/submissions/{id}":{"get":{"tags":["Submissions"],"summary":"Get a submission","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionResponse"}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}/runs":{"get":{"tags":["Submissions"],"summary":"List current and historical submission attempts","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"runs":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRunResponse"}}},"required":["runs"]}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}/runs/{submissionRunId}":{"get":{"tags":["Submissions"],"summary":"Get one submission attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"submissionRunId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionRunResponse"}}}},"404":{"description":"Submission attempt not found"}}}},"/api/v1/submissions/{id}/retry":{"post":{"tags":["Submissions"],"summary":"Retry current submission processing","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"submissionRunId":{"type":"string"}},"required":["submissionRunId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionRetryResponse"}}}},"404":{"description":"Submission not found"},"409":{"description":"Submission attempt is stale or dependencies are blocked"}}}},"/api/v1/requests":{"post":{"tags":["Requests"],"summary":"Submit request(s)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/CreateRequestInput"},{"type":"object","properties":{"count":{"type":"number","minimum":1,"maximum":10,"default":1},"skills":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"codebase":{"type":"string"}}}]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RequestResponse"},{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}]}}}}}},"get":{"tags":["Requests"],"summary":"List requests","parameters":[{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"profileId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"status","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"outcome","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"model","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"os","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"priority","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"agentVersion","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"type":"string","enum":["task","submissionId","profile"]},"required":false,"name":"groupBy","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"after","in":"query"},{"schema":{"type":"string"},"required":false,"name":"before","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"last","in":"query"},{"schema":{"type":"string","enum":["created","updated","priority","worker","status","id","duration","createdAt"]},"required":false,"name":"sortBy","in":"query"},{"schema":{"type":"string","enum":["asc","desc"]},"required":false,"name":"sortDir","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdAfter","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdBefore","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"turns","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"turnsOp","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"maxIterations","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"maxIterationsOp","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PaginatedRunsResponse"},{"$ref":"#/components/schemas/PaginatedRunGroupsResponse"}]}}}}}}},"/api/v1/requests/facets":{"get":{"tags":["Requests"],"summary":"List run filter facets","description":"Returns every distinct value and its full-dataset count per categorical filter dimension for the Runs list rail. Counts are absolute over all non-deleted runs **in the given project**: they intentionally ignore the active search, date range, iteration, and categorical selections so every selectable value stays visible with a stable count. Requires ?projectId=. Computed with parallel $group aggregations (Cosmos has no $facet) and cached per-project for a short TTL.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunFacetsResponse"}}}}}}},"/api/v1/requests/{id}":{"get":{"tags":["Requests"],"summary":"Get request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestResponse"}}}},"404":{"description":"Not found"}}},"delete":{"tags":["Requests"],"summary":"Soft-delete request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Not found"}}}},"/api/v1/analysis":{"get":{"tags":["Requests"],"summary":"Compute pass@k / success@T metrics","description":"Aggregates pass@k / success@T metrics over a single project's done runs. Requires ?projectId=.","parameters":[{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"k","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Analysis metrics"}}}}}}},"/api/v1/requests/bulk-resubmit":{"post":{"tags":["Requests"],"summary":"Bulk resubmit requests","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkResubmitInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}}}}}}},"/api/v1/requests/bulk":{"delete":{"tags":["Requests"],"summary":"Bulk soft-delete requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}},"required":["ids"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"number"}},"required":["deleted"]}}}}}}},"/api/v1/requests/archive":{"post":{"tags":["Requests"],"summary":"Download batch archive of multiple runs","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Gzipped batch archive containing individual run archives"},"400":{"description":"Invalid input"},"404":{"description":"One or more runs not found"}}}},"/api/v1/runs/upload":{"post":{"tags":["Requests"],"summary":"Import run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"}}}},"/api/v1/runs/upload-batch":{"post":{"tags":["Requests"],"summary":"Import batch run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"207":{"description":"Multi-status: 201 if all runs imported, 400 if none imported, 207 if partial"},"400":{"description":"No archive uploaded, empty archive, or all runs failed"}}}},"/api/v1/requests/{id}/reports":{"get":{"tags":["Reports"],"summary":"Get reports for request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Run not found"}}}},"/api/v1/requests/{id}/runs":{"get":{"tags":["Requests"],"summary":"List attempts for a request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RunState"}}}}},"404":{"description":"Request not found"}}}},"/api/v1/requests/{id}/runs/{runId}":{"get":{"tags":["Requests"],"summary":"Get a single attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunState"}}}},"404":{"description":"Request or run not found"}}}},"/api/v1/requests/bulk-retry":{"post":{"tags":["Requests"],"summary":"Bulk retry requests (start new attempts)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"force":{"type":"boolean"}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"retried":{"type":"integer"},"skipped":{"type":"integer"},"results":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"},"error":{"type":"string"}},"required":["requestId"]}}},"required":["retried","skipped","results"]}}}}}}},"/api/v1/requests/{id}/retry":{"post":{"tags":["Requests"],"summary":"Retry a request (start a new attempt)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"force":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"}},"required":["requestId","runId","attemptNumber"]}}}},"404":{"description":"Request not found"},"409":{"description":"Conflict — request is not in a retryable state, or a concurrent retry won the race"},"422":{"description":"Cannot retry — current run not yet terminal"}}}},"/api/v1/requests/{id}/priority":{"post":{"tags":["Requests"],"summary":"Set priority on a single request","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"priority":{"type":"integer"}},"required":["priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"priority":{"type":"number"}},"required":["id","priority"]}}}}}}},"/api/v1/requests/bulk-priority":{"post":{"tags":["Requests"],"summary":"Set priority on multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100},"priority":{"type":"integer"}},"required":["ids","priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/pause":{"post":{"tags":["Requests"],"summary":"Pause a single request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/{id}/resume":{"post":{"tags":["Requests"],"summary":"Resume a paused request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/bulk-pause":{"post":{"tags":["Requests"],"summary":"Pause multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/bulk-resume":{"post":{"tags":["Requests"],"summary":"Resume multiple paused requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/cancel":{"post":{"tags":["Requests"],"summary":"Cancel a request (marks as done/failed and signals worker to exit)","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"previousStatus":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"}},"required":["id","previousStatus","status","outcome"]}}}}}}},"/api/v1/requests/bulk-cancel":{"post":{"tags":["Requests"],"summary":"Cancel multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"cancelled":{"type":"number"},"skipped":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"cancelled":{"type":"boolean"},"previousStatus":{"type":"string"},"error":{"type":"string"}},"required":["id","cancelled"]}}},"required":["cancelled","skipped","results"]}}}}}}},"/api/v1/requests/{id}/logs":{"get":{"tags":["Requests"],"summary":"Stream request logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/logs":{"get":{"tags":["Requests"],"summary":"Stream logs for a specific attempt (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific attempt and iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/video":{"get":{"tags":["Requests"],"summary":"Download session recording","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/video":{"get":{"tags":["Requests"],"summary":"Download session recording for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/criteria/generate-prompt":{"post":{"tags":["Criteria"],"summary":"Generate criterion prompt from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"},"gates":{"type":"array","items":{"type":"string"}}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"},"suggestedId":{"type":"string"},"suggestedParents":{"type":"array","items":{"type":"string"}},"suggestedChildren":{"type":"array","items":{"type":"string"}}},"required":["prompt","suggestedId","suggestedParents","suggestedChildren"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/criteria/seed":{"post":{"tags":["Criteria"],"summary":"Seed criteria in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"criteria":{"type":"array","items":{"$ref":"#/components/schemas/CreateCriteriaInput"}}},"required":["criteria"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}},"400":{"description":"Seed batch would introduce a dependency cycle"}}}},"/api/v1/criteria":{"get":{"tags":["Criteria"],"summary":"List criteria","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","description":"Comma-separated criterion IDs to include"},"required":false,"description":"Comma-separated criterion IDs to include","name":"ids","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"When true and ids is set, also include dependency ancestors"},"required":false,"description":"When true and ids is set, also include dependency ancestors","name":"ancestors","in":"query"},{"schema":{"type":"string","enum":["gate","observation"],"description":"Filter by criterion kind"},"required":false,"description":"Filter by criterion kind","name":"kind","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaResponse"}}}}}}},"post":{"tags":["Criteria"],"summary":"Create criterion","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCriteriaInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"409":{"description":"Criterion already exists"}}}},"/api/v1/criteria/mdp":{"get":{"tags":["Criteria"],"summary":"Compute MDP transitions","parameters":[{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"since","in":"query"},{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{}}}}}}}},"/api/v1/criteria/graph":{"get":{"tags":["Criteria"],"summary":"Get criteria DAG","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaGraph"}}}}}}},"/api/v1/criteria/{id}":{"get":{"tags":["Criteria"],"summary":"Get criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"404":{"description":"Criterion not found"}}},"put":{"tags":["Criteria"],"summary":"Update criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCriteriaInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"400":{"description":"Invalid dependency reference or self-reference"},"404":{"description":"Criterion not found"}}},"delete":{"tags":["Criteria"],"summary":"Soft-delete criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Criterion not found"},"409":{"description":"Criterion has dependents"}}}},"/api/v1/prompt-features/generate-prompt":{"post":{"tags":["Prompt Features"],"summary":"Generate prompt feature from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"}},"required":["prompt"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features/seed":{"post":{"tags":["Prompt Features"],"summary":"Seed prompt features in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}},"required":["features"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}}}}},"/api/v1/prompt-features/extract-from-text":{"post":{"tags":["Prompt Features"],"summary":"Extract features from text","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string"},"model":{"type":"string"},"type":{"type":"string","enum":["select","agents.md"]}},"required":["text"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["features","cached"]}}}},"400":{"description":"Empty text string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features":{"get":{"tags":["Prompt Features"],"summary":"List features","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","enum":["select","agents.md"]},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}}}}},"post":{"tags":["Prompt Features"],"summary":"Create feature","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"409":{"description":"Feature already exists"}}}},"/api/v1/prompt-features/{id}":{"get":{"tags":["Prompt Features"],"summary":"Get feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"404":{"description":"Feature not found"}}},"put":{"tags":["Prompt Features"],"summary":"Update feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Feature not found"}}},"delete":{"tags":["Prompt Features"],"summary":"Soft-delete feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Feature not found"}}}},"/api/v1/task-prompts/generate":{"post":{"tags":["Task Prompts"],"summary":"Generate task prompts","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string"},"existingPrompt":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"tasks":{"type":"array","items":{"type":"string"}}},"required":["tasks"]}}}},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts":{"get":{"tags":["Task Prompts"],"summary":"List task prompts","parameters":[{"schema":{"type":["number","null"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":["number","null"]},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"$ref":"#/components/schemas/PromptType"},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/TaskPromptResponse"}},"total":{"type":"number"},"limit":{"type":"number"},"offset":{"type":"number"}},"required":["items","total","limit","offset"]}}}}}},"post":{"tags":["Task Prompts"],"summary":"Create or find task prompt","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTaskPromptInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Empty text string"}}}},"/api/v1/task-prompts/{id}":{"get":{"tags":["Task Prompts"],"summary":"Get task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"404":{"description":"Task prompt not found"}}},"delete":{"tags":["Task Prompts"],"summary":"Soft-delete task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/content":{"get":{"tags":["Task Prompts"],"summary":"Get resolved task prompt content","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"}},"required":["id","text"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/extract-features":{"post":{"tags":["Task Prompts"],"summary":"Extract features from task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"force","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"taskPromptId":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["taskPromptId","features","cached"]}}}},"404":{"description":"Task prompt not found"},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts/{id}/features/{featureId}":{"patch":{"tags":["Task Prompts"],"summary":"Toggle feature flag on task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"featureId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchTaskPromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Invalid detected value"},"404":{"description":"Task prompt or feature not found"}}}},"/api/v1/reports":{"post":{"tags":["Reports"],"summary":"Create report","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Run or template not found"}}},"get":{"tags":["Reports"],"summary":"List reports","parameters":[{"schema":{"type":"string"},"required":false,"name":"requestId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}}}}},"/api/v1/reports/bulk-create":{"post":{"tags":["Reports"],"summary":"Bulk create reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/bulk-status":{"post":{"tags":["Reports"],"summary":"Bulk get report statuses","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportStatusInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/bulk-summary":{"post":{"tags":["Reports"],"summary":"Bulk get report summary per run","description":"Returns aggregated report status counts per run. Only the latest report per template is counted (re-triggers are deduplicated).","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}":{"get":{"tags":["Reports"],"summary":"Get report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"404":{"description":"Report not found"}}}},"/api/v1/reports/{id}/logs":{"get":{"tags":["Reports"],"summary":"Stream report logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Report not found"}}}},"/api/v1/reports/trigger":{"post":{"tags":["Reports"],"summary":"Trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"triggered":{"type":"number"},"reports":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}},"required":["triggered","reports"]}}}},"400":{"description":"Invalid input"},"404":{"description":"Run not found"}}}},"/api/v1/reports/bulk-trigger":{"post":{"tags":["Reports"],"summary":"Bulk trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkTriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":{}}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}/insights":{"get":{"tags":["Reports"],"summary":"Get insights for report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/InsightResponse"},{"type":"object","properties":{"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}}}]}}}}},"404":{"description":"Report not found"}}},"post":{"tags":["Reports"],"summary":"Link insight to report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"isNew":{"type":"boolean"}},"required":["insightId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]}}}},"404":{"description":"Report or insight not found"},"409":{"description":"Insight already referenced by this report"}}}},"/api/v1/profiles":{"post":{"tags":["Profiles"],"summary":"Create a new profile","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"get":{"tags":["Profiles"],"summary":"List profiles","parameters":[{"schema":{"type":"string"},"required":false,"name":"workerType","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}":{"get":{"tags":["Profiles"],"summary":"Get profile with latest version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"post":{"tags":["Profiles"],"summary":"Create new profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["workerType","model"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}},"put":{"tags":["Profiles"],"summary":"Update profile identity","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileIdentity"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"}}}}}},"delete":{"tags":["Profiles"],"summary":"Delete profile","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/profiles/{profileId}/versions":{"get":{"tags":["Profiles"],"summary":"List profile versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}/versions/{version}":{"get":{"tags":["Profiles"],"summary":"Get profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"},{"schema":{"type":["number","null"]},"required":false,"name":"version","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}},"/api/v1/report-templates/default-system-prompt":{"get":{"tags":["Report Templates"],"summary":"Get default system prompt","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string"}},"required":["content"]}}}}}}},"/api/v1/report-templates/available-models":{"get":{"tags":["Report Templates"],"summary":"List models available for report generation","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"]}}}}}}}},"/api/v1/report-templates":{"get":{"tags":["Report Templates"],"summary":"List report templates","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}}}}},"post":{"tags":["Report Templates"],"summary":"Create report template","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportTemplateInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"409":{"description":"Report template already exists"}}}},"/api/v1/report-templates/{id}":{"get":{"tags":["Report Templates"],"summary":"Get report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"put":{"tags":["Report Templates"],"summary":"Update report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateReportTemplateInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"delete":{"tags":["Report Templates"],"summary":"Delete report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Report template not found"}}}},"/api/v1/agents":{"get":{"tags":["Agents"],"summary":"List agents","parameters":[{"schema":{"type":"string"},"required":false,"name":"modelProvider","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentResponse"}}}}}}},"post":{"tags":["Agents"],"summary":"Create or update agent (upsert)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"409":{"description":"Restored agent versions conflict with an active queue owner"}}}},"/api/v1/agents/{id}":{"get":{"tags":["Agents"],"summary":"Get agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"404":{"description":"Agent not found"}}},"put":{"tags":["Agents"],"summary":"Update agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAgentInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"}}},"delete":{"tags":["Agents"],"summary":"Delete agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Agent not found"}}}},"/api/v1/agents/{id}/versions":{"get":{"tags":["Agents"],"summary":"List agent versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}}}}},"404":{"description":"Agent not found"}}},"post":{"tags":["Agents"],"summary":"Register agent version (upsert)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterAgentVersionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent registration did not stabilize"}}}},"/api/v1/agents/{id}/versions/{agentVersion}":{"patch":{"tags":["Agents"],"summary":"Patch agent version","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"agentVersion","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchAgentVersionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Invalid status value"},"404":{"description":"Agent or version not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent activation did not stabilize"}}}},"/api/v1/models":{"get":{"tags":["Models"],"summary":"List models","parameters":[{"schema":{"type":"string"},"required":false,"name":"agentId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"provider","in":"query"},{"schema":{"type":"string","enum":["active","disappeared"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ModelResponse"}}}}}}}},"/api/v1/models/{id}":{"get":{"tags":["Models"],"summary":"Get model","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelResponse"}}}}}}},"/api/v1/models/sync":{"post":{"tags":["Models"],"summary":"Sync models from provider","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"agentId":{"type":"string"},"provider":{"type":"string"},"models":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"providerAvailableFrom":{"type":"string"},"providerEndOfLife":{"type":"string"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}}},"required":["id"]}},"scannedAt":{"type":"string"}},"required":["agentId","provider","models","scannedAt"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"added":{"type":"array","items":{"type":"string"}},"removed":{"type":"array","items":{"type":"string"}},"unchanged":{"type":"array","items":{"type":"string"}}},"required":["added","removed","unchanged"]}}}}}}},"/api/v1/mcp/servers":{"get":{"tags":["MCP Servers"],"summary":"List MCP servers","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"post":{"tags":["MCP Servers"],"summary":"Create MCP server","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}},"required":["_id","name","type"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"/api/v1/mcp/servers/{id}":{"get":{"tags":["MCP Servers"],"summary":"Get MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"put":{"tags":["MCP Servers"],"summary":"Update MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMcpServerInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"delete":{"tags":["MCP Servers"],"summary":"Delete MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/skills":{"get":{"tags":["Skills"],"summary":"List all skills","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"post":{"tags":["Skills"],"summary":"Create or import a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSkillInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"/api/v1/skills/search":{"get":{"tags":["Skills"],"summary":"Search skills (internal + external)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/search/external":{"get":{"tags":["Skills"],"summary":"Search external skills registry","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project."},"required":false,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/discover":{"get":{"tags":["Skills"],"summary":"Discover skills in a GitHub repository","parameters":[{"schema":{"type":"string"},"required":true,"name":"source","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillDiscoveryResult"}}}}},"400":{"description":"Missing or malformed source parameter"},"404":{"description":"Repository not found"},"502":{"description":"GitHub API error"}}}},"/api/v1/skills/{id}(*)/revisions":{"get":{"tags":["Skills"],"summary":"List skill revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}}},"404":{"description":"Skill not found"}}}},"/api/v1/skills/{id}(*)":{"get":{"tags":["Skills"],"summary":"Get skill by slug","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}},"404":{"description":"Skill not found"}}},"delete":{"tags":["Skills"],"summary":"Soft-delete a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Skill not found"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)/archive":{"get":{"tags":["Skill Revisions"],"summary":"Download skill revision archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Skill revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ref","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skill-revisions/{id}":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ID","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skills/{id}(*)/resolve":{"post":{"tags":["Skills"],"summary":"Trigger skill resolution","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebases":{"get":{"tags":["Codebases"],"summary":"List all codebases","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseResponse"}}}}}}},"post":{"tags":["Codebases"],"summary":"Create a codebase (archive codebases require the archive file)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input or missing archive"}}}},"/api/v1/codebases/{id}":{"get":{"tags":["Codebases"],"summary":"Get a codebase","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"patch":{"tags":["Codebases"],"summary":"Update a codebase","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCodebaseInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"delete":{"tags":["Codebases"],"summary":"Delete a codebase","responses":{"204":{"description":"Success"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebases/{id}/revisions":{"get":{"tags":["Codebases"],"summary":"List codebase revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}}},"404":{"description":"Codebase not found"}}},"post":{"tags":["Codebases"],"summary":"Resolve a new git codebase revision","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveCodebaseRevisionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"400":{"description":"Not a git codebase"},"404":{"description":"Codebase not found"},"502":{"description":"GitHub resolution failed"}}}},"/api/v1/codebases/{id}/upload":{"post":{"tags":["Codebases"],"summary":"Upload a codebase archive as a new revision","responses":{"201":{"description":"Success"},"400":{"description":"Missing archive or not an archive codebase"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebase-revisions/{id}/archive":{"get":{"tags":["Codebase Revisions"],"summary":"Download codebase revision archive","responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebase-revisions/{id}":{"get":{"tags":["Codebase Revisions"],"summary":"Get codebase revision by id","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources":{"get":{"tags":["Resources"],"summary":"List all resources","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceResponse"}}}}}}},"post":{"tags":["Resources"],"summary":"Create a resource and its first revision","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceInput"}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input"},"409":{"description":"Resource slug or revision ref already exists in this project"}}}},"/api/v1/resources/{id}":{"get":{"tags":["Resources"],"summary":"Get a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"patch":{"tags":["Resources"],"summary":"Update a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateResourceInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"delete":{"tags":["Resources"],"summary":"Delete a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Resource not found"}}}},"/api/v1/resources/{id}/revisions":{"get":{"tags":["Resource Revisions"],"summary":"List resource revisions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}}},"404":{"description":"Resource not found"}}},"post":{"tags":["Resource Revisions"],"summary":"Create a resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceRevisionInput"}}}},"responses":{"201":{"description":"Success"},"404":{"description":"Resource not found"},"409":{"description":"Resource revision ref already exists in this project"}}}},"/api/v1/resources/{id}/revisions/latest":{"get":{"tags":["Resource Revisions"],"summary":"Get the latest resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/{id}/revisions/{revisionNumber}":{"get":{"tags":["Resource Revisions"],"summary":"Get a resource revision by resource and revision number","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"revisionNumber","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/revisions/{id}":{"get":{"tags":["Resource Revisions"],"summary":"Get resource revision by id","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/extensions":{"get":{"tags":["Extensions"],"summary":"List all extensions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"post":{"tags":["Extensions"],"summary":"Create or import an extension","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExtensionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"/api/v1/extensions/search":{"get":{"tags":["Extensions"],"summary":"Search extensions (internal + marketplace)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/extensions/{id}":{"get":{"tags":["Extensions"],"summary":"Get extension by ID","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"put":{"tags":["Extensions"],"summary":"Update extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateExtensionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"delete":{"tags":["Extensions"],"summary":"Delete extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/extensions/{id}/versions":{"get":{"tags":["Extensions"],"summary":"List available versions for an extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"preRelease","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionVersionInfo"}}}}}}}},"/api/v1/insights":{"get":{"tags":["Insights"],"summary":"List insights","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"post":{"tags":["Insights"],"summary":"Create insight","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInsightInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"/api/v1/insights/search":{"get":{"tags":["Insights"],"summary":"Search insights","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}}},"/api/v1/insights/{id}":{"get":{"tags":["Insights"],"summary":"Get insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"put":{"tags":["Insights"],"summary":"Update insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateInsightInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"delete":{"tags":["Insights"],"summary":"Delete insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/upvote":{"post":{"tags":["Insights"],"summary":"Upvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/downvote":{"post":{"tags":["Insights"],"summary":"Downvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/block":{"post":{"tags":["Insights"],"summary":"Block insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/unblock":{"post":{"tags":["Insights"],"summary":"Unblock insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/reports":{"get":{"tags":["Insights"],"summary":"Get reports referencing insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Insight not found"}}}},"/api/v1/feature-flags":{"get":{"tags":["Feature Flags"],"summary":"List feature flags","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"/api/v1/feature-flags/{key}":{"put":{"tags":["Feature Flags"],"summary":"Update feature flag","parameters":[{"schema":{"type":"string"},"required":true,"name":"key","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeatureFlagInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"webhooks":{}} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"Scope API","version":"1.0.0","description":"REST API for the Scope platform — benchmarking AI coding agents"},"servers":[{"url":"/","description":"Current server"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Use the unchanged IdP access token, without the Bearer prefix."}},"schemas":{"KeyResponse":{"type":"object","properties":{},"additionalProperties":{}},"KeyInput":{"type":"object","properties":{},"additionalProperties":{}},"ValidateKeyInput":{"type":"object","properties":{"token":{"type":"string"}},"required":["token"]},"AccountResponse":{"type":"object","properties":{},"additionalProperties":{}},"AccountInput":{"type":"object","properties":{},"additionalProperties":{}},"UserMeResponse":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string"},"email":{"type":"string"},"displayName":{"type":"string"},"idp":{"type":"string"},"idpTenant":{"type":"string"}},"required":["id"]},"ProjectResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","createdAt"]},"CreateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"creator":{"type":"string"}},"required":["name"]},"UpdateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"SubmissionListResponse":{"type":"object","properties":{"submissions":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionResponse"}}},"required":["submissions"]},"SubmissionResponse":{"type":"object","properties":{"submissionId":{"type":"string"},"projectId":{"type":"string"},"requestIds":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"submissionRunId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["waiting","ready","blocked","processing","done","failed"]},"requests":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRequestSummary"}},"dependencyProjection":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionDependencyProjection"}},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"}},"handlerProgress":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/SubmissionHandlerStatus"}},"report":{"$ref":"#/components/schemas/SubmissionReportPointer"},"supersededAt":{"type":["string","null"],"format":"date-time"},"supersededBySubmissionRunId":{"type":"string"},"supersededReason":{"type":"string","enum":["request-vector-changed","manual-retry"]}},"required":["submissionId","projectId","requestIds","createdAt","submissionRunId","attemptNumber","readiness","requests","dependencyProjection","blocking"]},"SubmissionRequestSummary":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"task":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"},"workerType":{"type":"string"},"model":{"type":"string"},"profileId":{"type":"string"}},"required":["requestId","runId","task","status","workerType"]},"SubmissionDependencyProjection":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"state":{"type":"string","enum":["waiting","ready","failed","blocked"]},"version":{"type":"number"},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["requestId","runId","handlerId","state"]},"SubmissionBlockingDetail":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"reason":{"type":"string","enum":["request-missing","run-changed","dependency-not-ready","dependency-failed","dependency-blocked"]},"error":{"type":"string"}},"required":["requestId","runId","handlerId","reason"]},"SubmissionHandlerStatus":{"type":"object","properties":{"status":{"type":"string","enum":["queued","processing","done","failed"]},"version":{"type":"number"},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["status"]},"SubmissionReportPointer":{"type":"object","properties":{"reportId":{"type":"string"},"url":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"}}},"ReportResponse":{"type":"object","properties":{"_id":{"type":"string"},"requestId":{"type":"string"},"submissionId":{"type":"string"},"submissionRunId":{"type":"string"},"templateId":{"type":"string"},"reporter":{"$ref":"#/components/schemas/Reporter"},"content":{"type":"string"},"structuredArtifact":{"$ref":"#/components/schemas/StructuredReportArtifact"},"status":{"type":"string","enum":["pending","generating","completed","failed"]},"error":{"type":"string"},"insightReferences":{"type":"array","items":{"$ref":"#/components/schemas/InsightReference"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","status","createdAt","projectId"]},"Reporter":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"gitHash":{"type":"string"},"model":{"type":"string"},"agentId":{"type":"string"},"agentVersion":{"type":"string"}},"required":["id","name","gitHash","model","agentId","agentVersion"]},"StructuredReportArtifact":{"type":"object","properties":{"schemaId":{"type":"string"},"schemaVersion":{"type":"string"},"contentType":{"type":"string","enum":["application/json"]},"url":{"type":"string"}},"required":["schemaId","schemaVersion","contentType","url"]},"InsightReference":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]},"SubmissionRunResponse":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"vectorFingerprint":{"type":"string"},"requests":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRequestRef"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"dependencyProjection":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionDependencyProjection"},"default":[]},"handlerStatus":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/SubmissionHandlerStatus"}},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"},"default":[]},"report":{"$ref":"#/components/schemas/SubmissionReportPointer"},"supersededAt":{"type":["string","null"],"format":"date-time"},"supersededBySubmissionRunId":{"type":"string"},"supersededReason":{"type":"string","enum":["request-vector-changed","manual-retry"]},"submissionId":{"type":"string"},"projectId":{"type":"string"}},"required":["_id","attemptNumber","vectorFingerprint","requests","createdAt"]},"SubmissionRequestRef":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"}},"required":["requestId","runId"]},"SubmissionRetryResponse":{"type":"object","properties":{"submissionId":{"type":"string"},"submissionRunId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["waiting","ready","blocked","processing","done","failed"]},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"}}},"required":["submissionId","submissionRunId","attemptNumber","readiness","blocking"]},"RequestResponse":{"type":"object","properties":{"_id":{"type":"string"},"scenario":{"$ref":"#/components/schemas/Scenario"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"maxIterations":{"type":"number"},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"deletedAt":{"type":["string","null"],"format":"date-time"},"taskPromptId":{"type":"string"},"agentsMdPromptId":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBinding"}},"extensions":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"profileId":{"type":"string"},"profileVersionId":{"type":"string"},"submissionId":{"type":"string"},"submissionExpectedRequestCount":{"type":"integer","minimum":1},"submissionRequestIndex":{"type":"integer","minimum":0},"submissionInitialRunId":{"type":"string"},"submissionFinalizedAt":{"type":["string","null"],"format":"date-time"},"priority":{"type":"integer","default":0},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"observations":{"type":"array","items":{"type":"string"}},"gateSummaries":{"type":"array","items":{"$ref":"#/components/schemas/GateRunSummary"}},"run":{"$ref":"#/components/schemas/RunState"},"projectId":{"type":"string"}},"required":["_id","scenario","workerType","createdAt","projectId"]},"Scenario":{"type":"object","properties":{"version":{"type":"string","enum":["v1","v2"]},"task":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}}},"required":["task","criteria"]},"Persona":{"type":"object","properties":{"personality":{"type":"string","enum":["demanding","friendly"]},"experience":{"type":"string","enum":["junior","senior"]},"verbosity":{"type":"string","enum":["brief","moderate"]},"type":{"type":"string","enum":["traditional","ai_assisted","vibe"]}},"required":["personality","experience","verbosity","type"]},"ResourceBinding":{"type":"object","properties":{"ref":{"type":"string"},"revisionId":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref","revisionId","params"]},"GateConfig":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"promptId":{"type":"string"},"promptText":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}},"maxIterations":{"type":"integer","minimum":1,"maximum":50}},"required":["gate","criteria"]},"GateRunSummary":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"status":{"type":"string","enum":["passed","failed","skipped"]},"iterations":{"type":"integer","minimum":0}},"required":["gate","status","iterations"]},"RunState":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"queuedQueueName":{"type":"string"},"outcome":{"type":"string","enum":["succeeded","failed","finished"]},"result":{"type":"string"},"error":{"type":"string"},"logsUrl":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"turns":{"type":"array","items":{"$ref":"#/components/schemas/ConversationTurn"}},"workerVersion":{"type":"string"},"os":{"type":"object","properties":{"platform":{"type":"string"},"release":{"type":"string"},"arch":{"type":"string"}},"required":["platform","release","arch"]},"lastHeartbeatAt":{"type":["string","null"],"format":"date-time"},"worker":{"type":"object","properties":{"instanceId":{"type":"string"},"podName":{"type":"string"}},"required":["instanceId"]},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"setupVideoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"aiCallCount":{"type":"number"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRunOutcome"}},"mcpRegistered":{"type":"boolean"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"pausedAt":{"type":["string","null"],"format":"date-time"},"resumedAt":{"type":["string","null"],"format":"date-time"},"observationResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}}},"required":["_id","attemptNumber","status"]},"ConversationTurn":{"type":"object","properties":{"iteration":{"type":"number"},"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"codingAgentResponse":{"type":"string"},"judgeFeedback":{"type":"string"},"snapshotUrl":{"type":"string"},"passed":{"type":"boolean"},"timestamp":{"type":["string","null"],"format":"date-time"},"criteriaResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"observationResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"startedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"toolCalls":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"arguments":{"type":"object","additionalProperties":{}},"response":{"type":"string"},"timestamp":{"type":"string"}},"required":["id","name","arguments"]}},"toolCallsUrl":{"type":"string"},"toolCallCount":{"type":"number"},"aiCallCount":{"type":"number"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"chatResultUrl":{"type":"string"},"chatResultFormat":{"type":"string"}},"required":["iteration","judgeFeedback","snapshotUrl","passed","timestamp"]},"CriterionResult":{"type":"object","properties":{"criterionId":{"type":"string"},"passed":{"type":"boolean"},"feedback":{"type":"string"},"evaluated":{"type":"boolean"}},"required":["criterionId","passed","feedback","evaluated"]},"TokenUsage":{"type":"object","properties":{"promptTokens":{"type":"number"},"completionTokens":{"type":"number"},"totalTokens":{"type":"number"}},"required":["promptTokens","completionTokens","totalTokens"]},"ResourceRunOutcome":{"type":"object","properties":{"ref":{"type":"string"},"slug":{"type":"string"},"revisionId":{"type":"string"},"setupSucceeded":{"type":"boolean"},"published":{"type":"array","items":{"type":"string"}},"params":{"type":"object","additionalProperties":{"type":"string"}},"setupDurationMs":{"type":"number"},"error":{"type":"string"},"teardownRan":{"type":"boolean"}},"required":["ref","slug","revisionId","setupSucceeded","published"]},"ResourceBindingSpec":{"anyOf":[{"type":"string"},{"type":"object","properties":{"ref":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref"]}]},"CreateRequestInput":{"type":"object","properties":{"scenario":{"$ref":"#/components/schemas/Scenario"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"maxIterations":{"type":"integer","minimum":1,"maximum":50},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"profileId":{"type":"string"},"profileVariations":{"type":"array","items":{"type":"string"}},"priority":{"type":"integer"},"agentsMd":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"observations":{"type":"array","items":{"type":"string"}}},"required":["scenario"]},"RunFacetsResponse":{"type":"object","properties":{"total":{"type":"number"},"facets":{"type":"object","properties":{"workerType":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"status":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"outcome":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"model":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"os":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"priority":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"agentVersion":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"profileId":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}}},"required":["workerType","status","outcome","model","os","priority","agentVersion","profileId"]}},"required":["total","facets"]},"RunFacetBucket":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"number"}},"required":["value","count"]},"PaginatedRunsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"Cursors":{"type":"object","properties":{"next":{"type":["string","null"]},"prev":{"type":["string","null"]}},"required":["next","prev"]},"PaginatedRunGroupsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RunGroup"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"RunGroup":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"runIds":{"type":"array","items":{"type":"string"}},"aggregates":{"$ref":"#/components/schemas/GroupAggregates"},"uniform":{"$ref":"#/components/schemas/GroupUniformValues"}},"required":["key","label","runIds","aggregates","uniform"]},"GroupAggregates":{"type":"object","properties":{"count":{"type":"number"},"turns":{"$ref":"#/components/schemas/AggregateStats"},"duration":{"$ref":"#/components/schemas/AggregateStats"},"promptTokens":{"$ref":"#/components/schemas/AggregateStats"},"completionTokens":{"$ref":"#/components/schemas/AggregateStats"},"statusCounts":{"type":"object","additionalProperties":{"type":"number"}},"outcomeCounts":{"type":"object","additionalProperties":{"type":"number"}}},"required":["count","turns","duration","promptTokens","completionTokens","statusCounts","outcomeCounts"]},"AggregateStats":{"type":["object","null"],"properties":{"min":{"type":"number"},"max":{"type":"number"},"mean":{"type":"number"},"stdDev":{"type":"number"}},"required":["min","max","mean","stdDev"]},"GroupUniformValues":{"type":"object","properties":{"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":"string"},"platform":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"extensions":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"submissionId":{"type":"string"},"task":{"type":"string"}}},"BulkResubmitInput":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"count":{"type":"integer","minimum":1,"maximum":10,"default":1},"overrides":{"type":"object","properties":{"profileId":{"type":["string","null"]},"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":["string","null"]},"reasoningEffort":{"type":["string","null"]},"maxIterations":{"type":["number","null"]},"mcpServers":{"type":["array","null"],"items":{"type":"string"}},"skillRevisions":{"type":["array","null"],"items":{"type":"string"}},"extensions":{"type":["array","null"],"items":{"type":"string"}}}}},"required":["ids"]},"CreateCriteriaInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"],"default":"gate"},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}},"required":["id","prompt"]},"CriteriaResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"CriteriaGraph":{"type":"object","properties":{"nodes":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphNode"}},"edges":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphEdge"}}},"required":["nodes","edges"]},"CriteriaGraphNode":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}},"required":["id","prompt"]},"CriteriaGraphEdge":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"]},"UpdateCriteriaInput":{"type":"object","properties":{"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}}},"CreatePromptFeatureInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["id","prompt"]},"PromptType":{"type":"string","enum":["select","build","test","run","deploy","agents.md"]},"PromptFeatureResult":{"type":"object","properties":{"featureId":{"type":"string"},"detected":{"type":"boolean"},"evaluated":{"type":"boolean"}},"required":["featureId","detected","evaluated"]},"SuggestedPromptFeature":{"type":"object","properties":{"suggestedId":{"type":"string"},"behavior":{"type":"string"},"prompt":{"type":"string"}},"required":["suggestedId","behavior","prompt"]},"PromptFeatureResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"UpdatePromptFeatureInput":{"type":"object","properties":{"prompt":{"type":"string"}}},"TaskPromptResponse":{"type":"object","properties":{"_id":{"type":"string"},"keyId":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"text":{"type":"string"},"contentBlobUrl":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","keyId","createdAt","projectId"]},"CreateTaskPromptInput":{"type":"object","properties":{"text":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["text"]},"PatchTaskPromptFeatureInput":{"type":"object","properties":{"detected":{"type":"boolean"}},"required":["detected"]},"CreateReportInput":{"type":"object","properties":{"requestId":{"type":"string"},"templateId":{"type":"string"}},"required":["requestId"]},"BulkCreateReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}},"templateId":{"type":"string"}},"required":["requestIds"]},"BulkReportStatusInput":{"type":"object","properties":{"reportIds":{"type":"array","items":{"type":"string"}}},"required":["reportIds"]},"BulkReportSummaryResponse":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/ReportSummary"}},"ReportSummary":{"type":"object","properties":{"total":{"type":"number"},"pending":{"type":"number"},"generating":{"type":"number"},"completed":{"type":"number"},"failed":{"type":"number"}},"required":["total","pending","generating","completed","failed"]},"BulkReportSummaryInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"TriggerReportsInput":{"type":"object","properties":{"requestId":{"type":"string"}},"required":["requestId"]},"BulkTriggerReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"InsightResponse":{"type":"object","properties":{"_id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"upvotes":{"type":"number"},"downvotes":{"type":"number"},"blocked":{"type":"boolean"},"referenceCount":{"type":"number"},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","title","description","upvotes","downvotes","blocked","referenceCount","createdBy","createdAt","projectId"]},"ProfileWithVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"version":{"$ref":"#/components/schemas/ProfileVersionResponse"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","version","projectId"]},"ProfileVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"profileId":{"type":"string"},"version":{"type":"number"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","profileId","version","workerType","model","createdAt","projectId"]},"CreateProfileInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["name","workerType","model"]},"ProfileResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","projectId"]},"UpdateProfileIdentity":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512}}},"ReportTemplateResponse":{"type":"object","properties":{"_id":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","id","name","userPrompt","createdAt","projectId"]},"ReportTemplateSystemPrompt":{"type":"object","properties":{"mode":{"type":"string","enum":["append","override"]},"content":{"type":"string"}},"required":["mode","content"]},"ReportTrigger":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["always"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["criteria"]},"criteriaIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","criteriaIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["taskPrompt"]},"taskPromptIds":{"type":"array","items":{"type":"string"}}},"required":["type","taskPromptIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["promptFeature"]},"featureIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","featureIds"]}]},"CreateReportTemplateInput":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0}},"required":["id","name","userPrompt"]},"UpdateReportTemplateInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"allOf":[{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},{"type":["object","null"]}]},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":["string","null"]},"timeoutMs":{"type":["integer","null"],"exclusiveMinimum":0}}},"AgentResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"},"versions":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","supportedModels","createdAt"]},"AgentCapabilities":{"type":"object","properties":{"supportsReasoningEffort":{"type":"boolean"},"supportsMcpServers":{"type":"boolean"},"supportsSkills":{"type":"boolean"},"supportsExtensions":{"type":"boolean"},"supportsResources":{"type":"boolean"}}},"AgentVersion":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string"},"status":{"type":"string","enum":["active","retired"]},"createdAt":{"type":["string","null"],"format":"date-time"}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","status","createdAt"]},"CreateAgentInput":{"type":"object","properties":{"_id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}},"required":["_id","name"]},"UpdateAgentInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}}},"RegisterAgentVersionInput":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string","minLength":1}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","queueName"]},"PatchAgentVersionInput":{"type":"object","properties":{"status":{"type":"string","enum":["active","retired"]}},"required":["status"]},"ModelResponse":{"type":"object","properties":{"_id":{"type":"string"},"modelId":{"type":"string"},"provider":{"type":"string"},"agentId":{"type":"string"},"firstSeenAt":{"type":["string","null"],"format":"date-time"},"lastSeenAt":{"type":["string","null"],"format":"date-time"},"disappearedAt":{"type":["string","null"],"format":"date-time"},"providerAvailableFrom":{"type":["string","null"],"format":"date-time"},"providerEndOfLife":{"type":["string","null"],"format":"date-time"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"$ref":"#/components/schemas/ModelCapabilities"}},"required":["_id","modelId","provider","agentId","firstSeenAt","lastSeenAt"]},"ModelCapabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}},"McpServerResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","type","createdAt","projectId"]},"McpServerHeader":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}},"required":["name","value"]},"UpdateMcpServerInput":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}}},"SkillResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","source","skillName","name","origin","createdAt","projectId"]},"SkillSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"source":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"installs":{"type":"number"}},"required":["id","name","source","internal"]},"SkillDiscoveryResult":{"type":"object","properties":{"skillName":{"type":"string"},"skillPath":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"existsInLibrary":{"type":"boolean"},"currentRevisionCommitSha":{"type":"string"},"latestUpstreamCommitSha":{"type":"string"},"updateAvailable":{"type":"boolean"},"lastImportedAt":{"type":"string"}},"required":["skillName","skillPath"]},"SkillRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"ref":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"skillPath":{"type":"string"},"commitHash":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"name":{"type":"string"},"description":{"type":"string"},"license":{"type":"string"},"compatibility":{"type":"string"},"allowedTools":{"type":"string"},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"content":{"type":"string"},"archiveUrl":{"type":"string"},"validationWarnings":{"type":"array","items":{"type":"string"}},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","ref","source","skillName","skillPath","commitHash","commitTimestamp","name","description","content","archiveUrl","resolvedAt","createdAt","projectId"]},"CreateSkillInput":{"type":"object","properties":{"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]}},"required":["source","skillName","name","origin"]},"CodebaseResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"defaultBranch":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","sourceType","revisionCounter","createdAt","projectId"]},"UpdateCodebaseInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"defaultBranch":{"type":"string"}}},"CodebaseRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"codebaseId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"requestedRef":{"type":"string"},"resolvedCommitSha":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"originalFilename":{"type":"string"},"contentSha256":{"type":"string"},"archiveUrl":{"type":"string"},"sizeBytes":{"type":"number"},"fileCount":{"type":"number"},"creator":{"type":"string"},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","codebaseId","slug","revisionNumber","ref","sourceType","archiveUrl","resolvedAt","createdAt","projectId"]},"ResolveCodebaseRevisionInput":{"type":"object","properties":{"requestedRef":{"type":"string"},"creator":{"type":"string"}}},"ResourceResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"latestRevisionNumber":{"type":"number"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","revisionCounter","createdAt","projectId"]},"CreateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1},"description":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["name","setup"]},"ResourceScript":{"type":"object","properties":{"sh":{"type":"string","minLength":1}}},"ResourceParameter":{"type":"object","properties":{"name":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"description":{"type":"string"},"required":{"type":"boolean","default":false},"default":{"type":"string"},"example":{"type":"string"}},"required":["name"]},"UpdateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"ResourceRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"resourceId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"contentSha256":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","resourceId","slug","revisionNumber","ref","setup","exports","contentSha256","createdAt","projectId"]},"CreateResourceRevisionInput":{"type":"object","properties":{"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["setup"]},"ExtensionResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","publisher","name","origin","createdAt","projectId"]},"ExtensionSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"publisher":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"version":{"type":"string"}},"required":["id","name","publisher","internal"]},"CreateExtensionInput":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+$"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]}},"required":["_id","publisher","name","origin"]},"UpdateExtensionInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ExtensionVersionInfo":{"type":"object","properties":{"version":{"type":"string"},"preRelease":{"type":"boolean"},"lastUpdated":{"type":"string"}},"required":["version","preRelease","lastUpdated"]},"CreateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"}},"required":["title","description"]},"UpdateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}}}},"FeatureFlagResponse":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"enabled":{"type":"boolean"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["key","label","enabled","updatedAt"]},"UpdateFeatureFlagInput":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]}},"parameters":{}},"paths":{"/api/v1/keys/preview":{"post":{"tags":["Keys"],"summary":"Preview key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}}},"/api/v1/keys":{"post":{"tags":["Keys"],"summary":"Create key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"201":{"description":"Key created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"get":{"tags":["Keys"],"summary":"List keys","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KeyResponse"}}}}}}}},"/api/v1/keys/{id}":{"get":{"tags":["Keys"],"summary":"Get key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"put":{"tags":["Keys"],"summary":"Update key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"delete":{"tags":["Keys"],"summary":"Delete key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Key deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/keys/{id}/validate":{"post":{"tags":["Keys"],"summary":"Validate key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateKeyInput"}}}},"responses":{"200":{"description":"Validation result","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Key validation result"}}}}}}},"/api/v1/accounts":{"post":{"tags":["Accounts"],"summary":"Create account","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"201":{"description":"Account created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"get":{"tags":["Accounts"],"summary":"List accounts","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccountResponse"}}}}}}}},"/api/v1/accounts/{id}":{"get":{"tags":["Accounts"],"summary":"Get account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"put":{"tags":["Accounts"],"summary":"Update account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"200":{"description":"Account updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"delete":{"tags":["Accounts"],"summary":"Delete account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Account deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/users/me":{"get":{"tags":["Users"],"summary":"Get the authenticated user's identity","description":"Read the existing authenticated Scope identity without enrollment or profile writes. Responses must not be HTTP-cached.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is not enrolled or is disabled"},"503":{"description":"Authentication service unavailable"}}},"post":{"tags":["Users"],"summary":"Enroll the authenticated user","description":"JIT-enroll the authenticated user, refresh their profile and lastLoginAt, apply bootstrap-admin rules, and warm the access cache. Use this only after an explicit IdP login; do not prefetch, poll, or automatically retry transient failures.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is disabled"},"503":{"description":"Authentication service unavailable"}}}},"/health":{"get":{"tags":["Health"],"summary":"Liveness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"version":{"type":"string"}},"required":["status","version"]}}}}}}},"/ready":{"get":{"tags":["Health"],"summary":"Readiness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"migrations":{}},"required":["status"]}}}},"503":{"description":"Service is not ready"}}}},"/about":{"get":{"tags":["System"],"summary":"API metadata","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"version":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"description":{"type":"string"},"workers":{"type":"array","items":{"type":"string"}}},"required":["name","version","buildTime","environment","description","workers"]}}}}}}},"/api/v1/version":{"get":{"tags":["System"],"summary":"Version info","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"commit":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"strictAgentCapabilities":{"type":"boolean"}},"required":["commit","buildTime","environment","strictAgentCapabilities"]}}}}}}},"/api/v1/projects":{"get":{"tags":["Projects"],"summary":"List all projects","parameters":[{"schema":{"type":"string","enum":["true","false"],"description":"When true, include soft-deleted projects in the result"},"required":false,"description":"When true, include soft-deleted projects in the result","name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProjectResponse"}}}}}}},"post":{"tags":["Projects"],"summary":"Create a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/projects/{id}":{"get":{"tags":["Projects"],"summary":"Get a project","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"patch":{"tags":["Projects"],"summary":"Update a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProjectInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"delete":{"tags":["Projects"],"summary":"Soft-delete a project","responses":{"204":{"description":"Success"},"404":{"description":"Project not found"}}}},"/api/v1/projects/{id}/restore":{"post":{"tags":["Projects"],"summary":"Restore a soft-deleted project","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}}},"/api/v1/submissions":{"get":{"tags":["Submissions"],"summary":"List submissions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionListResponse"}}}}}}},"/api/v1/submissions/{id}/readout":{"get":{"tags":["Submissions"],"summary":"Get the current submission readout report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"404":{"description":"Current readout not found"}}}},"/api/v1/submissions/{id}":{"get":{"tags":["Submissions"],"summary":"Get a submission","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionResponse"}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}/runs":{"get":{"tags":["Submissions"],"summary":"List current and historical submission attempts","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"runs":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRunResponse"}}},"required":["runs"]}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}/runs/{submissionRunId}":{"get":{"tags":["Submissions"],"summary":"Get one submission attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"submissionRunId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionRunResponse"}}}},"404":{"description":"Submission attempt not found"}}}},"/api/v1/submissions/{id}/retry":{"post":{"tags":["Submissions"],"summary":"Retry current submission processing","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"submissionRunId":{"type":"string"}},"required":["submissionRunId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionRetryResponse"}}}},"404":{"description":"Submission not found"},"409":{"description":"Submission attempt is stale or dependencies are blocked"}}}},"/api/v1/requests":{"post":{"tags":["Requests"],"summary":"Submit request(s)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/CreateRequestInput"},{"type":"object","properties":{"count":{"type":"number","minimum":1,"maximum":10,"default":1},"skills":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"codebase":{"type":"string"}}}]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RequestResponse"},{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}]}}}}}},"get":{"tags":["Requests"],"summary":"List requests","parameters":[{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"profileId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"status","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"outcome","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"model","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"os","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"priority","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"agentVersion","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"type":"string","enum":["task","submissionId","profile"]},"required":false,"name":"groupBy","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"after","in":"query"},{"schema":{"type":"string"},"required":false,"name":"before","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"last","in":"query"},{"schema":{"type":"string","enum":["created","updated","priority","worker","status","id","duration","createdAt"]},"required":false,"name":"sortBy","in":"query"},{"schema":{"type":"string","enum":["asc","desc"]},"required":false,"name":"sortDir","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdAfter","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdBefore","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"turns","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"turnsOp","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"maxIterations","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"maxIterationsOp","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PaginatedRunsResponse"},{"$ref":"#/components/schemas/PaginatedRunGroupsResponse"}]}}}}}}},"/api/v1/requests/facets":{"get":{"tags":["Requests"],"summary":"List run filter facets","description":"Returns every distinct value and its full-dataset count per categorical filter dimension for the Runs list rail. Counts are absolute over all non-deleted runs **in the given project**: they intentionally ignore the active search, date range, iteration, and categorical selections so every selectable value stays visible with a stable count. Requires ?projectId=. Computed with parallel $group aggregations (Cosmos has no $facet) and cached per-project for a short TTL.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunFacetsResponse"}}}}}}},"/api/v1/requests/{id}":{"get":{"tags":["Requests"],"summary":"Get request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestResponse"}}}},"404":{"description":"Not found"}}},"delete":{"tags":["Requests"],"summary":"Soft-delete request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Not found"}}}},"/api/v1/analysis":{"get":{"tags":["Requests"],"summary":"Compute pass@k / success@T metrics","description":"Aggregates pass@k / success@T metrics over a single project's done runs. Requires ?projectId=.","parameters":[{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"k","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Analysis metrics"}}}}}}},"/api/v1/requests/bulk-resubmit":{"post":{"tags":["Requests"],"summary":"Bulk resubmit requests","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkResubmitInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}}}}}}},"/api/v1/requests/bulk":{"delete":{"tags":["Requests"],"summary":"Bulk soft-delete requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}},"required":["ids"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"number"}},"required":["deleted"]}}}}}}},"/api/v1/requests/archive":{"post":{"tags":["Requests"],"summary":"Download batch archive of multiple runs","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Gzipped batch archive containing individual run archives"},"400":{"description":"Invalid input"},"404":{"description":"One or more runs not found"}}}},"/api/v1/runs/upload":{"post":{"tags":["Requests"],"summary":"Import run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"}}}},"/api/v1/runs/upload-batch":{"post":{"tags":["Requests"],"summary":"Import batch run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"207":{"description":"Multi-status: 201 if all runs imported, 400 if none imported, 207 if partial"},"400":{"description":"No archive uploaded, empty archive, or all runs failed"}}}},"/api/v1/requests/{id}/reports":{"get":{"tags":["Reports"],"summary":"Get reports for request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Run not found"}}}},"/api/v1/requests/{id}/runs":{"get":{"tags":["Requests"],"summary":"List attempts for a request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RunState"}}}}},"404":{"description":"Request not found"}}}},"/api/v1/requests/{id}/runs/{runId}":{"get":{"tags":["Requests"],"summary":"Get a single attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunState"}}}},"404":{"description":"Request or run not found"}}}},"/api/v1/requests/bulk-retry":{"post":{"tags":["Requests"],"summary":"Bulk retry requests (start new attempts)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"force":{"type":"boolean"}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"retried":{"type":"integer"},"skipped":{"type":"integer"},"results":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"},"error":{"type":"string"}},"required":["requestId"]}}},"required":["retried","skipped","results"]}}}}}}},"/api/v1/requests/{id}/retry":{"post":{"tags":["Requests"],"summary":"Retry a request (start a new attempt)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"force":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"}},"required":["requestId","runId","attemptNumber"]}}}},"404":{"description":"Request not found"},"409":{"description":"Conflict — request is not in a retryable state, or a concurrent retry won the race"},"422":{"description":"Cannot retry — current run not yet terminal"}}}},"/api/v1/requests/{id}/priority":{"post":{"tags":["Requests"],"summary":"Set priority on a single request","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"priority":{"type":"integer"}},"required":["priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"priority":{"type":"number"}},"required":["id","priority"]}}}}}}},"/api/v1/requests/bulk-priority":{"post":{"tags":["Requests"],"summary":"Set priority on multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100},"priority":{"type":"integer"}},"required":["ids","priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/pause":{"post":{"tags":["Requests"],"summary":"Pause a single request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/{id}/resume":{"post":{"tags":["Requests"],"summary":"Resume a paused request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/bulk-pause":{"post":{"tags":["Requests"],"summary":"Pause multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/bulk-resume":{"post":{"tags":["Requests"],"summary":"Resume multiple paused requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/cancel":{"post":{"tags":["Requests"],"summary":"Cancel a request (marks as done/failed and signals worker to exit)","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"previousStatus":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"}},"required":["id","previousStatus","status","outcome"]}}}}}}},"/api/v1/requests/bulk-cancel":{"post":{"tags":["Requests"],"summary":"Cancel multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"cancelled":{"type":"number"},"skipped":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"cancelled":{"type":"boolean"},"previousStatus":{"type":"string"},"error":{"type":"string"}},"required":["id","cancelled"]}}},"required":["cancelled","skipped","results"]}}}}}}},"/api/v1/requests/{id}/logs":{"get":{"tags":["Requests"],"summary":"Stream request logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/logs":{"get":{"tags":["Requests"],"summary":"Stream logs for a specific attempt (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific attempt and iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/video":{"get":{"tags":["Requests"],"summary":"Download session recording","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/video":{"get":{"tags":["Requests"],"summary":"Download session recording for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/criteria/generate-prompt":{"post":{"tags":["Criteria"],"summary":"Generate criterion prompt from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"},"gates":{"type":"array","items":{"type":"string"}}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"},"suggestedId":{"type":"string"},"suggestedParents":{"type":"array","items":{"type":"string"}},"suggestedChildren":{"type":"array","items":{"type":"string"}}},"required":["prompt","suggestedId","suggestedParents","suggestedChildren"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/criteria/seed":{"post":{"tags":["Criteria"],"summary":"Seed criteria in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"criteria":{"type":"array","items":{"$ref":"#/components/schemas/CreateCriteriaInput"}}},"required":["criteria"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}},"400":{"description":"Seed batch would introduce a dependency cycle"}}}},"/api/v1/criteria":{"get":{"tags":["Criteria"],"summary":"List criteria","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","description":"Comma-separated criterion IDs to include"},"required":false,"description":"Comma-separated criterion IDs to include","name":"ids","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"When true and ids is set, also include dependency ancestors"},"required":false,"description":"When true and ids is set, also include dependency ancestors","name":"ancestors","in":"query"},{"schema":{"type":"string","enum":["gate","observation"],"description":"Filter by criterion kind"},"required":false,"description":"Filter by criterion kind","name":"kind","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaResponse"}}}}}}},"post":{"tags":["Criteria"],"summary":"Create criterion","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCriteriaInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"409":{"description":"Criterion already exists"}}}},"/api/v1/criteria/mdp":{"get":{"tags":["Criteria"],"summary":"Compute MDP transitions","parameters":[{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"since","in":"query"},{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{}}}}}}}},"/api/v1/criteria/graph":{"get":{"tags":["Criteria"],"summary":"Get criteria DAG","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaGraph"}}}}}}},"/api/v1/criteria/{id}":{"get":{"tags":["Criteria"],"summary":"Get criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"404":{"description":"Criterion not found"}}},"put":{"tags":["Criteria"],"summary":"Update criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCriteriaInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"400":{"description":"Invalid dependency reference or self-reference"},"404":{"description":"Criterion not found"}}},"delete":{"tags":["Criteria"],"summary":"Soft-delete criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Criterion not found"},"409":{"description":"Criterion has dependents"}}}},"/api/v1/prompt-features/generate-prompt":{"post":{"tags":["Prompt Features"],"summary":"Generate prompt feature from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"}},"required":["prompt"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features/seed":{"post":{"tags":["Prompt Features"],"summary":"Seed prompt features in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}},"required":["features"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}}}}},"/api/v1/prompt-features/extract-from-text":{"post":{"tags":["Prompt Features"],"summary":"Extract features from text","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string"},"model":{"type":"string"},"type":{"type":"string","enum":["select","agents.md"]}},"required":["text"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["features","cached"]}}}},"400":{"description":"Empty text string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features":{"get":{"tags":["Prompt Features"],"summary":"List features","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","enum":["select","agents.md"]},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}}}}},"post":{"tags":["Prompt Features"],"summary":"Create feature","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"409":{"description":"Feature already exists"}}}},"/api/v1/prompt-features/{id}":{"get":{"tags":["Prompt Features"],"summary":"Get feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"404":{"description":"Feature not found"}}},"put":{"tags":["Prompt Features"],"summary":"Update feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Feature not found"}}},"delete":{"tags":["Prompt Features"],"summary":"Soft-delete feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Feature not found"}}}},"/api/v1/task-prompts/generate":{"post":{"tags":["Task Prompts"],"summary":"Generate task prompts","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string"},"existingPrompt":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"tasks":{"type":"array","items":{"type":"string"}}},"required":["tasks"]}}}},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts":{"get":{"tags":["Task Prompts"],"summary":"List task prompts","parameters":[{"schema":{"type":["number","null"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":["number","null"]},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"$ref":"#/components/schemas/PromptType"},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/TaskPromptResponse"}},"total":{"type":"number"},"limit":{"type":"number"},"offset":{"type":"number"}},"required":["items","total","limit","offset"]}}}}}},"post":{"tags":["Task Prompts"],"summary":"Create or find task prompt","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTaskPromptInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Empty text string"}}}},"/api/v1/task-prompts/{id}":{"get":{"tags":["Task Prompts"],"summary":"Get task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"404":{"description":"Task prompt not found"}}},"delete":{"tags":["Task Prompts"],"summary":"Soft-delete task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/content":{"get":{"tags":["Task Prompts"],"summary":"Get resolved task prompt content","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"}},"required":["id","text"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/extract-features":{"post":{"tags":["Task Prompts"],"summary":"Extract features from task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"force","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"taskPromptId":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["taskPromptId","features","cached"]}}}},"404":{"description":"Task prompt not found"},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts/{id}/features/{featureId}":{"patch":{"tags":["Task Prompts"],"summary":"Toggle feature flag on task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"featureId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchTaskPromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Invalid detected value"},"404":{"description":"Task prompt or feature not found"}}}},"/api/v1/reports":{"post":{"tags":["Reports"],"summary":"Create report","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Run or template not found"}}},"get":{"tags":["Reports"],"summary":"List reports","parameters":[{"schema":{"type":"string"},"required":false,"name":"requestId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionRunId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}}}}},"/api/v1/reports/bulk-create":{"post":{"tags":["Reports"],"summary":"Bulk create reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}/structured":{"get":{"tags":["Reports"],"summary":"Download a report's structured JSON artifact","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success"},"404":{"description":"Report or structured artifact not found"}}}},"/api/v1/reports/bulk-status":{"post":{"tags":["Reports"],"summary":"Bulk get report statuses","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportStatusInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/bulk-summary":{"post":{"tags":["Reports"],"summary":"Bulk get report summary per run","description":"Returns aggregated report status counts per run. Only the latest report per template is counted (re-triggers are deduplicated).","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}":{"get":{"tags":["Reports"],"summary":"Get report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"404":{"description":"Report not found"}}}},"/api/v1/reports/{id}/logs":{"get":{"tags":["Reports"],"summary":"Stream report logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Report not found"}}}},"/api/v1/reports/trigger":{"post":{"tags":["Reports"],"summary":"Trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"triggered":{"type":"number"},"reports":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}},"required":["triggered","reports"]}}}},"400":{"description":"Invalid input"},"404":{"description":"Run not found"}}}},"/api/v1/reports/bulk-trigger":{"post":{"tags":["Reports"],"summary":"Bulk trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkTriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":{}}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}/insights":{"get":{"tags":["Reports"],"summary":"Get insights for report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/InsightResponse"},{"type":"object","properties":{"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}}}]}}}}},"404":{"description":"Report not found"}}},"post":{"tags":["Reports"],"summary":"Link insight to report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"isNew":{"type":"boolean"}},"required":["insightId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]}}}},"404":{"description":"Report or insight not found"},"409":{"description":"Insight already referenced by this report"}}}},"/api/v1/profiles":{"post":{"tags":["Profiles"],"summary":"Create a new profile","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"get":{"tags":["Profiles"],"summary":"List profiles","parameters":[{"schema":{"type":"string"},"required":false,"name":"workerType","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}":{"get":{"tags":["Profiles"],"summary":"Get profile with latest version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"post":{"tags":["Profiles"],"summary":"Create new profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["workerType","model"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}},"put":{"tags":["Profiles"],"summary":"Update profile identity","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileIdentity"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"}}}}}},"delete":{"tags":["Profiles"],"summary":"Delete profile","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/profiles/{profileId}/versions":{"get":{"tags":["Profiles"],"summary":"List profile versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}/versions/{version}":{"get":{"tags":["Profiles"],"summary":"Get profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"},{"schema":{"type":["number","null"]},"required":false,"name":"version","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}},"/api/v1/report-templates/default-system-prompt":{"get":{"tags":["Report Templates"],"summary":"Get default system prompt","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string"}},"required":["content"]}}}}}}},"/api/v1/report-templates/available-models":{"get":{"tags":["Report Templates"],"summary":"List models available for report generation","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"]}}}}}}}},"/api/v1/report-templates":{"get":{"tags":["Report Templates"],"summary":"List report templates","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}}}}},"post":{"tags":["Report Templates"],"summary":"Create report template","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportTemplateInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"409":{"description":"Report template already exists"}}}},"/api/v1/report-templates/{id}":{"get":{"tags":["Report Templates"],"summary":"Get report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"put":{"tags":["Report Templates"],"summary":"Update report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateReportTemplateInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"delete":{"tags":["Report Templates"],"summary":"Delete report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Report template not found"}}}},"/api/v1/agents":{"get":{"tags":["Agents"],"summary":"List agents","parameters":[{"schema":{"type":"string"},"required":false,"name":"modelProvider","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentResponse"}}}}}}},"post":{"tags":["Agents"],"summary":"Create or update agent (upsert)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"409":{"description":"Restored agent versions conflict with an active queue owner"}}}},"/api/v1/agents/{id}":{"get":{"tags":["Agents"],"summary":"Get agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"404":{"description":"Agent not found"}}},"put":{"tags":["Agents"],"summary":"Update agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAgentInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"}}},"delete":{"tags":["Agents"],"summary":"Delete agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Agent not found"}}}},"/api/v1/agents/{id}/versions":{"get":{"tags":["Agents"],"summary":"List agent versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}}}}},"404":{"description":"Agent not found"}}},"post":{"tags":["Agents"],"summary":"Register agent version (upsert)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterAgentVersionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent registration did not stabilize"}}}},"/api/v1/agents/{id}/versions/{agentVersion}":{"patch":{"tags":["Agents"],"summary":"Patch agent version","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"agentVersion","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchAgentVersionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Invalid status value"},"404":{"description":"Agent or version not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent activation did not stabilize"}}}},"/api/v1/models":{"get":{"tags":["Models"],"summary":"List models","parameters":[{"schema":{"type":"string"},"required":false,"name":"agentId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"provider","in":"query"},{"schema":{"type":"string","enum":["active","disappeared"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ModelResponse"}}}}}}}},"/api/v1/models/{id}":{"get":{"tags":["Models"],"summary":"Get model","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelResponse"}}}}}}},"/api/v1/models/sync":{"post":{"tags":["Models"],"summary":"Sync models from provider","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"agentId":{"type":"string"},"provider":{"type":"string"},"models":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"providerAvailableFrom":{"type":"string"},"providerEndOfLife":{"type":"string"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}}},"required":["id"]}},"scannedAt":{"type":"string"}},"required":["agentId","provider","models","scannedAt"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"added":{"type":"array","items":{"type":"string"}},"removed":{"type":"array","items":{"type":"string"}},"unchanged":{"type":"array","items":{"type":"string"}}},"required":["added","removed","unchanged"]}}}}}}},"/api/v1/mcp/servers":{"get":{"tags":["MCP Servers"],"summary":"List MCP servers","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"post":{"tags":["MCP Servers"],"summary":"Create MCP server","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}},"required":["_id","name","type"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"/api/v1/mcp/servers/{id}":{"get":{"tags":["MCP Servers"],"summary":"Get MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"put":{"tags":["MCP Servers"],"summary":"Update MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMcpServerInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"delete":{"tags":["MCP Servers"],"summary":"Delete MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/skills":{"get":{"tags":["Skills"],"summary":"List all skills","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"post":{"tags":["Skills"],"summary":"Create or import a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSkillInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"/api/v1/skills/search":{"get":{"tags":["Skills"],"summary":"Search skills (internal + external)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/search/external":{"get":{"tags":["Skills"],"summary":"Search external skills registry","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project."},"required":false,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/discover":{"get":{"tags":["Skills"],"summary":"Discover skills in a GitHub repository","parameters":[{"schema":{"type":"string"},"required":true,"name":"source","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillDiscoveryResult"}}}}},"400":{"description":"Missing or malformed source parameter"},"404":{"description":"Repository not found"},"502":{"description":"GitHub API error"}}}},"/api/v1/skills/{id}(*)/revisions":{"get":{"tags":["Skills"],"summary":"List skill revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}}},"404":{"description":"Skill not found"}}}},"/api/v1/skills/{id}(*)":{"get":{"tags":["Skills"],"summary":"Get skill by slug","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}},"404":{"description":"Skill not found"}}},"delete":{"tags":["Skills"],"summary":"Soft-delete a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Skill not found"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)/archive":{"get":{"tags":["Skill Revisions"],"summary":"Download skill revision archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Skill revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ref","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skill-revisions/{id}":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ID","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skills/{id}(*)/resolve":{"post":{"tags":["Skills"],"summary":"Trigger skill resolution","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebases":{"get":{"tags":["Codebases"],"summary":"List all codebases","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseResponse"}}}}}}},"post":{"tags":["Codebases"],"summary":"Create a codebase (archive codebases require the archive file)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input or missing archive"}}}},"/api/v1/codebases/{id}":{"get":{"tags":["Codebases"],"summary":"Get a codebase","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"patch":{"tags":["Codebases"],"summary":"Update a codebase","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCodebaseInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"delete":{"tags":["Codebases"],"summary":"Delete a codebase","responses":{"204":{"description":"Success"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebases/{id}/revisions":{"get":{"tags":["Codebases"],"summary":"List codebase revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}}},"404":{"description":"Codebase not found"}}},"post":{"tags":["Codebases"],"summary":"Resolve a new git codebase revision","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveCodebaseRevisionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"400":{"description":"Not a git codebase"},"404":{"description":"Codebase not found"},"502":{"description":"GitHub resolution failed"}}}},"/api/v1/codebases/{id}/upload":{"post":{"tags":["Codebases"],"summary":"Upload a codebase archive as a new revision","responses":{"201":{"description":"Success"},"400":{"description":"Missing archive or not an archive codebase"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebase-revisions/{id}/archive":{"get":{"tags":["Codebase Revisions"],"summary":"Download codebase revision archive","responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebase-revisions/{id}":{"get":{"tags":["Codebase Revisions"],"summary":"Get codebase revision by id","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources":{"get":{"tags":["Resources"],"summary":"List all resources","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceResponse"}}}}}}},"post":{"tags":["Resources"],"summary":"Create a resource and its first revision","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceInput"}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input"},"409":{"description":"Resource slug or revision ref already exists in this project"}}}},"/api/v1/resources/{id}":{"get":{"tags":["Resources"],"summary":"Get a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"patch":{"tags":["Resources"],"summary":"Update a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateResourceInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"delete":{"tags":["Resources"],"summary":"Delete a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Resource not found"}}}},"/api/v1/resources/{id}/revisions":{"get":{"tags":["Resource Revisions"],"summary":"List resource revisions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}}},"404":{"description":"Resource not found"}}},"post":{"tags":["Resource Revisions"],"summary":"Create a resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceRevisionInput"}}}},"responses":{"201":{"description":"Success"},"404":{"description":"Resource not found"},"409":{"description":"Resource revision ref already exists in this project"}}}},"/api/v1/resources/{id}/revisions/latest":{"get":{"tags":["Resource Revisions"],"summary":"Get the latest resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/{id}/revisions/{revisionNumber}":{"get":{"tags":["Resource Revisions"],"summary":"Get a resource revision by resource and revision number","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"revisionNumber","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/revisions/{id}":{"get":{"tags":["Resource Revisions"],"summary":"Get resource revision by id","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/extensions":{"get":{"tags":["Extensions"],"summary":"List all extensions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"post":{"tags":["Extensions"],"summary":"Create or import an extension","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExtensionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"/api/v1/extensions/search":{"get":{"tags":["Extensions"],"summary":"Search extensions (internal + marketplace)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/extensions/{id}":{"get":{"tags":["Extensions"],"summary":"Get extension by ID","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"put":{"tags":["Extensions"],"summary":"Update extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateExtensionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"delete":{"tags":["Extensions"],"summary":"Delete extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/extensions/{id}/versions":{"get":{"tags":["Extensions"],"summary":"List available versions for an extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"preRelease","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionVersionInfo"}}}}}}}},"/api/v1/insights":{"get":{"tags":["Insights"],"summary":"List insights","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"post":{"tags":["Insights"],"summary":"Create insight","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInsightInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"/api/v1/insights/search":{"get":{"tags":["Insights"],"summary":"Search insights","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}}},"/api/v1/insights/{id}":{"get":{"tags":["Insights"],"summary":"Get insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"put":{"tags":["Insights"],"summary":"Update insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateInsightInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"delete":{"tags":["Insights"],"summary":"Delete insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/upvote":{"post":{"tags":["Insights"],"summary":"Upvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/downvote":{"post":{"tags":["Insights"],"summary":"Downvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/block":{"post":{"tags":["Insights"],"summary":"Block insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/unblock":{"post":{"tags":["Insights"],"summary":"Unblock insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/reports":{"get":{"tags":["Insights"],"summary":"Get reports referencing insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Insight not found"}}}},"/api/v1/feature-flags":{"get":{"tags":["Feature Flags"],"summary":"List feature flags","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"/api/v1/feature-flags/{key}":{"put":{"tags":["Feature Flags"],"summary":"Update feature flag","parameters":[{"schema":{"type":"string"},"required":true,"name":"key","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeatureFlagInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"webhooks":{}} \ No newline at end of file From a294058730277d1f8d96f220566228f443cf665b Mon Sep 17 00:00:00 2001 From: Cedric Vidal Date: Tue, 13 Oct 2026 18:34:38 +0200 Subject: [PATCH 05/15] fix(readout): harden local container startup Install the system CA bundle required by the bundled Copilot CLI, forward configured npm registries, and run all source-based handler registration jobs from development images. Log SDK cleanup failures and add focused container configuration regression coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cbbd5eb-61c6-4e6a-bb38-c377be5b2163 --- apps/workers/readout/Dockerfile | 14 +++++- apps/workers/readout/src/readout-generator.ts | 6 ++- docker-compose.dev.yml | 4 ++ docker-compose.yml | 6 +++ docs/architecture/post-processing.md | 8 ++++ scripts/readout-container.test.ts | 47 +++++++++++++++++++ 6 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 scripts/readout-container.test.ts diff --git a/apps/workers/readout/Dockerfile b/apps/workers/readout/Dockerfile index dc615f72c..9a2a63675 100644 --- a/apps/workers/readout/Dockerfile +++ b/apps/workers/readout/Dockerfile @@ -1,6 +1,12 @@ FROM node:22.22.0-slim AS builder -RUN corepack enable && corepack prepare pnpm@10.29.1 --activate +# The bundled Copilot CLI trusts the OS system CA store. Without this bundle, +# SDK token validation fails with an opaque "fetch failed" error. +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* +ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ +ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ + COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} +RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) WORKDIR /app @@ -31,7 +37,11 @@ CMD ["/app/scripts/dev-entrypoint.sh"] # --- Production stage --- FROM node:22.22.0-slim -RUN corepack enable && corepack prepare pnpm@10.29.1 --activate +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* +ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ +ENV npm_config_registry=${NPM_CONFIG_REGISTRY} \ + COREPACK_NPM_REGISTRY=${NPM_CONFIG_REGISTRY} +RUN corepack enable && (corepack prepare pnpm@10.29.1 --activate || npm install -g pnpm@10.29.1 --force) WORKDIR /app diff --git a/apps/workers/readout/src/readout-generator.ts b/apps/workers/readout/src/readout-generator.ts index 51cc320d0..c30f2d76b 100644 --- a/apps/workers/readout/src/readout-generator.ts +++ b/apps/workers/readout/src/readout-generator.ts @@ -93,6 +93,10 @@ export async function generateSubmissionReadout(input: { } throw new Error("Readout generation ended without a valid result"); } finally { - await client.stop().catch(() => undefined); + await client.stop().catch(async (error: unknown) => { + await input.log("warn", "Failed to stop Copilot SDK client cleanly", { + error: error instanceof Error ? error.message : String(error), + }); + }); } } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 49c6443e8..0a22780d0 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -237,6 +237,10 @@ services: build: target: dev + register-handler-taxonomy: + build: + target: dev + register-handler-readout: build: target: dev diff --git a/docker-compose.yml b/docker-compose.yml index fabaea982..33105e7c1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -874,6 +874,8 @@ services: build: context: . dockerfile: apps/workers/readout/Dockerfile + args: + <<: *npm-build-args profiles: [readout, all] environment: <<: *common-env @@ -900,6 +902,8 @@ services: build: context: . dockerfile: apps/workers/readout/Dockerfile + args: + <<: *npm-build-args profiles: [readout, all] environment: <<: *common-env @@ -934,6 +938,8 @@ services: build: context: . dockerfile: apps/workers/post-processor/Dockerfile + args: + <<: *npm-build-args profiles: [post-processor] environment: <<: *common-env diff --git a/docs/architecture/post-processing.md b/docs/architecture/post-processing.md index 6fbd5a305..e7bdc0908 100644 --- a/docs/architecture/post-processing.md +++ b/docs/architecture/post-processing.md @@ -198,6 +198,14 @@ flowchart LR - **Docker Compose:** one `register-handler-` init service per handler (e.g. `pp-atif`) runs the generic script via `tsx`, gated on the scheduler being up. The worker waits for its registration service to complete. - **Kubernetes:** a `register-handler-post-processor` Job runs the compiled script (`node packages/shared/dist/scripts/register-handler.js`) on each deploy. `pp-atif` and `pp-observations` are registered in K8s via their respective registration Jobs (`register-handler-taxonomy` points its `HANDLER_YAML_PATH` at `handler.taxonomy.yaml`). The scheduler exposes a `Service` (`scheduler.scoped.svc.cluster.local:8080`) so Jobs and workers can reach `/handlers/register` and the notify endpoints. +Compose registration jobs that execute TypeScript source must use the worker's +`dev` image target so their working directory and source tree match the generic +registration entrypoint. Image builds also forward the configured npm registry +through `x-npm-build-args`. Workers that invoke the Copilot SDK, including +`pp-readout`, install the operating-system CA bundle in both development and +production stages because the bundled Copilot CLI validates credentials against +the system trust store. + ## Submission handlers Submission handlers use the same `services` registration endpoint but declare diff --git a/scripts/readout-container.test.ts b/scripts/readout-container.test.ts new file mode 100644 index 000000000..80d07532f --- /dev/null +++ b/scripts/readout-container.test.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { readFile } from "node:fs/promises"; +import { describe, expect, it } from "vitest"; + +const root = new URL("../", import.meta.url); + +async function read(path: string): Promise { + return readFile(new URL(path, root), "utf8"); +} + +describe("readout container configuration", () => { + it("installs the system CA bundle in development and production stages", async () => { + const dockerfile = await read("apps/workers/readout/Dockerfile"); + const installs = dockerfile.match( + /apt-get install -y --no-install-recommends ca-certificates/g, + ); + + expect(installs).toHaveLength(2); + }); + + it("forwards the configured npm registry to readout image builds", async () => { + const compose = await read("docker-compose.yml"); + + for (const service of ["readout-handler", "register-handler-readout"]) { + expect(compose).toMatch( + new RegExp( + `\\n ${service}:\\n[\\s\\S]*?dockerfile: apps/workers/readout/Dockerfile\\n[\\s\\S]*?args:\\n\\s+<<: \\*npm-build-args`, + ), + ); + } + }); + + it("uses the development image for both post-processor registration jobs", async () => { + const compose = await read("docker-compose.dev.yml"); + + for (const service of [ + "register-handler-post-processor", + "register-handler-taxonomy", + ]) { + expect(compose).toMatch( + new RegExp(`\\n ${service}:\\n\\s+build:\\n\\s+target: dev`), + ); + } + }); +}); From 0f8f70c7ecad1067a9358508e913173c9c95248f Mon Sep 17 00:00:00 2001 From: Cedric Vidal Date: Tue, 13 Oct 2026 21:08:47 +0200 Subject: [PATCH 06/15] fix(portal): use generic report terminology Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cbbd5eb-61c6-4e6a-bb38-c377be5b2163 --- .../SubmissionStatusCard.stories.tsx | 4 +- .../components/SubmissionStatusCard.test.tsx | 36 +++++- .../src/components/SubmissionStatusCard.tsx | 14 +-- .../src/pages/ReportPreviewPanel.test.tsx | 60 ++++++++++ apps/portal/src/pages/ReportPreviewPanel.tsx | 4 +- .../src/pages/SubmissionDetail.test.tsx | 108 ++++++++++++++++++ apps/portal/src/pages/SubmissionDetail.tsx | 22 ++-- 7 files changed, 226 insertions(+), 22 deletions(-) create mode 100644 apps/portal/src/pages/ReportPreviewPanel.test.tsx create mode 100644 apps/portal/src/pages/SubmissionDetail.test.tsx diff --git a/apps/portal/src/components/SubmissionStatusCard.stories.tsx b/apps/portal/src/components/SubmissionStatusCard.stories.tsx index 13c973d70..8809eb169 100644 --- a/apps/portal/src/components/SubmissionStatusCard.stories.tsx +++ b/apps/portal/src/components/SubmissionStatusCard.stories.tsx @@ -59,7 +59,7 @@ const meta = { export default meta; type Story = StoryObj; -export const Processing: Story = {}; +export const GeneratingReport: Story = {}; export const Retryable: Story = { args: { @@ -73,7 +73,7 @@ export const Retryable: Story = { }, }; -export const Completed: Story = { +export const ReportReady: Story = { args: { submission: { ...submission, diff --git a/apps/portal/src/components/SubmissionStatusCard.test.tsx b/apps/portal/src/components/SubmissionStatusCard.test.tsx index b735e762e..36323f4b7 100644 --- a/apps/portal/src/components/SubmissionStatusCard.test.tsx +++ b/apps/portal/src/components/SubmissionStatusCard.test.tsx @@ -61,8 +61,42 @@ describe("SubmissionStatusCard", () => { render(); - expect(screen.getByText("blocked")).toBeDefined(); + expect(screen.getByText("Report generation")).toBeDefined(); + expect(screen.getByText("Generation blocked")).toBeDefined(); expect(screen.getByText("3")).toBeDefined(); expect(screen.getByText("1/2")).toBeDefined(); }); + + it.each([ + ["waiting", "Waiting on runs"], + ["ready", "Ready to generate"], + ["processing", "Generating report"], + ["done", "Report ready"], + ["failed", "Generation failed"], + ] as const)("renders %s as %s", (readiness, label) => { + const submission: Submission = { + submissionId: "submission-1", + projectId: "project-1", + requestIds: ["request-1"], + createdAt: new Date().toISOString(), + submissionRunId: "submission-run-1", + attemptNumber: 1, + readiness, + requests: [ + { + requestId: "request-1", + runId: "run-1", + task: "Task", + status: "done", + workerType: "worker", + }, + ], + dependencyProjection: [], + blocking: [], + }; + + render(); + + expect(screen.getByText(label)).toBeDefined(); + }); }); diff --git a/apps/portal/src/components/SubmissionStatusCard.tsx b/apps/portal/src/components/SubmissionStatusCard.tsx index dd44a6792..ed7df211b 100644 --- a/apps/portal/src/components/SubmissionStatusCard.tsx +++ b/apps/portal/src/components/SubmissionStatusCard.tsx @@ -18,12 +18,12 @@ const readinessVariant: Record< }; const readinessLabel: Record = { - waiting: "waiting", - ready: "retryable", - blocked: "blocked", - processing: "processing", - done: "completed", - failed: "failed", + waiting: "Waiting on runs", + ready: "Ready to generate", + blocked: "Generation blocked", + processing: "Generating report", + done: "Report ready", + failed: "Generation failed", }; export function SubmissionStatusCard({ @@ -37,7 +37,7 @@ export function SubmissionStatusCard({ return ( - Processing + Report generation {readinessLabel[submission.readiness]} diff --git a/apps/portal/src/pages/ReportPreviewPanel.test.tsx b/apps/portal/src/pages/ReportPreviewPanel.test.tsx new file mode 100644 index 000000000..a7e9e5dc9 --- /dev/null +++ b/apps/portal/src/pages/ReportPreviewPanel.test.tsx @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// @vitest-environment happy-dom +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { cleanup, render, screen } from "@testing-library/react"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { Report } from "@/types"; + +const report: Report = { + _id: "report-1", + id: "report-1", + submissionId: "submission-1", + submissionRunId: "submission-run-1", + status: "completed", + content: "# Generated report", + reporter: { + id: "pp-readout", + name: "Submission Readout", + gitHash: "development", + model: "gpt-5.4", + agentId: "unknown", + agentVersion: "unknown", + }, + createdAt: "2026-01-01T00:00:00.000Z", +}; + +vi.mock("@/lib/api", () => ({ + api: { + getReport: vi.fn(async () => report), + }, +})); + +import { ReportPreviewPanel } from "./ReportPreviewPanel"; + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +describe("ReportPreviewPanel submission reports", () => { + it("identifies a template-less report without using report-type terminology", async () => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + render( + + + + } /> + + + , + ); + + expect(await screen.findByText("template-less")).toBeDefined(); + expect(screen.queryByText("default template")).toBeNull(); + }); +}); diff --git a/apps/portal/src/pages/ReportPreviewPanel.tsx b/apps/portal/src/pages/ReportPreviewPanel.tsx index f6ad08a20..9f682b906 100644 --- a/apps/portal/src/pages/ReportPreviewPanel.tsx +++ b/apps/portal/src/pages/ReportPreviewPanel.tsx @@ -91,7 +91,9 @@ export function ReportPreviewPanel() { {report.templateId} ) : ( - default template + + {report.submissionId ? "template-less" : "default template"} + )}
diff --git a/apps/portal/src/pages/SubmissionDetail.test.tsx b/apps/portal/src/pages/SubmissionDetail.test.tsx new file mode 100644 index 000000000..674bc6954 --- /dev/null +++ b/apps/portal/src/pages/SubmissionDetail.test.tsx @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// @vitest-environment happy-dom +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { cleanup, render, screen } from "@testing-library/react"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { Report, Submission } from "@/types"; + +const submission: Submission = { + submissionId: "submission-1", + projectId: "project-1", + requestIds: ["request-1"], + createdAt: "2026-01-01T00:00:00.000Z", + submissionRunId: "submission-run-2", + attemptNumber: 2, + readiness: "done", + requests: [ + { + requestId: "request-1", + runId: "run-1", + task: "Create an API", + status: "done", + outcome: "succeeded", + workerType: "coder-acp-copilot", + }, + ], + dependencyProjection: [ + { + requestId: "request-1", + runId: "run-1", + handlerId: "pp-observations", + state: "ready", + }, + ], + blocking: [], + report: { reportId: "report-1" }, +}; + +const history = { + runs: [ + { + _id: "submission-run-2", + attemptNumber: 2, + requests: [{ requestId: "request-1", runId: "run-1" }], + report: { reportId: "report-1" }, + createdAt: "2026-01-01T00:00:00.000Z", + }, + ], +}; + +const report: Report = { + _id: "report-1", + id: "report-1", + submissionId: "submission-1", + submissionRunId: "submission-run-2", + status: "completed", + content: "# Generated report", + structuredArtifact: { + schemaId: "scope.submission-readout", + schemaVersion: "1.0", + contentType: "application/json", + url: "reports/report-1/structured.json", + }, + createdAt: "2026-01-01T00:00:00.000Z", +}; + +vi.mock("@/lib/api", () => ({ + api: { + getSubmission: vi.fn(async () => submission), + getSubmissionRuns: vi.fn(async () => history), + getSubmissionReadout: vi.fn(async () => report), + retrySubmission: vi.fn(), + reportStructuredUrl: vi.fn(() => "/api/v1/reports/report-1/structured"), + }, +})); + +import { SubmissionDetail } from "./SubmissionDetail"; + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +describe("SubmissionDetail report terminology", () => { + it("uses generic report language for the current result and history", async () => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + render( + + + + } /> + + + , + ); + + expect(await screen.findByText("Current report")).toBeDefined(); + expect(screen.getByText("Report generation")).toBeDefined(); + expect(screen.getByText("Report ready")).toBeDefined(); + expect(screen.getByRole("button", { name: "Retry report generation" })).toBeDefined(); + expect(await screen.findByRole("link", { name: "View report" })).toBeDefined(); + expect(screen.queryByText(/readout/i)).toBeNull(); + }); +}); diff --git a/apps/portal/src/pages/SubmissionDetail.tsx b/apps/portal/src/pages/SubmissionDetail.tsx index 61cd684b0..c7f7dccb8 100644 --- a/apps/portal/src/pages/SubmissionDetail.tsx +++ b/apps/portal/src/pages/SubmissionDetail.tsx @@ -35,7 +35,7 @@ export function SubmissionDetail() { submissionQuery.data!.submissionRunId, ), onSuccess: () => { - toast.success("Submission processing restarted"); + toast.success("Report generation restarted"); void queryClient.invalidateQueries({ queryKey: ["submission", id] }); }, onError: (error) => { @@ -47,7 +47,7 @@ export function SubmissionDetail() { queryFn: () => api.getSubmissionRuns(id!), enabled: Boolean(id), }); - const readoutQuery = useQuery({ + const reportQuery = useQuery({ queryKey: ["submission-readout", id, submissionQuery.data?.submissionRunId], queryFn: () => api.getSubmissionReadout(id!), enabled: Boolean(id && submissionQuery.data?.report?.reportId), @@ -93,7 +93,7 @@ export function SubmissionDetail() { } > - Retry processing + Retry report generation @@ -101,10 +101,10 @@ export function SubmissionDetail() { - Current readout - {readoutQuery.data?.structuredArtifact && ( + Current report + {reportQuery.data?.structuredArtifact && ( )} From 532530ad89605c12b0621fa773ec184c417c362e Mon Sep 17 00:00:00 2001 From: Cedric Vidal Date: Tue, 13 Oct 2026 21:17:50 +0200 Subject: [PATCH 07/15] fix(portal): clarify pending report state Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cbbd5eb-61c6-4e6a-bb38-c377be5b2163 --- apps/portal/src/components/SubmissionStatusCard.test.tsx | 2 +- apps/portal/src/components/SubmissionStatusCard.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/portal/src/components/SubmissionStatusCard.test.tsx b/apps/portal/src/components/SubmissionStatusCard.test.tsx index 36323f4b7..27576956e 100644 --- a/apps/portal/src/components/SubmissionStatusCard.test.tsx +++ b/apps/portal/src/components/SubmissionStatusCard.test.tsx @@ -68,7 +68,7 @@ describe("SubmissionStatusCard", () => { }); it.each([ - ["waiting", "Waiting on runs"], + ["waiting", "Waiting on dependencies"], ["ready", "Ready to generate"], ["processing", "Generating report"], ["done", "Report ready"], diff --git a/apps/portal/src/components/SubmissionStatusCard.tsx b/apps/portal/src/components/SubmissionStatusCard.tsx index ed7df211b..a75bf7bc4 100644 --- a/apps/portal/src/components/SubmissionStatusCard.tsx +++ b/apps/portal/src/components/SubmissionStatusCard.tsx @@ -18,7 +18,7 @@ const readinessVariant: Record< }; const readinessLabel: Record = { - waiting: "Waiting on runs", + waiting: "Waiting on dependencies", ready: "Ready to generate", blocked: "Generation blocked", processing: "Generating report", From b1811e1000c7542870d2fe1af2cb90c61788be91 Mon Sep 17 00:00:00 2001 From: Cedric Vidal Date: Tue, 13 Oct 2026 21:41:00 +0200 Subject: [PATCH 08/15] fix(readout): include task criteria in evidence Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cbbd5eb-61c6-4e6a-bb38-c377be5b2163 --- .../src/readout-queue-processor.test.ts | 29 ++++++++++++++----- .../readout/src/readout-queue-processor.ts | 1 + 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/apps/workers/readout/src/readout-queue-processor.test.ts b/apps/workers/readout/src/readout-queue-processor.test.ts index 6d842073a..2dc986015 100644 --- a/apps/workers/readout/src/readout-queue-processor.test.ts +++ b/apps/workers/readout/src/readout-queue-processor.test.ts @@ -215,8 +215,8 @@ describe("readout queue orchestration", () => { _id: "request-1", projectId: "project-1", workerType: "coder-acp-copilot", - observations: [], - scenario: { task: "Task", criteria: [] }, + observations: ["observation-criterion"], + scenario: { task: "Task", criteria: ["task-criterion"] }, createdAt: new Date("2026-10-13T12:00:00.000Z"), priority: 0, run: { @@ -233,8 +233,17 @@ describe("readout queue orchestration", () => { attemptNumber: 1, status: "done", outcome: "failed", + observationResults: [{ + criterionId: "result-criterion", + evaluated: true, + passed: false, + feedback: "Archived evidence", + }], } satisfies RunHistoryDocument; const historyFindOne = vi.fn().mockResolvedValue(archivedRun); + const criteriaFind = vi.fn(() => ({ + toArray: vi.fn().mockResolvedValue([]), + })); Object.assign(instance as object, { db: { collection: vi.fn((name: string) => { @@ -247,11 +256,7 @@ describe("readout queue orchestration", () => { } if (name === "runs") return { findOne: historyFindOne }; if (name === "criteria") { - return { - find: vi.fn(() => ({ - toArray: vi.fn().mockResolvedValue([]), - })), - }; + return { find: criteriaFind }; } return {}; }), @@ -270,5 +275,15 @@ describe("readout queue orchestration", () => { projectId: "project-1", }); expect(evidence.runs[0].run?._id).toBe("run-1"); + expect(criteriaFind).toHaveBeenCalledWith({ + projectId: "project-1", + id: { + $in: expect.arrayContaining([ + "task-criterion", + "observation-criterion", + "result-criterion", + ]), + }, + }); }); }); diff --git a/apps/workers/readout/src/readout-queue-processor.ts b/apps/workers/readout/src/readout-queue-processor.ts index 3d0f52fd6..ee075a26d 100644 --- a/apps/workers/readout/src/readout-queue-processor.ts +++ b/apps/workers/readout/src/readout-queue-processor.ts @@ -251,6 +251,7 @@ export class ReadoutQueueProcessor extends BaseQueueProcessor [ + ...request.scenario.criteria, ...(request.observations ?? []), ...(run?.observationResults ?? []).map((result) => result.criterionId), ...(run?.turns ?? []).flatMap((turn) => From c1b81100395f8a0f0244785171e4c5af3c0a4ed4 Mon Sep 17 00:00:00 2001 From: Cedric Vidal Date: Tue, 13 Oct 2026 22:05:51 +0200 Subject: [PATCH 09/15] fix(readout): attach all criteria to run evidence Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cbbd5eb-61c6-4e6a-bb38-c377be5b2163 --- .../src/readout-queue-processor.test.ts | 38 +++++++++++++++++-- .../readout/src/readout-queue-processor.ts | 10 ++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/apps/workers/readout/src/readout-queue-processor.test.ts b/apps/workers/readout/src/readout-queue-processor.test.ts index 2dc986015..ee5c1ab91 100644 --- a/apps/workers/readout/src/readout-queue-processor.test.ts +++ b/apps/workers/readout/src/readout-queue-processor.test.ts @@ -3,7 +3,11 @@ import type { DequeuedMessageItem } from "@azure/storage-queue"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { SubmissionDocument, VisibilityHeartbeat } from "shared"; +import type { + CriteriaDocument, + SubmissionDocument, + VisibilityHeartbeat, +} from "shared"; import type { RequestDocument, RunHistoryDocument } from "shared"; const coordination = vi.hoisted(() => ({ @@ -241,8 +245,28 @@ describe("readout queue orchestration", () => { }], } satisfies RunHistoryDocument; const historyFindOne = vi.fn().mockResolvedValue(archivedRun); + const criteria = [ + { + id: "task-criterion", + prompt: "Task criterion", + projectId: "project-1", + createdAt: new Date("2026-10-13T12:00:00.000Z"), + }, + { + id: "observation-criterion", + prompt: "Configured observation", + projectId: "project-1", + createdAt: new Date("2026-10-13T12:00:00.000Z"), + }, + { + id: "result-criterion", + prompt: "Archived observation result", + projectId: "project-1", + createdAt: new Date("2026-10-13T12:00:00.000Z"), + }, + ] satisfies CriteriaDocument[]; const criteriaFind = vi.fn(() => ({ - toArray: vi.fn().mockResolvedValue([]), + toArray: vi.fn().mockResolvedValue(criteria), })); Object.assign(instance as object, { db: { @@ -265,7 +289,10 @@ describe("readout queue orchestration", () => { const evidence = await (instance as unknown as { loadEvidence(value: SubmissionDocument): Promise<{ - runs: Array<{ run: RunHistoryDocument | RequestDocument["run"] }>; + runs: Array<{ + run: RunHistoryDocument | RequestDocument["run"]; + criteria: CriteriaDocument[]; + }>; }>; }).loadEvidence(submission); @@ -275,6 +302,11 @@ describe("readout queue orchestration", () => { projectId: "project-1", }); expect(evidence.runs[0].run?._id).toBe("run-1"); + expect(evidence.runs[0].criteria.map((criterion) => criterion.id)).toEqual([ + "task-criterion", + "observation-criterion", + "result-criterion", + ]); expect(criteriaFind).toHaveBeenCalledWith({ projectId: "project-1", id: { diff --git a/apps/workers/readout/src/readout-queue-processor.ts b/apps/workers/readout/src/readout-queue-processor.ts index ee075a26d..a33d260e3 100644 --- a/apps/workers/readout/src/readout-queue-processor.ts +++ b/apps/workers/readout/src/readout-queue-processor.ts @@ -273,6 +273,14 @@ export class ReadoutQueueProcessor extends BaseQueueProcessor result.criterionId), + ...(run.turns ?? []).flatMap((turn) => + (turn.observationResults ?? []).map((result) => result.criterionId) + ), + ]); const trajectories = await Promise.all( (run.turns ?? []).flatMap((turn) => turn.atifUrl @@ -288,7 +296,7 @@ export class ReadoutQueueProcessor extends BaseQueueProcessor { + criteria: [...runCriterionIds].flatMap((id) => { const criterion = criteriaById.get(id); return criterion ? [criterion] : []; }), From 6ca5df982a1d7160b12e196c88427fd67682c68e Mon Sep 17 00:00:00 2001 From: Cedric Vidal Date: Tue, 13 Oct 2026 23:14:25 +0200 Subject: [PATCH 10/15] fix(reports): align submission report APIs Use the plural submission reports collection and resolve the current report through the standard report-by-ID resource. Align Portal and CLI clients, filters, Structured JSON retrieval, OpenAPI, tests, and architecture documentation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cbbd5eb-61c6-4e6a-bb38-c377be5b2163 --- .../openapi-snapshot.test.ts.snap | 13 +- apps/api/src/routes/submissions.test.ts | 43 ++++-- apps/api/src/routes/submissions.ts | 42 +++--- apps/cli/src/commands/report.test.ts | 130 ++++++++++++++++++ apps/cli/src/commands/report.ts | 53 ++++++- apps/cli/src/commands/submission.test.ts | 78 +++++++++++ apps/cli/src/commands/submission.ts | 92 ++++++------- apps/portal/src/lib/api.ts | 3 - .../src/pages/SubmissionDetail.test.tsx | 4 +- apps/portal/src/pages/SubmissionDetail.tsx | 7 +- docs/architecture/app-design.md | 4 +- docs/architecture/post-processing.md | 9 ++ website/src/openapi/scope-openapi.json | 2 +- 13 files changed, 376 insertions(+), 104 deletions(-) create mode 100644 apps/cli/src/commands/report.test.ts create mode 100644 apps/cli/src/commands/submission.test.ts diff --git a/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap b/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap index 4f68fb14e..350d502df 100644 --- a/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap +++ b/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap @@ -12197,7 +12197,7 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], }, }, - "/api/v1/submissions/{id}/readout": { + "/api/v1/submissions/{id}/reports": { "get": { "parameters": [ { @@ -12214,19 +12214,22 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReportResponse", + "items": { + "$ref": "#/components/schemas/ReportResponse", + }, + "type": "array", }, }, }, "description": "Success", }, "404": { - "description": "Current readout not found", + "description": "Submission not found", }, }, - "summary": "Get the current submission readout report", + "summary": "Get reports for submission", "tags": [ - "Submissions", + "Reports", ], }, }, diff --git a/apps/api/src/routes/submissions.test.ts b/apps/api/src/routes/submissions.test.ts index 4b1779484..b37064230 100644 --- a/apps/api/src/routes/submissions.test.ts +++ b/apps/api/src/routes/submissions.test.ts @@ -93,6 +93,13 @@ function buildApp(input: { find: vi.fn(() => queryResult(input.requests)), } as unknown as Collection, reportCollection: { + find: vi.fn((filter: Record) => + queryResult((input.reports ?? []).filter((report) => + Object.entries(filter).every(([key, value]) => + report[key as keyof ReportDocument] === value + ) + )), + ), findOne: vi.fn(async (filter: Record) => (input.reports ?? []).find((report) => Object.entries(filter).every(([key, value]) => @@ -262,7 +269,7 @@ describe("submission routes", () => { ]); }); - it("selects only the current submission run readout", async () => { + it("lists current and historical reports for the submission", async () => { const currentReport: ReportDocument = { _id: "report-current", projectId: "project-1", @@ -270,7 +277,14 @@ describe("submission routes", () => { submissionRunId: "submission-run-1", status: "completed", content: "# Current", - createdAt: new Date(), + createdAt: new Date("2026-01-02T00:00:00.000Z"), + }; + const historicalReport: ReportDocument = { + ...currentReport, + _id: "report-historical", + submissionRunId: "submission-run-0", + content: "# Historical", + createdAt: new Date("2026-01-01T00:00:00.000Z"), }; const response = await request( buildApp({ @@ -282,21 +296,20 @@ describe("submission routes", () => { report: { reportId: "report-current" }, }, }, - reports: [ - { - ...currentReport, - _id: "report-historical", - submissionRunId: "submission-run-0", - }, - currentReport, - ], + reports: [currentReport, historicalReport], }), - ).get("/api/v1/submissions/submission-1/readout"); + ).get("/api/v1/submissions/submission-1/reports"); expect(response.status).toBe(200); - expect(response.body).toMatchObject({ - _id: "report-current", - submissionRunId: "submission-run-1", - }); + expect(response.body).toMatchObject([ + { + _id: "report-current", + submissionRunId: "submission-run-1", + }, + { + _id: "report-historical", + submissionRunId: "submission-run-0", + }, + ]); }); }); diff --git a/apps/api/src/routes/submissions.ts b/apps/api/src/routes/submissions.ts index aca173167..b46ce23ab 100644 --- a/apps/api/src/routes/submissions.ts +++ b/apps/api/src/routes/submissions.ts @@ -126,29 +126,29 @@ export function registerSubmissionsRoutes(ctx: RouteContext): void { apiRoute(ctx.app, ctx.registry, { method: "get", - path: "/api/v1/submissions/:id/readout", - tags: ["Submissions"], - summary: "Get the current submission readout report", + path: "/api/v1/submissions/:id/reports", + tags: ["Reports"], + summary: "Get reports for submission", params: z.object({ id: z.string() }), - response: ReportResponseSchema, - errorResponses: { 404: { description: "Current readout not found" } }, - handler: async (req, res) => { - const submission = await ctx.submissionCollection.findOne({ _id: req.params.id }); - const reportId = submission?.run.report?.reportId; - if (!submission || !reportId) { - res.status(404).json({ error: "Current readout not found" }); - return; - } - const report = await ctx.reportCollection.findOne({ - _id: reportId, - submissionId: submission._id, - submissionRunId: submission.run._id, - }); - if (!report) { - res.status(404).json({ error: "Current readout not found" }); - return; + response: z.array(ReportResponseSchema), + errorResponses: { 404: { description: "Submission not found" } }, + handler: async (req, res, next) => { + try { + const submission = await ctx.submissionCollection.findOne({ + _id: req.params.id, + }); + if (!submission) { + res.status(404).json({ error: "Submission not found" }); + return; + } + const reports = await ctx.reportCollection.find({ + projectId: submission.projectId, + submissionId: submission._id, + }).sort({ createdAt: -1 }).toArray(); + res.json(reports.map((report) => ({ ...report, id: report._id }))); + } catch (error) { + next(error); } - res.json({ ...report, id: report._id }); }, }); diff --git a/apps/cli/src/commands/report.test.ts b/apps/cli/src/commands/report.test.ts new file mode 100644 index 000000000..9fd85335b --- /dev/null +++ b/apps/cli/src/commands/report.test.ts @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { Command } from "commander"; +import { registerReportCommands } from "./report.js"; + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + process.exitCode = undefined; +}); + +describe("report get", () => { + it("downloads Structured JSON through the standard report-by-ID command", async () => { + const urls: string[] = []; + vi.stubGlobal("fetch", vi.fn(async (request: Request) => { + urls.push(request.url); + return { + ok: true, + status: 200, + json: async () => ({ + meta: { schemaId: "scope.submission-readout" }, + }), + }; + })); + const output: string[] = []; + vi.spyOn(console, "log").mockImplementation((...parts: unknown[]) => { + output.push(parts.map(String).join(" ")); + }); + const program = new Command(); + registerReportCommands(program); + + await program.parseAsync( + [ + "report", + "get", + "-i", + "report-1", + "--structured", + "-u", + "http://localhost:3100", + ], + { from: "user" }, + ); + + expect(urls).toEqual([ + "http://localhost:3100/api/v1/reports/report-1/structured", + ]); + expect(output.join("\n")).toContain('"schemaId": "scope.submission-readout"'); + }); + + it("renders submission subject metadata through standard report retrieval", async () => { + vi.stubGlobal("fetch", vi.fn(async () => ({ + ok: true, + status: 200, + json: async () => ({ + id: "report-1", + submissionId: "submission-1", + submissionRunId: "submission-run-1", + status: "completed", + createdAt: "2026-01-01T00:00:00.000Z", + }), + }))); + const output: string[] = []; + vi.spyOn(console, "log").mockImplementation((...parts: unknown[]) => { + output.push(parts.map(String).join(" ")); + }); + const program = new Command(); + registerReportCommands(program); + + await program.parseAsync( + [ + "report", + "get", + "-i", + "report-1", + "-o", + "json", + "-u", + "http://localhost:3100", + ], + { from: "user" }, + ); + + expect(output.join("\n")).toContain('"submissionId": "submission-1"'); + expect(output.join("\n")).toContain( + '"submissionRunId": "submission-run-1"', + ); + }); +}); + +describe("report list", () => { + it("passes submission filters to the global reports collection", async () => { + const urls: string[] = []; + vi.stubGlobal("fetch", vi.fn(async (request: Request) => { + urls.push(request.url); + return { + ok: true, + status: 200, + json: async () => [], + }; + })); + vi.spyOn(console, "log").mockImplementation(() => undefined); + const program = new Command(); + registerReportCommands(program); + + await program.parseAsync( + [ + "report", + "list", + "--submission", + "submission-1", + "--submission-run", + "submission-run-1", + "--project", + "project-1", + "-o", + "json", + "-u", + "http://localhost:3100", + ], + { from: "user" }, + ); + + expect(urls).toEqual([ + "http://localhost:3100/api/v1/reports?submissionId=submission-1&submissionRunId=submission-run-1&projectId=project-1", + ]); + }); +}); diff --git a/apps/cli/src/commands/report.ts b/apps/cli/src/commands/report.ts index 1dce6f3b6..058f047ef 100644 --- a/apps/cli/src/commands/report.ts +++ b/apps/cli/src/commands/report.ts @@ -16,7 +16,7 @@ export function registerReportCommands(program: Command): void { const report = program .command("report") - .description("Generate, view, and monitor run reports") + .description("Generate, view, and monitor reports") .action(() => { report.help(); }); @@ -106,11 +106,26 @@ report .command("get") .description("Get a report by ID") .requiredOption("-i, --id ", "Report ID") + .option("--structured", "Get the report's Structured JSON artifact") .option("-u, --url ", "API base URL", getDefaultApiUrl()) , ['markdown']) .action(async (options) => { const format = (options.output || 'table') as OutputFormat; try { + if (options.structured) { + const structuredResponse = await apiFetch( + options.url, + `/reports/${encodeURIComponent(options.id)}/structured`, + ); + if (!structuredResponse.ok) { + const error = await structuredResponse.json(); + console.error(errorText("Error:"), error.error || JSON.stringify(error)); + process.exitCode = 1; + return; + } + console.log(JSON.stringify(await structuredResponse.json(), null, 2)); + return; + } const response = await apiFetch(options.url, `/reports/${options.id}`); if (!response.ok) { @@ -121,7 +136,9 @@ report const report = await response.json() as { id: string; - requestId: string; + requestId?: string; + submissionId?: string; + submissionRunId?: string; status: string; reporter?: { id: string; name: string; model: string; agentId: string; agentVersion: string }; content?: string; @@ -139,6 +156,8 @@ report const fields: DisplayField[] = [ { key: 'id', label: 'ID' }, { key: 'requestId', label: 'Run' }, + { key: 'submissionId', label: 'Submission' }, + { key: 'submissionRunId', label: 'Submission Run' }, { key: 'status', label: 'Status' }, { key: 'reporter', label: 'Reporter', formatter: (r: any) => r.reporter ? `${r.reporter.name} (${r.reporter.agentId}@${r.reporter.agentVersion})` : '' }, { key: 'model', label: 'Model', formatter: (r: any) => r.reporter?.model || '' }, @@ -152,7 +171,15 @@ report } console.log(`${label('Report:')} ${value(report.id)}`); - console.log(`${label('Run:')} ${value(report.requestId)}`); + if (report.requestId) { + console.log(`${label('Run:')} ${value(report.requestId)}`); + } + if (report.submissionId) { + console.log(`${label('Submission:')} ${value(report.submissionId)}`); + } + if (report.submissionRunId) { + console.log(`${label('Attempt:')} ${value(report.submissionRunId)}`); + } console.log(`${label('Status:')} ${colorLevel(report.status === "completed" ? "info" : report.status === "failed" ? "error" : "warn")} ${report.status}`); console.log(`${label('Created:')} ${dimTimestamp(new Date(report.createdAt).toLocaleString())}`); if (report.updatedAt) { @@ -181,8 +208,10 @@ report withProjectOption(withOutputOption( report .command("list") - .description("List all reports (optionally filter by run)") + .description("List all reports") .option("-r, --run ", "Filter by run ID") + .option("-s, --submission ", "Filter by submission ID") + .option("--submission-run ", "Filter by submission run ID") .option("-u, --url ", "API base URL", getDefaultApiUrl()) )) .action(async (options) => { @@ -191,6 +220,10 @@ report try { const params = new URLSearchParams(); if (options.run) params.set("requestId", options.run); + if (options.submission) params.set("submissionId", options.submission); + if (options.submissionRun) { + params.set("submissionRunId", options.submissionRun); + } const qs = params.toString(); const response = await apiFetch(options.url, `/reports${qs ? `?${qs}` : ""}`, { projectId }); @@ -202,7 +235,9 @@ report const reports = await response.json() as Array<{ id: string; - requestId: string; + requestId?: string; + submissionId?: string; + submissionRunId?: string; status: string; reporter?: { model: string }; createdAt: string; @@ -222,7 +257,13 @@ report tableFormatter: (r: any) => value(r.id), }, { key: 'requestId', label: 'Run ID', - tableFormatter: (r: any) => dimTimestamp(r.requestId), + tableFormatter: (r: any) => r.requestId ? dimTimestamp(r.requestId) : 'N/A', + }, + { key: 'submissionId', label: 'Submission', + tableFormatter: (r: any) => r.submissionId ? dimTimestamp(r.submissionId) : 'N/A', + }, + { key: 'submissionRunId', label: 'Submission Run', + tableFormatter: (r: any) => r.submissionRunId ? dimTimestamp(r.submissionRunId) : 'N/A', }, { key: 'status', label: 'Status', tableFormatter: (r: any) => { diff --git a/apps/cli/src/commands/submission.test.ts b/apps/cli/src/commands/submission.test.ts new file mode 100644 index 000000000..c7a43069e --- /dev/null +++ b/apps/cli/src/commands/submission.test.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { Command } from "commander"; +import { registerSubmissionCommands } from "./submission.js"; + +interface MockResponse { + ok: boolean; + status: number; + json: () => Promise; +} + +function makeProgram(): Command { + const program = new Command(); + registerSubmissionCommands(program); + return program; +} + +function response(body: unknown): MockResponse { + return { + ok: true, + status: 200, + json: async () => body, + }; +} + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + process.exitCode = undefined; +}); + +describe("submission report", () => { + it("lists current and historical reports through the plural resource", async () => { + const urls: string[] = []; + vi.stubGlobal("fetch", vi.fn(async (request: Request) => { + urls.push(request.url); + return response([ + { + id: "report-current", + submissionRunId: "submission-run-2", + status: "completed", + createdAt: "2026-01-02T00:00:00.000Z", + }, + { + id: "report-historical", + submissionRunId: "submission-run-1", + status: "failed", + createdAt: "2026-01-01T00:00:00.000Z", + }, + ]); + })); + const output: string[] = []; + vi.spyOn(console, "log").mockImplementation((...parts: unknown[]) => { + output.push(parts.map(String).join(" ")); + }); + + await makeProgram().parseAsync( + [ + "submission", + "reports", + "submission-1", + "-u", + "http://localhost:3100", + "-o", + "json", + ], + { from: "user" }, + ); + + expect(urls).toEqual([ + "http://localhost:3100/api/v1/submissions/submission-1/reports", + ]); + expect(output.join("\n")).toContain('"id": "report-current"'); + expect(output.join("\n")).toContain('"id": "report-historical"'); + }); +}); diff --git a/apps/cli/src/commands/submission.ts b/apps/cli/src/commands/submission.ts index 47e0db54c..00cdbeb8e 100644 --- a/apps/cli/src/commands/submission.ts +++ b/apps/cli/src/commands/submission.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. import { Command } from "commander"; -import { writeFile } from "node:fs/promises"; import { configureHelp } from "../utils/helpFormatter.js"; import { formatData, isMachineReadable } from "../utils/formatters.js"; import type { DisplayField, OutputFormat } from "../utils/types.js"; @@ -111,7 +110,7 @@ export function registerSubmissionCommands(program: Command): void { console.log(`${label("Readiness:")} ${value(doc.readiness)}`); console.log(`${label("Submission run:")} ${value(doc.submissionRunId)}`); if (doc.report?.reportId) { - console.log(`${label("Readout report:")} ${value(doc.report.reportId)}`); + console.log(`${label("Report:")} ${value(doc.report.reportId)}`); } for (const request of doc.requests) { console.log( @@ -173,51 +172,50 @@ export function registerSubmissionCommands(program: Command): void { } }); - submission - .command("readout") - .description("Get the current generated submission readout") - .argument("", "Submission ID") - .option("--structured", "Download structured JSON instead of Markdown") - .option("-o, --file ", "Write output to a file") - .option("-u, --url ", "API base URL", getDefaultApiUrl()) - .action(async (id: string, options) => { - try { - const response = await apiFetch( - options.url, - `/submissions/${encodeURIComponent(id)}/readout`, - ); - if (!response.ok) await throwApiError(response); - const report = await responseJson<{ - _id: string; - content?: string; - structuredArtifact?: { schemaId: string; schemaVersion: string }; - }>(response); - let output: string; - if (options.structured) { - const structuredResponse = await apiFetch( - options.url, - `/reports/${encodeURIComponent(report._id)}/structured`, - ); - if (!structuredResponse.ok) await throwApiError(structuredResponse); - output = JSON.stringify( - await responseJson(structuredResponse), - null, - 2, - ); - } else { - output = report.content ?? ""; - } - if (options.file) { - await writeFile(options.file, `${output}\n`, "utf8"); - console.log(successText(`Readout written to ${options.file}`)); - } else { - console.log(output); - } - } catch (error) { - console.error(errorText("Error:"), error instanceof Error ? error.message : error); - process.exitCode = 1; - } - }); + withOutputOption( + submission + .command("reports") + .description("List reports for a submission") + .argument("", "Submission ID") + .option("-u, --url ", "API base URL", getDefaultApiUrl()), + ).action(async (id: string, options) => { + try { + const response = await apiFetch( + options.url, + `/submissions/${encodeURIComponent(id)}/reports`, + ); + if (!response.ok) await throwApiError(response); + const reports = await responseJson>(response); + const reportFields: DisplayField[] = [ + { key: "id", label: "Report" }, + { key: "submissionRunId", label: "Submission Run" }, + { key: "status", label: "Status" }, + { + key: "model", + label: "Model", + formatter: (row: Record) => + (row.reporter as { model?: string } | undefined)?.model ?? "", + }, + { key: "createdAt", label: "Created" }, + ]; + console.log( + formatData( + reports, + reportFields, + (options.output ?? "table") as OutputFormat, + ), + ); + } catch (error) { + console.error(errorText("Error:"), error instanceof Error ? error.message : error); + process.exitCode = 1; + } + }); submission .command("retry") diff --git a/apps/portal/src/lib/api.ts b/apps/portal/src/lib/api.ts index 31559b018..6e57eca81 100644 --- a/apps/portal/src/lib/api.ts +++ b/apps/portal/src/lib/api.ts @@ -260,9 +260,6 @@ export const api = { }>; }> => request(`/submissions/${id}/runs`), - getSubmissionReadout: (id: string): Promise => - request(`/submissions/${id}/readout`), - retrySubmission: ( id: string, submissionRunId: string, diff --git a/apps/portal/src/pages/SubmissionDetail.test.tsx b/apps/portal/src/pages/SubmissionDetail.test.tsx index 674bc6954..14f39774b 100644 --- a/apps/portal/src/pages/SubmissionDetail.test.tsx +++ b/apps/portal/src/pages/SubmissionDetail.test.tsx @@ -70,7 +70,7 @@ vi.mock("@/lib/api", () => ({ api: { getSubmission: vi.fn(async () => submission), getSubmissionRuns: vi.fn(async () => history), - getSubmissionReadout: vi.fn(async () => report), + getReport: vi.fn(async () => report), retrySubmission: vi.fn(), reportStructuredUrl: vi.fn(() => "/api/v1/reports/report-1/structured"), }, @@ -103,6 +103,6 @@ describe("SubmissionDetail report terminology", () => { expect(screen.getByText("Report ready")).toBeDefined(); expect(screen.getByRole("button", { name: "Retry report generation" })).toBeDefined(); expect(await screen.findByRole("link", { name: "View report" })).toBeDefined(); - expect(screen.queryByText(/readout/i)).toBeNull(); + expect(await screen.findByRole("heading", { name: "Generated report" })).toBeDefined(); }); }); diff --git a/apps/portal/src/pages/SubmissionDetail.tsx b/apps/portal/src/pages/SubmissionDetail.tsx index c7f7dccb8..1753adebe 100644 --- a/apps/portal/src/pages/SubmissionDetail.tsx +++ b/apps/portal/src/pages/SubmissionDetail.tsx @@ -47,10 +47,11 @@ export function SubmissionDetail() { queryFn: () => api.getSubmissionRuns(id!), enabled: Boolean(id), }); + const currentReportId = submissionQuery.data?.report?.reportId; const reportQuery = useQuery({ - queryKey: ["submission-readout", id, submissionQuery.data?.submissionRunId], - queryFn: () => api.getSubmissionReadout(id!), - enabled: Boolean(id && submissionQuery.data?.report?.reportId), + queryKey: ["report", currentReportId], + queryFn: () => api.getReport(currentReportId!), + enabled: Boolean(currentReportId), }); if (submissionQuery.isLoading) { diff --git a/docs/architecture/app-design.md b/docs/architecture/app-design.md index ec449d69e..29c521879 100644 --- a/docs/architecture/app-design.md +++ b/docs/architecture/app-design.md @@ -536,12 +536,14 @@ artifact reference identifies `scope.submission-readout@1.0`, `application/json`, and the blob URL. The Markdown `content` is rendered from that validated object. Each successful regeneration uses a new report id, while the current embedded submission run points only to its latest successful -report. +report. The internal `pp-readout` handler and `scope.submission-readout` schema +names do not define the public submission API resource name. The API exposes: - `GET /api/v1/submissions?projectId=...` - `GET /api/v1/submissions/:id` +- `GET /api/v1/submissions/:id/reports` - `GET /api/v1/submissions/:id/runs` - `GET /api/v1/submissions/:id/runs/:submissionRunId` - `POST /api/v1/submissions/:id/retry` diff --git a/docs/architecture/post-processing.md b/docs/architecture/post-processing.md index e7bdc0908..ea5f0632c 100644 --- a/docs/architecture/post-processing.md +++ b/docs/architecture/post-processing.md @@ -248,6 +248,15 @@ report kind. The JSON artifact is uploaded to current attempt. Reports from previous generations and superseded attempts remain append-only history. +The public submission resource is generic: +`GET /api/v1/submissions/:id/reports` returns current and historical reports in +newest-first order, matching the run-level reports collection convention. The +embedded current attempt's `reportId` is resolved through +`GET /api/v1/reports/:reportId`; Structured JSON is downloaded through the +report artifact proxy. Internal handler, queue, and schema identifiers retain +`readout` because they identify this specific synthesis implementation rather +than the public resource. + To **add or change a handler**, edit its `handler.yaml` (bump `version`, adjust `dependsOn`, etc.) — no code or migration changes are needed for registration. ## Report Triggering on DAG Drain diff --git a/website/src/openapi/scope-openapi.json b/website/src/openapi/scope-openapi.json index 9f963ac5e..29db3dd5b 100644 --- a/website/src/openapi/scope-openapi.json +++ b/website/src/openapi/scope-openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"Scope API","version":"1.0.0","description":"REST API for the Scope platform — benchmarking AI coding agents"},"servers":[{"url":"/","description":"Current server"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Use the unchanged IdP access token, without the Bearer prefix."}},"schemas":{"KeyResponse":{"type":"object","properties":{},"additionalProperties":{}},"KeyInput":{"type":"object","properties":{},"additionalProperties":{}},"ValidateKeyInput":{"type":"object","properties":{"token":{"type":"string"}},"required":["token"]},"AccountResponse":{"type":"object","properties":{},"additionalProperties":{}},"AccountInput":{"type":"object","properties":{},"additionalProperties":{}},"UserMeResponse":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string"},"email":{"type":"string"},"displayName":{"type":"string"},"idp":{"type":"string"},"idpTenant":{"type":"string"}},"required":["id"]},"ProjectResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","createdAt"]},"CreateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"creator":{"type":"string"}},"required":["name"]},"UpdateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"SubmissionListResponse":{"type":"object","properties":{"submissions":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionResponse"}}},"required":["submissions"]},"SubmissionResponse":{"type":"object","properties":{"submissionId":{"type":"string"},"projectId":{"type":"string"},"requestIds":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"submissionRunId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["waiting","ready","blocked","processing","done","failed"]},"requests":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRequestSummary"}},"dependencyProjection":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionDependencyProjection"}},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"}},"handlerProgress":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/SubmissionHandlerStatus"}},"report":{"$ref":"#/components/schemas/SubmissionReportPointer"},"supersededAt":{"type":["string","null"],"format":"date-time"},"supersededBySubmissionRunId":{"type":"string"},"supersededReason":{"type":"string","enum":["request-vector-changed","manual-retry"]}},"required":["submissionId","projectId","requestIds","createdAt","submissionRunId","attemptNumber","readiness","requests","dependencyProjection","blocking"]},"SubmissionRequestSummary":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"task":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"},"workerType":{"type":"string"},"model":{"type":"string"},"profileId":{"type":"string"}},"required":["requestId","runId","task","status","workerType"]},"SubmissionDependencyProjection":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"state":{"type":"string","enum":["waiting","ready","failed","blocked"]},"version":{"type":"number"},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["requestId","runId","handlerId","state"]},"SubmissionBlockingDetail":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"reason":{"type":"string","enum":["request-missing","run-changed","dependency-not-ready","dependency-failed","dependency-blocked"]},"error":{"type":"string"}},"required":["requestId","runId","handlerId","reason"]},"SubmissionHandlerStatus":{"type":"object","properties":{"status":{"type":"string","enum":["queued","processing","done","failed"]},"version":{"type":"number"},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["status"]},"SubmissionReportPointer":{"type":"object","properties":{"reportId":{"type":"string"},"url":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"}}},"ReportResponse":{"type":"object","properties":{"_id":{"type":"string"},"requestId":{"type":"string"},"submissionId":{"type":"string"},"submissionRunId":{"type":"string"},"templateId":{"type":"string"},"reporter":{"$ref":"#/components/schemas/Reporter"},"content":{"type":"string"},"structuredArtifact":{"$ref":"#/components/schemas/StructuredReportArtifact"},"status":{"type":"string","enum":["pending","generating","completed","failed"]},"error":{"type":"string"},"insightReferences":{"type":"array","items":{"$ref":"#/components/schemas/InsightReference"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","status","createdAt","projectId"]},"Reporter":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"gitHash":{"type":"string"},"model":{"type":"string"},"agentId":{"type":"string"},"agentVersion":{"type":"string"}},"required":["id","name","gitHash","model","agentId","agentVersion"]},"StructuredReportArtifact":{"type":"object","properties":{"schemaId":{"type":"string"},"schemaVersion":{"type":"string"},"contentType":{"type":"string","enum":["application/json"]},"url":{"type":"string"}},"required":["schemaId","schemaVersion","contentType","url"]},"InsightReference":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]},"SubmissionRunResponse":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"vectorFingerprint":{"type":"string"},"requests":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRequestRef"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"dependencyProjection":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionDependencyProjection"},"default":[]},"handlerStatus":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/SubmissionHandlerStatus"}},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"},"default":[]},"report":{"$ref":"#/components/schemas/SubmissionReportPointer"},"supersededAt":{"type":["string","null"],"format":"date-time"},"supersededBySubmissionRunId":{"type":"string"},"supersededReason":{"type":"string","enum":["request-vector-changed","manual-retry"]},"submissionId":{"type":"string"},"projectId":{"type":"string"}},"required":["_id","attemptNumber","vectorFingerprint","requests","createdAt"]},"SubmissionRequestRef":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"}},"required":["requestId","runId"]},"SubmissionRetryResponse":{"type":"object","properties":{"submissionId":{"type":"string"},"submissionRunId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["waiting","ready","blocked","processing","done","failed"]},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"}}},"required":["submissionId","submissionRunId","attemptNumber","readiness","blocking"]},"RequestResponse":{"type":"object","properties":{"_id":{"type":"string"},"scenario":{"$ref":"#/components/schemas/Scenario"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"maxIterations":{"type":"number"},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"deletedAt":{"type":["string","null"],"format":"date-time"},"taskPromptId":{"type":"string"},"agentsMdPromptId":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBinding"}},"extensions":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"profileId":{"type":"string"},"profileVersionId":{"type":"string"},"submissionId":{"type":"string"},"submissionExpectedRequestCount":{"type":"integer","minimum":1},"submissionRequestIndex":{"type":"integer","minimum":0},"submissionInitialRunId":{"type":"string"},"submissionFinalizedAt":{"type":["string","null"],"format":"date-time"},"priority":{"type":"integer","default":0},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"observations":{"type":"array","items":{"type":"string"}},"gateSummaries":{"type":"array","items":{"$ref":"#/components/schemas/GateRunSummary"}},"run":{"$ref":"#/components/schemas/RunState"},"projectId":{"type":"string"}},"required":["_id","scenario","workerType","createdAt","projectId"]},"Scenario":{"type":"object","properties":{"version":{"type":"string","enum":["v1","v2"]},"task":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}}},"required":["task","criteria"]},"Persona":{"type":"object","properties":{"personality":{"type":"string","enum":["demanding","friendly"]},"experience":{"type":"string","enum":["junior","senior"]},"verbosity":{"type":"string","enum":["brief","moderate"]},"type":{"type":"string","enum":["traditional","ai_assisted","vibe"]}},"required":["personality","experience","verbosity","type"]},"ResourceBinding":{"type":"object","properties":{"ref":{"type":"string"},"revisionId":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref","revisionId","params"]},"GateConfig":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"promptId":{"type":"string"},"promptText":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}},"maxIterations":{"type":"integer","minimum":1,"maximum":50}},"required":["gate","criteria"]},"GateRunSummary":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"status":{"type":"string","enum":["passed","failed","skipped"]},"iterations":{"type":"integer","minimum":0}},"required":["gate","status","iterations"]},"RunState":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"queuedQueueName":{"type":"string"},"outcome":{"type":"string","enum":["succeeded","failed","finished"]},"result":{"type":"string"},"error":{"type":"string"},"logsUrl":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"turns":{"type":"array","items":{"$ref":"#/components/schemas/ConversationTurn"}},"workerVersion":{"type":"string"},"os":{"type":"object","properties":{"platform":{"type":"string"},"release":{"type":"string"},"arch":{"type":"string"}},"required":["platform","release","arch"]},"lastHeartbeatAt":{"type":["string","null"],"format":"date-time"},"worker":{"type":"object","properties":{"instanceId":{"type":"string"},"podName":{"type":"string"}},"required":["instanceId"]},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"setupVideoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"aiCallCount":{"type":"number"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRunOutcome"}},"mcpRegistered":{"type":"boolean"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"pausedAt":{"type":["string","null"],"format":"date-time"},"resumedAt":{"type":["string","null"],"format":"date-time"},"observationResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}}},"required":["_id","attemptNumber","status"]},"ConversationTurn":{"type":"object","properties":{"iteration":{"type":"number"},"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"codingAgentResponse":{"type":"string"},"judgeFeedback":{"type":"string"},"snapshotUrl":{"type":"string"},"passed":{"type":"boolean"},"timestamp":{"type":["string","null"],"format":"date-time"},"criteriaResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"observationResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"startedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"toolCalls":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"arguments":{"type":"object","additionalProperties":{}},"response":{"type":"string"},"timestamp":{"type":"string"}},"required":["id","name","arguments"]}},"toolCallsUrl":{"type":"string"},"toolCallCount":{"type":"number"},"aiCallCount":{"type":"number"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"chatResultUrl":{"type":"string"},"chatResultFormat":{"type":"string"}},"required":["iteration","judgeFeedback","snapshotUrl","passed","timestamp"]},"CriterionResult":{"type":"object","properties":{"criterionId":{"type":"string"},"passed":{"type":"boolean"},"feedback":{"type":"string"},"evaluated":{"type":"boolean"}},"required":["criterionId","passed","feedback","evaluated"]},"TokenUsage":{"type":"object","properties":{"promptTokens":{"type":"number"},"completionTokens":{"type":"number"},"totalTokens":{"type":"number"}},"required":["promptTokens","completionTokens","totalTokens"]},"ResourceRunOutcome":{"type":"object","properties":{"ref":{"type":"string"},"slug":{"type":"string"},"revisionId":{"type":"string"},"setupSucceeded":{"type":"boolean"},"published":{"type":"array","items":{"type":"string"}},"params":{"type":"object","additionalProperties":{"type":"string"}},"setupDurationMs":{"type":"number"},"error":{"type":"string"},"teardownRan":{"type":"boolean"}},"required":["ref","slug","revisionId","setupSucceeded","published"]},"ResourceBindingSpec":{"anyOf":[{"type":"string"},{"type":"object","properties":{"ref":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref"]}]},"CreateRequestInput":{"type":"object","properties":{"scenario":{"$ref":"#/components/schemas/Scenario"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"maxIterations":{"type":"integer","minimum":1,"maximum":50},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"profileId":{"type":"string"},"profileVariations":{"type":"array","items":{"type":"string"}},"priority":{"type":"integer"},"agentsMd":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"observations":{"type":"array","items":{"type":"string"}}},"required":["scenario"]},"RunFacetsResponse":{"type":"object","properties":{"total":{"type":"number"},"facets":{"type":"object","properties":{"workerType":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"status":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"outcome":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"model":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"os":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"priority":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"agentVersion":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"profileId":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}}},"required":["workerType","status","outcome","model","os","priority","agentVersion","profileId"]}},"required":["total","facets"]},"RunFacetBucket":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"number"}},"required":["value","count"]},"PaginatedRunsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"Cursors":{"type":"object","properties":{"next":{"type":["string","null"]},"prev":{"type":["string","null"]}},"required":["next","prev"]},"PaginatedRunGroupsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RunGroup"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"RunGroup":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"runIds":{"type":"array","items":{"type":"string"}},"aggregates":{"$ref":"#/components/schemas/GroupAggregates"},"uniform":{"$ref":"#/components/schemas/GroupUniformValues"}},"required":["key","label","runIds","aggregates","uniform"]},"GroupAggregates":{"type":"object","properties":{"count":{"type":"number"},"turns":{"$ref":"#/components/schemas/AggregateStats"},"duration":{"$ref":"#/components/schemas/AggregateStats"},"promptTokens":{"$ref":"#/components/schemas/AggregateStats"},"completionTokens":{"$ref":"#/components/schemas/AggregateStats"},"statusCounts":{"type":"object","additionalProperties":{"type":"number"}},"outcomeCounts":{"type":"object","additionalProperties":{"type":"number"}}},"required":["count","turns","duration","promptTokens","completionTokens","statusCounts","outcomeCounts"]},"AggregateStats":{"type":["object","null"],"properties":{"min":{"type":"number"},"max":{"type":"number"},"mean":{"type":"number"},"stdDev":{"type":"number"}},"required":["min","max","mean","stdDev"]},"GroupUniformValues":{"type":"object","properties":{"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":"string"},"platform":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"extensions":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"submissionId":{"type":"string"},"task":{"type":"string"}}},"BulkResubmitInput":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"count":{"type":"integer","minimum":1,"maximum":10,"default":1},"overrides":{"type":"object","properties":{"profileId":{"type":["string","null"]},"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":["string","null"]},"reasoningEffort":{"type":["string","null"]},"maxIterations":{"type":["number","null"]},"mcpServers":{"type":["array","null"],"items":{"type":"string"}},"skillRevisions":{"type":["array","null"],"items":{"type":"string"}},"extensions":{"type":["array","null"],"items":{"type":"string"}}}}},"required":["ids"]},"CreateCriteriaInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"],"default":"gate"},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}},"required":["id","prompt"]},"CriteriaResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"CriteriaGraph":{"type":"object","properties":{"nodes":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphNode"}},"edges":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphEdge"}}},"required":["nodes","edges"]},"CriteriaGraphNode":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}},"required":["id","prompt"]},"CriteriaGraphEdge":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"]},"UpdateCriteriaInput":{"type":"object","properties":{"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}}},"CreatePromptFeatureInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["id","prompt"]},"PromptType":{"type":"string","enum":["select","build","test","run","deploy","agents.md"]},"PromptFeatureResult":{"type":"object","properties":{"featureId":{"type":"string"},"detected":{"type":"boolean"},"evaluated":{"type":"boolean"}},"required":["featureId","detected","evaluated"]},"SuggestedPromptFeature":{"type":"object","properties":{"suggestedId":{"type":"string"},"behavior":{"type":"string"},"prompt":{"type":"string"}},"required":["suggestedId","behavior","prompt"]},"PromptFeatureResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"UpdatePromptFeatureInput":{"type":"object","properties":{"prompt":{"type":"string"}}},"TaskPromptResponse":{"type":"object","properties":{"_id":{"type":"string"},"keyId":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"text":{"type":"string"},"contentBlobUrl":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","keyId","createdAt","projectId"]},"CreateTaskPromptInput":{"type":"object","properties":{"text":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["text"]},"PatchTaskPromptFeatureInput":{"type":"object","properties":{"detected":{"type":"boolean"}},"required":["detected"]},"CreateReportInput":{"type":"object","properties":{"requestId":{"type":"string"},"templateId":{"type":"string"}},"required":["requestId"]},"BulkCreateReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}},"templateId":{"type":"string"}},"required":["requestIds"]},"BulkReportStatusInput":{"type":"object","properties":{"reportIds":{"type":"array","items":{"type":"string"}}},"required":["reportIds"]},"BulkReportSummaryResponse":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/ReportSummary"}},"ReportSummary":{"type":"object","properties":{"total":{"type":"number"},"pending":{"type":"number"},"generating":{"type":"number"},"completed":{"type":"number"},"failed":{"type":"number"}},"required":["total","pending","generating","completed","failed"]},"BulkReportSummaryInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"TriggerReportsInput":{"type":"object","properties":{"requestId":{"type":"string"}},"required":["requestId"]},"BulkTriggerReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"InsightResponse":{"type":"object","properties":{"_id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"upvotes":{"type":"number"},"downvotes":{"type":"number"},"blocked":{"type":"boolean"},"referenceCount":{"type":"number"},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","title","description","upvotes","downvotes","blocked","referenceCount","createdBy","createdAt","projectId"]},"ProfileWithVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"version":{"$ref":"#/components/schemas/ProfileVersionResponse"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","version","projectId"]},"ProfileVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"profileId":{"type":"string"},"version":{"type":"number"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","profileId","version","workerType","model","createdAt","projectId"]},"CreateProfileInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["name","workerType","model"]},"ProfileResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","projectId"]},"UpdateProfileIdentity":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512}}},"ReportTemplateResponse":{"type":"object","properties":{"_id":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","id","name","userPrompt","createdAt","projectId"]},"ReportTemplateSystemPrompt":{"type":"object","properties":{"mode":{"type":"string","enum":["append","override"]},"content":{"type":"string"}},"required":["mode","content"]},"ReportTrigger":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["always"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["criteria"]},"criteriaIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","criteriaIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["taskPrompt"]},"taskPromptIds":{"type":"array","items":{"type":"string"}}},"required":["type","taskPromptIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["promptFeature"]},"featureIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","featureIds"]}]},"CreateReportTemplateInput":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0}},"required":["id","name","userPrompt"]},"UpdateReportTemplateInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"allOf":[{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},{"type":["object","null"]}]},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":["string","null"]},"timeoutMs":{"type":["integer","null"],"exclusiveMinimum":0}}},"AgentResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"},"versions":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","supportedModels","createdAt"]},"AgentCapabilities":{"type":"object","properties":{"supportsReasoningEffort":{"type":"boolean"},"supportsMcpServers":{"type":"boolean"},"supportsSkills":{"type":"boolean"},"supportsExtensions":{"type":"boolean"},"supportsResources":{"type":"boolean"}}},"AgentVersion":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string"},"status":{"type":"string","enum":["active","retired"]},"createdAt":{"type":["string","null"],"format":"date-time"}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","status","createdAt"]},"CreateAgentInput":{"type":"object","properties":{"_id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}},"required":["_id","name"]},"UpdateAgentInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}}},"RegisterAgentVersionInput":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string","minLength":1}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","queueName"]},"PatchAgentVersionInput":{"type":"object","properties":{"status":{"type":"string","enum":["active","retired"]}},"required":["status"]},"ModelResponse":{"type":"object","properties":{"_id":{"type":"string"},"modelId":{"type":"string"},"provider":{"type":"string"},"agentId":{"type":"string"},"firstSeenAt":{"type":["string","null"],"format":"date-time"},"lastSeenAt":{"type":["string","null"],"format":"date-time"},"disappearedAt":{"type":["string","null"],"format":"date-time"},"providerAvailableFrom":{"type":["string","null"],"format":"date-time"},"providerEndOfLife":{"type":["string","null"],"format":"date-time"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"$ref":"#/components/schemas/ModelCapabilities"}},"required":["_id","modelId","provider","agentId","firstSeenAt","lastSeenAt"]},"ModelCapabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}},"McpServerResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","type","createdAt","projectId"]},"McpServerHeader":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}},"required":["name","value"]},"UpdateMcpServerInput":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}}},"SkillResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","source","skillName","name","origin","createdAt","projectId"]},"SkillSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"source":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"installs":{"type":"number"}},"required":["id","name","source","internal"]},"SkillDiscoveryResult":{"type":"object","properties":{"skillName":{"type":"string"},"skillPath":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"existsInLibrary":{"type":"boolean"},"currentRevisionCommitSha":{"type":"string"},"latestUpstreamCommitSha":{"type":"string"},"updateAvailable":{"type":"boolean"},"lastImportedAt":{"type":"string"}},"required":["skillName","skillPath"]},"SkillRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"ref":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"skillPath":{"type":"string"},"commitHash":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"name":{"type":"string"},"description":{"type":"string"},"license":{"type":"string"},"compatibility":{"type":"string"},"allowedTools":{"type":"string"},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"content":{"type":"string"},"archiveUrl":{"type":"string"},"validationWarnings":{"type":"array","items":{"type":"string"}},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","ref","source","skillName","skillPath","commitHash","commitTimestamp","name","description","content","archiveUrl","resolvedAt","createdAt","projectId"]},"CreateSkillInput":{"type":"object","properties":{"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]}},"required":["source","skillName","name","origin"]},"CodebaseResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"defaultBranch":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","sourceType","revisionCounter","createdAt","projectId"]},"UpdateCodebaseInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"defaultBranch":{"type":"string"}}},"CodebaseRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"codebaseId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"requestedRef":{"type":"string"},"resolvedCommitSha":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"originalFilename":{"type":"string"},"contentSha256":{"type":"string"},"archiveUrl":{"type":"string"},"sizeBytes":{"type":"number"},"fileCount":{"type":"number"},"creator":{"type":"string"},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","codebaseId","slug","revisionNumber","ref","sourceType","archiveUrl","resolvedAt","createdAt","projectId"]},"ResolveCodebaseRevisionInput":{"type":"object","properties":{"requestedRef":{"type":"string"},"creator":{"type":"string"}}},"ResourceResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"latestRevisionNumber":{"type":"number"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","revisionCounter","createdAt","projectId"]},"CreateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1},"description":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["name","setup"]},"ResourceScript":{"type":"object","properties":{"sh":{"type":"string","minLength":1}}},"ResourceParameter":{"type":"object","properties":{"name":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"description":{"type":"string"},"required":{"type":"boolean","default":false},"default":{"type":"string"},"example":{"type":"string"}},"required":["name"]},"UpdateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"ResourceRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"resourceId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"contentSha256":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","resourceId","slug","revisionNumber","ref","setup","exports","contentSha256","createdAt","projectId"]},"CreateResourceRevisionInput":{"type":"object","properties":{"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["setup"]},"ExtensionResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","publisher","name","origin","createdAt","projectId"]},"ExtensionSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"publisher":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"version":{"type":"string"}},"required":["id","name","publisher","internal"]},"CreateExtensionInput":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+$"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]}},"required":["_id","publisher","name","origin"]},"UpdateExtensionInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ExtensionVersionInfo":{"type":"object","properties":{"version":{"type":"string"},"preRelease":{"type":"boolean"},"lastUpdated":{"type":"string"}},"required":["version","preRelease","lastUpdated"]},"CreateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"}},"required":["title","description"]},"UpdateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}}}},"FeatureFlagResponse":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"enabled":{"type":"boolean"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["key","label","enabled","updatedAt"]},"UpdateFeatureFlagInput":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]}},"parameters":{}},"paths":{"/api/v1/keys/preview":{"post":{"tags":["Keys"],"summary":"Preview key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}}},"/api/v1/keys":{"post":{"tags":["Keys"],"summary":"Create key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"201":{"description":"Key created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"get":{"tags":["Keys"],"summary":"List keys","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KeyResponse"}}}}}}}},"/api/v1/keys/{id}":{"get":{"tags":["Keys"],"summary":"Get key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"put":{"tags":["Keys"],"summary":"Update key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"delete":{"tags":["Keys"],"summary":"Delete key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Key deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/keys/{id}/validate":{"post":{"tags":["Keys"],"summary":"Validate key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateKeyInput"}}}},"responses":{"200":{"description":"Validation result","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Key validation result"}}}}}}},"/api/v1/accounts":{"post":{"tags":["Accounts"],"summary":"Create account","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"201":{"description":"Account created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"get":{"tags":["Accounts"],"summary":"List accounts","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccountResponse"}}}}}}}},"/api/v1/accounts/{id}":{"get":{"tags":["Accounts"],"summary":"Get account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"put":{"tags":["Accounts"],"summary":"Update account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"200":{"description":"Account updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"delete":{"tags":["Accounts"],"summary":"Delete account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Account deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/users/me":{"get":{"tags":["Users"],"summary":"Get the authenticated user's identity","description":"Read the existing authenticated Scope identity without enrollment or profile writes. Responses must not be HTTP-cached.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is not enrolled or is disabled"},"503":{"description":"Authentication service unavailable"}}},"post":{"tags":["Users"],"summary":"Enroll the authenticated user","description":"JIT-enroll the authenticated user, refresh their profile and lastLoginAt, apply bootstrap-admin rules, and warm the access cache. Use this only after an explicit IdP login; do not prefetch, poll, or automatically retry transient failures.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is disabled"},"503":{"description":"Authentication service unavailable"}}}},"/health":{"get":{"tags":["Health"],"summary":"Liveness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"version":{"type":"string"}},"required":["status","version"]}}}}}}},"/ready":{"get":{"tags":["Health"],"summary":"Readiness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"migrations":{}},"required":["status"]}}}},"503":{"description":"Service is not ready"}}}},"/about":{"get":{"tags":["System"],"summary":"API metadata","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"version":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"description":{"type":"string"},"workers":{"type":"array","items":{"type":"string"}}},"required":["name","version","buildTime","environment","description","workers"]}}}}}}},"/api/v1/version":{"get":{"tags":["System"],"summary":"Version info","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"commit":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"strictAgentCapabilities":{"type":"boolean"}},"required":["commit","buildTime","environment","strictAgentCapabilities"]}}}}}}},"/api/v1/projects":{"get":{"tags":["Projects"],"summary":"List all projects","parameters":[{"schema":{"type":"string","enum":["true","false"],"description":"When true, include soft-deleted projects in the result"},"required":false,"description":"When true, include soft-deleted projects in the result","name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProjectResponse"}}}}}}},"post":{"tags":["Projects"],"summary":"Create a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/projects/{id}":{"get":{"tags":["Projects"],"summary":"Get a project","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"patch":{"tags":["Projects"],"summary":"Update a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProjectInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"delete":{"tags":["Projects"],"summary":"Soft-delete a project","responses":{"204":{"description":"Success"},"404":{"description":"Project not found"}}}},"/api/v1/projects/{id}/restore":{"post":{"tags":["Projects"],"summary":"Restore a soft-deleted project","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}}},"/api/v1/submissions":{"get":{"tags":["Submissions"],"summary":"List submissions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionListResponse"}}}}}}},"/api/v1/submissions/{id}/readout":{"get":{"tags":["Submissions"],"summary":"Get the current submission readout report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"404":{"description":"Current readout not found"}}}},"/api/v1/submissions/{id}":{"get":{"tags":["Submissions"],"summary":"Get a submission","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionResponse"}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}/runs":{"get":{"tags":["Submissions"],"summary":"List current and historical submission attempts","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"runs":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRunResponse"}}},"required":["runs"]}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}/runs/{submissionRunId}":{"get":{"tags":["Submissions"],"summary":"Get one submission attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"submissionRunId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionRunResponse"}}}},"404":{"description":"Submission attempt not found"}}}},"/api/v1/submissions/{id}/retry":{"post":{"tags":["Submissions"],"summary":"Retry current submission processing","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"submissionRunId":{"type":"string"}},"required":["submissionRunId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionRetryResponse"}}}},"404":{"description":"Submission not found"},"409":{"description":"Submission attempt is stale or dependencies are blocked"}}}},"/api/v1/requests":{"post":{"tags":["Requests"],"summary":"Submit request(s)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/CreateRequestInput"},{"type":"object","properties":{"count":{"type":"number","minimum":1,"maximum":10,"default":1},"skills":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"codebase":{"type":"string"}}}]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RequestResponse"},{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}]}}}}}},"get":{"tags":["Requests"],"summary":"List requests","parameters":[{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"profileId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"status","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"outcome","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"model","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"os","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"priority","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"agentVersion","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"type":"string","enum":["task","submissionId","profile"]},"required":false,"name":"groupBy","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"after","in":"query"},{"schema":{"type":"string"},"required":false,"name":"before","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"last","in":"query"},{"schema":{"type":"string","enum":["created","updated","priority","worker","status","id","duration","createdAt"]},"required":false,"name":"sortBy","in":"query"},{"schema":{"type":"string","enum":["asc","desc"]},"required":false,"name":"sortDir","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdAfter","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdBefore","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"turns","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"turnsOp","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"maxIterations","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"maxIterationsOp","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PaginatedRunsResponse"},{"$ref":"#/components/schemas/PaginatedRunGroupsResponse"}]}}}}}}},"/api/v1/requests/facets":{"get":{"tags":["Requests"],"summary":"List run filter facets","description":"Returns every distinct value and its full-dataset count per categorical filter dimension for the Runs list rail. Counts are absolute over all non-deleted runs **in the given project**: they intentionally ignore the active search, date range, iteration, and categorical selections so every selectable value stays visible with a stable count. Requires ?projectId=. Computed with parallel $group aggregations (Cosmos has no $facet) and cached per-project for a short TTL.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunFacetsResponse"}}}}}}},"/api/v1/requests/{id}":{"get":{"tags":["Requests"],"summary":"Get request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestResponse"}}}},"404":{"description":"Not found"}}},"delete":{"tags":["Requests"],"summary":"Soft-delete request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Not found"}}}},"/api/v1/analysis":{"get":{"tags":["Requests"],"summary":"Compute pass@k / success@T metrics","description":"Aggregates pass@k / success@T metrics over a single project's done runs. Requires ?projectId=.","parameters":[{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"k","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Analysis metrics"}}}}}}},"/api/v1/requests/bulk-resubmit":{"post":{"tags":["Requests"],"summary":"Bulk resubmit requests","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkResubmitInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}}}}}}},"/api/v1/requests/bulk":{"delete":{"tags":["Requests"],"summary":"Bulk soft-delete requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}},"required":["ids"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"number"}},"required":["deleted"]}}}}}}},"/api/v1/requests/archive":{"post":{"tags":["Requests"],"summary":"Download batch archive of multiple runs","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Gzipped batch archive containing individual run archives"},"400":{"description":"Invalid input"},"404":{"description":"One or more runs not found"}}}},"/api/v1/runs/upload":{"post":{"tags":["Requests"],"summary":"Import run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"}}}},"/api/v1/runs/upload-batch":{"post":{"tags":["Requests"],"summary":"Import batch run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"207":{"description":"Multi-status: 201 if all runs imported, 400 if none imported, 207 if partial"},"400":{"description":"No archive uploaded, empty archive, or all runs failed"}}}},"/api/v1/requests/{id}/reports":{"get":{"tags":["Reports"],"summary":"Get reports for request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Run not found"}}}},"/api/v1/requests/{id}/runs":{"get":{"tags":["Requests"],"summary":"List attempts for a request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RunState"}}}}},"404":{"description":"Request not found"}}}},"/api/v1/requests/{id}/runs/{runId}":{"get":{"tags":["Requests"],"summary":"Get a single attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunState"}}}},"404":{"description":"Request or run not found"}}}},"/api/v1/requests/bulk-retry":{"post":{"tags":["Requests"],"summary":"Bulk retry requests (start new attempts)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"force":{"type":"boolean"}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"retried":{"type":"integer"},"skipped":{"type":"integer"},"results":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"},"error":{"type":"string"}},"required":["requestId"]}}},"required":["retried","skipped","results"]}}}}}}},"/api/v1/requests/{id}/retry":{"post":{"tags":["Requests"],"summary":"Retry a request (start a new attempt)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"force":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"}},"required":["requestId","runId","attemptNumber"]}}}},"404":{"description":"Request not found"},"409":{"description":"Conflict — request is not in a retryable state, or a concurrent retry won the race"},"422":{"description":"Cannot retry — current run not yet terminal"}}}},"/api/v1/requests/{id}/priority":{"post":{"tags":["Requests"],"summary":"Set priority on a single request","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"priority":{"type":"integer"}},"required":["priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"priority":{"type":"number"}},"required":["id","priority"]}}}}}}},"/api/v1/requests/bulk-priority":{"post":{"tags":["Requests"],"summary":"Set priority on multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100},"priority":{"type":"integer"}},"required":["ids","priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/pause":{"post":{"tags":["Requests"],"summary":"Pause a single request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/{id}/resume":{"post":{"tags":["Requests"],"summary":"Resume a paused request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/bulk-pause":{"post":{"tags":["Requests"],"summary":"Pause multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/bulk-resume":{"post":{"tags":["Requests"],"summary":"Resume multiple paused requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/cancel":{"post":{"tags":["Requests"],"summary":"Cancel a request (marks as done/failed and signals worker to exit)","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"previousStatus":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"}},"required":["id","previousStatus","status","outcome"]}}}}}}},"/api/v1/requests/bulk-cancel":{"post":{"tags":["Requests"],"summary":"Cancel multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"cancelled":{"type":"number"},"skipped":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"cancelled":{"type":"boolean"},"previousStatus":{"type":"string"},"error":{"type":"string"}},"required":["id","cancelled"]}}},"required":["cancelled","skipped","results"]}}}}}}},"/api/v1/requests/{id}/logs":{"get":{"tags":["Requests"],"summary":"Stream request logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/logs":{"get":{"tags":["Requests"],"summary":"Stream logs for a specific attempt (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific attempt and iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/video":{"get":{"tags":["Requests"],"summary":"Download session recording","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/video":{"get":{"tags":["Requests"],"summary":"Download session recording for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/criteria/generate-prompt":{"post":{"tags":["Criteria"],"summary":"Generate criterion prompt from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"},"gates":{"type":"array","items":{"type":"string"}}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"},"suggestedId":{"type":"string"},"suggestedParents":{"type":"array","items":{"type":"string"}},"suggestedChildren":{"type":"array","items":{"type":"string"}}},"required":["prompt","suggestedId","suggestedParents","suggestedChildren"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/criteria/seed":{"post":{"tags":["Criteria"],"summary":"Seed criteria in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"criteria":{"type":"array","items":{"$ref":"#/components/schemas/CreateCriteriaInput"}}},"required":["criteria"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}},"400":{"description":"Seed batch would introduce a dependency cycle"}}}},"/api/v1/criteria":{"get":{"tags":["Criteria"],"summary":"List criteria","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","description":"Comma-separated criterion IDs to include"},"required":false,"description":"Comma-separated criterion IDs to include","name":"ids","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"When true and ids is set, also include dependency ancestors"},"required":false,"description":"When true and ids is set, also include dependency ancestors","name":"ancestors","in":"query"},{"schema":{"type":"string","enum":["gate","observation"],"description":"Filter by criterion kind"},"required":false,"description":"Filter by criterion kind","name":"kind","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaResponse"}}}}}}},"post":{"tags":["Criteria"],"summary":"Create criterion","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCriteriaInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"409":{"description":"Criterion already exists"}}}},"/api/v1/criteria/mdp":{"get":{"tags":["Criteria"],"summary":"Compute MDP transitions","parameters":[{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"since","in":"query"},{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{}}}}}}}},"/api/v1/criteria/graph":{"get":{"tags":["Criteria"],"summary":"Get criteria DAG","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaGraph"}}}}}}},"/api/v1/criteria/{id}":{"get":{"tags":["Criteria"],"summary":"Get criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"404":{"description":"Criterion not found"}}},"put":{"tags":["Criteria"],"summary":"Update criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCriteriaInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"400":{"description":"Invalid dependency reference or self-reference"},"404":{"description":"Criterion not found"}}},"delete":{"tags":["Criteria"],"summary":"Soft-delete criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Criterion not found"},"409":{"description":"Criterion has dependents"}}}},"/api/v1/prompt-features/generate-prompt":{"post":{"tags":["Prompt Features"],"summary":"Generate prompt feature from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"}},"required":["prompt"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features/seed":{"post":{"tags":["Prompt Features"],"summary":"Seed prompt features in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}},"required":["features"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}}}}},"/api/v1/prompt-features/extract-from-text":{"post":{"tags":["Prompt Features"],"summary":"Extract features from text","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string"},"model":{"type":"string"},"type":{"type":"string","enum":["select","agents.md"]}},"required":["text"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["features","cached"]}}}},"400":{"description":"Empty text string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features":{"get":{"tags":["Prompt Features"],"summary":"List features","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","enum":["select","agents.md"]},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}}}}},"post":{"tags":["Prompt Features"],"summary":"Create feature","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"409":{"description":"Feature already exists"}}}},"/api/v1/prompt-features/{id}":{"get":{"tags":["Prompt Features"],"summary":"Get feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"404":{"description":"Feature not found"}}},"put":{"tags":["Prompt Features"],"summary":"Update feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Feature not found"}}},"delete":{"tags":["Prompt Features"],"summary":"Soft-delete feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Feature not found"}}}},"/api/v1/task-prompts/generate":{"post":{"tags":["Task Prompts"],"summary":"Generate task prompts","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string"},"existingPrompt":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"tasks":{"type":"array","items":{"type":"string"}}},"required":["tasks"]}}}},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts":{"get":{"tags":["Task Prompts"],"summary":"List task prompts","parameters":[{"schema":{"type":["number","null"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":["number","null"]},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"$ref":"#/components/schemas/PromptType"},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/TaskPromptResponse"}},"total":{"type":"number"},"limit":{"type":"number"},"offset":{"type":"number"}},"required":["items","total","limit","offset"]}}}}}},"post":{"tags":["Task Prompts"],"summary":"Create or find task prompt","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTaskPromptInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Empty text string"}}}},"/api/v1/task-prompts/{id}":{"get":{"tags":["Task Prompts"],"summary":"Get task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"404":{"description":"Task prompt not found"}}},"delete":{"tags":["Task Prompts"],"summary":"Soft-delete task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/content":{"get":{"tags":["Task Prompts"],"summary":"Get resolved task prompt content","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"}},"required":["id","text"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/extract-features":{"post":{"tags":["Task Prompts"],"summary":"Extract features from task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"force","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"taskPromptId":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["taskPromptId","features","cached"]}}}},"404":{"description":"Task prompt not found"},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts/{id}/features/{featureId}":{"patch":{"tags":["Task Prompts"],"summary":"Toggle feature flag on task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"featureId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchTaskPromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Invalid detected value"},"404":{"description":"Task prompt or feature not found"}}}},"/api/v1/reports":{"post":{"tags":["Reports"],"summary":"Create report","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Run or template not found"}}},"get":{"tags":["Reports"],"summary":"List reports","parameters":[{"schema":{"type":"string"},"required":false,"name":"requestId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionRunId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}}}}},"/api/v1/reports/bulk-create":{"post":{"tags":["Reports"],"summary":"Bulk create reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}/structured":{"get":{"tags":["Reports"],"summary":"Download a report's structured JSON artifact","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success"},"404":{"description":"Report or structured artifact not found"}}}},"/api/v1/reports/bulk-status":{"post":{"tags":["Reports"],"summary":"Bulk get report statuses","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportStatusInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/bulk-summary":{"post":{"tags":["Reports"],"summary":"Bulk get report summary per run","description":"Returns aggregated report status counts per run. Only the latest report per template is counted (re-triggers are deduplicated).","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}":{"get":{"tags":["Reports"],"summary":"Get report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"404":{"description":"Report not found"}}}},"/api/v1/reports/{id}/logs":{"get":{"tags":["Reports"],"summary":"Stream report logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Report not found"}}}},"/api/v1/reports/trigger":{"post":{"tags":["Reports"],"summary":"Trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"triggered":{"type":"number"},"reports":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}},"required":["triggered","reports"]}}}},"400":{"description":"Invalid input"},"404":{"description":"Run not found"}}}},"/api/v1/reports/bulk-trigger":{"post":{"tags":["Reports"],"summary":"Bulk trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkTriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":{}}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}/insights":{"get":{"tags":["Reports"],"summary":"Get insights for report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/InsightResponse"},{"type":"object","properties":{"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}}}]}}}}},"404":{"description":"Report not found"}}},"post":{"tags":["Reports"],"summary":"Link insight to report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"isNew":{"type":"boolean"}},"required":["insightId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]}}}},"404":{"description":"Report or insight not found"},"409":{"description":"Insight already referenced by this report"}}}},"/api/v1/profiles":{"post":{"tags":["Profiles"],"summary":"Create a new profile","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"get":{"tags":["Profiles"],"summary":"List profiles","parameters":[{"schema":{"type":"string"},"required":false,"name":"workerType","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}":{"get":{"tags":["Profiles"],"summary":"Get profile with latest version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"post":{"tags":["Profiles"],"summary":"Create new profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["workerType","model"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}},"put":{"tags":["Profiles"],"summary":"Update profile identity","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileIdentity"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"}}}}}},"delete":{"tags":["Profiles"],"summary":"Delete profile","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/profiles/{profileId}/versions":{"get":{"tags":["Profiles"],"summary":"List profile versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}/versions/{version}":{"get":{"tags":["Profiles"],"summary":"Get profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"},{"schema":{"type":["number","null"]},"required":false,"name":"version","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}},"/api/v1/report-templates/default-system-prompt":{"get":{"tags":["Report Templates"],"summary":"Get default system prompt","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string"}},"required":["content"]}}}}}}},"/api/v1/report-templates/available-models":{"get":{"tags":["Report Templates"],"summary":"List models available for report generation","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"]}}}}}}}},"/api/v1/report-templates":{"get":{"tags":["Report Templates"],"summary":"List report templates","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}}}}},"post":{"tags":["Report Templates"],"summary":"Create report template","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportTemplateInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"409":{"description":"Report template already exists"}}}},"/api/v1/report-templates/{id}":{"get":{"tags":["Report Templates"],"summary":"Get report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"put":{"tags":["Report Templates"],"summary":"Update report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateReportTemplateInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"delete":{"tags":["Report Templates"],"summary":"Delete report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Report template not found"}}}},"/api/v1/agents":{"get":{"tags":["Agents"],"summary":"List agents","parameters":[{"schema":{"type":"string"},"required":false,"name":"modelProvider","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentResponse"}}}}}}},"post":{"tags":["Agents"],"summary":"Create or update agent (upsert)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"409":{"description":"Restored agent versions conflict with an active queue owner"}}}},"/api/v1/agents/{id}":{"get":{"tags":["Agents"],"summary":"Get agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"404":{"description":"Agent not found"}}},"put":{"tags":["Agents"],"summary":"Update agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAgentInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"}}},"delete":{"tags":["Agents"],"summary":"Delete agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Agent not found"}}}},"/api/v1/agents/{id}/versions":{"get":{"tags":["Agents"],"summary":"List agent versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}}}}},"404":{"description":"Agent not found"}}},"post":{"tags":["Agents"],"summary":"Register agent version (upsert)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterAgentVersionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent registration did not stabilize"}}}},"/api/v1/agents/{id}/versions/{agentVersion}":{"patch":{"tags":["Agents"],"summary":"Patch agent version","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"agentVersion","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchAgentVersionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Invalid status value"},"404":{"description":"Agent or version not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent activation did not stabilize"}}}},"/api/v1/models":{"get":{"tags":["Models"],"summary":"List models","parameters":[{"schema":{"type":"string"},"required":false,"name":"agentId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"provider","in":"query"},{"schema":{"type":"string","enum":["active","disappeared"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ModelResponse"}}}}}}}},"/api/v1/models/{id}":{"get":{"tags":["Models"],"summary":"Get model","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelResponse"}}}}}}},"/api/v1/models/sync":{"post":{"tags":["Models"],"summary":"Sync models from provider","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"agentId":{"type":"string"},"provider":{"type":"string"},"models":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"providerAvailableFrom":{"type":"string"},"providerEndOfLife":{"type":"string"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}}},"required":["id"]}},"scannedAt":{"type":"string"}},"required":["agentId","provider","models","scannedAt"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"added":{"type":"array","items":{"type":"string"}},"removed":{"type":"array","items":{"type":"string"}},"unchanged":{"type":"array","items":{"type":"string"}}},"required":["added","removed","unchanged"]}}}}}}},"/api/v1/mcp/servers":{"get":{"tags":["MCP Servers"],"summary":"List MCP servers","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"post":{"tags":["MCP Servers"],"summary":"Create MCP server","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}},"required":["_id","name","type"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"/api/v1/mcp/servers/{id}":{"get":{"tags":["MCP Servers"],"summary":"Get MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"put":{"tags":["MCP Servers"],"summary":"Update MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMcpServerInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"delete":{"tags":["MCP Servers"],"summary":"Delete MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/skills":{"get":{"tags":["Skills"],"summary":"List all skills","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"post":{"tags":["Skills"],"summary":"Create or import a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSkillInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"/api/v1/skills/search":{"get":{"tags":["Skills"],"summary":"Search skills (internal + external)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/search/external":{"get":{"tags":["Skills"],"summary":"Search external skills registry","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project."},"required":false,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/discover":{"get":{"tags":["Skills"],"summary":"Discover skills in a GitHub repository","parameters":[{"schema":{"type":"string"},"required":true,"name":"source","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillDiscoveryResult"}}}}},"400":{"description":"Missing or malformed source parameter"},"404":{"description":"Repository not found"},"502":{"description":"GitHub API error"}}}},"/api/v1/skills/{id}(*)/revisions":{"get":{"tags":["Skills"],"summary":"List skill revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}}},"404":{"description":"Skill not found"}}}},"/api/v1/skills/{id}(*)":{"get":{"tags":["Skills"],"summary":"Get skill by slug","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}},"404":{"description":"Skill not found"}}},"delete":{"tags":["Skills"],"summary":"Soft-delete a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Skill not found"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)/archive":{"get":{"tags":["Skill Revisions"],"summary":"Download skill revision archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Skill revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ref","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skill-revisions/{id}":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ID","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skills/{id}(*)/resolve":{"post":{"tags":["Skills"],"summary":"Trigger skill resolution","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebases":{"get":{"tags":["Codebases"],"summary":"List all codebases","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseResponse"}}}}}}},"post":{"tags":["Codebases"],"summary":"Create a codebase (archive codebases require the archive file)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input or missing archive"}}}},"/api/v1/codebases/{id}":{"get":{"tags":["Codebases"],"summary":"Get a codebase","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"patch":{"tags":["Codebases"],"summary":"Update a codebase","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCodebaseInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"delete":{"tags":["Codebases"],"summary":"Delete a codebase","responses":{"204":{"description":"Success"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebases/{id}/revisions":{"get":{"tags":["Codebases"],"summary":"List codebase revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}}},"404":{"description":"Codebase not found"}}},"post":{"tags":["Codebases"],"summary":"Resolve a new git codebase revision","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveCodebaseRevisionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"400":{"description":"Not a git codebase"},"404":{"description":"Codebase not found"},"502":{"description":"GitHub resolution failed"}}}},"/api/v1/codebases/{id}/upload":{"post":{"tags":["Codebases"],"summary":"Upload a codebase archive as a new revision","responses":{"201":{"description":"Success"},"400":{"description":"Missing archive or not an archive codebase"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebase-revisions/{id}/archive":{"get":{"tags":["Codebase Revisions"],"summary":"Download codebase revision archive","responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebase-revisions/{id}":{"get":{"tags":["Codebase Revisions"],"summary":"Get codebase revision by id","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources":{"get":{"tags":["Resources"],"summary":"List all resources","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceResponse"}}}}}}},"post":{"tags":["Resources"],"summary":"Create a resource and its first revision","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceInput"}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input"},"409":{"description":"Resource slug or revision ref already exists in this project"}}}},"/api/v1/resources/{id}":{"get":{"tags":["Resources"],"summary":"Get a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"patch":{"tags":["Resources"],"summary":"Update a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateResourceInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"delete":{"tags":["Resources"],"summary":"Delete a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Resource not found"}}}},"/api/v1/resources/{id}/revisions":{"get":{"tags":["Resource Revisions"],"summary":"List resource revisions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}}},"404":{"description":"Resource not found"}}},"post":{"tags":["Resource Revisions"],"summary":"Create a resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceRevisionInput"}}}},"responses":{"201":{"description":"Success"},"404":{"description":"Resource not found"},"409":{"description":"Resource revision ref already exists in this project"}}}},"/api/v1/resources/{id}/revisions/latest":{"get":{"tags":["Resource Revisions"],"summary":"Get the latest resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/{id}/revisions/{revisionNumber}":{"get":{"tags":["Resource Revisions"],"summary":"Get a resource revision by resource and revision number","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"revisionNumber","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/revisions/{id}":{"get":{"tags":["Resource Revisions"],"summary":"Get resource revision by id","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/extensions":{"get":{"tags":["Extensions"],"summary":"List all extensions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"post":{"tags":["Extensions"],"summary":"Create or import an extension","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExtensionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"/api/v1/extensions/search":{"get":{"tags":["Extensions"],"summary":"Search extensions (internal + marketplace)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/extensions/{id}":{"get":{"tags":["Extensions"],"summary":"Get extension by ID","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"put":{"tags":["Extensions"],"summary":"Update extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateExtensionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"delete":{"tags":["Extensions"],"summary":"Delete extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/extensions/{id}/versions":{"get":{"tags":["Extensions"],"summary":"List available versions for an extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"preRelease","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionVersionInfo"}}}}}}}},"/api/v1/insights":{"get":{"tags":["Insights"],"summary":"List insights","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"post":{"tags":["Insights"],"summary":"Create insight","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInsightInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"/api/v1/insights/search":{"get":{"tags":["Insights"],"summary":"Search insights","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}}},"/api/v1/insights/{id}":{"get":{"tags":["Insights"],"summary":"Get insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"put":{"tags":["Insights"],"summary":"Update insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateInsightInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"delete":{"tags":["Insights"],"summary":"Delete insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/upvote":{"post":{"tags":["Insights"],"summary":"Upvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/downvote":{"post":{"tags":["Insights"],"summary":"Downvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/block":{"post":{"tags":["Insights"],"summary":"Block insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/unblock":{"post":{"tags":["Insights"],"summary":"Unblock insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/reports":{"get":{"tags":["Insights"],"summary":"Get reports referencing insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Insight not found"}}}},"/api/v1/feature-flags":{"get":{"tags":["Feature Flags"],"summary":"List feature flags","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"/api/v1/feature-flags/{key}":{"put":{"tags":["Feature Flags"],"summary":"Update feature flag","parameters":[{"schema":{"type":"string"},"required":true,"name":"key","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeatureFlagInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"webhooks":{}} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"Scope API","version":"1.0.0","description":"REST API for the Scope platform — benchmarking AI coding agents"},"servers":[{"url":"/","description":"Current server"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Use the unchanged IdP access token, without the Bearer prefix."}},"schemas":{"KeyResponse":{"type":"object","properties":{},"additionalProperties":{}},"KeyInput":{"type":"object","properties":{},"additionalProperties":{}},"ValidateKeyInput":{"type":"object","properties":{"token":{"type":"string"}},"required":["token"]},"AccountResponse":{"type":"object","properties":{},"additionalProperties":{}},"AccountInput":{"type":"object","properties":{},"additionalProperties":{}},"UserMeResponse":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string"},"email":{"type":"string"},"displayName":{"type":"string"},"idp":{"type":"string"},"idpTenant":{"type":"string"}},"required":["id"]},"ProjectResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","createdAt"]},"CreateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"creator":{"type":"string"}},"required":["name"]},"UpdateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"SubmissionListResponse":{"type":"object","properties":{"submissions":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionResponse"}}},"required":["submissions"]},"SubmissionResponse":{"type":"object","properties":{"submissionId":{"type":"string"},"projectId":{"type":"string"},"requestIds":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"submissionRunId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["waiting","ready","blocked","processing","done","failed"]},"requests":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRequestSummary"}},"dependencyProjection":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionDependencyProjection"}},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"}},"handlerProgress":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/SubmissionHandlerStatus"}},"report":{"$ref":"#/components/schemas/SubmissionReportPointer"},"supersededAt":{"type":["string","null"],"format":"date-time"},"supersededBySubmissionRunId":{"type":"string"},"supersededReason":{"type":"string","enum":["request-vector-changed","manual-retry"]}},"required":["submissionId","projectId","requestIds","createdAt","submissionRunId","attemptNumber","readiness","requests","dependencyProjection","blocking"]},"SubmissionRequestSummary":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"task":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"},"workerType":{"type":"string"},"model":{"type":"string"},"profileId":{"type":"string"}},"required":["requestId","runId","task","status","workerType"]},"SubmissionDependencyProjection":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"state":{"type":"string","enum":["waiting","ready","failed","blocked"]},"version":{"type":"number"},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["requestId","runId","handlerId","state"]},"SubmissionBlockingDetail":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"reason":{"type":"string","enum":["request-missing","run-changed","dependency-not-ready","dependency-failed","dependency-blocked"]},"error":{"type":"string"}},"required":["requestId","runId","handlerId","reason"]},"SubmissionHandlerStatus":{"type":"object","properties":{"status":{"type":"string","enum":["queued","processing","done","failed"]},"version":{"type":"number"},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["status"]},"SubmissionReportPointer":{"type":"object","properties":{"reportId":{"type":"string"},"url":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"}}},"ReportResponse":{"type":"object","properties":{"_id":{"type":"string"},"requestId":{"type":"string"},"submissionId":{"type":"string"},"submissionRunId":{"type":"string"},"templateId":{"type":"string"},"reporter":{"$ref":"#/components/schemas/Reporter"},"content":{"type":"string"},"structuredArtifact":{"$ref":"#/components/schemas/StructuredReportArtifact"},"status":{"type":"string","enum":["pending","generating","completed","failed"]},"error":{"type":"string"},"insightReferences":{"type":"array","items":{"$ref":"#/components/schemas/InsightReference"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","status","createdAt","projectId"]},"Reporter":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"gitHash":{"type":"string"},"model":{"type":"string"},"agentId":{"type":"string"},"agentVersion":{"type":"string"}},"required":["id","name","gitHash","model","agentId","agentVersion"]},"StructuredReportArtifact":{"type":"object","properties":{"schemaId":{"type":"string"},"schemaVersion":{"type":"string"},"contentType":{"type":"string","enum":["application/json"]},"url":{"type":"string"}},"required":["schemaId","schemaVersion","contentType","url"]},"InsightReference":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]},"SubmissionRunResponse":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"vectorFingerprint":{"type":"string"},"requests":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRequestRef"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"dependencyProjection":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionDependencyProjection"},"default":[]},"handlerStatus":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/SubmissionHandlerStatus"}},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"},"default":[]},"report":{"$ref":"#/components/schemas/SubmissionReportPointer"},"supersededAt":{"type":["string","null"],"format":"date-time"},"supersededBySubmissionRunId":{"type":"string"},"supersededReason":{"type":"string","enum":["request-vector-changed","manual-retry"]},"submissionId":{"type":"string"},"projectId":{"type":"string"}},"required":["_id","attemptNumber","vectorFingerprint","requests","createdAt"]},"SubmissionRequestRef":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"}},"required":["requestId","runId"]},"SubmissionRetryResponse":{"type":"object","properties":{"submissionId":{"type":"string"},"submissionRunId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["waiting","ready","blocked","processing","done","failed"]},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"}}},"required":["submissionId","submissionRunId","attemptNumber","readiness","blocking"]},"RequestResponse":{"type":"object","properties":{"_id":{"type":"string"},"scenario":{"$ref":"#/components/schemas/Scenario"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"maxIterations":{"type":"number"},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"deletedAt":{"type":["string","null"],"format":"date-time"},"taskPromptId":{"type":"string"},"agentsMdPromptId":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBinding"}},"extensions":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"profileId":{"type":"string"},"profileVersionId":{"type":"string"},"submissionId":{"type":"string"},"submissionExpectedRequestCount":{"type":"integer","minimum":1},"submissionRequestIndex":{"type":"integer","minimum":0},"submissionInitialRunId":{"type":"string"},"submissionFinalizedAt":{"type":["string","null"],"format":"date-time"},"priority":{"type":"integer","default":0},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"observations":{"type":"array","items":{"type":"string"}},"gateSummaries":{"type":"array","items":{"$ref":"#/components/schemas/GateRunSummary"}},"run":{"$ref":"#/components/schemas/RunState"},"projectId":{"type":"string"}},"required":["_id","scenario","workerType","createdAt","projectId"]},"Scenario":{"type":"object","properties":{"version":{"type":"string","enum":["v1","v2"]},"task":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}}},"required":["task","criteria"]},"Persona":{"type":"object","properties":{"personality":{"type":"string","enum":["demanding","friendly"]},"experience":{"type":"string","enum":["junior","senior"]},"verbosity":{"type":"string","enum":["brief","moderate"]},"type":{"type":"string","enum":["traditional","ai_assisted","vibe"]}},"required":["personality","experience","verbosity","type"]},"ResourceBinding":{"type":"object","properties":{"ref":{"type":"string"},"revisionId":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref","revisionId","params"]},"GateConfig":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"promptId":{"type":"string"},"promptText":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}},"maxIterations":{"type":"integer","minimum":1,"maximum":50}},"required":["gate","criteria"]},"GateRunSummary":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"status":{"type":"string","enum":["passed","failed","skipped"]},"iterations":{"type":"integer","minimum":0}},"required":["gate","status","iterations"]},"RunState":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"queuedQueueName":{"type":"string"},"outcome":{"type":"string","enum":["succeeded","failed","finished"]},"result":{"type":"string"},"error":{"type":"string"},"logsUrl":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"turns":{"type":"array","items":{"$ref":"#/components/schemas/ConversationTurn"}},"workerVersion":{"type":"string"},"os":{"type":"object","properties":{"platform":{"type":"string"},"release":{"type":"string"},"arch":{"type":"string"}},"required":["platform","release","arch"]},"lastHeartbeatAt":{"type":["string","null"],"format":"date-time"},"worker":{"type":"object","properties":{"instanceId":{"type":"string"},"podName":{"type":"string"}},"required":["instanceId"]},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"setupVideoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"aiCallCount":{"type":"number"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRunOutcome"}},"mcpRegistered":{"type":"boolean"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"pausedAt":{"type":["string","null"],"format":"date-time"},"resumedAt":{"type":["string","null"],"format":"date-time"},"observationResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}}},"required":["_id","attemptNumber","status"]},"ConversationTurn":{"type":"object","properties":{"iteration":{"type":"number"},"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"codingAgentResponse":{"type":"string"},"judgeFeedback":{"type":"string"},"snapshotUrl":{"type":"string"},"passed":{"type":"boolean"},"timestamp":{"type":["string","null"],"format":"date-time"},"criteriaResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"observationResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"startedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"toolCalls":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"arguments":{"type":"object","additionalProperties":{}},"response":{"type":"string"},"timestamp":{"type":"string"}},"required":["id","name","arguments"]}},"toolCallsUrl":{"type":"string"},"toolCallCount":{"type":"number"},"aiCallCount":{"type":"number"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"chatResultUrl":{"type":"string"},"chatResultFormat":{"type":"string"}},"required":["iteration","judgeFeedback","snapshotUrl","passed","timestamp"]},"CriterionResult":{"type":"object","properties":{"criterionId":{"type":"string"},"passed":{"type":"boolean"},"feedback":{"type":"string"},"evaluated":{"type":"boolean"}},"required":["criterionId","passed","feedback","evaluated"]},"TokenUsage":{"type":"object","properties":{"promptTokens":{"type":"number"},"completionTokens":{"type":"number"},"totalTokens":{"type":"number"}},"required":["promptTokens","completionTokens","totalTokens"]},"ResourceRunOutcome":{"type":"object","properties":{"ref":{"type":"string"},"slug":{"type":"string"},"revisionId":{"type":"string"},"setupSucceeded":{"type":"boolean"},"published":{"type":"array","items":{"type":"string"}},"params":{"type":"object","additionalProperties":{"type":"string"}},"setupDurationMs":{"type":"number"},"error":{"type":"string"},"teardownRan":{"type":"boolean"}},"required":["ref","slug","revisionId","setupSucceeded","published"]},"ResourceBindingSpec":{"anyOf":[{"type":"string"},{"type":"object","properties":{"ref":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref"]}]},"CreateRequestInput":{"type":"object","properties":{"scenario":{"$ref":"#/components/schemas/Scenario"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"maxIterations":{"type":"integer","minimum":1,"maximum":50},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"profileId":{"type":"string"},"profileVariations":{"type":"array","items":{"type":"string"}},"priority":{"type":"integer"},"agentsMd":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"observations":{"type":"array","items":{"type":"string"}}},"required":["scenario"]},"RunFacetsResponse":{"type":"object","properties":{"total":{"type":"number"},"facets":{"type":"object","properties":{"workerType":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"status":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"outcome":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"model":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"os":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"priority":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"agentVersion":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"profileId":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}}},"required":["workerType","status","outcome","model","os","priority","agentVersion","profileId"]}},"required":["total","facets"]},"RunFacetBucket":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"number"}},"required":["value","count"]},"PaginatedRunsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"Cursors":{"type":"object","properties":{"next":{"type":["string","null"]},"prev":{"type":["string","null"]}},"required":["next","prev"]},"PaginatedRunGroupsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RunGroup"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"RunGroup":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"runIds":{"type":"array","items":{"type":"string"}},"aggregates":{"$ref":"#/components/schemas/GroupAggregates"},"uniform":{"$ref":"#/components/schemas/GroupUniformValues"}},"required":["key","label","runIds","aggregates","uniform"]},"GroupAggregates":{"type":"object","properties":{"count":{"type":"number"},"turns":{"$ref":"#/components/schemas/AggregateStats"},"duration":{"$ref":"#/components/schemas/AggregateStats"},"promptTokens":{"$ref":"#/components/schemas/AggregateStats"},"completionTokens":{"$ref":"#/components/schemas/AggregateStats"},"statusCounts":{"type":"object","additionalProperties":{"type":"number"}},"outcomeCounts":{"type":"object","additionalProperties":{"type":"number"}}},"required":["count","turns","duration","promptTokens","completionTokens","statusCounts","outcomeCounts"]},"AggregateStats":{"type":["object","null"],"properties":{"min":{"type":"number"},"max":{"type":"number"},"mean":{"type":"number"},"stdDev":{"type":"number"}},"required":["min","max","mean","stdDev"]},"GroupUniformValues":{"type":"object","properties":{"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":"string"},"platform":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"extensions":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"submissionId":{"type":"string"},"task":{"type":"string"}}},"BulkResubmitInput":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"count":{"type":"integer","minimum":1,"maximum":10,"default":1},"overrides":{"type":"object","properties":{"profileId":{"type":["string","null"]},"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":["string","null"]},"reasoningEffort":{"type":["string","null"]},"maxIterations":{"type":["number","null"]},"mcpServers":{"type":["array","null"],"items":{"type":"string"}},"skillRevisions":{"type":["array","null"],"items":{"type":"string"}},"extensions":{"type":["array","null"],"items":{"type":"string"}}}}},"required":["ids"]},"CreateCriteriaInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"],"default":"gate"},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}},"required":["id","prompt"]},"CriteriaResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"CriteriaGraph":{"type":"object","properties":{"nodes":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphNode"}},"edges":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphEdge"}}},"required":["nodes","edges"]},"CriteriaGraphNode":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}},"required":["id","prompt"]},"CriteriaGraphEdge":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"]},"UpdateCriteriaInput":{"type":"object","properties":{"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}}},"CreatePromptFeatureInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["id","prompt"]},"PromptType":{"type":"string","enum":["select","build","test","run","deploy","agents.md"]},"PromptFeatureResult":{"type":"object","properties":{"featureId":{"type":"string"},"detected":{"type":"boolean"},"evaluated":{"type":"boolean"}},"required":["featureId","detected","evaluated"]},"SuggestedPromptFeature":{"type":"object","properties":{"suggestedId":{"type":"string"},"behavior":{"type":"string"},"prompt":{"type":"string"}},"required":["suggestedId","behavior","prompt"]},"PromptFeatureResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"UpdatePromptFeatureInput":{"type":"object","properties":{"prompt":{"type":"string"}}},"TaskPromptResponse":{"type":"object","properties":{"_id":{"type":"string"},"keyId":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"text":{"type":"string"},"contentBlobUrl":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","keyId","createdAt","projectId"]},"CreateTaskPromptInput":{"type":"object","properties":{"text":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["text"]},"PatchTaskPromptFeatureInput":{"type":"object","properties":{"detected":{"type":"boolean"}},"required":["detected"]},"CreateReportInput":{"type":"object","properties":{"requestId":{"type":"string"},"templateId":{"type":"string"}},"required":["requestId"]},"BulkCreateReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}},"templateId":{"type":"string"}},"required":["requestIds"]},"BulkReportStatusInput":{"type":"object","properties":{"reportIds":{"type":"array","items":{"type":"string"}}},"required":["reportIds"]},"BulkReportSummaryResponse":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/ReportSummary"}},"ReportSummary":{"type":"object","properties":{"total":{"type":"number"},"pending":{"type":"number"},"generating":{"type":"number"},"completed":{"type":"number"},"failed":{"type":"number"}},"required":["total","pending","generating","completed","failed"]},"BulkReportSummaryInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"TriggerReportsInput":{"type":"object","properties":{"requestId":{"type":"string"}},"required":["requestId"]},"BulkTriggerReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"InsightResponse":{"type":"object","properties":{"_id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"upvotes":{"type":"number"},"downvotes":{"type":"number"},"blocked":{"type":"boolean"},"referenceCount":{"type":"number"},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","title","description","upvotes","downvotes","blocked","referenceCount","createdBy","createdAt","projectId"]},"ProfileWithVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"version":{"$ref":"#/components/schemas/ProfileVersionResponse"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","version","projectId"]},"ProfileVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"profileId":{"type":"string"},"version":{"type":"number"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","profileId","version","workerType","model","createdAt","projectId"]},"CreateProfileInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["name","workerType","model"]},"ProfileResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","projectId"]},"UpdateProfileIdentity":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512}}},"ReportTemplateResponse":{"type":"object","properties":{"_id":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","id","name","userPrompt","createdAt","projectId"]},"ReportTemplateSystemPrompt":{"type":"object","properties":{"mode":{"type":"string","enum":["append","override"]},"content":{"type":"string"}},"required":["mode","content"]},"ReportTrigger":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["always"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["criteria"]},"criteriaIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","criteriaIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["taskPrompt"]},"taskPromptIds":{"type":"array","items":{"type":"string"}}},"required":["type","taskPromptIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["promptFeature"]},"featureIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","featureIds"]}]},"CreateReportTemplateInput":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0}},"required":["id","name","userPrompt"]},"UpdateReportTemplateInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"allOf":[{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},{"type":["object","null"]}]},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":["string","null"]},"timeoutMs":{"type":["integer","null"],"exclusiveMinimum":0}}},"AgentResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"},"versions":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","supportedModels","createdAt"]},"AgentCapabilities":{"type":"object","properties":{"supportsReasoningEffort":{"type":"boolean"},"supportsMcpServers":{"type":"boolean"},"supportsSkills":{"type":"boolean"},"supportsExtensions":{"type":"boolean"},"supportsResources":{"type":"boolean"}}},"AgentVersion":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string"},"status":{"type":"string","enum":["active","retired"]},"createdAt":{"type":["string","null"],"format":"date-time"}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","status","createdAt"]},"CreateAgentInput":{"type":"object","properties":{"_id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}},"required":["_id","name"]},"UpdateAgentInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}}},"RegisterAgentVersionInput":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string","minLength":1}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","queueName"]},"PatchAgentVersionInput":{"type":"object","properties":{"status":{"type":"string","enum":["active","retired"]}},"required":["status"]},"ModelResponse":{"type":"object","properties":{"_id":{"type":"string"},"modelId":{"type":"string"},"provider":{"type":"string"},"agentId":{"type":"string"},"firstSeenAt":{"type":["string","null"],"format":"date-time"},"lastSeenAt":{"type":["string","null"],"format":"date-time"},"disappearedAt":{"type":["string","null"],"format":"date-time"},"providerAvailableFrom":{"type":["string","null"],"format":"date-time"},"providerEndOfLife":{"type":["string","null"],"format":"date-time"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"$ref":"#/components/schemas/ModelCapabilities"}},"required":["_id","modelId","provider","agentId","firstSeenAt","lastSeenAt"]},"ModelCapabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}},"McpServerResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","type","createdAt","projectId"]},"McpServerHeader":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}},"required":["name","value"]},"UpdateMcpServerInput":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}}},"SkillResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","source","skillName","name","origin","createdAt","projectId"]},"SkillSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"source":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"installs":{"type":"number"}},"required":["id","name","source","internal"]},"SkillDiscoveryResult":{"type":"object","properties":{"skillName":{"type":"string"},"skillPath":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"existsInLibrary":{"type":"boolean"},"currentRevisionCommitSha":{"type":"string"},"latestUpstreamCommitSha":{"type":"string"},"updateAvailable":{"type":"boolean"},"lastImportedAt":{"type":"string"}},"required":["skillName","skillPath"]},"SkillRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"ref":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"skillPath":{"type":"string"},"commitHash":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"name":{"type":"string"},"description":{"type":"string"},"license":{"type":"string"},"compatibility":{"type":"string"},"allowedTools":{"type":"string"},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"content":{"type":"string"},"archiveUrl":{"type":"string"},"validationWarnings":{"type":"array","items":{"type":"string"}},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","ref","source","skillName","skillPath","commitHash","commitTimestamp","name","description","content","archiveUrl","resolvedAt","createdAt","projectId"]},"CreateSkillInput":{"type":"object","properties":{"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]}},"required":["source","skillName","name","origin"]},"CodebaseResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"defaultBranch":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","sourceType","revisionCounter","createdAt","projectId"]},"UpdateCodebaseInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"defaultBranch":{"type":"string"}}},"CodebaseRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"codebaseId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"requestedRef":{"type":"string"},"resolvedCommitSha":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"originalFilename":{"type":"string"},"contentSha256":{"type":"string"},"archiveUrl":{"type":"string"},"sizeBytes":{"type":"number"},"fileCount":{"type":"number"},"creator":{"type":"string"},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","codebaseId","slug","revisionNumber","ref","sourceType","archiveUrl","resolvedAt","createdAt","projectId"]},"ResolveCodebaseRevisionInput":{"type":"object","properties":{"requestedRef":{"type":"string"},"creator":{"type":"string"}}},"ResourceResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"latestRevisionNumber":{"type":"number"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","revisionCounter","createdAt","projectId"]},"CreateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1},"description":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["name","setup"]},"ResourceScript":{"type":"object","properties":{"sh":{"type":"string","minLength":1}}},"ResourceParameter":{"type":"object","properties":{"name":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"description":{"type":"string"},"required":{"type":"boolean","default":false},"default":{"type":"string"},"example":{"type":"string"}},"required":["name"]},"UpdateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"ResourceRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"resourceId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"contentSha256":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","resourceId","slug","revisionNumber","ref","setup","exports","contentSha256","createdAt","projectId"]},"CreateResourceRevisionInput":{"type":"object","properties":{"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["setup"]},"ExtensionResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","publisher","name","origin","createdAt","projectId"]},"ExtensionSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"publisher":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"version":{"type":"string"}},"required":["id","name","publisher","internal"]},"CreateExtensionInput":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+$"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]}},"required":["_id","publisher","name","origin"]},"UpdateExtensionInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ExtensionVersionInfo":{"type":"object","properties":{"version":{"type":"string"},"preRelease":{"type":"boolean"},"lastUpdated":{"type":"string"}},"required":["version","preRelease","lastUpdated"]},"CreateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"}},"required":["title","description"]},"UpdateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}}}},"FeatureFlagResponse":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"enabled":{"type":"boolean"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["key","label","enabled","updatedAt"]},"UpdateFeatureFlagInput":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]}},"parameters":{}},"paths":{"/api/v1/keys/preview":{"post":{"tags":["Keys"],"summary":"Preview key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}}},"/api/v1/keys":{"post":{"tags":["Keys"],"summary":"Create key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"201":{"description":"Key created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"get":{"tags":["Keys"],"summary":"List keys","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KeyResponse"}}}}}}}},"/api/v1/keys/{id}":{"get":{"tags":["Keys"],"summary":"Get key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"put":{"tags":["Keys"],"summary":"Update key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"delete":{"tags":["Keys"],"summary":"Delete key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Key deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/keys/{id}/validate":{"post":{"tags":["Keys"],"summary":"Validate key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateKeyInput"}}}},"responses":{"200":{"description":"Validation result","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Key validation result"}}}}}}},"/api/v1/accounts":{"post":{"tags":["Accounts"],"summary":"Create account","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"201":{"description":"Account created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"get":{"tags":["Accounts"],"summary":"List accounts","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccountResponse"}}}}}}}},"/api/v1/accounts/{id}":{"get":{"tags":["Accounts"],"summary":"Get account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"put":{"tags":["Accounts"],"summary":"Update account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"200":{"description":"Account updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"delete":{"tags":["Accounts"],"summary":"Delete account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Account deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/users/me":{"get":{"tags":["Users"],"summary":"Get the authenticated user's identity","description":"Read the existing authenticated Scope identity without enrollment or profile writes. Responses must not be HTTP-cached.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is not enrolled or is disabled"},"503":{"description":"Authentication service unavailable"}}},"post":{"tags":["Users"],"summary":"Enroll the authenticated user","description":"JIT-enroll the authenticated user, refresh their profile and lastLoginAt, apply bootstrap-admin rules, and warm the access cache. Use this only after an explicit IdP login; do not prefetch, poll, or automatically retry transient failures.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is disabled"},"503":{"description":"Authentication service unavailable"}}}},"/health":{"get":{"tags":["Health"],"summary":"Liveness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"version":{"type":"string"}},"required":["status","version"]}}}}}}},"/ready":{"get":{"tags":["Health"],"summary":"Readiness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"migrations":{}},"required":["status"]}}}},"503":{"description":"Service is not ready"}}}},"/about":{"get":{"tags":["System"],"summary":"API metadata","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"version":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"description":{"type":"string"},"workers":{"type":"array","items":{"type":"string"}}},"required":["name","version","buildTime","environment","description","workers"]}}}}}}},"/api/v1/version":{"get":{"tags":["System"],"summary":"Version info","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"commit":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"strictAgentCapabilities":{"type":"boolean"}},"required":["commit","buildTime","environment","strictAgentCapabilities"]}}}}}}},"/api/v1/projects":{"get":{"tags":["Projects"],"summary":"List all projects","parameters":[{"schema":{"type":"string","enum":["true","false"],"description":"When true, include soft-deleted projects in the result"},"required":false,"description":"When true, include soft-deleted projects in the result","name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProjectResponse"}}}}}}},"post":{"tags":["Projects"],"summary":"Create a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/projects/{id}":{"get":{"tags":["Projects"],"summary":"Get a project","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"patch":{"tags":["Projects"],"summary":"Update a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProjectInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"delete":{"tags":["Projects"],"summary":"Soft-delete a project","responses":{"204":{"description":"Success"},"404":{"description":"Project not found"}}}},"/api/v1/projects/{id}/restore":{"post":{"tags":["Projects"],"summary":"Restore a soft-deleted project","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}}},"/api/v1/submissions":{"get":{"tags":["Submissions"],"summary":"List submissions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionListResponse"}}}}}}},"/api/v1/submissions/{id}/reports":{"get":{"tags":["Reports"],"summary":"Get reports for submission","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}":{"get":{"tags":["Submissions"],"summary":"Get a submission","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionResponse"}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}/runs":{"get":{"tags":["Submissions"],"summary":"List current and historical submission attempts","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"runs":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRunResponse"}}},"required":["runs"]}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}/runs/{submissionRunId}":{"get":{"tags":["Submissions"],"summary":"Get one submission attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"submissionRunId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionRunResponse"}}}},"404":{"description":"Submission attempt not found"}}}},"/api/v1/submissions/{id}/retry":{"post":{"tags":["Submissions"],"summary":"Retry current submission processing","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"submissionRunId":{"type":"string"}},"required":["submissionRunId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionRetryResponse"}}}},"404":{"description":"Submission not found"},"409":{"description":"Submission attempt is stale or dependencies are blocked"}}}},"/api/v1/requests":{"post":{"tags":["Requests"],"summary":"Submit request(s)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/CreateRequestInput"},{"type":"object","properties":{"count":{"type":"number","minimum":1,"maximum":10,"default":1},"skills":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"codebase":{"type":"string"}}}]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RequestResponse"},{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}]}}}}}},"get":{"tags":["Requests"],"summary":"List requests","parameters":[{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"profileId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"status","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"outcome","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"model","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"os","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"priority","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"agentVersion","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"type":"string","enum":["task","submissionId","profile"]},"required":false,"name":"groupBy","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"after","in":"query"},{"schema":{"type":"string"},"required":false,"name":"before","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"last","in":"query"},{"schema":{"type":"string","enum":["created","updated","priority","worker","status","id","duration","createdAt"]},"required":false,"name":"sortBy","in":"query"},{"schema":{"type":"string","enum":["asc","desc"]},"required":false,"name":"sortDir","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdAfter","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdBefore","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"turns","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"turnsOp","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"maxIterations","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"maxIterationsOp","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PaginatedRunsResponse"},{"$ref":"#/components/schemas/PaginatedRunGroupsResponse"}]}}}}}}},"/api/v1/requests/facets":{"get":{"tags":["Requests"],"summary":"List run filter facets","description":"Returns every distinct value and its full-dataset count per categorical filter dimension for the Runs list rail. Counts are absolute over all non-deleted runs **in the given project**: they intentionally ignore the active search, date range, iteration, and categorical selections so every selectable value stays visible with a stable count. Requires ?projectId=. Computed with parallel $group aggregations (Cosmos has no $facet) and cached per-project for a short TTL.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunFacetsResponse"}}}}}}},"/api/v1/requests/{id}":{"get":{"tags":["Requests"],"summary":"Get request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestResponse"}}}},"404":{"description":"Not found"}}},"delete":{"tags":["Requests"],"summary":"Soft-delete request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Not found"}}}},"/api/v1/analysis":{"get":{"tags":["Requests"],"summary":"Compute pass@k / success@T metrics","description":"Aggregates pass@k / success@T metrics over a single project's done runs. Requires ?projectId=.","parameters":[{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"k","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Analysis metrics"}}}}}}},"/api/v1/requests/bulk-resubmit":{"post":{"tags":["Requests"],"summary":"Bulk resubmit requests","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkResubmitInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}}}}}}},"/api/v1/requests/bulk":{"delete":{"tags":["Requests"],"summary":"Bulk soft-delete requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}},"required":["ids"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"number"}},"required":["deleted"]}}}}}}},"/api/v1/requests/archive":{"post":{"tags":["Requests"],"summary":"Download batch archive of multiple runs","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Gzipped batch archive containing individual run archives"},"400":{"description":"Invalid input"},"404":{"description":"One or more runs not found"}}}},"/api/v1/runs/upload":{"post":{"tags":["Requests"],"summary":"Import run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"}}}},"/api/v1/runs/upload-batch":{"post":{"tags":["Requests"],"summary":"Import batch run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"207":{"description":"Multi-status: 201 if all runs imported, 400 if none imported, 207 if partial"},"400":{"description":"No archive uploaded, empty archive, or all runs failed"}}}},"/api/v1/requests/{id}/reports":{"get":{"tags":["Reports"],"summary":"Get reports for request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Run not found"}}}},"/api/v1/requests/{id}/runs":{"get":{"tags":["Requests"],"summary":"List attempts for a request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RunState"}}}}},"404":{"description":"Request not found"}}}},"/api/v1/requests/{id}/runs/{runId}":{"get":{"tags":["Requests"],"summary":"Get a single attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunState"}}}},"404":{"description":"Request or run not found"}}}},"/api/v1/requests/bulk-retry":{"post":{"tags":["Requests"],"summary":"Bulk retry requests (start new attempts)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"force":{"type":"boolean"}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"retried":{"type":"integer"},"skipped":{"type":"integer"},"results":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"},"error":{"type":"string"}},"required":["requestId"]}}},"required":["retried","skipped","results"]}}}}}}},"/api/v1/requests/{id}/retry":{"post":{"tags":["Requests"],"summary":"Retry a request (start a new attempt)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"force":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"}},"required":["requestId","runId","attemptNumber"]}}}},"404":{"description":"Request not found"},"409":{"description":"Conflict — request is not in a retryable state, or a concurrent retry won the race"},"422":{"description":"Cannot retry — current run not yet terminal"}}}},"/api/v1/requests/{id}/priority":{"post":{"tags":["Requests"],"summary":"Set priority on a single request","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"priority":{"type":"integer"}},"required":["priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"priority":{"type":"number"}},"required":["id","priority"]}}}}}}},"/api/v1/requests/bulk-priority":{"post":{"tags":["Requests"],"summary":"Set priority on multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100},"priority":{"type":"integer"}},"required":["ids","priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/pause":{"post":{"tags":["Requests"],"summary":"Pause a single request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/{id}/resume":{"post":{"tags":["Requests"],"summary":"Resume a paused request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/bulk-pause":{"post":{"tags":["Requests"],"summary":"Pause multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/bulk-resume":{"post":{"tags":["Requests"],"summary":"Resume multiple paused requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/cancel":{"post":{"tags":["Requests"],"summary":"Cancel a request (marks as done/failed and signals worker to exit)","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"previousStatus":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"}},"required":["id","previousStatus","status","outcome"]}}}}}}},"/api/v1/requests/bulk-cancel":{"post":{"tags":["Requests"],"summary":"Cancel multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"cancelled":{"type":"number"},"skipped":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"cancelled":{"type":"boolean"},"previousStatus":{"type":"string"},"error":{"type":"string"}},"required":["id","cancelled"]}}},"required":["cancelled","skipped","results"]}}}}}}},"/api/v1/requests/{id}/logs":{"get":{"tags":["Requests"],"summary":"Stream request logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/logs":{"get":{"tags":["Requests"],"summary":"Stream logs for a specific attempt (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific attempt and iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/video":{"get":{"tags":["Requests"],"summary":"Download session recording","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/video":{"get":{"tags":["Requests"],"summary":"Download session recording for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/criteria/generate-prompt":{"post":{"tags":["Criteria"],"summary":"Generate criterion prompt from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"},"gates":{"type":"array","items":{"type":"string"}}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"},"suggestedId":{"type":"string"},"suggestedParents":{"type":"array","items":{"type":"string"}},"suggestedChildren":{"type":"array","items":{"type":"string"}}},"required":["prompt","suggestedId","suggestedParents","suggestedChildren"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/criteria/seed":{"post":{"tags":["Criteria"],"summary":"Seed criteria in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"criteria":{"type":"array","items":{"$ref":"#/components/schemas/CreateCriteriaInput"}}},"required":["criteria"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}},"400":{"description":"Seed batch would introduce a dependency cycle"}}}},"/api/v1/criteria":{"get":{"tags":["Criteria"],"summary":"List criteria","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","description":"Comma-separated criterion IDs to include"},"required":false,"description":"Comma-separated criterion IDs to include","name":"ids","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"When true and ids is set, also include dependency ancestors"},"required":false,"description":"When true and ids is set, also include dependency ancestors","name":"ancestors","in":"query"},{"schema":{"type":"string","enum":["gate","observation"],"description":"Filter by criterion kind"},"required":false,"description":"Filter by criterion kind","name":"kind","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaResponse"}}}}}}},"post":{"tags":["Criteria"],"summary":"Create criterion","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCriteriaInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"409":{"description":"Criterion already exists"}}}},"/api/v1/criteria/mdp":{"get":{"tags":["Criteria"],"summary":"Compute MDP transitions","parameters":[{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"since","in":"query"},{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{}}}}}}}},"/api/v1/criteria/graph":{"get":{"tags":["Criteria"],"summary":"Get criteria DAG","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaGraph"}}}}}}},"/api/v1/criteria/{id}":{"get":{"tags":["Criteria"],"summary":"Get criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"404":{"description":"Criterion not found"}}},"put":{"tags":["Criteria"],"summary":"Update criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCriteriaInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"400":{"description":"Invalid dependency reference or self-reference"},"404":{"description":"Criterion not found"}}},"delete":{"tags":["Criteria"],"summary":"Soft-delete criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Criterion not found"},"409":{"description":"Criterion has dependents"}}}},"/api/v1/prompt-features/generate-prompt":{"post":{"tags":["Prompt Features"],"summary":"Generate prompt feature from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"}},"required":["prompt"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features/seed":{"post":{"tags":["Prompt Features"],"summary":"Seed prompt features in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}},"required":["features"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}}}}},"/api/v1/prompt-features/extract-from-text":{"post":{"tags":["Prompt Features"],"summary":"Extract features from text","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string"},"model":{"type":"string"},"type":{"type":"string","enum":["select","agents.md"]}},"required":["text"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["features","cached"]}}}},"400":{"description":"Empty text string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features":{"get":{"tags":["Prompt Features"],"summary":"List features","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","enum":["select","agents.md"]},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}}}}},"post":{"tags":["Prompt Features"],"summary":"Create feature","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"409":{"description":"Feature already exists"}}}},"/api/v1/prompt-features/{id}":{"get":{"tags":["Prompt Features"],"summary":"Get feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"404":{"description":"Feature not found"}}},"put":{"tags":["Prompt Features"],"summary":"Update feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Feature not found"}}},"delete":{"tags":["Prompt Features"],"summary":"Soft-delete feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Feature not found"}}}},"/api/v1/task-prompts/generate":{"post":{"tags":["Task Prompts"],"summary":"Generate task prompts","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string"},"existingPrompt":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"tasks":{"type":"array","items":{"type":"string"}}},"required":["tasks"]}}}},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts":{"get":{"tags":["Task Prompts"],"summary":"List task prompts","parameters":[{"schema":{"type":["number","null"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":["number","null"]},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"$ref":"#/components/schemas/PromptType"},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/TaskPromptResponse"}},"total":{"type":"number"},"limit":{"type":"number"},"offset":{"type":"number"}},"required":["items","total","limit","offset"]}}}}}},"post":{"tags":["Task Prompts"],"summary":"Create or find task prompt","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTaskPromptInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Empty text string"}}}},"/api/v1/task-prompts/{id}":{"get":{"tags":["Task Prompts"],"summary":"Get task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"404":{"description":"Task prompt not found"}}},"delete":{"tags":["Task Prompts"],"summary":"Soft-delete task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/content":{"get":{"tags":["Task Prompts"],"summary":"Get resolved task prompt content","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"}},"required":["id","text"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/extract-features":{"post":{"tags":["Task Prompts"],"summary":"Extract features from task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"force","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"taskPromptId":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["taskPromptId","features","cached"]}}}},"404":{"description":"Task prompt not found"},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts/{id}/features/{featureId}":{"patch":{"tags":["Task Prompts"],"summary":"Toggle feature flag on task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"featureId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchTaskPromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Invalid detected value"},"404":{"description":"Task prompt or feature not found"}}}},"/api/v1/reports":{"post":{"tags":["Reports"],"summary":"Create report","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Run or template not found"}}},"get":{"tags":["Reports"],"summary":"List reports","parameters":[{"schema":{"type":"string"},"required":false,"name":"requestId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionRunId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}}}}},"/api/v1/reports/bulk-create":{"post":{"tags":["Reports"],"summary":"Bulk create reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}/structured":{"get":{"tags":["Reports"],"summary":"Download a report's structured JSON artifact","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success"},"404":{"description":"Report or structured artifact not found"}}}},"/api/v1/reports/bulk-status":{"post":{"tags":["Reports"],"summary":"Bulk get report statuses","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportStatusInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/bulk-summary":{"post":{"tags":["Reports"],"summary":"Bulk get report summary per run","description":"Returns aggregated report status counts per run. Only the latest report per template is counted (re-triggers are deduplicated).","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}":{"get":{"tags":["Reports"],"summary":"Get report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"404":{"description":"Report not found"}}}},"/api/v1/reports/{id}/logs":{"get":{"tags":["Reports"],"summary":"Stream report logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Report not found"}}}},"/api/v1/reports/trigger":{"post":{"tags":["Reports"],"summary":"Trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"triggered":{"type":"number"},"reports":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}},"required":["triggered","reports"]}}}},"400":{"description":"Invalid input"},"404":{"description":"Run not found"}}}},"/api/v1/reports/bulk-trigger":{"post":{"tags":["Reports"],"summary":"Bulk trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkTriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":{}}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}/insights":{"get":{"tags":["Reports"],"summary":"Get insights for report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/InsightResponse"},{"type":"object","properties":{"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}}}]}}}}},"404":{"description":"Report not found"}}},"post":{"tags":["Reports"],"summary":"Link insight to report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"isNew":{"type":"boolean"}},"required":["insightId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]}}}},"404":{"description":"Report or insight not found"},"409":{"description":"Insight already referenced by this report"}}}},"/api/v1/profiles":{"post":{"tags":["Profiles"],"summary":"Create a new profile","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"get":{"tags":["Profiles"],"summary":"List profiles","parameters":[{"schema":{"type":"string"},"required":false,"name":"workerType","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}":{"get":{"tags":["Profiles"],"summary":"Get profile with latest version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"post":{"tags":["Profiles"],"summary":"Create new profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["workerType","model"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}},"put":{"tags":["Profiles"],"summary":"Update profile identity","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileIdentity"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"}}}}}},"delete":{"tags":["Profiles"],"summary":"Delete profile","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/profiles/{profileId}/versions":{"get":{"tags":["Profiles"],"summary":"List profile versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}/versions/{version}":{"get":{"tags":["Profiles"],"summary":"Get profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"},{"schema":{"type":["number","null"]},"required":false,"name":"version","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}},"/api/v1/report-templates/default-system-prompt":{"get":{"tags":["Report Templates"],"summary":"Get default system prompt","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string"}},"required":["content"]}}}}}}},"/api/v1/report-templates/available-models":{"get":{"tags":["Report Templates"],"summary":"List models available for report generation","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"]}}}}}}}},"/api/v1/report-templates":{"get":{"tags":["Report Templates"],"summary":"List report templates","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}}}}},"post":{"tags":["Report Templates"],"summary":"Create report template","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportTemplateInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"409":{"description":"Report template already exists"}}}},"/api/v1/report-templates/{id}":{"get":{"tags":["Report Templates"],"summary":"Get report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"put":{"tags":["Report Templates"],"summary":"Update report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateReportTemplateInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"delete":{"tags":["Report Templates"],"summary":"Delete report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Report template not found"}}}},"/api/v1/agents":{"get":{"tags":["Agents"],"summary":"List agents","parameters":[{"schema":{"type":"string"},"required":false,"name":"modelProvider","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentResponse"}}}}}}},"post":{"tags":["Agents"],"summary":"Create or update agent (upsert)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"409":{"description":"Restored agent versions conflict with an active queue owner"}}}},"/api/v1/agents/{id}":{"get":{"tags":["Agents"],"summary":"Get agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"404":{"description":"Agent not found"}}},"put":{"tags":["Agents"],"summary":"Update agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAgentInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"}}},"delete":{"tags":["Agents"],"summary":"Delete agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Agent not found"}}}},"/api/v1/agents/{id}/versions":{"get":{"tags":["Agents"],"summary":"List agent versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}}}}},"404":{"description":"Agent not found"}}},"post":{"tags":["Agents"],"summary":"Register agent version (upsert)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterAgentVersionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent registration did not stabilize"}}}},"/api/v1/agents/{id}/versions/{agentVersion}":{"patch":{"tags":["Agents"],"summary":"Patch agent version","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"agentVersion","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchAgentVersionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Invalid status value"},"404":{"description":"Agent or version not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent activation did not stabilize"}}}},"/api/v1/models":{"get":{"tags":["Models"],"summary":"List models","parameters":[{"schema":{"type":"string"},"required":false,"name":"agentId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"provider","in":"query"},{"schema":{"type":"string","enum":["active","disappeared"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ModelResponse"}}}}}}}},"/api/v1/models/{id}":{"get":{"tags":["Models"],"summary":"Get model","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelResponse"}}}}}}},"/api/v1/models/sync":{"post":{"tags":["Models"],"summary":"Sync models from provider","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"agentId":{"type":"string"},"provider":{"type":"string"},"models":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"providerAvailableFrom":{"type":"string"},"providerEndOfLife":{"type":"string"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}}},"required":["id"]}},"scannedAt":{"type":"string"}},"required":["agentId","provider","models","scannedAt"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"added":{"type":"array","items":{"type":"string"}},"removed":{"type":"array","items":{"type":"string"}},"unchanged":{"type":"array","items":{"type":"string"}}},"required":["added","removed","unchanged"]}}}}}}},"/api/v1/mcp/servers":{"get":{"tags":["MCP Servers"],"summary":"List MCP servers","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"post":{"tags":["MCP Servers"],"summary":"Create MCP server","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}},"required":["_id","name","type"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"/api/v1/mcp/servers/{id}":{"get":{"tags":["MCP Servers"],"summary":"Get MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"put":{"tags":["MCP Servers"],"summary":"Update MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMcpServerInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"delete":{"tags":["MCP Servers"],"summary":"Delete MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/skills":{"get":{"tags":["Skills"],"summary":"List all skills","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"post":{"tags":["Skills"],"summary":"Create or import a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSkillInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"/api/v1/skills/search":{"get":{"tags":["Skills"],"summary":"Search skills (internal + external)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/search/external":{"get":{"tags":["Skills"],"summary":"Search external skills registry","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project."},"required":false,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/discover":{"get":{"tags":["Skills"],"summary":"Discover skills in a GitHub repository","parameters":[{"schema":{"type":"string"},"required":true,"name":"source","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillDiscoveryResult"}}}}},"400":{"description":"Missing or malformed source parameter"},"404":{"description":"Repository not found"},"502":{"description":"GitHub API error"}}}},"/api/v1/skills/{id}(*)/revisions":{"get":{"tags":["Skills"],"summary":"List skill revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}}},"404":{"description":"Skill not found"}}}},"/api/v1/skills/{id}(*)":{"get":{"tags":["Skills"],"summary":"Get skill by slug","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}},"404":{"description":"Skill not found"}}},"delete":{"tags":["Skills"],"summary":"Soft-delete a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Skill not found"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)/archive":{"get":{"tags":["Skill Revisions"],"summary":"Download skill revision archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Skill revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ref","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skill-revisions/{id}":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ID","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skills/{id}(*)/resolve":{"post":{"tags":["Skills"],"summary":"Trigger skill resolution","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebases":{"get":{"tags":["Codebases"],"summary":"List all codebases","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseResponse"}}}}}}},"post":{"tags":["Codebases"],"summary":"Create a codebase (archive codebases require the archive file)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input or missing archive"}}}},"/api/v1/codebases/{id}":{"get":{"tags":["Codebases"],"summary":"Get a codebase","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"patch":{"tags":["Codebases"],"summary":"Update a codebase","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCodebaseInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"delete":{"tags":["Codebases"],"summary":"Delete a codebase","responses":{"204":{"description":"Success"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebases/{id}/revisions":{"get":{"tags":["Codebases"],"summary":"List codebase revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}}},"404":{"description":"Codebase not found"}}},"post":{"tags":["Codebases"],"summary":"Resolve a new git codebase revision","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveCodebaseRevisionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"400":{"description":"Not a git codebase"},"404":{"description":"Codebase not found"},"502":{"description":"GitHub resolution failed"}}}},"/api/v1/codebases/{id}/upload":{"post":{"tags":["Codebases"],"summary":"Upload a codebase archive as a new revision","responses":{"201":{"description":"Success"},"400":{"description":"Missing archive or not an archive codebase"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebase-revisions/{id}/archive":{"get":{"tags":["Codebase Revisions"],"summary":"Download codebase revision archive","responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebase-revisions/{id}":{"get":{"tags":["Codebase Revisions"],"summary":"Get codebase revision by id","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources":{"get":{"tags":["Resources"],"summary":"List all resources","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceResponse"}}}}}}},"post":{"tags":["Resources"],"summary":"Create a resource and its first revision","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceInput"}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input"},"409":{"description":"Resource slug or revision ref already exists in this project"}}}},"/api/v1/resources/{id}":{"get":{"tags":["Resources"],"summary":"Get a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"patch":{"tags":["Resources"],"summary":"Update a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateResourceInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"delete":{"tags":["Resources"],"summary":"Delete a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Resource not found"}}}},"/api/v1/resources/{id}/revisions":{"get":{"tags":["Resource Revisions"],"summary":"List resource revisions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}}},"404":{"description":"Resource not found"}}},"post":{"tags":["Resource Revisions"],"summary":"Create a resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceRevisionInput"}}}},"responses":{"201":{"description":"Success"},"404":{"description":"Resource not found"},"409":{"description":"Resource revision ref already exists in this project"}}}},"/api/v1/resources/{id}/revisions/latest":{"get":{"tags":["Resource Revisions"],"summary":"Get the latest resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/{id}/revisions/{revisionNumber}":{"get":{"tags":["Resource Revisions"],"summary":"Get a resource revision by resource and revision number","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"revisionNumber","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/revisions/{id}":{"get":{"tags":["Resource Revisions"],"summary":"Get resource revision by id","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/extensions":{"get":{"tags":["Extensions"],"summary":"List all extensions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"post":{"tags":["Extensions"],"summary":"Create or import an extension","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExtensionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"/api/v1/extensions/search":{"get":{"tags":["Extensions"],"summary":"Search extensions (internal + marketplace)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/extensions/{id}":{"get":{"tags":["Extensions"],"summary":"Get extension by ID","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"put":{"tags":["Extensions"],"summary":"Update extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateExtensionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"delete":{"tags":["Extensions"],"summary":"Delete extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/extensions/{id}/versions":{"get":{"tags":["Extensions"],"summary":"List available versions for an extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"preRelease","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionVersionInfo"}}}}}}}},"/api/v1/insights":{"get":{"tags":["Insights"],"summary":"List insights","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"post":{"tags":["Insights"],"summary":"Create insight","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInsightInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"/api/v1/insights/search":{"get":{"tags":["Insights"],"summary":"Search insights","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}}},"/api/v1/insights/{id}":{"get":{"tags":["Insights"],"summary":"Get insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"put":{"tags":["Insights"],"summary":"Update insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateInsightInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"delete":{"tags":["Insights"],"summary":"Delete insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/upvote":{"post":{"tags":["Insights"],"summary":"Upvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/downvote":{"post":{"tags":["Insights"],"summary":"Downvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/block":{"post":{"tags":["Insights"],"summary":"Block insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/unblock":{"post":{"tags":["Insights"],"summary":"Unblock insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/reports":{"get":{"tags":["Insights"],"summary":"Get reports referencing insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Insight not found"}}}},"/api/v1/feature-flags":{"get":{"tags":["Feature Flags"],"summary":"List feature flags","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"/api/v1/feature-flags/{key}":{"put":{"tags":["Feature Flags"],"summary":"Update feature flag","parameters":[{"schema":{"type":"string"},"required":true,"name":"key","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeatureFlagInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"webhooks":{}} \ No newline at end of file From 229a43958e4742a0d3eabf16fdd8c40f2bde416b Mon Sep 17 00:00:00 2001 From: Cedric Vidal Date: Tue, 13 Oct 2026 23:50:33 +0200 Subject: [PATCH 11/15] feat: stream exact submission attempt logs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cbbd5eb-61c6-4e6a-bb38-c377be5b2163 --- .../openapi-snapshot.test.ts.snap | 34 ++++ apps/api/src/routes/submissions.test.ts | 41 ++++ apps/api/src/routes/submissions.ts | 190 ++++++++++++++++++ apps/api/src/utils/sse.ts | 21 +- .../shared/src/logging/log-publisher.test.ts | 1 + packages/shared/src/logging/log-publisher.ts | 6 +- .../src/queue/base-queue-processor.test.ts | 57 +++++- .../shared/src/queue/base-queue-processor.ts | 13 +- website/src/openapi/scope-openapi.json | 2 +- 9 files changed, 347 insertions(+), 18 deletions(-) diff --git a/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap b/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap index 350d502df..f7bf50f65 100644 --- a/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap +++ b/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap @@ -12371,6 +12371,40 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], }, }, + "/api/v1/submissions/{id}/runs/{submissionRunId}/logs": { + "get": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + }, + }, + { + "in": "path", + "name": "submissionRunId", + "required": true, + "schema": { + "type": "string", + }, + }, + ], + "responses": { + "200": { + "description": "Server-sent event stream of log entries", + }, + "404": { + "description": "Submission or submission attempt not found", + }, + }, + "summary": "Stream logs for a submission attempt (SSE)", + "tags": [ + "Submissions", + ], + }, + }, "/api/v1/task-prompts": { "get": { "parameters": [ diff --git a/apps/api/src/routes/submissions.test.ts b/apps/api/src/routes/submissions.test.ts index b37064230..8a2ed2340 100644 --- a/apps/api/src/routes/submissions.test.ts +++ b/apps/api/src/routes/submissions.test.ts @@ -30,6 +30,12 @@ function buildApp(input: { submission: SubmissionDocument; history?: SubmissionRunHistoryDocument[]; reports?: ReportDocument[]; + logs?: Array<{ + timestamp: string; + level: "info" | "warn" | "error" | "debug"; + source: string; + message: string; + }>; }) { let submission = structuredClone(input.submission); const services: ServiceHandlerDocument[] = [ @@ -115,6 +121,9 @@ function buildApp(input: { (input.history ?? []).find((run) => run._id === filter._id) ?? null, ), } as unknown as Collection, + blobStorage: { + getLogEvents: vi.fn(async () => input.logs ?? []), + }, } as unknown as RouteContext; ctx.app.use(express.json()); registerSubmissionsRoutes(ctx); @@ -312,4 +321,36 @@ describe("submission routes", () => { }, ]); }); + + it("replays logs for the exact submission attempt", async () => { + const response = await request( + buildApp({ + requests: [requestDocument], + submission: { + ...submissionDocument, + run: { + ...submissionDocument.run, + handlerStatus: { + "pp-readout": { status: "done", version: 1 }, + }, + }, + }, + logs: [ + { + timestamp: "2026-01-01T00:00:00.000Z", + level: "info", + source: "pp-readout", + message: "Submission report completed", + }, + ], + }), + ).get( + "/api/v1/submissions/submission-1/runs/submission-run-1/logs?fromStart=true", + ); + + expect(response.status).toBe(200); + expect(response.headers["content-type"]).toContain("text/event-stream"); + expect(response.text).toContain("Submission report completed"); + expect(response.text).toContain("event: done"); + }); }); diff --git a/apps/api/src/routes/submissions.ts b/apps/api/src/routes/submissions.ts index b46ce23ab..a593c05c3 100644 --- a/apps/api/src/routes/submissions.ts +++ b/apps/api/src/routes/submissions.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { z } from "zod"; +import type { Response } from "express"; import { SubmissionCoordinator, SubmissionListResponseSchema, @@ -16,6 +17,8 @@ import type { SubmissionDocument } from "../route-context.js"; import type { RouteContext } from "../route-context.js"; import { apiRoute } from "../openapi/api-route.js"; import { ProjectIdQuerySchema, getQueryProjectId } from "../utils/project-scope.js"; +import { subscribeClient, unsubscribeClient } from "../utils/sse.js"; +import type { SSEClient } from "../utils/sse.js"; function coordinator(ctx: RouteContext): SubmissionCoordinator { return new SubmissionCoordinator({ @@ -50,6 +53,159 @@ function retryBlocking(submission: SubmissionDocument) { })); } +async function streamSubmissionRunLogs( + ctx: RouteContext, + req: import("express").Request, + res: Response, + submission: SubmissionDocument, + submissionRunId: string, + fromStart: boolean, +): Promise { + const isCurrent = submission.run._id === submissionRunId; + const historical = isCurrent + ? undefined + : await ctx.submissionRunsCollection.findOne({ + _id: submissionRunId, + submissionId: submission._id, + }); + const attempt = isCurrent ? submission.run : historical; + if (!attempt) { + res.status(404).json({ error: "Submission attempt not found" }); + return; + } + + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + res.setHeader("X-Accel-Buffering", "no"); + res.flushHeaders(); + + if (fromStart) { + try { + const pastLogs = await ctx.blobStorage.getLogEvents( + submission._id, + submissionRunId, + ); + for (const log of pastLogs) { + res.write(`data: ${JSON.stringify(log)}\n\n`); + } + } catch (error) { + console.error( + `Failed to replay logs for submission ${submission._id}/${submissionRunId}:`, + error, + ); + res.write( + `event: error\ndata: ${JSON.stringify({ message: "Cannot connect to log storage" })}\n\n`, + ); + res.end(); + return; + } + } + + const readiness = deriveSubmissionReadiness(attempt); + if ( + !isCurrent || + readiness === "done" || + readiness === "failed" || + readiness === "blocked" + ) { + res.write( + `event: done\ndata: ${JSON.stringify({ status: readiness })}\n\n`, + ); + res.end(); + return; + } + + let cleaned = false; + let redisSubscribed = false; + let changeStream: ReturnType | null = + null; + const heartbeat = setInterval(() => { + if (!cleaned) res.write(`:\n\n`); + }, 30_000); + + const client: SSEClient = { + res, + accepts: (event) => event.data?.runId === submissionRunId, + cleanup: () => { + if (cleaned) return; + cleaned = true; + clearInterval(heartbeat); + if (changeStream) { + changeStream.close().catch((error: unknown) => { + console.error("Error closing submission log change stream:", error); + }); + } + if (redisSubscribed) unsubscribeClient(submission._id, client); + res.end(); + }, + }; + + if (process.env.REDIS_HOST) { + try { + await subscribeClient(submission._id, client); + redisSubscribed = true; + } catch (error) { + console.error( + `Redis subscription failed for submission ${submission._id}:`, + error, + ); + } + } + + try { + changeStream = ctx.submissionCollection.watch( + [ + { + $match: { + "documentKey._id": submission._id, + operationType: "update", + }, + }, + ], + { fullDocument: "updateLookup" }, + ); + changeStream.on("change", (change: { + operationType?: string; + fullDocument?: SubmissionDocument; + }) => { + const updated = change.fullDocument; + if (change.operationType !== "update" || !updated) return; + if (updated.run._id !== submissionRunId) { + res.write( + `event: done\ndata: ${JSON.stringify({ status: "superseded" })}\n\n`, + ); + client.cleanup(); + return; + } + const updatedReadiness = deriveSubmissionReadiness(updated.run); + if ( + updatedReadiness === "done" || + updatedReadiness === "failed" || + updatedReadiness === "blocked" + ) { + res.write( + `event: done\ndata: ${JSON.stringify({ status: updatedReadiness })}\n\n`, + ); + client.cleanup(); + } + }); + changeStream.on("error", (error: unknown) => { + console.error( + `Submission log change stream failed for ${submission._id}:`, + error, + ); + }); + } catch (error) { + console.error( + `Failed to create submission log change stream for ${submission._id}:`, + error, + ); + } + + req.on("close", () => client.cleanup()); +} + async function toResponse(ctx: RouteContext, submission: SubmissionDocument) { const requestDocs = await ctx.requestCollection .find({ @@ -124,6 +280,40 @@ export function registerSubmissionsRoutes(ctx: RouteContext): void { }, }); + apiRoute(ctx.app, ctx.registry, { + method: "get", + path: "/api/v1/submissions/:id/runs/:submissionRunId/logs", + tags: ["Submissions"], + summary: "Stream logs for a submission attempt (SSE)", + params: z.object({ + id: z.string(), + submissionRunId: z.string(), + }), + response: z.any(), + rawResponse: true, + responseDescription: "Server-sent event stream of log entries", + errorResponses: { + 404: { description: "Submission or submission attempt not found" }, + }, + handler: async (req, res) => { + const submission = await ctx.submissionCollection.findOne({ + _id: req.params.id, + }); + if (!submission) { + res.status(404).json({ error: "Submission not found" }); + return; + } + await streamSubmissionRunLogs( + ctx, + req, + res, + submission, + req.params.submissionRunId, + req.query.fromStart === "true", + ); + }, + }); + apiRoute(ctx.app, ctx.registry, { method: "get", path: "/api/v1/submissions/:id/reports", diff --git a/apps/api/src/utils/sse.ts b/apps/api/src/utils/sse.ts index 2e073662d..093faa2ff 100644 --- a/apps/api/src/utils/sse.ts +++ b/apps/api/src/utils/sse.ts @@ -13,6 +13,7 @@ export type SSEClient = { res: Response; cleanup: () => void; onActivity?: () => void; + accepts?: (event: LogEvent) => boolean; }; const sseClients: Map> = new Map(); @@ -50,20 +51,24 @@ function getOrCreateSubscriber(): InstanceType { sharedSubscriber.on("message", (channel: string, message: string) => { const requestId = channel.replace("logs:", ""); const clients = sseClients.get(requestId); + let logEvent: LogEvent | undefined; + try { + logEvent = JSON.parse(message) as LogEvent; + } catch { + // Preserve legacy forwarding for clients that do not filter events. + } if (clients) { for (const client of clients) { + if (client.accepts && (!logEvent || !client.accepts(logEvent))) { + continue; + } client.res.write(`data: ${message}\n\n`); client.onActivity?.(); // Check for completion - try { - const logEvent = JSON.parse(message) as LogEvent; - if (logEvent.data?.final === true) { - client.res.write(`event: done\ndata: {"status":"done"}\n\n`); - client.cleanup(); - } - } catch { - // Ignore parse errors + if (logEvent?.data?.final === true) { + client.res.write(`event: done\ndata: {"status":"done"}\n\n`); + client.cleanup(); } } } diff --git a/packages/shared/src/logging/log-publisher.test.ts b/packages/shared/src/logging/log-publisher.test.ts index d332ec8ad..bd8808355 100644 --- a/packages/shared/src/logging/log-publisher.test.ts +++ b/packages/shared/src/logging/log-publisher.test.ts @@ -68,6 +68,7 @@ describe("LogPublisher", () => { expect(parsed.message).toBe("hello"); expect(parsed.level).toBe("info"); expect(parsed.source).toBe("test-worker"); + expect(parsed.data.runId).toBe("attempt-1"); }); it("persists the log event via appendLogEvent under the per-attempt path", async () => { diff --git a/packages/shared/src/logging/log-publisher.ts b/packages/shared/src/logging/log-publisher.ts index 88c0dbe68..ef85f9184 100644 --- a/packages/shared/src/logging/log-publisher.ts +++ b/packages/shared/src/logging/log-publisher.ts @@ -87,9 +87,13 @@ export class LogPublisher { // The Redis channel is keyed on requestId so SSE listeners receive logs // for whichever attempt is currently running. const channel = `logs:${requestId}`; + const redisLogEvent: LogEvent = { + ...logEvent, + data: { ...data, runId }, + }; try { await this.redisBreaker.execute(() => - this.redis.publish(channel, JSON.stringify(logEvent)) + this.redis.publish(channel, JSON.stringify(redisLogEvent)) ); } catch (error) { // Silently ignore - circuit breaker handles logging state changes diff --git a/packages/shared/src/queue/base-queue-processor.test.ts b/packages/shared/src/queue/base-queue-processor.test.ts index 00d485be4..a8ebf9d71 100644 --- a/packages/shared/src/queue/base-queue-processor.test.ts +++ b/packages/shared/src/queue/base-queue-processor.test.ts @@ -162,8 +162,10 @@ describe("BaseQueueProcessor visibility heartbeat lifecycle", () => { vi.useRealTimers(); }); - function makeMessage(): DequeuedMessageItem { - const payload = Buffer.from(JSON.stringify({ requestId: "doc-1" })).toString("base64"); + function makeMessage( + messagePayload: Record = { requestId: "doc-1" }, + ): DequeuedMessageItem { + const payload = Buffer.from(JSON.stringify(messagePayload)).toString("base64"); return { messageId: "msg-1", popReceipt: "receipt-0", @@ -171,7 +173,16 @@ describe("BaseQueueProcessor visibility heartbeat lifecycle", () => { } as unknown as DequeuedMessageItem; } - function setupProcessor(handleRequest: (heartbeat: VisibilityHeartbeat) => Promise | void) { + function setupProcessor( + handleRequest: ( + heartbeat: VisibilityHeartbeat, + log: ( + level: LogEvent["level"], + message: string, + data?: Record, + ) => Promise, + ) => Promise | void, + ) { class HBProcessor extends BaseQueueProcessor<{ _id: string }> { public capturedHeartbeat: VisibilityHeartbeat | undefined; public handleRequestCalls = 0; @@ -181,11 +192,24 @@ describe("BaseQueueProcessor visibility heartbeat lifecycle", () => { _doc: { _id: string }, _message: DequeuedMessageItem, heartbeat: VisibilityHeartbeat, + log: ( + level: LogEvent["level"], + message: string, + data?: Record, + ) => Promise, ): Promise { this.capturedHeartbeat = heartbeat; this.popReceiptAtHandleStart = heartbeat.popReceipt; this.handleRequestCalls++; - await handleRequest(heartbeat); + await handleRequest(heartbeat, log); + } + + protected override extractDocumentId( + payload: Record, + ): string { + return typeof payload.submissionId === "string" + ? payload.submissionId + : super.extractDocumentId(payload); } protected override async cleanup(): Promise { @@ -203,11 +227,12 @@ describe("BaseQueueProcessor visibility heartbeat lifecycle", () => { findOne: vi.fn().mockResolvedValue({ _id: "doc-1", run: { _id: "run-1" } }), updateOne: vi.fn().mockResolvedValue({ matchedCount: 1 }), }; - (proc as any).logPublisher = { + const logPublisher = { publish: vi.fn().mockResolvedValue(undefined), evictRun: vi.fn(), }; - return { proc, updateMessage, deleteMessage }; + (proc as any).logPublisher = logPublisher; + return { proc, updateMessage, deleteMessage, logPublisher }; } it("starts the heartbeat before handleRequest runs", async () => { @@ -223,6 +248,26 @@ describe("BaseQueueProcessor visibility heartbeat lifecycle", () => { expect(updateMessage).not.toHaveBeenCalled(); }); + it("persists submission worker logs under the exact submission attempt", async () => { + const { proc, logPublisher } = setupProcessor(async (_heartbeat, log) => { + await log("info", "Submission processing started"); + }); + await (proc as any).processMessage( + makeMessage({ + submissionId: "doc-1", + submissionRunId: "submission-run-1", + }), + ); + + expect(logPublisher.publish).toHaveBeenCalledWith( + "doc-1", + "submission-run-1", + "info", + "Submission processing started", + undefined, + ); + }); + it("the heartbeat passed to handleRequest exposes the live pop receipt", async () => { let observedAfterTick: string | undefined; const { proc } = setupProcessor(async (heartbeat) => { diff --git a/packages/shared/src/queue/base-queue-processor.ts b/packages/shared/src/queue/base-queue-processor.ts index 08bc35d51..5a0d3772b 100644 --- a/packages/shared/src/queue/base-queue-processor.ts +++ b/packages/shared/src/queue/base-queue-processor.ts @@ -242,8 +242,13 @@ export abstract class BaseQueueProcessor Date: Tue, 13 Oct 2026 23:50:37 +0200 Subject: [PATCH 12/15] feat: refine submission report experience Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cbbd5eb-61c6-4e6a-bb38-c377be5b2163 --- .../src/components/SubmissionLink.stories.tsx | 32 + .../src/components/SubmissionLink.test.tsx | 24 + apps/portal/src/components/SubmissionLink.tsx | 33 + .../SubmissionStatusCard.stories.tsx | 4 +- .../components/SubmissionStatusCard.test.tsx | 9 +- .../src/components/SubmissionStatusCard.tsx | 37 +- .../src/components/ui/stories.play.test.tsx | 10 +- apps/portal/src/lib/api.ts | 32 +- apps/portal/src/pages/RunDetail.tsx | 19 +- apps/portal/src/pages/RunsList.tsx | 23 +- .../src/pages/SubmissionDetail.test.tsx | 311 +++++- apps/portal/src/pages/SubmissionDetail.tsx | 939 ++++++++++++++---- apps/portal/src/types.ts | 18 + 13 files changed, 1232 insertions(+), 259 deletions(-) create mode 100644 apps/portal/src/components/SubmissionLink.stories.tsx create mode 100644 apps/portal/src/components/SubmissionLink.test.tsx create mode 100644 apps/portal/src/components/SubmissionLink.tsx diff --git a/apps/portal/src/components/SubmissionLink.stories.tsx b/apps/portal/src/components/SubmissionLink.stories.tsx new file mode 100644 index 000000000..0df3ea96b --- /dev/null +++ b/apps/portal/src/components/SubmissionLink.stories.tsx @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect } from "storybook/test"; +import { SubmissionLink } from "./SubmissionLink"; + +const meta = { + title: "Submissions/SubmissionLink", + component: SubmissionLink, + args: { + submissionId: "submission-12345678", + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Labeled: Story = { + play: async ({ canvas }) => { + await expect( + canvas.getByRole("link", { + name: "Open submission submission-12345678", + }), + ).toBeVisible(); + await expect(canvas.getByText("Submission")).toBeVisible(); + }, +}; + +export const Compact: Story = { + args: { compact: true }, +}; diff --git a/apps/portal/src/components/SubmissionLink.test.tsx b/apps/portal/src/components/SubmissionLink.test.tsx new file mode 100644 index 000000000..358959353 --- /dev/null +++ b/apps/portal/src/components/SubmissionLink.test.tsx @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// @vitest-environment happy-dom +import { render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { describe, expect, it } from "vitest"; +import { SubmissionLink } from "./SubmissionLink"; + +describe("SubmissionLink", () => { + it("provides a labeled, accessible path to Submission Detail", () => { + render( + + + , + ); + + const link = screen.getByRole("link", { + name: "Open submission submission-123", + }); + expect(link.getAttribute("href")).toBe("/submissions/submission-123"); + expect(screen.getByText("Submission")).toBeDefined(); + }); +}); diff --git a/apps/portal/src/components/SubmissionLink.tsx b/apps/portal/src/components/SubmissionLink.tsx new file mode 100644 index 000000000..243bc5268 --- /dev/null +++ b/apps/portal/src/components/SubmissionLink.tsx @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Layers3 } from "lucide-react"; +import { Link } from "react-router-dom"; +import { ShortId } from "@/components/ShortId"; +import { cn } from "@/lib/utils"; + +export function SubmissionLink({ + submissionId, + className, + compact = false, +}: { + submissionId: string; + className?: string; + compact?: boolean; +}) { + return ( + event.stopPropagation()} + > + + {!compact && Submission} + + + ); +} diff --git a/apps/portal/src/components/SubmissionStatusCard.stories.tsx b/apps/portal/src/components/SubmissionStatusCard.stories.tsx index 8809eb169..bf08bb7ef 100644 --- a/apps/portal/src/components/SubmissionStatusCard.stories.tsx +++ b/apps/portal/src/components/SubmissionStatusCard.stories.tsx @@ -41,7 +41,7 @@ const submission: Submission = { { requestId: "request-2", runId: "run-2", - handlerId: "pp-observations", + handlerId: "security-scan", state: "ready", }, ], @@ -95,7 +95,7 @@ export const Blocked: Story = { { requestId: "request-2", runId: "run-2", - handlerId: "pp-observations", + handlerId: "security-scan", reason: "dependency-failed", }, ], diff --git a/apps/portal/src/components/SubmissionStatusCard.test.tsx b/apps/portal/src/components/SubmissionStatusCard.test.tsx index 27576956e..4a1477b9f 100644 --- a/apps/portal/src/components/SubmissionStatusCard.test.tsx +++ b/apps/portal/src/components/SubmissionStatusCard.test.tsx @@ -10,7 +10,7 @@ import { SubmissionStatusCard } from "./SubmissionStatusCard"; afterEach(cleanup); describe("SubmissionStatusCard", () => { - it("renders readiness and exact observation progress", () => { + it("renders readiness and arbitrary dependency progress", () => { const submission: Submission = { submissionId: "submission-1", projectId: "project-1", @@ -45,7 +45,7 @@ describe("SubmissionStatusCard", () => { { requestId: "request-2", runId: "run-2", - handlerId: "pp-observations", + handlerId: "security-scan", state: "failed", }, ], @@ -53,7 +53,7 @@ describe("SubmissionStatusCard", () => { { requestId: "request-2", runId: "run-2", - handlerId: "pp-observations", + handlerId: "security-scan", reason: "dependency-failed", }, ], @@ -64,7 +64,10 @@ describe("SubmissionStatusCard", () => { expect(screen.getByText("Report generation")).toBeDefined(); expect(screen.getByText("Generation blocked")).toBeDefined(); expect(screen.getByText("3")).toBeDefined(); + expect(screen.getByText("Dependencies ready")).toBeDefined(); expect(screen.getByText("1/2")).toBeDefined(); + expect(screen.getByText("Observations 1/1")).toBeDefined(); + expect(screen.getByText("Security Scan 0/1")).toBeDefined(); }); it.each([ diff --git a/apps/portal/src/components/SubmissionStatusCard.tsx b/apps/portal/src/components/SubmissionStatusCard.tsx index a75bf7bc4..b227d11be 100644 --- a/apps/portal/src/components/SubmissionStatusCard.tsx +++ b/apps/portal/src/components/SubmissionStatusCard.tsx @@ -26,6 +26,15 @@ const readinessLabel: Record = { failed: "Generation failed", }; +export function submissionHandlerLabel(handlerId: string): string { + return handlerId + .replace(/^pp-/, "") + .split(/[-_]+/) + .filter(Boolean) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); +} + export function SubmissionStatusCard({ submission, }: { @@ -34,6 +43,21 @@ export function SubmissionStatusCard({ const ready = submission.dependencyProjection.filter( (dependency) => dependency.state === "ready", ).length; + const dependencyProgress = Array.from( + submission.dependencyProjection.reduce( + (progress, dependency) => { + const current = progress.get(dependency.handlerId) ?? { + ready: 0, + total: 0, + }; + current.total += 1; + if (dependency.state === "ready") current.ready += 1; + progress.set(dependency.handlerId, current); + return progress; + }, + new Map(), + ), + ); return ( @@ -48,15 +72,24 @@ export function SubmissionStatusCard({
{submission.attemptNumber}
-
Observations ready
+
Dependencies ready
- {ready}/{submission.requests.length} + {ready}/{submission.dependencyProjection.length}
Blocking runs
{submission.blocking.length}
+ {dependencyProgress.length > 0 && ( +
+ {dependencyProgress.map(([handlerId, progress]) => ( + + {submissionHandlerLabel(handlerId)} {progress.ready}/{progress.total} + + ))} +
+ )}
); diff --git a/apps/portal/src/components/ui/stories.play.test.tsx b/apps/portal/src/components/ui/stories.play.test.tsx index 6ed917da2..7ca2290cc 100644 --- a/apps/portal/src/components/ui/stories.play.test.tsx +++ b/apps/portal/src/components/ui/stories.play.test.tsx @@ -5,6 +5,7 @@ import { describe, it, afterEach } from "vitest"; import { cleanup, render, within } from "@testing-library/react"; import { composeStories } from "@storybook/react-vite"; +import { MemoryRouter } from "react-router-dom"; import * as alert from "./alert.stories"; import * as alertDialog from "./alert-dialog.stories"; @@ -24,11 +25,12 @@ import * as tabs from "./tabs.stories"; import * as textarea from "./textarea.stories"; import * as tooltip from "./tooltip.stories"; import * as sonner from "./sonner.stories"; +import * as submissionLink from "../SubmissionLink.stories"; const modules: Record> = { alert, alertDialog, checkbox, dialog, dropdownMenu, label, popover, scrollArea, select, separator, sheet, skeleton, switch: sw, table, tabs, textarea, - tooltip, sonner, + tooltip, sonner, submissionLink, }; afterEach(() => cleanup()); @@ -41,7 +43,11 @@ for (const [name, mod] of Object.entries(modules)) { it(`${storyName} renders and play passes`, async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const S = Story as any; - const { container } = render(); + const { container } = render( + + + , + ); // The story play functions only depend on `canvas` (userEvent/screen/expect // are imported directly in the story files). Invoke the raw play with a // canvas bound to the rendered container to avoid needing the vitest addon. diff --git a/apps/portal/src/lib/api.ts b/apps/portal/src/lib/api.ts index 6e57eca81..2099ee14c 100644 --- a/apps/portal/src/lib/api.ts +++ b/apps/portal/src/lib/api.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import type { Run, RunState, CriteriaDocument, CriteriaGraphData, GeneratePromptResponse, AnalysisResponse, PromptFeatureDocument, Report, BulkReportStatus, BulkReportSummary, ReportTemplate, ReportTrigger, ReportTemplateSystemPrompt, KeyDocument, KeyValidationResult, CreateKeyRequest, UpdateKeyRequest, CodingAgent, AgentVersion, McpServerDocument, CreateMcpServerRequest, UpdateMcpServerRequest, BulkResubmitOverrides, Insight, InsightWithReference, TaskPrompt, TaskPromptFeatureExtractionResult, Model, FeatureFlag, SkillDocument, SkillSearchResult, SkillDiscoveryResult, SkillRevisionDocument, CodebaseDocument, CodebaseRevisionDocument, CodebaseSourceType, ResourceDocument, ResourceRevisionDocument, ResourceBindingSpec, CreateResourceBody, CreateResourceRevisionBody, ExtensionDocument, ExtensionSearchResult, ExtensionVersionInfo, MdpResponse, AccountDocument, CreateAccountRequest, UpdateAccountRequest, ProfileWithVersion, ProfileVersionDocument, ProfileDocument, RunGroup, RunFacetsResponse, CursorPaginatedResponse, IterationOp, GateConfig, GateId, PromptType, RunSortField, RunSortDir, CriterionKind, TaxonomyElementId, CriterionSubject, Submission } from "@/types"; +import type { Run, RunState, CriteriaDocument, CriteriaGraphData, GeneratePromptResponse, AnalysisResponse, PromptFeatureDocument, Report, BulkReportStatus, BulkReportSummary, ReportTemplate, ReportTrigger, ReportTemplateSystemPrompt, KeyDocument, KeyValidationResult, CreateKeyRequest, UpdateKeyRequest, CodingAgent, AgentVersion, BulkResubmitOverrides, Insight, InsightWithReference, TaskPrompt, TaskPromptFeatureExtractionResult, Model, FeatureFlag, SkillDocument, SkillSearchResult, SkillDiscoveryResult, SkillRevisionDocument, CodebaseDocument, CodebaseRevisionDocument, CodebaseSourceType, ResourceDocument, ResourceRevisionDocument, ResourceBindingSpec, CreateResourceBody, CreateResourceRevisionBody, ExtensionDocument, ExtensionSearchResult, ExtensionVersionInfo, MdpResponse, AccountDocument, CreateAccountRequest, UpdateAccountRequest, ProfileWithVersion, ProfileVersionDocument, ProfileDocument, RunGroup, RunFacetsResponse, CursorPaginatedResponse, IterationOp, GateConfig, GateId, PromptType, RunSortField, RunSortDir, CriterionKind, TaxonomyElementId, CriterionSubject, Submission, SubmissionRunAttempt } from "@/types"; import type { Project, CreateProjectRequest, UpdateProjectRequest } from "@/types"; import { qs } from "./url"; @@ -247,19 +247,27 @@ export const api = { request(`/submissions/${id}`), getSubmissionRuns: (id: string): Promise<{ - runs: Array<{ - _id: string; - attemptNumber: number; - requests: Array<{ requestId: string; runId: string }>; - createdAt: string; - report?: { reportId?: string; url?: string; updatedAt?: string }; - handlerStatus?: Submission["handlerProgress"]; - supersededAt?: string; - supersededBySubmissionRunId?: string; - supersededReason?: "request-vector-changed" | "manual-retry"; - }>; + runs: SubmissionRunAttempt[]; }> => request(`/submissions/${id}/runs`), + getSubmissionRun: ( + id: string, + submissionRunId: string, + ): Promise => + request( + `/submissions/${encodeURIComponent(id)}/runs/${encodeURIComponent(submissionRunId)}`, + ), + + getSubmissionReports: (id: string): Promise => + request(`/submissions/${encodeURIComponent(id)}/reports`), + + submissionRunLogsUrl: ( + id: string, + submissionRunId: string, + fromStart = true, + ): string => + `${BASE}/submissions/${encodeURIComponent(id)}/runs/${encodeURIComponent(submissionRunId)}/logs?fromStart=${fromStart}`, + retrySubmission: ( id: string, submissionRunId: string, diff --git a/apps/portal/src/pages/RunDetail.tsx b/apps/portal/src/pages/RunDetail.tsx index 9eaed350f..946233ae4 100644 --- a/apps/portal/src/pages/RunDetail.tsx +++ b/apps/portal/src/pages/RunDetail.tsx @@ -424,6 +424,7 @@ export function RunDetail() { const queryClient = useQueryClient(); const [searchParams] = useSearchParams(); const selectedRunId = searchParams.get("runId"); + const fromSubmissionId = searchParams.get("fromSubmission"); const [copied, setCopied] = useState(false); const [reportsView, setReportsView] = useState<"grid" | "list">("grid"); const [reportsFilter, setReportsFilter] = useState<"latest" | "all">("latest"); @@ -705,9 +706,10 @@ export function RunDetail() { if (error || !run) { return (
- +
@@ -804,9 +806,18 @@ export function RunDetail() {
{/* Back link + header */}
- + diff --git a/apps/portal/src/pages/RunsList.tsx b/apps/portal/src/pages/RunsList.tsx index c7257fc97..aeeb2f4a1 100644 --- a/apps/portal/src/pages/RunsList.tsx +++ b/apps/portal/src/pages/RunsList.tsx @@ -28,6 +28,7 @@ import { StatusBadge, OutcomeBadge } from "@/components/StatusBadge"; import { TaskPromptBadge } from "@/components/TaskPromptBadge"; import { AgentBadge } from "@/components/AgentBadge"; import { ShortId } from "@/components/ShortId"; +import { SubmissionLink } from "@/components/SubmissionLink"; import { ListLayout, FilterRail, @@ -1346,16 +1347,11 @@ export function RunsList() { { id: "submission", header: "Submission", - width: "120px", + width: "190px", hidden: columnVisibility.isHidden("submission"), cell: (r) => r.submissionId ? ( - event.stopPropagation()} - > - - + ) : ( — ), @@ -2511,6 +2507,12 @@ export function RunsList() { switch (column.id) { case "submission": + if ( + groupBy === "submissionId" && + groupKey !== "no-submission" + ) { + return ; + } return distinct("submissions", (r) => r.submissionId); case "task": return distinct("tasks", (r) => r.scenario?.task); @@ -2689,12 +2691,7 @@ export function RunsList() {
{groupBy === "submissionId" && groupKey !== "no-submission" && ( - - View submission - + )} {runs.length} run{runs.length !== 1 ? "s" : ""} diff --git a/apps/portal/src/pages/SubmissionDetail.test.tsx b/apps/portal/src/pages/SubmissionDetail.test.tsx index 14f39774b..9481a6ff2 100644 --- a/apps/portal/src/pages/SubmissionDetail.test.tsx +++ b/apps/portal/src/pages/SubmissionDetail.test.tsx @@ -3,15 +3,20 @@ // @vitest-environment happy-dom import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { cleanup, render, screen } from "@testing-library/react"; +import { cleanup, render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { MemoryRouter, Route, Routes } from "react-router-dom"; import { afterEach, describe, expect, it, vi } from "vitest"; -import type { Report, Submission } from "@/types"; +import type { + Report, + Submission, + SubmissionRunAttempt, +} from "@/types"; const submission: Submission = { submissionId: "submission-1", projectId: "project-1", - requestIds: ["request-1"], + requestIds: ["request-1", "request-2"], createdAt: "2026-01-01T00:00:00.000Z", submissionRunId: "submission-run-2", attemptNumber: 2, @@ -19,90 +24,298 @@ const submission: Submission = { requests: [ { requestId: "request-1", - runId: "run-1", + runId: "run-current-1", task: "Create an API", status: "done", outcome: "succeeded", workerType: "coder-acp-copilot", + model: "gpt-5.4", + }, + { + requestId: "request-2", + runId: "run-current-2", + task: "Document the API", + status: "done", + outcome: "succeeded", + workerType: "coder-acp-copilot", + model: "gpt-5.4", }, ], dependencyProjection: [ { requestId: "request-1", - runId: "run-1", + runId: "run-current-1", handlerId: "pp-observations", state: "ready", }, + { + requestId: "request-2", + runId: "run-current-2", + handlerId: "security-scan", + state: "ready", + }, ], blocking: [], - report: { reportId: "report-1" }, + handlerProgress: { + "pp-readout": { status: "done", version: 1 }, + }, + report: { reportId: "report-current" }, }; -const history = { - runs: [ - { - _id: "submission-run-2", - attemptNumber: 2, - requests: [{ requestId: "request-1", runId: "run-1" }], - report: { reportId: "report-1" }, - createdAt: "2026-01-01T00:00:00.000Z", - }, +const currentAttempt: SubmissionRunAttempt = { + _id: "submission-run-2", + attemptNumber: 2, + vectorFingerprint: "vector-current", + requests: [ + { requestId: "request-1", runId: "run-current-1" }, + { requestId: "request-2", runId: "run-current-2" }, ], + dependencyProjection: submission.dependencyProjection, + blocking: [], + handlerStatus: submission.handlerProgress, + report: { reportId: "report-current" }, + createdAt: "2026-01-02T00:00:00.000Z", }; -const report: Report = { - _id: "report-1", - id: "report-1", - submissionId: "submission-1", - submissionRunId: "submission-run-2", - status: "completed", - content: "# Generated report", - structuredArtifact: { - schemaId: "scope.submission-readout", - schemaVersion: "1.0", - contentType: "application/json", - url: "reports/report-1/structured.json", +const historicalAttempt: SubmissionRunAttempt = { + _id: "submission-run-1", + attemptNumber: 1, + vectorFingerprint: "vector-historical", + requests: [ + { requestId: "request-1", runId: "run-historical-1" }, + { requestId: "request-2", runId: "run-historical-2" }, + ], + dependencyProjection: [ + { + requestId: "request-1", + runId: "run-historical-1", + handlerId: "pp-observations", + state: "ready", + }, + { + requestId: "request-2", + runId: "run-historical-2", + handlerId: "security-scan", + state: "ready", + }, + ], + blocking: [], + handlerStatus: { + "pp-readout": { status: "done", version: 1 }, }, + report: { reportId: "report-historical" }, createdAt: "2026-01-01T00:00:00.000Z", + supersededAt: "2026-01-02T00:00:00.000Z", + supersededBySubmissionRunId: "submission-run-2", + supersededReason: "request-vector-changed", }; +const reports: Report[] = [ + { + _id: "report-current", + id: "report-current", + submissionId: "submission-1", + submissionRunId: "submission-run-2", + status: "completed", + content: "# Current generated report", + reporter: { + id: "pp-readout", + name: "Submission Readout", + gitHash: "abc", + model: "gpt-5.4", + agentId: "copilot", + agentVersion: "1.0.0", + }, + structuredArtifact: { + schemaId: "scope.submission-readout", + schemaVersion: "1.0", + contentType: "application/json", + url: "reports/report-current/structured.json", + }, + createdAt: "2026-01-02T00:00:00.000Z", + }, + { + _id: "report-historical", + id: "report-historical", + submissionId: "submission-1", + submissionRunId: "submission-run-1", + status: "completed", + content: "# Historical generated report", + createdAt: "2026-01-01T00:00:00.000Z", + }, +]; + vi.mock("@/lib/api", () => ({ api: { getSubmission: vi.fn(async () => submission), - getSubmissionRuns: vi.fn(async () => history), - getReport: vi.fn(async () => report), + getSubmissionRuns: vi.fn(async () => ({ + runs: [currentAttempt, historicalAttempt], + })), + getSubmissionReports: vi.fn(async () => reports), retrySubmission: vi.fn(), - reportStructuredUrl: vi.fn(() => "/api/v1/reports/report-1/structured"), + reportStructuredUrl: vi.fn( + (id: string) => `/api/v1/reports/${id}/structured`, + ), + submissionRunLogsUrl: vi.fn( + (_id: string, attemptId: string) => + `/api/v1/submissions/submission-1/runs/${attemptId}/logs`, + ), }, })); +vi.mock("@/hooks/use-log-stream", () => ({ + useLogStream: vi.fn(() => ({ + logs: [], + isConnected: false, + isDone: true, + error: null, + clear: vi.fn(), + })), +})); + import { SubmissionDetail } from "./SubmissionDetail"; +import { api } from "@/lib/api"; afterEach(() => { cleanup(); vi.clearAllMocks(); }); -describe("SubmissionDetail report terminology", () => { - it("uses generic report language for the current result and history", async () => { - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - render( - - - - } /> - - - , +function renderDetail(initialEntry = "/submissions/submission-1") { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return render( + + + + } /> + Run detail destination
} /> + + + , + ); +} + +describe("SubmissionDetail", () => { + it("defaults to Reports and supports multiple current and historical reports", async () => { + renderDetail(); + + expect( + await screen.findByRole("heading", { name: "Current generated report" }), + ).toBeDefined(); + expect( + screen.getByRole("tab", { name: "Reports (2)" }).getAttribute("data-state"), + ).toBe("active"); + expect(screen.getAllByText("Current").length).toBeGreaterThan(0); + expect(screen.getByText("Historical")).toBeDefined(); + expect( + screen.getByRole("link", { name: "Structured JSON" }).getAttribute("href"), + ).toBe("/api/v1/reports/report-current/structured"); + + const historicalReportButton = screen + .getAllByRole("button") + .find( + (button) => + button.textContent?.includes("Historical") && + button.textContent?.includes("completed"), + ); + expect(historicalReportButton).toBeDefined(); + await userEvent.click(historicalReportButton!); + expect( + await screen.findByRole("heading", { + name: "Historical generated report", + }), + ).toBeDefined(); + expect( + screen.getByText(/Viewing attempt 1 \(historical and read-only\)/), + ).toBeDefined(); + }); + + it("renders the immutable member vector with the shared table and run navigation", async () => { + renderDetail(); + await screen.findByRole("heading", { name: "Current generated report" }); + + await userEvent.click(screen.getByRole("tab", { name: "Runs (2)" })); + + expect( + screen.getByRole("heading", { name: "Immutable member run vector" }), + ).toBeDefined(); + expect(screen.getByRole("columnheader", { name: "Request" })).toBeDefined(); + expect( + screen.getByRole("columnheader", { name: "Exact run attempt" }), + ).toBeDefined(); + expect( + screen.getByRole("columnheader", { name: "Dependencies" }), + ).toBeDefined(); + const requestLink = screen + .getAllByRole("link") + .find((link) => link.getAttribute("href")?.includes("run-current-1")); + expect(requestLink).toBeDefined(); + expect(requestLink!.getAttribute("href")).toContain( + "runId=run-current-1", + ); + expect(requestLink!.getAttribute("href")).toContain( + "fromSubmission=submission-1", + ); + }); + + it("switches attempts through the picker and scopes Processing to history", async () => { + renderDetail("/submissions/submission-1?tab=processing"); + await screen.findByText("Report ready"); + + const picker = screen.getByRole("combobox", { name: "Submission attempt" }); + await userEvent.click(picker); + const listbox = await screen.findByRole("listbox"); + await userEvent.click( + within(listbox).getByText(/Attempt 1 · Superseded/), ); - expect(await screen.findByText("Current report")).toBeDefined(); - expect(screen.getByText("Report generation")).toBeDefined(); - expect(screen.getByText("Report ready")).toBeDefined(); - expect(screen.getByRole("button", { name: "Retry report generation" })).toBeDefined(); - expect(await screen.findByRole("link", { name: "View report" })).toBeDefined(); - expect(await screen.findByRole("heading", { name: "Generated report" })).toBeDefined(); + expect( + await screen.findByText(/Viewing attempt 1 \(historical and read-only\)/), + ).toBeDefined(); + expect(screen.getByText("Dependencies ready")).toBeDefined(); + expect(screen.getByText("Observations 1/1")).toBeDefined(); + expect(screen.getByText("Security Scan 1/1")).toBeDefined(); + expect( + screen.getByRole("heading", { name: "Attempt processing logs" }), + ).toBeDefined(); + }); + + it("shows current attempt progress when only historical reports exist", async () => { + vi.mocked(api.getSubmission).mockResolvedValueOnce({ + ...submission, + readiness: "processing", + report: undefined, + handlerProgress: { + "pp-readout": { status: "processing", version: 1 }, + }, + }); + vi.mocked(api.getSubmissionRuns).mockResolvedValueOnce({ + runs: [ + { + ...currentAttempt, + report: undefined, + handlerStatus: { + "pp-readout": { status: "processing", version: 1 }, + }, + }, + historicalAttempt, + ], + }); + vi.mocked(api.getSubmissionReports).mockResolvedValueOnce([ + reports[1]!, + ]); + + renderDetail(); + + expect( + await screen.findByRole("tab", { name: "Reports (1)" }), + ).toBeDefined(); + expect(screen.getByText("Report not available")).toBeDefined(); + expect( + screen.getByText("A report is being generated for this attempt."), + ).toBeDefined(); + expect(screen.getByText("Historical")).toBeDefined(); }); }); diff --git a/apps/portal/src/pages/SubmissionDetail.tsx b/apps/portal/src/pages/SubmissionDetail.tsx index 1753adebe..0a5e0e658 100644 --- a/apps/portal/src/pages/SubmissionDetail.tsx +++ b/apps/portal/src/pages/SubmissionDetail.tsx @@ -1,21 +1,329 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { + useCallback, + useMemo, +} from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, Download, FileText, RotateCcw } from "lucide-react"; -import { Link, useParams } from "react-router-dom"; +import { + ArrowLeft, + ArrowRight, + Clock, + Download, + FileText, + ListChecks, + RotateCcw, + ScrollText, +} from "lucide-react"; +import { + Link, + useNavigate, + useParams, + useSearchParams, +} from "react-router-dom"; import { toast } from "sonner"; -import { SubmissionStatusCard } from "@/components/SubmissionStatusCard"; +import { LogViewer } from "@/components/LogViewer"; import { MarkdownRenderer } from "@/components/MarkdownRenderer"; import { ShortId } from "@/components/ShortId"; +import { + SubmissionStatusCard, + submissionHandlerLabel, +} from "@/components/SubmissionStatusCard"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs"; +import { + DataTable, + type DataTableColumn, +} from "@/components/list-layout"; +import { useLogStream } from "@/hooks/use-log-stream"; import { api } from "@/lib/api"; -import { formatDate } from "@/lib/utils"; +import { cn, formatDate } from "@/lib/utils"; +import type { + Report, + Submission, + SubmissionReadiness, + SubmissionRequestSummary, + SubmissionRunAttempt, +} from "@/types"; + +type SubmissionTab = "reports" | "runs" | "processing"; + +interface SubmissionMemberRow extends SubmissionRequestSummary { + dependencyStates: Array<{ + handlerId: string; + state: string; + }>; + historical: boolean; +} + +function deriveAttemptReadiness( + attempt: SubmissionRunAttempt, +): SubmissionReadiness { + if (attempt.blocking.length > 0) return "blocked"; + const handlers = Object.values(attempt.handlerStatus ?? {}); + if (handlers.some((handler) => handler.status === "failed")) return "failed"; + if (handlers.some((handler) => handler.status === "processing")) { + return "processing"; + } + if (handlers.length > 0 && handlers.every((handler) => handler.status === "done")) { + return "done"; + } + if ( + attempt.dependencyProjection.length > 0 && + attempt.dependencyProjection.every((dependency) => dependency.state === "ready") + ) { + return "ready"; + } + return "waiting"; +} + +function reportId(report: Report): string { + return report.id || report._id; +} + +function attemptLabel( + attempt: SubmissionRunAttempt, + currentAttemptId: string, +): string { + const state = + attempt._id === currentAttemptId + ? "Current" + : attempt.supersededAt + ? "Superseded" + : "Historical"; + return `Attempt ${attempt.attemptNumber} · ${state} · ${formatDate(attempt.createdAt)}`; +} + +function SubmissionAttemptPicker({ + attempts, + currentAttemptId, + selectedAttemptId, + onSelect, +}: { + attempts: SubmissionRunAttempt[]; + currentAttemptId: string; + selectedAttemptId: string; + onSelect: (attemptId: string) => void; +}) { + return ( +
+ + +
+ ); +} + +function SubmissionReports({ + reports, + selectedReport, + currentReportId, + selectedAttempt, + readiness, + onSelect, + onShowProcessing, +}: { + reports: Report[]; + selectedReport?: Report; + currentReportId?: string; + selectedAttempt: SubmissionRunAttempt; + readiness: SubmissionReadiness; + onSelect: (report: Report) => void; + onShowProcessing: () => void; +}) { + const unavailableMessage = + readiness === "blocked" + ? "Report generation is blocked by one or more dependencies." + : readiness === "failed" + ? "Report generation failed and can be retried from the current attempt." + : readiness === "processing" + ? "A report is being generated for this attempt." + : "No report has been generated for this attempt yet."; + const unavailable = ( + + +
+

Report not available

+

+ {unavailableMessage} +

+
+ +
+
+ ); + if (reports.length === 0) return unavailable; + + return ( +
+ + + Reports + + + {reports.map((report) => { + const id = reportId(report); + const isCurrent = id === currentReportId; + const isSelected = id === (selectedReport && reportId(selectedReport)); + const isSelectedAttempt = + report.submissionRunId === selectedAttempt._id; + return ( + + ); + })} + + + + {selectedReport ? ( + + <> + +
+
+
+ Generated report + + {reportId(selectedReport) === currentReportId + ? "Current" + : "Historical"} + + {selectedReport.status} +
+
+ + Report + + {selectedReport.submissionRunId && ( + + Attempt{" "} + + + )} + Created {formatDate(selectedReport.createdAt)} + {selectedReport.updatedAt && ( + Updated {formatDate(selectedReport.updatedAt)} + )} +
+ {selectedReport.reporter && ( +

+ {selectedReport.reporter.name} · {selectedReport.reporter.model} ·{" "} + {selectedReport.reporter.agentId}@ + {selectedReport.reporter.agentVersion} +

+ )} +
+ {selectedReport.structuredArtifact && ( + + )} +
+
+ + {selectedReport.content ? ( +
+ + {selectedReport.content} + +
+ ) : selectedReport.error ? ( +

{selectedReport.error}

+ ) : ( +

+ This report does not have rendered Markdown content. +

+ )} +
+ +
+ ) : ( + unavailable + )} +
+ ); +} export function SubmissionDetail() { const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); const queryClient = useQueryClient(); const submissionQuery = useQuery({ queryKey: ["submission", id], @@ -23,11 +331,35 @@ export function SubmissionDetail() { enabled: Boolean(id), refetchInterval: (query) => { const readiness = query.state.data?.readiness; - return readiness === "done" || readiness === "failed" || readiness === "blocked" + return readiness === "done" || + readiness === "failed" || + readiness === "blocked" ? false : 5_000; }, }); + const attemptsQuery = useQuery({ + queryKey: ["submission-runs", id], + queryFn: () => api.getSubmissionRuns(id!), + enabled: Boolean(id), + refetchInterval: + submissionQuery.data?.readiness === "done" || + submissionQuery.data?.readiness === "failed" || + submissionQuery.data?.readiness === "blocked" + ? false + : 5_000, + }); + const reportsQuery = useQuery({ + queryKey: ["submission-reports", id], + queryFn: () => api.getSubmissionReports(id!), + enabled: Boolean(id), + refetchInterval: + submissionQuery.data?.readiness === "done" || + submissionQuery.data?.readiness === "failed" || + submissionQuery.data?.readiness === "blocked" + ? false + : 5_000, + }); const retry = useMutation({ mutationFn: () => api.retrySubmission( @@ -37,37 +369,264 @@ export function SubmissionDetail() { onSuccess: () => { toast.success("Report generation restarted"); void queryClient.invalidateQueries({ queryKey: ["submission", id] }); + void queryClient.invalidateQueries({ queryKey: ["submission-runs", id] }); + void queryClient.invalidateQueries({ queryKey: ["submission-reports", id] }); }, onError: (error) => { toast.error(error instanceof Error ? error.message : "Retry failed"); }, }); - const historyQuery = useQuery({ - queryKey: ["submission-runs", id], - queryFn: () => api.getSubmissionRuns(id!), - enabled: Boolean(id), - }); - const currentReportId = submissionQuery.data?.report?.reportId; - const reportQuery = useQuery({ - queryKey: ["report", currentReportId], - queryFn: () => api.getReport(currentReportId!), - enabled: Boolean(currentReportId), + + const submission = submissionQuery.data; + const attempts = attemptsQuery.data?.runs ?? []; + const selectedAttemptId = + searchParams.get("attempt") ?? submission?.submissionRunId ?? ""; + const selectedAttempt = + attempts.find((attempt) => attempt._id === selectedAttemptId) ?? + attempts.find((attempt) => attempt._id === submission?.submissionRunId); + const isCurrentAttempt = + selectedAttempt?._id === submission?.submissionRunId; + const readiness = + submission && selectedAttempt + ? isCurrentAttempt + ? submission.readiness + : deriveAttemptReadiness(selectedAttempt) + : "waiting"; + const reports = reportsQuery.data ?? []; + const requestedTab = searchParams.get("tab"); + const activeTab: SubmissionTab = + requestedTab === "reports" || + requestedTab === "runs" || + requestedTab === "processing" + ? requestedTab + : submission?.report?.reportId || reports.length > 0 + ? "reports" + : "runs"; + + const requestedReportId = searchParams.get("report"); + const defaultReportId = + selectedAttempt?.report?.reportId ?? + (isCurrentAttempt ? submission?.report?.reportId : undefined); + const selectedReport = + reports.find((report) => reportId(report) === requestedReportId) ?? + reports.find((report) => reportId(report) === defaultReportId) ?? + reports.find((report) => report.submissionRunId === selectedAttempt?._id); + + const memberRows = useMemo(() => { + if (!submission || !selectedAttempt) return []; + const summaryById = new Map( + submission.requests.map((request) => [request.requestId, request]), + ); + return selectedAttempt.requests.map((reference) => { + const summary = summaryById.get(reference.requestId); + return { + requestId: reference.requestId, + runId: reference.runId, + task: summary?.task ?? "Unknown task", + status: + isCurrentAttempt && summary?.runId === reference.runId + ? summary.status + : "historical", + ...(isCurrentAttempt && + summary?.runId === reference.runId && + summary.outcome + ? { outcome: summary.outcome } + : {}), + workerType: summary?.workerType ?? "unknown", + ...(summary?.model ? { model: summary.model } : {}), + ...(summary?.profileId ? { profileId: summary.profileId } : {}), + dependencyStates: selectedAttempt.dependencyProjection + .filter( + (dependency) => + dependency.requestId === reference.requestId && + dependency.runId === reference.runId, + ) + .map((dependency) => ({ + handlerId: dependency.handlerId, + state: dependency.state, + })), + historical: !isCurrentAttempt, + }; + }); + }, [isCurrentAttempt, selectedAttempt, submission]); + + const memberColumns = useMemo[]>( + () => [ + { + id: "request", + header: "Request", + width: "130px", + cell: (row) => ( + event.stopPropagation()} + > + + + ), + }, + { + id: "run", + header: "Exact run attempt", + width: "150px", + cell: (row) => , + }, + { + id: "task", + header: "Task", + cell: (row) => {row.task}, + }, + { + id: "status", + header: "Status", + width: "110px", + cell: (row) => ( + + {row.status} + + ), + }, + { + id: "outcome", + header: "Outcome", + width: "110px", + cell: (row) => + row.outcome ? ( + + {row.outcome} + + ) : ( + — + ), + }, + { + id: "worker", + header: "Worker / model", + cell: (row) => ( +
+
{row.workerType}
+ {row.model && ( +
{row.model}
+ )} +
+ ), + }, + { + id: "dependencies", + header: "Dependencies", + cell: (row) => + row.dependencyStates.length > 0 ? ( +
+ {row.dependencyStates.map((dependency) => ( + + {submissionHandlerLabel(dependency.handlerId)} ·{" "} + {dependency.state} + + ))} +
+ ) : ( + — + ), + }, + { + id: "open", + header: "", + width: "44px", + align: "right", + cell: () => , + }, + ], + [submission?.submissionId], + ); + + const logUrlBuilder = useCallback( + (_attemptId: string, fromStart: boolean) => + api.submissionRunLogsUrl(id!, selectedAttemptId, fromStart), + [id, selectedAttemptId], + ); + const logStream = useLogStream({ + id: selectedAttemptId, + enabled: Boolean(id && selectedAttemptId && activeTab === "processing"), + fromStart: true, + attemptNumber: selectedAttempt?.attemptNumber, + urlBuilder: logUrlBuilder, }); - if (submissionQuery.isLoading) { - return
Loading submission…
; + if (submissionQuery.isLoading || attemptsQuery.isLoading) { + return ( +
+ Loading submission… +
+ ); } - if (submissionQuery.isError || !submissionQuery.data) { + if (submissionQuery.isError || !submission) { return (

Submission could not be loaded.

); } - const submission = submissionQuery.data; + if (!selectedAttempt) { + return ( +
+

Submission attempt could not be loaded.

+
+ ); + } + + const statusSubmission: Submission = { + ...submission, + submissionRunId: selectedAttempt._id, + attemptNumber: selectedAttempt.attemptNumber, + readiness, + dependencyProjection: selectedAttempt.dependencyProjection, + blocking: selectedAttempt.blocking, + handlerProgress: selectedAttempt.handlerStatus, + report: selectedAttempt.report, + requests: memberRows, + }; + + const setTab = (tab: SubmissionTab) => { + const next = new URLSearchParams(searchParams); + next.set("tab", tab); + setSearchParams(next, { replace: true }); + }; + const selectAttempt = (attemptId: string) => { + const next = new URLSearchParams(searchParams); + if (attemptId === submission.submissionRunId) next.delete("attempt"); + else next.set("attempt", attemptId); + next.delete("report"); + setSearchParams(next, { replace: true }); + }; + const selectReport = (report: Report) => { + const next = new URLSearchParams(searchParams); + next.set("tab", "reports"); + next.set("report", reportId(report)); + if ( + report.submissionRunId && + report.submissionRunId !== submission.submissionRunId + ) { + next.set("attempt", report.submissionRunId); + } else { + next.delete("attempt"); + } + setSearchParams(next, { replace: true }); + }; + return ( -
-
+
+

- Created {formatDate(submission.createdAt)} + Created {formatDate(submission.createdAt)} · {submission.requests.length}{" "} + member run{submission.requests.length === 1 ? "" : "s"}

- +
+ + +
- - - - - Current report - {reportQuery.data?.structuredArtifact && ( - - )} - - - {reportQuery.data?.content ? ( -
- - {reportQuery.data.content} - -
- ) : submission.readiness === "blocked" ? ( -

- Report generation is blocked by observation dependencies. -

- ) : submission.readiness === "failed" ? ( -

- Report generation failed and can be retried. -

- ) : ( -

- {submission.readiness === "done" - ? "The current report is not available." - : "Waiting for the exact run vector and observations to be ready."} -

- )} -
-
- - {submission.blocking.length > 0 && ( - - - Blocking dependencies - - - {submission.blocking.map((blocking) => ( -
- - {blocking.requestId.slice(0, 8)} - - {blocking.reason} - - {blocking.runId} - -
- ))} -
-
+ {!isCurrentAttempt && ( +
+ + + Viewing attempt {selectedAttempt.attemptNumber} (historical and + read-only). + + +
)} - - - Runs - - - {submission.requests.map((request) => { - const dependency = submission.dependencyProjection.find( - (item) => - item.requestId === request.requestId && item.runId === request.runId, - ); - return ( - -
-
- - {request.task} -
-
- {request.requestId} · {request.runId} + setTab(value as SubmissionTab)}> + + + + Reports {reports.length > 0 ? `(${reports.length})` : ""} + + + + Runs ({memberRows.length}) + + + + Processing + + + + + setTab("processing")} + /> + + + + + + + Immutable member run vector + +

+ Each row preserves the exact request and run-attempt identity for + submission attempt {selectedAttempt.attemptNumber}. +

+
+ + `${row.requestId}:${row.runId}`} + density="compact" + onRowClick={(row) => + navigate( + `/runs/${row.requestId}?runId=${row.runId}&fromSubmission=${submission.submissionId}`, + ) + } + emptyState={ +
+ This attempt has no member runs.
-
-
- {request.status} - - {dependency?.state ?? "waiting"} - -
- - ); - })} - - + } + /> + + + - - - Submission history - - - {(historyQuery.data?.runs ?? []).map((run) => ( -
-
-
Attempt {run.attemptNumber}
-
- {run._id} · {run.requests.length} run{run.requests.length === 1 ? "" : "s"} -
-
-
- - {run.supersededAt ? "superseded" : "current"} - - {run.report?.reportId && ( - - )} -
-
- ))} -
-
+ + run {blocking.runId} + + {blocking.error && ( + + {blocking.error} + + )} +
+ ))} +
+
+ )} + + {selectedAttempt.handlerStatus && + Object.keys(selectedAttempt.handlerStatus).length > 0 && ( + + + Processing stages + + + {Object.entries(selectedAttempt.handlerStatus).map( + ([handlerId, status]) => ( + + {submissionHandlerLabel(handlerId)} · {status.status} + + ), + )} + + + )} + + + + Attempt processing logs + + + + + + +
); } diff --git a/apps/portal/src/types.ts b/apps/portal/src/types.ts index 82ca84832..56bee9cff 100644 --- a/apps/portal/src/types.ts +++ b/apps/portal/src/types.ts @@ -267,6 +267,24 @@ export interface SubmissionRequestSummary { profileId?: string; } +export interface SubmissionRunAttempt { + _id: string; + attemptNumber: number; + vectorFingerprint: string; + requests: Array<{ requestId: string; runId: string }>; + createdAt: string; + updatedAt?: string; + startedAt?: string; + finishedAt?: string; + dependencyProjection: SubmissionDependencyProjection[]; + handlerStatus?: Submission["handlerProgress"]; + blocking: SubmissionBlockingDetail[]; + report?: { reportId?: string; url?: string; updatedAt?: string }; + supersededAt?: string; + supersededBySubmissionRunId?: string; + supersededReason?: "request-vector-changed" | "manual-retry"; +} + export interface Submission { submissionId: string; projectId: string; From deaf47d7efb158916e9fe9e289d2a4d25b673cbf Mon Sep 17 00:00:00 2001 From: Cedric Vidal Date: Tue, 13 Oct 2026 23:50:41 +0200 Subject: [PATCH 13/15] docs: describe attempt-scoped submission reports Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cbbd5eb-61c6-4e6a-bb38-c377be5b2163 --- docs/architecture/app-design.md | 9 +++++++++ docs/architecture/post-processing.md | 13 ++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/architecture/app-design.md b/docs/architecture/app-design.md index 29c521879..d8682677d 100644 --- a/docs/architecture/app-design.md +++ b/docs/architecture/app-design.md @@ -546,6 +546,7 @@ The API exposes: - `GET /api/v1/submissions/:id/reports` - `GET /api/v1/submissions/:id/runs` - `GET /api/v1/submissions/:id/runs/:submissionRunId` +- `GET /api/v1/submissions/:id/runs/:submissionRunId/logs` - `POST /api/v1/submissions/:id/retry` Responses expose readiness, exact blocking request/run pairs, handler progress, @@ -554,6 +555,14 @@ pointer without queue names or scheduler claim fields. Portal submit flows navigate to Submission Detail, and the CLI mirrors list/get/history/retry capabilities under `scope submission`. +Submission Detail uses an attempt-scoped view rather than rendering every +attempt at once. Its attempt picker switches the exact immutable request/run +vector, dependency projection, processing logs, and report subject together. +Horizontal Reports, Runs, and Processing tabs keep member runs one click away +while a long report is open. Reports use a master/detail selector over the +append-only submission report collection; only the report referenced by the +embedded current attempt is labeled current. + ## Runs List Query API `GET /api/v1/requests` powers the Portal **Runs** list and `scope run list`. All diff --git a/docs/architecture/post-processing.md b/docs/architecture/post-processing.md index ea5f0632c..285531042 100644 --- a/docs/architecture/post-processing.md +++ b/docs/architecture/post-processing.md @@ -80,7 +80,11 @@ Gate criteria are always `subject: "iteration"` (the API rejects `"run"` for gat - **Runs exactly once per run:** the scheduler dispatches `pp-observations` a single time (it never sets `message.iteration`), so the run-subject branch needs no idempotency guard. 5. Stamps `run.handlerStatus["pp-observations"] = { status: "done", version: N }` and notifies the scheduler. -Dimension grouping is **derived at read time** by joining each result's `criterionId` → criterion `taxonomyElementId` (the three R&A Readout dimensions; unset = unclassified). It never feeds back to the agent and never affects pass/fail. (#1053 later extends this worker with a richer scorecard artifact.) +Dimension grouping is **derived at read time** by joining each result's +`criterionId` → criterion `taxonomyElementId` (the three R&A Readout +dimensions; unset = unclassified). It never feeds back to the agent and never +affects pass/fail. Submission report synthesis consumes these classifications +after every exact-vector dependency is ready. ## Handler Interface @@ -257,6 +261,13 @@ report artifact proxy. Internal handler, queue, and schema identifiers retain `readout` because they identify this specific synthesis implementation rather than the public resource. +Submission-handler logs are persisted under the exact submission attempt and +streamed through +`GET /api/v1/submissions/:id/runs/:submissionRunId/logs`. Queue processors use +the message's `submissionRunId` as the log attempt key, so superseded attempts +remain independently replayable and cannot inherit logs from a newer current +attempt. + To **add or change a handler**, edit its `handler.yaml` (bump `version`, adjust `dependsOn`, etc.) — no code or migration changes are needed for registration. ## Report Triggering on DAG Drain From 65aa8818a9037017660ccccf0ac3e725864d1fcd Mon Sep 17 00:00:00 2001 From: Cedric Vidal Date: Wed, 14 Oct 2026 00:54:20 +0200 Subject: [PATCH 14/15] fix: scope submission reports by type Replace report-version history with attempt-scoped horizontal report-type tabs and deterministic newest-per-type selection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cbbd5eb-61c6-4e6a-bb38-c377be5b2163 --- .../components/SubmissionReports.stories.tsx | 113 +++++++++ .../src/components/ui/stories.play.test.tsx | 3 +- .../src/pages/SubmissionDetail.test.tsx | 119 ++++++++-- apps/portal/src/pages/SubmissionDetail.tsx | 218 +++++++++++------- docs/architecture/app-design.md | 9 +- 5 files changed, 353 insertions(+), 109 deletions(-) create mode 100644 apps/portal/src/components/SubmissionReports.stories.tsx diff --git a/apps/portal/src/components/SubmissionReports.stories.tsx b/apps/portal/src/components/SubmissionReports.stories.tsx new file mode 100644 index 000000000..d3762acbf --- /dev/null +++ b/apps/portal/src/components/SubmissionReports.stories.tsx @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, userEvent, within } from "storybook/test"; +import { + SubmissionReports, + reportId, +} from "@/pages/SubmissionDetail"; +import type { + Report, + SubmissionRunAttempt, +} from "@/types"; + +const attempt: SubmissionRunAttempt = { + _id: "submission-run-current", + attemptNumber: 2, + vectorFingerprint: "vector-current", + requests: [{ requestId: "request-1", runId: "run-1" }], + dependencyProjection: [], + blocking: [], + handlerStatus: { + "pp-readout": { status: "done", version: 1 }, + }, + report: { reportId: "report-readout" }, + createdAt: "2026-01-02T00:00:00.000Z", +}; + +const readoutReport: Report = { + _id: "report-readout", + id: "report-readout", + submissionId: "submission-1", + submissionRunId: attempt._id, + status: "completed", + content: "# Submission report\n\nFive runs completed successfully.", + reporter: { + id: "pp-readout", + name: "Submission Readout", + gitHash: "abc", + model: "gpt-5.4", + agentId: "copilot", + agentVersion: "1.0.0", + }, + structuredArtifact: { + schemaId: "scope.submission-readout", + schemaVersion: "1.0", + contentType: "application/json", + url: "reports/report-readout/structured.json", + }, + createdAt: "2026-01-02T00:00:00.000Z", +}; + +const templateReport: Report = { + _id: "report-executive", + id: "report-executive", + submissionId: "submission-1", + submissionRunId: attempt._id, + templateId: "executive-summary", + status: "completed", + content: "# Executive summary\n\nA concise template-based view.", + createdAt: "2026-01-02T01:00:00.000Z", +}; + +function ReportTypesStory({ reports }: { reports: Report[] }) { + const [selected, setSelected] = useState(reports[0]); + return ( + undefined} + /> + ); +} + +const meta = { + title: "Submissions/Submission Reports", + component: ReportTypesStory, + parameters: { layout: "padded" }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const OneReportType: Story = { + args: { reports: [readoutReport] }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect( + canvas.getByRole("tab", { name: "Submission Readout" }), + ).toBeVisible(); + await expect( + canvas.getByRole("heading", { name: "Submission report" }), + ).toBeVisible(); + }, +}; + +export const MultipleReportTypes: Story = { + args: { reports: [readoutReport, templateReport] }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click( + canvas.getByRole("tab", { name: "Executive Summary" }), + ); + await expect( + canvas.getByRole("heading", { name: "Executive summary" }), + ).toBeVisible(); + }, +}; diff --git a/apps/portal/src/components/ui/stories.play.test.tsx b/apps/portal/src/components/ui/stories.play.test.tsx index 7ca2290cc..5137d9b6d 100644 --- a/apps/portal/src/components/ui/stories.play.test.tsx +++ b/apps/portal/src/components/ui/stories.play.test.tsx @@ -26,11 +26,12 @@ import * as textarea from "./textarea.stories"; import * as tooltip from "./tooltip.stories"; import * as sonner from "./sonner.stories"; import * as submissionLink from "../SubmissionLink.stories"; +import * as submissionReports from "../SubmissionReports.stories"; const modules: Record> = { alert, alertDialog, checkbox, dialog, dropdownMenu, label, popover, scrollArea, select, separator, sheet, skeleton, switch: sw, table, tabs, textarea, - tooltip, sonner, submissionLink, + tooltip, sonner, submissionLink, submissionReports, }; afterEach(() => cleanup()); diff --git a/apps/portal/src/pages/SubmissionDetail.test.tsx b/apps/portal/src/pages/SubmissionDetail.test.tsx index 9481a6ff2..e2c825ccf 100644 --- a/apps/portal/src/pages/SubmissionDetail.test.tsx +++ b/apps/portal/src/pages/SubmissionDetail.test.tsx @@ -134,6 +134,33 @@ const reports: Report[] = [ }, createdAt: "2026-01-02T00:00:00.000Z", }, + { + _id: "report-current-older-duplicate", + id: "report-current-older-duplicate", + submissionId: "submission-1", + submissionRunId: "submission-run-2", + status: "completed", + content: "# Stale duplicate report", + reporter: { + id: "pp-readout", + name: "Submission Readout", + gitHash: "abc", + model: "gpt-5.4", + agentId: "copilot", + agentVersion: "1.0.0", + }, + createdAt: "2026-01-01T12:00:00.000Z", + }, + { + _id: "report-template-current", + id: "report-template-current", + submissionId: "submission-1", + submissionRunId: "submission-run-2", + templateId: "executive-summary", + status: "completed", + content: "# Executive summary report", + createdAt: "2026-01-02T01:00:00.000Z", + }, { _id: "report-historical", id: "report-historical", @@ -141,6 +168,14 @@ const reports: Report[] = [ submissionRunId: "submission-run-1", status: "completed", content: "# Historical generated report", + reporter: { + id: "pp-readout", + name: "Submission Readout", + gitHash: "abc", + model: "gpt-5.4", + agentId: "copilot", + agentVersion: "1.0.0", + }, createdAt: "2026-01-01T00:00:00.000Z", }, ]; @@ -198,7 +233,7 @@ function renderDetail(initialEntry = "/submissions/submission-1") { } describe("SubmissionDetail", () => { - it("defaults to Reports and supports multiple current and historical reports", async () => { + it("defaults to Reports and shows attempt-scoped report types", async () => { renderDetail(); expect( @@ -207,29 +242,35 @@ describe("SubmissionDetail", () => { expect( screen.getByRole("tab", { name: "Reports (2)" }).getAttribute("data-state"), ).toBe("active"); - expect(screen.getAllByText("Current").length).toBeGreaterThan(0); - expect(screen.getByText("Historical")).toBeDefined(); + expect( + screen.getByRole("tab", { name: "Submission Readout" }), + ).toBeDefined(); + expect( + screen.getByRole("tab", { name: "Executive Summary" }), + ).toBeDefined(); + expect(screen.getByText("Current")).toBeDefined(); expect( screen.getByRole("link", { name: "Structured JSON" }).getAttribute("href"), ).toBe("/api/v1/reports/report-current/structured"); - const historicalReportButton = screen - .getAllByRole("button") - .find( - (button) => - button.textContent?.includes("Historical") && - button.textContent?.includes("completed"), - ); - expect(historicalReportButton).toBeDefined(); - await userEvent.click(historicalReportButton!); + await userEvent.click( + screen.getByRole("tab", { name: "Executive Summary" }), + ); expect( - await screen.findByRole("heading", { - name: "Historical generated report", - }), + await screen.findByRole("heading", { name: "Executive summary report" }), ).toBeDefined(); + expect(screen.getByText("This attempt")).toBeDefined(); + }); + + it("selects the newest report when one attempt has duplicate report types", async () => { + renderDetail(); + expect( - screen.getByText(/Viewing attempt 1 \(historical and read-only\)/), + await screen.findByRole("heading", { name: "Current generated report" }), ).toBeDefined(); + expect( + screen.getByRole("link", { name: "Structured JSON" }).getAttribute("href"), + ).toBe("/api/v1/reports/report-current/structured"); }); it("renders the immutable member vector with the shared table and run navigation", async () => { @@ -282,6 +323,45 @@ describe("SubmissionDetail", () => { ).toBeDefined(); }); + it("scopes report types to the selected historical attempt", async () => { + renderDetail(); + await screen.findByRole("heading", { name: "Current generated report" }); + + const picker = screen.getByRole("combobox", { name: "Submission attempt" }); + await userEvent.click(picker); + const listbox = await screen.findByRole("listbox"); + await userEvent.click(within(listbox).getByText(/Attempt 1 · Superseded/)); + + expect( + await screen.findByRole("heading", { + name: "Historical generated report", + }), + ).toBeDefined(); + expect( + screen.getByRole("tab", { name: "Reports (1)" }), + ).toBeDefined(); + expect( + screen.getByRole("tab", { name: "Submission Readout" }), + ).toBeDefined(); + expect(screen.getByText("Historical")).toBeDefined(); + }); + + it("renders one report-type tab for a single template-less report", async () => { + vi.mocked(api.getSubmissionReports).mockResolvedValueOnce([reports[0]!]); + + renderDetail(); + + expect( + await screen.findByRole("heading", { name: "Current generated report" }), + ).toBeDefined(); + expect( + screen.getByRole("tab", { name: "Reports (1)" }), + ).toBeDefined(); + expect( + screen.getByRole("tab", { name: "Submission Readout" }), + ).toBeDefined(); + }); + it("shows current attempt progress when only historical reports exist", async () => { vi.mocked(api.getSubmission).mockResolvedValueOnce({ ...submission, @@ -303,19 +383,16 @@ describe("SubmissionDetail", () => { historicalAttempt, ], }); - vi.mocked(api.getSubmissionReports).mockResolvedValueOnce([ - reports[1]!, - ]); + vi.mocked(api.getSubmissionReports).mockResolvedValueOnce([reports[3]!]); renderDetail(); expect( - await screen.findByRole("tab", { name: "Reports (1)" }), + await screen.findByRole("tab", { name: "Reports" }), ).toBeDefined(); expect(screen.getByText("Report not available")).toBeDefined(); expect( screen.getByText("A report is being generated for this attempt."), ).toBeDefined(); - expect(screen.getByText("Historical")).toBeDefined(); }); }); diff --git a/apps/portal/src/pages/SubmissionDetail.tsx b/apps/portal/src/pages/SubmissionDetail.tsx index 0a5e0e658..1b1a048ca 100644 --- a/apps/portal/src/pages/SubmissionDetail.tsx +++ b/apps/portal/src/pages/SubmissionDetail.tsx @@ -57,7 +57,7 @@ import { } from "@/components/list-layout"; import { useLogStream } from "@/hooks/use-log-stream"; import { api } from "@/lib/api"; -import { cn, formatDate } from "@/lib/utils"; +import { formatDate } from "@/lib/utils"; import type { Report, Submission, @@ -97,10 +97,46 @@ function deriveAttemptReadiness( return "waiting"; } -function reportId(report: Report): string { +export function reportId(report: Report): string { return report.id || report._id; } +export function submissionReportTypeKey(report: Report): string { + if (report.templateId) return `template:${report.templateId}`; + if (report.reporter?.id) return `reporter:${report.reporter.id}`; + return `report:${reportId(report)}`; +} + +export function submissionReportTypeLabel(report: Report): string { + if (report.templateId) return submissionHandlerLabel(report.templateId); + if (report.reporter?.name.trim()) return report.reporter.name; + if (report.reporter?.id) return submissionHandlerLabel(report.reporter.id); + return "Report"; +} + +export function newestSubmissionReportsByType( + reports: Report[], +): Array<{ key: string; label: string; report: Report }> { + const reportsByType = new Map(); + for (const report of reports) { + const key = submissionReportTypeKey(report); + const existing = reportsByType.get(key); + if ( + !existing || + report.createdAt > existing.createdAt || + (report.createdAt === existing.createdAt && + reportId(report).localeCompare(reportId(existing)) > 0) + ) { + reportsByType.set(key, report); + } + } + return Array.from(reportsByType, ([key, report]) => ({ + key, + label: submissionReportTypeLabel(report), + report, + })).sort((left, right) => left.label.localeCompare(right.label)); +} + function attemptLabel( attempt: SubmissionRunAttempt, currentAttemptId: string, @@ -149,11 +185,11 @@ function SubmissionAttemptPicker({ ); } -function SubmissionReports({ +export function SubmissionReports({ reports, selectedReport, currentReportId, - selectedAttempt, + isCurrentAttempt, readiness, onSelect, onShowProcessing, @@ -161,7 +197,7 @@ function SubmissionReports({ reports: Report[]; selectedReport?: Report; currentReportId?: string; - selectedAttempt: SubmissionRunAttempt; + isCurrentAttempt: boolean; readiness: SubmissionReadiness; onSelect: (report: Report) => void; onShowProcessing: () => void; @@ -191,52 +227,34 @@ function SubmissionReports({ ); if (reports.length === 0) return unavailable; + const reportTypes = newestSubmissionReportsByType(reports); + const selectedType = + reportTypes.find( + (option) => + selectedReport && + submissionReportTypeKey(selectedReport) === option.key, + ) ?? reportTypes[0]; + const selectedTypeReport = selectedType?.report; return ( -
- - - Reports - - - {reports.map((report) => { - const id = reportId(report); - const isCurrent = id === currentReportId; - const isSelected = id === (selectedReport && reportId(selectedReport)); - const isSelectedAttempt = - report.submissionRunId === selectedAttempt._id; - return ( - - ); - })} - - +
+ { + const option = reportTypes.find((candidate) => candidate.key === key); + if (option) onSelect(option.report); + }} + > + + {reportTypes.map((option) => ( + + {option.label} + + ))} + + - {selectedReport ? ( + {selectedTypeReport ? ( <> @@ -246,47 +264,56 @@ function SubmissionReports({ Generated report - {reportId(selectedReport) === currentReportId + {reportId(selectedTypeReport) === currentReportId ? "Current" - : "Historical"} + : isCurrentAttempt + ? "This attempt" + : "Historical"} - {selectedReport.status} + {selectedTypeReport.status}
- Report + Report{" "} + - {selectedReport.submissionRunId && ( + {selectedTypeReport.submissionRunId && ( Attempt{" "} )} - Created {formatDate(selectedReport.createdAt)} - {selectedReport.updatedAt && ( - Updated {formatDate(selectedReport.updatedAt)} + Created {formatDate(selectedTypeReport.createdAt)} + {selectedTypeReport.updatedAt && ( + Updated {formatDate(selectedTypeReport.updatedAt)} )}
- {selectedReport.reporter && ( + {selectedTypeReport.reporter && (

- {selectedReport.reporter.name} · {selectedReport.reporter.model} ·{" "} - {selectedReport.reporter.agentId}@ - {selectedReport.reporter.agentVersion} + {selectedTypeReport.reporter.name} ·{" "} + {selectedTypeReport.reporter.model} ·{" "} + {selectedTypeReport.reporter.agentId}@ + {selectedTypeReport.reporter.agentVersion}

)}
- {selectedReport.structuredArtifact && ( + {selectedTypeReport.structuredArtifact && (
- {selectedReport.content ? ( + {selectedTypeReport.content ? (
- {selectedReport.content} + {selectedTypeReport.content}
- ) : selectedReport.error ? ( -

{selectedReport.error}

+ ) : selectedTypeReport.error ? ( +

+ {selectedTypeReport.error} +

) : (

This report does not have rendered Markdown content. @@ -393,13 +422,26 @@ export function SubmissionDetail() { : deriveAttemptReadiness(selectedAttempt) : "waiting"; const reports = reportsQuery.data ?? []; + const selectedAttemptReports = useMemo( + () => + selectedAttempt + ? reports.filter( + (report) => report.submissionRunId === selectedAttempt._id, + ) + : [], + [reports, selectedAttempt], + ); + const selectedAttemptReportTypes = useMemo( + () => newestSubmissionReportsByType(selectedAttemptReports), + [selectedAttemptReports], + ); const requestedTab = searchParams.get("tab"); const activeTab: SubmissionTab = requestedTab === "reports" || requestedTab === "runs" || requestedTab === "processing" ? requestedTab - : submission?.report?.reportId || reports.length > 0 + : selectedAttemptReportTypes.length > 0 || readiness !== "waiting" ? "reports" : "runs"; @@ -407,10 +449,21 @@ export function SubmissionDetail() { const defaultReportId = selectedAttempt?.report?.reportId ?? (isCurrentAttempt ? submission?.report?.reportId : undefined); + const requestedReport = selectedAttemptReports.find( + (report) => reportId(report) === requestedReportId, + ); + const defaultReport = selectedAttemptReports.find( + (report) => reportId(report) === defaultReportId, + ); + const selectedReportTypeKey = requestedReport + ? submissionReportTypeKey(requestedReport) + : defaultReport + ? submissionReportTypeKey(defaultReport) + : undefined; const selectedReport = - reports.find((report) => reportId(report) === requestedReportId) ?? - reports.find((report) => reportId(report) === defaultReportId) ?? - reports.find((report) => report.submissionRunId === selectedAttempt?._id); + selectedAttemptReportTypes.find( + (option) => option.key === selectedReportTypeKey, + )?.report ?? selectedAttemptReportTypes[0]?.report; const memberRows = useMemo(() => { if (!submission || !selectedAttempt) return []; @@ -613,14 +666,6 @@ export function SubmissionDetail() { const next = new URLSearchParams(searchParams); next.set("tab", "reports"); next.set("report", reportId(report)); - if ( - report.submissionRunId && - report.submissionRunId !== submission.submissionRunId - ) { - next.set("attempt", report.submissionRunId); - } else { - next.delete("attempt"); - } setSearchParams(next, { replace: true }); }; @@ -688,7 +733,10 @@ export function SubmissionDetail() { - Reports {reports.length > 0 ? `(${reports.length})` : ""} + Reports{" "} + {selectedAttemptReportTypes.length > 0 + ? `(${selectedAttemptReportTypes.length})` + : ""} @@ -702,10 +750,12 @@ export function SubmissionDetail() { setTab("processing")} diff --git a/docs/architecture/app-design.md b/docs/architecture/app-design.md index d8682677d..c6033656e 100644 --- a/docs/architecture/app-design.md +++ b/docs/architecture/app-design.md @@ -559,9 +559,12 @@ Submission Detail uses an attempt-scoped view rather than rendering every attempt at once. Its attempt picker switches the exact immutable request/run vector, dependency projection, processing logs, and report subject together. Horizontal Reports, Runs, and Processing tabs keep member runs one click away -while a long report is open. Reports use a master/detail selector over the -append-only submission report collection; only the report referenced by the -embedded current attempt is labeled current. +while a long report is open. Reports are first scoped to the selected attempt, +then grouped by report type: template-based reports use `templateId`, while +template-less handler reports use `reporter.id`. A second horizontal tab row +shows the newest report for each type and deliberately does not expose +same-type version history. Only the report referenced by the embedded current +attempt is labeled current. ## Runs List Query API From 337c8a0a592cac2ff5967ac904a062883d0657f5 Mon Sep 17 00:00:00 2001 From: Cedric Vidal Date: Wed, 14 Oct 2026 01:55:50 +0200 Subject: [PATCH 15/15] feat: trigger submission report templates Add submission-scoped templates, exact-attempt automatic triggering, browser-style report tabs, and stable report deep links. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cbbd5eb-61c6-4e6a-bb38-c377be5b2163 --- .../openapi-snapshot.test.ts.snap | 95 +++++++++++ apps/api/src/openapi/openapi.test.ts | 12 ++ apps/api/src/routes/report-templates.ts | 48 +++++- .../routes/reports-submission-trigger.test.ts | 158 ++++++++++++++++++ apps/api/src/routes/reports.ts | 98 ++++++++++- .../components/SubmissionReports.stories.tsx | 7 +- apps/portal/src/lib/api.ts | 2 + .../src/pages/CreateReportTemplate.test.tsx | 88 ++++++++++ .../portal/src/pages/CreateReportTemplate.tsx | 43 ++++- .../portal/src/pages/ReportTemplateDetail.tsx | 34 +++- apps/portal/src/pages/ReportTemplateList.tsx | 12 ++ .../src/pages/SubmissionDetail.test.tsx | 77 ++++++++- apps/portal/src/pages/SubmissionDetail.tsx | 64 ++++--- apps/portal/src/types.ts | 1 + apps/scheduler/src/index.ts | 2 + .../src/submission-dispatcher.test.ts | 96 +++++++++++ apps/scheduler/src/submission-dispatcher.ts | 64 ++++++- .../src/report-queue-processor.ts | 75 ++++++--- .../src/submission-tools.test.ts | 69 ++++++++ .../report-generator/src/submission-tools.ts | 92 ++++++++++ docs/architecture/app-design.md | 21 ++- docs/architecture/prompt-evaluations.md | 2 +- .../static-prompts/adapter/adapter.test.ts | 36 +++- .../static-prompts/adapter/targets/reports.ts | 76 +++++++-- .../static-prompts/datasets/manifest.json | 6 +- .../static-prompts/datasets/v1/readouts.jsonl | 1 + .../static-prompts/evaluation-manifest.yaml | 2 +- .../scripts/validate-coverage.test.ts | 2 +- .../scripts/validate-dataset.ts | 8 +- .../src/report-templates/prompt.test.ts | 14 +- .../shared/src/report-templates/prompt.ts | 6 + .../shared/src/schemas/report-template.ts | 3 + packages/shared/src/schemas/report.ts | 19 +++ packages/shared/src/schemas/submission.ts | 1 + packages/shared/src/types/types.ts | 3 + website/src/openapi/scope-openapi.json | 2 +- 36 files changed, 1247 insertions(+), 92 deletions(-) create mode 100644 apps/api/src/routes/reports-submission-trigger.test.ts create mode 100644 apps/portal/src/pages/CreateReportTemplate.test.tsx create mode 100644 apps/scheduler/src/submission-dispatcher.test.ts create mode 100644 apps/workers/report-generator/src/submission-tools.test.ts create mode 100644 apps/workers/report-generator/src/submission-tools.ts diff --git a/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap b/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap index f7bf50f65..293a09669 100644 --- a/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap +++ b/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap @@ -906,6 +906,14 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "name": { "type": "string", }, + "subject": { + "default": "run", + "enum": [ + "run", + "submission", + ], + "type": "string", + }, "systemPrompt": { "$ref": "#/components/schemas/ReportTemplateSystemPrompt", }, @@ -2539,6 +2547,14 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "projectId": { "type": "string", }, + "subject": { + "default": "run", + "enum": [ + "run", + "submission", + ], + "type": "string", + }, "systemPrompt": { "$ref": "#/components/schemas/ReportTemplateSystemPrompt", }, @@ -4086,6 +4102,13 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "report": { "$ref": "#/components/schemas/SubmissionReportPointer", }, + "reportsTriggeredAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, "requests": { "items": { "$ref": "#/components/schemas/SubmissionRequestRef", @@ -4244,6 +4267,21 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], "type": "object", }, + "TriggerSubmissionReportsInput": { + "properties": { + "submissionId": { + "type": "string", + }, + "submissionRunId": { + "type": "string", + }, + }, + "required": [ + "submissionId", + "submissionRunId", + ], + "type": "object", + }, "UpdateAgentInput": { "properties": { "available": { @@ -4480,6 +4518,13 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "name": { "type": "string", }, + "subject": { + "enum": [ + "run", + "submission", + ], + "type": "string", + }, "systemPrompt": { "allOf": [ { @@ -9100,6 +9145,56 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], }, }, + "/api/v1/reports/trigger-submission": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TriggerSubmissionReportsInput", + }, + }, + }, + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "reports": { + "items": { + "$ref": "#/components/schemas/ReportResponse", + }, + "type": "array", + }, + "triggered": { + "type": "number", + }, + }, + "required": [ + "triggered", + "reports", + ], + "type": "object", + }, + }, + }, + "description": "Success", + }, + "404": { + "description": "Submission attempt not found", + }, + "409": { + "description": "Submission attempt is not ready", + }, + }, + "summary": "Trigger submission reports", + "tags": [ + "Reports", + ], + }, + }, "/api/v1/reports/{id}": { "get": { "parameters": [ diff --git a/apps/api/src/openapi/openapi.test.ts b/apps/api/src/openapi/openapi.test.ts index 87426de43..6ebe182c8 100644 --- a/apps/api/src/openapi/openapi.test.ts +++ b/apps/api/src/openapi/openapi.test.ts @@ -36,6 +36,7 @@ import { BulkCreateReportsInputSchema, BulkReportStatusInputSchema, TriggerReportsInputSchema, + TriggerSubmissionReportsInputSchema, BulkTriggerReportsInputSchema, SkillResponseSchema, SkillRevisionResponseSchema, @@ -433,6 +434,17 @@ apiRoute(testApp, registry, { successStatus: 201, errorResponses: { 400: { description: "Invalid input" }, 404: { description: "Run not found" } }, handler: noop, }); +apiRoute(testApp, registry, { + method: "post", path: "/api/v1/reports/trigger-submission", tags: ["Reports"], + summary: "Trigger submission reports", body: TriggerSubmissionReportsInputSchema, + response: z.object({ triggered: z.number(), reports: z.array(ReportResponseSchema) }), + successStatus: 201, + errorResponses: { + 404: { description: "Submission attempt not found" }, + 409: { description: "Submission attempt is not ready" }, + }, + handler: noop, +}); apiRoute(testApp, registry, { method: "post", path: "/api/v1/reports/bulk-trigger", tags: ["Reports"], summary: "Bulk trigger reports", body: BulkTriggerReportsInputSchema, diff --git a/apps/api/src/routes/report-templates.ts b/apps/api/src/routes/report-templates.ts index d0e2a98d1..4429420d7 100644 --- a/apps/api/src/routes/report-templates.ts +++ b/apps/api/src/routes/report-templates.ts @@ -119,7 +119,17 @@ apiRoute(ctx.app, ctx.registry, { }, handler: async (req, res, next) => { try { - const { id, name, description, userPrompt, systemPrompt, trigger, model, timeoutMs } = req.body; + const { + id, + name, + subject = "run", + description, + userPrompt, + systemPrompt, + trigger, + model, + timeoutMs, + } = req.body; const projectId = getQueryProjectId(req); if (!id || typeof id !== "string") { @@ -138,6 +148,16 @@ apiRoute(ctx.app, ctx.registry, { res.status(400).json({ error: "userPrompt is required and must be a string" }); return; } + if ( + subject === "submission" && + trigger !== undefined && + trigger.type !== "always" + ) { + res.status(400).json({ + error: "Submission report templates currently support the automatic trigger only", + }); + return; + } // Validate systemPrompt if provided if (systemPrompt !== undefined) { @@ -196,6 +216,7 @@ apiRoute(ctx.app, ctx.registry, { { $set: { name, + subject, ...(description !== undefined ? { description } : {}), userPrompt, ...(systemPrompt !== undefined ? { systemPrompt } : {}), @@ -214,6 +235,7 @@ apiRoute(ctx.app, ctx.registry, { projectId, id, name, + subject, ...(description ? { description } : {}), userPrompt, ...(systemPrompt ? { systemPrompt } : {}), @@ -248,7 +270,16 @@ apiRoute(ctx.app, ctx.registry, { try { const { id } = req.params; const projectId = getQueryProjectId(req); - const { name, description, userPrompt, systemPrompt, trigger, model, timeoutMs } = req.body; + const { + name, + subject, + description, + userPrompt, + systemPrompt, + trigger, + model, + timeoutMs, + } = req.body; const existing = await ctx.reportTemplateCollection.findOne({ projectId, id, deletedAt: { $exists: false } }); if (!existing) { @@ -258,6 +289,7 @@ apiRoute(ctx.app, ctx.registry, { const updateFields: Record = { updatedAt: new Date() }; if (name !== undefined) updateFields.name = name; + if (subject !== undefined) updateFields.subject = subject; if (description !== undefined) updateFields.description = description; if (userPrompt !== undefined) { if (typeof userPrompt !== "string" || !userPrompt.trim()) { @@ -292,6 +324,18 @@ apiRoute(ctx.app, ctx.registry, { res.status(400).json({ error: triggerError }); return; } + const effectiveSubject = subject ?? existing.subject ?? "run"; + const effectiveTrigger = trigger ?? existing.trigger; + if ( + effectiveSubject === "submission" && + effectiveTrigger !== undefined && + effectiveTrigger.type !== "always" + ) { + res.status(400).json({ + error: "Submission report templates currently support the automatic trigger only", + }); + return; + } updateFields.trigger = trigger; } } diff --git a/apps/api/src/routes/reports-submission-trigger.test.ts b/apps/api/src/routes/reports-submission-trigger.test.ts new file mode 100644 index 000000000..0dada00ed --- /dev/null +++ b/apps/api/src/routes/reports-submission-trigger.test.ts @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi"; +import express from "express"; +import type { Collection } from "mongodb"; +import request from "supertest"; +import { describe, expect, it, vi } from "vitest"; +import type { + ReportDocument, + ReportTemplateDocument, + SubmissionDocument, +} from "shared"; +import type { RouteContext } from "../route-context.js"; +import { registerReportsRoutes } from "./reports.js"; + +function queryResult(items: T[]) { + return { + sort: () => queryResult(items), + toArray: async () => items, + }; +} + +function buildApp(input: { + existingReport?: ReportDocument; + submissionRunId?: string; +}) { + const submission: SubmissionDocument = { + _id: "submission-1", + submissionId: "submission-1", + projectId: "project-1", + requestIds: ["request-1"], + createdAt: new Date(), + run: { + _id: input.submissionRunId ?? "submission-run-1", + attemptNumber: 1, + vectorFingerprint: "vector", + requests: [{ requestId: "request-1", runId: "run-1" }], + dependencyProjection: [], + blocking: [], + handlerStatus: { + "pp-readout": { status: "done", version: 1 }, + }, + report: { reportId: "readout-report" }, + createdAt: new Date(), + }, + }; + const template: ReportTemplateDocument = { + _id: "template-document-1", + projectId: "project-1", + id: "executive-summary", + name: "Executive Summary", + subject: "submission", + userPrompt: "Summarize this submission.", + trigger: { type: "always" }, + createdAt: new Date(), + }; + const insertOne = vi.fn(async () => ({ acknowledged: true })); + const sendMessage = vi.fn(async () => ({ messageId: "queued" })); + const ctx = { + app: express(), + registry: new OpenAPIRegistry(), + submissionCollection: { + findOne: vi.fn(async (filter: Record) => + filter._id === submission._id && + filter["run._id"] === submission.run._id + ? submission + : null, + ), + } as unknown as Collection, + reportTemplateCollection: { + find: vi.fn(() => queryResult([template])), + } as unknown as Collection, + reportCollection: { + findOne: vi.fn(async () => input.existingReport ?? null), + insertOne, + } as unknown as Collection, + reportQueueClient: { sendMessage }, + } as unknown as RouteContext; + ctx.app.use(express.json()); + registerReportsRoutes(ctx); + ctx.app.use( + ( + error: Error, + _req: express.Request, + res: express.Response, + _next: express.NextFunction, + ) => res.status(500).json({ error: error.message }), + ); + return { app: ctx.app, insertOne, sendMessage }; +} + +describe("submission report template trigger", () => { + it("queues one report for each matching submission template", async () => { + const { app, insertOne, sendMessage } = buildApp({}); + + const response = await request(app) + .post("/api/v1/reports/trigger-submission") + .send({ + submissionId: "submission-1", + submissionRunId: "submission-run-1", + }); + + expect(response.status).toBe(201); + expect(response.body.triggered).toBe(1); + expect(insertOne).toHaveBeenCalledWith( + expect.objectContaining({ + submissionId: "submission-1", + submissionRunId: "submission-run-1", + templateId: "executive-summary", + status: "pending", + }), + ); + expect(sendMessage).toHaveBeenCalledOnce(); + }); + + it("rejects a stale submission attempt", async () => { + const { app, insertOne } = buildApp({ + submissionRunId: "submission-run-current", + }); + + const response = await request(app) + .post("/api/v1/reports/trigger-submission") + .send({ + submissionId: "submission-1", + submissionRunId: "submission-run-stale", + }); + + expect(response.status).toBe(404); + expect(insertOne).not.toHaveBeenCalled(); + }); + + it("does not create a duplicate report for the same attempt and template", async () => { + const { app, insertOne, sendMessage } = buildApp({ + existingReport: { + _id: "existing-report", + projectId: "project-1", + submissionId: "submission-1", + submissionRunId: "submission-run-1", + templateId: "executive-summary", + status: "completed", + createdAt: new Date(), + }, + }); + + const response = await request(app) + .post("/api/v1/reports/trigger-submission") + .send({ + submissionId: "submission-1", + submissionRunId: "submission-run-1", + }); + + expect(response.status).toBe(201); + expect(response.body.triggered).toBe(0); + expect(insertOne).not.toHaveBeenCalled(); + expect(sendMessage).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/api/src/routes/reports.ts b/apps/api/src/routes/reports.ts index 381600660..4a1d71664 100644 --- a/apps/api/src/routes/reports.ts +++ b/apps/api/src/routes/reports.ts @@ -14,6 +14,7 @@ import { InsightResponseSchema, ReportResponseSchema, TriggerReportsInputSchema, + TriggerSubmissionReportsInputSchema, evaluateTrigger, withRetry, } from "shared"; @@ -563,7 +564,11 @@ apiRoute(ctx.app, ctx.registry, { // Load all active report templates const templates = await ctx.reportTemplateCollection - .find({ deletedAt: { $exists: false } }) + .find({ + projectId: run.projectId, + deletedAt: { $exists: false }, + $or: [{ subject: "run" }, { subject: { $exists: false } }], + }) .toArray(); // Evaluate each template's trigger against the run @@ -604,6 +609,89 @@ apiRoute(ctx.app, ctx.registry, { }, }); +// POST /api/v1/reports/trigger-submission — trigger submission-scoped templates +// once the exact current attempt and its generated evidence are complete. +apiRoute(ctx.app, ctx.registry, { + method: "post", + path: "/api/v1/reports/trigger-submission", + tags: ["Reports"], + summary: "Trigger submission reports", + body: TriggerSubmissionReportsInputSchema, + response: z.object({ + triggered: z.number(), + reports: z.array(ReportResponseSchema), + }), + successStatus: 201, + errorResponses: { + 404: { description: "Submission attempt not found" }, + 409: { description: "Submission attempt is not ready" }, + }, + handler: async (req, res, next) => { + try { + const { submissionId, submissionRunId } = req.body; + const submission = await ctx.submissionCollection.findOne({ + _id: submissionId, + "run._id": submissionRunId, + }); + if (!submission) { + res.status(404).json({ error: "Current submission attempt not found" }); + return; + } + if ( + submission.run.handlerStatus?.["pp-readout"]?.status !== "done" || + !submission.run.report?.reportId + ) { + res.status(409).json({ + error: "Submission attempt evidence is not complete", + }); + return; + } + + const templates = await ctx.reportTemplateCollection + .find({ + projectId: submission.projectId, + subject: "submission", + deletedAt: { $exists: false }, + }) + .toArray(); + const created: ReportDocument[] = []; + for (const template of templates) { + if (template.trigger && template.trigger.type !== "always") continue; + const existing = await ctx.reportCollection.findOne({ + submissionId, + submissionRunId, + templateId: template.id, + }); + if (existing) continue; + + const reportDoc: ReportDocument = { + _id: uuidv4(), + projectId: submission.projectId, + submissionId, + submissionRunId, + templateId: template.id, + status: "pending", + createdAt: new Date(), + }; + await ctx.reportCollection.insertOne(reportDoc); + await ctx.reportQueueClient.sendMessage( + Buffer.from(JSON.stringify({ reportId: reportDoc._id })).toString( + "base64", + ), + ); + created.push(reportDoc); + } + + res.status(201).json({ + triggered: created.length, + reports: created, + }); + } catch (error) { + next(error); + } + }, +}); + // POST /api/v1/reports/bulk-trigger — evaluate report templates for multiple runs at once apiRoute(ctx.app, ctx.registry, { method: "post", @@ -631,8 +719,13 @@ apiRoute(ctx.app, ctx.registry, { const notFound = requestIds.filter(id => !foundIds.has(id)); // Load all active templates + const projectIds = [...new Set(runs.map((run) => run.projectId))]; const templates = await ctx.reportTemplateCollection - .find({ deletedAt: { $exists: false } }) + .find({ + projectId: { $in: projectIds }, + deletedAt: { $exists: false }, + $or: [{ subject: "run" }, { subject: { $exists: false } }], + }) .toArray(); const created: Array<{ reportId: string; requestId: string; templateId: string }> = []; @@ -645,6 +738,7 @@ apiRoute(ctx.app, ctx.registry, { } for (const template of templates) { + if (template.projectId !== run.projectId) continue; const triggerResult = evaluateTrigger( template.trigger as any, run as any, diff --git a/apps/portal/src/components/SubmissionReports.stories.tsx b/apps/portal/src/components/SubmissionReports.stories.tsx index d3762acbf..6e1bcca02 100644 --- a/apps/portal/src/components/SubmissionReports.stories.tsx +++ b/apps/portal/src/components/SubmissionReports.stories.tsx @@ -103,9 +103,10 @@ export const MultipleReportTypes: Story = { args: { reports: [readoutReport, templateReport] }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await userEvent.click( - canvas.getByRole("tab", { name: "Executive Summary" }), - ); + const executiveTab = canvas.getByRole("tab", { name: "Executive Summary" }); + await expect(executiveTab).toHaveClass("rounded-b-none"); + await userEvent.click(executiveTab); + await expect(executiveTab).toHaveAttribute("data-state", "active"); await expect( canvas.getByRole("heading", { name: "Executive summary" }), ).toBeVisible(); diff --git a/apps/portal/src/lib/api.ts b/apps/portal/src/lib/api.ts index 2099ee14c..fa7013541 100644 --- a/apps/portal/src/lib/api.ts +++ b/apps/portal/src/lib/api.ts @@ -859,6 +859,7 @@ export const api = { createReportTemplate: (body: { id: string; name: string; + subject?: "run" | "submission"; userPrompt: string; description?: string; systemPrompt?: ReportTemplateSystemPrompt; @@ -875,6 +876,7 @@ export const api = { /** Update an existing report template */ updateReportTemplate: (id: string, body: { name?: string; + subject?: "run" | "submission"; description?: string; userPrompt?: string; systemPrompt?: ReportTemplateSystemPrompt | null; diff --git a/apps/portal/src/pages/CreateReportTemplate.test.tsx b/apps/portal/src/pages/CreateReportTemplate.test.tsx new file mode 100644 index 000000000..f751f16a6 --- /dev/null +++ b/apps/portal/src/pages/CreateReportTemplate.test.tsx @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// @vitest-environment happy-dom +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { cleanup, render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@/lib/api", () => ({ + api: { + listAvailableReportModels: vi.fn(async () => []), + createReportTemplate: vi.fn(async (body: Record) => ({ + _id: "template-document-1", + projectId: "project-1", + createdAt: "2026-01-01T00:00:00.000Z", + ...body, + })), + }, +})); + +import { api } from "@/lib/api"; +import { CreateReportTemplate } from "./CreateReportTemplate"; + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +describe("CreateReportTemplate", () => { + it("creates an automatically triggered submission-scoped template", async () => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + render( + + + + } + /> + Template created

} + /> + + + , + ); + + await userEvent.type(screen.getByLabelText("Name *"), "Submission Summary"); + await userEvent.type( + screen.getByLabelText("User Prompt *"), + "Summarize the exact submission attempt.", + ); + await userEvent.click(screen.getByRole("combobox", { name: "Report scope" })); + await userEvent.click( + within(await screen.findByRole("listbox")).getByText( + "Submission attempt", + ), + ); + + expect( + screen.getByText( + "Runs automatically once the submission attempt and its generated evidence are complete.", + ), + ).toBeDefined(); + expect( + screen.getByRole("combobox", { name: "Trigger" }).textContent, + ).toContain("Always"); + + await userEvent.click( + screen.getByRole("button", { name: "Create Template" }), + ); + + expect(api.createReportTemplate).toHaveBeenCalledWith( + expect.objectContaining({ + id: "submission-summary", + name: "Submission Summary", + subject: "submission", + userPrompt: "Summarize the exact submission attempt.", + }), + ); + expect(await screen.findByText("Template created")).toBeDefined(); + }); +}); diff --git a/apps/portal/src/pages/CreateReportTemplate.tsx b/apps/portal/src/pages/CreateReportTemplate.tsx index 50f412460..da3a23f33 100644 --- a/apps/portal/src/pages/CreateReportTemplate.tsx +++ b/apps/portal/src/pages/CreateReportTemplate.tsx @@ -66,6 +66,7 @@ export function CreateReportTemplate() { const [id, setId] = useState(""); const [idManuallyEdited, setIdManuallyEdited] = useState(false); const [description, setDescription] = useState(""); + const [subject, setSubject] = useState<"run" | "submission">("run"); const [userPrompt, setUserPrompt] = useState(""); const [sysMode, setSysMode] = useState<"none" | "append" | "override">("none"); const [sysContent, setSysContent] = useState(""); @@ -106,6 +107,7 @@ export function CreateReportTemplate() { const body: Parameters[0] = { id: id.trim(), name: name.trim(), + subject, userPrompt: userPrompt.trim(), }; @@ -123,7 +125,7 @@ export function CreateReportTemplate() { } // Trigger - if (triggerType !== "always") { + if (subject === "run" && triggerType !== "always") { const idsArray = triggerIds.split(",").map(s => s.trim()).filter(Boolean); if (triggerType === "criteria") { body.trigger = { type: "criteria", criteriaIds: triggerCriteriaIds, match: triggerMatch }; @@ -192,6 +194,29 @@ export function CreateReportTemplate() { />
+
+ + +

+ Submission templates run once after the exact submission attempt + and its generated evidence are complete. +

+
+ {/* User Prompt */} @@ -250,7 +275,7 @@ export function CreateReportTemplate() {
setTriggerType(v as any)}> - + setEditDescription(e.target.value)} placeholder="Optional description" />
+
+ + +
@@ -320,7 +344,13 @@ export function ReportTemplateDetail() {
- + setEditTriggerType(value) + } + disabled={editSubject === "submission"} + > diff --git a/apps/portal/src/pages/ReportTemplateList.tsx b/apps/portal/src/pages/ReportTemplateList.tsx index cd7259799..f3fc575bf 100644 --- a/apps/portal/src/pages/ReportTemplateList.tsx +++ b/apps/portal/src/pages/ReportTemplateList.tsx @@ -112,6 +112,7 @@ export function ReportTemplateList() { const columnOptions: CustomizeColumnsOption[] = [ { id: "id", label: "ID", required: true }, { id: "name", label: "Name" }, + { id: "subject", label: "Scope" }, { id: "trigger", label: "Trigger" }, { id: "model", label: "Model" }, { id: "systemPrompt", label: "Sys Prompt" }, @@ -133,6 +134,17 @@ export function ReportTemplateList() { hidden: columnVisibility.isHidden("name"), cell: (t) => {t.name}, }, + { + id: "subject", + header: "Scope", + width: "120px", + hidden: columnVisibility.isHidden("subject"), + cell: (template) => ( + + {template.subject === "submission" ? "Submission" : "Run"} + + ), + }, { id: "trigger", header: "Trigger", diff --git a/apps/portal/src/pages/SubmissionDetail.test.tsx b/apps/portal/src/pages/SubmissionDetail.test.tsx index e2c825ccf..db0d60fbb 100644 --- a/apps/portal/src/pages/SubmissionDetail.test.tsx +++ b/apps/portal/src/pages/SubmissionDetail.test.tsx @@ -5,7 +5,13 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { cleanup, render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { MemoryRouter, Route, Routes } from "react-router-dom"; +import { + MemoryRouter, + Route, + Routes, + useLocation, + useNavigate, +} from "react-router-dom"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { Report, @@ -224,7 +230,15 @@ function renderDetail(initialEntry = "/submissions/submission-1") { - } /> + + + + + } + /> Run detail destination
} /> @@ -232,6 +246,18 @@ function renderDetail(initialEntry = "/submissions/submission-1") { ); } +function LocationProbe() { + const location = useLocation(); + const navigate = useNavigate(); + return ( +
+ {location.pathname}{location.search} + + +
+ ); +} + describe("SubmissionDetail", () => { it("defaults to Reports and shows attempt-scoped report types", async () => { renderDetail(); @@ -263,7 +289,9 @@ describe("SubmissionDetail", () => { }); it("selects the newest report when one attempt has duplicate report types", async () => { - renderDetail(); + renderDetail( + "/submissions/submission-1?tab=reports&attempt=submission-run-2&report=report-current-older-duplicate", + ); expect( await screen.findByRole("heading", { name: "Current generated report" }), @@ -271,6 +299,49 @@ describe("SubmissionDetail", () => { expect( screen.getByRole("link", { name: "Structured JSON" }).getAttribute("href"), ).toBe("/api/v1/reports/report-current/structured"); + expect(screen.getByLabelText("Current URL").textContent).toContain( + "report=report-current", + ); + }); + + it("deep-links to a report type and preserves browser back and forward navigation", async () => { + renderDetail( + "/submissions/submission-1?tab=reports&attempt=submission-run-2&report=report-template-current", + ); + + expect( + await screen.findByRole("heading", { name: "Executive summary report" }), + ).toBeDefined(); + expect( + screen.getByRole("tab", { name: "Executive Summary" }).getAttribute( + "data-state", + ), + ).toBe("active"); + + await userEvent.click( + screen.getByRole("tab", { name: "Submission Readout" }), + ); + expect( + await screen.findByRole("heading", { name: "Current generated report" }), + ).toBeDefined(); + expect(screen.getByLabelText("Current URL").textContent).toContain( + "report=report-current", + ); + + await userEvent.click(screen.getByRole("button", { name: "History back" })); + expect( + await screen.findByRole("heading", { name: "Executive summary report" }), + ).toBeDefined(); + expect(screen.getByLabelText("Current URL").textContent).toContain( + "report=report-template-current", + ); + + await userEvent.click( + screen.getByRole("button", { name: "History forward" }), + ); + expect( + await screen.findByRole("heading", { name: "Current generated report" }), + ).toBeDefined(); }); it("renders the immutable member vector with the shared table and run navigation", async () => { diff --git a/apps/portal/src/pages/SubmissionDetail.tsx b/apps/portal/src/pages/SubmissionDetail.tsx index 1b1a048ca..79d90fe70 100644 --- a/apps/portal/src/pages/SubmissionDetail.tsx +++ b/apps/portal/src/pages/SubmissionDetail.tsx @@ -3,6 +3,7 @@ import { useCallback, + useEffect, useMemo, } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; @@ -237,25 +238,30 @@ export function SubmissionReports({ const selectedTypeReport = selectedType?.report; return ( -
- { - const option = reportTypes.find((candidate) => candidate.key === key); - if (option) onSelect(option.report); - }} + { + const option = reportTypes.find((candidate) => candidate.key === key); + if (option) onSelect(option.report); + }} + > + - - {reportTypes.map((option) => ( - - {option.label} - - ))} - - + {reportTypes.map((option) => ( + + {option.label} + + ))} + {selectedTypeReport ? ( - + <>
@@ -342,10 +348,8 @@ export function SubmissionReports({ - ) : ( - unavailable - )} -
+ ) : unavailable} +
); } @@ -465,6 +469,20 @@ export function SubmissionDetail() { (option) => option.key === selectedReportTypeKey, )?.report ?? selectedAttemptReportTypes[0]?.report; + useEffect(() => { + if (!requestedReportId || !selectedReport) return; + const canonicalReportId = reportId(selectedReport); + if (requestedReportId === canonicalReportId) return; + const next = new URLSearchParams(searchParams); + next.set("report", canonicalReportId); + setSearchParams(next, { replace: true }); + }, [ + requestedReportId, + searchParams, + selectedReport, + setSearchParams, + ]); + const memberRows = useMemo(() => { if (!submission || !selectedAttempt) return []; const summaryById = new Map( @@ -653,20 +671,20 @@ export function SubmissionDetail() { const setTab = (tab: SubmissionTab) => { const next = new URLSearchParams(searchParams); next.set("tab", tab); - setSearchParams(next, { replace: true }); + setSearchParams(next); }; const selectAttempt = (attemptId: string) => { const next = new URLSearchParams(searchParams); if (attemptId === submission.submissionRunId) next.delete("attempt"); else next.set("attempt", attemptId); next.delete("report"); - setSearchParams(next, { replace: true }); + setSearchParams(next); }; const selectReport = (report: Report) => { const next = new URLSearchParams(searchParams); next.set("tab", "reports"); next.set("report", reportId(report)); - setSearchParams(next, { replace: true }); + setSearchParams(next); }; return ( diff --git a/apps/portal/src/types.ts b/apps/portal/src/types.ts index 56bee9cff..29ca91bc6 100644 --- a/apps/portal/src/types.ts +++ b/apps/portal/src/types.ts @@ -670,6 +670,7 @@ export interface ReportTemplate { _id: string; id: string; name: string; + subject?: "run" | "submission"; description?: string; userPrompt: string; systemPrompt?: ReportTemplateSystemPrompt; diff --git a/apps/scheduler/src/index.ts b/apps/scheduler/src/index.ts index 9fe0fbf79..adb6629e0 100644 --- a/apps/scheduler/src/index.ts +++ b/apps/scheduler/src/index.ts @@ -207,6 +207,8 @@ async function main(): Promise { db, createQueueClient, ppPollIntervalMs, + 30, + API_URL, ); submissionDispatcher.start(); console.log(`[Scheduler] Submission dispatcher started (poll=${ppPollIntervalMs}ms)`); diff --git a/apps/scheduler/src/submission-dispatcher.test.ts b/apps/scheduler/src/submission-dispatcher.test.ts new file mode 100644 index 000000000..db6dbffe9 --- /dev/null +++ b/apps/scheduler/src/submission-dispatcher.test.ts @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { Collection, Db } from "mongodb"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { + RequestDocument, + SubmissionDocument, + SubmissionRunHistoryDocument, +} from "shared"; +import { SubmissionDispatcher } from "./submission-dispatcher.js"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("SubmissionDispatcher report template triggering", () => { + it("claims and triggers the exact completed submission attempt once", async () => { + const submission: SubmissionDocument = { + _id: "submission-1", + submissionId: "submission-1", + projectId: "project-1", + requestIds: ["request-1"], + createdAt: new Date(), + run: { + _id: "submission-run-1", + attemptNumber: 1, + vectorFingerprint: "vector", + requests: [{ requestId: "request-1", runId: "run-1" }], + dependencyProjection: [], + blocking: [], + handlerStatus: { + "pp-readout": { status: "done", version: 1 }, + }, + report: { reportId: "readout-report" }, + createdAt: new Date(), + }, + }; + const findOneAndUpdate = vi.fn(async () => submission); + const dispatcher = new SubmissionDispatcher( + {} as Collection, + { + findOneAndUpdate, + updateOne: vi.fn(), + } as unknown as Collection, + {} as Collection, + { collection: vi.fn() } as unknown as Db, + vi.fn(), + 30_000, + 30, + "http://api.test", + ); + const reconcile = vi.fn(async () => ({ + submission, + readiness: "done" as const, + rolled: false, + dispatched: false, + })); + ( + dispatcher as unknown as { + coordinator: { reconcile: typeof reconcile }; + } + ).coordinator = { reconcile }; + const fetchMock = vi.fn(async () => ({ + ok: true, + status: 201, + text: async () => "", + })); + vi.stubGlobal("fetch", fetchMock); + + await ( + dispatcher as unknown as { + reconcileAndTrigger: (submissionId: string) => Promise; + } + ).reconcileAndTrigger("submission-1"); + + expect(findOneAndUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + _id: "submission-1", + "run._id": "submission-run-1", + }), + expect.any(Object), + { returnDocument: "after" }, + ); + expect(fetchMock).toHaveBeenCalledWith( + "http://api.test/api/v1/reports/trigger-submission", + expect.objectContaining({ + method: "POST", + body: JSON.stringify({ + submissionId: "submission-1", + submissionRunId: "submission-run-1", + }), + }), + ); + }); +}); diff --git a/apps/scheduler/src/submission-dispatcher.ts b/apps/scheduler/src/submission-dispatcher.ts index cd2914853..2282d86bf 100644 --- a/apps/scheduler/src/submission-dispatcher.ts +++ b/apps/scheduler/src/submission-dispatcher.ts @@ -11,6 +11,7 @@ import { type SubmissionQueueMessagePayload, type SubmissionRunHistoryDocument, type ServiceHandlerDocument, + withRetry, } from "shared"; export class SubmissionDispatcher { @@ -28,6 +29,7 @@ export class SubmissionDispatcher { private readonly createQueueClient: (queueName: string) => QueueClient, private readonly pollIntervalMs = 30_000, private readonly batchSize = 30, + private readonly apiUrl = "http://api:3000", ) { this.coordinator = new SubmissionCoordinator( { @@ -63,7 +65,7 @@ export class SubmissionDispatcher { { projection: { submissionId: 1 } }, ); if (request?.submissionId) { - await this.coordinator.reconcile(request.submissionId); + await this.reconcileAndTrigger(request.submissionId); } } @@ -87,7 +89,7 @@ export class SubmissionDispatcher { ? candidates.at(-1)?._id ?? null : null; for (const candidate of candidates) { - await this.coordinator.reconcile(candidate._id); + await this.reconcileAndTrigger(candidate._id); } } catch (error) { console.error("[SubmissionDispatcher] Poll failed:", error); @@ -96,6 +98,64 @@ export class SubmissionDispatcher { } } + private async reconcileAndTrigger(submissionId: string): Promise { + const reconciled = await this.coordinator.reconcile(submissionId); + if (!reconciled || reconciled.readiness !== "done") return; + const submissionRunId = reconciled.submission.run._id; + if (reconciled.submission.run.reportsTriggeredAt) return; + + const now = new Date(); + const claimed = await this.submissions.findOneAndUpdate( + { + _id: submissionId, + "run._id": submissionRunId, + "run.reportsTriggeredAt": { $exists: false }, + }, + { + $set: { + "run.reportsTriggeredAt": now, + "run.updatedAt": now, + updatedAt: now, + }, + }, + { returnDocument: "after" }, + ); + if (!claimed) return; + + const url = `${this.apiUrl}/api/v1/reports/trigger-submission`; + try { + await withRetry(async () => { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ submissionId, submissionRunId }), + }); + if (!response.ok) { + const detail = await response.text().catch(() => ""); + throw new Error( + `POST ${url} -> ${response.status} ${detail}`.trim(), + ); + } + }); + console.log( + `[SubmissionDispatcher] Triggered report templates for ${submissionId}/${submissionRunId}`, + ); + } catch (error) { + const failedAt = new Date(); + await this.submissions.updateOne( + { _id: submissionId, "run._id": submissionRunId }, + { + $unset: { "run.reportsTriggeredAt": "" }, + $set: { + "run.updatedAt": failedAt, + updatedAt: failedAt, + }, + }, + ); + throw error; + } + } + private async enqueue( service: SubmissionHandlerServiceDocument, message: SubmissionQueueMessagePayload, diff --git a/apps/workers/report-generator/src/report-queue-processor.ts b/apps/workers/report-generator/src/report-queue-processor.ts index 1a0523a97..5c0cb5252 100644 --- a/apps/workers/report-generator/src/report-queue-processor.ts +++ b/apps/workers/report-generator/src/report-queue-processor.ts @@ -13,11 +13,13 @@ import { ReportDocument, ReportTemplateDocument, Reporter, + SUBMISSION_REPORT_SYSTEM_PROMPT, TokenManagerClient, type VisibilityHeartbeat, } from "shared"; import { trackMetric, trackEvent } from "telemetry"; import { createReportTools } from "./tools.js"; +import { createSubmissionReportTools } from "./submission-tools.js"; import { REPORT_SYSTEM_PROMPT, withRetry } from "shared"; export interface ReportQueueProcessorConfig extends BaseQueueProcessorConfig { @@ -36,21 +38,22 @@ export interface ResolvedReportPrompts { export function resolveReportPrompts( template: Pick, - requestId: string, + subjectId: string, + defaultSystemPrompt = REPORT_SYSTEM_PROMPT, ): ResolvedReportPrompts { const userPrompt = template.userPrompt.replace( - /\{\{requestId\}\}|\{requestId\}/g, - requestId, + /\{\{requestId\}\}|\{requestId\}|\{\{submissionId\}\}|\{submissionId\}/g, + subjectId, ); if (!template.systemPrompt) { - return { userPrompt, systemPrompt: REPORT_SYSTEM_PROMPT }; + return { userPrompt, systemPrompt: defaultSystemPrompt }; } if (template.systemPrompt.mode === "override") { return { userPrompt, systemPrompt: template.systemPrompt.content }; } return { userPrompt, - systemPrompt: `${REPORT_SYSTEM_PROMPT}\n\n${template.systemPrompt.content}`, + systemPrompt: `${defaultSystemPrompt}\n\n${template.systemPrompt.content}`, }; } @@ -88,9 +91,11 @@ export class ReportQueueProcessor extends BaseQueueProcessor { const generationStart = Date.now(); const reportId = doc._id; const requestId = doc.requestId; - if (!requestId) { + const submissionId = doc.submissionId; + const submissionRunId = doc.submissionRunId; + if (!requestId && !(submissionId && submissionRunId)) { throw new Error( - `Report ${reportId} has no requestId and cannot be processed by report-generator`, + `Report ${reportId} has no run or submission subject`, ); } @@ -99,8 +104,20 @@ export class ReportQueueProcessor extends BaseQueueProcessor { trackMetric({ name: "report_generator.cold_start_ms", value: process.uptime() * 1000, properties: { service: "report-generator" } }); } - trackEvent({ name: "report_generator.generation_started", properties: { requestId, reportId, templateId: doc.templateId || "unknown" } }); - await log("info", `Starting report generation for run ${requestId}`); + trackEvent({ + name: "report_generator.generation_started", + properties: { + requestId: requestId ?? "submission", + reportId, + templateId: doc.templateId || "unknown", + }, + }); + await log( + "info", + requestId + ? `Starting report generation for run ${requestId}` + : `Starting report generation for submission ${submissionId}/${submissionRunId}`, + ); // --- Prepare snapshots temp directory --- const snapshotsDir = join(tmpdir(), `report-${reportId}`); @@ -108,12 +125,18 @@ export class ReportQueueProcessor extends BaseQueueProcessor { try { // --- Create Copilot SDK tools --- - const tools = createReportTools( - this.reportConfig.apiBaseUrl, - requestId, - snapshotsDir, - reportId - ); + const tools = requestId + ? createReportTools( + this.reportConfig.apiBaseUrl, + requestId, + snapshotsDir, + reportId, + ) + : createSubmissionReportTools( + this.reportConfig.apiBaseUrl, + submissionId!, + submissionRunId!, + ); await log("info", "Initialized tools, starting Copilot SDK session"); @@ -156,18 +179,19 @@ export class ReportQueueProcessor extends BaseQueueProcessor { await log("info", `Reporter: ${reporter.agentId}@${reporter.agentVersion}, model: ${reporter.model}`); - const { - userPrompt: resolvedUserPrompt, - systemPrompt: resolvedSystemPrompt, - } = resolveReportPrompts(template, requestId); + const prompts = resolveReportPrompts( + template, + requestId ?? submissionId!, + requestId ? REPORT_SYSTEM_PROMPT : SUBMISSION_REPORT_SYSTEM_PROMPT, + ); // --- Run Copilot SDK session --- const resolvedTimeoutMs = template.timeoutMs ?? this.reportConfig.sessionTimeoutMs ?? 5 * 60 * 1000; const llmStart = Date.now(); const content = await this.runCopilotSession( tools, - resolvedUserPrompt, - resolvedSystemPrompt, + prompts.userPrompt, + prompts.systemPrompt, resolvedModel, resolvedTimeoutMs, log @@ -188,7 +212,10 @@ export class ReportQueueProcessor extends BaseQueueProcessor { await log("info", `Report completed (${content.length} chars)`, { final: true }); trackMetric({ name: "report_generator.generation_duration_ms", value: Date.now() - generationStart, properties: { service: "report-generator" } }); - trackEvent({ name: "report_generator.generation_completed", properties: { requestId, reportId } }); + trackEvent({ + name: "report_generator.generation_completed", + properties: { requestId: requestId ?? "submission", reportId }, + }); } finally { // Clean up extracted snapshot files if (existsSync(snapshotsDir)) { @@ -216,7 +243,9 @@ export class ReportQueueProcessor extends BaseQueueProcessor { * real-time progress on the Logs tab. */ private async runCopilotSession( - tools: ReturnType, + tools: + | ReturnType + | ReturnType, userPrompt: string, systemPrompt: string, model: string, diff --git a/apps/workers/report-generator/src/submission-tools.test.ts b/apps/workers/report-generator/src/submission-tools.test.ts new file mode 100644 index 000000000..f7fa32a4b --- /dev/null +++ b/apps/workers/report-generator/src/submission-tools.test.ts @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createSubmissionReportTools } from "./submission-tools.js"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("createSubmissionReportTools", () => { + it("loads structured evidence for the exact submission attempt", async () => { + const fetchMock = vi + .fn() + .mockResolvedValueOnce({ + ok: true, + json: async () => [ + { + _id: "readout-report", + submissionRunId: "submission-run-1", + reporter: { id: "pp-readout" }, + structuredArtifact: { url: "reports/readout-report/structured.json" }, + createdAt: "2026-01-01T00:00:00.000Z", + }, + { + _id: "other-attempt", + submissionRunId: "submission-run-2", + reporter: { id: "pp-readout" }, + structuredArtifact: { url: "reports/other/structured.json" }, + createdAt: "2026-01-02T00:00:00.000Z", + }, + ], + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ submissionRunId: "submission-run-1", runs: [] }), + }); + vi.stubGlobal("fetch", fetchMock); + const tools = createSubmissionReportTools( + "http://api.test", + "submission-1", + "submission-run-1", + ); + const evidenceTool = tools.find( + (tool) => tool.name === "get_submission_evidence", + ); + + expect(evidenceTool).toBeDefined(); + const result = await ( + evidenceTool as unknown as { + handler: ( + args: Record, + context: Record, + ) => Promise; + } + ).handler({}, {}); + + expect(fetchMock.mock.calls[0]?.[0]).toBe( + "http://api.test/api/v1/submissions/submission-1/reports", + ); + expect(fetchMock.mock.calls[1]?.[0]).toBe( + "http://api.test/api/v1/reports/readout-report/structured", + ); + expect(result).toEqual({ + reportId: "readout-report", + evidence: { submissionRunId: "submission-run-1", runs: [] }, + }); + }); +}); diff --git a/apps/workers/report-generator/src/submission-tools.ts b/apps/workers/report-generator/src/submission-tools.ts new file mode 100644 index 000000000..e00ecfca3 --- /dev/null +++ b/apps/workers/report-generator/src/submission-tools.ts @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { defineTool } from "@github/copilot-sdk"; +import type { ReportDocument, SubmissionRunState } from "shared"; + +interface SubmissionAttemptResponse extends SubmissionRunState { + submissionId?: string; + projectId?: string; +} + +export function createSubmissionReportTools( + apiBaseUrl: string, + submissionId: string, + submissionRunId: string, +) { + const getSubmissionSummary = defineTool("get_submission_summary", { + description: + "Get the exact immutable submission attempt, including its request/run vector, dependency projection, handler states, and report pointer.", + parameters: { type: "object", properties: {} }, + handler: async () => { + const response = await fetch( + `${apiBaseUrl}/api/v1/submissions/${encodeURIComponent(submissionId)}/runs/${encodeURIComponent(submissionRunId)}`, + ); + if (!response.ok) { + return { + error: `Failed to fetch submission attempt: ${response.status} ${response.statusText}`, + }; + } + const attempt: SubmissionAttemptResponse = await response.json(); + return { + submissionId, + submissionRunId, + attemptNumber: attempt.attemptNumber, + requests: attempt.requests, + dependencyProjection: attempt.dependencyProjection, + handlerStatus: attempt.handlerStatus, + blocking: attempt.blocking, + createdAt: attempt.createdAt, + finishedAt: attempt.finishedAt, + }; + }, + }); + + const getSubmissionEvidence = defineTool("get_submission_evidence", { + description: + "Get the schema-validated structured evidence for this exact submission attempt, including provenance, outcomes, profiles, criteria, trajectories, and observations.", + parameters: { type: "object", properties: {} }, + handler: async () => { + const reportsResponse = await fetch( + `${apiBaseUrl}/api/v1/submissions/${encodeURIComponent(submissionId)}/reports`, + ); + if (!reportsResponse.ok) { + return { + error: `Failed to list submission reports: ${reportsResponse.status} ${reportsResponse.statusText}`, + }; + } + const reports: ReportDocument[] = await reportsResponse.json(); + const evidenceReport = reports + .filter( + (report) => + report.submissionRunId === submissionRunId && + report.reporter?.id === "pp-readout" && + report.structuredArtifact, + ) + .sort( + (left, right) => + new Date(right.createdAt).getTime() - + new Date(left.createdAt).getTime() || + right._id.localeCompare(left._id), + )[0]; + if (!evidenceReport) { + return { error: "Structured submission evidence is not available" }; + } + const response = await fetch( + `${apiBaseUrl}/api/v1/reports/${encodeURIComponent(evidenceReport._id)}/structured`, + ); + if (!response.ok) { + return { + error: `Failed to fetch structured evidence: ${response.status} ${response.statusText}`, + }; + } + const evidence: unknown = await response.json(); + return { reportId: evidenceReport._id, evidence }; + }, + }); + + return [getSubmissionSummary, getSubmissionEvidence].map((tool) => ({ + ...tool, + skipPermission: true, + })); +} diff --git a/docs/architecture/app-design.md b/docs/architecture/app-design.md index c6033656e..ff2b81973 100644 --- a/docs/architecture/app-design.md +++ b/docs/architecture/app-design.md @@ -562,9 +562,24 @@ Horizontal Reports, Runs, and Processing tabs keep member runs one click away while a long report is open. Reports are first scoped to the selected attempt, then grouped by report type: template-based reports use `templateId`, while template-less handler reports use `reporter.id`. A second horizontal tab row -shows the newest report for each type and deliberately does not expose -same-type version history. Only the report referenced by the embedded current -attempt is labeled current. +uses browser-style tabs connected to the report panel, shows the newest report +for each type, and deliberately does not expose same-type version history. The +`attempt` and `report` query parameters form the stable deep-link contract. +Selecting an attempt or report adds a browser-history entry; stale duplicate +report ids are canonicalized to the newest report of that type for the selected +attempt. Only the report referenced by the embedded current attempt is labeled +current. + +Report templates have a `subject` of `run` (the backward-compatible default) or +`submission`. Run templates retain their criteria/task/prompt-feature triggers. +Submission templates currently use the automatic trigger: after `pp-readout` +finishes for the exact current `submissionRunId`, the scheduler atomically +claims `run.reportsTriggeredAt` and asks the API to create at most one +template-based report per template and attempt. The normal report-generator +worker consumes that report, reads the exact attempt plus its validated +structured readout evidence, and stores Markdown as a sibling report type. A +failed trigger clears the exact-attempt claim so polling can retry; archived +attempts are never trigger targets. ## Runs List Query API diff --git a/docs/architecture/prompt-evaluations.md b/docs/architecture/prompt-evaluations.md index ba40805f4..e6a3cc3d6 100644 --- a/docs/architecture/prompt-evaluations.md +++ b/docs/architecture/prompt-evaluations.md @@ -30,7 +30,7 @@ families: | Prompt-feature extraction | Positive, negative, ambiguous, ancestor/descendant, and empty-feature cases | | Judge instructions | Bundled and independent strategies; file, tool-history, response, conflicting, missing, and prior-pass evidence | | Developer feedback | Default persona, custom persona, descendant guard on/off, and multiple root failures | -| Run reports and submission readouts | Default and appended run-report system prompts; override as a control where the default prompt is intentionally absent; exact-vector submission readout synthesis | +| Run reports and submission readouts | Default and appended run-report system prompts; override as a control where the default prompt is intentionally absent; exact-vector submission readout synthesis; submission-template report composition from exact-attempt summary and structured evidence tools | Judge and report tool descriptions are static AI-facing instructions, but they are evaluated through their owning end-to-end judge and report families rather diff --git a/evaluations/static-prompts/adapter/adapter.test.ts b/evaluations/static-prompts/adapter/adapter.test.ts index a5380d9f7..33841fa18 100644 --- a/evaluations/static-prompts/adapter/adapter.test.ts +++ b/evaluations/static-prompts/adapter/adapter.test.ts @@ -2,7 +2,10 @@ // Licensed under the MIT License. import { describe, expect, it } from "vitest"; -import { REPORT_SYSTEM_PROMPT } from "../../../packages/shared/src/index.js"; +import { + REPORT_SYSTEM_PROMPT, + SUBMISSION_REPORT_SYSTEM_PROMPT, +} from "../../../packages/shared/src/index.js"; import { READOUT_SYSTEM_PROMPT } from "../../../apps/workers/readout/src/prompt.js"; import { buildCriteriaAuthoringRequest, @@ -72,6 +75,7 @@ describe("static prompt adapter registry", () => { "append", "override-control", "submission-readout", + "submission-template", ], }, ]); @@ -106,6 +110,11 @@ describe("static prompt adapter registry", () => { family: "run-report", variant: "submission-readout", }); + expect(targets).toContainEqual({ + adapterId: "run-report/submission-template", + family: "run-report", + variant: "submission-template", + }); expect(JSON.parse(JSON.stringify(targets))).toEqual(targets); }); }); @@ -461,4 +470,29 @@ describe("session prompt adapters", () => { ]); expect(result.request.tools).toEqual([]); }); + + it("composes submission-template prompts with exact-attempt tools", async () => { + const result = await getPromptTarget( + "run-report", + "submission-template", + ).run( + { + submissionId: "submission-1", + submissionRunId: "submission-run-1", + reportId: "report-1", + userPrompt: "Summarize {submissionId}.", + }, + "submission-template", + fakeContext(() => "# Submission report"), + ); + + expect(result.request.messages).toEqual([ + { role: "system", content: SUBMISSION_REPORT_SYSTEM_PROMPT }, + { role: "user", content: "Summarize submission-1." }, + ]); + expect(result.request.tools?.map((tool) => tool.name)).toEqual([ + "get_submission_summary", + "get_submission_evidence", + ]); + }); }); diff --git a/evaluations/static-prompts/adapter/targets/reports.ts b/evaluations/static-prompts/adapter/targets/reports.ts index bef13c8e5..721cd4fce 100644 --- a/evaluations/static-prompts/adapter/targets/reports.ts +++ b/evaluations/static-prompts/adapter/targets/reports.ts @@ -3,7 +3,9 @@ import type { ReportTemplateSystemPrompt } from "../../../../packages/shared/src/index.js"; import { createReportTools } from "../../../../apps/workers/report-generator/src/tools.js"; +import { createSubmissionReportTools } from "../../../../apps/workers/report-generator/src/submission-tools.js"; import { resolveReportPrompts } from "../../../../apps/workers/report-generator/src/report-queue-processor.js"; +import { SUBMISSION_REPORT_SYSTEM_PROMPT } from "../../../../packages/shared/src/index.js"; import { READOUT_SYSTEM_PROMPT, composeReadoutUserPrompt, @@ -18,6 +20,8 @@ import { normalizeTools } from "./common.js"; interface ReportInput { requestId: string; + submissionId?: string; + submissionRunId?: string; reportId: string; userPrompt: string; systemPrompt?: ReportTemplateSystemPrompt; @@ -54,7 +58,22 @@ function parseReportInput(input: unknown, variant: string): ReportInput { ); } return { - requestId: requiredString(value.requestId, "input.requestId"), + requestId: + variant === "submission-template" + ? optionalString(value.requestId, "input.requestId") ?? "" + : requiredString(value.requestId, "input.requestId"), + ...(variant === "submission-template" + ? { + submissionId: requiredString( + value.submissionId, + "input.submissionId", + ), + submissionRunId: requiredString( + value.submissionRunId, + "input.submissionRunId", + ), + } + : {}), reportId: optionalString(value.reportId, "input.reportId") ?? "prompt-eval-report", userPrompt: requiredString(value.userPrompt, "input.userPrompt"), @@ -84,9 +103,10 @@ function parseReportInput(input: unknown, variant: string): ReportInput { function createFixtureToolOptions( evidenceContext: Record | undefined, ): CompletionOptions { - const request = evidenceContext - ? record(evidenceContext.request, "input.evidenceContext.request") - : {}; + const request = + evidenceContext?.request === undefined + ? {} + : record(evidenceContext.request, "input.evidenceContext.request"); const turns = Array.isArray(request.turns) ? request.turns.filter( (turn): turn is Record => @@ -102,6 +122,13 @@ function createFixtureToolOptions( ? (arguments_ as Record) : {}; switch (name) { + case "get_submission_summary": + return evidenceContext?.submission ?? {}; + case "get_submission_evidence": + return { + reportId: "prompt-eval-readout", + evidence: evidenceContext?.evidence ?? {}, + }; case "get_run_summary": return request; case "list_turns": @@ -180,19 +207,30 @@ function composeParsedReportRequest( variant: string, defaultModel: string, ): ComposedPromptRequest { + const submissionTemplate = variant === "submission-template"; + const subjectId = submissionTemplate + ? parsed.submissionId! + : parsed.requestId; const prompts = resolveReportPrompts( { userPrompt: parsed.userPrompt, systemPrompt: parsed.systemPrompt, }, - parsed.requestId, - ); - const tools = createReportTools( - parsed.apiBaseUrl, - parsed.requestId, - parsed.snapshotsDir, - parsed.reportId, + subjectId, + submissionTemplate ? SUBMISSION_REPORT_SYSTEM_PROMPT : undefined, ); + const tools = submissionTemplate + ? createSubmissionReportTools( + parsed.apiBaseUrl, + parsed.submissionId!, + parsed.submissionRunId!, + ) + : createReportTools( + parsed.apiBaseUrl, + parsed.requestId, + parsed.snapshotsDir, + parsed.reportId, + ); return { messages: [ { role: "system", content: prompts.systemPrompt }, @@ -204,8 +242,12 @@ function composeParsedReportRequest( adapterId: `run-report/${variant}`, productionSources: [ "apps/workers/report-generator/src/report-queue-processor.ts#resolveReportPrompts", - "apps/workers/report-generator/src/tools.ts#createReportTools", - "packages/shared/src/report-templates/prompt.ts#REPORT_SYSTEM_PROMPT", + submissionTemplate + ? "apps/workers/report-generator/src/submission-tools.ts#createSubmissionReportTools" + : "apps/workers/report-generator/src/tools.ts#createReportTools", + submissionTemplate + ? "packages/shared/src/report-templates/prompt.ts#SUBMISSION_REPORT_SYSTEM_PROMPT" + : "packages/shared/src/report-templates/prompt.ts#REPORT_SYSTEM_PROMPT", ], fidelity: "exact", }, @@ -252,7 +294,13 @@ export function composeReportRequest( export const reportAdapter: PromptTargetAdapter = { family: "run-report", - variants: ["default", "append", "override-control", "submission-readout"], + variants: [ + "default", + "append", + "override-control", + "submission-readout", + "submission-template", + ], async run(input, variant, context) { if (variant === "submission-readout") { const request = composeReportRequest(input, variant, context.model); diff --git a/evaluations/static-prompts/datasets/manifest.json b/evaluations/static-prompts/datasets/manifest.json index 21a8b112b..489fe7a17 100644 --- a/evaluations/static-prompts/datasets/manifest.json +++ b/evaluations/static-prompts/datasets/manifest.json @@ -36,7 +36,7 @@ "prompt-feature-extraction": 30, "judge-instructions": 30, "developer-feedback": 25, - "run-report": 31 + "run-report": 32 } }, "files": [ @@ -92,8 +92,8 @@ }, { "path": "v1/readouts.jsonl", - "sha256": "a23f372bf8d4c4cb6668a2094facfe284bf8030685e6405b0c76a03a1f19f1e4", - "rows": 1, + "sha256": "7f5b6f50f66e5f182a265996817a910763722c01e68766c95bc9d2ba9abadda7", + "rows": 2, "families": [ "run-report" ] diff --git a/evaluations/static-prompts/datasets/v1/readouts.jsonl b/evaluations/static-prompts/datasets/v1/readouts.jsonl index 9ba053ab2..753c46325 100644 --- a/evaluations/static-prompts/datasets/v1/readouts.jsonl +++ b/evaluations/static-prompts/datasets/v1/readouts.jsonl @@ -1 +1,2 @@ {"schemaVersion":1,"id":"run-report-0123456789abcdef","family":"run-report","variant":"submission-readout","smoke":true,"input":{"evidence":{"submissionId":"submission-1","submissionRunId":"submission-run-1","runs":[{"request":{"_id":"request-1","projectId":"project-1","workerType":"coder-acp-copilot","profileId":"profile-1","observations":["uses-current-api"],"scenario":{"task":"Update the API","criteria":[]},"createdAt":"2026-10-13T12:00:00.000Z","priority":0},"run":{"_id":"run-1","attemptNumber":1,"status":"done","outcome":"failed","observationResults":[{"criterionId":"uses-current-api","passed":false,"feedback":"The deprecated API remained.","evaluated":true}],"turns":[]},"criteria":[{"id":"uses-current-api","projectId":"project-1","prompt":"Use the current API","kind":"observation","subject":"run","taxonomyElementId":"dimension:dependency-currency","createdAt":"2026-10-13T12:00:00.000Z"}],"trajectories":[]}]}},"expected":{"referenceReport":"{\"title\":\"Submission readout\",\"executiveSummary\":\"The exact run failed and did not satisfy the selected observation.\",\"profiles\":[{\"profileId\":\"profile-1\",\"requestIds\":[\"request-1\"],\"summary\":\"One profiled run.\"}],\"runs\":[{\"requestId\":\"request-1\",\"runId\":\"run-1\",\"trajectoryHighlights\":[]}],\"findings\":[{\"id\":\"finding-1\",\"title\":\"Deprecated API remained\",\"description\":\"The final run evidence failed the selected observation.\",\"severity\":\"high\",\"evidence\":[{\"requestId\":\"request-1\",\"runId\":\"run-1\",\"criterionId\":\"uses-current-api\",\"excerpt\":\"The deprecated API remained.\"}]}],\"actions\":[{\"id\":\"action-1\",\"title\":\"Update the API usage\",\"priority\":\"high\",\"description\":\"Replace the deprecated API and verify the final state.\",\"relatedFindingIds\":[\"finding-1\"]}]}"},"evaluators":["generation_success","output_schema","non_empty_report","no_fabricated_references","relevance","coherence","groundedness","task_adherence","report_quality"],"tags":["submission-readout","failed"],"provenance":{"kind":"synthetic-reviewed","sourceEndpoints":[],"projectId":"synthetic-project","sourceEntityIds":["submission-1","submission-run-1","request-1","run-1"],"harvestedAt":"2026-10-13T12:00:00.000Z","selectionSeed":"scope-static-prompts-v1","sourceHash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","note":"Reviewed synthetic exact-vector case covering failed coding outcome and passed:false observation evidence."},"review":{"status":"approved","method":"manual-contract-review-v1"}} +{"schemaVersion":1,"id":"run-report-fedcba9876543210","family":"run-report","variant":"submission-template","smoke":true,"input":{"submissionId":"submission-1","submissionRunId":"submission-run-1","reportId":"template-report-1","userPrompt":"Create an executive summary of this exact submission attempt. Compare run outcomes and cite only supplied evidence.","evidenceContext":{"submission":{"submissionId":"submission-1","submissionRunId":"submission-run-1","attemptNumber":1,"requests":[{"requestId":"request-1","runId":"run-1"},{"requestId":"request-2","runId":"run-2"}]},"evidence":{"submissionId":"submission-1","submissionRunId":"submission-run-1","runs":[{"requestId":"request-1","runId":"run-1","outcome":"succeeded"},{"requestId":"request-2","runId":"run-2","outcome":"failed"}],"findings":[{"title":"Mixed outcomes","description":"One of two exact runs failed."}]}}},"expected":{"referenceReport":"# Executive summary\n\nThe exact submission attempt contains two runs with mixed outcomes: `request-1` / `run-1` succeeded, while `request-2` / `run-2` failed.\n\n## Key finding\n\nOne of the two runs failed, so the submission should be reviewed before treating the result as consistently successful."},"evaluators":["generation_success","non_empty_report","valid_markdown","no_fabricated_references","report_quality","relevance","coherence","groundedness","task_adherence","tool_call_accuracy"],"tags":["submission-template","mixed-outcomes"],"provenance":{"kind":"synthetic-reviewed","sourceEndpoints":["/api/v1/submissions/:id/runs/:submissionRunId","/api/v1/submissions/:id/reports","/api/v1/reports/:id/structured"],"projectId":"synthetic-project","sourceEntityIds":["submission-1","submission-run-1","request-1","run-1","request-2","run-2"],"harvestedAt":"2026-10-14T00:00:00.000Z","selectionSeed":"scope-static-prompts-v1","sourceHash":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","note":"Reviewed synthetic case for submission-scoped template composition and exact-attempt evidence tools."},"review":{"status":"approved","method":"manual-contract-review-v1"}} diff --git a/evaluations/static-prompts/evaluation-manifest.yaml b/evaluations/static-prompts/evaluation-manifest.yaml index 975574108..f82b7dbe9 100644 --- a/evaluations/static-prompts/evaluation-manifest.yaml +++ b/evaluations/static-prompts/evaluation-manifest.yaml @@ -19,4 +19,4 @@ qualityFamilies: - id: developer-feedback variants: [default, persona, descendant-guard] - id: run-report - variants: [default, append, override-control, submission-readout] + variants: [default, append, override-control, submission-readout, submission-template] diff --git a/evaluations/static-prompts/scripts/validate-coverage.test.ts b/evaluations/static-prompts/scripts/validate-coverage.test.ts index f525c4e76..f08588162 100644 --- a/evaluations/static-prompts/scripts/validate-coverage.test.ts +++ b/evaluations/static-prompts/scripts/validate-coverage.test.ts @@ -6,6 +6,6 @@ import { validateCoverage } from "./validate-coverage.js"; it("covers every quality manifest family and variant", async () => { await expect(validateCoverage()).resolves.toEqual({ - qualityTargets: 16, + qualityTargets: 17, }); }); diff --git a/evaluations/static-prompts/scripts/validate-dataset.ts b/evaluations/static-prompts/scripts/validate-dataset.ts index 608d5cdf9..f066b0672 100644 --- a/evaluations/static-prompts/scripts/validate-dataset.ts +++ b/evaluations/static-prompts/scripts/validate-dataset.ts @@ -183,6 +183,11 @@ function validateFamilyContract(row: DatasetCase, prefix: string, errors: string case "run-report": if (row.variant === "submission-readout") { assert(isRecord(input.evidence), `${prefix}: input.evidence is required`, errors); + } else if (row.variant === "submission-template") { + assert(stringField(input, "submissionId"), `${prefix}: input.submissionId is required`, errors); + assert(stringField(input, "submissionRunId"), `${prefix}: input.submissionRunId is required`, errors); + assert(stringField(input, "reportId"), `${prefix}: input.reportId is required`, errors); + assert(stringField(input, "userPrompt"), `${prefix}: input.userPrompt is required`, errors); } else { assert(stringField(input, "requestId"), `${prefix}: input.requestId is required`, errors); assert(stringField(input, "reportId"), `${prefix}: input.reportId is required`, errors); @@ -353,9 +358,10 @@ export async function validateDataset( row.variant === "default" || row.variant === "override-control" || row.variant === "submission-readout" + || row.variant === "submission-template" )) || (row.family === "developer-feedback" && row.variant === "persona")), - "synthetic cases are only permitted for unavailable judge evidence, feedback persona, report override controls, and submission readouts", + "synthetic cases are only permitted for unavailable judge evidence, feedback persona, report override controls, submission readouts, and submission templates", errors, ); const hasTag = (family: QualityFamily, tag: string): boolean => diff --git a/packages/shared/src/report-templates/prompt.test.ts b/packages/shared/src/report-templates/prompt.test.ts index 249ec548b..a3f21ffea 100644 --- a/packages/shared/src/report-templates/prompt.test.ts +++ b/packages/shared/src/report-templates/prompt.test.ts @@ -2,7 +2,10 @@ // Licensed under the MIT License. import { describe, it, expect } from 'vitest'; -import { REPORT_SYSTEM_PROMPT } from './prompt.js'; +import { + REPORT_SYSTEM_PROMPT, + SUBMISSION_REPORT_SYSTEM_PROMPT, +} from './prompt.js'; describe('REPORT_SYSTEM_PROMPT', () => { it('is a non-empty string', () => { @@ -10,6 +13,15 @@ describe('REPORT_SYSTEM_PROMPT', () => { expect(REPORT_SYSTEM_PROMPT.length).toBeGreaterThan(0); }); + describe('SUBMISSION_REPORT_SYSTEM_PROMPT', () => { + it('requires exact submission-attempt evidence', () => { + expect(SUBMISSION_REPORT_SYSTEM_PROMPT).toContain('exact, immutable submission attempt'); + expect(SUBMISSION_REPORT_SYSTEM_PROMPT).toContain('get_submission_summary'); + expect(SUBMISSION_REPORT_SYSTEM_PROMPT).toContain('get_submission_evidence'); + expect(SUBMISSION_REPORT_SYSTEM_PROMPT).toContain('Never substitute newer runs'); + }); + }); + it('describes the report generation task', () => { expect(REPORT_SYSTEM_PROMPT).toContain('report'); expect(REPORT_SYSTEM_PROMPT).toContain('agentic coding session'); diff --git a/packages/shared/src/report-templates/prompt.ts b/packages/shared/src/report-templates/prompt.ts index c812e5816..4502646df 100644 --- a/packages/shared/src/report-templates/prompt.ts +++ b/packages/shared/src/report-templates/prompt.ts @@ -71,3 +71,9 @@ Keep insight titles concise (one line). Use markdown in descriptions for detail. - \`> [!WARNING]\` for potential issues or pitfalls - \`> [!CAUTION]\` for critical problems or regressions `; + +export const SUBMISSION_REPORT_SYSTEM_PROMPT = `Your job is to analyze one exact, immutable submission attempt containing multiple agentic coding runs and produce a Markdown report. + +Use get_submission_summary to understand the selected attempt and its exact request/run membership. Use get_submission_evidence to inspect the schema-validated evidence generated for that same attempt. Preserve the distinction between individual run outcomes and cross-run patterns. Never substitute newer runs or evidence from another submission attempt. + +The report must follow the user's template instructions, remain grounded in the supplied evidence, and use GitHub Flavored Markdown. Do not mention tool use or internal processing in the report body.`; diff --git a/packages/shared/src/schemas/report-template.ts b/packages/shared/src/schemas/report-template.ts index 333e079cd..10bc44669 100644 --- a/packages/shared/src/schemas/report-template.ts +++ b/packages/shared/src/schemas/report-template.ts @@ -47,6 +47,7 @@ export const CreateReportTemplateInputSchema = z .object({ id: z.string(), name: z.string(), + subject: z.enum(["run", "submission"]).default("run"), description: z.string().optional(), userPrompt: z.string(), systemPrompt: ReportTemplateSystemPromptSchema.optional(), @@ -59,6 +60,7 @@ export const CreateReportTemplateInputSchema = z export const UpdateReportTemplateInputSchema = z .object({ name: z.string().optional(), + subject: z.enum(["run", "submission"]).optional(), description: z.string().optional(), userPrompt: z.string().optional(), systemPrompt: ReportTemplateSystemPromptSchema.nullable().optional(), @@ -73,6 +75,7 @@ export const ReportTemplateResponseSchema = z _id: z.string(), id: z.string(), name: z.string(), + subject: z.enum(["run", "submission"]).default("run"), description: z.string().optional(), userPrompt: z.string(), systemPrompt: ReportTemplateSystemPromptSchema.optional(), diff --git a/packages/shared/src/schemas/report.ts b/packages/shared/src/schemas/report.ts index afeb0ac5d..d3cbbe0e5 100644 --- a/packages/shared/src/schemas/report.ts +++ b/packages/shared/src/schemas/report.ts @@ -59,6 +59,18 @@ export const ReportResponseSchema = z updatedAt: z.coerce.date().optional(), projectId: z.string(), }) + .superRefine((report, context) => { + if ( + !report.requestId && + !(report.submissionId && report.submissionRunId) + ) { + context.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Report must identify either a request or an exact submission attempt", + }); + } + }) .openapi("ReportResponse"); export const CreateReportInputSchema = z @@ -87,6 +99,13 @@ export const TriggerReportsInputSchema = z }) .openapi("TriggerReportsInput"); +export const TriggerSubmissionReportsInputSchema = z + .object({ + submissionId: z.string(), + submissionRunId: z.string(), + }) + .openapi("TriggerSubmissionReportsInput"); + export const BulkTriggerReportsInputSchema = z .object({ requestIds: z.array(z.string()), diff --git a/packages/shared/src/schemas/submission.ts b/packages/shared/src/schemas/submission.ts index 3558ae603..6e6a80c36 100644 --- a/packages/shared/src/schemas/submission.ts +++ b/packages/shared/src/schemas/submission.ts @@ -87,6 +87,7 @@ export const SubmissionRunStateSchema = z handlerStatus: z.record(z.string(), SubmissionHandlerStatusSchema).optional(), blocking: z.array(SubmissionBlockingDetailSchema).default([]), report: SubmissionReportPointerSchema.optional(), + reportsTriggeredAt: z.coerce.date().optional(), reconcileAfter: z.coerce.date().optional(), rollover: SubmissionRolloverClaimSchema.optional(), supersededAt: z.coerce.date().optional(), diff --git a/packages/shared/src/types/types.ts b/packages/shared/src/types/types.ts index c921fe006..0c86fa6e4 100644 --- a/packages/shared/src/types/types.ts +++ b/packages/shared/src/types/types.ts @@ -651,6 +651,8 @@ export interface SubmissionRunState { handlerStatus?: Record; blocking: SubmissionBlockingDetail[]; report?: SubmissionReportPointer; + /** Exactly-once guard for automatic submission-scoped report templates. */ + reportsTriggeredAt?: Date; /** Internal scheduler hold used by bulk request retry so one completed * replacement batch produces one vector rollover. Expiry self-heals crashes. */ reconcileAfter?: Date; @@ -1015,6 +1017,7 @@ export interface ReportTemplateDocument { projectId: string; // FK → ProjectDocument._id (immutable scope) id: string; // Human-readable slug (e.g. "default", "failure-analysis") name: string; // Display name + subject?: "run" | "submission"; // Defaults to "run" for legacy templates description?: string; userPrompt: string; // REQUIRED — the instruction sent to the agent systemPrompt?: ReportTemplateSystemPrompt; // OPTIONAL — customize base system prompt diff --git a/website/src/openapi/scope-openapi.json b/website/src/openapi/scope-openapi.json index 7a9426193..5c6583cda 100644 --- a/website/src/openapi/scope-openapi.json +++ b/website/src/openapi/scope-openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"Scope API","version":"1.0.0","description":"REST API for the Scope platform — benchmarking AI coding agents"},"servers":[{"url":"/","description":"Current server"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Use the unchanged IdP access token, without the Bearer prefix."}},"schemas":{"KeyResponse":{"type":"object","properties":{},"additionalProperties":{}},"KeyInput":{"type":"object","properties":{},"additionalProperties":{}},"ValidateKeyInput":{"type":"object","properties":{"token":{"type":"string"}},"required":["token"]},"AccountResponse":{"type":"object","properties":{},"additionalProperties":{}},"AccountInput":{"type":"object","properties":{},"additionalProperties":{}},"UserMeResponse":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string"},"email":{"type":"string"},"displayName":{"type":"string"},"idp":{"type":"string"},"idpTenant":{"type":"string"}},"required":["id"]},"ProjectResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","createdAt"]},"CreateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"creator":{"type":"string"}},"required":["name"]},"UpdateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"SubmissionListResponse":{"type":"object","properties":{"submissions":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionResponse"}}},"required":["submissions"]},"SubmissionResponse":{"type":"object","properties":{"submissionId":{"type":"string"},"projectId":{"type":"string"},"requestIds":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"submissionRunId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["waiting","ready","blocked","processing","done","failed"]},"requests":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRequestSummary"}},"dependencyProjection":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionDependencyProjection"}},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"}},"handlerProgress":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/SubmissionHandlerStatus"}},"report":{"$ref":"#/components/schemas/SubmissionReportPointer"},"supersededAt":{"type":["string","null"],"format":"date-time"},"supersededBySubmissionRunId":{"type":"string"},"supersededReason":{"type":"string","enum":["request-vector-changed","manual-retry"]}},"required":["submissionId","projectId","requestIds","createdAt","submissionRunId","attemptNumber","readiness","requests","dependencyProjection","blocking"]},"SubmissionRequestSummary":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"task":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"},"workerType":{"type":"string"},"model":{"type":"string"},"profileId":{"type":"string"}},"required":["requestId","runId","task","status","workerType"]},"SubmissionDependencyProjection":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"state":{"type":"string","enum":["waiting","ready","failed","blocked"]},"version":{"type":"number"},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["requestId","runId","handlerId","state"]},"SubmissionBlockingDetail":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"reason":{"type":"string","enum":["request-missing","run-changed","dependency-not-ready","dependency-failed","dependency-blocked"]},"error":{"type":"string"}},"required":["requestId","runId","handlerId","reason"]},"SubmissionHandlerStatus":{"type":"object","properties":{"status":{"type":"string","enum":["queued","processing","done","failed"]},"version":{"type":"number"},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["status"]},"SubmissionReportPointer":{"type":"object","properties":{"reportId":{"type":"string"},"url":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"}}},"ReportResponse":{"type":"object","properties":{"_id":{"type":"string"},"requestId":{"type":"string"},"submissionId":{"type":"string"},"submissionRunId":{"type":"string"},"templateId":{"type":"string"},"reporter":{"$ref":"#/components/schemas/Reporter"},"content":{"type":"string"},"structuredArtifact":{"$ref":"#/components/schemas/StructuredReportArtifact"},"status":{"type":"string","enum":["pending","generating","completed","failed"]},"error":{"type":"string"},"insightReferences":{"type":"array","items":{"$ref":"#/components/schemas/InsightReference"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","status","createdAt","projectId"]},"Reporter":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"gitHash":{"type":"string"},"model":{"type":"string"},"agentId":{"type":"string"},"agentVersion":{"type":"string"}},"required":["id","name","gitHash","model","agentId","agentVersion"]},"StructuredReportArtifact":{"type":"object","properties":{"schemaId":{"type":"string"},"schemaVersion":{"type":"string"},"contentType":{"type":"string","enum":["application/json"]},"url":{"type":"string"}},"required":["schemaId","schemaVersion","contentType","url"]},"InsightReference":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]},"SubmissionRunResponse":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"vectorFingerprint":{"type":"string"},"requests":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRequestRef"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"dependencyProjection":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionDependencyProjection"},"default":[]},"handlerStatus":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/SubmissionHandlerStatus"}},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"},"default":[]},"report":{"$ref":"#/components/schemas/SubmissionReportPointer"},"supersededAt":{"type":["string","null"],"format":"date-time"},"supersededBySubmissionRunId":{"type":"string"},"supersededReason":{"type":"string","enum":["request-vector-changed","manual-retry"]},"submissionId":{"type":"string"},"projectId":{"type":"string"}},"required":["_id","attemptNumber","vectorFingerprint","requests","createdAt"]},"SubmissionRequestRef":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"}},"required":["requestId","runId"]},"SubmissionRetryResponse":{"type":"object","properties":{"submissionId":{"type":"string"},"submissionRunId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["waiting","ready","blocked","processing","done","failed"]},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"}}},"required":["submissionId","submissionRunId","attemptNumber","readiness","blocking"]},"RequestResponse":{"type":"object","properties":{"_id":{"type":"string"},"scenario":{"$ref":"#/components/schemas/Scenario"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"maxIterations":{"type":"number"},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"deletedAt":{"type":["string","null"],"format":"date-time"},"taskPromptId":{"type":"string"},"agentsMdPromptId":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBinding"}},"extensions":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"profileId":{"type":"string"},"profileVersionId":{"type":"string"},"submissionId":{"type":"string"},"submissionExpectedRequestCount":{"type":"integer","minimum":1},"submissionRequestIndex":{"type":"integer","minimum":0},"submissionInitialRunId":{"type":"string"},"submissionFinalizedAt":{"type":["string","null"],"format":"date-time"},"priority":{"type":"integer","default":0},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"observations":{"type":"array","items":{"type":"string"}},"gateSummaries":{"type":"array","items":{"$ref":"#/components/schemas/GateRunSummary"}},"run":{"$ref":"#/components/schemas/RunState"},"projectId":{"type":"string"}},"required":["_id","scenario","workerType","createdAt","projectId"]},"Scenario":{"type":"object","properties":{"version":{"type":"string","enum":["v1","v2"]},"task":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}}},"required":["task","criteria"]},"Persona":{"type":"object","properties":{"personality":{"type":"string","enum":["demanding","friendly"]},"experience":{"type":"string","enum":["junior","senior"]},"verbosity":{"type":"string","enum":["brief","moderate"]},"type":{"type":"string","enum":["traditional","ai_assisted","vibe"]}},"required":["personality","experience","verbosity","type"]},"ResourceBinding":{"type":"object","properties":{"ref":{"type":"string"},"revisionId":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref","revisionId","params"]},"GateConfig":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"promptId":{"type":"string"},"promptText":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}},"maxIterations":{"type":"integer","minimum":1,"maximum":50}},"required":["gate","criteria"]},"GateRunSummary":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"status":{"type":"string","enum":["passed","failed","skipped"]},"iterations":{"type":"integer","minimum":0}},"required":["gate","status","iterations"]},"RunState":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"queuedQueueName":{"type":"string"},"outcome":{"type":"string","enum":["succeeded","failed","finished"]},"result":{"type":"string"},"error":{"type":"string"},"logsUrl":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"turns":{"type":"array","items":{"$ref":"#/components/schemas/ConversationTurn"}},"workerVersion":{"type":"string"},"os":{"type":"object","properties":{"platform":{"type":"string"},"release":{"type":"string"},"arch":{"type":"string"}},"required":["platform","release","arch"]},"lastHeartbeatAt":{"type":["string","null"],"format":"date-time"},"worker":{"type":"object","properties":{"instanceId":{"type":"string"},"podName":{"type":"string"}},"required":["instanceId"]},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"setupVideoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"aiCallCount":{"type":"number"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRunOutcome"}},"mcpRegistered":{"type":"boolean"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"pausedAt":{"type":["string","null"],"format":"date-time"},"resumedAt":{"type":["string","null"],"format":"date-time"},"observationResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}}},"required":["_id","attemptNumber","status"]},"ConversationTurn":{"type":"object","properties":{"iteration":{"type":"number"},"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"codingAgentResponse":{"type":"string"},"judgeFeedback":{"type":"string"},"snapshotUrl":{"type":"string"},"passed":{"type":"boolean"},"timestamp":{"type":["string","null"],"format":"date-time"},"criteriaResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"observationResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"startedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"toolCalls":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"arguments":{"type":"object","additionalProperties":{}},"response":{"type":"string"},"timestamp":{"type":"string"}},"required":["id","name","arguments"]}},"toolCallsUrl":{"type":"string"},"toolCallCount":{"type":"number"},"aiCallCount":{"type":"number"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"chatResultUrl":{"type":"string"},"chatResultFormat":{"type":"string"}},"required":["iteration","judgeFeedback","snapshotUrl","passed","timestamp"]},"CriterionResult":{"type":"object","properties":{"criterionId":{"type":"string"},"passed":{"type":"boolean"},"feedback":{"type":"string"},"evaluated":{"type":"boolean"}},"required":["criterionId","passed","feedback","evaluated"]},"TokenUsage":{"type":"object","properties":{"promptTokens":{"type":"number"},"completionTokens":{"type":"number"},"totalTokens":{"type":"number"}},"required":["promptTokens","completionTokens","totalTokens"]},"ResourceRunOutcome":{"type":"object","properties":{"ref":{"type":"string"},"slug":{"type":"string"},"revisionId":{"type":"string"},"setupSucceeded":{"type":"boolean"},"published":{"type":"array","items":{"type":"string"}},"params":{"type":"object","additionalProperties":{"type":"string"}},"setupDurationMs":{"type":"number"},"error":{"type":"string"},"teardownRan":{"type":"boolean"}},"required":["ref","slug","revisionId","setupSucceeded","published"]},"ResourceBindingSpec":{"anyOf":[{"type":"string"},{"type":"object","properties":{"ref":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref"]}]},"CreateRequestInput":{"type":"object","properties":{"scenario":{"$ref":"#/components/schemas/Scenario"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"maxIterations":{"type":"integer","minimum":1,"maximum":50},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"profileId":{"type":"string"},"profileVariations":{"type":"array","items":{"type":"string"}},"priority":{"type":"integer"},"agentsMd":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"observations":{"type":"array","items":{"type":"string"}}},"required":["scenario"]},"RunFacetsResponse":{"type":"object","properties":{"total":{"type":"number"},"facets":{"type":"object","properties":{"workerType":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"status":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"outcome":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"model":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"os":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"priority":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"agentVersion":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"profileId":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}}},"required":["workerType","status","outcome","model","os","priority","agentVersion","profileId"]}},"required":["total","facets"]},"RunFacetBucket":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"number"}},"required":["value","count"]},"PaginatedRunsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"Cursors":{"type":"object","properties":{"next":{"type":["string","null"]},"prev":{"type":["string","null"]}},"required":["next","prev"]},"PaginatedRunGroupsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RunGroup"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"RunGroup":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"runIds":{"type":"array","items":{"type":"string"}},"aggregates":{"$ref":"#/components/schemas/GroupAggregates"},"uniform":{"$ref":"#/components/schemas/GroupUniformValues"}},"required":["key","label","runIds","aggregates","uniform"]},"GroupAggregates":{"type":"object","properties":{"count":{"type":"number"},"turns":{"$ref":"#/components/schemas/AggregateStats"},"duration":{"$ref":"#/components/schemas/AggregateStats"},"promptTokens":{"$ref":"#/components/schemas/AggregateStats"},"completionTokens":{"$ref":"#/components/schemas/AggregateStats"},"statusCounts":{"type":"object","additionalProperties":{"type":"number"}},"outcomeCounts":{"type":"object","additionalProperties":{"type":"number"}}},"required":["count","turns","duration","promptTokens","completionTokens","statusCounts","outcomeCounts"]},"AggregateStats":{"type":["object","null"],"properties":{"min":{"type":"number"},"max":{"type":"number"},"mean":{"type":"number"},"stdDev":{"type":"number"}},"required":["min","max","mean","stdDev"]},"GroupUniformValues":{"type":"object","properties":{"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":"string"},"platform":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"extensions":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"submissionId":{"type":"string"},"task":{"type":"string"}}},"BulkResubmitInput":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"count":{"type":"integer","minimum":1,"maximum":10,"default":1},"overrides":{"type":"object","properties":{"profileId":{"type":["string","null"]},"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":["string","null"]},"reasoningEffort":{"type":["string","null"]},"maxIterations":{"type":["number","null"]},"mcpServers":{"type":["array","null"],"items":{"type":"string"}},"skillRevisions":{"type":["array","null"],"items":{"type":"string"}},"extensions":{"type":["array","null"],"items":{"type":"string"}}}}},"required":["ids"]},"CreateCriteriaInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"],"default":"gate"},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}},"required":["id","prompt"]},"CriteriaResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"CriteriaGraph":{"type":"object","properties":{"nodes":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphNode"}},"edges":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphEdge"}}},"required":["nodes","edges"]},"CriteriaGraphNode":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}},"required":["id","prompt"]},"CriteriaGraphEdge":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"]},"UpdateCriteriaInput":{"type":"object","properties":{"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}}},"CreatePromptFeatureInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["id","prompt"]},"PromptType":{"type":"string","enum":["select","build","test","run","deploy","agents.md"]},"PromptFeatureResult":{"type":"object","properties":{"featureId":{"type":"string"},"detected":{"type":"boolean"},"evaluated":{"type":"boolean"}},"required":["featureId","detected","evaluated"]},"SuggestedPromptFeature":{"type":"object","properties":{"suggestedId":{"type":"string"},"behavior":{"type":"string"},"prompt":{"type":"string"}},"required":["suggestedId","behavior","prompt"]},"PromptFeatureResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"UpdatePromptFeatureInput":{"type":"object","properties":{"prompt":{"type":"string"}}},"TaskPromptResponse":{"type":"object","properties":{"_id":{"type":"string"},"keyId":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"text":{"type":"string"},"contentBlobUrl":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","keyId","createdAt","projectId"]},"CreateTaskPromptInput":{"type":"object","properties":{"text":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["text"]},"PatchTaskPromptFeatureInput":{"type":"object","properties":{"detected":{"type":"boolean"}},"required":["detected"]},"CreateReportInput":{"type":"object","properties":{"requestId":{"type":"string"},"templateId":{"type":"string"}},"required":["requestId"]},"BulkCreateReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}},"templateId":{"type":"string"}},"required":["requestIds"]},"BulkReportStatusInput":{"type":"object","properties":{"reportIds":{"type":"array","items":{"type":"string"}}},"required":["reportIds"]},"BulkReportSummaryResponse":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/ReportSummary"}},"ReportSummary":{"type":"object","properties":{"total":{"type":"number"},"pending":{"type":"number"},"generating":{"type":"number"},"completed":{"type":"number"},"failed":{"type":"number"}},"required":["total","pending","generating","completed","failed"]},"BulkReportSummaryInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"TriggerReportsInput":{"type":"object","properties":{"requestId":{"type":"string"}},"required":["requestId"]},"BulkTriggerReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"InsightResponse":{"type":"object","properties":{"_id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"upvotes":{"type":"number"},"downvotes":{"type":"number"},"blocked":{"type":"boolean"},"referenceCount":{"type":"number"},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","title","description","upvotes","downvotes","blocked","referenceCount","createdBy","createdAt","projectId"]},"ProfileWithVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"version":{"$ref":"#/components/schemas/ProfileVersionResponse"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","version","projectId"]},"ProfileVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"profileId":{"type":"string"},"version":{"type":"number"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","profileId","version","workerType","model","createdAt","projectId"]},"CreateProfileInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["name","workerType","model"]},"ProfileResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","projectId"]},"UpdateProfileIdentity":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512}}},"ReportTemplateResponse":{"type":"object","properties":{"_id":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","id","name","userPrompt","createdAt","projectId"]},"ReportTemplateSystemPrompt":{"type":"object","properties":{"mode":{"type":"string","enum":["append","override"]},"content":{"type":"string"}},"required":["mode","content"]},"ReportTrigger":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["always"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["criteria"]},"criteriaIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","criteriaIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["taskPrompt"]},"taskPromptIds":{"type":"array","items":{"type":"string"}}},"required":["type","taskPromptIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["promptFeature"]},"featureIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","featureIds"]}]},"CreateReportTemplateInput":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0}},"required":["id","name","userPrompt"]},"UpdateReportTemplateInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"allOf":[{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},{"type":["object","null"]}]},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":["string","null"]},"timeoutMs":{"type":["integer","null"],"exclusiveMinimum":0}}},"AgentResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"},"versions":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","supportedModels","createdAt"]},"AgentCapabilities":{"type":"object","properties":{"supportsReasoningEffort":{"type":"boolean"},"supportsMcpServers":{"type":"boolean"},"supportsSkills":{"type":"boolean"},"supportsExtensions":{"type":"boolean"},"supportsResources":{"type":"boolean"}}},"AgentVersion":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string"},"status":{"type":"string","enum":["active","retired"]},"createdAt":{"type":["string","null"],"format":"date-time"}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","status","createdAt"]},"CreateAgentInput":{"type":"object","properties":{"_id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}},"required":["_id","name"]},"UpdateAgentInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}}},"RegisterAgentVersionInput":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string","minLength":1}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","queueName"]},"PatchAgentVersionInput":{"type":"object","properties":{"status":{"type":"string","enum":["active","retired"]}},"required":["status"]},"ModelResponse":{"type":"object","properties":{"_id":{"type":"string"},"modelId":{"type":"string"},"provider":{"type":"string"},"agentId":{"type":"string"},"firstSeenAt":{"type":["string","null"],"format":"date-time"},"lastSeenAt":{"type":["string","null"],"format":"date-time"},"disappearedAt":{"type":["string","null"],"format":"date-time"},"providerAvailableFrom":{"type":["string","null"],"format":"date-time"},"providerEndOfLife":{"type":["string","null"],"format":"date-time"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"$ref":"#/components/schemas/ModelCapabilities"}},"required":["_id","modelId","provider","agentId","firstSeenAt","lastSeenAt"]},"ModelCapabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}},"McpServerResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","type","createdAt","projectId"]},"McpServerHeader":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}},"required":["name","value"]},"UpdateMcpServerInput":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}}},"SkillResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","source","skillName","name","origin","createdAt","projectId"]},"SkillSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"source":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"installs":{"type":"number"}},"required":["id","name","source","internal"]},"SkillDiscoveryResult":{"type":"object","properties":{"skillName":{"type":"string"},"skillPath":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"existsInLibrary":{"type":"boolean"},"currentRevisionCommitSha":{"type":"string"},"latestUpstreamCommitSha":{"type":"string"},"updateAvailable":{"type":"boolean"},"lastImportedAt":{"type":"string"}},"required":["skillName","skillPath"]},"SkillRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"ref":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"skillPath":{"type":"string"},"commitHash":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"name":{"type":"string"},"description":{"type":"string"},"license":{"type":"string"},"compatibility":{"type":"string"},"allowedTools":{"type":"string"},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"content":{"type":"string"},"archiveUrl":{"type":"string"},"validationWarnings":{"type":"array","items":{"type":"string"}},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","ref","source","skillName","skillPath","commitHash","commitTimestamp","name","description","content","archiveUrl","resolvedAt","createdAt","projectId"]},"CreateSkillInput":{"type":"object","properties":{"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]}},"required":["source","skillName","name","origin"]},"CodebaseResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"defaultBranch":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","sourceType","revisionCounter","createdAt","projectId"]},"UpdateCodebaseInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"defaultBranch":{"type":"string"}}},"CodebaseRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"codebaseId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"requestedRef":{"type":"string"},"resolvedCommitSha":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"originalFilename":{"type":"string"},"contentSha256":{"type":"string"},"archiveUrl":{"type":"string"},"sizeBytes":{"type":"number"},"fileCount":{"type":"number"},"creator":{"type":"string"},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","codebaseId","slug","revisionNumber","ref","sourceType","archiveUrl","resolvedAt","createdAt","projectId"]},"ResolveCodebaseRevisionInput":{"type":"object","properties":{"requestedRef":{"type":"string"},"creator":{"type":"string"}}},"ResourceResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"latestRevisionNumber":{"type":"number"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","revisionCounter","createdAt","projectId"]},"CreateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1},"description":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["name","setup"]},"ResourceScript":{"type":"object","properties":{"sh":{"type":"string","minLength":1}}},"ResourceParameter":{"type":"object","properties":{"name":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"description":{"type":"string"},"required":{"type":"boolean","default":false},"default":{"type":"string"},"example":{"type":"string"}},"required":["name"]},"UpdateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"ResourceRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"resourceId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"contentSha256":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","resourceId","slug","revisionNumber","ref","setup","exports","contentSha256","createdAt","projectId"]},"CreateResourceRevisionInput":{"type":"object","properties":{"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["setup"]},"ExtensionResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","publisher","name","origin","createdAt","projectId"]},"ExtensionSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"publisher":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"version":{"type":"string"}},"required":["id","name","publisher","internal"]},"CreateExtensionInput":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+$"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]}},"required":["_id","publisher","name","origin"]},"UpdateExtensionInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ExtensionVersionInfo":{"type":"object","properties":{"version":{"type":"string"},"preRelease":{"type":"boolean"},"lastUpdated":{"type":"string"}},"required":["version","preRelease","lastUpdated"]},"CreateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"}},"required":["title","description"]},"UpdateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}}}},"FeatureFlagResponse":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"enabled":{"type":"boolean"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["key","label","enabled","updatedAt"]},"UpdateFeatureFlagInput":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]}},"parameters":{}},"paths":{"/api/v1/keys/preview":{"post":{"tags":["Keys"],"summary":"Preview key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}}},"/api/v1/keys":{"post":{"tags":["Keys"],"summary":"Create key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"201":{"description":"Key created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"get":{"tags":["Keys"],"summary":"List keys","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KeyResponse"}}}}}}}},"/api/v1/keys/{id}":{"get":{"tags":["Keys"],"summary":"Get key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"put":{"tags":["Keys"],"summary":"Update key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"delete":{"tags":["Keys"],"summary":"Delete key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Key deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/keys/{id}/validate":{"post":{"tags":["Keys"],"summary":"Validate key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateKeyInput"}}}},"responses":{"200":{"description":"Validation result","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Key validation result"}}}}}}},"/api/v1/accounts":{"post":{"tags":["Accounts"],"summary":"Create account","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"201":{"description":"Account created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"get":{"tags":["Accounts"],"summary":"List accounts","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccountResponse"}}}}}}}},"/api/v1/accounts/{id}":{"get":{"tags":["Accounts"],"summary":"Get account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"put":{"tags":["Accounts"],"summary":"Update account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"200":{"description":"Account updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"delete":{"tags":["Accounts"],"summary":"Delete account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Account deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/users/me":{"get":{"tags":["Users"],"summary":"Get the authenticated user's identity","description":"Read the existing authenticated Scope identity without enrollment or profile writes. Responses must not be HTTP-cached.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is not enrolled or is disabled"},"503":{"description":"Authentication service unavailable"}}},"post":{"tags":["Users"],"summary":"Enroll the authenticated user","description":"JIT-enroll the authenticated user, refresh their profile and lastLoginAt, apply bootstrap-admin rules, and warm the access cache. Use this only after an explicit IdP login; do not prefetch, poll, or automatically retry transient failures.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is disabled"},"503":{"description":"Authentication service unavailable"}}}},"/health":{"get":{"tags":["Health"],"summary":"Liveness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"version":{"type":"string"}},"required":["status","version"]}}}}}}},"/ready":{"get":{"tags":["Health"],"summary":"Readiness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"migrations":{}},"required":["status"]}}}},"503":{"description":"Service is not ready"}}}},"/about":{"get":{"tags":["System"],"summary":"API metadata","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"version":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"description":{"type":"string"},"workers":{"type":"array","items":{"type":"string"}}},"required":["name","version","buildTime","environment","description","workers"]}}}}}}},"/api/v1/version":{"get":{"tags":["System"],"summary":"Version info","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"commit":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"strictAgentCapabilities":{"type":"boolean"}},"required":["commit","buildTime","environment","strictAgentCapabilities"]}}}}}}},"/api/v1/projects":{"get":{"tags":["Projects"],"summary":"List all projects","parameters":[{"schema":{"type":"string","enum":["true","false"],"description":"When true, include soft-deleted projects in the result"},"required":false,"description":"When true, include soft-deleted projects in the result","name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProjectResponse"}}}}}}},"post":{"tags":["Projects"],"summary":"Create a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/projects/{id}":{"get":{"tags":["Projects"],"summary":"Get a project","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"patch":{"tags":["Projects"],"summary":"Update a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProjectInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"delete":{"tags":["Projects"],"summary":"Soft-delete a project","responses":{"204":{"description":"Success"},"404":{"description":"Project not found"}}}},"/api/v1/projects/{id}/restore":{"post":{"tags":["Projects"],"summary":"Restore a soft-deleted project","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}}},"/api/v1/submissions":{"get":{"tags":["Submissions"],"summary":"List submissions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionListResponse"}}}}}}},"/api/v1/submissions/{id}/runs/{submissionRunId}/logs":{"get":{"tags":["Submissions"],"summary":"Stream logs for a submission attempt (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"submissionRunId","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Submission or submission attempt not found"}}}},"/api/v1/submissions/{id}/reports":{"get":{"tags":["Reports"],"summary":"Get reports for submission","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}":{"get":{"tags":["Submissions"],"summary":"Get a submission","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionResponse"}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}/runs":{"get":{"tags":["Submissions"],"summary":"List current and historical submission attempts","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"runs":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRunResponse"}}},"required":["runs"]}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}/runs/{submissionRunId}":{"get":{"tags":["Submissions"],"summary":"Get one submission attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"submissionRunId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionRunResponse"}}}},"404":{"description":"Submission attempt not found"}}}},"/api/v1/submissions/{id}/retry":{"post":{"tags":["Submissions"],"summary":"Retry current submission processing","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"submissionRunId":{"type":"string"}},"required":["submissionRunId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionRetryResponse"}}}},"404":{"description":"Submission not found"},"409":{"description":"Submission attempt is stale or dependencies are blocked"}}}},"/api/v1/requests":{"post":{"tags":["Requests"],"summary":"Submit request(s)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/CreateRequestInput"},{"type":"object","properties":{"count":{"type":"number","minimum":1,"maximum":10,"default":1},"skills":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"codebase":{"type":"string"}}}]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RequestResponse"},{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}]}}}}}},"get":{"tags":["Requests"],"summary":"List requests","parameters":[{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"profileId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"status","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"outcome","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"model","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"os","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"priority","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"agentVersion","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"type":"string","enum":["task","submissionId","profile"]},"required":false,"name":"groupBy","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"after","in":"query"},{"schema":{"type":"string"},"required":false,"name":"before","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"last","in":"query"},{"schema":{"type":"string","enum":["created","updated","priority","worker","status","id","duration","createdAt"]},"required":false,"name":"sortBy","in":"query"},{"schema":{"type":"string","enum":["asc","desc"]},"required":false,"name":"sortDir","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdAfter","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdBefore","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"turns","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"turnsOp","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"maxIterations","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"maxIterationsOp","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PaginatedRunsResponse"},{"$ref":"#/components/schemas/PaginatedRunGroupsResponse"}]}}}}}}},"/api/v1/requests/facets":{"get":{"tags":["Requests"],"summary":"List run filter facets","description":"Returns every distinct value and its full-dataset count per categorical filter dimension for the Runs list rail. Counts are absolute over all non-deleted runs **in the given project**: they intentionally ignore the active search, date range, iteration, and categorical selections so every selectable value stays visible with a stable count. Requires ?projectId=. Computed with parallel $group aggregations (Cosmos has no $facet) and cached per-project for a short TTL.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunFacetsResponse"}}}}}}},"/api/v1/requests/{id}":{"get":{"tags":["Requests"],"summary":"Get request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestResponse"}}}},"404":{"description":"Not found"}}},"delete":{"tags":["Requests"],"summary":"Soft-delete request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Not found"}}}},"/api/v1/analysis":{"get":{"tags":["Requests"],"summary":"Compute pass@k / success@T metrics","description":"Aggregates pass@k / success@T metrics over a single project's done runs. Requires ?projectId=.","parameters":[{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"k","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Analysis metrics"}}}}}}},"/api/v1/requests/bulk-resubmit":{"post":{"tags":["Requests"],"summary":"Bulk resubmit requests","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkResubmitInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}}}}}}},"/api/v1/requests/bulk":{"delete":{"tags":["Requests"],"summary":"Bulk soft-delete requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}},"required":["ids"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"number"}},"required":["deleted"]}}}}}}},"/api/v1/requests/archive":{"post":{"tags":["Requests"],"summary":"Download batch archive of multiple runs","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Gzipped batch archive containing individual run archives"},"400":{"description":"Invalid input"},"404":{"description":"One or more runs not found"}}}},"/api/v1/runs/upload":{"post":{"tags":["Requests"],"summary":"Import run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"}}}},"/api/v1/runs/upload-batch":{"post":{"tags":["Requests"],"summary":"Import batch run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"207":{"description":"Multi-status: 201 if all runs imported, 400 if none imported, 207 if partial"},"400":{"description":"No archive uploaded, empty archive, or all runs failed"}}}},"/api/v1/requests/{id}/reports":{"get":{"tags":["Reports"],"summary":"Get reports for request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Run not found"}}}},"/api/v1/requests/{id}/runs":{"get":{"tags":["Requests"],"summary":"List attempts for a request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RunState"}}}}},"404":{"description":"Request not found"}}}},"/api/v1/requests/{id}/runs/{runId}":{"get":{"tags":["Requests"],"summary":"Get a single attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunState"}}}},"404":{"description":"Request or run not found"}}}},"/api/v1/requests/bulk-retry":{"post":{"tags":["Requests"],"summary":"Bulk retry requests (start new attempts)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"force":{"type":"boolean"}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"retried":{"type":"integer"},"skipped":{"type":"integer"},"results":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"},"error":{"type":"string"}},"required":["requestId"]}}},"required":["retried","skipped","results"]}}}}}}},"/api/v1/requests/{id}/retry":{"post":{"tags":["Requests"],"summary":"Retry a request (start a new attempt)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"force":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"}},"required":["requestId","runId","attemptNumber"]}}}},"404":{"description":"Request not found"},"409":{"description":"Conflict — request is not in a retryable state, or a concurrent retry won the race"},"422":{"description":"Cannot retry — current run not yet terminal"}}}},"/api/v1/requests/{id}/priority":{"post":{"tags":["Requests"],"summary":"Set priority on a single request","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"priority":{"type":"integer"}},"required":["priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"priority":{"type":"number"}},"required":["id","priority"]}}}}}}},"/api/v1/requests/bulk-priority":{"post":{"tags":["Requests"],"summary":"Set priority on multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100},"priority":{"type":"integer"}},"required":["ids","priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/pause":{"post":{"tags":["Requests"],"summary":"Pause a single request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/{id}/resume":{"post":{"tags":["Requests"],"summary":"Resume a paused request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/bulk-pause":{"post":{"tags":["Requests"],"summary":"Pause multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/bulk-resume":{"post":{"tags":["Requests"],"summary":"Resume multiple paused requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/cancel":{"post":{"tags":["Requests"],"summary":"Cancel a request (marks as done/failed and signals worker to exit)","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"previousStatus":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"}},"required":["id","previousStatus","status","outcome"]}}}}}}},"/api/v1/requests/bulk-cancel":{"post":{"tags":["Requests"],"summary":"Cancel multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"cancelled":{"type":"number"},"skipped":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"cancelled":{"type":"boolean"},"previousStatus":{"type":"string"},"error":{"type":"string"}},"required":["id","cancelled"]}}},"required":["cancelled","skipped","results"]}}}}}}},"/api/v1/requests/{id}/logs":{"get":{"tags":["Requests"],"summary":"Stream request logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/logs":{"get":{"tags":["Requests"],"summary":"Stream logs for a specific attempt (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific attempt and iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/video":{"get":{"tags":["Requests"],"summary":"Download session recording","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/video":{"get":{"tags":["Requests"],"summary":"Download session recording for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/criteria/generate-prompt":{"post":{"tags":["Criteria"],"summary":"Generate criterion prompt from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"},"gates":{"type":"array","items":{"type":"string"}}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"},"suggestedId":{"type":"string"},"suggestedParents":{"type":"array","items":{"type":"string"}},"suggestedChildren":{"type":"array","items":{"type":"string"}}},"required":["prompt","suggestedId","suggestedParents","suggestedChildren"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/criteria/seed":{"post":{"tags":["Criteria"],"summary":"Seed criteria in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"criteria":{"type":"array","items":{"$ref":"#/components/schemas/CreateCriteriaInput"}}},"required":["criteria"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}},"400":{"description":"Seed batch would introduce a dependency cycle"}}}},"/api/v1/criteria":{"get":{"tags":["Criteria"],"summary":"List criteria","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","description":"Comma-separated criterion IDs to include"},"required":false,"description":"Comma-separated criterion IDs to include","name":"ids","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"When true and ids is set, also include dependency ancestors"},"required":false,"description":"When true and ids is set, also include dependency ancestors","name":"ancestors","in":"query"},{"schema":{"type":"string","enum":["gate","observation"],"description":"Filter by criterion kind"},"required":false,"description":"Filter by criterion kind","name":"kind","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaResponse"}}}}}}},"post":{"tags":["Criteria"],"summary":"Create criterion","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCriteriaInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"409":{"description":"Criterion already exists"}}}},"/api/v1/criteria/mdp":{"get":{"tags":["Criteria"],"summary":"Compute MDP transitions","parameters":[{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"since","in":"query"},{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{}}}}}}}},"/api/v1/criteria/graph":{"get":{"tags":["Criteria"],"summary":"Get criteria DAG","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaGraph"}}}}}}},"/api/v1/criteria/{id}":{"get":{"tags":["Criteria"],"summary":"Get criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"404":{"description":"Criterion not found"}}},"put":{"tags":["Criteria"],"summary":"Update criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCriteriaInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"400":{"description":"Invalid dependency reference or self-reference"},"404":{"description":"Criterion not found"}}},"delete":{"tags":["Criteria"],"summary":"Soft-delete criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Criterion not found"},"409":{"description":"Criterion has dependents"}}}},"/api/v1/prompt-features/generate-prompt":{"post":{"tags":["Prompt Features"],"summary":"Generate prompt feature from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"}},"required":["prompt"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features/seed":{"post":{"tags":["Prompt Features"],"summary":"Seed prompt features in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}},"required":["features"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}}}}},"/api/v1/prompt-features/extract-from-text":{"post":{"tags":["Prompt Features"],"summary":"Extract features from text","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string"},"model":{"type":"string"},"type":{"type":"string","enum":["select","agents.md"]}},"required":["text"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["features","cached"]}}}},"400":{"description":"Empty text string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features":{"get":{"tags":["Prompt Features"],"summary":"List features","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","enum":["select","agents.md"]},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}}}}},"post":{"tags":["Prompt Features"],"summary":"Create feature","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"409":{"description":"Feature already exists"}}}},"/api/v1/prompt-features/{id}":{"get":{"tags":["Prompt Features"],"summary":"Get feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"404":{"description":"Feature not found"}}},"put":{"tags":["Prompt Features"],"summary":"Update feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Feature not found"}}},"delete":{"tags":["Prompt Features"],"summary":"Soft-delete feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Feature not found"}}}},"/api/v1/task-prompts/generate":{"post":{"tags":["Task Prompts"],"summary":"Generate task prompts","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string"},"existingPrompt":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"tasks":{"type":"array","items":{"type":"string"}}},"required":["tasks"]}}}},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts":{"get":{"tags":["Task Prompts"],"summary":"List task prompts","parameters":[{"schema":{"type":["number","null"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":["number","null"]},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"$ref":"#/components/schemas/PromptType"},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/TaskPromptResponse"}},"total":{"type":"number"},"limit":{"type":"number"},"offset":{"type":"number"}},"required":["items","total","limit","offset"]}}}}}},"post":{"tags":["Task Prompts"],"summary":"Create or find task prompt","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTaskPromptInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Empty text string"}}}},"/api/v1/task-prompts/{id}":{"get":{"tags":["Task Prompts"],"summary":"Get task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"404":{"description":"Task prompt not found"}}},"delete":{"tags":["Task Prompts"],"summary":"Soft-delete task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/content":{"get":{"tags":["Task Prompts"],"summary":"Get resolved task prompt content","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"}},"required":["id","text"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/extract-features":{"post":{"tags":["Task Prompts"],"summary":"Extract features from task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"force","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"taskPromptId":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["taskPromptId","features","cached"]}}}},"404":{"description":"Task prompt not found"},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts/{id}/features/{featureId}":{"patch":{"tags":["Task Prompts"],"summary":"Toggle feature flag on task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"featureId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchTaskPromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Invalid detected value"},"404":{"description":"Task prompt or feature not found"}}}},"/api/v1/reports":{"post":{"tags":["Reports"],"summary":"Create report","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Run or template not found"}}},"get":{"tags":["Reports"],"summary":"List reports","parameters":[{"schema":{"type":"string"},"required":false,"name":"requestId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionRunId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}}}}},"/api/v1/reports/bulk-create":{"post":{"tags":["Reports"],"summary":"Bulk create reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}/structured":{"get":{"tags":["Reports"],"summary":"Download a report's structured JSON artifact","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success"},"404":{"description":"Report or structured artifact not found"}}}},"/api/v1/reports/bulk-status":{"post":{"tags":["Reports"],"summary":"Bulk get report statuses","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportStatusInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/bulk-summary":{"post":{"tags":["Reports"],"summary":"Bulk get report summary per run","description":"Returns aggregated report status counts per run. Only the latest report per template is counted (re-triggers are deduplicated).","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}":{"get":{"tags":["Reports"],"summary":"Get report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"404":{"description":"Report not found"}}}},"/api/v1/reports/{id}/logs":{"get":{"tags":["Reports"],"summary":"Stream report logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Report not found"}}}},"/api/v1/reports/trigger":{"post":{"tags":["Reports"],"summary":"Trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"triggered":{"type":"number"},"reports":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}},"required":["triggered","reports"]}}}},"400":{"description":"Invalid input"},"404":{"description":"Run not found"}}}},"/api/v1/reports/bulk-trigger":{"post":{"tags":["Reports"],"summary":"Bulk trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkTriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":{}}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}/insights":{"get":{"tags":["Reports"],"summary":"Get insights for report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/InsightResponse"},{"type":"object","properties":{"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}}}]}}}}},"404":{"description":"Report not found"}}},"post":{"tags":["Reports"],"summary":"Link insight to report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"isNew":{"type":"boolean"}},"required":["insightId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]}}}},"404":{"description":"Report or insight not found"},"409":{"description":"Insight already referenced by this report"}}}},"/api/v1/profiles":{"post":{"tags":["Profiles"],"summary":"Create a new profile","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"get":{"tags":["Profiles"],"summary":"List profiles","parameters":[{"schema":{"type":"string"},"required":false,"name":"workerType","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}":{"get":{"tags":["Profiles"],"summary":"Get profile with latest version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"post":{"tags":["Profiles"],"summary":"Create new profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["workerType","model"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}},"put":{"tags":["Profiles"],"summary":"Update profile identity","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileIdentity"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"}}}}}},"delete":{"tags":["Profiles"],"summary":"Delete profile","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/profiles/{profileId}/versions":{"get":{"tags":["Profiles"],"summary":"List profile versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}/versions/{version}":{"get":{"tags":["Profiles"],"summary":"Get profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"},{"schema":{"type":["number","null"]},"required":false,"name":"version","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}},"/api/v1/report-templates/default-system-prompt":{"get":{"tags":["Report Templates"],"summary":"Get default system prompt","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string"}},"required":["content"]}}}}}}},"/api/v1/report-templates/available-models":{"get":{"tags":["Report Templates"],"summary":"List models available for report generation","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"]}}}}}}}},"/api/v1/report-templates":{"get":{"tags":["Report Templates"],"summary":"List report templates","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}}}}},"post":{"tags":["Report Templates"],"summary":"Create report template","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportTemplateInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"409":{"description":"Report template already exists"}}}},"/api/v1/report-templates/{id}":{"get":{"tags":["Report Templates"],"summary":"Get report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"put":{"tags":["Report Templates"],"summary":"Update report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateReportTemplateInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"delete":{"tags":["Report Templates"],"summary":"Delete report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Report template not found"}}}},"/api/v1/agents":{"get":{"tags":["Agents"],"summary":"List agents","parameters":[{"schema":{"type":"string"},"required":false,"name":"modelProvider","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentResponse"}}}}}}},"post":{"tags":["Agents"],"summary":"Create or update agent (upsert)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"409":{"description":"Restored agent versions conflict with an active queue owner"}}}},"/api/v1/agents/{id}":{"get":{"tags":["Agents"],"summary":"Get agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"404":{"description":"Agent not found"}}},"put":{"tags":["Agents"],"summary":"Update agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAgentInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"}}},"delete":{"tags":["Agents"],"summary":"Delete agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Agent not found"}}}},"/api/v1/agents/{id}/versions":{"get":{"tags":["Agents"],"summary":"List agent versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}}}}},"404":{"description":"Agent not found"}}},"post":{"tags":["Agents"],"summary":"Register agent version (upsert)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterAgentVersionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent registration did not stabilize"}}}},"/api/v1/agents/{id}/versions/{agentVersion}":{"patch":{"tags":["Agents"],"summary":"Patch agent version","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"agentVersion","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchAgentVersionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Invalid status value"},"404":{"description":"Agent or version not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent activation did not stabilize"}}}},"/api/v1/models":{"get":{"tags":["Models"],"summary":"List models","parameters":[{"schema":{"type":"string"},"required":false,"name":"agentId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"provider","in":"query"},{"schema":{"type":"string","enum":["active","disappeared"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ModelResponse"}}}}}}}},"/api/v1/models/{id}":{"get":{"tags":["Models"],"summary":"Get model","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelResponse"}}}}}}},"/api/v1/models/sync":{"post":{"tags":["Models"],"summary":"Sync models from provider","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"agentId":{"type":"string"},"provider":{"type":"string"},"models":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"providerAvailableFrom":{"type":"string"},"providerEndOfLife":{"type":"string"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}}},"required":["id"]}},"scannedAt":{"type":"string"}},"required":["agentId","provider","models","scannedAt"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"added":{"type":"array","items":{"type":"string"}},"removed":{"type":"array","items":{"type":"string"}},"unchanged":{"type":"array","items":{"type":"string"}}},"required":["added","removed","unchanged"]}}}}}}},"/api/v1/mcp/servers":{"get":{"tags":["MCP Servers"],"summary":"List MCP servers","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"post":{"tags":["MCP Servers"],"summary":"Create MCP server","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}},"required":["_id","name","type"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"/api/v1/mcp/servers/{id}":{"get":{"tags":["MCP Servers"],"summary":"Get MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"put":{"tags":["MCP Servers"],"summary":"Update MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMcpServerInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"delete":{"tags":["MCP Servers"],"summary":"Delete MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/skills":{"get":{"tags":["Skills"],"summary":"List all skills","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"post":{"tags":["Skills"],"summary":"Create or import a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSkillInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"/api/v1/skills/search":{"get":{"tags":["Skills"],"summary":"Search skills (internal + external)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/search/external":{"get":{"tags":["Skills"],"summary":"Search external skills registry","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project."},"required":false,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/discover":{"get":{"tags":["Skills"],"summary":"Discover skills in a GitHub repository","parameters":[{"schema":{"type":"string"},"required":true,"name":"source","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillDiscoveryResult"}}}}},"400":{"description":"Missing or malformed source parameter"},"404":{"description":"Repository not found"},"502":{"description":"GitHub API error"}}}},"/api/v1/skills/{id}(*)/revisions":{"get":{"tags":["Skills"],"summary":"List skill revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}}},"404":{"description":"Skill not found"}}}},"/api/v1/skills/{id}(*)":{"get":{"tags":["Skills"],"summary":"Get skill by slug","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}},"404":{"description":"Skill not found"}}},"delete":{"tags":["Skills"],"summary":"Soft-delete a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Skill not found"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)/archive":{"get":{"tags":["Skill Revisions"],"summary":"Download skill revision archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Skill revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ref","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skill-revisions/{id}":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ID","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skills/{id}(*)/resolve":{"post":{"tags":["Skills"],"summary":"Trigger skill resolution","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebases":{"get":{"tags":["Codebases"],"summary":"List all codebases","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseResponse"}}}}}}},"post":{"tags":["Codebases"],"summary":"Create a codebase (archive codebases require the archive file)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input or missing archive"}}}},"/api/v1/codebases/{id}":{"get":{"tags":["Codebases"],"summary":"Get a codebase","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"patch":{"tags":["Codebases"],"summary":"Update a codebase","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCodebaseInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"delete":{"tags":["Codebases"],"summary":"Delete a codebase","responses":{"204":{"description":"Success"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebases/{id}/revisions":{"get":{"tags":["Codebases"],"summary":"List codebase revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}}},"404":{"description":"Codebase not found"}}},"post":{"tags":["Codebases"],"summary":"Resolve a new git codebase revision","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveCodebaseRevisionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"400":{"description":"Not a git codebase"},"404":{"description":"Codebase not found"},"502":{"description":"GitHub resolution failed"}}}},"/api/v1/codebases/{id}/upload":{"post":{"tags":["Codebases"],"summary":"Upload a codebase archive as a new revision","responses":{"201":{"description":"Success"},"400":{"description":"Missing archive or not an archive codebase"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebase-revisions/{id}/archive":{"get":{"tags":["Codebase Revisions"],"summary":"Download codebase revision archive","responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebase-revisions/{id}":{"get":{"tags":["Codebase Revisions"],"summary":"Get codebase revision by id","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources":{"get":{"tags":["Resources"],"summary":"List all resources","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceResponse"}}}}}}},"post":{"tags":["Resources"],"summary":"Create a resource and its first revision","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceInput"}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input"},"409":{"description":"Resource slug or revision ref already exists in this project"}}}},"/api/v1/resources/{id}":{"get":{"tags":["Resources"],"summary":"Get a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"patch":{"tags":["Resources"],"summary":"Update a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateResourceInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"delete":{"tags":["Resources"],"summary":"Delete a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Resource not found"}}}},"/api/v1/resources/{id}/revisions":{"get":{"tags":["Resource Revisions"],"summary":"List resource revisions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}}},"404":{"description":"Resource not found"}}},"post":{"tags":["Resource Revisions"],"summary":"Create a resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceRevisionInput"}}}},"responses":{"201":{"description":"Success"},"404":{"description":"Resource not found"},"409":{"description":"Resource revision ref already exists in this project"}}}},"/api/v1/resources/{id}/revisions/latest":{"get":{"tags":["Resource Revisions"],"summary":"Get the latest resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/{id}/revisions/{revisionNumber}":{"get":{"tags":["Resource Revisions"],"summary":"Get a resource revision by resource and revision number","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"revisionNumber","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/revisions/{id}":{"get":{"tags":["Resource Revisions"],"summary":"Get resource revision by id","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/extensions":{"get":{"tags":["Extensions"],"summary":"List all extensions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"post":{"tags":["Extensions"],"summary":"Create or import an extension","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExtensionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"/api/v1/extensions/search":{"get":{"tags":["Extensions"],"summary":"Search extensions (internal + marketplace)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/extensions/{id}":{"get":{"tags":["Extensions"],"summary":"Get extension by ID","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"put":{"tags":["Extensions"],"summary":"Update extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateExtensionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"delete":{"tags":["Extensions"],"summary":"Delete extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/extensions/{id}/versions":{"get":{"tags":["Extensions"],"summary":"List available versions for an extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"preRelease","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionVersionInfo"}}}}}}}},"/api/v1/insights":{"get":{"tags":["Insights"],"summary":"List insights","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"post":{"tags":["Insights"],"summary":"Create insight","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInsightInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"/api/v1/insights/search":{"get":{"tags":["Insights"],"summary":"Search insights","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}}},"/api/v1/insights/{id}":{"get":{"tags":["Insights"],"summary":"Get insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"put":{"tags":["Insights"],"summary":"Update insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateInsightInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"delete":{"tags":["Insights"],"summary":"Delete insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/upvote":{"post":{"tags":["Insights"],"summary":"Upvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/downvote":{"post":{"tags":["Insights"],"summary":"Downvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/block":{"post":{"tags":["Insights"],"summary":"Block insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/unblock":{"post":{"tags":["Insights"],"summary":"Unblock insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/reports":{"get":{"tags":["Insights"],"summary":"Get reports referencing insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Insight not found"}}}},"/api/v1/feature-flags":{"get":{"tags":["Feature Flags"],"summary":"List feature flags","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"/api/v1/feature-flags/{key}":{"put":{"tags":["Feature Flags"],"summary":"Update feature flag","parameters":[{"schema":{"type":"string"},"required":true,"name":"key","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeatureFlagInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"webhooks":{}} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"Scope API","version":"1.0.0","description":"REST API for the Scope platform — benchmarking AI coding agents"},"servers":[{"url":"/","description":"Current server"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Use the unchanged IdP access token, without the Bearer prefix."}},"schemas":{"KeyResponse":{"type":"object","properties":{},"additionalProperties":{}},"KeyInput":{"type":"object","properties":{},"additionalProperties":{}},"ValidateKeyInput":{"type":"object","properties":{"token":{"type":"string"}},"required":["token"]},"AccountResponse":{"type":"object","properties":{},"additionalProperties":{}},"AccountInput":{"type":"object","properties":{},"additionalProperties":{}},"UserMeResponse":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string"},"email":{"type":"string"},"displayName":{"type":"string"},"idp":{"type":"string"},"idpTenant":{"type":"string"}},"required":["id"]},"ProjectResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","createdAt"]},"CreateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"creator":{"type":"string"}},"required":["name"]},"UpdateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"SubmissionListResponse":{"type":"object","properties":{"submissions":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionResponse"}}},"required":["submissions"]},"SubmissionResponse":{"type":"object","properties":{"submissionId":{"type":"string"},"projectId":{"type":"string"},"requestIds":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"submissionRunId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["waiting","ready","blocked","processing","done","failed"]},"requests":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRequestSummary"}},"dependencyProjection":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionDependencyProjection"}},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"}},"handlerProgress":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/SubmissionHandlerStatus"}},"report":{"$ref":"#/components/schemas/SubmissionReportPointer"},"supersededAt":{"type":["string","null"],"format":"date-time"},"supersededBySubmissionRunId":{"type":"string"},"supersededReason":{"type":"string","enum":["request-vector-changed","manual-retry"]}},"required":["submissionId","projectId","requestIds","createdAt","submissionRunId","attemptNumber","readiness","requests","dependencyProjection","blocking"]},"SubmissionRequestSummary":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"task":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"},"workerType":{"type":"string"},"model":{"type":"string"},"profileId":{"type":"string"}},"required":["requestId","runId","task","status","workerType"]},"SubmissionDependencyProjection":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"state":{"type":"string","enum":["waiting","ready","failed","blocked"]},"version":{"type":"number"},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["requestId","runId","handlerId","state"]},"SubmissionBlockingDetail":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"reason":{"type":"string","enum":["request-missing","run-changed","dependency-not-ready","dependency-failed","dependency-blocked"]},"error":{"type":"string"}},"required":["requestId","runId","handlerId","reason"]},"SubmissionHandlerStatus":{"type":"object","properties":{"status":{"type":"string","enum":["queued","processing","done","failed"]},"version":{"type":"number"},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["status"]},"SubmissionReportPointer":{"type":"object","properties":{"reportId":{"type":"string"},"url":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"}}},"ReportResponse":{"type":"object","properties":{"_id":{"type":"string"},"requestId":{"type":"string"},"submissionId":{"type":"string"},"submissionRunId":{"type":"string"},"templateId":{"type":"string"},"reporter":{"$ref":"#/components/schemas/Reporter"},"content":{"type":"string"},"structuredArtifact":{"$ref":"#/components/schemas/StructuredReportArtifact"},"status":{"type":"string","enum":["pending","generating","completed","failed"]},"error":{"type":"string"},"insightReferences":{"type":"array","items":{"$ref":"#/components/schemas/InsightReference"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","status","createdAt","projectId"]},"Reporter":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"gitHash":{"type":"string"},"model":{"type":"string"},"agentId":{"type":"string"},"agentVersion":{"type":"string"}},"required":["id","name","gitHash","model","agentId","agentVersion"]},"StructuredReportArtifact":{"type":"object","properties":{"schemaId":{"type":"string"},"schemaVersion":{"type":"string"},"contentType":{"type":"string","enum":["application/json"]},"url":{"type":"string"}},"required":["schemaId","schemaVersion","contentType","url"]},"InsightReference":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]},"SubmissionRunResponse":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"vectorFingerprint":{"type":"string"},"requests":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRequestRef"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"dependencyProjection":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionDependencyProjection"},"default":[]},"handlerStatus":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/SubmissionHandlerStatus"}},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"},"default":[]},"report":{"$ref":"#/components/schemas/SubmissionReportPointer"},"reportsTriggeredAt":{"type":["string","null"],"format":"date-time"},"supersededAt":{"type":["string","null"],"format":"date-time"},"supersededBySubmissionRunId":{"type":"string"},"supersededReason":{"type":"string","enum":["request-vector-changed","manual-retry"]},"submissionId":{"type":"string"},"projectId":{"type":"string"}},"required":["_id","attemptNumber","vectorFingerprint","requests","createdAt"]},"SubmissionRequestRef":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"}},"required":["requestId","runId"]},"SubmissionRetryResponse":{"type":"object","properties":{"submissionId":{"type":"string"},"submissionRunId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["waiting","ready","blocked","processing","done","failed"]},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"}}},"required":["submissionId","submissionRunId","attemptNumber","readiness","blocking"]},"RequestResponse":{"type":"object","properties":{"_id":{"type":"string"},"scenario":{"$ref":"#/components/schemas/Scenario"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"maxIterations":{"type":"number"},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"deletedAt":{"type":["string","null"],"format":"date-time"},"taskPromptId":{"type":"string"},"agentsMdPromptId":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBinding"}},"extensions":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"profileId":{"type":"string"},"profileVersionId":{"type":"string"},"submissionId":{"type":"string"},"submissionExpectedRequestCount":{"type":"integer","minimum":1},"submissionRequestIndex":{"type":"integer","minimum":0},"submissionInitialRunId":{"type":"string"},"submissionFinalizedAt":{"type":["string","null"],"format":"date-time"},"priority":{"type":"integer","default":0},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"observations":{"type":"array","items":{"type":"string"}},"gateSummaries":{"type":"array","items":{"$ref":"#/components/schemas/GateRunSummary"}},"run":{"$ref":"#/components/schemas/RunState"},"projectId":{"type":"string"}},"required":["_id","scenario","workerType","createdAt","projectId"]},"Scenario":{"type":"object","properties":{"version":{"type":"string","enum":["v1","v2"]},"task":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}}},"required":["task","criteria"]},"Persona":{"type":"object","properties":{"personality":{"type":"string","enum":["demanding","friendly"]},"experience":{"type":"string","enum":["junior","senior"]},"verbosity":{"type":"string","enum":["brief","moderate"]},"type":{"type":"string","enum":["traditional","ai_assisted","vibe"]}},"required":["personality","experience","verbosity","type"]},"ResourceBinding":{"type":"object","properties":{"ref":{"type":"string"},"revisionId":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref","revisionId","params"]},"GateConfig":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"promptId":{"type":"string"},"promptText":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}},"maxIterations":{"type":"integer","minimum":1,"maximum":50}},"required":["gate","criteria"]},"GateRunSummary":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"status":{"type":"string","enum":["passed","failed","skipped"]},"iterations":{"type":"integer","minimum":0}},"required":["gate","status","iterations"]},"RunState":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"queuedQueueName":{"type":"string"},"outcome":{"type":"string","enum":["succeeded","failed","finished"]},"result":{"type":"string"},"error":{"type":"string"},"logsUrl":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"turns":{"type":"array","items":{"$ref":"#/components/schemas/ConversationTurn"}},"workerVersion":{"type":"string"},"os":{"type":"object","properties":{"platform":{"type":"string"},"release":{"type":"string"},"arch":{"type":"string"}},"required":["platform","release","arch"]},"lastHeartbeatAt":{"type":["string","null"],"format":"date-time"},"worker":{"type":"object","properties":{"instanceId":{"type":"string"},"podName":{"type":"string"}},"required":["instanceId"]},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"setupVideoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"aiCallCount":{"type":"number"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRunOutcome"}},"mcpRegistered":{"type":"boolean"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"pausedAt":{"type":["string","null"],"format":"date-time"},"resumedAt":{"type":["string","null"],"format":"date-time"},"observationResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}}},"required":["_id","attemptNumber","status"]},"ConversationTurn":{"type":"object","properties":{"iteration":{"type":"number"},"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"codingAgentResponse":{"type":"string"},"judgeFeedback":{"type":"string"},"snapshotUrl":{"type":"string"},"passed":{"type":"boolean"},"timestamp":{"type":["string","null"],"format":"date-time"},"criteriaResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"observationResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"startedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"toolCalls":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"arguments":{"type":"object","additionalProperties":{}},"response":{"type":"string"},"timestamp":{"type":"string"}},"required":["id","name","arguments"]}},"toolCallsUrl":{"type":"string"},"toolCallCount":{"type":"number"},"aiCallCount":{"type":"number"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"chatResultUrl":{"type":"string"},"chatResultFormat":{"type":"string"}},"required":["iteration","judgeFeedback","snapshotUrl","passed","timestamp"]},"CriterionResult":{"type":"object","properties":{"criterionId":{"type":"string"},"passed":{"type":"boolean"},"feedback":{"type":"string"},"evaluated":{"type":"boolean"}},"required":["criterionId","passed","feedback","evaluated"]},"TokenUsage":{"type":"object","properties":{"promptTokens":{"type":"number"},"completionTokens":{"type":"number"},"totalTokens":{"type":"number"}},"required":["promptTokens","completionTokens","totalTokens"]},"ResourceRunOutcome":{"type":"object","properties":{"ref":{"type":"string"},"slug":{"type":"string"},"revisionId":{"type":"string"},"setupSucceeded":{"type":"boolean"},"published":{"type":"array","items":{"type":"string"}},"params":{"type":"object","additionalProperties":{"type":"string"}},"setupDurationMs":{"type":"number"},"error":{"type":"string"},"teardownRan":{"type":"boolean"}},"required":["ref","slug","revisionId","setupSucceeded","published"]},"ResourceBindingSpec":{"anyOf":[{"type":"string"},{"type":"object","properties":{"ref":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref"]}]},"CreateRequestInput":{"type":"object","properties":{"scenario":{"$ref":"#/components/schemas/Scenario"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"maxIterations":{"type":"integer","minimum":1,"maximum":50},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"profileId":{"type":"string"},"profileVariations":{"type":"array","items":{"type":"string"}},"priority":{"type":"integer"},"agentsMd":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"observations":{"type":"array","items":{"type":"string"}}},"required":["scenario"]},"RunFacetsResponse":{"type":"object","properties":{"total":{"type":"number"},"facets":{"type":"object","properties":{"workerType":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"status":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"outcome":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"model":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"os":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"priority":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"agentVersion":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"profileId":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}}},"required":["workerType","status","outcome","model","os","priority","agentVersion","profileId"]}},"required":["total","facets"]},"RunFacetBucket":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"number"}},"required":["value","count"]},"PaginatedRunsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"Cursors":{"type":"object","properties":{"next":{"type":["string","null"]},"prev":{"type":["string","null"]}},"required":["next","prev"]},"PaginatedRunGroupsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RunGroup"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"RunGroup":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"runIds":{"type":"array","items":{"type":"string"}},"aggregates":{"$ref":"#/components/schemas/GroupAggregates"},"uniform":{"$ref":"#/components/schemas/GroupUniformValues"}},"required":["key","label","runIds","aggregates","uniform"]},"GroupAggregates":{"type":"object","properties":{"count":{"type":"number"},"turns":{"$ref":"#/components/schemas/AggregateStats"},"duration":{"$ref":"#/components/schemas/AggregateStats"},"promptTokens":{"$ref":"#/components/schemas/AggregateStats"},"completionTokens":{"$ref":"#/components/schemas/AggregateStats"},"statusCounts":{"type":"object","additionalProperties":{"type":"number"}},"outcomeCounts":{"type":"object","additionalProperties":{"type":"number"}}},"required":["count","turns","duration","promptTokens","completionTokens","statusCounts","outcomeCounts"]},"AggregateStats":{"type":["object","null"],"properties":{"min":{"type":"number"},"max":{"type":"number"},"mean":{"type":"number"},"stdDev":{"type":"number"}},"required":["min","max","mean","stdDev"]},"GroupUniformValues":{"type":"object","properties":{"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":"string"},"platform":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"extensions":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"submissionId":{"type":"string"},"task":{"type":"string"}}},"BulkResubmitInput":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"count":{"type":"integer","minimum":1,"maximum":10,"default":1},"overrides":{"type":"object","properties":{"profileId":{"type":["string","null"]},"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":["string","null"]},"reasoningEffort":{"type":["string","null"]},"maxIterations":{"type":["number","null"]},"mcpServers":{"type":["array","null"],"items":{"type":"string"}},"skillRevisions":{"type":["array","null"],"items":{"type":"string"}},"extensions":{"type":["array","null"],"items":{"type":"string"}}}}},"required":["ids"]},"CreateCriteriaInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"],"default":"gate"},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}},"required":["id","prompt"]},"CriteriaResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"CriteriaGraph":{"type":"object","properties":{"nodes":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphNode"}},"edges":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphEdge"}}},"required":["nodes","edges"]},"CriteriaGraphNode":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}},"required":["id","prompt"]},"CriteriaGraphEdge":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"]},"UpdateCriteriaInput":{"type":"object","properties":{"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}}},"CreatePromptFeatureInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["id","prompt"]},"PromptType":{"type":"string","enum":["select","build","test","run","deploy","agents.md"]},"PromptFeatureResult":{"type":"object","properties":{"featureId":{"type":"string"},"detected":{"type":"boolean"},"evaluated":{"type":"boolean"}},"required":["featureId","detected","evaluated"]},"SuggestedPromptFeature":{"type":"object","properties":{"suggestedId":{"type":"string"},"behavior":{"type":"string"},"prompt":{"type":"string"}},"required":["suggestedId","behavior","prompt"]},"PromptFeatureResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"UpdatePromptFeatureInput":{"type":"object","properties":{"prompt":{"type":"string"}}},"TaskPromptResponse":{"type":"object","properties":{"_id":{"type":"string"},"keyId":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"text":{"type":"string"},"contentBlobUrl":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","keyId","createdAt","projectId"]},"CreateTaskPromptInput":{"type":"object","properties":{"text":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["text"]},"PatchTaskPromptFeatureInput":{"type":"object","properties":{"detected":{"type":"boolean"}},"required":["detected"]},"CreateReportInput":{"type":"object","properties":{"requestId":{"type":"string"},"templateId":{"type":"string"}},"required":["requestId"]},"BulkCreateReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}},"templateId":{"type":"string"}},"required":["requestIds"]},"BulkReportStatusInput":{"type":"object","properties":{"reportIds":{"type":"array","items":{"type":"string"}}},"required":["reportIds"]},"BulkReportSummaryResponse":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/ReportSummary"}},"ReportSummary":{"type":"object","properties":{"total":{"type":"number"},"pending":{"type":"number"},"generating":{"type":"number"},"completed":{"type":"number"},"failed":{"type":"number"}},"required":["total","pending","generating","completed","failed"]},"BulkReportSummaryInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"TriggerReportsInput":{"type":"object","properties":{"requestId":{"type":"string"}},"required":["requestId"]},"TriggerSubmissionReportsInput":{"type":"object","properties":{"submissionId":{"type":"string"},"submissionRunId":{"type":"string"}},"required":["submissionId","submissionRunId"]},"BulkTriggerReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"InsightResponse":{"type":"object","properties":{"_id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"upvotes":{"type":"number"},"downvotes":{"type":"number"},"blocked":{"type":"boolean"},"referenceCount":{"type":"number"},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","title","description","upvotes","downvotes","blocked","referenceCount","createdBy","createdAt","projectId"]},"ProfileWithVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"version":{"$ref":"#/components/schemas/ProfileVersionResponse"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","version","projectId"]},"ProfileVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"profileId":{"type":"string"},"version":{"type":"number"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","profileId","version","workerType","model","createdAt","projectId"]},"CreateProfileInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["name","workerType","model"]},"ProfileResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","projectId"]},"UpdateProfileIdentity":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512}}},"ReportTemplateResponse":{"type":"object","properties":{"_id":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"subject":{"type":"string","enum":["run","submission"],"default":"run"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","id","name","userPrompt","createdAt","projectId"]},"ReportTemplateSystemPrompt":{"type":"object","properties":{"mode":{"type":"string","enum":["append","override"]},"content":{"type":"string"}},"required":["mode","content"]},"ReportTrigger":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["always"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["criteria"]},"criteriaIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","criteriaIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["taskPrompt"]},"taskPromptIds":{"type":"array","items":{"type":"string"}}},"required":["type","taskPromptIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["promptFeature"]},"featureIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","featureIds"]}]},"CreateReportTemplateInput":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"subject":{"type":"string","enum":["run","submission"],"default":"run"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0}},"required":["id","name","userPrompt"]},"UpdateReportTemplateInput":{"type":"object","properties":{"name":{"type":"string"},"subject":{"type":"string","enum":["run","submission"]},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"allOf":[{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},{"type":["object","null"]}]},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":["string","null"]},"timeoutMs":{"type":["integer","null"],"exclusiveMinimum":0}}},"AgentResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"},"versions":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","supportedModels","createdAt"]},"AgentCapabilities":{"type":"object","properties":{"supportsReasoningEffort":{"type":"boolean"},"supportsMcpServers":{"type":"boolean"},"supportsSkills":{"type":"boolean"},"supportsExtensions":{"type":"boolean"},"supportsResources":{"type":"boolean"}}},"AgentVersion":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string"},"status":{"type":"string","enum":["active","retired"]},"createdAt":{"type":["string","null"],"format":"date-time"}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","status","createdAt"]},"CreateAgentInput":{"type":"object","properties":{"_id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}},"required":["_id","name"]},"UpdateAgentInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}}},"RegisterAgentVersionInput":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string","minLength":1}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","queueName"]},"PatchAgentVersionInput":{"type":"object","properties":{"status":{"type":"string","enum":["active","retired"]}},"required":["status"]},"ModelResponse":{"type":"object","properties":{"_id":{"type":"string"},"modelId":{"type":"string"},"provider":{"type":"string"},"agentId":{"type":"string"},"firstSeenAt":{"type":["string","null"],"format":"date-time"},"lastSeenAt":{"type":["string","null"],"format":"date-time"},"disappearedAt":{"type":["string","null"],"format":"date-time"},"providerAvailableFrom":{"type":["string","null"],"format":"date-time"},"providerEndOfLife":{"type":["string","null"],"format":"date-time"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"$ref":"#/components/schemas/ModelCapabilities"}},"required":["_id","modelId","provider","agentId","firstSeenAt","lastSeenAt"]},"ModelCapabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}},"McpServerResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","type","createdAt","projectId"]},"McpServerHeader":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}},"required":["name","value"]},"UpdateMcpServerInput":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}}},"SkillResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","source","skillName","name","origin","createdAt","projectId"]},"SkillSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"source":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"installs":{"type":"number"}},"required":["id","name","source","internal"]},"SkillDiscoveryResult":{"type":"object","properties":{"skillName":{"type":"string"},"skillPath":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"existsInLibrary":{"type":"boolean"},"currentRevisionCommitSha":{"type":"string"},"latestUpstreamCommitSha":{"type":"string"},"updateAvailable":{"type":"boolean"},"lastImportedAt":{"type":"string"}},"required":["skillName","skillPath"]},"SkillRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"ref":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"skillPath":{"type":"string"},"commitHash":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"name":{"type":"string"},"description":{"type":"string"},"license":{"type":"string"},"compatibility":{"type":"string"},"allowedTools":{"type":"string"},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"content":{"type":"string"},"archiveUrl":{"type":"string"},"validationWarnings":{"type":"array","items":{"type":"string"}},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","ref","source","skillName","skillPath","commitHash","commitTimestamp","name","description","content","archiveUrl","resolvedAt","createdAt","projectId"]},"CreateSkillInput":{"type":"object","properties":{"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]}},"required":["source","skillName","name","origin"]},"CodebaseResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"defaultBranch":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","sourceType","revisionCounter","createdAt","projectId"]},"UpdateCodebaseInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"defaultBranch":{"type":"string"}}},"CodebaseRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"codebaseId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"requestedRef":{"type":"string"},"resolvedCommitSha":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"originalFilename":{"type":"string"},"contentSha256":{"type":"string"},"archiveUrl":{"type":"string"},"sizeBytes":{"type":"number"},"fileCount":{"type":"number"},"creator":{"type":"string"},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","codebaseId","slug","revisionNumber","ref","sourceType","archiveUrl","resolvedAt","createdAt","projectId"]},"ResolveCodebaseRevisionInput":{"type":"object","properties":{"requestedRef":{"type":"string"},"creator":{"type":"string"}}},"ResourceResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"latestRevisionNumber":{"type":"number"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","revisionCounter","createdAt","projectId"]},"CreateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1},"description":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["name","setup"]},"ResourceScript":{"type":"object","properties":{"sh":{"type":"string","minLength":1}}},"ResourceParameter":{"type":"object","properties":{"name":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"description":{"type":"string"},"required":{"type":"boolean","default":false},"default":{"type":"string"},"example":{"type":"string"}},"required":["name"]},"UpdateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"ResourceRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"resourceId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"contentSha256":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","resourceId","slug","revisionNumber","ref","setup","exports","contentSha256","createdAt","projectId"]},"CreateResourceRevisionInput":{"type":"object","properties":{"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["setup"]},"ExtensionResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","publisher","name","origin","createdAt","projectId"]},"ExtensionSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"publisher":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"version":{"type":"string"}},"required":["id","name","publisher","internal"]},"CreateExtensionInput":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+$"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]}},"required":["_id","publisher","name","origin"]},"UpdateExtensionInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ExtensionVersionInfo":{"type":"object","properties":{"version":{"type":"string"},"preRelease":{"type":"boolean"},"lastUpdated":{"type":"string"}},"required":["version","preRelease","lastUpdated"]},"CreateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"}},"required":["title","description"]},"UpdateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}}}},"FeatureFlagResponse":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"enabled":{"type":"boolean"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["key","label","enabled","updatedAt"]},"UpdateFeatureFlagInput":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]}},"parameters":{}},"paths":{"/api/v1/keys/preview":{"post":{"tags":["Keys"],"summary":"Preview key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}}},"/api/v1/keys":{"post":{"tags":["Keys"],"summary":"Create key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"201":{"description":"Key created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"get":{"tags":["Keys"],"summary":"List keys","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KeyResponse"}}}}}}}},"/api/v1/keys/{id}":{"get":{"tags":["Keys"],"summary":"Get key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"put":{"tags":["Keys"],"summary":"Update key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"delete":{"tags":["Keys"],"summary":"Delete key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Key deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/keys/{id}/validate":{"post":{"tags":["Keys"],"summary":"Validate key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateKeyInput"}}}},"responses":{"200":{"description":"Validation result","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Key validation result"}}}}}}},"/api/v1/accounts":{"post":{"tags":["Accounts"],"summary":"Create account","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"201":{"description":"Account created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"get":{"tags":["Accounts"],"summary":"List accounts","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccountResponse"}}}}}}}},"/api/v1/accounts/{id}":{"get":{"tags":["Accounts"],"summary":"Get account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"put":{"tags":["Accounts"],"summary":"Update account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"200":{"description":"Account updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"delete":{"tags":["Accounts"],"summary":"Delete account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Account deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/users/me":{"get":{"tags":["Users"],"summary":"Get the authenticated user's identity","description":"Read the existing authenticated Scope identity without enrollment or profile writes. Responses must not be HTTP-cached.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is not enrolled or is disabled"},"503":{"description":"Authentication service unavailable"}}},"post":{"tags":["Users"],"summary":"Enroll the authenticated user","description":"JIT-enroll the authenticated user, refresh their profile and lastLoginAt, apply bootstrap-admin rules, and warm the access cache. Use this only after an explicit IdP login; do not prefetch, poll, or automatically retry transient failures.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is disabled"},"503":{"description":"Authentication service unavailable"}}}},"/health":{"get":{"tags":["Health"],"summary":"Liveness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"version":{"type":"string"}},"required":["status","version"]}}}}}}},"/ready":{"get":{"tags":["Health"],"summary":"Readiness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"migrations":{}},"required":["status"]}}}},"503":{"description":"Service is not ready"}}}},"/about":{"get":{"tags":["System"],"summary":"API metadata","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"version":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"description":{"type":"string"},"workers":{"type":"array","items":{"type":"string"}}},"required":["name","version","buildTime","environment","description","workers"]}}}}}}},"/api/v1/version":{"get":{"tags":["System"],"summary":"Version info","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"commit":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"strictAgentCapabilities":{"type":"boolean"}},"required":["commit","buildTime","environment","strictAgentCapabilities"]}}}}}}},"/api/v1/projects":{"get":{"tags":["Projects"],"summary":"List all projects","parameters":[{"schema":{"type":"string","enum":["true","false"],"description":"When true, include soft-deleted projects in the result"},"required":false,"description":"When true, include soft-deleted projects in the result","name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProjectResponse"}}}}}}},"post":{"tags":["Projects"],"summary":"Create a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/projects/{id}":{"get":{"tags":["Projects"],"summary":"Get a project","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"patch":{"tags":["Projects"],"summary":"Update a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProjectInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"delete":{"tags":["Projects"],"summary":"Soft-delete a project","responses":{"204":{"description":"Success"},"404":{"description":"Project not found"}}}},"/api/v1/projects/{id}/restore":{"post":{"tags":["Projects"],"summary":"Restore a soft-deleted project","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}}},"/api/v1/submissions":{"get":{"tags":["Submissions"],"summary":"List submissions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionListResponse"}}}}}}},"/api/v1/submissions/{id}/runs/{submissionRunId}/logs":{"get":{"tags":["Submissions"],"summary":"Stream logs for a submission attempt (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"submissionRunId","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Submission or submission attempt not found"}}}},"/api/v1/submissions/{id}/reports":{"get":{"tags":["Reports"],"summary":"Get reports for submission","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}":{"get":{"tags":["Submissions"],"summary":"Get a submission","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionResponse"}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}/runs":{"get":{"tags":["Submissions"],"summary":"List current and historical submission attempts","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"runs":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRunResponse"}}},"required":["runs"]}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}/runs/{submissionRunId}":{"get":{"tags":["Submissions"],"summary":"Get one submission attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"submissionRunId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionRunResponse"}}}},"404":{"description":"Submission attempt not found"}}}},"/api/v1/submissions/{id}/retry":{"post":{"tags":["Submissions"],"summary":"Retry current submission processing","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"submissionRunId":{"type":"string"}},"required":["submissionRunId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionRetryResponse"}}}},"404":{"description":"Submission not found"},"409":{"description":"Submission attempt is stale or dependencies are blocked"}}}},"/api/v1/requests":{"post":{"tags":["Requests"],"summary":"Submit request(s)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/CreateRequestInput"},{"type":"object","properties":{"count":{"type":"number","minimum":1,"maximum":10,"default":1},"skills":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"codebase":{"type":"string"}}}]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RequestResponse"},{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}]}}}}}},"get":{"tags":["Requests"],"summary":"List requests","parameters":[{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"profileId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"status","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"outcome","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"model","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"os","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"priority","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"agentVersion","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"type":"string","enum":["task","submissionId","profile"]},"required":false,"name":"groupBy","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"after","in":"query"},{"schema":{"type":"string"},"required":false,"name":"before","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"last","in":"query"},{"schema":{"type":"string","enum":["created","updated","priority","worker","status","id","duration","createdAt"]},"required":false,"name":"sortBy","in":"query"},{"schema":{"type":"string","enum":["asc","desc"]},"required":false,"name":"sortDir","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdAfter","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdBefore","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"turns","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"turnsOp","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"maxIterations","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"maxIterationsOp","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PaginatedRunsResponse"},{"$ref":"#/components/schemas/PaginatedRunGroupsResponse"}]}}}}}}},"/api/v1/requests/facets":{"get":{"tags":["Requests"],"summary":"List run filter facets","description":"Returns every distinct value and its full-dataset count per categorical filter dimension for the Runs list rail. Counts are absolute over all non-deleted runs **in the given project**: they intentionally ignore the active search, date range, iteration, and categorical selections so every selectable value stays visible with a stable count. Requires ?projectId=. Computed with parallel $group aggregations (Cosmos has no $facet) and cached per-project for a short TTL.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunFacetsResponse"}}}}}}},"/api/v1/requests/{id}":{"get":{"tags":["Requests"],"summary":"Get request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestResponse"}}}},"404":{"description":"Not found"}}},"delete":{"tags":["Requests"],"summary":"Soft-delete request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Not found"}}}},"/api/v1/analysis":{"get":{"tags":["Requests"],"summary":"Compute pass@k / success@T metrics","description":"Aggregates pass@k / success@T metrics over a single project's done runs. Requires ?projectId=.","parameters":[{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"k","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Analysis metrics"}}}}}}},"/api/v1/requests/bulk-resubmit":{"post":{"tags":["Requests"],"summary":"Bulk resubmit requests","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkResubmitInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}}}}}}},"/api/v1/requests/bulk":{"delete":{"tags":["Requests"],"summary":"Bulk soft-delete requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}},"required":["ids"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"number"}},"required":["deleted"]}}}}}}},"/api/v1/requests/archive":{"post":{"tags":["Requests"],"summary":"Download batch archive of multiple runs","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Gzipped batch archive containing individual run archives"},"400":{"description":"Invalid input"},"404":{"description":"One or more runs not found"}}}},"/api/v1/runs/upload":{"post":{"tags":["Requests"],"summary":"Import run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"}}}},"/api/v1/runs/upload-batch":{"post":{"tags":["Requests"],"summary":"Import batch run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"207":{"description":"Multi-status: 201 if all runs imported, 400 if none imported, 207 if partial"},"400":{"description":"No archive uploaded, empty archive, or all runs failed"}}}},"/api/v1/requests/{id}/reports":{"get":{"tags":["Reports"],"summary":"Get reports for request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Run not found"}}}},"/api/v1/requests/{id}/runs":{"get":{"tags":["Requests"],"summary":"List attempts for a request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RunState"}}}}},"404":{"description":"Request not found"}}}},"/api/v1/requests/{id}/runs/{runId}":{"get":{"tags":["Requests"],"summary":"Get a single attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunState"}}}},"404":{"description":"Request or run not found"}}}},"/api/v1/requests/bulk-retry":{"post":{"tags":["Requests"],"summary":"Bulk retry requests (start new attempts)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"force":{"type":"boolean"}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"retried":{"type":"integer"},"skipped":{"type":"integer"},"results":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"},"error":{"type":"string"}},"required":["requestId"]}}},"required":["retried","skipped","results"]}}}}}}},"/api/v1/requests/{id}/retry":{"post":{"tags":["Requests"],"summary":"Retry a request (start a new attempt)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"force":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"}},"required":["requestId","runId","attemptNumber"]}}}},"404":{"description":"Request not found"},"409":{"description":"Conflict — request is not in a retryable state, or a concurrent retry won the race"},"422":{"description":"Cannot retry — current run not yet terminal"}}}},"/api/v1/requests/{id}/priority":{"post":{"tags":["Requests"],"summary":"Set priority on a single request","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"priority":{"type":"integer"}},"required":["priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"priority":{"type":"number"}},"required":["id","priority"]}}}}}}},"/api/v1/requests/bulk-priority":{"post":{"tags":["Requests"],"summary":"Set priority on multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100},"priority":{"type":"integer"}},"required":["ids","priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/pause":{"post":{"tags":["Requests"],"summary":"Pause a single request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/{id}/resume":{"post":{"tags":["Requests"],"summary":"Resume a paused request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/bulk-pause":{"post":{"tags":["Requests"],"summary":"Pause multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/bulk-resume":{"post":{"tags":["Requests"],"summary":"Resume multiple paused requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/cancel":{"post":{"tags":["Requests"],"summary":"Cancel a request (marks as done/failed and signals worker to exit)","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"previousStatus":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"}},"required":["id","previousStatus","status","outcome"]}}}}}}},"/api/v1/requests/bulk-cancel":{"post":{"tags":["Requests"],"summary":"Cancel multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"cancelled":{"type":"number"},"skipped":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"cancelled":{"type":"boolean"},"previousStatus":{"type":"string"},"error":{"type":"string"}},"required":["id","cancelled"]}}},"required":["cancelled","skipped","results"]}}}}}}},"/api/v1/requests/{id}/logs":{"get":{"tags":["Requests"],"summary":"Stream request logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/logs":{"get":{"tags":["Requests"],"summary":"Stream logs for a specific attempt (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific attempt and iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/video":{"get":{"tags":["Requests"],"summary":"Download session recording","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/video":{"get":{"tags":["Requests"],"summary":"Download session recording for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/criteria/generate-prompt":{"post":{"tags":["Criteria"],"summary":"Generate criterion prompt from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"},"gates":{"type":"array","items":{"type":"string"}}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"},"suggestedId":{"type":"string"},"suggestedParents":{"type":"array","items":{"type":"string"}},"suggestedChildren":{"type":"array","items":{"type":"string"}}},"required":["prompt","suggestedId","suggestedParents","suggestedChildren"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/criteria/seed":{"post":{"tags":["Criteria"],"summary":"Seed criteria in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"criteria":{"type":"array","items":{"$ref":"#/components/schemas/CreateCriteriaInput"}}},"required":["criteria"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}},"400":{"description":"Seed batch would introduce a dependency cycle"}}}},"/api/v1/criteria":{"get":{"tags":["Criteria"],"summary":"List criteria","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","description":"Comma-separated criterion IDs to include"},"required":false,"description":"Comma-separated criterion IDs to include","name":"ids","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"When true and ids is set, also include dependency ancestors"},"required":false,"description":"When true and ids is set, also include dependency ancestors","name":"ancestors","in":"query"},{"schema":{"type":"string","enum":["gate","observation"],"description":"Filter by criterion kind"},"required":false,"description":"Filter by criterion kind","name":"kind","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaResponse"}}}}}}},"post":{"tags":["Criteria"],"summary":"Create criterion","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCriteriaInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"409":{"description":"Criterion already exists"}}}},"/api/v1/criteria/mdp":{"get":{"tags":["Criteria"],"summary":"Compute MDP transitions","parameters":[{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"since","in":"query"},{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{}}}}}}}},"/api/v1/criteria/graph":{"get":{"tags":["Criteria"],"summary":"Get criteria DAG","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaGraph"}}}}}}},"/api/v1/criteria/{id}":{"get":{"tags":["Criteria"],"summary":"Get criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"404":{"description":"Criterion not found"}}},"put":{"tags":["Criteria"],"summary":"Update criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCriteriaInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"400":{"description":"Invalid dependency reference or self-reference"},"404":{"description":"Criterion not found"}}},"delete":{"tags":["Criteria"],"summary":"Soft-delete criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Criterion not found"},"409":{"description":"Criterion has dependents"}}}},"/api/v1/prompt-features/generate-prompt":{"post":{"tags":["Prompt Features"],"summary":"Generate prompt feature from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"}},"required":["prompt"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features/seed":{"post":{"tags":["Prompt Features"],"summary":"Seed prompt features in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}},"required":["features"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}}}}},"/api/v1/prompt-features/extract-from-text":{"post":{"tags":["Prompt Features"],"summary":"Extract features from text","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string"},"model":{"type":"string"},"type":{"type":"string","enum":["select","agents.md"]}},"required":["text"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["features","cached"]}}}},"400":{"description":"Empty text string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features":{"get":{"tags":["Prompt Features"],"summary":"List features","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","enum":["select","agents.md"]},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}}}}},"post":{"tags":["Prompt Features"],"summary":"Create feature","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"409":{"description":"Feature already exists"}}}},"/api/v1/prompt-features/{id}":{"get":{"tags":["Prompt Features"],"summary":"Get feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"404":{"description":"Feature not found"}}},"put":{"tags":["Prompt Features"],"summary":"Update feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Feature not found"}}},"delete":{"tags":["Prompt Features"],"summary":"Soft-delete feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Feature not found"}}}},"/api/v1/task-prompts/generate":{"post":{"tags":["Task Prompts"],"summary":"Generate task prompts","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string"},"existingPrompt":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"tasks":{"type":"array","items":{"type":"string"}}},"required":["tasks"]}}}},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts":{"get":{"tags":["Task Prompts"],"summary":"List task prompts","parameters":[{"schema":{"type":["number","null"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":["number","null"]},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"$ref":"#/components/schemas/PromptType"},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/TaskPromptResponse"}},"total":{"type":"number"},"limit":{"type":"number"},"offset":{"type":"number"}},"required":["items","total","limit","offset"]}}}}}},"post":{"tags":["Task Prompts"],"summary":"Create or find task prompt","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTaskPromptInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Empty text string"}}}},"/api/v1/task-prompts/{id}":{"get":{"tags":["Task Prompts"],"summary":"Get task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"404":{"description":"Task prompt not found"}}},"delete":{"tags":["Task Prompts"],"summary":"Soft-delete task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/content":{"get":{"tags":["Task Prompts"],"summary":"Get resolved task prompt content","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"}},"required":["id","text"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/extract-features":{"post":{"tags":["Task Prompts"],"summary":"Extract features from task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"force","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"taskPromptId":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["taskPromptId","features","cached"]}}}},"404":{"description":"Task prompt not found"},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts/{id}/features/{featureId}":{"patch":{"tags":["Task Prompts"],"summary":"Toggle feature flag on task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"featureId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchTaskPromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Invalid detected value"},"404":{"description":"Task prompt or feature not found"}}}},"/api/v1/reports":{"post":{"tags":["Reports"],"summary":"Create report","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Run or template not found"}}},"get":{"tags":["Reports"],"summary":"List reports","parameters":[{"schema":{"type":"string"},"required":false,"name":"requestId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionRunId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}}}}},"/api/v1/reports/bulk-create":{"post":{"tags":["Reports"],"summary":"Bulk create reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}/structured":{"get":{"tags":["Reports"],"summary":"Download a report's structured JSON artifact","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success"},"404":{"description":"Report or structured artifact not found"}}}},"/api/v1/reports/bulk-status":{"post":{"tags":["Reports"],"summary":"Bulk get report statuses","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportStatusInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/bulk-summary":{"post":{"tags":["Reports"],"summary":"Bulk get report summary per run","description":"Returns aggregated report status counts per run. Only the latest report per template is counted (re-triggers are deduplicated).","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}":{"get":{"tags":["Reports"],"summary":"Get report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"404":{"description":"Report not found"}}}},"/api/v1/reports/{id}/logs":{"get":{"tags":["Reports"],"summary":"Stream report logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Report not found"}}}},"/api/v1/reports/trigger":{"post":{"tags":["Reports"],"summary":"Trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"triggered":{"type":"number"},"reports":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}},"required":["triggered","reports"]}}}},"400":{"description":"Invalid input"},"404":{"description":"Run not found"}}}},"/api/v1/reports/trigger-submission":{"post":{"tags":["Reports"],"summary":"Trigger submission reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerSubmissionReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"triggered":{"type":"number"},"reports":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}},"required":["triggered","reports"]}}}},"404":{"description":"Submission attempt not found"},"409":{"description":"Submission attempt is not ready"}}}},"/api/v1/reports/bulk-trigger":{"post":{"tags":["Reports"],"summary":"Bulk trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkTriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":{}}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}/insights":{"get":{"tags":["Reports"],"summary":"Get insights for report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/InsightResponse"},{"type":"object","properties":{"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}}}]}}}}},"404":{"description":"Report not found"}}},"post":{"tags":["Reports"],"summary":"Link insight to report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"isNew":{"type":"boolean"}},"required":["insightId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]}}}},"404":{"description":"Report or insight not found"},"409":{"description":"Insight already referenced by this report"}}}},"/api/v1/profiles":{"post":{"tags":["Profiles"],"summary":"Create a new profile","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"get":{"tags":["Profiles"],"summary":"List profiles","parameters":[{"schema":{"type":"string"},"required":false,"name":"workerType","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}":{"get":{"tags":["Profiles"],"summary":"Get profile with latest version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"post":{"tags":["Profiles"],"summary":"Create new profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["workerType","model"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}},"put":{"tags":["Profiles"],"summary":"Update profile identity","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileIdentity"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"}}}}}},"delete":{"tags":["Profiles"],"summary":"Delete profile","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/profiles/{profileId}/versions":{"get":{"tags":["Profiles"],"summary":"List profile versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}/versions/{version}":{"get":{"tags":["Profiles"],"summary":"Get profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"},{"schema":{"type":["number","null"]},"required":false,"name":"version","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}},"/api/v1/report-templates/default-system-prompt":{"get":{"tags":["Report Templates"],"summary":"Get default system prompt","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string"}},"required":["content"]}}}}}}},"/api/v1/report-templates/available-models":{"get":{"tags":["Report Templates"],"summary":"List models available for report generation","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"]}}}}}}}},"/api/v1/report-templates":{"get":{"tags":["Report Templates"],"summary":"List report templates","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}}}}},"post":{"tags":["Report Templates"],"summary":"Create report template","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportTemplateInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"409":{"description":"Report template already exists"}}}},"/api/v1/report-templates/{id}":{"get":{"tags":["Report Templates"],"summary":"Get report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"put":{"tags":["Report Templates"],"summary":"Update report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateReportTemplateInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"delete":{"tags":["Report Templates"],"summary":"Delete report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Report template not found"}}}},"/api/v1/agents":{"get":{"tags":["Agents"],"summary":"List agents","parameters":[{"schema":{"type":"string"},"required":false,"name":"modelProvider","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentResponse"}}}}}}},"post":{"tags":["Agents"],"summary":"Create or update agent (upsert)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"409":{"description":"Restored agent versions conflict with an active queue owner"}}}},"/api/v1/agents/{id}":{"get":{"tags":["Agents"],"summary":"Get agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"404":{"description":"Agent not found"}}},"put":{"tags":["Agents"],"summary":"Update agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAgentInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"}}},"delete":{"tags":["Agents"],"summary":"Delete agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Agent not found"}}}},"/api/v1/agents/{id}/versions":{"get":{"tags":["Agents"],"summary":"List agent versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}}}}},"404":{"description":"Agent not found"}}},"post":{"tags":["Agents"],"summary":"Register agent version (upsert)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterAgentVersionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent registration did not stabilize"}}}},"/api/v1/agents/{id}/versions/{agentVersion}":{"patch":{"tags":["Agents"],"summary":"Patch agent version","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"agentVersion","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchAgentVersionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Invalid status value"},"404":{"description":"Agent or version not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent activation did not stabilize"}}}},"/api/v1/models":{"get":{"tags":["Models"],"summary":"List models","parameters":[{"schema":{"type":"string"},"required":false,"name":"agentId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"provider","in":"query"},{"schema":{"type":"string","enum":["active","disappeared"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ModelResponse"}}}}}}}},"/api/v1/models/{id}":{"get":{"tags":["Models"],"summary":"Get model","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelResponse"}}}}}}},"/api/v1/models/sync":{"post":{"tags":["Models"],"summary":"Sync models from provider","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"agentId":{"type":"string"},"provider":{"type":"string"},"models":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"providerAvailableFrom":{"type":"string"},"providerEndOfLife":{"type":"string"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}}},"required":["id"]}},"scannedAt":{"type":"string"}},"required":["agentId","provider","models","scannedAt"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"added":{"type":"array","items":{"type":"string"}},"removed":{"type":"array","items":{"type":"string"}},"unchanged":{"type":"array","items":{"type":"string"}}},"required":["added","removed","unchanged"]}}}}}}},"/api/v1/mcp/servers":{"get":{"tags":["MCP Servers"],"summary":"List MCP servers","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"post":{"tags":["MCP Servers"],"summary":"Create MCP server","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}},"required":["_id","name","type"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"/api/v1/mcp/servers/{id}":{"get":{"tags":["MCP Servers"],"summary":"Get MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"put":{"tags":["MCP Servers"],"summary":"Update MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMcpServerInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"delete":{"tags":["MCP Servers"],"summary":"Delete MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/skills":{"get":{"tags":["Skills"],"summary":"List all skills","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"post":{"tags":["Skills"],"summary":"Create or import a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSkillInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"/api/v1/skills/search":{"get":{"tags":["Skills"],"summary":"Search skills (internal + external)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/search/external":{"get":{"tags":["Skills"],"summary":"Search external skills registry","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project."},"required":false,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/discover":{"get":{"tags":["Skills"],"summary":"Discover skills in a GitHub repository","parameters":[{"schema":{"type":"string"},"required":true,"name":"source","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillDiscoveryResult"}}}}},"400":{"description":"Missing or malformed source parameter"},"404":{"description":"Repository not found"},"502":{"description":"GitHub API error"}}}},"/api/v1/skills/{id}(*)/revisions":{"get":{"tags":["Skills"],"summary":"List skill revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}}},"404":{"description":"Skill not found"}}}},"/api/v1/skills/{id}(*)":{"get":{"tags":["Skills"],"summary":"Get skill by slug","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}},"404":{"description":"Skill not found"}}},"delete":{"tags":["Skills"],"summary":"Soft-delete a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Skill not found"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)/archive":{"get":{"tags":["Skill Revisions"],"summary":"Download skill revision archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Skill revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ref","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skill-revisions/{id}":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ID","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skills/{id}(*)/resolve":{"post":{"tags":["Skills"],"summary":"Trigger skill resolution","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebases":{"get":{"tags":["Codebases"],"summary":"List all codebases","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseResponse"}}}}}}},"post":{"tags":["Codebases"],"summary":"Create a codebase (archive codebases require the archive file)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input or missing archive"}}}},"/api/v1/codebases/{id}":{"get":{"tags":["Codebases"],"summary":"Get a codebase","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"patch":{"tags":["Codebases"],"summary":"Update a codebase","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCodebaseInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"delete":{"tags":["Codebases"],"summary":"Delete a codebase","responses":{"204":{"description":"Success"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebases/{id}/revisions":{"get":{"tags":["Codebases"],"summary":"List codebase revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}}},"404":{"description":"Codebase not found"}}},"post":{"tags":["Codebases"],"summary":"Resolve a new git codebase revision","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveCodebaseRevisionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"400":{"description":"Not a git codebase"},"404":{"description":"Codebase not found"},"502":{"description":"GitHub resolution failed"}}}},"/api/v1/codebases/{id}/upload":{"post":{"tags":["Codebases"],"summary":"Upload a codebase archive as a new revision","responses":{"201":{"description":"Success"},"400":{"description":"Missing archive or not an archive codebase"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebase-revisions/{id}/archive":{"get":{"tags":["Codebase Revisions"],"summary":"Download codebase revision archive","responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebase-revisions/{id}":{"get":{"tags":["Codebase Revisions"],"summary":"Get codebase revision by id","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources":{"get":{"tags":["Resources"],"summary":"List all resources","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceResponse"}}}}}}},"post":{"tags":["Resources"],"summary":"Create a resource and its first revision","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceInput"}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input"},"409":{"description":"Resource slug or revision ref already exists in this project"}}}},"/api/v1/resources/{id}":{"get":{"tags":["Resources"],"summary":"Get a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"patch":{"tags":["Resources"],"summary":"Update a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateResourceInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"delete":{"tags":["Resources"],"summary":"Delete a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Resource not found"}}}},"/api/v1/resources/{id}/revisions":{"get":{"tags":["Resource Revisions"],"summary":"List resource revisions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}}},"404":{"description":"Resource not found"}}},"post":{"tags":["Resource Revisions"],"summary":"Create a resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceRevisionInput"}}}},"responses":{"201":{"description":"Success"},"404":{"description":"Resource not found"},"409":{"description":"Resource revision ref already exists in this project"}}}},"/api/v1/resources/{id}/revisions/latest":{"get":{"tags":["Resource Revisions"],"summary":"Get the latest resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/{id}/revisions/{revisionNumber}":{"get":{"tags":["Resource Revisions"],"summary":"Get a resource revision by resource and revision number","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"revisionNumber","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/revisions/{id}":{"get":{"tags":["Resource Revisions"],"summary":"Get resource revision by id","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/extensions":{"get":{"tags":["Extensions"],"summary":"List all extensions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"post":{"tags":["Extensions"],"summary":"Create or import an extension","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExtensionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"/api/v1/extensions/search":{"get":{"tags":["Extensions"],"summary":"Search extensions (internal + marketplace)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/extensions/{id}":{"get":{"tags":["Extensions"],"summary":"Get extension by ID","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"put":{"tags":["Extensions"],"summary":"Update extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateExtensionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"delete":{"tags":["Extensions"],"summary":"Delete extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/extensions/{id}/versions":{"get":{"tags":["Extensions"],"summary":"List available versions for an extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"preRelease","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionVersionInfo"}}}}}}}},"/api/v1/insights":{"get":{"tags":["Insights"],"summary":"List insights","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"post":{"tags":["Insights"],"summary":"Create insight","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInsightInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"/api/v1/insights/search":{"get":{"tags":["Insights"],"summary":"Search insights","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}}},"/api/v1/insights/{id}":{"get":{"tags":["Insights"],"summary":"Get insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"put":{"tags":["Insights"],"summary":"Update insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateInsightInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"delete":{"tags":["Insights"],"summary":"Delete insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/upvote":{"post":{"tags":["Insights"],"summary":"Upvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/downvote":{"post":{"tags":["Insights"],"summary":"Downvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/block":{"post":{"tags":["Insights"],"summary":"Block insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/unblock":{"post":{"tags":["Insights"],"summary":"Unblock insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/reports":{"get":{"tags":["Insights"],"summary":"Get reports referencing insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Insight not found"}}}},"/api/v1/feature-flags":{"get":{"tags":["Feature Flags"],"summary":"List feature flags","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"/api/v1/feature-flags/{key}":{"put":{"tags":["Feature Flags"],"summary":"Update feature flag","parameters":[{"schema":{"type":"string"},"required":true,"name":"key","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeatureFlagInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"webhooks":{}} \ No newline at end of file