From 9abaccb3490a6a681c192e20f0e564f7d63a3395 Mon Sep 17 00:00:00 2001 From: Cedric Vidal Date: Fri, 16 Oct 2026 17:21:44 -0700 Subject: [PATCH] feat: add Experiment lifecycle recovery Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e767814e-3331-4a7e-ae96-15207b53ac9c --- .../openapi-snapshot.test.ts.snap | 519 ++++++++++++++ apps/api/src/experiment-coordinator.test.ts | 120 ++++ apps/api/src/experiment-coordinator.ts | 35 +- apps/api/src/experiment-dispatcher.ts | 22 +- apps/api/src/experiment-lifecycle.test.ts | 623 +++++++++++++++++ apps/api/src/experiment-lifecycle.ts | 661 ++++++++++++++++++ apps/api/src/routes/experiments.test.ts | 118 ++++ apps/api/src/routes/experiments.ts | 268 ++++++- .../api/src/routes/reports-structured.test.ts | 1 + apps/api/src/routes/reports.ts | 4 + .../__snapshots__/cli-commands.test.ts.snap | 25 + apps/cli/src/commands/experiment.test.ts | 133 ++++ apps/cli/src/commands/experiment.ts | 260 ++++++- apps/portal/src/lib/api.ts | 55 +- apps/portal/src/lib/experiments.ts | 106 +++ .../src/pages/ExperimentDetail.play.test.tsx | 65 ++ .../src/pages/ExperimentDetail.stories.tsx | 51 ++ .../src/pages/ExperimentDetail.test.tsx | 76 ++ apps/portal/src/pages/ExperimentDetail.tsx | 160 ++++- .../src/pages/ExperimentsList.play.test.tsx | 72 +- .../src/pages/ExperimentsList.stories.tsx | 48 ++ .../portal/src/pages/ExperimentsList.test.tsx | 42 ++ apps/portal/src/pages/ExperimentsList.tsx | 46 +- .../readout/src/readout-queue-processor.ts | 3 + .../src/report-queue-processor.ts | 14 +- docs/architecture/app-design.md | 10 + docs/architecture/experiments.md | 58 +- ...6-add-experiment-lifecycle-indexes.test.ts | 45 ++ .../036-add-experiment-lifecycle-indexes.ts | 48 ++ .../db-migrations/src/required-migrations.ts | 1 + .../src/experiments/experiment-store.test.ts | 7 + .../src/experiments/experiment-store.ts | 53 +- packages/shared/src/experiments/index.ts | 1 + .../src/experiments/lifecycle-target-store.ts | 109 +++ packages/shared/src/experiments/schemas.ts | 39 ++ packages/shared/src/experiments/types.ts | 43 ++ .../src/queue/base-queue-processor.test.ts | 5 +- .../shared/src/queue/base-queue-processor.ts | 10 +- .../shared/src/queue/queue-processor.test.ts | 1 + packages/shared/src/queue/queue-processor.ts | 1 + packages/shared/src/schemas/report.ts | 9 + packages/shared/src/schemas/request.ts | 8 + packages/shared/src/types/types.ts | 11 + .../docs/guides/creating-experiments.mdx | 42 ++ website/src/openapi/scope-openapi.json | 2 +- 45 files changed, 3968 insertions(+), 62 deletions(-) create mode 100644 apps/api/src/experiment-lifecycle.test.ts create mode 100644 apps/api/src/experiment-lifecycle.ts create mode 100644 packages/db-migrations/src/036-add-experiment-lifecycle-indexes.test.ts create mode 100644 packages/db-migrations/src/migrations/036-add-experiment-lifecycle-indexes.ts create mode 100644 packages/shared/src/experiments/lifecycle-target-store.ts diff --git a/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap b/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap index 605b9ae9..bcfe5dd4 100644 --- a/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap +++ b/apps/api/src/__snapshots__/openapi-snapshot.test.ts.snap @@ -1841,6 +1841,124 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], "type": "object", }, + "ExperimentLifecycleResponse": { + "properties": { + "blocking": { + "items": { + "properties": { + "error": { + "type": "string", + }, + "id": { + "type": "string", + }, + "kind": { + "enum": [ + "request", + "report", + "experiment", + ], + "type": "string", + }, + "reason": { + "type": "string", + }, + }, + "required": [ + "kind", + "id", + "reason", + ], + "type": "object", + }, + "type": "array", + }, + "deleteCompletedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "deleteRequestedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "experimentId": { + "type": "string", + }, + "operationId": { + "type": "string", + }, + "phase": { + "enum": [ + "deleting", + "deleted", + "restoring", + "restored", + ], + "type": "string", + }, + "progress": { + "properties": { + "completedReports": { + "minimum": 0, + "type": "integer", + }, + "completedRequests": { + "minimum": 0, + "type": "integer", + }, + "expectedReports": { + "minimum": 0, + "type": "integer", + }, + "expectedRequests": { + "minimum": 0, + "type": "integer", + }, + }, + "required": [ + "expectedRequests", + "completedRequests", + "expectedReports", + "completedReports", + ], + "type": "object", + }, + "restoreCompletedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "restoreRequestedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "stateVersion": { + "exclusiveMinimum": 0, + "type": "integer", + }, + }, + "required": [ + "experimentId", + "operationId", + "phase", + "stateVersion", + "progress", + "blocking", + "deleteRequestedAt", + ], + "type": "object", + }, "ExperimentListResponse": { "properties": { "data": { @@ -2207,9 +2325,135 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], "type": "object", }, + "deletedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, "experimentId": { "type": "string", }, + "lifecycle": { + "properties": { + "blocking": { + "items": { + "properties": { + "error": { + "type": "string", + }, + "id": { + "type": "string", + }, + "kind": { + "enum": [ + "request", + "report", + "experiment", + ], + "type": "string", + }, + "reason": { + "enum": [ + "target-missing", + "target-conflict", + "reconciliation-failed", + ], + "type": "string", + }, + }, + "required": [ + "kind", + "id", + "reason", + ], + "type": "object", + }, + "type": "array", + }, + "deleteCompletedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "deleteRequestedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "operationId": { + "type": "string", + }, + "phase": { + "enum": [ + "deleting", + "deleted", + "restoring", + "restored", + ], + "type": "string", + }, + "progress": { + "properties": { + "completedReports": { + "minimum": 0, + "type": "integer", + }, + "completedRequests": { + "minimum": 0, + "type": "integer", + }, + "expectedReports": { + "minimum": 0, + "type": "integer", + }, + "expectedRequests": { + "minimum": 0, + "type": "integer", + }, + }, + "required": [ + "expectedRequests", + "completedRequests", + "expectedReports", + "completedReports", + ], + "type": "object", + }, + "restoreCompletedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "restoreRequestedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "stateVersion": { + "exclusiveMinimum": 0, + "type": "integer", + }, + }, + "required": [ + "operationId", + "phase", + "stateVersion", + "progress", + "blocking", + "deleteRequestedAt", + ], + "type": "object", + }, "metadata": { "additionalProperties": false, "properties": { @@ -3364,6 +3608,38 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "null", ], }, + "deletedAt": { + "format": "date-time", + "type": [ + "string", + "null", + ], + }, + "deletionProvenance": { + "additionalProperties": false, + "properties": { + "experimentId": { + "minLength": 1, + "type": "string", + }, + "kind": { + "enum": [ + "experiment", + ], + "type": "string", + }, + "operationId": { + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "kind", + "experimentId", + "operationId", + ], + "type": "object", + }, "error": { "type": "string", }, @@ -3814,6 +4090,31 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "null", ], }, + "deletionProvenance": { + "additionalProperties": false, + "properties": { + "experimentId": { + "minLength": 1, + "type": "string", + }, + "kind": { + "enum": [ + "experiment", + ], + "type": "string", + }, + "operationId": { + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "kind", + "experimentId", + "operationId", + ], + "type": "object", + }, "experimentExpectedRequestCount": { "minimum": 0, "type": "integer", @@ -6801,6 +7102,18 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "type": "string", }, }, + { + "in": "query", + "name": "includeDeleted", + "required": false, + "schema": { + "enum": [ + "true", + "false", + ], + "type": "string", + }, + }, ], "responses": { "200": { @@ -6872,6 +7185,54 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` }, }, "/api/v1/experiments/{id}": { + "delete": { + "description": "Returns 202 with a stable lifecycle operation ID. Poll the lifecycle endpoint until phase is deleted or blockers require intervention.", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "minLength": 1, + "type": "string", + }, + }, + { + "description": "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", + }, + }, + ], + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExperimentLifecycleResponse", + }, + }, + }, + "description": "Success", + }, + "404": { + "description": "Experiment not found", + }, + "409": { + "description": "An incompatible Experiment mutation is active", + }, + }, + "summary": "Start or resume cascading Experiment deletion", + "tags": [ + "Experiments", + ], + }, "get": { "parameters": [ { @@ -6895,6 +7256,18 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "type": "string", }, }, + { + "in": "query", + "name": "includeDeleted", + "required": false, + "schema": { + "enum": [ + "true", + "false", + ], + "type": "string", + }, + }, ], "responses": { "200": { @@ -6961,6 +7334,18 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "type": "string", }, }, + { + "in": "query", + "name": "includeDeleted", + "required": false, + "schema": { + "enum": [ + "true", + "false", + ], + "type": "string", + }, + }, ], "responses": { "200": { @@ -7016,6 +7401,18 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "type": "string", }, }, + { + "in": "query", + "name": "includeDeleted", + "required": false, + "schema": { + "enum": [ + "true", + "false", + ], + "type": "string", + }, + }, ], "responses": { "200": { @@ -7131,6 +7528,18 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "type": "string", }, }, + { + "in": "query", + "name": "includeDeleted", + "required": false, + "schema": { + "enum": [ + "true", + "false", + ], + "type": "string", + }, + }, ], "responses": { "200": { @@ -7281,6 +7690,55 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], }, }, + "/api/v1/experiments/{id}/lifecycle": { + "get": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "minLength": 1, + "type": "string", + }, + }, + { + "description": "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", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExperimentLifecycleResponse", + }, + }, + }, + "description": "Success", + }, + "404": { + "description": "Experiment not found", + }, + "409": { + "description": "Experiment has no lifecycle operation", + }, + }, + "summary": "Get and repair Experiment lifecycle progress", + "tags": [ + "Experiments", + ], + }, + }, "/api/v1/experiments/{id}/report-templates": { "put": { "description": "Selection is an unordered set. A real change creates a new Experiment revision and execution; duplicate- or order-only changes are no-ops.", @@ -7457,6 +7915,55 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` ], }, }, + "/api/v1/experiments/{id}/restore": { + "post": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "minLength": 1, + "type": "string", + }, + }, + { + "description": "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", + }, + }, + ], + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExperimentLifecycleResponse", + }, + }, + }, + "description": "Success", + }, + "404": { + "description": "Experiment not found", + }, + "409": { + "description": "Experiment is not deleted or another intent is active", + }, + }, + "summary": "Start or resume cascading Experiment restore", + "tags": [ + "Experiments", + ], + }, + }, "/api/v1/experiments/{id}/retry": { "post": { "parameters": [ @@ -10464,6 +10971,18 @@ exports[`OpenAPI spec snapshot > matches the committed snapshot 1`] = ` "type": "string", }, }, + { + "in": "query", + "name": "includeDeleted", + "required": false, + "schema": { + "enum": [ + "true", + "false", + ], + "type": "string", + }, + }, ], "responses": { "200": { diff --git a/apps/api/src/experiment-coordinator.test.ts b/apps/api/src/experiment-coordinator.test.ts index 056ac11b..2fab1d4e 100644 --- a/apps/api/src/experiment-coordinator.test.ts +++ b/apps/api/src/experiment-coordinator.test.ts @@ -265,6 +265,126 @@ describe("ExperimentCoordinator", () => { ); }); + it("soft-deletes an uncommitted report when lifecycle deletion wins the CAS", async () => { + const initial = experiment([], { + handlerSummary: { + "pp-readout": { + status: "done", + version: 1, + updatedAt: now, + }, + }, + reportTemplates: {}, + report: { + reportId: "readout-1", + url: "/api/v1/reports/readout-1", + updatedAt: now, + }, + }); + initial.revision.reportTemplateIds = []; + const deleting = structuredClone(initial); + deleting.lifecycle = { + operationId: "operation-1", + phase: "deleting", + stateVersion: 1, + targets: { + storage: "inline", + entryCount: 0, + payloadFingerprint: fingerprintExperimentPayload([]), + payloadVersion: 1, + entries: [], + }, + targetFingerprint: fingerprintExperimentPayload([]), + progress: { + expectedRequests: 0, + completedRequests: 0, + expectedReports: 0, + completedReports: 0, + }, + blocking: [], + deleteRequestedAt: now, + }; + const template = { + _id: "always", + projectId: "project-1", + id: "always", + subject: "experiment", + name: "Always", + userPrompt: "Summarize", + trigger: { type: "always" as const }, + enabled: true, + currentVersionId: "always:v1", + currentVersion: 1, + contentFingerprint: "always-fingerprint", + createdAt: now, + }; + const updateMany = vi.fn().mockResolvedValue({ modifiedCount: 1 }); + vi.spyOn( + ExperimentStore.prototype, + "updateReportTemplateSummaries", + ).mockResolvedValue(null); + vi.spyOn( + ExperimentStore.prototype, + "getIncludingDeleted", + ).mockResolvedValue(deleting); + const coordinator = new ExperimentCoordinator( + { + requests: {} as Collection, + experiments: {} as Collection, + executions: {} as Collection, + chunks: {} as Collection, + services: { + find: vi.fn().mockReturnValue(cursor([{ + _id: "pp-readout", + type: "experiment-handler", + version: 1, + queue: "queue-readout", + selector: "readout", + autoBackfill: false, + dependsOn: [], + dependencyMode: "all-runs", + }])), + } as unknown as Collection, + reportTemplates: { + find: vi.fn().mockReturnValue(cursor([template])), + } as unknown as Collection, + reportTemplateVersions: {} as Collection, + reports: { + insertOne: vi.fn().mockResolvedValue({ acknowledged: true }), + findOne: vi.fn(), + updateMany, + } as unknown as Collection, + }, + vi.fn(), + ); + + const dispatched = await ( + coordinator as unknown as { + dispatchReadyReportTemplates( + experiment: ExperimentDocument, + requests: readonly ExperimentRequestState[], + requestById: ReadonlyMap, + ): Promise; + } + ).dispatchReadyReportTemplates(initial, [], new Map()); + + expect(dispatched).toBe(false); + expect(updateMany).toHaveBeenCalledWith( + expect.objectContaining({ + projectId: "project-1", + experimentId: "experiment-1", + experimentExecutionId: "execution-1", + status: "pending", + deletedAt: { $exists: false }, + }), + { + $set: { + deletedAt: expect.any(Date), + }, + }, + ); + }); + it("projects authoritative run status and dispatches an exact execution message", async () => { const initialRequest: ExperimentRequestState = { requestId: "request-1", diff --git a/apps/api/src/experiment-coordinator.ts b/apps/api/src/experiment-coordinator.ts index 4aee93ff..d10482b6 100644 --- a/apps/api/src/experiment-coordinator.ts +++ b/apps/api/src/experiment-coordinator.ts @@ -826,7 +826,40 @@ export class ExperimentCoordinator { summaries, now, }); - if (!updated) return false; + if (!updated) { + const current = await this.store.getIncludingDeleted( + experiment.projectId, + experiment.experimentId, + ); + if ( + current?.lifecycle?.phase === "deleting" || + current?.lifecycle?.phase === "deleted" || + current?.lifecycle?.phase === "restoring" + ) { + await this.collections.reports.updateMany( + { + _id: { + $in: [...newlyQueued].flatMap((templateId) => { + const reportId = summaries[templateId]?.report?.reportId; + return reportId ? [reportId] : []; + }), + }, + projectId: experiment.projectId, + experimentId: experiment.experimentId, + experimentExecutionId: + experiment.execution.experimentExecutionId, + status: "pending", + deletedAt: { $exists: false }, + } as Document, + { + $set: { + deletedAt: new Date(), + }, + } as Document, + ); + } + return false; + } experiment = updated; } diff --git a/apps/api/src/experiment-dispatcher.ts b/apps/api/src/experiment-dispatcher.ts index d5e25664..c8938ee3 100644 --- a/apps/api/src/experiment-dispatcher.ts +++ b/apps/api/src/experiment-dispatcher.ts @@ -16,6 +16,7 @@ import { type ExperimentDispatchMessage, type ExperimentTemplateDispatchMessage, } from "./experiment-coordinator.js"; +import { ExperimentLifecycleCoordinator } from "./experiment-lifecycle.js"; export class ExperimentDispatcher { private interval: ReturnType | null = null; @@ -23,6 +24,7 @@ export class ExperimentDispatcher { private scanAfterExperimentId: string | null = null; private readonly queueClients = new Map(); private readonly coordinator: ExperimentCoordinator; + private readonly lifecycle: ExperimentLifecycleCoordinator; constructor( private readonly requests: Collection, @@ -47,6 +49,13 @@ export class ExperimentDispatcher { }, (service, message) => this.enqueue(service, message), ); + this.lifecycle = new ExperimentLifecycleCoordinator({ + requests, + reports: db.collection("reports"), + experiments, + executions, + chunks, + }); } start(): void { @@ -127,14 +136,25 @@ export class ExperimentDispatcher { try { await this.coordinator.recoverInterruptedCreations(this.batchSize); await this.coordinator.recoverInterruptedMemberships(this.batchSize); + await this.lifecycle.repairActive(this.batchSize); const candidates = await this.experiments .find( this.scanAfterExperimentId ? { _id: { $gt: this.scanAfterExperimentId }, finalizedAt: { $exists: true }, + deletedAt: { $exists: false }, + "lifecycle.phase": { + $nin: ["deleting", "deleted", "restoring"], + }, } - : { finalizedAt: { $exists: true } }, + : { + finalizedAt: { $exists: true }, + deletedAt: { $exists: false }, + "lifecycle.phase": { + $nin: ["deleting", "deleted", "restoring"], + }, + }, ) .sort({ _id: 1 }) .limit(this.batchSize) diff --git a/apps/api/src/experiment-lifecycle.test.ts b/apps/api/src/experiment-lifecycle.test.ts new file mode 100644 index 00000000..bc23e830 --- /dev/null +++ b/apps/api/src/experiment-lifecycle.test.ts @@ -0,0 +1,623 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { Collection, Document } from "mongodb"; +import { + EXPERIMENT_STORAGE_PROFILE_VERSION, + ExperimentLifecycleTargetStore, + ExperimentStore, + FixedPayloadStore, + fingerprintExperimentPayload, + type ExperimentDocument, + type ExperimentExecutionHistoryDocument, + type ExperimentLifecycleTarget, + type ExperimentRequestState, + type ReportDocument, + type RequestDocument, +} from "shared"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + ExperimentLifecycleCoordinator, + type ExperimentLifecycleCollections, +} from "./experiment-lifecycle.js"; + +const now = new Date("2026-03-10T12:00:00.000Z"); +const requestState: ExperimentRequestState = { + requestId: "request-1", + runId: "run-1", + dependencies: [], + blockers: [], +}; +const requestFingerprint = fingerprintExperimentPayload([requestState]); + +function root( + lifecycle?: ExperimentDocument["lifecycle"], +): ExperimentDocument { + return { + _id: "experiment-1", + experimentId: "experiment-1", + projectId: "project-1", + metadata: { name: "Lifecycle experiment" }, + storageProfileVersion: EXPERIMENT_STORAGE_PROFILE_VERSION, + stateVersion: lifecycle ? 2 : 1, + revision: { + experimentRevisionId: "revision-1", + revisionNumber: 1, + memberCount: 1, + membershipFingerprint: requestFingerprint, + definitionFingerprint: "definition-1", + createdAt: now, + }, + execution: { + experimentExecutionId: "execution-1", + attemptNumber: 1, + version: lifecycle ? 2 : 1, + requests: { + storage: "inline", + entryCount: 1, + payloadFingerprint: requestFingerprint, + payloadVersion: 1, + entries: [requestState], + }, + vectorFingerprint: requestFingerprint, + handlerSummary: {}, + blocking: [], + createdAt: now, + updatedAt: now, + ...(lifecycle ? { frozenAt: now } : {}), + }, + createdAt: now, + updatedAt: now, + finalizedAt: now, + ...(lifecycle ? { lifecycle } : {}), + }; +} + +function lifecycle( + phase: "deleting" | "deleted" | "restoring", + targets: ExperimentLifecycleTarget[], +): NonNullable { + const encoded = targets.map((target) => ({ + requestId: fingerprintExperimentPayload([target.kind, target.id]), + runId: target.kind, + dependencies: [{ + handlerId: target.id, + state: "ready" as const, + }], + blockers: [], + })); + return { + operationId: "operation-1", + phase, + stateVersion: 1, + targets: { + storage: "inline", + entryCount: encoded.length, + payloadFingerprint: fingerprintExperimentPayload(encoded), + payloadVersion: 1, + entries: encoded, + }, + targetFingerprint: fingerprintExperimentPayload(targets), + progress: { + expectedRequests: targets.filter((target) => target.kind === "request") + .length, + completedRequests: 0, + expectedReports: targets.filter((target) => target.kind === "report") + .length, + completedReports: 0, + }, + blocking: [], + deleteRequestedAt: now, + ...(phase !== "deleting" ? { deleteCompletedAt: now } : {}), + ...(phase === "restoring" ? { restoreRequestedAt: now } : {}), + }; +} + +function cursor(documents: Document[]) { + return { + sort: vi.fn().mockReturnThis(), + limit: vi.fn().mockReturnThis(), + project: vi.fn().mockReturnThis(), + toArray: vi.fn().mockResolvedValue(documents), + }; +} + +function collections(input: { + current: ExperimentDocument; + requests?: Document[]; + reports?: Document[]; + updated: ExperimentDocument; +}): ExperimentLifecycleCollections { + return { + requests: { + find: vi.fn().mockReturnValue(cursor(input.requests ?? [])), + updateMany: vi.fn().mockResolvedValue({ matchedCount: 1 }), + } as unknown as Collection, + reports: { + find: vi.fn().mockReturnValue(cursor(input.reports ?? [])), + updateMany: vi.fn().mockResolvedValue({ matchedCount: 1 }), + } as unknown as Collection, + experiments: { + findOne: vi.fn().mockResolvedValue(input.current), + findOneAndUpdate: vi.fn().mockResolvedValue(input.updated), + } as unknown as Collection, + executions: {} as Collection, + chunks: {} as Collection, + }; +} + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("ExperimentLifecycleCoordinator", () => { + it("treats a completed restore as an idempotent success", async () => { + const restored = root({ + ...lifecycle("deleted", []), + phase: "restored", + restoreCompletedAt: now, + }); + const db = collections({ + current: restored, + updated: restored, + }); + + const result = await new ExperimentLifecycleCoordinator(db).startRestore( + "project-1", + "experiment-1", + now, + ); + + expect(result.lifecycle?.phase).toBe("restored"); + expect(result.lifecycle?.operationId).toBe("operation-1"); + expect(db.experiments.findOneAndUpdate).not.toHaveBeenCalled(); + }); + + it("claims an asynchronous delete with only active current members and reports", async () => { + const current = root(); + const claimed = root( + lifecycle("deleting", [ + { kind: "request", id: "request-1" }, + { kind: "report", id: "report-1" }, + ]), + ); + const db = collections({ + current, + requests: [{ _id: "request-1" }], + reports: [{ _id: "report-1" }], + updated: claimed, + }); + vi.spyOn(FixedPayloadStore.prototype, "readRequests").mockResolvedValue([ + requestState, + ]); + const prepare = vi + .spyOn(ExperimentLifecycleTargetStore.prototype, "prepare") + .mockImplementation(async (input) => ({ + storage: "inline", + entryCount: input.targets.length, + payloadFingerprint: "target-payload", + payloadVersion: 1, + entries: [], + })); + + const result = await new ExperimentLifecycleCoordinator(db).startDelete( + "project-1", + "experiment-1", + now, + ); + + expect(result.lifecycle?.phase).toBe("deleting"); + expect(prepare).toHaveBeenCalledWith( + expect.objectContaining({ + projectId: "project-1", + experimentId: "experiment-1", + targets: [ + { kind: "request", id: "request-1" }, + { kind: "report", id: "report-1" }, + ], + }), + ); + expect(db.requests.updateMany).not.toHaveBeenCalled(); + expect(db.reports.updateMany).not.toHaveBeenCalled(); + expect(db.experiments.findOneAndUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + _id: "experiment-1", + projectId: "project-1", + stateVersion: 1, + }), + expect.objectContaining({ + $set: expect.objectContaining({ + "execution.frozenAt": now, + }), + }), + { returnDocument: "after" }, + ); + }); + + it("repairs deletion with exact provenance and reaches deleted only after every target", async () => { + const targets: ExperimentLifecycleTarget[] = [ + { kind: "request", id: "request-1" }, + { kind: "report", id: "report-1" }, + ]; + const current = root(lifecycle("deleting", targets)); + const deleted = { + ...root(lifecycle("deleted", targets)), + deletedAt: now, + }; + const provenance = { + kind: "experiment", + experimentId: "experiment-1", + operationId: "operation-1", + }; + const db = collections({ + current, + requests: [{ + _id: "request-1", + deletedAt: now, + deletionProvenance: provenance, + }], + reports: [{ + _id: "report-1", + deletedAt: now, + deletionProvenance: provenance, + }], + updated: deleted, + }); + vi.spyOn(ExperimentLifecycleTargetStore.prototype, "read").mockResolvedValue( + targets, + ); + + const result = await new ExperimentLifecycleCoordinator(db).repair( + "project-1", + "experiment-1", + ); + + expect(result.lifecycle?.phase).toBe("deleted"); + expect(db.requests.updateMany).toHaveBeenCalledWith( + expect.objectContaining({ + _id: { $in: ["request-1"] }, + projectId: "project-1", + experimentId: "experiment-1", + deletedAt: { $exists: false }, + }), + { $set: { deletedAt: expect.any(Date), deletionProvenance: provenance } }, + ); + expect(db.reports.updateMany).toHaveBeenCalledWith( + expect.objectContaining({ + _id: { $in: ["report-1"] }, + deletedAt: { $exists: false }, + }), + { $set: { deletedAt: expect.any(Date), deletionProvenance: provenance } }, + ); + expect(db.experiments.findOneAndUpdate).toHaveBeenLastCalledWith( + expect.objectContaining({ + stateVersion: current.stateVersion, + "lifecycle.stateVersion": current.lifecycle?.stateVersion, + }), + expect.objectContaining({ + $set: expect.objectContaining({ + "lifecycle.phase": "deleted", + deletedAt: expect.any(Date), + }), + }), + { returnDocument: "after" }, + ); + }); + + it("restores only exact-operation children before reviving the Experiment", async () => { + const targets: ExperimentLifecycleTarget[] = [ + { kind: "request", id: "request-1" }, + { kind: "report", id: "report-1" }, + ]; + const current = root(lifecycle("restoring", targets)); + current.deletedAt = now; + const restored = root({ + ...lifecycle("restoring", targets), + phase: "restored", + restoreCompletedAt: now, + }); + const db = collections({ + current, + requests: [{ _id: "request-1", run: { _id: "run-1" } }], + reports: [{ _id: "report-1" }], + updated: restored, + }); + vi.spyOn(ExperimentLifecycleTargetStore.prototype, "read").mockResolvedValue( + targets, + ); + vi.spyOn(FixedPayloadStore.prototype, "readRequests").mockResolvedValue([ + requestState, + ]); + + const result = await new ExperimentLifecycleCoordinator(db).repair( + "project-1", + "experiment-1", + ); + + const provenance = { + kind: "experiment", + experimentId: "experiment-1", + operationId: "operation-1", + }; + expect(db.requests.updateMany).toHaveBeenCalledWith( + expect.objectContaining({ + _id: { $in: ["request-1"] }, + projectId: "project-1", + deletionProvenance: provenance, + }), + { $unset: { deletedAt: "", deletionProvenance: "" } }, + ); + expect(db.reports.updateMany).toHaveBeenCalledWith( + expect.objectContaining({ + deletionProvenance: provenance, + }), + { $unset: { deletedAt: "", deletionProvenance: "" } }, + ); + expect(result.lifecycle?.phase).toBe("restored"); + expect(db.experiments.findOneAndUpdate).toHaveBeenLastCalledWith( + expect.objectContaining({ + stateVersion: current.stateVersion, + "lifecycle.stateVersion": current.lifecycle?.stateVersion, + }), + expect.objectContaining({ + $unset: { + deletedAt: "", + "execution.frozenAt": "", + }, + }), + { returnDocument: "after" }, + ); + }); + + it("converges after a delete completion CAS loses to another repair", async () => { + const targets: ExperimentLifecycleTarget[] = [ + { kind: "request", id: "request-1" }, + ]; + const deleting = root(lifecycle("deleting", targets)); + const deleted = { + ...root(lifecycle("deleted", targets)), + deletedAt: now, + }; + const provenance = { + kind: "experiment", + experimentId: "experiment-1", + operationId: "operation-1", + }; + const db = collections({ + current: deleting, + requests: [{ + _id: "request-1", + deletedAt: now, + deletionProvenance: provenance, + }], + updated: deleted, + }); + vi.mocked(db.experiments.findOne) + .mockResolvedValueOnce(deleting) + .mockResolvedValueOnce(deleted); + vi.mocked(db.experiments.findOneAndUpdate).mockResolvedValueOnce(null); + vi.spyOn(ExperimentLifecycleTargetStore.prototype, "read").mockResolvedValue( + targets, + ); + + const result = await new ExperimentLifecycleCoordinator(db).repair( + "project-1", + "experiment-1", + ); + + expect(result.lifecycle?.phase).toBe("deleted"); + expect(db.experiments.findOneAndUpdate).toHaveBeenCalledTimes(1); + expect(db.experiments.findOne).toHaveBeenCalledTimes(2); + }); + + it("keeps a missing target as a repairable blocker", async () => { + const targets: ExperimentLifecycleTarget[] = [ + { kind: "request", id: "missing-request" }, + ]; + const deleting = root(lifecycle("deleting", targets)); + const blocked = root({ + ...lifecycle("deleting", targets), + stateVersion: 2, + blocking: [{ + kind: "request", + id: "missing-request", + reason: "target-missing", + }], + }); + const db = collections({ + current: deleting, + requests: [], + updated: blocked, + }); + vi.spyOn(ExperimentLifecycleTargetStore.prototype, "read").mockResolvedValue( + targets, + ); + + const result = await new ExperimentLifecycleCoordinator(db).repair( + "project-1", + "experiment-1", + ); + + expect(result.lifecycle?.phase).toBe("deleting"); + expect(result.lifecycle?.blocking).toEqual([{ + kind: "request", + id: "missing-request", + reason: "target-missing", + }]); + expect(db.experiments.findOneAndUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + stateVersion: deleting.stateVersion, + "lifecycle.stateVersion": deleting.lifecycle?.stateVersion, + }), + expect.objectContaining({ + $set: expect.objectContaining({ + "lifecycle.progress.completedRequests": 0, + "lifecycle.blocking": [{ + kind: "request", + id: "missing-request", + reason: "target-missing", + }], + }), + }), + { returnDocument: "after" }, + ); + }); + + it("does not restore an independently deleted record outside the operation snapshot", async () => { + const targets: ExperimentLifecycleTarget[] = [ + { kind: "request", id: "request-1" }, + ]; + const restoring = root(lifecycle("restoring", targets)); + restoring.deletedAt = now; + const restored = root({ + ...lifecycle("restoring", targets), + phase: "restored", + restoreCompletedAt: now, + }); + const db = collections({ + current: restoring, + requests: [ + { _id: "request-1", run: { _id: "run-1" } }, + { + _id: "request-independent", + deletedAt: now, + deletionProvenance: { kind: "user" }, + run: { _id: "run-independent" }, + }, + ], + updated: restored, + }); + vi.spyOn(ExperimentLifecycleTargetStore.prototype, "read").mockResolvedValue( + targets, + ); + vi.spyOn(FixedPayloadStore.prototype, "readRequests").mockResolvedValue([ + requestState, + ]); + + await new ExperimentLifecycleCoordinator(db).repair( + "project-1", + "experiment-1", + ); + + expect(db.requests.updateMany).toHaveBeenCalledWith( + expect.objectContaining({ + _id: { $in: ["request-1"] }, + deletionProvenance: { + kind: "experiment", + experimentId: "experiment-1", + operationId: "operation-1", + }, + }), + { $unset: { deletedAt: "", deletionProvenance: "" } }, + ); + expect( + vi.mocked(db.requests.updateMany).mock.calls.some(([filter]) => + JSON.stringify(filter).includes("request-independent"), + ), + ).toBe(false); + }); + + it("completes an empty delete without child writes", async () => { + const deleting = root(lifecycle("deleting", [])); + const deleted = { + ...root(lifecycle("deleted", [])), + deletedAt: now, + }; + const db = collections({ + current: deleting, + updated: deleted, + }); + vi.spyOn(ExperimentLifecycleTargetStore.prototype, "read").mockResolvedValue( + [], + ); + + const result = await new ExperimentLifecycleCoordinator(db).repair( + "project-1", + "experiment-1", + ); + + expect(result.lifecycle?.phase).toBe("deleted"); + expect(db.requests.updateMany).not.toHaveBeenCalled(); + expect(db.reports.updateMany).not.toHaveBeenCalled(); + expect(db.experiments.findOneAndUpdate).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + $set: expect.objectContaining({ + "lifecycle.phase": "deleted", + "lifecycle.progress.completedRequests": 0, + "lifecycle.progress.completedReports": 0, + }), + }), + { returnDocument: "after" }, + ); + }); + + it("rolls to a fresh execution before restore when the exact request vector drifted", async () => { + const targets: ExperimentLifecycleTarget[] = [ + { kind: "request", id: "request-1" }, + ]; + const restoring = root(lifecycle("restoring", targets)); + restoring.deletedAt = now; + const nextRequest = { ...requestState, runId: "run-2" }; + const rolled = root(lifecycle("restoring", targets)); + rolled.stateVersion = 3; + rolled.execution = { + ...rolled.execution, + experimentExecutionId: "execution-2", + attemptNumber: 2, + version: 3, + requests: { + storage: "inline", + entryCount: 1, + payloadFingerprint: fingerprintExperimentPayload([nextRequest]), + payloadVersion: 1, + entries: [nextRequest], + }, + vectorFingerprint: fingerprintExperimentPayload([nextRequest]), + }; + const restored = { + ...rolled, + lifecycle: { + ...rolled.lifecycle!, + phase: "restored" as const, + restoreCompletedAt: now, + }, + }; + const db = collections({ + current: restoring, + requests: [{ _id: "request-1", run: { _id: "run-2" } }], + updated: restored, + }); + vi.spyOn(ExperimentLifecycleTargetStore.prototype, "read").mockResolvedValue( + targets, + ); + vi.spyOn(FixedPayloadStore.prototype, "readRequests").mockResolvedValue([ + requestState, + ]); + const rollover = vi + .spyOn(ExperimentStore.prototype, "rollover") + .mockResolvedValue(rolled); + + const result = await new ExperimentLifecycleCoordinator(db).repair( + "project-1", + "experiment-1", + ); + + expect(rollover).toHaveBeenCalledWith( + expect.objectContaining({ + projectId: "project-1", + experimentId: "experiment-1", + expectedExperimentExecutionId: "execution-1", + expectedExecutionVersion: restoring.execution.version, + nextExperimentExecutionId: expect.any(String), + requests: [nextRequest], + reason: "rollover", + }), + ); + expect( + vi.mocked(rollover).mock.calls[0]?.[0].nextExperimentExecutionId, + ).not.toBe("execution-1"); + expect(result.execution.experimentExecutionId).toBe("execution-2"); + expect(result.lifecycle?.phase).toBe("restored"); + }); +}); diff --git a/apps/api/src/experiment-lifecycle.ts b/apps/api/src/experiment-lifecycle.ts new file mode 100644 index 00000000..633f0cc5 --- /dev/null +++ b/apps/api/src/experiment-lifecycle.ts @@ -0,0 +1,661 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { randomUUID } from "node:crypto"; +import type { Collection, Document } from "mongodb"; +import { + ExperimentLifecycleTargetStore, + ExperimentStore, + FixedPayloadStore, + fingerprintExperimentPayload, + type ExperimentDocument, + type ExperimentExecutionHistoryDocument, + type ExperimentLifecycleBlocker, + type ExperimentLifecycleState, + type ExperimentLifecycleTarget, + type ExperimentRequestState, + type ReportDocument, + type RequestDocument, +} from "shared"; + +const TARGET_BATCH_SIZE = 500; + +export class ExperimentLifecycleError extends Error { + constructor( + message: string, + readonly status: 404 | 409, + readonly code: string, + readonly lifecycle?: ExperimentLifecycleState, + ) { + super(message); + } +} + +export interface ExperimentLifecycleCollections { + requests: Collection; + reports: Collection; + experiments: Collection; + executions: Collection; + chunks: Collection; +} + +function batches(values: readonly T[]): T[][] { + const result: T[][] = []; + for (let index = 0; index < values.length; index += TARGET_BATCH_SIZE) { + result.push(values.slice(index, index + TARGET_BATCH_SIZE)); + } + return result; +} + +function lifecycleCounts(targets: readonly ExperimentLifecycleTarget[]) { + return { + expectedRequests: targets.filter((target) => target.kind === "request").length, + completedRequests: 0, + expectedReports: targets.filter((target) => target.kind === "report").length, + completedReports: 0, + }; +} + +export class ExperimentLifecycleCoordinator { + private readonly store: ExperimentStore; + private readonly targetStore: ExperimentLifecycleTargetStore; + private readonly payloads: FixedPayloadStore; + + constructor(private readonly collections: ExperimentLifecycleCollections) { + this.store = new ExperimentStore({ + experiments: collections.experiments, + executions: collections.executions, + chunks: collections.chunks, + }); + this.targetStore = new ExperimentLifecycleTargetStore(collections.chunks); + this.payloads = new FixedPayloadStore(collections.chunks); + } + + async startDelete( + projectId: string, + experimentId: string, + now = new Date(), + ): Promise { + const current = await this.store.getIncludingDeleted(projectId, experimentId); + if (!current) { + throw new ExperimentLifecycleError( + `Experiment ${experimentId} was not found`, + 404, + "experiment-not-found", + ); + } + if ( + current.lifecycle?.phase === "deleting" || + current.lifecycle?.phase === "deleted" + ) { + return this.repair(projectId, experimentId); + } + if (current.lifecycle?.phase === "restoring") { + throw new ExperimentLifecycleError( + "Experiment restore is already active", + 409, + "experiment-lifecycle-conflict", + current.lifecycle, + ); + } + if (current.execution.rollover || current.execution.requestStateUpdate) { + throw new ExperimentLifecycleError( + "Experiment publication is active", + 409, + "experiment-mutation-active", + current.lifecycle, + ); + } + + const requestVector = await this.payloads.readRequests( + { + projectId, + experimentId, + ownerId: current.execution.experimentExecutionId, + generation: current.execution.attemptNumber, + }, + current.execution.requests, + ); + const requestIds = requestVector.map(({ requestId }) => requestId); + const activeRequests = requestIds.length === 0 + ? [] + : await this.collections.requests + .find({ + _id: { $in: requestIds }, + projectId, + experimentId, + deletedAt: { $exists: false }, + } as Document) + .project({ _id: 1 }) + .toArray(); + const activeReports = await this.collections.reports + .find({ + projectId, + experimentId, + deletedAt: { $exists: false }, + } as Document) + .project({ _id: 1 }) + .toArray(); + const targets: ExperimentLifecycleTarget[] = [ + ...activeRequests.map((request) => ({ + kind: "request" as const, + id: request._id, + })), + ...activeReports.map((report) => ({ + kind: "report" as const, + id: report._id, + })), + ]; + const operationId = randomUUID(); + const targetFingerprint = fingerprintExperimentPayload(targets); + const buildLifecycle = ( + payload: ExperimentLifecycleState["targets"], + ): ExperimentLifecycleState => ({ + operationId, + phase: "deleting", + stateVersion: 1, + targets: payload, + targetFingerprint, + progress: lifecycleCounts(targets), + blocking: [], + deleteRequestedAt: now, + }); + const payload = await this.targetStore.prepare({ + projectId, + experimentId, + operationId, + targets, + inlineDocument: (candidate) => ({ + ...current, + lifecycle: buildLifecycle(candidate), + }), + now, + }); + const lifecycle = buildLifecycle(payload); + const claimed = await this.collections.experiments.findOneAndUpdate( + { + _id: experimentId, + projectId, + stateVersion: current.stateVersion, + $or: [ + { lifecycle: { $exists: false } }, + { "lifecycle.phase": "restored" }, + ], + } as Document, + { + $set: { + lifecycle, + "execution.frozenAt": current.execution.frozenAt ?? now, + "execution.updatedAt": now, + updatedAt: now, + }, + $inc: { + stateVersion: 1, + "execution.version": current.execution.frozenAt ? 0 : 1, + }, + } as Document, + { returnDocument: "after" }, + ); + if (!claimed) { + await this.targetStore.discard(payload); + const raced = await this.store.getIncludingDeleted(projectId, experimentId); + if ( + raced?.lifecycle?.phase === "deleting" || + raced?.lifecycle?.phase === "deleted" + ) { + return this.repair(projectId, experimentId); + } + throw new ExperimentLifecycleError( + "Experiment lifecycle changed before deletion could start", + 409, + "experiment-lifecycle-contention", + raced?.lifecycle, + ); + } + return claimed; + } + + async startRestore( + projectId: string, + experimentId: string, + now = new Date(), + ): Promise { + const current = await this.store.getIncludingDeleted(projectId, experimentId); + if (!current) { + throw new ExperimentLifecycleError( + `Experiment ${experimentId} was not found`, + 404, + "experiment-not-found", + ); + } + if ( + current.lifecycle?.phase === "restoring" || + current.lifecycle?.phase === "restored" + ) { + return this.repair(projectId, experimentId); + } + if (current.lifecycle?.phase !== "deleted") { + throw new ExperimentLifecycleError( + "Only a deleted Experiment can be restored", + 409, + "experiment-lifecycle-conflict", + current.lifecycle, + ); + } + const updated = await this.collections.experiments.findOneAndUpdate( + { + _id: experimentId, + projectId, + stateVersion: current.stateVersion, + "lifecycle.operationId": current.lifecycle.operationId, + "lifecycle.phase": "deleted", + } as Document, + { + $set: { + "lifecycle.phase": "restoring", + "lifecycle.restoreRequestedAt": now, + "lifecycle.blocking": [], + "lifecycle.progress.completedRequests": 0, + "lifecycle.progress.completedReports": 0, + updatedAt: now, + }, + $inc: { stateVersion: 1, "lifecycle.stateVersion": 1 }, + } as Document, + { returnDocument: "after" }, + ); + if (!updated) { + const raced = await this.store.getIncludingDeleted(projectId, experimentId); + if (raced?.lifecycle?.phase === "restoring") { + return this.repair(projectId, experimentId); + } + throw new ExperimentLifecycleError( + "Experiment lifecycle changed before restore could start", + 409, + "experiment-lifecycle-contention", + raced?.lifecycle, + ); + } + return updated; + } + + async repair( + projectId: string, + experimentId: string, + ): Promise { + const current = await this.store.getIncludingDeleted(projectId, experimentId); + if (!current?.lifecycle) { + throw new ExperimentLifecycleError( + `Experiment ${experimentId} has no lifecycle operation`, + current ? 409 : 404, + current ? "experiment-lifecycle-inactive" : "experiment-not-found", + ); + } + if ( + current.lifecycle.phase === "deleted" || + current.lifecycle.phase === "restored" + ) { + return current; + } + const targets = await this.targetStore.read({ + projectId, + experimentId, + operationId: current.lifecycle.operationId, + payload: current.lifecycle.targets, + }); + const requests = targets + .filter((target) => target.kind === "request") + .map((target) => target.id); + const reports = targets + .filter((target) => target.kind === "report") + .map((target) => target.id); + if (current.lifecycle.phase === "deleting") { + await this.deleteTargets(current, requests, reports); + return this.finishDelete(current, requests, reports); + } + await this.restoreTargets(current, requests, reports); + return this.finishRestore(current, requests, reports); + } + + async repairActive(limit = 30): Promise { + const roots = await this.collections.experiments + .find({ + "lifecycle.phase": { $in: ["deleting", "restoring"] }, + } as Document) + .sort({ updatedAt: 1 }) + .limit(limit) + .project({ _id: 1, projectId: 1 }) + .toArray(); + for (const root of roots) { + try { + await this.repair(root.projectId, root._id); + } catch (error) { + console.error( + `[experiment-lifecycle] Failed to repair ${root.projectId}/${root._id}:`, + error, + ); + } + } + return roots.length; + } + + private async deleteTargets( + experiment: ExperimentDocument, + requestIds: readonly string[], + reportIds: readonly string[], + ): Promise { + const provenance = { + kind: "experiment" as const, + experimentId: experiment.experimentId, + operationId: experiment.lifecycle!.operationId, + }; + const now = new Date(); + for (const ids of batches(requestIds)) { + await this.collections.requests.updateMany( + { + _id: { $in: ids }, + projectId: experiment.projectId, + experimentId: experiment.experimentId, + deletedAt: { $exists: false }, + } as Document, + { $set: { deletedAt: now, deletionProvenance: provenance } } as Document, + ); + } + for (const ids of batches(reportIds)) { + await this.collections.reports.updateMany( + { + _id: { $in: ids }, + projectId: experiment.projectId, + experimentId: experiment.experimentId, + deletedAt: { $exists: false }, + } as Document, + { $set: { deletedAt: now, deletionProvenance: provenance } } as Document, + ); + } + } + + private async restoreTargets( + experiment: ExperimentDocument, + requestIds: readonly string[], + reportIds: readonly string[], + ): Promise { + const filter = { + kind: "experiment", + experimentId: experiment.experimentId, + operationId: experiment.lifecycle!.operationId, + }; + for (const ids of batches(requestIds)) { + await this.collections.requests.updateMany( + { + _id: { $in: ids }, + projectId: experiment.projectId, + deletionProvenance: filter, + } as Document, + { $unset: { deletedAt: "", deletionProvenance: "" } } as Document, + ); + } + for (const ids of batches(reportIds)) { + await this.collections.reports.updateMany( + { + _id: { $in: ids }, + projectId: experiment.projectId, + deletionProvenance: filter, + } as Document, + { $unset: { deletedAt: "", deletionProvenance: "" } } as Document, + ); + } + } + + private async targetState( + experiment: ExperimentDocument, + requestIds: readonly string[], + reportIds: readonly string[], + restoring: boolean, + ): Promise<{ + completedRequests: number; + completedReports: number; + blocking: ExperimentLifecycleBlocker[]; + }> { + const provenance = { + kind: "experiment", + experimentId: experiment.experimentId, + operationId: experiment.lifecycle!.operationId, + }; + const requestDocs = requestIds.length === 0 + ? [] + : await this.collections.requests + .find({ + _id: { $in: requestIds }, + projectId: experiment.projectId, + } as Document) + .project({ _id: 1, deletedAt: 1, deletionProvenance: 1 }) + .toArray(); + const reportDocs = reportIds.length === 0 + ? [] + : await this.collections.reports + .find({ + _id: { $in: reportIds }, + projectId: experiment.projectId, + } as Document) + .project({ _id: 1, deletedAt: 1, deletionProvenance: 1 }) + .toArray(); + const blockers: ExperimentLifecycleBlocker[] = []; + const inspect = ( + kind: "request" | "report", + ids: readonly string[], + docs: Document[], + ): number => { + const byId = new Map( + docs.flatMap((doc) => + typeof doc._id === "string" ? [[doc._id, doc] as const] : [], + ), + ); + let completed = 0; + for (const id of ids) { + const doc = byId.get(id); + if (!doc) { + blockers.push({ kind, id, reason: "target-missing" }); + continue; + } + const ownsDeletion = + doc.deletionProvenance !== undefined && + fingerprintExperimentPayload(doc.deletionProvenance) === + fingerprintExperimentPayload(provenance); + const done = restoring + ? !doc.deletedAt && doc.deletionProvenance === undefined + : Boolean(doc.deletedAt && ownsDeletion); + if (done) { + completed += 1; + } else { + blockers.push({ kind, id, reason: "target-conflict" }); + } + } + return completed; + }; + return { + completedRequests: inspect("request", requestIds, requestDocs), + completedReports: inspect("report", reportIds, reportDocs), + blocking: blockers.slice(0, 32), + }; + } + + private async finishDelete( + current: ExperimentDocument, + requestIds: readonly string[], + reportIds: readonly string[], + ): Promise { + const state = await this.targetState(current, requestIds, reportIds, false); + const complete = + state.completedRequests === requestIds.length && + state.completedReports === reportIds.length && + state.blocking.length === 0; + const now = new Date(); + const updated = await this.collections.experiments.findOneAndUpdate( + { + _id: current.experimentId, + projectId: current.projectId, + stateVersion: current.stateVersion, + "lifecycle.operationId": current.lifecycle!.operationId, + "lifecycle.phase": "deleting", + "lifecycle.stateVersion": current.lifecycle!.stateVersion, + } as Document, + { + $set: { + "lifecycle.progress.completedRequests": state.completedRequests, + "lifecycle.progress.completedReports": state.completedReports, + "lifecycle.blocking": state.blocking, + ...(complete + ? { + "lifecycle.phase": "deleted", + "lifecycle.deleteCompletedAt": now, + deletedAt: now, + } + : {}), + updatedAt: now, + }, + $inc: { stateVersion: 1, "lifecycle.stateVersion": 1 }, + } as Document, + { returnDocument: "after" }, + ); + if (!updated) return this.repair(current.projectId, current.experimentId); + return updated; + } + + private async finishRestore( + current: ExperimentDocument, + requestIds: readonly string[], + reportIds: readonly string[], + ): Promise { + const state = await this.targetState(current, requestIds, reportIds, true); + if ( + state.completedRequests !== requestIds.length || + state.completedReports !== reportIds.length || + state.blocking.length > 0 + ) { + const updated = await this.collections.experiments.findOneAndUpdate( + { + _id: current.experimentId, + projectId: current.projectId, + stateVersion: current.stateVersion, + "lifecycle.operationId": current.lifecycle!.operationId, + "lifecycle.phase": "restoring", + "lifecycle.stateVersion": current.lifecycle!.stateVersion, + } as Document, + { + $set: { + "lifecycle.progress.completedRequests": state.completedRequests, + "lifecycle.progress.completedReports": state.completedReports, + "lifecycle.blocking": state.blocking, + updatedAt: new Date(), + }, + $inc: { stateVersion: 1, "lifecycle.stateVersion": 1 }, + } as Document, + { returnDocument: "after" }, + ); + return updated ?? current; + } + + const requestVector = await this.payloads.readRequests( + { + projectId: current.projectId, + experimentId: current.experimentId, + ownerId: current.execution.experimentExecutionId, + generation: current.execution.attemptNumber, + }, + current.execution.requests, + ); + const requestDocs = requestIds.length === 0 + ? [] + : await this.collections.requests + .find({ + _id: { $in: requestIds }, + projectId: current.projectId, + experimentId: current.experimentId, + deletedAt: { $exists: false }, + } as Document) + .toArray(); + const currentById = new Map(requestDocs.map((request) => [request._id, request])); + const drifted = requestVector.some( + (request) => currentById.get(request.requestId)?.run?._id !== request.runId, + ); + if (drifted) { + const nextRequests: ExperimentRequestState[] = requestVector.map((request) => { + const runId = currentById.get(request.requestId)?.run?._id; + return runId && runId !== request.runId + ? { requestId: request.requestId, runId, dependencies: [], blockers: [] } + : request; + }); + const rolled = await this.store.rollover({ + projectId: current.projectId, + experimentId: current.experimentId, + expectedExperimentExecutionId: current.execution.experimentExecutionId, + expectedExecutionVersion: current.execution.version, + nextExperimentExecutionId: randomUUID(), + requests: nextRequests, + reason: "rollover", + }); + if (!rolled) { + const blocker: ExperimentLifecycleBlocker = { + kind: "experiment", + id: current.experimentId, + reason: "reconciliation-failed", + error: "Current request vector changed during restore reconciliation", + }; + const updated = await this.collections.experiments.findOneAndUpdate( + { + _id: current.experimentId, + projectId: current.projectId, + stateVersion: current.stateVersion, + "lifecycle.operationId": current.lifecycle!.operationId, + "lifecycle.phase": "restoring", + "lifecycle.stateVersion": current.lifecycle!.stateVersion, + } as Document, + { + $set: { + "lifecycle.blocking": [blocker], + updatedAt: new Date(), + }, + $inc: { stateVersion: 1, "lifecycle.stateVersion": 1 }, + } as Document, + { returnDocument: "after" }, + ); + return updated ?? current; + } + current = rolled; + } + + const now = new Date(); + const updated = await this.collections.experiments.findOneAndUpdate( + { + _id: current.experimentId, + projectId: current.projectId, + stateVersion: current.stateVersion, + "lifecycle.operationId": current.lifecycle!.operationId, + "lifecycle.phase": "restoring", + "lifecycle.stateVersion": current.lifecycle!.stateVersion, + } as Document, + { + $set: { + "lifecycle.phase": "restored", + "lifecycle.restoreCompletedAt": now, + "lifecycle.progress.completedRequests": requestIds.length, + "lifecycle.progress.completedReports": reportIds.length, + "lifecycle.blocking": [], + "execution.updatedAt": now, + updatedAt: now, + }, + $unset: { + deletedAt: "", + "execution.frozenAt": "", + }, + $inc: { + stateVersion: 1, + "execution.version": 1, + "lifecycle.stateVersion": 1, + }, + } as Document, + { returnDocument: "after" }, + ); + if (!updated) return this.repair(current.projectId, current.experimentId); + return updated; + } +} diff --git a/apps/api/src/routes/experiments.test.ts b/apps/api/src/routes/experiments.test.ts index 4ec89c07..a51f6e2f 100644 --- a/apps/api/src/routes/experiments.test.ts +++ b/apps/api/src/routes/experiments.test.ts @@ -12,6 +12,10 @@ import { describe, expect, it, vi } from "vitest"; import { z } from "zod"; import type { RouteContext } from "../route-context.js"; import { ExperimentStore, type ExperimentDocument } from "shared"; +import { + ExperimentLifecycleCoordinator, + ExperimentLifecycleError, +} from "../experiment-lifecycle.js"; import { registerExperimentsRoutes } from "./experiments.js"; extendZodWithOpenApi(z); @@ -72,6 +76,108 @@ function generateDocument(registry: OpenAPIRegistry) { } describe("Experiment API contract", () => { + it("scopes lifecycle operations and returns stable asynchronous progress", async () => { + const lifecycleExperiment = { + experimentId: "experiment-1", + lifecycle: { + operationId: "operation-1", + phase: "deleting", + stateVersion: 2, + progress: { + expectedRequests: 2, + completedRequests: 1, + expectedReports: 1, + completedReports: 0, + }, + blocking: [], + deleteRequestedAt: new Date("2026-10-16T12:00:00.000Z"), + }, + } as ExperimentDocument; + const startDelete = vi + .spyOn(ExperimentLifecycleCoordinator.prototype, "startDelete") + .mockResolvedValue(lifecycleExperiment); + const repair = vi + .spyOn(ExperimentLifecycleCoordinator.prototype, "repair") + .mockResolvedValue(lifecycleExperiment); + const { app } = harness(); + + const missingScope = await request(app) + .delete("/api/v1/experiments/experiment-1"); + const started = await request(app) + .delete("/api/v1/experiments/experiment-1?projectId=project-1"); + const status = await request(app) + .get("/api/v1/experiments/experiment-1/lifecycle?projectId=project-1"); + + expect(missingScope.status).toBe(400); + expect(started.status).toBe(202); + expect(started.body).toMatchObject({ + experimentId: "experiment-1", + operationId: "operation-1", + phase: "deleting", + progress: { + expectedRequests: 2, + completedRequests: 1, + }, + }); + expect(status.status).toBe(200); + expect(startDelete).toHaveBeenCalledWith("project-1", "experiment-1"); + expect(repair).toHaveBeenCalledWith("project-1", "experiment-1"); + + startDelete.mockRestore(); + repair.mockRestore(); + }); + + it("starts restore and exposes machine-readable lifecycle conflicts", async () => { + const restoringExperiment = { + experimentId: "experiment-1", + lifecycle: { + operationId: "operation-restore", + phase: "restoring", + stateVersion: 4, + progress: { + expectedRequests: 2, + completedRequests: 0, + expectedReports: 1, + completedReports: 0, + }, + blocking: [], + deleteRequestedAt: new Date("2026-10-16T12:00:00.000Z"), + restoreRequestedAt: new Date("2026-10-16T13:00:00.000Z"), + }, + } as ExperimentDocument; + const startRestore = vi + .spyOn(ExperimentLifecycleCoordinator.prototype, "startRestore") + .mockResolvedValueOnce(restoringExperiment) + .mockRejectedValueOnce( + new ExperimentLifecycleError( + "Experiment restore conflicts with an active delete", + 409, + "lifecycle-conflict", + restoringExperiment.lifecycle, + ), + ); + const { app } = harness(); + + const started = await request(app) + .post("/api/v1/experiments/experiment-1/restore?projectId=project-1"); + const conflict = await request(app) + .post("/api/v1/experiments/experiment-1/restore?projectId=project-1"); + + expect(started.status).toBe(202); + expect(started.body).toMatchObject({ + operationId: "operation-restore", + phase: "restoring", + }); + expect(conflict.status).toBe(409); + expect(conflict.body).toMatchObject({ + code: "lifecycle-conflict", + operationId: "operation-restore", + phase: "restoring", + }); + + startRestore.mockRestore(); + }); + it("requires project scope on root routes", async () => { const { app } = harness(); @@ -121,6 +227,10 @@ describe("Experiment API contract", () => { _id: "experiment-1", projectId: "wrong-project", finalizedAt: { $exists: true }, + deletedAt: { $exists: false }, + "lifecycle.phase": { + $nin: ["deleting", "deleted", "restoring"], + }, }); }); @@ -275,8 +385,10 @@ describe("Experiment API contract", () => { "/api/v1/experiments/{id}/executions/{experimentExecutionId}/reports", "/api/v1/experiments/{id}/executions/{experimentExecutionId}/reports/{reportId}/structured", "/api/v1/experiments/{id}/executions/{experimentExecutionId}/reports/{templateId}/retry", + "/api/v1/experiments/{id}/lifecycle", "/api/v1/experiments/{id}/report-templates", "/api/v1/experiments/{id}/requests", + "/api/v1/experiments/{id}/restore", "/api/v1/experiments/{id}/retry", ]); expect(document.paths).not.toHaveProperty("/api/v1/submissions"); @@ -327,6 +439,10 @@ describe("Experiment API contract", () => { const getExecution = vi .spyOn(ExperimentStore.prototype, "getExecution") .mockResolvedValue(execution); + vi.spyOn(ExperimentStore.prototype, "get").mockResolvedValue({ + _id: "experiment-1", + projectId: "project-1", + } as ExperimentDocument); const toArray = vi.fn().mockResolvedValue([{ _id: "report-1", projectId: "project-1", @@ -354,11 +470,13 @@ describe("Experiment API contract", () => { projectId: "project-1", experimentId: "experiment-1", experimentExecutionId: "execution-2", + includeDeleted: false, }); expect(find).toHaveBeenCalledWith({ projectId: "project-1", experimentId: "experiment-1", experimentExecutionId: "execution-2", + deletedAt: { $exists: false }, }); expect(response.body).toEqual([ expect.objectContaining({ diff --git a/apps/api/src/routes/experiments.ts b/apps/api/src/routes/experiments.ts index 44fe7c96..be3d5a43 100644 --- a/apps/api/src/routes/experiments.ts +++ b/apps/api/src/routes/experiments.ts @@ -31,11 +31,16 @@ import { ExperimentMembershipError, type ReplaceMembershipResult, } from "../experiment-membership.js"; +import { + ExperimentLifecycleCoordinator, + ExperimentLifecycleError, +} from "../experiment-lifecycle.js"; const CursorSchema = z.string().min(1).optional(); const PageQuerySchema = ProjectIdQuerySchema.extend({ limit: z.coerce.number().int().min(1).max(100).default(50), after: CursorSchema, + includeDeleted: z.enum(["true", "false"]).optional(), }); const ExperimentMetadataInputSchema = z.object({ @@ -191,9 +196,62 @@ const ExperimentResponseSchema = z }), createdAt: z.coerce.date(), updatedAt: z.coerce.date(), + deletedAt: z.coerce.date().optional(), + lifecycle: z + .object({ + operationId: z.string(), + phase: z.enum(["deleting", "deleted", "restoring", "restored"]), + stateVersion: z.number().int().positive(), + progress: z.object({ + expectedRequests: z.number().int().nonnegative(), + completedRequests: z.number().int().nonnegative(), + expectedReports: z.number().int().nonnegative(), + completedReports: z.number().int().nonnegative(), + }), + blocking: z.array(z.object({ + kind: z.enum(["request", "report", "experiment"]), + id: z.string(), + reason: z.enum([ + "target-missing", + "target-conflict", + "reconciliation-failed", + ]), + error: z.string().optional(), + })), + deleteRequestedAt: z.coerce.date(), + deleteCompletedAt: z.coerce.date().optional(), + restoreRequestedAt: z.coerce.date().optional(), + restoreCompletedAt: z.coerce.date().optional(), + }) + .optional(), }) .openapi("ExperimentResponse"); +const ExperimentLifecycleResponseSchema = z + .object({ + experimentId: z.string(), + operationId: z.string(), + phase: z.enum(["deleting", "deleted", "restoring", "restored"]), + stateVersion: z.number().int().positive(), + progress: z.object({ + expectedRequests: z.number().int().nonnegative(), + completedRequests: z.number().int().nonnegative(), + expectedReports: z.number().int().nonnegative(), + completedReports: z.number().int().nonnegative(), + }), + blocking: z.array(z.object({ + kind: z.enum(["request", "report", "experiment"]), + id: z.string(), + reason: z.string(), + error: z.string().optional(), + })), + deleteRequestedAt: z.coerce.date(), + deleteCompletedAt: z.coerce.date().optional(), + restoreRequestedAt: z.coerce.date().optional(), + restoreCompletedAt: z.coerce.date().optional(), + }) + .openapi("ExperimentLifecycleResponse"); + const ExperimentListResponseSchema = z .object({ data: z.array(ExperimentResponseSchema), @@ -301,6 +359,57 @@ function membership(ctx: RouteContext): ExperimentMembershipCoordinator { }); } +function lifecycle(ctx: RouteContext): ExperimentLifecycleCoordinator { + return new ExperimentLifecycleCoordinator({ + requests: ctx.requestCollection, + reports: ctx.reportCollection, + experiments: ctx.experimentCollection, + executions: ctx.experimentExecutionsCollection, + chunks: ctx.experimentChunksCollection, + }); +} + +function lifecycleResponse(experiment: ExperimentDocument) { + if (!experiment.lifecycle) { + throw new Error(`Experiment ${experiment.experimentId} has no lifecycle operation`); + } + return { + experimentId: experiment.experimentId, + operationId: experiment.lifecycle.operationId, + phase: experiment.lifecycle.phase, + stateVersion: experiment.lifecycle.stateVersion, + progress: experiment.lifecycle.progress, + blocking: experiment.lifecycle.blocking, + deleteRequestedAt: experiment.lifecycle.deleteRequestedAt, + ...(experiment.lifecycle.deleteCompletedAt + ? { deleteCompletedAt: experiment.lifecycle.deleteCompletedAt } + : {}), + ...(experiment.lifecycle.restoreRequestedAt + ? { restoreRequestedAt: experiment.lifecycle.restoreRequestedAt } + : {}), + ...(experiment.lifecycle.restoreCompletedAt + ? { restoreCompletedAt: experiment.lifecycle.restoreCompletedAt } + : {}), + }; +} + +function sendLifecycleError(res: Response, error: unknown): boolean { + if (!(error instanceof ExperimentLifecycleError)) return false; + res.status(error.status).json({ + error: error.message, + code: error.code, + ...(error.lifecycle + ? { + operationId: error.lifecycle.operationId, + phase: error.lifecycle.phase, + progress: error.lifecycle.progress, + blockers: error.lifecycle.blocking, + } + : {}), + }); + return true; +} + function membershipResponse(result: ReplaceMembershipResult) { return { experiment: experimentResponse(result.experiment), @@ -579,6 +688,28 @@ function experimentResponse(experiment: ExperimentDocument) { currentExecution: executionResponse(experiment.execution), createdAt: experiment.createdAt, updatedAt: experiment.updatedAt, + ...(experiment.deletedAt ? { deletedAt: experiment.deletedAt } : {}), + ...(experiment.lifecycle + ? { + lifecycle: { + operationId: experiment.lifecycle.operationId, + phase: experiment.lifecycle.phase, + stateVersion: experiment.lifecycle.stateVersion, + progress: experiment.lifecycle.progress, + blocking: experiment.lifecycle.blocking, + deleteRequestedAt: experiment.lifecycle.deleteRequestedAt, + ...(experiment.lifecycle.deleteCompletedAt + ? { deleteCompletedAt: experiment.lifecycle.deleteCompletedAt } + : {}), + ...(experiment.lifecycle.restoreRequestedAt + ? { restoreRequestedAt: experiment.lifecycle.restoreRequestedAt } + : {}), + ...(experiment.lifecycle.restoreCompletedAt + ? { restoreCompletedAt: experiment.lifecycle.restoreCompletedAt } + : {}), + }, + } + : {}), }; } @@ -895,6 +1026,7 @@ export function registerExperimentsRoutes(ctx: RouteContext): void { projectId: getQueryProjectId(req), limit: req.query.limit, afterExperimentId: req.query.after, + includeDeleted: req.query.includeDeleted === "true", }); res.json({ data: page.experiments.map(experimentResponse), @@ -909,14 +1041,22 @@ export function registerExperimentsRoutes(ctx: RouteContext): void { tags: ["Experiments"], summary: "Get an experiment", params: z.object({ id: z.string().min(1) }), - query: ProjectIdQuerySchema, + query: ProjectIdQuerySchema.extend({ + includeDeleted: z.enum(["true", "false"]).optional(), + }), response: ExperimentResponseSchema, errorResponses: { 404: { description: "Experiment not found" } }, handler: async (req, res) => { - const experiment = await store(ctx).get( - getQueryProjectId(req), - req.params.id, - ); + const experimentStore = store(ctx); + const experiment = req.query.includeDeleted === "true" + ? await experimentStore.getIncludingDeleted( + getQueryProjectId(req), + req.params.id, + ) + : await experimentStore.get( + getQueryProjectId(req), + req.params.id, + ); if (!experiment) { res.status(404).json({ error: "Experiment not found" }); return; @@ -925,6 +1065,86 @@ export function registerExperimentsRoutes(ctx: RouteContext): void { }, }); + apiRoute(ctx.app, ctx.registry, { + method: "delete", + path: "/api/v1/experiments/:id", + tags: ["Experiments"], + summary: "Start or resume cascading Experiment deletion", + description: + "Returns 202 with a stable lifecycle operation ID. Poll the lifecycle " + + "endpoint until phase is deleted or blockers require intervention.", + params: z.object({ id: z.string().min(1) }), + query: ProjectIdQuerySchema, + response: ExperimentLifecycleResponseSchema, + successStatus: 202, + errorResponses: { + 404: { description: "Experiment not found" }, + 409: { description: "An incompatible Experiment mutation is active" }, + }, + handler: async (req, res) => { + try { + const experiment = await lifecycle(ctx).startDelete( + getQueryProjectId(req), + req.params.id, + ); + res.status(202).json(lifecycleResponse(experiment)); + } catch (error) { + if (!sendLifecycleError(res, error)) throw error; + } + }, + }); + + apiRoute(ctx.app, ctx.registry, { + method: "post", + path: "/api/v1/experiments/:id/restore", + tags: ["Experiments"], + summary: "Start or resume cascading Experiment restore", + params: z.object({ id: z.string().min(1) }), + query: ProjectIdQuerySchema, + response: ExperimentLifecycleResponseSchema, + successStatus: 202, + errorResponses: { + 404: { description: "Experiment not found" }, + 409: { description: "Experiment is not deleted or another intent is active" }, + }, + handler: async (req, res) => { + try { + const experiment = await lifecycle(ctx).startRestore( + getQueryProjectId(req), + req.params.id, + ); + res.status(202).json(lifecycleResponse(experiment)); + } catch (error) { + if (!sendLifecycleError(res, error)) throw error; + } + }, + }); + + apiRoute(ctx.app, ctx.registry, { + method: "get", + path: "/api/v1/experiments/:id/lifecycle", + tags: ["Experiments"], + summary: "Get and repair Experiment lifecycle progress", + params: z.object({ id: z.string().min(1) }), + query: ProjectIdQuerySchema, + response: ExperimentLifecycleResponseSchema, + errorResponses: { + 404: { description: "Experiment not found" }, + 409: { description: "Experiment has no lifecycle operation" }, + }, + handler: async (req, res) => { + try { + const experiment = await lifecycle(ctx).repair( + getQueryProjectId(req), + req.params.id, + ); + res.json(lifecycleResponse(experiment)); + } catch (error) { + if (!sendLifecycleError(res, error)) throw error; + } + }, + }); + apiRoute(ctx.app, ctx.registry, { method: "put", path: "/api/v1/experiments/:id/report-templates", @@ -1038,6 +1258,7 @@ export function registerExperimentsRoutes(ctx: RouteContext): void { query: ProjectIdQuerySchema.extend({ limit: z.coerce.number().int().min(1).max(100).default(50), after: CursorSchema, + includeDeleted: z.enum(["true", "false"]).optional(), }), response: ExecutionListResponseSchema, errorResponses: { 404: { description: "Experiment not found" } }, @@ -1047,6 +1268,14 @@ export function registerExperimentsRoutes(ctx: RouteContext): void { _id: req.params.id, projectId, finalizedAt: { $exists: true }, + ...(req.query.includeDeleted === "true" + ? {} + : { + deletedAt: { $exists: false }, + "lifecycle.phase": { + $nin: ["deleting", "deleted", "restoring"], + }, + }), }); if (!experiment) { res.status(404).json({ error: "Experiment not found" }); @@ -1101,7 +1330,9 @@ export function registerExperimentsRoutes(ctx: RouteContext): void { id: z.string().min(1), experimentExecutionId: z.string().min(1), }), - query: ProjectIdQuerySchema, + query: ProjectIdQuerySchema.extend({ + includeDeleted: z.enum(["true", "false"]).optional(), + }), response: ExecutionResponseSchema, errorResponses: { 404: { description: "Experiment execution not found" } }, handler: async (req, res) => { @@ -1110,6 +1341,14 @@ export function registerExperimentsRoutes(ctx: RouteContext): void { _id: req.params.id, projectId, finalizedAt: { $exists: true }, + ...(req.query.includeDeleted === "true" + ? {} + : { + deletedAt: { $exists: false }, + "lifecycle.phase": { + $nin: ["deleting", "deleted", "restoring"], + }, + }), }); if (!experiment) { res.status(404).json({ error: "Experiment execution not found" }); @@ -1137,16 +1376,24 @@ export function registerExperimentsRoutes(ctx: RouteContext): void { id: z.string().min(1), experimentExecutionId: z.string().min(1), }), - query: ProjectIdQuerySchema, + query: ProjectIdQuerySchema.extend({ + includeDeleted: z.enum(["true", "false"]).optional(), + }), response: z.array(ReportResponseSchema), errorResponses: { 404: { description: "Experiment execution not found" } }, handler: async (req, res) => { const projectId = getQueryProjectId(req); - const execution = await store(ctx).getExecution({ + const parent = req.query.includeDeleted === "true" + ? await store(ctx).getIncludingDeleted(projectId, req.params.id) + : await store(ctx).get(projectId, req.params.id); + const execution = parent + ? await store(ctx).getExecution({ projectId, experimentId: req.params.id, experimentExecutionId: req.params.experimentExecutionId, - }); + includeDeleted: req.query.includeDeleted === "true", + }) + : null; if (!execution) { res.status(404).json({ error: "Experiment execution not found" }); return; @@ -1156,6 +1403,9 @@ export function registerExperimentsRoutes(ctx: RouteContext): void { projectId, experimentId: req.params.id, experimentExecutionId: req.params.experimentExecutionId, + ...(req.query.includeDeleted === "true" + ? {} + : { deletedAt: { $exists: false } }), }) .sort({ createdAt: -1 }) .toArray(); diff --git a/apps/api/src/routes/reports-structured.test.ts b/apps/api/src/routes/reports-structured.test.ts index 159613f8..1793621f 100644 --- a/apps/api/src/routes/reports-structured.test.ts +++ b/apps/api/src/routes/reports-structured.test.ts @@ -91,6 +91,7 @@ describe("structured report download", () => { projectId: "project-1", experimentId: "experiment-1", experimentExecutionId: "execution-1", + deletedAt: { $exists: false }, }); expect(response.body[0]).toMatchObject({ id: "report-1", diff --git a/apps/api/src/routes/reports.ts b/apps/api/src/routes/reports.ts index 5c2d264c..1e75373c 100644 --- a/apps/api/src/routes/reports.ts +++ b/apps/api/src/routes/reports.ts @@ -159,6 +159,7 @@ apiRoute(ctx.app, ctx.registry, { experimentId: z.string().optional(), experimentExecutionId: z.string().optional(), projectId: z.string().optional(), + includeDeleted: z.enum(["true", "false"]).optional(), }).superRefine((value, ctx) => { if (value.experimentExecutionId && !value.experimentId) { ctx.addIssue({ @@ -176,6 +177,9 @@ apiRoute(ctx.app, ctx.registry, { const experimentExecutionIdFilter = req.query.experimentExecutionId as string; const filter: Record = {}; + if (req.query.includeDeleted !== "true") { + filter.deletedAt = { $exists: false }; + } if (requestIdFilter) { // Parent-scoped: the run id already pins the project. filter.requestId = requestIdFilter; diff --git a/apps/cli/src/__snapshots__/cli-commands.test.ts.snap b/apps/cli/src/__snapshots__/cli-commands.test.ts.snap index b5adba7f..ba858e01 100644 --- a/apps/cli/src/__snapshots__/cli-commands.test.ts.snap +++ b/apps/cli/src/__snapshots__/cli-commands.test.ts.snap @@ -245,6 +245,14 @@ exports[`CLI command registration > full command tree matches snapshot 1`] = ` "--worker", ], }, + "delete": { + "options": [ + "--no-wait", + "--output", + "--project", + "--url", + ], + }, "executions": { "options": [ "--cursor", @@ -256,6 +264,7 @@ exports[`CLI command registration > full command tree matches snapshot 1`] = ` }, "get": { "options": [ + "--include-deleted", "--output", "--project", "--url", @@ -264,6 +273,7 @@ exports[`CLI command registration > full command tree matches snapshot 1`] = ` "list": { "options": [ "--cursor", + "--include-deleted", "--limit", "--output", "--project", @@ -311,6 +321,14 @@ exports[`CLI command registration > full command tree matches snapshot 1`] = ` "--url", ], }, + "restore": { + "options": [ + "--no-wait", + "--output", + "--project", + "--url", + ], + }, "retry": { "options": [ "--execution", @@ -318,6 +336,13 @@ exports[`CLI command registration > full command tree matches snapshot 1`] = ` "--url", ], }, + "status": { + "options": [ + "--output", + "--project", + "--url", + ], + }, }, }, "extension": { diff --git a/apps/cli/src/commands/experiment.test.ts b/apps/cli/src/commands/experiment.test.ts index c33a930c..2b3622ea 100644 --- a/apps/cli/src/commands/experiment.test.ts +++ b/apps/cli/src/commands/experiment.test.ts @@ -35,6 +35,19 @@ const experiment = { createdAt: "2026-01-01T00:00:00.000Z", }; +const lifecycle = { + operationId: "operation-1", + phase: "deleting", + stateVersion: 2, + progress: { + expectedRequests: 1, + completedRequests: 0, + expectedReports: 1, + completedReports: 0, + }, + blocking: [], +}; + function makeProgram(): Command { const program = new Command(); registerExperimentCommands(program); @@ -308,6 +321,126 @@ describe("experiment commands", () => { ); }); + it("starts deletion without polling when --no-wait is set", async () => { + const { requests } = mockFetchWith(lifecycle); + const output: string[] = []; + vi.spyOn(console, "log").mockImplementation((...parts: unknown[]) => { + output.push(parts.map(String).join(" ")); + }); + + await makeProgram().parseAsync( + [ + "experiment", + "delete", + "experiment-1", + "--no-wait", + "--output", + "json", + "--project", + "project-1", + "--url", + "http://localhost:3100", + ], + { from: "user" }, + ); + + expect(requests).toEqual([ + expect.objectContaining({ + method: "DELETE", + url: "http://localhost:3100/api/v1/experiments/experiment-1?projectId=project-1", + }), + ]); + expect(output.join("\n")).toContain('"phase": "deleting"'); + }); + + it("polls restore until the lifecycle reaches restored", async () => { + vi.useFakeTimers(); + const requests: CapturedRequest[] = []; + const responses = [ + { ...lifecycle, phase: "restoring" }, + { + ...lifecycle, + phase: "restored", + stateVersion: 3, + progress: { + ...lifecycle.progress, + completedRequests: 1, + completedReports: 1, + }, + }, + ]; + vi.stubGlobal("fetch", vi.fn(async (request: Request) => { + requests.push({ + url: request.url, + method: request.method, + body: await request.text(), + }); + return new Response(JSON.stringify(responses.shift()), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + })); + vi.spyOn(console, "log").mockImplementation(() => {}); + + const command = makeProgram().parseAsync( + [ + "experiment", + "restore", + "experiment-1", + "--project", + "project-1", + "--url", + "http://localhost:3100", + ], + { from: "user" }, + ); + await vi.advanceTimersByTimeAsync(1_000); + await command; + vi.useRealTimers(); + + expect(requests.map(({ method, url }) => ({ method, url }))).toEqual([ + { + method: "POST", + url: "http://localhost:3100/api/v1/experiments/experiment-1/restore?projectId=project-1", + }, + { + method: "GET", + url: "http://localhost:3100/api/v1/experiments/experiment-1/lifecycle?projectId=project-1", + }, + ]); + }); + + it("surfaces lifecycle blockers without polling", async () => { + const { requests } = mockFetchWith({ + ...lifecycle, + blocking: [{ + kind: "request", + id: "request-1", + reason: "missing", + }], + }); + const error = vi.spyOn(console, "error").mockImplementation(() => {}); + + await makeProgram().parseAsync( + [ + "experiment", + "delete", + "experiment-1", + "--project", + "project-1", + "--url", + "http://localhost:3100", + ], + { from: "user" }, + ); + + expect(requests).toHaveLength(1); + expect(error.mock.calls.flat().join("\n")).toContain( + "request:request-1 missing", + ); + expect(process.exitCode).toBe(1); + }); + it("retrieves structured JSON for one exact experiment execution", async () => { const requests: CapturedRequest[] = []; vi.stubGlobal("fetch", vi.fn(async (request: Request) => { diff --git a/apps/cli/src/commands/experiment.ts b/apps/cli/src/commands/experiment.ts index 63bfe207..29e60ffe 100644 --- a/apps/cli/src/commands/experiment.ts +++ b/apps/cli/src/commands/experiment.ts @@ -27,6 +27,24 @@ interface ExperimentMetadata { description?: string; } +interface ExperimentLifecycle { + operationId: string; + phase: "deleting" | "deleted" | "restoring" | "restored"; + stateVersion: number; + progress: { + expectedRequests: number; + completedRequests: number; + expectedReports: number; + completedReports: number; + }; + blocking: Array<{ + kind: "request" | "report" | "experiment"; + id: string; + reason: string; + error?: string; + }>; +} + interface ExperimentExecution { experimentExecutionId: string; attemptNumber: number; @@ -93,6 +111,8 @@ interface ExperimentView { createdAt: string; updatedAt?: string; finalizedAt?: string; + deletedAt?: string; + lifecycle?: ExperimentLifecycle; } interface ExperimentPage { @@ -331,6 +351,93 @@ function parseExperiment(value: unknown): ExperimentView { createdAt: stringField(experiment, "createdAt", "experiment")!, updatedAt: stringField(experiment, "updatedAt", "experiment", true), finalizedAt: stringField(experiment, "finalizedAt", "experiment", true), + deletedAt: stringField(experiment, "deletedAt", "experiment", true), + lifecycle: + experiment.lifecycle === undefined + ? undefined + : parseLifecycle(experiment.lifecycle), + }; +} + +function parseLifecycle(value: unknown): ExperimentLifecycle { + const lifecycle = asRecord(value, "experiment lifecycle"); + const progress = asRecord( + lifecycle.progress, + "experiment lifecycle progress", + ); + const phase = stringField(lifecycle, "phase", "experiment lifecycle")!; + if ( + phase !== "deleting" && + phase !== "deleted" && + phase !== "restoring" && + phase !== "restored" + ) { + throw new Error("Invalid experiment lifecycle.phase response from Scope"); + } + const blocking = lifecycle.blocking; + if (!Array.isArray(blocking)) { + throw new Error("Invalid experiment lifecycle.blocking response from Scope"); + } + return { + operationId: stringField( + lifecycle, + "operationId", + "experiment lifecycle", + )!, + phase, + stateVersion: numberField( + lifecycle, + "stateVersion", + "experiment lifecycle", + ), + progress: { + expectedRequests: numberField( + progress, + "expectedRequests", + "experiment lifecycle progress", + ), + completedRequests: numberField( + progress, + "completedRequests", + "experiment lifecycle progress", + ), + expectedReports: numberField( + progress, + "expectedReports", + "experiment lifecycle progress", + ), + completedReports: numberField( + progress, + "completedReports", + "experiment lifecycle progress", + ), + }, + blocking: blocking.map((item) => { + const blocker = asRecord(item, "experiment lifecycle blocker"); + const kind = stringField( + blocker, + "kind", + "experiment lifecycle blocker", + ); + if (kind !== "request" && kind !== "report" && kind !== "experiment") { + throw new Error("Invalid experiment lifecycle blocker kind"); + } + return { + kind, + id: stringField(blocker, "id", "experiment lifecycle blocker")!, + reason: stringField( + blocker, + "reason", + "experiment lifecycle blocker", + )!, + error: stringField( + blocker, + "error", + "experiment lifecycle blocker", + true, + ), + }; + }), }; } @@ -879,6 +986,152 @@ export function registerExperimentCommands(program: Command): void { } }); + const lifecycleAction = ( + command: Command, + method: "DELETE" | "POST", + pathSuffix: "" | "/restore", + terminalPhase: "deleted" | "restored", + ) => { + withProjectOption( + withOutputOption( + command + .argument("", "Experiment ID") + .option("--no-wait", "Return after the lifecycle operation starts") + .option("-u, --url ", "API base URL", getDefaultApiUrl()), + ), + ).action(async (id: string, options) => { + try { + const projectId = requireProjectId(options.project); + const response = await apiFetch( + options.url, + `/experiments/${encodeURIComponent(id)}${pathSuffix}`, + { method, projectId }, + ); + if (!response.ok) await throwApiError(response); + let state = parseLifecycle(await json(response)); + while (options.wait !== false && state.phase !== terminalPhase) { + if (state.blocking.length > 0) { + throw new Error( + `Experiment lifecycle blocked: ${state.blocking + .map((blocker) => + `${blocker.kind}:${blocker.id} ${blocker.reason}${ + blocker.error ? ` (${blocker.error})` : "" + }` + ) + .join(", ")}`, + ); + } + await new Promise((resolve) => setTimeout(resolve, 1_000)); + const status = await apiFetch( + options.url, + `/experiments/${encodeURIComponent(id)}/lifecycle`, + { projectId }, + ); + if (!status.ok) await throwApiError(status); + state = parseLifecycle(await json(status)); + } + const format = (options.output ?? "table") as OutputFormat; + if (isMachineReadable(format)) { + console.log(formatData([state], [ + { key: "operationId", label: "Operation" }, + { key: "phase", label: "Phase" }, + { key: "stateVersion", label: "Version" }, + ], format)); + return; + } + console.log(`${label("Operation:")} ${value(state.operationId)}`); + console.log(`${label("Phase:")} ${value(state.phase)}`); + console.log( + `${label("Requests:")} ${value( + `${state.progress.completedRequests}/${state.progress.expectedRequests}`, + )}`, + ); + console.log( + `${label("Reports:")} ${value( + `${state.progress.completedReports}/${state.progress.expectedReports}`, + )}`, + ); + } catch (error) { + console.error( + errorText("Error:"), + error instanceof Error ? error.message : error, + ); + process.exitCode = 1; + } + }); + }; + + lifecycleAction( + experiment + .command("delete") + .description("Cascade soft-delete an Experiment and poll its progress"), + "DELETE", + "", + "deleted", + ); + lifecycleAction( + experiment + .command("restore") + .description("Restore an Experiment cascade and poll its progress"), + "POST", + "/restore", + "restored", + ); + + withProjectOption( + withOutputOption( + experiment + .command("status") + .description("Show Experiment lifecycle progress and blockers") + .argument("", "Experiment ID") + .option("-u, --url ", "API base URL", getDefaultApiUrl()), + ), + ).action(async (id: string, options) => { + try { + const projectId = requireProjectId(options.project); + const response = await apiFetch( + options.url, + `/experiments/${encodeURIComponent(id)}/lifecycle`, + { projectId }, + ); + if (!response.ok) await throwApiError(response); + const state = parseLifecycle(await json(response)); + console.log( + formatData( + [state as unknown as JsonRecord], + [ + { key: "operationId", label: "Operation" }, + { key: "phase", label: "Phase" }, + { key: "stateVersion", label: "Version" }, + { + key: "progress", + label: "Requests", + formatter: (row: JsonRecord) => { + const progress = row.progress as ExperimentLifecycle["progress"]; + return `${progress.completedRequests}/${progress.expectedRequests}`; + }, + }, + { + key: "progress", + label: "Reports", + formatter: (row: JsonRecord) => { + const progress = row.progress as ExperimentLifecycle["progress"]; + return `${progress.completedReports}/${progress.expectedReports}`; + }, + }, + ], + (options.output ?? "table") as OutputFormat, + ), + ); + } catch (error) { + console.error( + errorText("Error:"), + error instanceof Error ? error.message : error, + ); + process.exitCode = 1; + } + }); + withProjectOption( withOutputOption( experiment @@ -886,6 +1139,7 @@ export function registerExperimentCommands(program: Command): void { .description("List experiments") .option("--cursor ", "Continue from a list cursor") .option("--limit ", "Maximum items", Number.parseInt) + .option("--include-deleted", "Include deleted Experiments") .option("-u, --url ", "API base URL", getDefaultApiUrl()), ), ).action(async (options) => { @@ -894,6 +1148,7 @@ export function registerExperimentCommands(program: Command): void { const params = new URLSearchParams(); if (options.cursor) params.set("after", options.cursor); if (options.limit) params.set("limit", String(options.limit)); + if (options.includeDeleted) params.set("includeDeleted", "true"); const response = await apiFetch( options.url, `/experiments${params.size ? `?${params}` : ""}`, @@ -926,6 +1181,7 @@ export function registerExperimentCommands(program: Command): void { .command("get") .description("Get an experiment and its current execution") .argument("", "Experiment ID") + .option("--include-deleted", "Include a deleted Experiment") .option("-u, --url ", "API base URL", getDefaultApiUrl()), ), ).action(async (id: string, options) => { @@ -933,7 +1189,9 @@ export function registerExperimentCommands(program: Command): void { const projectId = requireProjectId(options.project); const response = await apiFetch( options.url, - `/experiments/${encodeURIComponent(id)}`, + `/experiments/${encodeURIComponent(id)}${ + options.includeDeleted ? "?includeDeleted=true" : "" + }`, { projectId }, ); if (!response.ok) await throwApiError(response); diff --git a/apps/portal/src/lib/api.ts b/apps/portal/src/lib/api.ts index 390263d0..f42d8908 100644 --- a/apps/portal/src/lib/api.ts +++ b/apps/portal/src/lib/api.ts @@ -13,10 +13,12 @@ import { parseExperiment, parseExperimentExecution, parseExperimentExecutionPage, + parseExperimentLifecycle, parseExperimentPage, type ExistingRunExperimentCreateView, type ExperimentExecutionPageView, type ExperimentExecutionView, + type ExperimentLifecycleView, type ExperimentMembershipMutationView, type ExperimentMetadataView, type ExperimentPageView, @@ -390,11 +392,13 @@ export const api = { listExperiments: async (opts?: { cursor?: string; limit?: number; + includeDeleted?: boolean; }): Promise => { const value = await request( `/experiments${qs({ after: opts?.cursor, limit: opts?.limit ? String(opts.limit) : undefined, + includeDeleted: opts?.includeDeleted ? "true" : undefined, })}`, undefined, { scoped: true }, @@ -402,23 +406,58 @@ export const api = { return parseExperimentPage(value); }, - getExperiment: async (id: string): Promise => { + getExperiment: async ( + id: string, + opts?: { includeDeleted?: boolean }, + ): Promise => { const value = await request( - `/experiments/${encodeURIComponent(id)}`, + `/experiments/${encodeURIComponent(id)}${qs({ + includeDeleted: opts?.includeDeleted ? "true" : undefined, + })}`, undefined, { scoped: true }, ); return parseExperiment(value); }, + deleteExperiment: async (id: string): Promise => { + const value = await request( + `/experiments/${encodeURIComponent(id)}`, + { method: "DELETE" }, + { scoped: true }, + ); + return parseExperimentLifecycle(value); + }, + + restoreExperiment: async (id: string): Promise => { + const value = await request( + `/experiments/${encodeURIComponent(id)}/restore`, + { method: "POST" }, + { scoped: true }, + ); + return parseExperimentLifecycle(value); + }, + + getExperimentLifecycle: async ( + id: string, + ): Promise => { + const value = await request( + `/experiments/${encodeURIComponent(id)}/lifecycle`, + undefined, + { scoped: true }, + ); + return parseExperimentLifecycle(value); + }, + listExperimentExecutions: async ( id: string, - opts?: { cursor?: string; limit?: number }, + opts?: { cursor?: string; limit?: number; includeDeleted?: boolean }, ): Promise => { const value = await request( `/experiments/${encodeURIComponent(id)}/executions${qs({ after: opts?.cursor, limit: opts?.limit ? String(opts.limit) : undefined, + includeDeleted: opts?.includeDeleted ? "true" : undefined, })}`, undefined, { scoped: true }, @@ -429,9 +468,12 @@ export const api = { getExperimentExecution: async ( id: string, experimentExecutionId: string, + opts?: { includeDeleted?: boolean }, ): Promise => { const value = await request( - `/experiments/${encodeURIComponent(id)}/executions/${encodeURIComponent(experimentExecutionId)}`, + `/experiments/${encodeURIComponent(id)}/executions/${encodeURIComponent(experimentExecutionId)}${qs({ + includeDeleted: opts?.includeDeleted ? "true" : undefined, + })}`, undefined, { scoped: true }, ); @@ -441,9 +483,12 @@ export const api = { listExperimentReports: ( id: string, experimentExecutionId: string, + opts?: { includeDeleted?: boolean }, ): Promise => request( - `/experiments/${encodeURIComponent(id)}/executions/${encodeURIComponent(experimentExecutionId)}/reports`, + `/experiments/${encodeURIComponent(id)}/executions/${encodeURIComponent(experimentExecutionId)}/reports${qs({ + includeDeleted: opts?.includeDeleted ? "true" : undefined, + })}`, undefined, { scoped: true }, ), diff --git a/apps/portal/src/lib/experiments.ts b/apps/portal/src/lib/experiments.ts index 01fb7166..40537c77 100644 --- a/apps/portal/src/lib/experiments.ts +++ b/apps/portal/src/lib/experiments.ts @@ -79,6 +79,104 @@ export interface ExperimentExecutionView { archivedAt?: string; } +export interface ExperimentLifecycleView { + operationId: string; + phase: "deleting" | "deleted" | "restoring" | "restored"; + stateVersion: number; + progress: { + expectedRequests: number; + completedRequests: number; + expectedReports: number; + completedReports: number; + }; + blocking: Array<{ + kind: "request" | "report" | "experiment"; + id: string; + reason: string; + error?: string; + }>; + deleteRequestedAt?: string; + deleteCompletedAt?: string; + restoreRequestedAt?: string; + restoreCompletedAt?: string; +} + +export function parseExperimentLifecycle( + value: unknown, +): ExperimentLifecycleView { + const item = record(value, "experiment lifecycle"); + const progress = record(item.progress, "experiment lifecycle progress"); + const phase = requiredString(item, "phase", "experiment lifecycle"); + if ( + phase !== "deleting" && + phase !== "deleted" && + phase !== "restoring" && + phase !== "restored" + ) { + throw new Error("Invalid experiment lifecycle phase"); + } + const blocking = Array.isArray(item.blocking) + ? item.blocking.map((value) => { + const blocker = record(value, "experiment lifecycle blocker"); + const kind = requiredString( + blocker, + "kind", + "experiment lifecycle blocker", + ); + if ( + kind !== "request" && + kind !== "report" && + kind !== "experiment" + ) { + throw new Error("Invalid experiment lifecycle blocker kind"); + } + const blockerKind: "request" | "report" | "experiment" = kind; + return { + kind: blockerKind, + id: requiredString(blocker, "id", "experiment lifecycle blocker"), + reason: requiredString( + blocker, + "reason", + "experiment lifecycle blocker", + ), + error: optionalString(blocker, "error"), + }; + }) + : []; + return { + operationId: requiredString(item, "operationId", "experiment lifecycle"), + phase, + stateVersion: requiredNumber(item, "stateVersion", "experiment lifecycle"), + progress: { + expectedRequests: requiredNumber( + progress, + "expectedRequests", + "experiment lifecycle progress", + ), + completedRequests: requiredNumber( + progress, + "completedRequests", + "experiment lifecycle progress", + ), + expectedReports: requiredNumber( + progress, + "expectedReports", + "experiment lifecycle progress", + ), + completedReports: requiredNumber( + progress, + "completedReports", + "experiment lifecycle progress", + ), + }, + blocking, + deleteRequestedAt: optionalString(item, "deleteRequestedAt"), + deleteCompletedAt: optionalString(item, "deleteCompletedAt"), + restoreRequestedAt: optionalString(item, "restoreRequestedAt"), + restoreCompletedAt: optionalString(item, "restoreCompletedAt"), + }; +} + export interface ExperimentView { experimentId: string; projectId: string; @@ -91,6 +189,8 @@ export interface ExperimentView { createdAt: string; updatedAt?: string; finalizedAt?: string; + deletedAt?: string; + lifecycle?: ExperimentLifecycleView; } export interface ExperimentPageView { @@ -279,6 +379,10 @@ export function parseExperiment(value: unknown): ExperimentView { return id; }) : []; + const lifecycleValue = experiment.lifecycle; + const lifecycle = lifecycleValue === undefined + ? undefined + : parseExperimentLifecycle(lifecycleValue); return { experimentId: requiredString(experiment, "experimentId", "experiment"), @@ -320,6 +424,8 @@ export function parseExperiment(value: unknown): ExperimentView { createdAt: requiredString(experiment, "createdAt", "experiment"), updatedAt: optionalString(experiment, "updatedAt"), finalizedAt: optionalString(experiment, "finalizedAt"), + deletedAt: optionalString(experiment, "deletedAt"), + lifecycle, }; } diff --git a/apps/portal/src/pages/ExperimentDetail.play.test.tsx b/apps/portal/src/pages/ExperimentDetail.play.test.tsx index 70782ff6..863c137e 100644 --- a/apps/portal/src/pages/ExperimentDetail.play.test.tsx +++ b/apps/portal/src/pages/ExperimentDetail.play.test.tsx @@ -25,6 +25,7 @@ vi.mock("@/lib/api", async (importOriginal) => { listReportTemplates: vi.fn(), listReportTemplateVersions: vi.fn(), updateExperimentReportTemplates: vi.fn(), + restoreExperiment: vi.fn(), }, }; }); @@ -32,6 +33,7 @@ vi.mock("@/lib/api", async (importOriginal) => { import { api } from "@/lib/api"; import experimentDetailMeta, { AttemptSelectionAndRunsTable, + DeletedLifecycleAndRestore, ReportTemplateConfiguration, } from "./ExperimentDetail.stories"; @@ -237,4 +239,67 @@ describe("ExperimentDetail stories", () => { canvasElement: container, }); }); + + it("runs the deleted lifecycle restore interaction", async () => { + vi.mocked(api.getExperiment).mockResolvedValue({ + experimentId: "experiment-demo", + projectId: "demo-project", + metadata: { name: "Profile comparison" }, + revision: { + experimentRevisionId: "revision-demo", + revisionNumber: 1, + memberCount: 1, + }, + execution: { + experimentExecutionId: "execution-current", + attemptNumber: 2, + requestCount: 1, + requests: [], + handlerProgress: {}, + blocking: [], + createdAt: "2026-10-16T10:00:00.000Z", + }, + readiness: "done", + blocking: [], + createdRequestIds: ["request-current"], + createdAt: "2026-10-16T09:00:00.000Z", + deletedAt: "2026-10-16T12:00:00.000Z", + lifecycle: { + operationId: "operation-delete", + phase: "deleted", + stateVersion: 3, + progress: { + expectedRequests: 1, + completedRequests: 1, + expectedReports: 3, + completedReports: 3, + }, + blocking: [], + }, + }); + vi.mocked(api.restoreExperiment).mockResolvedValue({ + operationId: "operation-restore", + phase: "restoring", + stateVersion: 4, + progress: { + expectedRequests: 1, + completedRequests: 1, + expectedReports: 3, + completedReports: 3, + }, + blocking: [], + }); + const StoryRender = experimentDetailMeta.render; + const { container } = render(); + const play = DeletedLifecycleAndRestore.play as unknown as + | ((context: { + canvas: ReturnType; + canvasElement: HTMLElement; + }) => Promise) + | undefined; + await play?.({ + canvas: within(container), + canvasElement: container, + }); + }); }); diff --git a/apps/portal/src/pages/ExperimentDetail.stories.tsx b/apps/portal/src/pages/ExperimentDetail.stories.tsx index 7d327903..e6611b96 100644 --- a/apps/portal/src/pages/ExperimentDetail.stories.tsx +++ b/apps/portal/src/pages/ExperimentDetail.stories.tsx @@ -88,6 +88,25 @@ const experiment = { createdAt: "2026-10-16T09:00:00.000Z", }; +const deletedExperiment = { + ...experiment, + deletedAt: "2026-10-16T12:00:00.000Z", + lifecycle: { + operationId: "operation-delete", + phase: "deleted", + stateVersion: 3, + progress: { + expectedRequests: 1, + completedRequests: 1, + expectedReports: 3, + completedReports: 3, + }, + blocking: [], + deleteRequestedAt: "2026-10-16T11:55:00.000Z", + deleteCompletedAt: "2026-10-16T12:00:00.000Z", + }, +}; + const currentReports = [ { _id: "report-current", @@ -295,6 +314,38 @@ export const AttemptSelectionAndRunsTable: Story = { }, }; +export const DeletedLifecycleAndRestore: Story = { + parameters: { + msw: { + handlers: [ + http.get("/api/v1/experiments/experiment-demo", () => + HttpResponse.json(deletedExperiment), + ), + http.post("/api/v1/experiments/experiment-demo/restore", () => + HttpResponse.json({ + ...deletedExperiment.lifecycle, + operationId: "operation-restore", + phase: "restoring", + stateVersion: 4, + restoreRequestedAt: "2026-10-16T12:05:00.000Z", + }, { status: 202 }), + ), + ], + }, + }, + play: async ({ canvas }) => { + await expect( + await screen.findByRole("heading", { + name: "Lifecycle operation · deleted", + }), + ).toBeVisible(); + await expect( + canvas.getByText("Requests 1/1 · Reports 3/3"), + ).toBeVisible(); + await userEvent.click(canvas.getByRole("button", { name: "Restore" })); + }, +}; + export const ReportTemplateConfiguration: Story = { play: async ({ canvas }) => { await expect( diff --git a/apps/portal/src/pages/ExperimentDetail.test.tsx b/apps/portal/src/pages/ExperimentDetail.test.tsx index d0e22e8f..9318ceea 100644 --- a/apps/portal/src/pages/ExperimentDetail.test.tsx +++ b/apps/portal/src/pages/ExperimentDetail.test.tsx @@ -38,6 +38,8 @@ vi.mock("@/lib/api", async (importOriginal) => { listExperimentReports: vi.fn(), retryExperiment: vi.fn(), updateExperimentRequests: vi.fn(), + deleteExperiment: vi.fn(), + restoreExperiment: vi.fn(), experimentReportStructuredUrl: vi.fn( (_experimentId: string, executionId: string, reportId: string) => `/api/v1/experiments/experiment-1/executions/${executionId}/reports/${reportId}/structured?projectId=project-1`, @@ -234,6 +236,8 @@ beforeAll(() => { beforeEach(() => { vi.mocked(api.listExperimentReports).mockReset(); vi.mocked(api.listExperimentReports).mockResolvedValue([]); + vi.mocked(api.deleteExperiment).mockReset(); + vi.mocked(api.restoreExperiment).mockReset(); }); afterEach(() => { @@ -243,6 +247,75 @@ afterEach(() => { }); describe("ExperimentDetail", () => { + it("starts an asynchronous deletion from the confirmation dialog", async () => { + const user = userEvent.setup(); + vi.mocked(api.deleteExperiment).mockResolvedValue({ + operationId: "operation-delete", + phase: "deleting", + stateVersion: 2, + progress: { + expectedRequests: 11, + completedRequests: 0, + expectedReports: 2, + completedReports: 0, + }, + blocking: [], + }); + renderDetail(); + + await user.click(await screen.findByRole("button", { name: "Delete" })); + expect( + screen.getByRole("heading", { name: "Delete this Experiment?" }), + ).toBeTruthy(); + await user.click( + screen.getByRole("button", { name: /Delete Experiment/ }), + ); + + await waitFor(() => { + expect(api.deleteExperiment).toHaveBeenCalledWith("experiment-1"); + }); + }); + + it("shows deleted lifecycle progress and restores the Experiment", async () => { + const user = userEvent.setup(); + const deletedExperiment: ExperimentView = { + ...experiment, + deletedAt: "2026-10-16T12:00:00.000Z", + lifecycle: { + operationId: "operation-delete", + phase: "deleted", + stateVersion: 3, + progress: { + expectedRequests: 11, + completedRequests: 11, + expectedReports: 2, + completedReports: 2, + }, + blocking: [], + }, + }; + vi.mocked(api.restoreExperiment).mockResolvedValue({ + ...deletedExperiment.lifecycle!, + operationId: "operation-restore", + phase: "restoring", + stateVersion: 4, + }); + renderDetail("/experiments/experiment-1?includeDeleted=true", deletedExperiment); + + expect( + await screen.findByRole("heading", { + name: "Lifecycle operation · deleted", + }), + ).toBeTruthy(); + expect(screen.getByText("Requests 11/11 · Reports 2/2")).toBeTruthy(); + await user.click(screen.getByRole("button", { name: "Restore" })); + + await waitFor(() => { + expect(api.restoreExperiment).toHaveBeenCalledWith("experiment-1"); + }); + expect(screen.queryByRole("button", { name: "Delete" })).toBeNull(); + }); + it("defaults to the current attempt and paginates its shared runs table", async () => { renderDetail(); @@ -349,6 +422,7 @@ describe("ExperimentDetail", () => { expect(api.listExperimentReports).toHaveBeenCalledWith( "experiment-1", "execution-history", + { includeDeleted: false }, ); expect( screen.getByRole("link", { name: "Structured JSON" }).getAttribute("href"), @@ -364,6 +438,7 @@ describe("ExperimentDetail", () => { expect(api.listExperimentReports).toHaveBeenCalledWith( "experiment-1", "execution-current", + { includeDeleted: false }, ); }); }); @@ -383,6 +458,7 @@ describe("ExperimentDetail", () => { expect(api.listExperimentReports).toHaveBeenLastCalledWith( "experiment-1", "execution-current", + { includeDeleted: false }, ); expect( await screen.findByText("Runs in attempt 2"), diff --git a/apps/portal/src/pages/ExperimentDetail.tsx b/apps/portal/src/pages/ExperimentDetail.tsx index 38eeab60..a7b1c3c6 100644 --- a/apps/portal/src/pages/ExperimentDetail.tsx +++ b/apps/portal/src/pages/ExperimentDetail.tsx @@ -3,7 +3,14 @@ import { useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, Clock, RotateCcw, Unlink } from "lucide-react"; +import { + ArchiveRestore, + ArrowLeft, + Clock, + RotateCcw, + Trash2, + Unlink, +} from "lucide-react"; import { Link, useNavigate, @@ -274,6 +281,7 @@ export function ExperimentDetail() { const [searchParams, setSearchParams] = useSearchParams(); const [selectedRequestIds, setSelectedRequestIds] = useState>(new Set()); const [removeDialogOpen, setRemoveDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [selectedTemplateIds, setSelectedTemplateIds] = useState>( new Set(), ); @@ -299,10 +307,12 @@ export function ExperimentDetail() { }); const experimentQuery = useQuery({ queryKey: ["experiment", id], - queryFn: () => api.getExperiment(id!), + queryFn: () => api.getExperiment(id!, { includeDeleted: true }), enabled: Boolean(id), refetchInterval: (query) => { const readiness = query.state.data?.readiness; + const phase = query.state.data?.lifecycle?.phase; + if (phase === "deleting" || phase === "restoring") return 1_000; return readiness === "done" || readiness === "failed" || readiness === "blocked" @@ -312,7 +322,11 @@ export function ExperimentDetail() { }); const executionsQuery = useQuery({ queryKey: ["experiment-executions", id], - queryFn: () => api.listExperimentExecutions(id!, { limit: 100 }), + queryFn: () => + api.listExperimentExecutions(id!, { + limit: 100, + includeDeleted: Boolean(experimentQuery.data?.lifecycle), + }), enabled: Boolean(id) && experimentQuery.isSuccess, }); const templatesQuery = useQuery({ @@ -331,12 +345,18 @@ export function ExperimentDetail() { selectedExecutionId !== currentExecutionId; const selectedExecutionQuery = useQuery({ queryKey: ["experiment-execution", id, selectedExecutionId], - queryFn: () => api.getExperimentExecution(id!, selectedExecutionId!), + queryFn: () => + api.getExperimentExecution(id!, selectedExecutionId!, { + includeDeleted: Boolean(experimentQuery.data?.lifecycle), + }), enabled: Boolean(id && selectedExecutionId && isViewingHistorical), }); const reportsQuery = useQuery({ queryKey: ["experiment-reports", id, reportExecutionId], - queryFn: () => api.listExperimentReports(id!, reportExecutionId!), + queryFn: () => + api.listExperimentReports(id!, reportExecutionId!, { + includeDeleted: Boolean(experimentQuery.data?.lifecycle), + }), enabled: Boolean(id && reportExecutionId), refetchInterval: (query) => { const reports = query.state.data; @@ -373,6 +393,29 @@ export function ExperimentDetail() { toast.error(error instanceof Error ? error.message : "Retry failed"); }, }); + const deleteExperiment = useMutation({ + mutationFn: () => api.deleteExperiment(experimentQuery.data!.experimentId), + onSuccess: () => { + setDeleteDialogOpen(false); + toast.success("Experiment deletion started"); + void queryClient.invalidateQueries({ queryKey: ["experiment", id] }); + void queryClient.invalidateQueries({ queryKey: ["experiments"] }); + }, + onError: (error) => { + toast.error(error instanceof Error ? error.message : "Delete failed"); + }, + }); + const restoreExperiment = useMutation({ + mutationFn: () => api.restoreExperiment(experimentQuery.data!.experimentId), + onSuccess: () => { + toast.success("Experiment restore started"); + void queryClient.invalidateQueries({ queryKey: ["experiment", id] }); + void queryClient.invalidateQueries({ queryKey: ["experiments"] }); + }, + onError: (error) => { + toast.error(error instanceof Error ? error.message : "Restore failed"); + }, + }); const retryReport = useMutation({ mutationFn: (templateId: string) => api.retryExperimentReport( @@ -849,9 +892,13 @@ export function ExperimentDetail() { } const experiment = experimentQuery.data; - const canRetry = !["waiting", "processing"].includes( - experiment.readiness ?? "", - ); + const lifecycleActive = + experiment.lifecycle?.phase === "deleting" || + experiment.lifecycle?.phase === "deleted" || + experiment.lifecycle?.phase === "restoring"; + const canRetry = + !lifecycleActive && + !["waiting", "processing"].includes(experiment.readiness ?? ""); const selectedValue = activeExecution?.experimentExecutionId ?? selectedExecutionId ?? @@ -888,6 +935,17 @@ export function ExperimentDetail() { {isViewingHistorical && ( Historical attempt )} + {experiment.lifecycle && ( + + {experiment.lifecycle.phase} + + )} {experiment.metadata.description && (

@@ -896,6 +954,23 @@ export function ExperimentDetail() { )}

+ {experiment.lifecycle?.phase === "deleted" ? ( + + ) : !experiment.lifecycle ? ( + + ) : null}
); } diff --git a/apps/portal/src/pages/ExperimentsList.play.test.tsx b/apps/portal/src/pages/ExperimentsList.play.test.tsx index 9167338b..6deb9bda 100644 --- a/apps/portal/src/pages/ExperimentsList.play.test.tsx +++ b/apps/portal/src/pages/ExperimentsList.play.test.tsx @@ -8,6 +8,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { MemoryRouter } from "react-router-dom"; import { api } from "@/lib/api"; import experimentsListMeta, { + DeletedVisibility, SharedListInteractions, } from "./ExperimentsList.stories"; @@ -43,8 +44,45 @@ beforeAll(() => { configurable: true, value: IntersectionObserverMock, }); - vi.mocked(api.listExperiments).mockResolvedValue({ - items: [ + vi.mocked(api.listExperiments).mockImplementation(async (options) => ({ + items: options?.includeDeleted + ? [{ + experimentId: "experiment-deleted", + projectId: "demo-project", + metadata: { name: "Deleted benchmark" }, + revision: { + experimentRevisionId: "revision-deleted", + revisionNumber: 1, + memberCount: 4, + }, + execution: { + experimentExecutionId: "execution-deleted", + attemptNumber: 2, + requestCount: 4, + requests: [], + handlerProgress: {}, + blocking: [], + createdAt: "2026-10-16T10:00:00.000Z", + }, + readiness: "done", + blocking: [], + createdRequestIds: [], + createdAt: "2026-10-16T09:00:00.000Z", + deletedAt: "2026-10-16T12:00:00.000Z", + lifecycle: { + operationId: "operation-delete", + phase: "deleted", + stateVersion: 3, + progress: { + expectedRequests: 4, + completedRequests: 4, + expectedReports: 1, + completedReports: 1, + }, + blocking: [], + }, + }] + : [ { experimentId: "experiment-alpha", projectId: "demo-project", @@ -91,8 +129,8 @@ beforeAll(() => { createdRequestIds: [], createdAt: "2026-10-16T09:00:00.000Z", }, - ], - }); + ], + })); }); describe("ExperimentsList stories", () => { @@ -121,4 +159,30 @@ describe("ExperimentsList stories", () => { canvasElement: container, }); }); + + it("runs the deleted visibility interaction", async () => { + const StoryRender = experimentsListMeta.component; + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + const { container } = render( + + +
+ +
+
+
, + ); + const play = DeletedVisibility.play as unknown as + | ((context: { + canvas: ReturnType; + canvasElement: HTMLElement; + }) => Promise) + | undefined; + await play?.({ + canvas: within(container), + canvasElement: container, + }); + }); }); diff --git a/apps/portal/src/pages/ExperimentsList.stories.tsx b/apps/portal/src/pages/ExperimentsList.stories.tsx index 2b5015c4..1b34b83d 100644 --- a/apps/portal/src/pages/ExperimentsList.stories.tsx +++ b/apps/portal/src/pages/ExperimentsList.stories.tsx @@ -98,3 +98,51 @@ export const SharedListInteractions: Story = { ).toBeVisible(); }, }; + +export const DeletedVisibility: Story = { + parameters: { + msw: { + handlers: [ + http.get("/api/v1/experiments", ({ request }) => { + const includeDeleted = + new URL(request.url).searchParams.get("includeDeleted") === "true"; + return HttpResponse.json({ + data: includeDeleted + ? [{ + ...experiment( + "experiment-deleted", + "Deleted benchmark", + "done", + ), + deletedAt: "2026-10-16T12:00:00.000Z", + lifecycle: { + operationId: "operation-delete", + phase: "deleted", + stateVersion: 3, + progress: { + expectedRequests: 4, + completedRequests: 4, + expectedReports: 1, + completedReports: 1, + }, + blocking: [], + }, + }] + : [ + experiment("experiment-alpha", "Alpha benchmark", "done"), + ], + }); + }), + ], + }, + }, + play: async ({ canvas }) => { + await userEvent.click( + canvas.getByRole("button", { name: "Show deleted" }), + ); + await expect( + (await screen.findAllByText("Deleted benchmark"))[0], + ).toBeVisible(); + await expect(canvas.getByText("Hide deleted")).toBeVisible(); + }, +}; diff --git a/apps/portal/src/pages/ExperimentsList.test.tsx b/apps/portal/src/pages/ExperimentsList.test.tsx index b9027ba0..f7dd9d39 100644 --- a/apps/portal/src/pages/ExperimentsList.test.tsx +++ b/apps/portal/src/pages/ExperimentsList.test.tsx @@ -119,6 +119,47 @@ afterEach(() => { }); describe("ExperimentsList", () => { + it("includes deleted Experiments on demand and preserves deleted navigation", async () => { + const deletedExperiment = { + ...experiment("experiment-deleted", "Deleted benchmark", "done"), + deletedAt: "2026-10-16T12:00:00.000Z", + lifecycle: { + operationId: "operation-delete", + phase: "deleted" as const, + stateVersion: 3, + progress: { + expectedRequests: 2, + completedRequests: 2, + expectedReports: 1, + completedReports: 1, + }, + blocking: [], + }, + }; + vi.mocked(api.listExperiments) + .mockResolvedValueOnce({ items: firstPage }) + .mockResolvedValueOnce({ items: [deletedExperiment] }); + renderList(); + + fireEvent.click( + await screen.findByRole("button", { name: "Show deleted" }), + ); + expect( + (await screen.findAllByText("Deleted benchmark")).length, + ).toBeGreaterThan(0); + expect(api.listExperiments).toHaveBeenLastCalledWith({ + cursor: undefined, + includeDeleted: true, + limit: 25, + }); + fireEvent.click(screen.getAllByText("Deleted benchmark")[0]); + await waitFor(() => { + expect(screen.getByTestId("location").textContent).toBe( + "/experiments/experiment-deleted", + ); + }); + }); + it("uses the shared list controls for filtering and customization", async () => { vi.mocked(api.listExperiments).mockResolvedValue({ items: firstPage, @@ -154,6 +195,7 @@ describe("ExperimentsList", () => { expect((await screen.findAllByText("Gamma benchmark")).length).toBeGreaterThan(0); expect(api.listExperiments).toHaveBeenLastCalledWith({ cursor: "next-page", + includeDeleted: false, limit: 25, }); diff --git a/apps/portal/src/pages/ExperimentsList.tsx b/apps/portal/src/pages/ExperimentsList.tsx index 6c13e4c4..bd6713d5 100644 --- a/apps/portal/src/pages/ExperimentsList.tsx +++ b/apps/portal/src/pages/ExperimentsList.tsx @@ -3,7 +3,7 @@ import { useEffect, useMemo, useState } from "react"; import { keepPreviousData, useQuery } from "@tanstack/react-query"; -import { Atom, Plus } from "lucide-react"; +import { ArchiveRestore, Atom, Plus } from "lucide-react"; import { Link, useNavigate } from "react-router-dom"; import { CheckboxFilterGroup, @@ -86,6 +86,7 @@ export function ExperimentsList() { filterKeys: FILTER_KEYS, }); const [customizeOpen, setCustomizeOpen] = useState(false); + const [showDeleted, setShowDeleted] = useState(false); const [cursorStack, setCursorStack] = useState>([ undefined, ]); @@ -99,9 +100,13 @@ export function ExperimentsList() { }); const cursor = cursorStack[state.page - 1]; const experimentsQuery = useQuery({ - queryKey: ["experiments", cursor, state.pageSize], + queryKey: ["experiments", cursor, state.pageSize, showDeleted], queryFn: () => - api.listExperiments({ cursor, limit: state.pageSize }), + api.listExperiments({ + cursor, + limit: state.pageSize, + includeDeleted: showDeleted, + }), placeholderData: keepPreviousData, }); @@ -200,7 +205,9 @@ export function ExperimentsList() { hidden: columnVisibility.isHidden("readiness"), cell: (experiment) => ( - {experiment.readiness ?? "unknown"} + {experiment.lifecycle?.phase === "deleted" + ? "deleted" + : experiment.lifecycle?.phase ?? experiment.readiness ?? "unknown"} ), }, @@ -305,12 +312,25 @@ export function ExperimentsList() { description="Reproducible benchmark compositions and processing history" railStorageKey="experiments" actions={ - +
+ + +
} filterRail={ experiment.experimentId} onRowClick={(experiment) => - navigate(`/experiments/${experiment.experimentId}`) + navigate( + `/experiments/${experiment.experimentId}${ + experiment.lifecycle ? "?includeDeleted=true" : "" + }`, + ) } sort={state.sort} sortDir={state.sortDir} diff --git a/apps/workers/readout/src/readout-queue-processor.ts b/apps/workers/readout/src/readout-queue-processor.ts index c9331322..1f04cbfb 100644 --- a/apps/workers/readout/src/readout-queue-processor.ts +++ b/apps/workers/readout/src/readout-queue-processor.ts @@ -169,6 +169,7 @@ export class ReadoutQueueProcessor extends BaseQueueProcessor { }; // Update status to "generating" and set reporter - await withRetry(() => this.collection.updateOne( - { _id: reportId } as any, + const claimed = await withRetry(() => this.collection.updateOne( + { _id: reportId, deletedAt: { $exists: false } } as any, { $set: { status: "generating", @@ -166,6 +166,9 @@ export class ReportQueueProcessor extends BaseQueueProcessor { }, } as any )); + if (claimed.matchedCount === 0) { + throw new Error(`Report ${reportId} is deleted or stale`); + } await log("info", `Reporter: ${reporter.agentId}@${reporter.agentVersion}, model: ${reporter.model}`); @@ -188,8 +191,8 @@ export class ReportQueueProcessor extends BaseQueueProcessor { trackMetric({ name: "report_generator.llm_session_duration_ms", value: Date.now() - llmStart, properties: { service: "report-generator" } }); // --- Persist report content --- - await withRetry(() => this.collection.updateOne( - { _id: reportId } as any, + const persisted = await withRetry(() => this.collection.updateOne( + { _id: reportId, deletedAt: { $exists: false } } as any, { $set: { status: "completed", @@ -198,6 +201,9 @@ export class ReportQueueProcessor extends BaseQueueProcessor { }, } as any )); + if (persisted.matchedCount === 0) { + throw new Error(`Report ${reportId} is deleted or stale`); + } 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" } }); diff --git a/docs/architecture/app-design.md b/docs/architecture/app-design.md index 50e59a77..aadad061 100644 --- a/docs/architecture/app-design.md +++ b/docs/architecture/app-design.md @@ -495,6 +495,16 @@ authoritative. Direct deletion of a member publishes a removal revision and clears that field before soft deletion. Only legacy composition inputs that overlap Experiment creation are deprecated. +Deleting an Experiment is an asynchronous recoverable lifecycle operation, not +a direct document removal. The root becomes non-dispatchable immediately and +stores a stable operation ID, phase, progress, blockers, and CAS +`stateVersion`. Exact request and report targets are snapshotted through the +same deterministic fixed-payload storage used by execution vectors. Cascaded +children store operation provenance; records deleted independently are never +claimed or restored. Restore revives matching children first, then the +Experiment, and finally reconciles the current request/run vector. Immutable +execution history and report artifacts are retained throughout. + ## Runs List Query API `GET /api/v1/requests` powers the Portal **Runs** list and `scope run list`. All diff --git a/docs/architecture/experiments.md b/docs/architecture/experiments.md index 2ca8325b..fb1e886b 100644 --- a/docs/architecture/experiments.md +++ b/docs/architecture/experiments.md @@ -245,16 +245,20 @@ non-dispatchable, and a later add can resume it with another revision. ## Collection and index ownership -| Collection | Owner and contents | Indexes created by migration 034 | +| Collection | Owner and contents | Indexes | | --- | --- | --- | -| `experiments` | One finalized/current document per Experiment; `_id` equals `experimentId` | `{ projectId: 1, _id: 1 }` for project-scoped paging and lookup | +| `experiments` | One finalized/current document per Experiment; `_id` equals `experimentId` | `{ projectId: 1, _id: 1 }` for project-scoped paging and lookup; `{ "lifecycle.phase": 1 }` for lifecycle repair | | `experiment-executions` | Immutable archived execution history; `_id` equals `experimentExecutionId` | `{ projectId: 1 }`; `{ experimentId: 1, attemptNumber: -1 }` | | `experiment-chunks` | Deterministic immutable request-vector chunks | No secondary index; chunks are addressed directly by deterministic `_id` | +| `requests` | Current request ownership and lifecycle provenance | `{ "deletionProvenance.operationId": 1 }` for exact cascade repair | +| `reports` | Current and historical report records with retained artifacts | `{ experimentId: 1 }`; `{ "deletionProvenance.operationId": 1 }` | The `ExperimentStore` owns root and history writes. `FixedPayloadStore` owns chunk preparation, read verification, and unpublished cleanup. Migration `034-create-experiment-storage-indexes.ts` owns the secondary indexes and intentionally leaves removal to a manual operation in `down()`. +Migration `036-add-experiment-lifecycle-indexes.ts` owns lifecycle repair and +provenance indexes using Cosmos-compatible single-field definitions. Every lookup includes `projectId` in addition to the entity ID. An Experiment ID is never resolved globally. @@ -266,6 +270,22 @@ readiness, rollover, retry, publication repair, and Experiment-handler dispatch policy. Its polling loop repairs missed notifications, vector drift, dropped queue notices, and interrupted publication or rollover. +`ExperimentLifecycleCoordinator` owns recoverable delete and restore +operations. A stable operation ID and compare-and-swap `stateVersion` advance +the root through `deleting`, `deleted`, and `restoring`. Starting deletion +makes the Experiment non-dispatchable immediately. Exact immutable target +snapshots reuse `FixedPayloadStore`; requests and reports are soft-deleted only +when they were active and receive `{ experimentId, operationId }` provenance. +Independent deletions are never claimed. Restore processes matching children +first, revives the Experiment, then reconciles the current request/run vector +and rolls to a fresh execution only when drift exists. Archived executions and +report blobs remain immutable. + +The dispatcher polls lifecycle operations alongside publication repair so +partial cascades, dropped notifications, and process restarts converge. Queue +claims and worker final writes require an active, nondeleted request or report, +which prevents stale in-flight work from publishing after deletion starts. + The scheduler continues to own coding-agent and per-run handler dispatch. For Experiment handlers it only applies exact claim and completion messages through shared `ExperimentStore` compare-and-swap primitives. Storage layout, @@ -279,8 +299,11 @@ All routes require `?projectId=`: | Method | Route | Behavior | | --- | --- | --- | | `POST` | `/api/v1/experiments` | Create from composed definitions, explicitly empty membership, or `{ mode: "existing", metadata, requestIds }` | -| `GET` | `/api/v1/experiments` | Cursor-page finalized Experiments | -| `GET` | `/api/v1/experiments/:id` | Read the public Experiment and current execution | +| `GET` | `/api/v1/experiments` | Cursor-page finalized Experiments; `includeDeleted=true` includes lifecycle records | +| `GET` | `/api/v1/experiments/:id` | Read the public Experiment and current execution; `includeDeleted=true` reads a deleted record | +| `DELETE` | `/api/v1/experiments/:id` | Start or resume a recoverable cascading delete and return `202` | +| `POST` | `/api/v1/experiments/:id/restore` | Start or resume child-first restore and return `202` | +| `GET` | `/api/v1/experiments/:id/lifecycle` | Read and repair lifecycle progress and blockers | | `GET` | `/api/v1/experiments/:id/executions` | List current and archived execution summaries | | `GET` | `/api/v1/experiments/:id/executions/:experimentExecutionId` | Read one exact current or archived execution | | `POST` | `/api/v1/experiments/:id/retry` | Start a new processing attempt for the current exact vector | @@ -325,6 +348,9 @@ scope experiment executions [experiment-execution-id] scope experiment retry --execution scope experiment reports set --revision --template [...] scope experiment report retry --execution --template +scope experiment delete [--no-wait] +scope experiment restore [--no-wait] +scope experiment status ``` Creation reuses run configuration flags, adds Experiment metadata, supports @@ -333,6 +359,11 @@ standalone requests. `add`/`move` and `remove` accept repeatable request IDs and print the resulting revision and execution identities. Every command requires project scope. +Delete and restore commands poll the stable lifecycle operation by default. +`--no-wait` returns the accepted state immediately for automation that polls +separately. `scope experiment status` prints exact progress and blockers, while +`list --include-deleted` and `get --include-deleted` expose recoverable records. + The Portal provides `/experiments`, `/experiments/new`, and `/experiments/:id`. The shared creation form retains the established run configuration and expansion count and adds Experiment metadata. No selected @@ -390,6 +421,13 @@ visible, identify ineligible requests and owners, and avoid partial navigation. Coordinated deletion warns about membership changes and shows the resulting revision. +The Experiments list hides deleted records by default and provides a +**Show deleted** toggle. Experiment Detail displays lifecycle phase, operation +ID, request/report progress, and exact blockers. Deletion uses an explicit +confirmation dialog; a deleted Experiment exposes **Restore**. Membership, +retry, and report publication controls stay disabled while lifecycle work is +active. + ## Permanent request creation and composition deprecation `POST /api/v1/requests`, `scope run submit`, and the Portal's **New Run** flow @@ -414,14 +452,7 @@ Experiment creation. ## Deferred capabilities -The following remain outside the current implementation: - -| Follow-up | Deferred capability | -| --- | --- | -| PR D | Report-template attachment or template-driven report orchestration | -| PR E | Cascading lifecycle operations across an Experiment and its member requests/runs | - -GEPA restacking is also excluded. +GEPA restacking is excluded from the Experiment lifecycle implementation. ## Key files @@ -432,10 +463,13 @@ GEPA restacking is also excluded. | `packages/shared/src/experiments/layout.ts` | BSON limits and inline/chunked layout selection | | `packages/shared/src/experiments/routing.ts` | SHA-256 bucket routing, chunk IDs, and fingerprints | | `packages/shared/src/experiments/fixed-payload-store.ts` | Deterministic chunk publication and verified reads | +| `packages/shared/src/experiments/lifecycle-target-store.ts` | Exact chunk-backed lifecycle target snapshots | | `packages/shared/src/experiments/experiment-store.ts` | Creation, CAS processing, freeze, archive, rollover, retry, and repair | | `packages/db-migrations/src/migrations/034-create-experiment-storage-indexes.ts` | Collection index ownership | | `packages/db-migrations/src/migrations/035-version-report-templates.ts` | Existing-template backfill, immutable template versions, and report lookup indexes | +| `packages/db-migrations/src/migrations/036-add-experiment-lifecycle-indexes.ts` | Lifecycle repair and deletion-provenance indexes | | `apps/api/src/experiment-coordinator.ts` | API-owned workflow policy and repair | +| `apps/api/src/experiment-lifecycle.ts` | Recoverable delete/restore orchestration and reconciliation | | `apps/api/src/experiment-membership.ts` | Complete-set membership validation, request ownership, publication, and repair | | `apps/api/src/experiment-dispatcher.ts` | Durable polling and queue delivery | | `apps/cli/src/commands/experiment.ts` | Project-scoped CLI surface | diff --git a/packages/db-migrations/src/036-add-experiment-lifecycle-indexes.test.ts b/packages/db-migrations/src/036-add-experiment-lifecycle-indexes.test.ts new file mode 100644 index 00000000..5d5955df --- /dev/null +++ b/packages/db-migrations/src/036-add-experiment-lifecycle-indexes.test.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { Db } from "mongodb"; +import { describe, expect, it, vi } from "vitest"; +import { AddExperimentLifecycleIndexes } from "./migrations/036-add-experiment-lifecycle-indexes.js"; + +function collection() { + return { createIndex: vi.fn(async () => "index") }; +} + +describe("migration 036: experiment lifecycle indexes", () => { + it("creates the lifecycle repair and provenance indexes", async () => { + const experiments = collection(); + const requests = collection(); + const reports = collection(); + const db = { + collection: vi.fn((name: string) => { + if (name === "experiments") return experiments; + if (name === "requests") return requests; + if (name === "reports") return reports; + throw new Error(`Unexpected collection ${name}`); + }), + } as unknown as Db; + + await new AddExperimentLifecycleIndexes().up(db); + + expect(experiments.createIndex).toHaveBeenCalledWith( + { "lifecycle.phase": 1, updatedAt: 1 }, + {}, + ); + expect(requests.createIndex).toHaveBeenCalledWith( + { "deletionProvenance.operationId": 1 }, + {}, + ); + expect(reports.createIndex).toHaveBeenCalledWith( + { experimentId: 1 }, + {}, + ); + expect(reports.createIndex).toHaveBeenCalledWith( + { "deletionProvenance.operationId": 1 }, + {}, + ); + }); +}); diff --git a/packages/db-migrations/src/migrations/036-add-experiment-lifecycle-indexes.ts b/packages/db-migrations/src/migrations/036-add-experiment-lifecycle-indexes.ts new file mode 100644 index 00000000..5f4a82e5 --- /dev/null +++ b/packages/db-migrations/src/migrations/036-add-experiment-lifecycle-indexes.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { Db } from "mongodb"; +import type { MigrationInterface } from "mongo-migrate-ts"; +import { ensureIndex } from "../cosmos-index-helpers.js"; + +const TAG = "036"; + +export class AddExperimentLifecycleIndexes implements MigrationInterface { + async up(db: Db): Promise { + await ensureIndex( + db.collection("experiments"), + { "lifecycle.phase": 1, updatedAt: 1 }, + {}, + "experiments", + TAG, + ); + await ensureIndex( + db.collection("requests"), + { "deletionProvenance.operationId": 1 }, + {}, + "requests", + TAG, + ); + await ensureIndex( + db.collection("reports"), + { experimentId: 1 }, + {}, + "reports", + TAG, + ); + await ensureIndex( + db.collection("reports"), + { "deletionProvenance.operationId": 1 }, + {}, + "reports", + TAG, + ); + console.log(`[${TAG}] Experiment lifecycle indexes complete`); + } + + async down(_db: Db): Promise { + console.log( + `[${TAG}-down] Skipping index changes - drop lifecycle indexes manually if needed`, + ); + } +} diff --git a/packages/db-migrations/src/required-migrations.ts b/packages/db-migrations/src/required-migrations.ts index 7753dc16..1c19619f 100644 --- a/packages/db-migrations/src/required-migrations.ts +++ b/packages/db-migrations/src/required-migrations.ts @@ -48,6 +48,7 @@ export const REQUIRED_MIGRATIONS: readonly string[] = [ "033-backfill-handler-status.ts", "034-create-experiment-storage-indexes.ts", "035-version-report-templates.ts", + "036-add-experiment-lifecycle-indexes.ts", "021-backfill-criteria-kind.ts", "022-backfill-criteria-subject.ts", ]; diff --git a/packages/shared/src/experiments/experiment-store.test.ts b/packages/shared/src/experiments/experiment-store.test.ts index 0e364795..3e7c991b 100644 --- a/packages/shared/src/experiments/experiment-store.test.ts +++ b/packages/shared/src/experiments/experiment-store.test.ts @@ -63,6 +63,13 @@ function matches(document: object, filter: Document): boolean { Boolean((expected as { $exists: unknown }).$exists) ); } + if ( + typeof expected === "object" && + expected !== null && + "$nin" in expected + ) { + return !(expected as { $nin: unknown[] }).$nin.includes(actual); + } return actual === expected; }); } diff --git a/packages/shared/src/experiments/experiment-store.ts b/packages/shared/src/experiments/experiment-store.ts index 66197e3d..884de07d 100644 --- a/packages/shared/src/experiments/experiment-store.ts +++ b/packages/shared/src/experiments/experiment-store.ts @@ -257,6 +257,22 @@ export class ExperimentStore { async get( projectId: string, experimentId: string, + ): Promise { + const raw: unknown = await this.collections.experiments.findOne({ + _id: experimentId, + projectId, + finalizedAt: { $exists: true }, + deletedAt: { $exists: false }, + "lifecycle.phase": { + $nin: ["deleting", "deleted", "restoring"], + }, + } as Document); + return raw ? parseExperiment(raw) : null; + } + + async getIncludingDeleted( + projectId: string, + experimentId: string, ): Promise { const raw: unknown = await this.collections.experiments.findOne({ _id: experimentId, @@ -270,11 +286,20 @@ export class ExperimentStore { projectId: string; limit?: number; afterExperimentId?: string; + includeDeleted?: boolean; }): Promise { const limit = Math.min(Math.max(input.limit ?? 50, 1), 100); const filter: Document = { projectId: input.projectId, finalizedAt: { $exists: true }, + ...(input.includeDeleted + ? {} + : { + deletedAt: { $exists: false }, + "lifecycle.phase": { + $nin: ["deleting", "deleted", "restoring"], + }, + }), ...(input.afterExperimentId ? { _id: { $gt: input.afterExperimentId } } : {}), @@ -299,8 +324,11 @@ export class ExperimentStore { projectId: string; experimentId: string; experimentExecutionId: string; + includeDeleted?: boolean; }): Promise { - const experiment = await this.get(input.projectId, input.experimentId); + const experiment = input.includeDeleted + ? await this.getIncludingDeleted(input.projectId, input.experimentId) + : await this.get(input.projectId, input.experimentId); if ( experiment?.execution.experimentExecutionId === input.experimentExecutionId @@ -319,8 +347,11 @@ export class ExperimentStore { projectId: string; experimentId: string; experimentExecutionId: string; + includeDeleted?: boolean; }): Promise { - const experiment = await this.get(input.projectId, input.experimentId); + const experiment = input.includeDeleted + ? await this.getIncludingDeleted(input.projectId, input.experimentId) + : await this.get(input.projectId, input.experimentId); const execution = experiment?.execution.experimentExecutionId === input.experimentExecutionId @@ -378,6 +409,9 @@ export class ExperimentStore { "execution.version": input.expectedExecutionVersion, "execution.frozenAt": { $exists: false }, "execution.rollover": { $exists: false }, + "lifecycle.phase": { + $nin: ["deleting", "deleted", "restoring"], + }, [`${path}.status`]: "queued", [`${path}.version`]: input.handlerVersion, [`${path}.claimToken`]: input.claimToken, @@ -426,6 +460,9 @@ export class ExperimentStore { "execution.version": input.expectedExecutionVersion, "execution.rollover": { $exists: false }, "execution.frozenAt": { $exists: false }, + "lifecycle.phase": { + $nin: ["deleting", "deleted", "restoring"], + }, [`${path}.status`]: "processing", [`${path}.version`]: input.handlerVersion, [`${path}.claimToken`]: input.claimToken, @@ -468,6 +505,9 @@ export class ExperimentStore { "execution.version": input.expectedExecutionVersion, "execution.frozenAt": { $exists: false }, "execution.rollover": { $exists: false }, + "lifecycle.phase": { + $nin: ["deleting", "deleted", "restoring"], + }, } as Document, { $set: { @@ -503,6 +543,9 @@ export class ExperimentStore { "execution.version": input.expectedExecutionVersion, "execution.frozenAt": { $exists: false }, "execution.rollover": { $exists: false }, + "lifecycle.phase": { + $nin: ["deleting", "deleted", "restoring"], + }, [`${path}.status`]: "queued", [`${path}.templateVersionId`]: input.templateVersionId, [`${path}.claimToken`]: input.claimToken, @@ -545,6 +588,9 @@ export class ExperimentStore { "execution.version": input.expectedExecutionVersion, "execution.frozenAt": { $exists: false }, "execution.rollover": { $exists: false }, + "lifecycle.phase": { + $nin: ["deleting", "deleted", "restoring"], + }, [`${path}.status`]: "processing", [`${path}.templateVersionId`]: input.templateVersionId, [`${path}.claimToken`]: input.claimToken, @@ -583,6 +629,9 @@ export class ExperimentStore { "execution.frozenAt": { $exists: false }, "execution.rollover": { $exists: false }, "execution.requestStateUpdate": { $exists: false }, + "lifecycle.phase": { + $nin: ["deleting", "deleted", "restoring"], + }, } as Document, { $set: { diff --git a/packages/shared/src/experiments/index.ts b/packages/shared/src/experiments/index.ts index acdbd6b8..27bcb35e 100644 --- a/packages/shared/src/experiments/index.ts +++ b/packages/shared/src/experiments/index.ts @@ -6,4 +6,5 @@ export * from "./schemas.js"; export * from "./routing.js"; export * from "./layout.js"; export * from "./fixed-payload-store.js"; +export * from "./lifecycle-target-store.js"; export * from "./experiment-store.js"; diff --git a/packages/shared/src/experiments/lifecycle-target-store.ts b/packages/shared/src/experiments/lifecycle-target-store.ts new file mode 100644 index 00000000..e536047f --- /dev/null +++ b/packages/shared/src/experiments/lifecycle-target-store.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { Collection, Document } from "mongodb"; +import { FixedPayloadStore } from "./fixed-payload-store.js"; +import { fingerprintExperimentPayload } from "./routing.js"; +import type { + ExperimentLifecycleTarget, + ExperimentRequestState, + FixedPayload, +} from "./types.js"; + +function targetKey(target: ExperimentLifecycleTarget): string { + return fingerprintExperimentPayload([target.kind, target.id]); +} + +function toState(target: ExperimentLifecycleTarget): ExperimentRequestState { + return { + requestId: targetKey(target), + runId: target.kind, + dependencies: [{ + handlerId: target.id, + state: "ready", + }], + blockers: [], + }; +} + +function fromState(state: ExperimentRequestState): ExperimentLifecycleTarget { + const id = state.dependencies[0]?.handlerId; + if ( + (state.runId !== "request" && state.runId !== "report") || + state.dependencies.length !== 1 || + !id + ) { + throw new Error("Invalid Experiment lifecycle target state"); + } + const target = { kind: state.runId, id } satisfies ExperimentLifecycleTarget; + if (state.requestId !== targetKey(target)) { + throw new Error("Experiment lifecycle target fingerprint mismatch"); + } + return target; +} + +export class ExperimentLifecycleTargetStore { + private readonly payloads: FixedPayloadStore; + + constructor(chunks: Collection) { + this.payloads = new FixedPayloadStore(chunks); + } + + async prepare(input: { + projectId: string; + experimentId: string; + operationId: string; + targets: readonly ExperimentLifecycleTarget[]; + inlineDocument: ( + payload: FixedPayload, + ) => object; + now?: Date; + }): Promise> { + const seen = new Set(); + const canonical = [...input.targets] + .sort((left, right) => + left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id) + ) + .map((target) => { + const key = `${target.kind}\0${target.id}`; + if (seen.has(key)) { + throw new Error(`Duplicate Experiment lifecycle target ${target.kind}:${target.id}`); + } + seen.add(key); + return toState(target); + }); + return this.payloads.prepareRequests({ + identity: { + projectId: input.projectId, + experimentId: input.experimentId, + ownerId: input.operationId, + generation: 1, + }, + entries: canonical, + inlineDocument: input.inlineDocument, + now: input.now, + }); + } + + async read(input: { + projectId: string; + experimentId: string; + operationId: string; + payload: FixedPayload; + }): Promise { + const states = await this.payloads.readRequests( + { + projectId: input.projectId, + experimentId: input.experimentId, + ownerId: input.operationId, + generation: 1, + }, + input.payload, + ); + return states.map(fromState); + } + + async discard(payload: FixedPayload): Promise { + await this.payloads.discardUnpublished(payload); + } +} diff --git a/packages/shared/src/experiments/schemas.ts b/packages/shared/src/experiments/schemas.ts index 23e42478..5d861608 100644 --- a/packages/shared/src/experiments/schemas.ts +++ b/packages/shared/src/experiments/schemas.ts @@ -237,6 +237,45 @@ export const ExperimentDocumentSchema = z createdAt: z.coerce.date(), updatedAt: z.coerce.date(), finalizedAt: z.coerce.date().optional(), + deletedAt: z.coerce.date().optional(), + lifecycle: z + .object({ + operationId: IdSchema, + phase: z.enum(["deleting", "deleted", "restoring", "restored"]), + stateVersion: z.number().int().positive(), + targets: ExperimentFixedRequestPayloadSchema, + targetFingerprint: IdSchema, + progress: z + .object({ + expectedRequests: z.number().int().nonnegative(), + completedRequests: z.number().int().nonnegative(), + expectedReports: z.number().int().nonnegative(), + completedReports: z.number().int().nonnegative(), + }) + .strict(), + blocking: z + .array( + z + .object({ + kind: z.enum(["request", "report", "experiment"]), + id: IdSchema, + reason: z.enum([ + "target-missing", + "target-conflict", + "reconciliation-failed", + ]), + error: ErrorSchema.optional(), + }) + .strict(), + ) + .max(32), + deleteRequestedAt: z.coerce.date(), + deleteCompletedAt: z.coerce.date().optional(), + restoreRequestedAt: z.coerce.date().optional(), + restoreCompletedAt: z.coerce.date().optional(), + }) + .strict() + .optional(), }) .strict(); diff --git a/packages/shared/src/experiments/types.ts b/packages/shared/src/experiments/types.ts index 5941341b..88719b02 100644 --- a/packages/shared/src/experiments/types.ts +++ b/packages/shared/src/experiments/types.ts @@ -30,6 +30,47 @@ export type FixedPayload = | InlineFixedPayload | ChunkedFixedPayload; +export type ExperimentLifecyclePhase = + | "deleting" + | "deleted" + | "restoring" + | "restored"; + +export type ExperimentLifecycleTargetKind = "request" | "report"; + +export interface ExperimentLifecycleTarget { + kind: ExperimentLifecycleTargetKind; + id: string; +} + +export interface ExperimentLifecycleBlocker { + kind: ExperimentLifecycleTargetKind | "experiment"; + id: string; + reason: "target-missing" | "target-conflict" | "reconciliation-failed"; + error?: string; +} + +export interface ExperimentLifecycleProgress { + expectedRequests: number; + completedRequests: number; + expectedReports: number; + completedReports: number; +} + +export interface ExperimentLifecycleState { + operationId: string; + phase: ExperimentLifecyclePhase; + stateVersion: number; + targets: FixedPayload; + targetFingerprint: string; + progress: ExperimentLifecycleProgress; + blocking: ExperimentLifecycleBlocker[]; + deleteRequestedAt: Date; + deleteCompletedAt?: Date; + restoreRequestedAt?: Date; + restoreCompletedAt?: Date; +} + export type ExperimentDependencyState = | "waiting" | "ready" @@ -189,6 +230,8 @@ export interface ExperimentDocument { createdAt: Date; updatedAt: Date; finalizedAt?: Date; + deletedAt?: Date; + lifecycle?: ExperimentLifecycleState; } export interface ExperimentExecutionHistoryDocument diff --git a/packages/shared/src/queue/base-queue-processor.test.ts b/packages/shared/src/queue/base-queue-processor.test.ts index b4567994..a9be0e74 100644 --- a/packages/shared/src/queue/base-queue-processor.test.ts +++ b/packages/shared/src/queue/base-queue-processor.test.ts @@ -436,7 +436,10 @@ describe("BaseQueueProcessor error-path failure write", () => { await (proc as any).processMessage(msg); const [filter, update] = updateOne.mock.calls[0]; - expect(filter).toEqual({ _id: "doc-1" }); + expect(filter).toEqual({ + _id: "doc-1", + deletedAt: { $exists: false }, + }); expect(update.$set.status).toBe("done"); expect(update.$set.outcome).toBe("failed"); }); diff --git a/packages/shared/src/queue/base-queue-processor.ts b/packages/shared/src/queue/base-queue-processor.ts index 1ec8107c..35553654 100644 --- a/packages/shared/src/queue/base-queue-processor.ts +++ b/packages/shared/src/queue/base-queue-processor.ts @@ -235,6 +235,13 @@ export abstract class BaseQueueProcessor this.collection.updateOne( - { _id: documentId } as any, + { _id: documentId, deletedAt: { $exists: false } } as any, { $set: { status: "done", diff --git a/packages/shared/src/queue/queue-processor.test.ts b/packages/shared/src/queue/queue-processor.test.ts index 43da3385..5a76aad1 100644 --- a/packages/shared/src/queue/queue-processor.test.ts +++ b/packages/shared/src/queue/queue-processor.test.ts @@ -538,6 +538,7 @@ describe("CodingAgentQueueProcessor pre-processing failures", () => { }); expect(updateOne.mock.calls[1][0]).toEqual({ _id: requestId, + deletedAt: { $exists: false }, "run._id": runId, "run.status": "processing", "run.worker.instanceId": (qp as any).instanceId, diff --git a/packages/shared/src/queue/queue-processor.ts b/packages/shared/src/queue/queue-processor.ts index a37f9a4b..171f716f 100644 --- a/packages/shared/src/queue/queue-processor.ts +++ b/packages/shared/src/queue/queue-processor.ts @@ -358,6 +358,7 @@ export class CodingAgentQueueProcessor extends BaseQueueProcessor --project "$SCOPE_PROJECT" +scope experiment status --project "$SCOPE_PROJECT" +scope experiment restore --project "$SCOPE_PROJECT" +``` + +Add `--no-wait` to `delete` or `restore` to return as soon as the API accepts +the operation. Use `scope experiment list --include-deleted` or +`scope experiment get --include-deleted` to inspect recoverable +records. + ## Create from the CLI `scope experiment create` accepts the run configuration flags from @@ -158,6 +186,20 @@ Content-Type: application/json } ``` +Start deletion and poll its stable lifecycle operation: + +```http +DELETE /api/v1/experiments/experiment-123?projectId=project-123 +``` + +The API returns `202` with `operationId`, `phase`, `stateVersion`, request and +report progress, and blockers. Poll +`GET /api/v1/experiments/experiment-123/lifecycle?projectId=project-123`. +Restore uses +`POST /api/v1/experiments/experiment-123/restore?projectId=project-123` and +also returns `202`. Incompatible lifecycle intent returns a machine-readable +`409`. + ## Composition field mapping Each entry in `requests` uses the existing request-creation fields. diff --git a/website/src/openapi/scope-openapi.json b/website/src/openapi/scope-openapi.json index 5843ee31..9eed882d 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"}}},"CreateExperimentResponse":{"type":"object","properties":{"experiment":{"$ref":"#/components/schemas/ExperimentResponse"},"requestIds":{"type":"array","items":{"type":"string"}}},"required":["experiment","requestIds"]},"ExperimentResponse":{"type":"object","properties":{"experimentId":{"type":"string"},"projectId":{"type":"string"},"metadata":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":256},"description":{"type":"string","maxLength":4096},"createdBy":{"type":"string"}},"required":["name"],"additionalProperties":false},"revision":{"type":"object","properties":{"experimentRevisionId":{"type":"string"},"revisionNumber":{"type":"integer"},"memberCount":{"type":"integer"},"reportTemplateIds":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"}},"required":["experimentRevisionId","revisionNumber","memberCount","reportTemplateIds","createdAt"]},"currentExecution":{"type":"object","properties":{"experimentExecutionId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["empty","waiting","blocked","ready","processing","done","failed"]},"blockers":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"reason":{"type":"string"},"error":{"type":"string"}},"required":["handlerId","reason"]}},"progress":{"type":"object","additionalProperties":{"type":"object","properties":{"status":{"type":"string","enum":["queued","processing","done","failed"]},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["status","updatedAt"]}},"reportTemplates":{"type":"object","additionalProperties":{"type":"object","properties":{"templateId":{"type":"string"},"templateVersionId":{"type":"string"},"templateVersion":{"type":"integer","exclusiveMinimum":0},"templateContentFingerprint":{"type":"string"},"source":{"type":"string","enum":["always","linked","selected"]},"status":{"type":"string","enum":["waiting","not-applicable","unavailable","queued","processing","done","failed"]},"matchingRunCount":{"type":"integer","minimum":0},"blockers":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"runTemplateId":{"type":"string"},"reason":{"type":"string","enum":["run-template-unused","run-report-pending","run-report-failed","template-unavailable"]},"error":{"type":"string"}},"required":["reason"]}},"report":{"type":"object","properties":{"reportId":{"type":"string"},"url":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["reportId","url","updatedAt"]},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["templateId","source","status","matchingRunCount","blockers","updatedAt"]}},"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"},"discardedAt":{"type":["string","null"],"format":"date-time"},"discardReason":{"type":"string"},"report":{"type":"object","properties":{"reportId":{"type":"string"},"url":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["reportId","url","updatedAt"]}},"required":["experimentExecutionId","attemptNumber","readiness","blockers","progress","reportTemplates","createdAt","updatedAt"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["experimentId","projectId","metadata","revision","currentExecution","createdAt","updatedAt"]},"CreateExperimentInput":{"type":"object","properties":{"mode":{"type":"string","enum":["compose","empty","existing"]},"metadata":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":256},"description":{"type":"string","maxLength":4096}},"required":["name"],"additionalProperties":false},"requests":{"type":"array","items":{"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"},"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"}},"reportTemplateIds":{"type":"array","items":{"type":"string","minLength":1}},"workerType":{"type":"string","minLength":1}},"required":["scenario","workerType"]},"maxItems":250000,"default":[]},"requestIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":250000,"default":[]},"reportTemplateIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":1000,"default":[]}},"required":["mode","metadata"]},"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"]},"ResourceBindingSpec":{"anyOf":[{"type":"string"},{"type":"object","properties":{"ref":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref"]}]},"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"]},"ExperimentMembershipResponse":{"type":"object","properties":{"experiment":{"$ref":"#/components/schemas/ExperimentResponse"},"attachedRequestIds":{"type":"array","items":{"type":"string"}},"detachedRequestIds":{"type":"array","items":{"type":"string"}}},"required":["experiment","attachedRequestIds","detachedRequestIds"]},"ExperimentMembershipInput":{"type":"object","properties":{"expectedExperimentRevisionId":{"type":"string","minLength":1},"requestIds":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":250000}},"required":["expectedExperimentRevisionId","requestIds"],"additionalProperties":false},"ExperimentListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ExperimentResponse"}},"nextCursor":{"type":"string"}},"required":["data"]},"ExperimentReportTemplateSelectionInput":{"type":"object","properties":{"expectedExperimentRevisionId":{"type":"string","minLength":1},"reportTemplateIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":1000}},"required":["expectedExperimentRevisionId","reportTemplateIds"],"additionalProperties":false},"ExperimentExecutionListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ExperimentExecutionResponse"}},"nextCursor":{"type":"string"}},"required":["data"]},"ExperimentExecutionResponse":{"type":"object","properties":{"experimentExecutionId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["empty","waiting","blocked","ready","processing","done","failed"]},"blockers":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"reason":{"type":"string"},"error":{"type":"string"}},"required":["handlerId","reason"]}},"progress":{"type":"object","additionalProperties":{"type":"object","properties":{"status":{"type":"string","enum":["queued","processing","done","failed"]},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["status","updatedAt"]}},"reportTemplates":{"type":"object","additionalProperties":{"type":"object","properties":{"templateId":{"type":"string"},"templateVersionId":{"type":"string"},"templateVersion":{"type":"integer","exclusiveMinimum":0},"templateContentFingerprint":{"type":"string"},"source":{"type":"string","enum":["always","linked","selected"]},"status":{"type":"string","enum":["waiting","not-applicable","unavailable","queued","processing","done","failed"]},"matchingRunCount":{"type":"integer","minimum":0},"blockers":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"runTemplateId":{"type":"string"},"reason":{"type":"string","enum":["run-template-unused","run-report-pending","run-report-failed","template-unavailable"]},"error":{"type":"string"}},"required":["reason"]}},"report":{"type":"object","properties":{"reportId":{"type":"string"},"url":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["reportId","url","updatedAt"]},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["templateId","source","status","matchingRunCount","blockers","updatedAt"]}},"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"},"discardedAt":{"type":["string","null"],"format":"date-time"},"discardReason":{"type":"string"},"supersededAt":{"type":["string","null"],"format":"date-time"},"supersededByExperimentExecutionId":{"type":"string"},"supersededReason":{"type":"string","enum":["rollover","retry","membership","templates"]},"report":{"type":"object","properties":{"reportId":{"type":"string"},"url":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["reportId","url","updatedAt"]}},"required":["experimentExecutionId","attemptNumber","readiness","blockers","progress","reportTemplates","createdAt","updatedAt"]},"ReportResponse":{"type":"object","properties":{"_id":{"type":"string"},"requestId":{"type":"string"},"runId":{"type":"string"},"experimentId":{"type":"string"},"experimentExecutionId":{"type":"string"},"templateId":{"type":"string"},"templateVersionId":{"type":"string"},"templateVersion":{"type":"integer","exclusiveMinimum":0},"templateContentFingerprint":{"type":"string"},"reportVersion":{"type":"integer","exclusiveMinimum":0},"supersedesReportId":{"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"]},"RetryExperimentResponse":{"type":"object","properties":{"experimentId":{"type":"string"},"experimentExecutionId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["empty","waiting","blocked","ready","processing","done","failed"]},"blockers":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"reason":{"type":"string"},"error":{"type":"string"}},"required":["handlerId","reason"]}}},"required":["experimentId","experimentExecutionId","attemptNumber","readiness","blockers"]},"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"},"experimentId":{"type":"string"},"experimentExpectedRequestCount":{"type":"integer","minimum":0},"experimentRequestIndex":{"type":"integer","minimum":0},"experimentInitialRunId":{"type":"string"},"experimentFinalizedAt":{"type":["string","null"],"format":"date-time"},"experimentMembershipOperationId":{"type":"string"},"experimentMembershipRevisionId":{"type":"string"},"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"]},"ResourceBinding":{"type":"object","properties":{"ref":{"type":"string"},"revisionId":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref","revisionId","params"]},"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"}},"reportTemplateIds":{"type":"array","items":{"type":"string","minLength":1}}},"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"]},"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"},"deprecated":true,"description":"Deprecated for composition. Use POST /api/v1/experiments and its requests array."},"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"}},"reportTemplateIds":{"type":"array","items":{"type":"string","minLength":1}}},"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"},"experimentId":{"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"},"subject":{"$ref":"#/components/schemas/ReportTemplateSubject"},"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},"enabled":{"type":"boolean"},"currentVersionId":{"type":"string"},"currentVersion":{"type":"integer","exclusiveMinimum":0},"contentFingerprint":{"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","id","subject","name","userPrompt","enabled","currentVersionId","currentVersion","contentFingerprint","createdAt","projectId"]},"ReportTemplateSubject":{"type":"string","enum":["run","experiment"]},"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"]},{"type":"object","properties":{"type":{"type":"string","enum":["run-report-templates"]},"reportTemplateIds":{"type":"array","items":{"type":"string","minLength":1},"minItems":1}},"required":["type","reportTemplateIds"]}]},"ReportTemplateVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"projectId":{"type":"string"},"templateId":{"type":"string"},"version":{"type":"integer","exclusiveMinimum":0},"contentFingerprint":{"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"}},"required":["_id","projectId","templateId","version","contentFingerprint","userPrompt","createdAt"]},"CreateReportTemplateInput":{"type":"object","properties":{"id":{"type":"string"},"subject":{"$ref":"#/components/schemas/ReportTemplateSubject"},"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},"enabled":{"type":"boolean","default":true}},"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},"enabled":{"type":"boolean"}}},"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/experiments":{"post":{"tags":["Experiments"],"summary":"Create an experiment and zero or more new requests","description":"Canonical composition endpoint. Request and run IDs are minted before insertion; the exact revision/execution is then finalized idempotently.","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/CreateExperimentInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExperimentResponse"}}}},"400":{"description":"Invalid request selection"},"404":{"description":"Request not found"},"409":{"description":"Experiment identity conflict"}}},"get":{"tags":["Experiments"],"summary":"List experiments","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"},{"schema":{"type":"string","minLength":1},"required":false,"name":"after","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentListResponse"}}}}}}},"/api/v1/experiments/{id}/requests":{"post":{"tags":["Experiments"],"summary":"Move standalone requests into an Experiment","parameters":[{"schema":{"type":"string","minLength":1},"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/ExperimentMembershipInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentMembershipResponse"}}}},"404":{"description":"Experiment or request not found"},"409":{"description":"Stale revision or membership conflict"}}},"delete":{"tags":["Experiments"],"summary":"Remove requests from an Experiment","parameters":[{"schema":{"type":"string","minLength":1},"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/ExperimentMembershipInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentMembershipResponse"}}}},"404":{"description":"Experiment or request not found"},"409":{"description":"Stale revision or membership conflict"}}}},"/api/v1/experiments/{id}":{"get":{"tags":["Experiments"],"summary":"Get an experiment","parameters":[{"schema":{"type":"string","minLength":1},"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/ExperimentResponse"}}}},"404":{"description":"Experiment not found"}}}},"/api/v1/experiments/{id}/report-templates":{"put":{"tags":["Experiments"],"summary":"Replace manually selected Experiment report templates","description":"Selection is an unordered set. A real change creates a new Experiment revision and execution; duplicate- or order-only changes are no-ops.","parameters":[{"schema":{"type":"string","minLength":1},"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/ExperimentReportTemplateSelectionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentResponse"}}}},"400":{"description":"One or more template IDs are not selectable"},"404":{"description":"Experiment not found"},"409":{"description":"Stale Experiment revision"}}}},"/api/v1/experiments/{id}/executions":{"get":{"tags":["Experiments"],"summary":"List current and historical experiment executions","parameters":[{"schema":{"type":"string","minLength":1},"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":"integer","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1},"required":false,"name":"after","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentExecutionListResponse"}}}},"404":{"description":"Experiment not found"}}}},"/api/v1/experiments/{id}/executions/{experimentExecutionId}":{"get":{"tags":["Experiments"],"summary":"Get one exact experiment execution","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"experimentExecutionId","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/ExperimentExecutionResponse"}}}},"404":{"description":"Experiment execution not found"}}}},"/api/v1/experiments/{id}/executions/{experimentExecutionId}/reports":{"get":{"tags":["Reports"],"summary":"List reports for one exact Experiment execution","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"experimentExecutionId","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":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Experiment execution not found"}}}},"/api/v1/experiments/{id}/executions/{experimentExecutionId}/reports/{templateId}/retry":{"post":{"tags":["Reports"],"summary":"Retry one current Experiment template report","description":"Creates a new append-only report version in the same exact Experiment execution and preserves the frozen immutable template identity.","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"experimentExecutionId","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"templateId","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":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"404":{"description":"Experiment template report not found"},"409":{"description":"Execution is stale or report is already active"}}}},"/api/v1/experiments/{id}/executions/{experimentExecutionId}/reports/{reportId}/structured":{"get":{"tags":["Reports"],"summary":"Download structured JSON for an exact Experiment report","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"experimentExecutionId","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"reportId","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"},"404":{"description":"Experiment report or structured artifact not found"}}}},"/api/v1/experiments/{id}/executions/{experimentExecutionId}/logs":{"get":{"tags":["Experiments"],"summary":"Stream logs for one exact Experiment execution","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"experimentExecutionId","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","enum":["true","false"]},"required":false,"name":"fromStart","in":"query"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Experiment execution not found"}}}},"/api/v1/experiments/{id}/retry":{"post":{"tags":["Experiments"],"summary":"Retry current experiment processing","parameters":[{"schema":{"type":"string","minLength":1},"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":{"type":"object","properties":{"experimentExecutionId":{"type":"string","minLength":1}},"required":["experimentExecutionId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryExperimentResponse"}}}},"404":{"description":"Experiment not found"},"409":{"description":"Execution is stale or cannot be retried"}}}},"/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,"deprecated":true,"description":"Deprecated for composition. Use POST /api/v1/experiments and its requests array."},"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":{"type":"string"},"required":false,"name":"experimentId","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","experimentId","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":{"id":{"type":"string"},"deleted":{"type":"boolean","enum":[true]},"membershipRevision":{"type":"object","properties":{"experimentId":{"type":"string"},"experimentRevisionId":{"type":"string"},"revisionNumber":{"type":"integer"}},"required":["experimentId","experimentRevisionId","revisionNumber"]}},"required":["id","deleted"]}}}},"404":{"description":"Not found"},"409":{"description":"Membership changed"},"410":{"description":"Already deleted"}}}},"/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":"experimentId","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"},"notFound":{"type":"array","items":{"type":"string"}},"membershipRevision":{"type":"object","properties":{"experimentId":{"type":"string"},"experimentRevisionId":{"type":"string"},"revisionNumber":{"type":"integer"}},"required":["experimentId","experimentRevisionId","revisionNumber"]}},"required":["deleted","notFound"]}}}},"409":{"description":"Deletion spans Experiments or membership changed"}}}},"/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":"experimentId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"experimentExecutionId","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","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on 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":"q","in":"query"},{"schema":{"type":"string","enum":["run","experiment"]},"required":false,"name":"subject","in":"query"},{"schema":{"type":["boolean","null"],"default":false},"required":false,"name":"includeDisabled","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}}}}},"post":{"tags":["Report Templates"],"summary":"Create a versioned 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"}}}},"400":{"description":"Invalid template or linked run templates"},"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 a report template and append immutable content versions","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"}}}},"400":{"description":"Invalid template or linked run templates"},"404":{"description":"Report template not found"},"409":{"description":"Concurrent template update"}}},"delete":{"tags":["Report Templates"],"summary":"Soft-delete a 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/report-templates/{id}/versions":{"get":{"tags":["Report Templates"],"summary":"List immutable report template versions","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":"array","items":{"$ref":"#/components/schemas/ReportTemplateVersionResponse"}}}}}}}},"/api/v1/report-templates/{id}/versions/{versionId}":{"get":{"tags":["Report Templates"],"summary":"Get an exact immutable report template version","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"versionId","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/ReportTemplateVersionResponse"}}}},"404":{"description":"Template version 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"}}},"CreateExperimentResponse":{"type":"object","properties":{"experiment":{"$ref":"#/components/schemas/ExperimentResponse"},"requestIds":{"type":"array","items":{"type":"string"}}},"required":["experiment","requestIds"]},"ExperimentResponse":{"type":"object","properties":{"experimentId":{"type":"string"},"projectId":{"type":"string"},"metadata":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":256},"description":{"type":"string","maxLength":4096},"createdBy":{"type":"string"}},"required":["name"],"additionalProperties":false},"revision":{"type":"object","properties":{"experimentRevisionId":{"type":"string"},"revisionNumber":{"type":"integer"},"memberCount":{"type":"integer"},"reportTemplateIds":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"}},"required":["experimentRevisionId","revisionNumber","memberCount","reportTemplateIds","createdAt"]},"currentExecution":{"type":"object","properties":{"experimentExecutionId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["empty","waiting","blocked","ready","processing","done","failed"]},"blockers":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"reason":{"type":"string"},"error":{"type":"string"}},"required":["handlerId","reason"]}},"progress":{"type":"object","additionalProperties":{"type":"object","properties":{"status":{"type":"string","enum":["queued","processing","done","failed"]},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["status","updatedAt"]}},"reportTemplates":{"type":"object","additionalProperties":{"type":"object","properties":{"templateId":{"type":"string"},"templateVersionId":{"type":"string"},"templateVersion":{"type":"integer","exclusiveMinimum":0},"templateContentFingerprint":{"type":"string"},"source":{"type":"string","enum":["always","linked","selected"]},"status":{"type":"string","enum":["waiting","not-applicable","unavailable","queued","processing","done","failed"]},"matchingRunCount":{"type":"integer","minimum":0},"blockers":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"runTemplateId":{"type":"string"},"reason":{"type":"string","enum":["run-template-unused","run-report-pending","run-report-failed","template-unavailable"]},"error":{"type":"string"}},"required":["reason"]}},"report":{"type":"object","properties":{"reportId":{"type":"string"},"url":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["reportId","url","updatedAt"]},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["templateId","source","status","matchingRunCount","blockers","updatedAt"]}},"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"},"discardedAt":{"type":["string","null"],"format":"date-time"},"discardReason":{"type":"string"},"report":{"type":"object","properties":{"reportId":{"type":"string"},"url":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["reportId","url","updatedAt"]}},"required":["experimentExecutionId","attemptNumber","readiness","blockers","progress","reportTemplates","createdAt","updatedAt"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"lifecycle":{"type":"object","properties":{"operationId":{"type":"string"},"phase":{"type":"string","enum":["deleting","deleted","restoring","restored"]},"stateVersion":{"type":"integer","exclusiveMinimum":0},"progress":{"type":"object","properties":{"expectedRequests":{"type":"integer","minimum":0},"completedRequests":{"type":"integer","minimum":0},"expectedReports":{"type":"integer","minimum":0},"completedReports":{"type":"integer","minimum":0}},"required":["expectedRequests","completedRequests","expectedReports","completedReports"]},"blocking":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["request","report","experiment"]},"id":{"type":"string"},"reason":{"type":"string","enum":["target-missing","target-conflict","reconciliation-failed"]},"error":{"type":"string"}},"required":["kind","id","reason"]}},"deleteRequestedAt":{"type":["string","null"],"format":"date-time"},"deleteCompletedAt":{"type":["string","null"],"format":"date-time"},"restoreRequestedAt":{"type":["string","null"],"format":"date-time"},"restoreCompletedAt":{"type":["string","null"],"format":"date-time"}},"required":["operationId","phase","stateVersion","progress","blocking","deleteRequestedAt"]}},"required":["experimentId","projectId","metadata","revision","currentExecution","createdAt","updatedAt"]},"CreateExperimentInput":{"type":"object","properties":{"mode":{"type":"string","enum":["compose","empty","existing"]},"metadata":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":256},"description":{"type":"string","maxLength":4096}},"required":["name"],"additionalProperties":false},"requests":{"type":"array","items":{"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"},"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"}},"reportTemplateIds":{"type":"array","items":{"type":"string","minLength":1}},"workerType":{"type":"string","minLength":1}},"required":["scenario","workerType"]},"maxItems":250000,"default":[]},"requestIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":250000,"default":[]},"reportTemplateIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":1000,"default":[]}},"required":["mode","metadata"]},"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"]},"ResourceBindingSpec":{"anyOf":[{"type":"string"},{"type":"object","properties":{"ref":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref"]}]},"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"]},"ExperimentMembershipResponse":{"type":"object","properties":{"experiment":{"$ref":"#/components/schemas/ExperimentResponse"},"attachedRequestIds":{"type":"array","items":{"type":"string"}},"detachedRequestIds":{"type":"array","items":{"type":"string"}}},"required":["experiment","attachedRequestIds","detachedRequestIds"]},"ExperimentMembershipInput":{"type":"object","properties":{"expectedExperimentRevisionId":{"type":"string","minLength":1},"requestIds":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":250000}},"required":["expectedExperimentRevisionId","requestIds"],"additionalProperties":false},"ExperimentListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ExperimentResponse"}},"nextCursor":{"type":"string"}},"required":["data"]},"ExperimentLifecycleResponse":{"type":"object","properties":{"experimentId":{"type":"string"},"operationId":{"type":"string"},"phase":{"type":"string","enum":["deleting","deleted","restoring","restored"]},"stateVersion":{"type":"integer","exclusiveMinimum":0},"progress":{"type":"object","properties":{"expectedRequests":{"type":"integer","minimum":0},"completedRequests":{"type":"integer","minimum":0},"expectedReports":{"type":"integer","minimum":0},"completedReports":{"type":"integer","minimum":0}},"required":["expectedRequests","completedRequests","expectedReports","completedReports"]},"blocking":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["request","report","experiment"]},"id":{"type":"string"},"reason":{"type":"string"},"error":{"type":"string"}},"required":["kind","id","reason"]}},"deleteRequestedAt":{"type":["string","null"],"format":"date-time"},"deleteCompletedAt":{"type":["string","null"],"format":"date-time"},"restoreRequestedAt":{"type":["string","null"],"format":"date-time"},"restoreCompletedAt":{"type":["string","null"],"format":"date-time"}},"required":["experimentId","operationId","phase","stateVersion","progress","blocking","deleteRequestedAt"]},"ExperimentReportTemplateSelectionInput":{"type":"object","properties":{"expectedExperimentRevisionId":{"type":"string","minLength":1},"reportTemplateIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":1000}},"required":["expectedExperimentRevisionId","reportTemplateIds"],"additionalProperties":false},"ExperimentExecutionListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ExperimentExecutionResponse"}},"nextCursor":{"type":"string"}},"required":["data"]},"ExperimentExecutionResponse":{"type":"object","properties":{"experimentExecutionId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["empty","waiting","blocked","ready","processing","done","failed"]},"blockers":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"reason":{"type":"string"},"error":{"type":"string"}},"required":["handlerId","reason"]}},"progress":{"type":"object","additionalProperties":{"type":"object","properties":{"status":{"type":"string","enum":["queued","processing","done","failed"]},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["status","updatedAt"]}},"reportTemplates":{"type":"object","additionalProperties":{"type":"object","properties":{"templateId":{"type":"string"},"templateVersionId":{"type":"string"},"templateVersion":{"type":"integer","exclusiveMinimum":0},"templateContentFingerprint":{"type":"string"},"source":{"type":"string","enum":["always","linked","selected"]},"status":{"type":"string","enum":["waiting","not-applicable","unavailable","queued","processing","done","failed"]},"matchingRunCount":{"type":"integer","minimum":0},"blockers":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"runTemplateId":{"type":"string"},"reason":{"type":"string","enum":["run-template-unused","run-report-pending","run-report-failed","template-unavailable"]},"error":{"type":"string"}},"required":["reason"]}},"report":{"type":"object","properties":{"reportId":{"type":"string"},"url":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["reportId","url","updatedAt"]},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["templateId","source","status","matchingRunCount","blockers","updatedAt"]}},"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"},"discardedAt":{"type":["string","null"],"format":"date-time"},"discardReason":{"type":"string"},"supersededAt":{"type":["string","null"],"format":"date-time"},"supersededByExperimentExecutionId":{"type":"string"},"supersededReason":{"type":"string","enum":["rollover","retry","membership","templates"]},"report":{"type":"object","properties":{"reportId":{"type":"string"},"url":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["reportId","url","updatedAt"]}},"required":["experimentExecutionId","attemptNumber","readiness","blockers","progress","reportTemplates","createdAt","updatedAt"]},"ReportResponse":{"type":"object","properties":{"_id":{"type":"string"},"requestId":{"type":"string"},"runId":{"type":"string"},"experimentId":{"type":"string"},"experimentExecutionId":{"type":"string"},"templateId":{"type":"string"},"templateVersionId":{"type":"string"},"templateVersion":{"type":"integer","exclusiveMinimum":0},"templateContentFingerprint":{"type":"string"},"reportVersion":{"type":"integer","exclusiveMinimum":0},"supersedesReportId":{"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"},"deletedAt":{"type":["string","null"],"format":"date-time"},"deletionProvenance":{"type":"object","properties":{"kind":{"type":"string","enum":["experiment"]},"experimentId":{"type":"string","minLength":1},"operationId":{"type":"string","minLength":1}},"required":["kind","experimentId","operationId"],"additionalProperties":false},"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"]},"RetryExperimentResponse":{"type":"object","properties":{"experimentId":{"type":"string"},"experimentExecutionId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["empty","waiting","blocked","ready","processing","done","failed"]},"blockers":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"reason":{"type":"string"},"error":{"type":"string"}},"required":["handlerId","reason"]}}},"required":["experimentId","experimentExecutionId","attemptNumber","readiness","blockers"]},"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"},"deletionProvenance":{"type":"object","properties":{"kind":{"type":"string","enum":["experiment"]},"experimentId":{"type":"string","minLength":1},"operationId":{"type":"string","minLength":1}},"required":["kind","experimentId","operationId"],"additionalProperties":false},"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"},"experimentId":{"type":"string"},"experimentExpectedRequestCount":{"type":"integer","minimum":0},"experimentRequestIndex":{"type":"integer","minimum":0},"experimentInitialRunId":{"type":"string"},"experimentFinalizedAt":{"type":["string","null"],"format":"date-time"},"experimentMembershipOperationId":{"type":"string"},"experimentMembershipRevisionId":{"type":"string"},"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"]},"ResourceBinding":{"type":"object","properties":{"ref":{"type":"string"},"revisionId":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref","revisionId","params"]},"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"}},"reportTemplateIds":{"type":"array","items":{"type":"string","minLength":1}}},"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"]},"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"},"deprecated":true,"description":"Deprecated for composition. Use POST /api/v1/experiments and its requests array."},"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"}},"reportTemplateIds":{"type":"array","items":{"type":"string","minLength":1}}},"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"},"experimentId":{"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"},"subject":{"$ref":"#/components/schemas/ReportTemplateSubject"},"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},"enabled":{"type":"boolean"},"currentVersionId":{"type":"string"},"currentVersion":{"type":"integer","exclusiveMinimum":0},"contentFingerprint":{"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","id","subject","name","userPrompt","enabled","currentVersionId","currentVersion","contentFingerprint","createdAt","projectId"]},"ReportTemplateSubject":{"type":"string","enum":["run","experiment"]},"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"]},{"type":"object","properties":{"type":{"type":"string","enum":["run-report-templates"]},"reportTemplateIds":{"type":"array","items":{"type":"string","minLength":1},"minItems":1}},"required":["type","reportTemplateIds"]}]},"ReportTemplateVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"projectId":{"type":"string"},"templateId":{"type":"string"},"version":{"type":"integer","exclusiveMinimum":0},"contentFingerprint":{"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"}},"required":["_id","projectId","templateId","version","contentFingerprint","userPrompt","createdAt"]},"CreateReportTemplateInput":{"type":"object","properties":{"id":{"type":"string"},"subject":{"$ref":"#/components/schemas/ReportTemplateSubject"},"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},"enabled":{"type":"boolean","default":true}},"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},"enabled":{"type":"boolean"}}},"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/experiments":{"post":{"tags":["Experiments"],"summary":"Create an experiment and zero or more new requests","description":"Canonical composition endpoint. Request and run IDs are minted before insertion; the exact revision/execution is then finalized idempotently.","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/CreateExperimentInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExperimentResponse"}}}},"400":{"description":"Invalid request selection"},"404":{"description":"Request not found"},"409":{"description":"Experiment identity conflict"}}},"get":{"tags":["Experiments"],"summary":"List experiments","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"},{"schema":{"type":"string","minLength":1},"required":false,"name":"after","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentListResponse"}}}}}}},"/api/v1/experiments/{id}/requests":{"post":{"tags":["Experiments"],"summary":"Move standalone requests into an Experiment","parameters":[{"schema":{"type":"string","minLength":1},"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/ExperimentMembershipInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentMembershipResponse"}}}},"404":{"description":"Experiment or request not found"},"409":{"description":"Stale revision or membership conflict"}}},"delete":{"tags":["Experiments"],"summary":"Remove requests from an Experiment","parameters":[{"schema":{"type":"string","minLength":1},"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/ExperimentMembershipInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentMembershipResponse"}}}},"404":{"description":"Experiment or request not found"},"409":{"description":"Stale revision or membership conflict"}}}},"/api/v1/experiments/{id}":{"get":{"tags":["Experiments"],"summary":"Get an experiment","parameters":[{"schema":{"type":"string","minLength":1},"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","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentResponse"}}}},"404":{"description":"Experiment not found"}}},"delete":{"tags":["Experiments"],"summary":"Start or resume cascading Experiment deletion","description":"Returns 202 with a stable lifecycle operation ID. Poll the lifecycle endpoint until phase is deleted or blockers require intervention.","parameters":[{"schema":{"type":"string","minLength":1},"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":{"202":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentLifecycleResponse"}}}},"404":{"description":"Experiment not found"},"409":{"description":"An incompatible Experiment mutation is active"}}}},"/api/v1/experiments/{id}/restore":{"post":{"tags":["Experiments"],"summary":"Start or resume cascading Experiment restore","parameters":[{"schema":{"type":"string","minLength":1},"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":{"202":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentLifecycleResponse"}}}},"404":{"description":"Experiment not found"},"409":{"description":"Experiment is not deleted or another intent is active"}}}},"/api/v1/experiments/{id}/lifecycle":{"get":{"tags":["Experiments"],"summary":"Get and repair Experiment lifecycle progress","parameters":[{"schema":{"type":"string","minLength":1},"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/ExperimentLifecycleResponse"}}}},"404":{"description":"Experiment not found"},"409":{"description":"Experiment has no lifecycle operation"}}}},"/api/v1/experiments/{id}/report-templates":{"put":{"tags":["Experiments"],"summary":"Replace manually selected Experiment report templates","description":"Selection is an unordered set. A real change creates a new Experiment revision and execution; duplicate- or order-only changes are no-ops.","parameters":[{"schema":{"type":"string","minLength":1},"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/ExperimentReportTemplateSelectionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentResponse"}}}},"400":{"description":"One or more template IDs are not selectable"},"404":{"description":"Experiment not found"},"409":{"description":"Stale Experiment revision"}}}},"/api/v1/experiments/{id}/executions":{"get":{"tags":["Experiments"],"summary":"List current and historical experiment executions","parameters":[{"schema":{"type":"string","minLength":1},"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":"integer","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1},"required":false,"name":"after","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentExecutionListResponse"}}}},"404":{"description":"Experiment not found"}}}},"/api/v1/experiments/{id}/executions/{experimentExecutionId}":{"get":{"tags":["Experiments"],"summary":"Get one exact experiment execution","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"experimentExecutionId","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","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentExecutionResponse"}}}},"404":{"description":"Experiment execution not found"}}}},"/api/v1/experiments/{id}/executions/{experimentExecutionId}/reports":{"get":{"tags":["Reports"],"summary":"List reports for one exact Experiment execution","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"experimentExecutionId","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","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Experiment execution not found"}}}},"/api/v1/experiments/{id}/executions/{experimentExecutionId}/reports/{templateId}/retry":{"post":{"tags":["Reports"],"summary":"Retry one current Experiment template report","description":"Creates a new append-only report version in the same exact Experiment execution and preserves the frozen immutable template identity.","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"experimentExecutionId","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"templateId","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":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"404":{"description":"Experiment template report not found"},"409":{"description":"Execution is stale or report is already active"}}}},"/api/v1/experiments/{id}/executions/{experimentExecutionId}/reports/{reportId}/structured":{"get":{"tags":["Reports"],"summary":"Download structured JSON for an exact Experiment report","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"experimentExecutionId","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"reportId","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"},"404":{"description":"Experiment report or structured artifact not found"}}}},"/api/v1/experiments/{id}/executions/{experimentExecutionId}/logs":{"get":{"tags":["Experiments"],"summary":"Stream logs for one exact Experiment execution","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"experimentExecutionId","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","enum":["true","false"]},"required":false,"name":"fromStart","in":"query"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Experiment execution not found"}}}},"/api/v1/experiments/{id}/retry":{"post":{"tags":["Experiments"],"summary":"Retry current experiment processing","parameters":[{"schema":{"type":"string","minLength":1},"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":{"type":"object","properties":{"experimentExecutionId":{"type":"string","minLength":1}},"required":["experimentExecutionId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryExperimentResponse"}}}},"404":{"description":"Experiment not found"},"409":{"description":"Execution is stale or cannot be retried"}}}},"/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,"deprecated":true,"description":"Deprecated for composition. Use POST /api/v1/experiments and its requests array."},"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":{"type":"string"},"required":false,"name":"experimentId","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","experimentId","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":{"id":{"type":"string"},"deleted":{"type":"boolean","enum":[true]},"membershipRevision":{"type":"object","properties":{"experimentId":{"type":"string"},"experimentRevisionId":{"type":"string"},"revisionNumber":{"type":"integer"}},"required":["experimentId","experimentRevisionId","revisionNumber"]}},"required":["id","deleted"]}}}},"404":{"description":"Not found"},"409":{"description":"Membership changed"},"410":{"description":"Already deleted"}}}},"/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":"experimentId","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"},"notFound":{"type":"array","items":{"type":"string"}},"membershipRevision":{"type":"object","properties":{"experimentId":{"type":"string"},"experimentRevisionId":{"type":"string"},"revisionNumber":{"type":"integer"}},"required":["experimentId","experimentRevisionId","revisionNumber"]}},"required":["deleted","notFound"]}}}},"409":{"description":"Deletion spans Experiments or membership changed"}}}},"/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":"experimentId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"experimentExecutionId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"projectId","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/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","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on 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":"q","in":"query"},{"schema":{"type":"string","enum":["run","experiment"]},"required":false,"name":"subject","in":"query"},{"schema":{"type":["boolean","null"],"default":false},"required":false,"name":"includeDisabled","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}}}}},"post":{"tags":["Report Templates"],"summary":"Create a versioned 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"}}}},"400":{"description":"Invalid template or linked run templates"},"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 a report template and append immutable content versions","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"}}}},"400":{"description":"Invalid template or linked run templates"},"404":{"description":"Report template not found"},"409":{"description":"Concurrent template update"}}},"delete":{"tags":["Report Templates"],"summary":"Soft-delete a 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/report-templates/{id}/versions":{"get":{"tags":["Report Templates"],"summary":"List immutable report template versions","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":"array","items":{"$ref":"#/components/schemas/ReportTemplateVersionResponse"}}}}}}}},"/api/v1/report-templates/{id}/versions/{versionId}":{"get":{"tags":["Report Templates"],"summary":"Get an exact immutable report template version","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"versionId","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/ReportTemplateVersionResponse"}}}},"404":{"description":"Template version 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