diff --git a/apps/api/src/endpoints.test.ts b/apps/api/src/endpoints.test.ts index 6af7b8cb5..c105562c7 100644 --- a/apps/api/src/endpoints.test.ts +++ b/apps/api/src/endpoints.test.ts @@ -1955,6 +1955,44 @@ describe("API Endpoints", () => { expect(res.status).toBe(201); const doc = (mocks.collection.insertOne as any).mock.calls[0][0]; expect(doc).toHaveProperty("reasoningEffort", "high"); + expect(doc).toHaveProperty("submissionExpectedRequestCount", 1); + const submissionInsert = (mocks.submissionCollection.updateOne as any).mock.calls[0][1].$setOnInsert; + expect(submissionInsert.requestIds).toEqual([doc._id]); + expect(submissionInsert.run.requests).toEqual([ + { requestId: doc._id, runId: doc.run._id }, + ]); + }); + + it("creates one complete submission vector for count fan-out", async () => { + const res = await request(testServer()) + .post(`/api/v1/requests?worker=coder-acp-copilot&projectId=${TEST_PROJECT_ID}`) + .send({ + scenario: { task: "Build something", criteria: ["works"] }, + count: 3, + }); + + expect(res.status).toBe(201); + const docs = (mocks.collection.insertMany as any).mock.calls[0][0]; + expect(docs).toHaveLength(3); + expect(docs.every((doc: { submissionExpectedRequestCount: number }) => + doc.submissionExpectedRequestCount === 3)).toBe(true); + expect( + docs.map((doc: { submissionRequestIndex: number }) => doc.submissionRequestIndex), + ).toEqual([0, 1, 2]); + expect( + docs.every( + (doc: { submissionInitialRunId: string; run: { _id: string } }) => + doc.submissionInitialRunId === doc.run._id, + ), + ).toBe(true); + const submissionInsert = (mocks.submissionCollection.updateOne as any).mock.calls[0][1].$setOnInsert; + expect(submissionInsert.requestIds).toEqual(docs.map((doc: { _id: string }) => doc._id)); + expect(submissionInsert.run.requests).toEqual( + docs.map((doc: { _id: string; run: { _id: string } }) => ({ + requestId: doc._id, + runId: doc.run._id, + })), + ); }); it("profile reasoningEffort takes precedence over client-provided effort", async () => { @@ -2259,6 +2297,7 @@ describe("API Endpoints", () => { new Set(["base-profile", "var-profile"]), ); for (const doc of docs) { + expect(doc.submissionExpectedRequestCount).toBe(2); expect(Array.isArray(doc.gates)).toBe(true); const selectGate = doc.gates.find((g: { gate: string }) => g.gate === "select"); const buildGate = doc.gates.find((g: { gate: string }) => g.gate === "build"); @@ -2269,6 +2308,10 @@ describe("API Endpoints", () => { expect(buildGate.promptId).toBe("tp-build"); expect(buildGate.promptText).toBeUndefined(); } + const submissionInsert = (mocks.submissionCollection.updateOne as any).mock.calls[0][1].$setOnInsert; + expect(submissionInsert.requestIds).toEqual( + docs.map((doc: { _id: string }) => doc._id), + ); // Response surfaces the configured gate count. expect(res.body.gates).toBe(2); }); @@ -2426,6 +2469,9 @@ describe("API Endpoints", () => { expect(insertCall[0]).toHaveProperty("taskPromptId", "tp-123"); expect(insertCall[0]).toHaveProperty("model", "gpt-4o"); expect(insertCall[0]).toHaveProperty("maxIterations", 5); + expect(insertCall[0]).toHaveProperty("submissionExpectedRequestCount", 1); + const submissionInsert = (mocks.submissionCollection.updateOne as any).mock.calls[0][1].$setOnInsert; + expect(submissionInsert.requestIds).toEqual([insertCall[0]._id]); }); it("applies profile override: uses profile fields for worker, model, mcpServers, skillRevisions, extensions", async () => { @@ -2802,11 +2848,13 @@ describe("API Endpoints", () => { it("retries eligible requests and skips non-terminal ones", async () => { const doc1 = { _id: "req-1", + submissionId: "submission-1", workerType: "coder-acp-copilot", run: { _id: "run-1", attemptNumber: 1, status: "done", outcome: "failed" }, }; const doc2 = { _id: "req-2", + submissionId: "submission-1", workerType: "coder-acp-copilot", run: { _id: "run-2", attemptNumber: 1, status: "processing" }, }; @@ -2832,6 +2880,22 @@ describe("API Endpoints", () => { $set: expect.objectContaining({ agentVersion: "1.0.0" }), }), ); + expect(mocks.submissionCollection.updateMany).toHaveBeenNthCalledWith( + 1, + { _id: { $in: ["submission-1"] } }, + expect.objectContaining({ + $set: expect.objectContaining({ + "run.reconcileAfter": expect.any(Date), + }), + }), + ); + expect(mocks.submissionCollection.updateMany).toHaveBeenNthCalledWith( + 2, + { _id: { $in: ["submission-1"] } }, + expect.objectContaining({ + $unset: { "run.reconcileAfter": "" }, + }), + ); }); it("returns 400 when ids array is empty", async () => { diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 9803a0581..7f95ab2f5 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -47,6 +47,7 @@ import { registerInsightsRoutes } from "./routes/insights.js"; import { registerSecretsRoutes } from "./routes/secrets.js"; import { registerProfilesRoutes } from "./routes/profiles.js"; import { registerProjectsRoutes } from "./routes/projects.js"; +import { registerSubmissionsRoutes } from "./routes/submissions.js"; import { ProjectScopeError } from "./utils/project-scope.js"; import { registerUsersRoutes } from "./routes/users.js"; import { shutdownPortalCopilotClientPool } from "./copilot-client-pool.js"; @@ -59,6 +60,8 @@ import type { InsightDocument, RequestDocument, RunHistoryDocument, + SubmissionDocument, + SubmissionRunHistoryDocument, CodingAgentDocument, ModelDocument, McpServerDocument, @@ -97,6 +100,8 @@ let mongoClient: MongoClient; let db: Db; let collection: Collection; let runsCol: Collection; +let submissionCollection: Collection; +let submissionRunsCollection: Collection; let projectCollection: Collection; let projectStore: ProjectStore; let criteriaCollection: Collection; @@ -144,6 +149,8 @@ async function initializeClients(): Promise { db = mongoClient.db(mongoDatabase); collection = db.collection(mongoCollection); runsCol = db.collection("runs"); + submissionCollection = db.collection("submissions"); + submissionRunsCollection = db.collection("submission-runs"); projectCollection = db.collection("projects"); projectStore = new ProjectStore(projectCollection); criteriaCollection = db.collection("criteria"); @@ -277,6 +284,8 @@ const routeCtx: RouteContext = { get db() { return db; }, get requestCollection() { return collection; }, get runsCollection() { return runsCol; }, + get submissionCollection() { return submissionCollection; }, + get submissionRunsCollection() { return submissionRunsCollection; }, get projectCollection() { return projectCollection; }, get criteriaCollection() { return criteriaCollection; }, get promptFeatureCollection() { return promptFeatureCollection; }, @@ -338,6 +347,7 @@ app.use(createUserAccessMiddleware(() => userAccessResolver)); registerSecretsRoutes(routeCtx); registerSystemRoutes(routeCtx); registerProjectsRoutes(routeCtx); +registerSubmissionsRoutes(routeCtx); registerRequestsRoutes(routeCtx); registerRequestsCancelRoutes(routeCtx); registerRequestsLogsRoutes(routeCtx); @@ -424,6 +434,8 @@ export interface TestDependencies { db?: Db; collection?: Collection; runsCollection?: Collection; + submissionCollection?: Collection; + submissionRunsCollection?: Collection; criteriaCollection?: Collection; promptFeatureCollection?: Collection; reportCollection?: Collection; @@ -465,6 +477,8 @@ export function _injectTestDependencies(deps: TestDependencies): void { if (deps.db) db = deps.db; if (deps.collection) collection = deps.collection; if (deps.runsCollection) runsCol = deps.runsCollection; + if (deps.submissionCollection) submissionCollection = deps.submissionCollection; + if (deps.submissionRunsCollection) submissionRunsCollection = deps.submissionRunsCollection; if (deps.criteriaCollection) criteriaCollection = deps.criteriaCollection; if (deps.promptFeatureCollection) promptFeatureCollection = deps.promptFeatureCollection; if (deps.reportCollection) reportCollection = deps.reportCollection; diff --git a/apps/api/src/route-context.ts b/apps/api/src/route-context.ts index 580eae160..b675e8b9f 100644 --- a/apps/api/src/route-context.ts +++ b/apps/api/src/route-context.ts @@ -52,6 +52,8 @@ import type { ReportTemplateResponseSchema, RequestResponseSchema, RunHistoryDocumentSchema, + SubmissionDocumentSchema, + SubmissionRunHistoryDocumentSchema, } from "shared"; // ─── Document types (inferred from Zod schemas) ───────────────────────────── @@ -74,6 +76,8 @@ export type ReportTrigger = z.infer; export type ReportTemplateDocument = Omit, "_id">; export type RequestDocument = z.infer; export type RunHistoryDocument = z.infer; +export type SubmissionDocument = z.infer; +export type SubmissionRunHistoryDocument = z.infer; export type WorkerType = string; @@ -92,6 +96,8 @@ export interface RouteContext { db: Db; requestCollection: Collection; runsCollection: Collection; + submissionCollection: Collection; + submissionRunsCollection: Collection; projectCollection: Collection; criteriaCollection: Collection; promptFeatureCollection: Collection; diff --git a/apps/api/src/routes/requests/index.ts b/apps/api/src/routes/requests/index.ts index ffb48667d..070a583f3 100644 --- a/apps/api/src/routes/requests/index.ts +++ b/apps/api/src/routes/requests/index.ts @@ -37,6 +37,7 @@ import { isCriterionCompatibleWithGate, orderGates, computeTaskPromptId, + ensureNativeSubmission, } from "shared"; import type { ProfileDocument, @@ -844,6 +845,7 @@ apiRoute(ctx.app, ctx.registry, { const variationResults: Array<{ profileId: string; label?: string; ids: string[] }> = []; const agentsMdPromptId = await resolveAgentsMdPromptId(); const agentsMdFields = buildAgentsMdFields(agentsMdPromptId); + const expectedRequestCount = resolved.length * count; for (const r of resolved) { const newIds: string[] = []; @@ -878,6 +880,9 @@ apiRoute(ctx.app, ctx.registry, { profileVersionId: r.profileVersion.ref ?? r.profileVersion._id, ...(persistedVariationGates ? { gates: persistedVariationGates } : {}), submissionId, + submissionExpectedRequestCount: expectedRequestCount, + submissionRequestIndex: newDocs.length, + submissionInitialRunId: runId, ...agentsMdFields, run: { _id: runId, attemptNumber: 1, status: "pending", logsUrl: ctx.blobStorage.getLogsBlobUrl(`${requestId}/runs/${runId}/run.jsonl`) }, }; @@ -891,6 +896,20 @@ apiRoute(ctx.app, ctx.registry, { } await ctx.requestCollection.insertMany(newDocs); + await ensureNativeSubmission({ + collections: { submissions: ctx.submissionCollection }, + submissionId, + projectId, + requests: newDocs.map((doc) => ({ + requestId: doc._id, + runId: doc.run!._id, + })), + createdAt: newDocs[0]?.createdAt, + }); + await ctx.requestCollection.updateMany( + { _id: { $in: allNewIds }, submissionId }, + { $set: { submissionFinalizedAt: new Date() } }, + ); res.status(201).json({ ids: allNewIds, @@ -1274,7 +1293,7 @@ apiRoute(ctx.app, ctx.registry, { : undefined; // Persisted observation-criteria selection (issue #1156); deduped, evaluated - // by the pp-taxonomy post-processor. Absent/empty = no observations evaluated. + // by the pp-observations post-processor. Absent/empty = no observations evaluated. const persistedObservations: string[] | undefined = observationsProvided ? [...new Set(resolvedObservations)] : undefined; @@ -1316,6 +1335,9 @@ apiRoute(ctx.app, ctx.registry, { ...(persistedGates ? { gates: persistedGates } : {}), ...(persistedObservations ? { observations: persistedObservations } : {}), submissionId, + submissionExpectedRequestCount: count, + submissionRequestIndex: i, + submissionInitialRunId: runId, ...agentsMdFields, // Mint a distinct run id for the first attempt. Blob artifacts // are scoped under `{requestId}/runs/{runId}/...` so retries @@ -1327,6 +1349,20 @@ apiRoute(ctx.app, ctx.registry, { // Bulk insert all documents — scheduler will dispatch to queues await ctx.requestCollection.insertMany(newDocs); + await ensureNativeSubmission({ + collections: { submissions: ctx.submissionCollection }, + submissionId, + projectId, + requests: newDocs.map((doc) => ({ + requestId: doc._id, + runId: doc.run!._id, + })), + createdAt: newDocs[0]?.createdAt, + }); + await ctx.requestCollection.updateMany( + { _id: { $in: newIds }, submissionId }, + { $set: { submissionFinalizedAt: new Date() } }, + ); console.log(`Created ${count} ${mode} requests for ${workerType} (priority: ${requestedPriority ?? 0})`); @@ -1379,6 +1415,9 @@ apiRoute(ctx.app, ctx.registry, { ...(persistedGates ? { gates: persistedGates } : {}), ...(persistedObservations ? { observations: persistedObservations } : {}), submissionId, + submissionExpectedRequestCount: 1, + submissionRequestIndex: 0, + submissionInitialRunId: runId, ...agentsMdFields, // Mint a distinct run id for the first attempt. Blob artifacts // are scoped under `{requestId}/runs/{runId}/...` so retries @@ -1388,6 +1427,17 @@ apiRoute(ctx.app, ctx.registry, { // Store in MongoDB — scheduler will dispatch to queue await ctx.requestCollection.insertOne(requestDoc); + await ensureNativeSubmission({ + collections: { submissions: ctx.submissionCollection }, + submissionId, + projectId, + requests: [{ requestId, runId }], + createdAt: requestDoc.createdAt, + }); + await ctx.requestCollection.updateOne( + { _id: requestId, submissionId }, + { $set: { submissionFinalizedAt: new Date() } }, + ); console.log(`Created ${mode} request ${requestId} for ${workerType} (priority: ${requestedPriority ?? 0})`); @@ -2206,6 +2256,9 @@ apiRoute(ctx.app, ctx.registry, { ...(original.agentsMdPromptId ? { agentsMdPromptId: original.agentsMdPromptId } : {}), ...(original.agentsMdParentIds && original.agentsMdParentIds.length > 0 ? { agentsMdParentIds: original.agentsMdParentIds } : {}), submissionId, + submissionExpectedRequestCount: originalRuns.length * count, + submissionRequestIndex: newDocs.length, + submissionInitialRunId: runId, // Bulk re-submit creates a brand-new request — mint a distinct // run id for the first attempt so blob artifacts live under // `{requestId}/runs/{runId}/...` (independent of the original run). @@ -2219,6 +2272,20 @@ apiRoute(ctx.app, ctx.registry, { // Insert all new documents — scheduler will dispatch to queues if (newDocs.length > 0) { await ctx.requestCollection.insertMany(newDocs); + await ensureNativeSubmission({ + collections: { submissions: ctx.submissionCollection }, + submissionId, + projectId: newDocs[0].projectId, + requests: newDocs.map((doc) => ({ + requestId: doc._id, + runId: doc.run!._id, + })), + createdAt: newDocs[0].createdAt, + }); + await ctx.requestCollection.updateMany( + { _id: { $in: newIds }, submissionId }, + { $set: { submissionFinalizedAt: new Date() } }, + ); } console.log(`Bulk re-submitted ${newIds.length} runs from ${originalRuns.length} originals (count=${count})`); @@ -3129,6 +3196,24 @@ apiRoute(ctx.app, ctx.registry, { { _id: { $in: ids }, deletedAt: { $exists: false } }, ).toArray(); const requestMap = new Map(requests.map((r) => [r._id, r])); + const affectedSubmissionIds = [ + ...new Set( + requests.flatMap((request) => + request.submissionId ? [request.submissionId] : [], + ), + ), + ]; + if (affectedSubmissionIds.length > 0) { + await ctx.submissionCollection.updateMany( + { _id: { $in: affectedSubmissionIds } }, + { + $set: { + "run.reconcileAfter": new Date(Date.now() + 60_000), + updatedAt: new Date(), + }, + }, + ); + } const results: Array<{ requestId: string; runId?: string; attemptNumber?: number; error?: string }> = []; let retried = 0; @@ -3210,6 +3295,16 @@ apiRoute(ctx.app, ctx.registry, { retried++; } + if (affectedSubmissionIds.length > 0) { + await ctx.submissionCollection.updateMany( + { _id: { $in: affectedSubmissionIds } }, + { + $unset: { "run.reconcileAfter": "" }, + $set: { updatedAt: new Date() }, + }, + ); + } + res.status(201).json({ retried, skipped, results }); }, }); diff --git a/apps/api/src/routes/submissions.test.ts b/apps/api/src/routes/submissions.test.ts new file mode 100644 index 000000000..984bdd358 --- /dev/null +++ b/apps/api/src/routes/submissions.test.ts @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi"; +import express from "express"; +import request from "supertest"; +import { describe, expect, it, vi } from "vitest"; +import type { Collection, Db } from "mongodb"; +import type { + RequestDocument, + ServiceHandlerDocument, + SubmissionDocument, + SubmissionRunHistoryDocument, +} from "shared"; +import { computeSubmissionVectorFingerprint } from "shared"; +import type { RouteContext } from "../route-context.js"; +import { registerSubmissionsRoutes } from "./submissions.js"; + +function queryResult(items: T[]) { + return { + sort: () => queryResult(items), + limit: () => queryResult(items), + toArray: async () => items, + }; +} + +function buildApp(input: { + requests: RequestDocument[]; + submission: SubmissionDocument; + history?: SubmissionRunHistoryDocument[]; +}) { + let submission = structuredClone(input.submission); + const services: ServiceHandlerDocument[] = [ + { + _id: "pp-atif", + type: "post-process-handler", + version: 1, + queue: "post-processor", + selector: "atif", + autoBackfill: true, + dependsOn: [], + }, + { + _id: "pp-observations", + type: "post-process-handler", + version: 1, + queue: "post-processor", + selector: "observations", + autoBackfill: true, + dependsOn: ["pp-atif"], + }, + ]; + const submissionCollection = { + findOne: vi.fn(async () => structuredClone(submission)), + find: vi.fn(() => queryResult([structuredClone(submission)])), + findOneAndUpdate: vi.fn(async ( + filter: Record, + update: { $set: Record }, + ) => { + if (filter["run._id"] !== submission.run._id) return null; + submission = { + ...submission, + updatedAt: update.$set.updatedAt as Date, + run: { + ...submission.run, + dependencyProjection: update.$set[ + "run.dependencyProjection" + ] as SubmissionDocument["run"]["dependencyProjection"], + blocking: update.$set[ + "run.blocking" + ] as SubmissionDocument["run"]["blocking"], + updatedAt: update.$set["run.updatedAt"] as Date, + }, + }; + return structuredClone(submission); + }), + } as unknown as Collection; + const db = { + collection: vi.fn(() => ({ + find: vi.fn(() => + queryResult(services.filter((service) => service.type === "post-process-handler")), + ), + findOne: vi.fn(async () => null), + })), + } as unknown as Db; + const ctx = { + app: express(), + registry: new OpenAPIRegistry(), + db, + requestCollection: { + find: vi.fn(() => queryResult(input.requests)), + } as unknown as Collection, + submissionCollection, + submissionRunsCollection: { + find: vi.fn(() => queryResult(input.history ?? [])), + findOne: vi.fn(async (filter: { _id: string }) => + (input.history ?? []).find((run) => run._id === filter._id) ?? null, + ), + } as unknown as Collection, + } as unknown as RouteContext; + ctx.app.use(express.json()); + registerSubmissionsRoutes(ctx); + ctx.app.use( + ( + error: Error, + _req: express.Request, + res: express.Response, + _next: express.NextFunction, + ) => res.status(500).json({ error: error.message }), + ); + return ctx.app; +} + +describe("submission routes", () => { + const requestDocument: RequestDocument = { + _id: "request-1", + projectId: "project-1", + scenario: { task: "Implement feature", criteria: ["criterion"] }, + workerType: "worker", + createdAt: new Date(), + priority: 0, + run: { + _id: "run-1", + attemptNumber: 1, + status: "done", + outcome: "failed", + handlerStatus: { + "pp-observations": { status: "done", version: 1 }, + }, + }, + }; + const submissionDocument: SubmissionDocument = { + _id: "submission-1", + submissionId: "submission-1", + projectId: "project-1", + requestIds: ["request-1"], + createdAt: new Date(), + run: { + _id: "submission-run-1", + attemptNumber: 1, + vectorFingerprint: computeSubmissionVectorFingerprint([ + { requestId: "request-1", runId: "run-1" }, + ]), + requests: [{ requestId: "request-1", runId: "run-1" }], + createdAt: new Date(), + dependencyProjection: [], + blocking: [], + }, + }; + + it("returns readiness and the exact request/run vector", async () => { + const response = await request( + buildApp({ + requests: [requestDocument], + submission: submissionDocument, + }), + ).get("/api/v1/submissions/submission-1"); + + expect(response.status, JSON.stringify(response.body)).toBe(200); + expect(response.body).toMatchObject({ + submissionId: "submission-1", + submissionRunId: "submission-run-1", + readiness: "ready", + requests: [{ requestId: "request-1", runId: "run-1", outcome: "failed" }], + dependencyProjection: [{ + requestId: "request-1", + runId: "run-1", + handlerId: "pp-observations", + state: "ready", + }], + }); + }); + + it("lists current and immutable archived attempts", async () => { + const historical: SubmissionRunHistoryDocument = { + ...submissionDocument.run, + _id: "submission-run-0", + attemptNumber: 1, + reconcileAfter: new Date(), + rollover: { + nextSubmissionRunId: "internal-next", + vectorFingerprint: "internal-fingerprint", + requests: [{ requestId: "request-1", runId: "run-1" }], + reason: "request-vector-changed", + claimedAt: new Date(), + }, + submissionId: "submission-1", + projectId: "project-1", + }; + const response = await request( + buildApp({ + requests: [requestDocument], + submission: submissionDocument, + history: [historical], + }), + ).get("/api/v1/submissions/submission-1/runs"); + + expect(response.status).toBe(200); + expect(response.body.runs.map((run: { _id: string }) => run._id)).toEqual([ + "submission-run-1", + "submission-run-0", + ]); + expect(response.body.runs[1]).not.toHaveProperty("reconcileAfter"); + expect(response.body.runs[1]).not.toHaveProperty("rollover"); + }); + + it("rejects a manual retry targeting a stale submission run", async () => { + const response = await request( + buildApp({ + requests: [requestDocument], + submission: submissionDocument, + }), + ) + .post("/api/v1/submissions/submission-1/retry") + .send({ submissionRunId: "stale-submission-run" }); + + expect(response.status).toBe(409); + expect(response.body).toMatchObject({ + error: "Submission attempt is stale", + submissionRunId: "submission-run-1", + }); + }); + + it("returns exact waiting request/run pairs when retry dependencies are not ready", async () => { + const waitingRequest: RequestDocument = { + ...requestDocument, + run: { + _id: "run-1", + attemptNumber: 1, + status: "done", + handlerStatus: {}, + }, + }; + const response = await request( + buildApp({ + requests: [waitingRequest], + submission: submissionDocument, + }), + ) + .post("/api/v1/submissions/submission-1/retry") + .send({ submissionRunId: "submission-run-1" }); + + expect(response.status).toBe(409); + expect(response.body.blocking).toEqual([ + { + requestId: "request-1", + runId: "run-1", + handlerId: "pp-observations", + reason: "dependency-not-ready", + }, + ]); + }); +}); diff --git a/apps/api/src/routes/submissions.ts b/apps/api/src/routes/submissions.ts new file mode 100644 index 000000000..201aae5bd --- /dev/null +++ b/apps/api/src/routes/submissions.ts @@ -0,0 +1,245 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { z } from "zod"; +import { + SubmissionCoordinator, + SubmissionListResponseSchema, + SubmissionResponseSchema, + SubmissionRetryResponseSchema, + SubmissionRunResponseSchema, + type ServiceHandlerDocument, + deriveSubmissionReadiness, +} from "shared"; +import type { SubmissionDocument } from "../route-context.js"; +import type { RouteContext } from "../route-context.js"; +import { apiRoute } from "../openapi/api-route.js"; +import { ProjectIdQuerySchema, getQueryProjectId } from "../utils/project-scope.js"; + +function coordinator(ctx: RouteContext): SubmissionCoordinator { + return new SubmissionCoordinator({ + requests: ctx.requestCollection, + submissions: ctx.submissionCollection, + submissionRuns: ctx.submissionRunsCollection, + services: ctx.db.collection("services"), + }); +} + +function toRunResponse( + run: T, +): Omit { + const { + reconcileAfter: _reconcileAfter, + rollover: _rollover, + ...response + } = run; + return response; +} + +function retryBlocking(submission: SubmissionDocument) { + if (submission.run.blocking.length > 0) return submission.run.blocking; + return submission.run.dependencyProjection + .filter((dependency) => dependency.state !== "ready") + .map((dependency) => ({ + requestId: dependency.requestId, + runId: dependency.runId, + handlerId: dependency.handlerId, + reason: "dependency-not-ready" as const, + ...(dependency.error ? { error: dependency.error } : {}), + })); +} + +async function toResponse(ctx: RouteContext, submission: SubmissionDocument) { + const requestDocs = await ctx.requestCollection + .find({ + _id: { $in: submission.requestIds }, + projectId: submission.projectId, + deletedAt: { $exists: false }, + }) + .toArray(); + const byId = new Map(requestDocs.map((request) => [request._id, request])); + const requests = submission.run.requests.flatMap((ref) => { + const request = byId.get(ref.requestId); + if (!request) return []; + return [{ + requestId: request._id, + runId: ref.runId, + task: request.scenario.task, + status: request.run?.status ?? "unknown", + ...(request.run?.outcome ? { outcome: request.run.outcome } : {}), + workerType: request.workerType, + ...(request.model ? { model: request.model } : {}), + ...(request.profileId ? { profileId: request.profileId } : {}), + }]; + }); + return { + submissionId: submission._id, + projectId: submission.projectId, + requestIds: submission.requestIds, + createdAt: submission.createdAt, + ...(submission.updatedAt ? { updatedAt: submission.updatedAt } : {}), + submissionRunId: submission.run._id, + attemptNumber: submission.run.attemptNumber, + readiness: deriveSubmissionReadiness(submission.run), + requests, + dependencyProjection: submission.run.dependencyProjection, + blocking: submission.run.blocking, + ...(submission.run.handlerStatus + ? { handlerProgress: submission.run.handlerStatus } + : {}), + ...(submission.run.report ? { report: submission.run.report } : {}), + ...(submission.run.supersededAt + ? { supersededAt: submission.run.supersededAt } + : {}), + ...(submission.run.supersededBySubmissionRunId + ? { supersededBySubmissionRunId: submission.run.supersededBySubmissionRunId } + : {}), + ...(submission.run.supersededReason + ? { supersededReason: submission.run.supersededReason } + : {}), + }; +} + +export function registerSubmissionsRoutes(ctx: RouteContext): void { + apiRoute(ctx.app, ctx.registry, { + method: "get", + path: "/api/v1/submissions", + tags: ["Submissions"], + summary: "List submissions", + query: ProjectIdQuerySchema.extend({ + limit: z.coerce.number().int().min(1).max(100).default(50), + }), + response: SubmissionListResponseSchema, + handler: async (req, res) => { + const projectId = getQueryProjectId(req); + const docs = await ctx.submissionCollection + .find({ projectId }) + .sort({ createdAt: -1, _id: -1 }) + .limit(req.query.limit) + .toArray(); + res.json({ + submissions: await Promise.all(docs.map((doc) => toResponse(ctx, doc))), + }); + }, + }); + + apiRoute(ctx.app, ctx.registry, { + method: "get", + path: "/api/v1/submissions/:id", + tags: ["Submissions"], + summary: "Get a submission", + params: z.object({ id: z.string() }), + response: SubmissionResponseSchema, + errorResponses: { 404: { description: "Submission not found" } }, + handler: async (req, res) => { + const reconciled = await coordinator(ctx).reconcile(req.params.id); + if (!reconciled) { + res.status(404).json({ error: "Submission not found" }); + return; + } + res.json(await toResponse(ctx, reconciled.submission)); + }, + }); + + apiRoute(ctx.app, ctx.registry, { + method: "get", + path: "/api/v1/submissions/:id/runs", + tags: ["Submissions"], + summary: "List current and historical submission attempts", + params: z.object({ id: z.string() }), + response: z.object({ runs: z.array(SubmissionRunResponseSchema) }), + errorResponses: { 404: { description: "Submission not found" } }, + handler: async (req, res) => { + const submission = await ctx.submissionCollection.findOne({ _id: req.params.id }); + if (!submission) { + res.status(404).json({ error: "Submission not found" }); + return; + } + const historical = await ctx.submissionRunsCollection + .find({ submissionId: req.params.id }) + .sort({ attemptNumber: -1 }) + .toArray(); + res.json({ + runs: [toRunResponse(submission.run), ...historical.map(toRunResponse)], + }); + }, + }); + + apiRoute(ctx.app, ctx.registry, { + method: "get", + path: "/api/v1/submissions/:id/runs/:submissionRunId", + tags: ["Submissions"], + summary: "Get one submission attempt", + params: z.object({ id: z.string(), submissionRunId: z.string() }), + response: SubmissionRunResponseSchema, + errorResponses: { 404: { description: "Submission attempt not found" } }, + handler: async (req, res) => { + const submission = await ctx.submissionCollection.findOne({ _id: req.params.id }); + if (submission?.run._id === req.params.submissionRunId) { + res.json(toRunResponse(submission.run)); + return; + } + const historical = await ctx.submissionRunsCollection.findOne({ + _id: req.params.submissionRunId, + submissionId: req.params.id, + }); + if (!historical) { + res.status(404).json({ error: "Submission attempt not found" }); + return; + } + res.json(toRunResponse(historical)); + }, + }); + + apiRoute(ctx.app, ctx.registry, { + method: "post", + path: "/api/v1/submissions/:id/retry", + tags: ["Submissions"], + summary: "Retry current submission processing", + params: z.object({ id: z.string() }), + body: z.object({ submissionRunId: z.string() }), + response: SubmissionRetryResponseSchema, + errorResponses: { + 404: { description: "Submission not found" }, + 409: { description: "Submission attempt is stale or dependencies are blocked" }, + }, + handler: async (req, res) => { + const current = await ctx.submissionCollection.findOne({ _id: req.params.id }); + if (!current) { + res.status(404).json({ error: "Submission not found" }); + return; + } + if (current.run._id !== req.body.submissionRunId) { + res.status(409).json({ + error: "Submission attempt is stale", + submissionRunId: current.run._id, + blocking: retryBlocking(current), + }); + return; + } + const result = await coordinator(ctx).retryCurrent( + req.params.id, + req.body.submissionRunId, + ); + if (!result) { + res.status(404).json({ error: "Submission not found" }); + return; + } + if (result.readiness === "blocked" || result.readiness === "waiting") { + res.status(409).json({ + error: "Submission dependencies are not ready", + submissionRunId: result.submission.run._id, + blocking: retryBlocking(result.submission), + }); + return; + } + res.status(201).json({ + submissionId: result.submission._id, + submissionRunId: result.submission.run._id, + attemptNumber: result.submission.run.attemptNumber, + readiness: result.readiness, + blocking: result.submission.run.blocking, + }); + }, + }); +} diff --git a/apps/api/src/test-helpers.ts b/apps/api/src/test-helpers.ts index 023c46b73..74447d6e7 100644 --- a/apps/api/src/test-helpers.ts +++ b/apps/api/src/test-helpers.ts @@ -49,11 +49,29 @@ export function createMockCollection(docs: T[] = insertedCount: 1, acknowledged: true, }), - updateOne: vi.fn().mockResolvedValue({ - matchedCount: 1, - modifiedCount: 1, - acknowledged: true, - }), + updateOne: vi.fn().mockImplementation( + async ( + filter: Record, + update: { $setOnInsert?: T }, + options?: { upsert?: boolean }, + ) => { + const existing = data.find((doc) => doc._id === filter._id); + if (!existing && options?.upsert && update.$setOnInsert) { + data.push({ ...update.$setOnInsert }); + return { + matchedCount: 0, + modifiedCount: 0, + upsertedCount: 1, + acknowledged: true, + }; + } + return { + matchedCount: 1, + modifiedCount: 1, + acknowledged: true, + }; + }, + ), updateMany: vi.fn().mockResolvedValue({ matchedCount: 1, modifiedCount: 1, @@ -204,6 +222,8 @@ export function createAllMockDependencies() { const db = createMockDb(); const collection = createMockCollection(); const runsCollection = createMockCollection(); + const submissionCollection = createMockCollection(); + const submissionRunsCollection = createMockCollection(); const criteriaCollection = createMockCollection(); const promptFeatureCollection = createMockCollection(); const reportCollection = createMockCollection(); @@ -232,6 +252,8 @@ export function createAllMockDependencies() { db, collection, runsCollection, + submissionCollection, + submissionRunsCollection, criteriaCollection, promptFeatureCollection, reportCollection, @@ -262,6 +284,8 @@ export function createAllMockDependencies() { db: ReturnType; collection: Collection; runsCollection: Collection; + submissionCollection: Collection; + submissionRunsCollection: Collection; criteriaCollection: Collection; promptFeatureCollection: Collection; reportCollection: Collection; diff --git a/apps/cli/src/commands/submission.ts b/apps/cli/src/commands/submission.ts new file mode 100644 index 000000000..835e7f3ed --- /dev/null +++ b/apps/cli/src/commands/submission.ts @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Command } from "commander"; +import { configureHelp } from "../utils/helpFormatter.js"; +import { formatData, isMachineReadable } from "../utils/formatters.js"; +import type { DisplayField, OutputFormat } from "../utils/types.js"; +import { + getDefaultApiUrl, + withOutputOption, + withProjectOption, +} from "../utils/shared.js"; +import { requireProjectId } from "../utils/config.js"; +import { apiFetch } from "../utils/api-client.js"; +import { errorText, label, successText, value } from "../utils/style.js"; + +interface SubmissionSummary { + submissionId: string; + submissionRunId: string; + attemptNumber: number; + readiness: string; + createdAt: string; + requestIds: string[]; + requests: Array<{ + requestId: string; + runId: string; + task: string; + status: string; + outcome?: string; + }>; + blocking: Array<{ + requestId: string; + runId: string; + reason: string; + }>; +} +async function responseJson(response: Response): Promise { + return response.json() as Promise; +} + +async function throwApiError(response: Response): Promise { + const body = await responseJson<{ error?: string }>(response); + throw new Error(body.error ?? `HTTP ${response.status}`); +} + +const fields: DisplayField[] = [ + { key: "submissionId", label: "Submission" }, + { key: "attemptNumber", label: "Attempt" }, + { key: "readiness", label: "Readiness" }, + { + key: "requestIds", + label: "Runs", + formatter: (row: Record) => + String((row.requestIds as string[] | undefined)?.length ?? 0), + }, + { key: "createdAt", label: "Created" }, +]; + +export function registerSubmissionCommands(program: Command): void { + const submission = program + .command("submission") + .description("Inspect and retry grouped run submissions") + .action(() => submission.help()); + configureHelp(submission); + + withProjectOption( + withOutputOption( + submission + .command("list") + .description("List submissions") + .option("-u, --url ", "API base URL", getDefaultApiUrl()), + ), + ).action(async (options) => { + try { + const projectId = requireProjectId(options.project); + const response = await apiFetch(options.url, "/submissions", { projectId }); + if (!response.ok) await throwApiError(response); + const body = await responseJson<{ submissions: SubmissionSummary[] }>(response); + console.log( + formatData(body.submissions, fields, (options.output ?? "table") as OutputFormat), + ); + } catch (error) { + console.error(errorText("Error:"), error instanceof Error ? error.message : error); + process.exitCode = 1; + } + }); + + withOutputOption( + submission + .command("get") + .description("Get a submission and its current request/run vector") + .argument("", "Submission ID") + .option("-u, --url ", "API base URL", getDefaultApiUrl()), + ).action(async (id: string, options) => { + try { + const response = await apiFetch( + options.url, + `/submissions/${encodeURIComponent(id)}`, + ); + if (!response.ok) await throwApiError(response); + const doc = await responseJson(response); + const format = (options.output ?? "table") as OutputFormat; + if (isMachineReadable(format)) { + console.log(formatData([doc], fields, format)); + return; + } + console.log(`${label("Submission:")} ${value(doc.submissionId)}`); + console.log(`${label("Attempt:")} ${value(String(doc.attemptNumber))}`); + console.log(`${label("Readiness:")} ${value(doc.readiness)}`); + console.log(`${label("Submission run:")} ${value(doc.submissionRunId)}`); + for (const request of doc.requests) { + console.log( + ` ${value(request.requestId)} ${request.status}` + + `${request.outcome ? `/${request.outcome}` : ""} run=${request.runId}`, + ); + } + for (const blocking of doc.blocking) { + console.log( + ` blocked ${blocking.requestId}/${blocking.runId}: ${blocking.reason}`, + ); + } + } catch (error) { + console.error(errorText("Error:"), error instanceof Error ? error.message : error); + process.exitCode = 1; + } + }); + + withOutputOption( + submission + .command("runs") + .description("List current and archived submission attempts") + .argument("", "Submission ID") + .option("-u, --url ", "API base URL", getDefaultApiUrl()), + ).action(async (id: string, options) => { + try { + const response = await apiFetch( + options.url, + `/submissions/${encodeURIComponent(id)}/runs`, + ); + if (!response.ok) await throwApiError(response); + const body = await responseJson<{ + runs: Array<{ + _id: string; + attemptNumber: number; + createdAt: string; + requests: unknown[]; + supersededReason?: string; + }>; + }>(response); + const runFields: DisplayField[] = [ + { key: "_id", label: "Submission Run" }, + { key: "attemptNumber", label: "Attempt" }, + { + key: "requests", + label: "Runs", + formatter: (row: Record) => + String((row.requests as unknown[] | undefined)?.length ?? 0), + }, + { key: "supersededReason", label: "Superseded" }, + { key: "createdAt", label: "Created" }, + ]; + console.log( + formatData(body.runs, runFields, (options.output ?? "table") as OutputFormat), + ); + } catch (error) { + console.error(errorText("Error:"), error instanceof Error ? error.message : error); + process.exitCode = 1; + } + }); + + submission + .command("retry") + .description("Retry the current submission processing attempt") + .argument("", "Submission ID") + .requiredOption("--submission-run ", "Current submission run ID") + .option("-u, --url ", "API base URL", getDefaultApiUrl()) + .action(async (id: string, options) => { + try { + const response = await apiFetch( + options.url, + `/submissions/${encodeURIComponent(id)}/retry`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ submissionRunId: options.submissionRun }), + }, + ); + if (!response.ok) await throwApiError(response); + const result = await responseJson<{ + submissionRunId: string; + attemptNumber: number; + readiness: string; + }>(response); + console.log( + successText( + `Submission retry started: attempt ${result.attemptNumber} (${result.submissionRunId}, ${result.readiness})`, + ), + ); + } catch (error) { + console.error(errorText("Error:"), error instanceof Error ? error.message : error); + process.exitCode = 1; + } + }); +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 399f12266..6d59d10a6 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -25,6 +25,7 @@ import { registerTaskPromptCommands } from "./commands/task-prompt.js"; import { registerProfileCommands } from "./commands/profile.js"; import { registerProjectCommands } from "./commands/project.js"; import { registerSecretCommands } from "./commands/secret.js"; +import { registerSubmissionCommands } from "./commands/submission.js"; import { registerUpdateCommand } from "./commands/update.js"; import { checkForUpdates } from "./utils/update-check.js"; import { errorText } from "./utils/style.js"; @@ -70,6 +71,7 @@ configureHelp(program); registerSecretCommands(program); registerProjectCommands(program); registerRunCommands(program); +registerSubmissionCommands(program); registerCriteriaCommands(program); registerPromptFeatureCommands(program); registerReportCommands(program); diff --git a/apps/judge/src/agent-trajectory.test.ts b/apps/judge/src/agent-trajectory.test.ts index 8bb870184..ac4d713b8 100644 --- a/apps/judge/src/agent-trajectory.test.ts +++ b/apps/judge/src/agent-trajectory.test.ts @@ -48,7 +48,7 @@ async function call( /** * Realistic ATIF blob: system + user step, then agent steps that install, - * use, and uninstall `left-pad`. Mirrors the real ATIF shape pp-taxonomy + * use, and uninstall `left-pad`. Mirrors the real ATIF shape pp-observations * downloads (steps carry their own tool_calls AND the matching * observation.results, paired by source_call_id === tool_call_id). */ diff --git a/apps/judge/src/observation-trajectory.integration.test.ts b/apps/judge/src/observation-trajectory.integration.test.ts index 066c40da3..d8b89150d 100644 --- a/apps/judge/src/observation-trajectory.integration.test.ts +++ b/apps/judge/src/observation-trajectory.integration.test.ts @@ -34,7 +34,7 @@ const hasToken = !!process.env.GITHUB_TOKEN; const JUDGE_MODEL = process.env.JUDGE_MODEL || "gpt-5.4-mini"; /** - * Realistic ATIF trajectory (the exact shape pp-taxonomy downloads and forwards + * Realistic ATIF trajectory (the exact shape pp-observations downloads and forwards * via `fromAtif`): a system + user step, then agent steps that install * `left-pad`, use it, and uninstall it before finishing. Each agent step pairs * its `tool_calls[]` with the matching same-step `observation.results[]` by diff --git a/apps/portal/src/App.tsx b/apps/portal/src/App.tsx index 26d5f1d74..56e6a2d79 100644 --- a/apps/portal/src/App.tsx +++ b/apps/portal/src/App.tsx @@ -6,6 +6,7 @@ import { Layout } from "@/components/Layout"; import { RunsList } from "@/pages/RunsList"; import { RunDetail } from "@/pages/RunDetail"; import { SubmitRun } from "@/pages/SubmitRun"; +import { SubmissionDetail } from "@/pages/SubmissionDetail"; import { CriteriaList } from "@/pages/CriteriaList"; import { CriterionDetail } from "@/pages/CriterionDetail"; import { CriterionPreviewPanel } from "@/pages/CriterionPreviewPanel"; @@ -85,6 +86,7 @@ export function App() { } /> } /> + } /> }> } /> diff --git a/apps/portal/src/components/SubmissionStatusCard.stories.tsx b/apps/portal/src/components/SubmissionStatusCard.stories.tsx new file mode 100644 index 000000000..0c465e7e9 --- /dev/null +++ b/apps/portal/src/components/SubmissionStatusCard.stories.tsx @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { SubmissionStatusCard } from "./SubmissionStatusCard"; +import type { Submission } from "@/types"; + +const submission: Submission = { + submissionId: "submission-123", + projectId: "project-123", + requestIds: ["request-1", "request-2"], + createdAt: new Date().toISOString(), + submissionRunId: "submission-run-1", + attemptNumber: 2, + readiness: "processing", + requests: [ + { + requestId: "request-1", + runId: "run-1", + task: "Implement the feature", + status: "done", + outcome: "succeeded", + workerType: "coder-acp-copilot", + }, + { + requestId: "request-2", + runId: "run-2", + task: "Implement the feature", + status: "done", + outcome: "failed", + workerType: "coder-acp-copilot", + }, + ], + dependencyProjection: [ + { + requestId: "request-1", + runId: "run-1", + handlerId: "pp-observations", + state: "ready", + }, + { + requestId: "request-2", + runId: "run-2", + handlerId: "pp-observations", + state: "ready", + }, + ], + blocking: [], + handlerProgress: { + "pp-readout": { status: "processing", version: 1 }, + }, +}; +const meta = { + title: "Submissions/SubmissionStatusCard", + component: SubmissionStatusCard, + args: { submission }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Processing: Story = {}; + +export const Blocked: Story = { + args: { + submission: { + ...submission, + readiness: "blocked", + blocking: [ + { + requestId: "request-2", + runId: "run-2", + handlerId: "pp-observations", + reason: "dependency-failed", + }, + ], + }, + }, +}; diff --git a/apps/portal/src/components/SubmissionStatusCard.test.tsx b/apps/portal/src/components/SubmissionStatusCard.test.tsx new file mode 100644 index 000000000..b735e762e --- /dev/null +++ b/apps/portal/src/components/SubmissionStatusCard.test.tsx @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// @vitest-environment happy-dom +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; +import type { Submission } from "@/types"; +import { SubmissionStatusCard } from "./SubmissionStatusCard"; + +afterEach(cleanup); + +describe("SubmissionStatusCard", () => { + it("renders readiness and exact observation progress", () => { + const submission: Submission = { + submissionId: "submission-1", + projectId: "project-1", + requestIds: ["request-1", "request-2"], + createdAt: new Date().toISOString(), + submissionRunId: "submission-run-1", + attemptNumber: 3, + readiness: "blocked", + requests: [ + { + requestId: "request-1", + runId: "run-1", + task: "Task", + status: "done", + workerType: "worker", + }, + { + requestId: "request-2", + runId: "run-2", + task: "Task", + status: "done", + workerType: "worker", + }, + ], + dependencyProjection: [ + { + requestId: "request-1", + runId: "run-1", + handlerId: "pp-observations", + state: "ready", + }, + { + requestId: "request-2", + runId: "run-2", + handlerId: "pp-observations", + state: "failed", + }, + ], + blocking: [ + { + requestId: "request-2", + runId: "run-2", + handlerId: "pp-observations", + reason: "dependency-failed", + }, + ], + }; + + render(); + + expect(screen.getByText("blocked")).toBeDefined(); + expect(screen.getByText("3")).toBeDefined(); + expect(screen.getByText("1/2")).toBeDefined(); + }); +}); diff --git a/apps/portal/src/components/SubmissionStatusCard.tsx b/apps/portal/src/components/SubmissionStatusCard.tsx new file mode 100644 index 000000000..f711bdd1c --- /dev/null +++ b/apps/portal/src/components/SubmissionStatusCard.tsx @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import type { Submission } from "@/types"; + +const readinessVariant: Record< + Submission["readiness"], + "default" | "secondary" | "destructive" | "outline" +> = { + waiting: "secondary", + ready: "outline", + blocked: "destructive", + processing: "default", + done: "default", + failed: "destructive", +}; + +export function SubmissionStatusCard({ + submission, +}: { + submission: Submission; +}) { + const ready = submission.dependencyProjection.filter( + (dependency) => dependency.state === "ready", + ).length; + return ( + + + Processing + + {submission.readiness} + + + +
+
Attempt
+
{submission.attemptNumber}
+
+
+
Observations ready
+
+ {ready}/{submission.requests.length} +
+
+
+
Blocking runs
+
{submission.blocking.length}
+
+
+
+ ); +} diff --git a/apps/portal/src/lib/api.ts b/apps/portal/src/lib/api.ts index 32e1d4a7d..1258ac38e 100644 --- a/apps/portal/src/lib/api.ts +++ b/apps/portal/src/lib/api.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import type { Run, RunState, CriteriaDocument, CriteriaGraphData, GeneratePromptResponse, AnalysisResponse, PromptFeatureDocument, Report, BulkReportStatus, BulkReportSummary, ReportTemplate, ReportTrigger, ReportTemplateSystemPrompt, KeyDocument, KeyValidationResult, CreateKeyRequest, UpdateKeyRequest, CodingAgent, AgentVersion, McpServerDocument, CreateMcpServerRequest, UpdateMcpServerRequest, BulkResubmitOverrides, Insight, InsightWithReference, TaskPrompt, TaskPromptFeatureExtractionResult, Model, FeatureFlag, SkillDocument, SkillSearchResult, SkillDiscoveryResult, SkillRevisionDocument, CodebaseDocument, CodebaseRevisionDocument, CodebaseSourceType, ResourceDocument, ResourceRevisionDocument, ResourceBindingSpec, CreateResourceBody, CreateResourceRevisionBody, ExtensionDocument, ExtensionSearchResult, ExtensionVersionInfo, MdpResponse, AccountDocument, CreateAccountRequest, UpdateAccountRequest, ProfileWithVersion, ProfileVersionDocument, ProfileDocument, RunGroup, RunFacetsResponse, CursorPaginatedResponse, IterationOp, GateConfig, GateId, PromptType, RunSortField, RunSortDir, CriterionKind, TaxonomyElementId, CriterionSubject } from "@/types"; +import type { Run, RunState, CriteriaDocument, CriteriaGraphData, GeneratePromptResponse, AnalysisResponse, PromptFeatureDocument, Report, BulkReportStatus, BulkReportSummary, ReportTemplate, ReportTrigger, ReportTemplateSystemPrompt, KeyDocument, KeyValidationResult, CreateKeyRequest, UpdateKeyRequest, CodingAgent, AgentVersion, McpServerDocument, CreateMcpServerRequest, UpdateMcpServerRequest, BulkResubmitOverrides, Insight, InsightWithReference, TaskPrompt, TaskPromptFeatureExtractionResult, Model, FeatureFlag, SkillDocument, SkillSearchResult, SkillDiscoveryResult, SkillRevisionDocument, CodebaseDocument, CodebaseRevisionDocument, CodebaseSourceType, ResourceDocument, ResourceRevisionDocument, ResourceBindingSpec, CreateResourceBody, CreateResourceRevisionBody, ExtensionDocument, ExtensionSearchResult, ExtensionVersionInfo, MdpResponse, AccountDocument, CreateAccountRequest, UpdateAccountRequest, ProfileWithVersion, ProfileVersionDocument, ProfileDocument, RunGroup, RunFacetsResponse, CursorPaginatedResponse, IterationOp, GateConfig, GateId, PromptType, RunSortField, RunSortDir, CriterionKind, TaxonomyElementId, CriterionSubject, Submission } from "@/types"; import type { Project, CreateProjectRequest, UpdateProjectRequest } from "@/types"; import { qs } from "./url"; @@ -231,7 +231,7 @@ export const api = { gates?: GateConfig[]; codebase?: string; codebaseRevisionId?: string; - }): Promise<(Run & { message: string }) | { ids: string[]; count: number; message: string }> => { + }): Promise<(Run & { message: string; submissionId: string }) | { ids: string[]; count: number; message: string; submissionId: string }> => { const { worker, ...payload } = body; const url = worker ? `/requests?worker=${encodeURIComponent(worker)}` : `/requests`; return request(url, { @@ -240,6 +240,27 @@ export const api = { }, { scoped: true }); }, + listSubmissions: (): Promise<{ submissions: Submission[] }> => + request("/submissions", undefined, { scoped: true }), + + getSubmission: (id: string): Promise => + request(`/submissions/${id}`), + + retrySubmission: ( + id: string, + submissionRunId: string, + ): Promise<{ + submissionId: string; + submissionRunId: string; + attemptNumber: number; + readiness: Submission["readiness"]; + blocking: Submission["blocking"]; + }> => + request(`/submissions/${id}/retry`, { + method: "POST", + body: JSON.stringify({ submissionRunId }), + }), + /** Soft-delete a run */ deleteRun: (id: string): Promise<{ id: string; deleted: boolean }> => { return request(`/requests/${id}`, { method: "DELETE" }); diff --git a/apps/portal/src/lib/enrichment.test.ts b/apps/portal/src/lib/enrichment.test.ts index 879bc5b11..9e4b19024 100644 --- a/apps/portal/src/lib/enrichment.test.ts +++ b/apps/portal/src/lib/enrichment.test.ts @@ -33,59 +33,59 @@ describe("deriveEnrichmentStatus", () => { expect(deriveEnrichmentStatus(run, false)).toBe("done"); }); - it("does NOT flip to done after pp-atif while pp-taxonomy is still pending (the bug)", () => { - // Observations configured, pp-atif done, pp-taxonomy not yet dispatched. + it("does NOT flip to done after pp-atif while pp-observations is still pending (the bug)", () => { + // Observations configured, pp-atif done, pp-observations not yet dispatched. const beforeDispatch = makeRun({ postProcessorStatus: "done", // legacy scalar already flipped — must be ignored handlerStatus: { "pp-atif": { status: "done" } }, }); expect(deriveEnrichmentStatus(beforeDispatch, true)).toBe("processing"); - // pp-taxonomy queued. + // pp-observations queued. const queued = makeRun({ postProcessorStatus: "done", - handlerStatus: { "pp-atif": { status: "done" }, "pp-taxonomy": { status: "queued" } }, + handlerStatus: { "pp-atif": { status: "done" }, "pp-observations": { status: "queued" } }, }); expect(deriveEnrichmentStatus(queued, true)).toBe("processing"); - // pp-taxonomy processing. + // pp-observations processing. const processing = makeRun({ postProcessorStatus: "done", - handlerStatus: { "pp-atif": { status: "done" }, "pp-taxonomy": { status: "processing" } }, + handlerStatus: { "pp-atif": { status: "done" }, "pp-observations": { status: "processing" } }, }); expect(deriveEnrichmentStatus(processing, true)).toBe("processing"); }); - it("flips to done only once both pp-atif and pp-taxonomy are done", () => { + it("flips to done only once both pp-atif and pp-observations are done", () => { const run = makeRun({ - handlerStatus: { "pp-atif": { status: "done" }, "pp-taxonomy": { status: "done" } }, + handlerStatus: { "pp-atif": { status: "done" }, "pp-observations": { status: "done" } }, }); expect(deriveEnrichmentStatus(run, true)).toBe("done"); }); it("reports failed when a handler failed and nothing is still in flight", () => { const run = makeRun({ - handlerStatus: { "pp-atif": { status: "done" }, "pp-taxonomy": { status: "failed" } }, + handlerStatus: { "pp-atif": { status: "done" }, "pp-observations": { status: "failed" } }, }); expect(deriveEnrichmentStatus(run, true)).toBe("failed"); }); it("prefers processing over failed while another handler is still running", () => { const run = makeRun({ - handlerStatus: { "pp-atif": { status: "processing" }, "pp-taxonomy": { status: "failed" } }, + handlerStatus: { "pp-atif": { status: "processing" }, "pp-observations": { status: "failed" } }, }); expect(deriveEnrichmentStatus(run, true)).toBe("processing"); }); - it("treats a no-op pp-taxonomy (no observations) as part of the DAG", () => { - // No observations configured, but pp-taxonomy still runs as a no-op handler. + it("treats a no-op pp-observations (no observations) as part of the DAG", () => { + // No observations configured, but pp-observations still runs as a no-op handler. const stillRunning = makeRun({ - handlerStatus: { "pp-atif": { status: "done" }, "pp-taxonomy": { status: "processing" } }, + handlerStatus: { "pp-atif": { status: "done" }, "pp-observations": { status: "processing" } }, }); expect(deriveEnrichmentStatus(stillRunning, false)).toBe("processing"); const allDone = makeRun({ - handlerStatus: { "pp-atif": { status: "done" }, "pp-taxonomy": { status: "done" } }, + handlerStatus: { "pp-atif": { status: "done" }, "pp-observations": { status: "done" } }, }); expect(deriveEnrichmentStatus(allDone, false)).toBe("done"); }); diff --git a/apps/portal/src/lib/enrichment.ts b/apps/portal/src/lib/enrichment.ts index 99f5bfebc..756a510d9 100644 --- a/apps/portal/src/lib/enrichment.ts +++ b/apps/portal/src/lib/enrichment.ts @@ -12,14 +12,14 @@ export type PostProcessorStatus = "queued" | "processing" | "done" | "failed"; * * Each post-processor handler overwrites that scalar on its own completion, so * it flips to "done" as soon as the FIRST handler (pp-atif) finishes — even - * while pp-taxonomy is still computing observations. The "Enriched" badge (and + * while pp-observations is still computing observations. The "Enriched" badge (and * the run-detail poller) must instead stay "Enriching…" until every handler in * the DAG is terminal. * - * Known topology: pp-atif always runs; pp-taxonomy is expected whenever the run + * Known topology: pp-atif always runs; pp-observations is expected whenever the run * has configured observations. We treat taxonomy as still-pending until - * pp-taxonomy reaches a terminal state, which also covers the brief window - * after pp-atif finishes but before the scheduler has dispatched pp-taxonomy + * pp-observations reaches a terminal state, which also covers the brief window + * after pp-atif finishes but before the scheduler has dispatched pp-observations * (so it isn't yet present in `handlerStatus`). * * Falls back to the legacy `postProcessorStatus` scalar for runs that predate @@ -36,7 +36,7 @@ export function deriveEnrichmentStatus( return run.postProcessorStatus; } const statuses = Object.values(handlerStatus).map((h) => h?.status); - const taxonomyStatus = handlerStatus["pp-taxonomy"]?.status; + const taxonomyStatus = handlerStatus["pp-observations"]?.status; const taxonomyPending = hasConfiguredObservations && taxonomyStatus !== "done" && taxonomyStatus !== "failed"; if (taxonomyPending || statuses.some((s) => s === "queued" || s === "processing")) { diff --git a/apps/portal/src/pages/RunDetail.tsx b/apps/portal/src/pages/RunDetail.tsx index 8cd9ab00e..9eaed350f 100644 --- a/apps/portal/src/pages/RunDetail.tsx +++ b/apps/portal/src/pages/RunDetail.tsx @@ -531,7 +531,7 @@ export function RunDetail() { (activeRun?.observationResults?.length ?? 0) > 0; const hasConfiguredObservations = (run?.observations?.length ?? 0) > 0; // Aggregate enrichment status across the whole post-processing DAG so the - // "Enriched" badge waits for pp-taxonomy instead of flipping after pp-atif. + // "Enriched" badge waits for pp-observations instead of flipping after pp-atif. const enrichmentStatus = useMemo( () => deriveEnrichmentStatus(activeRun, hasConfiguredObservations), [activeRun, hasConfiguredObservations], @@ -905,6 +905,19 @@ export function RunDetail() { /> )} + {run.submissionId && ( + + {run.submissionId.slice(0, 8)} + + )} + /> + )} {run.maxIterations && } {(() => { const totalDuration = activeRun?.turns?.reduce((sum, t) => sum + (t.durationMs ?? 0), 0); diff --git a/apps/portal/src/pages/RunsList.tsx b/apps/portal/src/pages/RunsList.tsx index 27b685df5..c7257fc97 100644 --- a/apps/portal/src/pages/RunsList.tsx +++ b/apps/portal/src/pages/RunsList.tsx @@ -1350,7 +1350,12 @@ export function RunsList() { hidden: columnVisibility.isHidden("submission"), cell: (r) => r.submissionId ? ( - + event.stopPropagation()} + > + + ) : ( — ), @@ -2666,31 +2671,36 @@ export function RunsList() { ? (profileNameById.get(groupKey) ?? formatId(groupKey)) : groupKey; return ( -
+ {groupBy === "submissionId" && groupKey !== "no-submission" && ( + + View submission + + )} {runs.length} run{runs.length !== 1 ? "s" : ""} - - {expanded ? "Click to collapse" : "Click to expand"} -
- + ); }, }} diff --git a/apps/portal/src/pages/SubmissionDetail.tsx b/apps/portal/src/pages/SubmissionDetail.tsx new file mode 100644 index 000000000..a4a7b67bf --- /dev/null +++ b/apps/portal/src/pages/SubmissionDetail.tsx @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { ArrowLeft, FileText, RotateCcw } from "lucide-react"; +import { Link, useParams } from "react-router-dom"; +import { toast } from "sonner"; +import { SubmissionStatusCard } from "@/components/SubmissionStatusCard"; +import { ShortId } from "@/components/ShortId"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { api } from "@/lib/api"; +import { formatDate } from "@/lib/utils"; + +export function SubmissionDetail() { + const { id } = useParams<{ id: string }>(); + const queryClient = useQueryClient(); + const submissionQuery = useQuery({ + queryKey: ["submission", id], + queryFn: () => api.getSubmission(id!), + enabled: Boolean(id), + refetchInterval: (query) => { + const readiness = query.state.data?.readiness; + return readiness === "done" || readiness === "failed" || readiness === "blocked" + ? false + : 5_000; + }, + }); + const retry = useMutation({ + mutationFn: () => + api.retrySubmission( + submissionQuery.data!.submissionId, + submissionQuery.data!.submissionRunId, + ), + onSuccess: () => { + toast.success("Submission processing restarted"); + void queryClient.invalidateQueries({ queryKey: ["submission", id] }); + }, + onError: (error) => { + toast.error(error instanceof Error ? error.message : "Retry failed"); + }, + }); + + if (submissionQuery.isLoading) { + return
Loading submission…
; + } + if (submissionQuery.isError || !submissionQuery.data) { + return ( +
+

Submission could not be loaded.

+
+ ); + } + const submission = submissionQuery.data; + return ( +
+
+
+ +
+

Submission

+ +
+

+ Created {formatDate(submission.createdAt)} +

+
+ +
+ + + + {submission.blocking.length > 0 && ( + + + Blocking dependencies + + + {submission.blocking.map((blocking) => ( +
+ + {blocking.requestId.slice(0, 8)} + + {blocking.reason} + + {blocking.runId} + +
+ ))} +
+
+ )} + + + + Runs + + + {submission.requests.map((request) => { + const dependency = submission.dependencyProjection.find( + (item) => + item.requestId === request.requestId && item.runId === request.runId, + ); + return ( + +
+
+ + {request.task} +
+
+ {request.requestId} · {request.runId} +
+
+
+ {request.status} + + {dependency?.state ?? "waiting"} + +
+ + ); + })} +
+
+
+ ); +} diff --git a/apps/portal/src/pages/SubmitRun.tsx b/apps/portal/src/pages/SubmitRun.tsx index ec6b639ea..b6c51baeb 100644 --- a/apps/portal/src/pages/SubmitRun.tsx +++ b/apps/portal/src/pages/SubmitRun.tsx @@ -667,13 +667,7 @@ export function SubmitRun() { const submitMutation = useMutation({ mutationFn: api.submitRun, onSuccess: (data) => { - if ("ids" in data && data.ids.length > 1) { - navigate("/runs"); - } else if ("id" in data) { - navigate(`/runs/${data.id}`); - } else { - navigate("/runs"); - } + navigate(`/submissions/${data.submissionId}`); }, }); diff --git a/apps/portal/src/types.ts b/apps/portal/src/types.ts index 51a91eae0..1ee0d61f0 100644 --- a/apps/portal/src/types.ts +++ b/apps/portal/src/types.ts @@ -126,7 +126,7 @@ export interface OsInfo { /** * Per-handler post-processing status in the DAG (issue #1147). Keys in - * `RunState.handlerStatus` are handler IDs (e.g. "pp-atif", "pp-taxonomy"). + * `RunState.handlerStatus` are handler IDs (e.g. "pp-atif", "pp-observations"). */ export interface HandlerRunStatus { status: "queued" | "processing" | "done" | "failed"; @@ -173,7 +173,7 @@ export interface RunState { postProcessorStatus?: "queued" | "processing" | "done" | "failed"; /** * Per-handler post-processing status keyed by handler ID (e.g. "pp-atif", - * "pp-taxonomy"). Drives the aggregate "Enriched" badge so it only flips to + * "pp-observations"). Drives the aggregate "Enriched" badge so it only flips to * done once every handler in the DAG is terminal — not after the first * handler (pp-atif) completes. The legacy `postProcessorStatus` scalar only * tracks pp-atif and is kept for backward compatibility. @@ -225,6 +225,71 @@ export interface Run { gateSummaries?: GateRunSummary[]; } +export type SubmissionReadiness = + | "waiting" + | "ready" + | "blocked" + | "processing" + | "done" + | "failed"; + +export interface SubmissionDependencyProjection { + requestId: string; + runId: string; + handlerId: string; + state: "waiting" | "ready" | "failed" | "blocked"; + version?: number; + updatedAt?: string; + error?: string; +} + +export interface SubmissionBlockingDetail { + requestId: string; + runId: string; + handlerId: string; + reason: + | "request-missing" + | "run-changed" + | "dependency-failed" + | "dependency-blocked"; + error?: string; +} + +export interface SubmissionRequestSummary { + requestId: string; + runId: string; + task: string; + status: string; + outcome?: string; + workerType: string; + model?: string; + profileId?: string; +} + +export interface Submission { + submissionId: string; + projectId: string; + requestIds: string[]; + createdAt: string; + updatedAt?: string; + submissionRunId: string; + attemptNumber: number; + readiness: SubmissionReadiness; + requests: SubmissionRequestSummary[]; + dependencyProjection: SubmissionDependencyProjection[]; + blocking: SubmissionBlockingDetail[]; + handlerProgress?: Record< + string, + { + status: "queued" | "processing" | "done" | "failed"; + version?: number; + updatedAt?: string; + error?: string; + } + >; + report?: { reportId?: string; url?: string; updatedAt?: string }; +} + export interface CursorPaginatedResponse { data: T[]; limit: number; diff --git a/apps/scheduler/src/handler-dispatcher.ts b/apps/scheduler/src/handler-dispatcher.ts index 4ff1f8275..d744092c3 100644 --- a/apps/scheduler/src/handler-dispatcher.ts +++ b/apps/scheduler/src/handler-dispatcher.ts @@ -3,8 +3,13 @@ import { Collection, Db } from "mongodb"; import { QueueClient } from "@azure/storage-queue"; -import type { RequestDocument, HandlerServiceDocument, HandlerRunStatus } from "shared"; -import { DependencyGraph, withRetry } from "shared"; +import type { + RequestDocument, + HandlerServiceDocument, + HandlerRunStatus, + ServiceHandlerDocument, +} from "shared"; +import { DependencyGraph, isHandlerDagDrained, withRetry } from "shared"; import type { RetryOptions } from "shared"; import type { NotifyHandler } from "./notify-routes.js"; @@ -151,7 +156,27 @@ export class HandlerDispatcher implements NotifyHandler { await this.maybeTriggerReports(requestId, runId); } - async registerHandler(doc: HandlerServiceDocument): Promise { + async registerHandler(doc: ServiceHandlerDocument): Promise { + if (doc.type === "submission-handler") { + const postProcessHandlers = await this.loadHandlers(true); + for (const dependency of doc.dependsOn) { + if (!postProcessHandlers.some((handler) => handler._id === dependency)) { + throw new Error( + `Submission handler '${doc._id}' depends on unknown run handler '${dependency}'`, + ); + } + } + const { _id, ...fields } = doc; + await this.db.collection("services").updateOne( + { _id } as any, + { $set: { ...fields, dependsOn: [...fields.dependsOn], updatedAt: new Date() } } as any, + { upsert: true }, + ); + console.log( + `[HandlerDispatcher] Registered submission handler ${_id} (version=${doc.version}, queue=${doc.queue}, dependsOn=[${doc.dependsOn.join(", ")}])`, + ); + return; + } // Validate the resulting topology has no cycles before persisting. Read // fresh (bypass the cache) so validation reflects any concurrent registrations. const existing = await this.loadHandlers(true); @@ -183,43 +208,12 @@ export class HandlerDispatcher implements NotifyHandler { // ── Report trigger on DAG drain ───────────────────────────────────── - /** - * A handler is "terminal" for a run when it can produce no further work: - * it has succeeded (`done`), failed (`failed`), or is permanently blocked - * because one of its transitive dependencies failed (so it can never run). - */ - private isHandlerTerminal( - handlerId: string, - graph: DependencyGraph, - handlerStatus: Record | undefined, - ): boolean { - const status = handlerStatus?.[handlerId]?.status; - if (status === "done" || status === "failed") return true; - - // Not yet dispatched/processed — terminal only if blocked by a failed - // ancestor (any transitive dependency that failed). - if (status === undefined) { - for (const ancestorId of graph.getAncestors(handlerId)) { - if (handlerStatus?.[ancestorId]?.status === "failed") return true; - } - } - - // "queued" or "processing" — still active. - return false; - } - - /** - * The DAG is drained for a run when every registered handler is terminal. - * Vacuously true when there are no handlers. - */ private isDagDrained( handlers: HandlerServiceDocument[], - graph: DependencyGraph, + _graph: DependencyGraph, handlerStatus: Record | undefined, ): boolean { - return handlers.every((h) => - this.isHandlerTerminal(h._id, graph, handlerStatus), - ); + return isHandlerDagDrained(handlers, handlerStatus); } /** @@ -239,12 +233,11 @@ export class HandlerDispatcher implements NotifyHandler { if ((run as any).reportsTriggeredAt) return; const handlers = await this.loadHandlers(); - const graph = this.buildGraph(handlers); const handlerStatus = (run as any).handlerStatus as | Record | undefined; - if (!this.isDagDrained(handlers, graph, handlerStatus)) return; + if (!this.isDagDrained(handlers, this.buildGraph(handlers), handlerStatus)) return; // Atomically claim the trigger so concurrent notify/poll paths fire once. const claimed = await this.collection.findOneAndUpdate( diff --git a/apps/scheduler/src/index.ts b/apps/scheduler/src/index.ts index 101750a30..9fe0fbf79 100644 --- a/apps/scheduler/src/index.ts +++ b/apps/scheduler/src/index.ts @@ -8,10 +8,16 @@ import { DefaultAzureCredential } from "@azure/identity"; import { RequestScheduler } from "./request-scheduler.js"; import { PostProcessorDispatcher } from "./post-processor-dispatcher.js"; import { HandlerDispatcher } from "./handler-dispatcher.js"; +import { SubmissionDispatcher } from "./submission-dispatcher.js"; import { createHttpServer, type NotifyHandler } from "./notify-routes.js"; import { StuckRunReaper } from "./stuck-run-reaper.js"; import { RedisHeartbeatStore, type HeartbeatStore } from "shared"; -import type { CodingAgentDocument, RequestDocument } from "shared"; +import type { + CodingAgentDocument, + RequestDocument, + SubmissionDocument, + SubmissionRunHistoryDocument, +} from "shared"; import { initTelemetry, trackMetric, trackEvent, shutdownTelemetry } from "telemetry"; // ── Configuration ──────────────────────────────────────────────────── @@ -128,6 +134,9 @@ async function main(): Promise { const db = mongoClient.db(MONGO_DATABASE); const collection = db.collection(MONGO_COLLECTION); const agentCollection = db.collection("agents"); + const submissionCollection = db.collection("submissions"); + const submissionRunsCollection = + db.collection("submission-runs"); // Start the scheduler const scheduler = new RequestScheduler( @@ -191,6 +200,17 @@ async function main(): Promise { `[Scheduler] Handler dispatcher (DAG) started (poll=${ppPollIntervalMs}ms, topologyCacheTtl=${handlerCacheTtlMs}ms)`, ); + const submissionDispatcher = new SubmissionDispatcher( + collection, + submissionCollection, + submissionRunsCollection, + db, + createQueueClient, + ppPollIntervalMs, + ); + submissionDispatcher.start(); + console.log(`[Scheduler] Submission dispatcher started (poll=${ppPollIntervalMs}ms)`); + // Start the stuck-run reaper (authoritative backstop). Redis is non-fatal: // on misconfig or connection failure the reaper self-disables (its sweeps // skip when ping() fails) while the dispatch loop above keeps running. @@ -227,8 +247,25 @@ async function main(): Promise { ); } - // Wire notify handler to the HandlerDispatcher - const httpServer = createHttpServer(handlerDispatcher); + // Run notifications update the run DAG and immediately reconcile the parent + // submission. The polling loop remains the durable safety net. + const notifyHandler: NotifyHandler = { + onRunTerminal: async (requestId, runId) => { + await handlerDispatcher.onRunTerminal(requestId, runId); + await submissionDispatcher.onRequestChanged(requestId); + }, + onHandlerComplete: async (requestId, runId, handlerId, status) => { + await handlerDispatcher.onHandlerComplete( + requestId, + runId, + handlerId, + status, + ); + await submissionDispatcher.onRequestChanged(requestId); + }, + registerHandler: (doc) => handlerDispatcher.registerHandler(doc), + }; + const httpServer = createHttpServer(notifyHandler); httpServer.listen(HEALTH_PORT, () => { console.log(`[Scheduler] HTTP server listening on :${HEALTH_PORT}`); }); @@ -239,6 +276,7 @@ async function main(): Promise { await scheduler.stop(); await postProcessorDispatcher.stop(); await handlerDispatcher.stop(); + await submissionDispatcher.stop(); if (stuckRunReaper) await stuckRunReaper.stop(); if (heartbeatStore) await heartbeatStore.close(); httpServer.close(); diff --git a/apps/scheduler/src/notify-routes.ts b/apps/scheduler/src/notify-routes.ts index b63d13dd9..6810a90c0 100644 --- a/apps/scheduler/src/notify-routes.ts +++ b/apps/scheduler/src/notify-routes.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import http from "node:http"; -import type { HandlerServiceDocument } from "shared"; +import type { ServiceHandlerDocument } from "shared"; /** * Maximum accepted request body size for the scheduler's HTTP endpoints. @@ -28,7 +28,7 @@ export interface NotifyHandler { handlerId: string, status: "done" | "failed", ): Promise; - registerHandler(doc: HandlerServiceDocument): Promise; + registerHandler(doc: ServiceHandlerDocument): Promise; } interface RunTerminalRequest { @@ -98,7 +98,7 @@ function parseHandlerCompleteRequest(body: unknown): HandlerCompleteRequest | nu }; } -function parseHandlerRegisterRequest(body: unknown): HandlerServiceDocument | null { +function parseHandlerRegisterRequest(body: unknown): ServiceHandlerDocument | null { if (!isRecord(body)) { return null; } @@ -115,7 +115,7 @@ function parseHandlerRegisterRequest(body: unknown): HandlerServiceDocument | nu if ( !isNonEmptyString(_id) || - type !== "post-process-handler" || + (type !== "post-process-handler" && type !== "submission-handler") || typeof version !== "number" || !Number.isFinite(version) || !isNonEmptyString(queue) || @@ -127,15 +127,23 @@ function parseHandlerRegisterRequest(body: unknown): HandlerServiceDocument | nu return null; } - return { + const common = { _id, - type, version, queue, selector, autoBackfill, dependsOn: dependsOn as string[], }; + if (type === "submission-handler") { + if (body.dependencyMode !== "all-runs") return null; + return { + ...common, + type, + dependencyMode: "all-runs", + }; + } + return { ...common, type }; } export function readJsonBody( @@ -262,7 +270,7 @@ export function createHttpServer( if (!parsedBody) { sendJson(res, 400, { error: - "Invalid handler registration: _id, type='post-process-handler', version (number), queue, selector, autoBackfill (boolean), and dependsOn (string[]) are required", + "Invalid handler registration: _id, supported type, version, queue, selector, autoBackfill, dependsOn, and submission dependencyMode when applicable are required", }); return; } diff --git a/apps/scheduler/src/submission-dispatcher.ts b/apps/scheduler/src/submission-dispatcher.ts new file mode 100644 index 000000000..cd2914853 --- /dev/null +++ b/apps/scheduler/src/submission-dispatcher.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { QueueClient } from "@azure/storage-queue"; +import type { Collection, Db } from "mongodb"; +import { + SubmissionCoordinator, + type RequestDocument, + type SubmissionDocument, + type SubmissionHandlerServiceDocument, + type SubmissionQueueMessagePayload, + type SubmissionRunHistoryDocument, + type ServiceHandlerDocument, +} from "shared"; + +export class SubmissionDispatcher { + private interval: ReturnType | null = null; + private dispatching = false; + private scanAfterSubmissionId: string | null = null; + private readonly queueClients = new Map(); + private readonly coordinator: SubmissionCoordinator; + + constructor( + private readonly requests: Collection, + private readonly submissions: Collection, + submissionRuns: Collection, + db: Db, + private readonly createQueueClient: (queueName: string) => QueueClient, + private readonly pollIntervalMs = 30_000, + private readonly batchSize = 30, + ) { + this.coordinator = new SubmissionCoordinator( + { + requests, + submissions, + submissionRuns, + services: db.collection("services"), + }, + (service, message) => this.enqueue(service, message), + ); + } + + start(): void { + if (this.interval) return; + this.interval = setInterval(() => this.poll(), this.pollIntervalMs); + void this.poll(); + } + + async stop(): Promise { + if (this.interval) { + clearInterval(this.interval); + this.interval = null; + } + const deadline = Date.now() + 5_000; + while (this.dispatching && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } + + async onRequestChanged(requestId: string): Promise { + const request = await this.requests.findOne( + { _id: requestId }, + { projection: { submissionId: 1 } }, + ); + if (request?.submissionId) { + await this.coordinator.reconcile(request.submissionId); + } + } + + private async poll(): Promise { + if (this.dispatching) return; + this.dispatching = true; + try { + await this.coordinator.recoverMissingNativeSubmissions(this.batchSize); + const candidates = await this.submissions + .find( + this.scanAfterSubmissionId + ? { _id: { $gt: this.scanAfterSubmissionId } } + : {}, + ) + .sort({ _id: 1 }) + .limit(this.batchSize) + .project({ _id: 1 }) + .toArray(); + this.scanAfterSubmissionId = + candidates.length === this.batchSize + ? candidates.at(-1)?._id ?? null + : null; + for (const candidate of candidates) { + await this.coordinator.reconcile(candidate._id); + } + } catch (error) { + console.error("[SubmissionDispatcher] Poll failed:", error); + } finally { + this.dispatching = false; + } + } + + private async enqueue( + service: SubmissionHandlerServiceDocument, + message: SubmissionQueueMessagePayload, + ): Promise { + let client = this.queueClients.get(service.queue); + if (!client) { + client = this.createQueueClient(service.queue); + await client.createIfNotExists(); + this.queueClients.set(service.queue, client); + } + const encoded = Buffer.from(JSON.stringify(message)).toString("base64"); + await client.sendMessage(encoded); + } +} diff --git a/apps/workers/post-processor/Dockerfile b/apps/workers/post-processor/Dockerfile index 50f1c7a7c..2f5427cfe 100644 --- a/apps/workers/post-processor/Dockerfile +++ b/apps/workers/post-processor/Dockerfile @@ -24,7 +24,7 @@ COPY packages/telemetry/src ./packages/telemetry/src/ COPY apps/workers/post-processor/tsconfig.json ./apps/workers/post-processor/ COPY apps/workers/post-processor/src ./apps/workers/post-processor/src/ # Copy every handler manifest (handler.yaml = pp-atif, handler.taxonomy.yaml = -# pp-taxonomy, plus any future handlers). The registration jobs read these by +# pp-observations, plus any future handlers). The registration jobs read these by # HANDLER_YAML_PATH, so all must be present in the image. COPY apps/workers/post-processor/handler*.yaml ./apps/workers/post-processor/ diff --git a/apps/workers/post-processor/handler.taxonomy.yaml b/apps/workers/post-processor/handler.taxonomy.yaml index 37411625f..849568456 100644 --- a/apps/workers/post-processor/handler.taxonomy.yaml +++ b/apps/workers/post-processor/handler.taxonomy.yaml @@ -2,16 +2,16 @@ # Read by the generic register-handler script and POSTed to the # scheduler's POST /handlers/register endpoint on deploy. # -# pp-taxonomy records per-iteration boolean observations (kind:"observation" +# pp-observations records boolean observations (kind:"observation" # criteria) by reusing the judge. It depends on pp-atif so each turn's ATIF # trajectory is available, giving the judge codebase (snapshot) + trajectory # (ATIF) access. Shares the post-processor-queue with pp-atif (same worker). # See issue #1156. -_id: pp-taxonomy +_id: pp-observations type: post-process-handler version: 1 queue: post-processor-queue -selector: taxonomy +selector: observations autoBackfill: true dependsOn: - pp-atif diff --git a/apps/workers/post-processor/src/handlers/taxonomy-handler.test.ts b/apps/workers/post-processor/src/handlers/taxonomy-handler.test.ts index 1f191caa9..1aeb0b2ec 100644 --- a/apps/workers/post-processor/src/handlers/taxonomy-handler.test.ts +++ b/apps/workers/post-processor/src/handlers/taxonomy-handler.test.ts @@ -32,7 +32,7 @@ function makeCtx( } as any; } -const msg: PostProcessorMessage = { type: "taxonomy", requestId: "req-1", runId: "run-1" }; +const msg: PostProcessorMessage = { type: "observations", requestId: "req-1", runId: "run-1" }; describe("TaxonomyHandler", () => { let judge: { evaluate: ReturnType }; @@ -41,8 +41,8 @@ describe("TaxonomyHandler", () => { judge = { evaluate: vi.fn() }; }); - it("has type 'taxonomy'", () => { - expect(new TaxonomyHandler(judge as unknown as JudgeClient).type).toBe("taxonomy"); + it("has type 'observations'", () => { + expect(new TaxonomyHandler(judge as unknown as JudgeClient).type).toBe("observations"); }); it("no-ops when no observations are selected", async () => { diff --git a/apps/workers/post-processor/src/handlers/taxonomy-handler.ts b/apps/workers/post-processor/src/handlers/taxonomy-handler.ts index 62cf41a85..2baa81365 100644 --- a/apps/workers/post-processor/src/handlers/taxonomy-handler.ts +++ b/apps/workers/post-processor/src/handlers/taxonomy-handler.ts @@ -26,7 +26,7 @@ interface CriterionMeta { } /** - * Taxonomy Handler (pp-taxonomy): records boolean observations. + * Taxonomy Handler (pp-observations): records boolean observations. * * Each selected observation criterion is routed by its `subject` (#1156): * @@ -40,12 +40,12 @@ interface CriterionMeta { * behavior (e.g. a dependency added in one turn and removed in another) visible. * * Reuses the judge service; never gates, never steers the agent. Empty/absent - * `observations` → no-op. pp-taxonomy is dispatched exactly once per run (the + * `observations` → no-op. pp-observations is dispatched exactly once per run (the * scheduler never sets `message.iteration`), so the run-subject branch runs once * with no idempotency guard. */ export class TaxonomyHandler implements PostProcessHandler { - readonly type = "taxonomy"; + readonly type = "observations"; readonly version = POST_PROCESSOR_VERSION; readonly autoBackfill = true; private readonly judge: JudgeClient; diff --git a/docker-compose.yml b/docker-compose.yml index 027888cd2..221dd5c03 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -886,7 +886,7 @@ services: restart: "no" entrypoint: ["npx", "tsx", "packages/shared/src/scripts/register-handler.ts"] - # Registers the pp-taxonomy handler (observation evaluator) — shares the + # Registers the pp-observations handler (observation evaluator) — shares the # post-processor worker + post-processor-queue, depends on pp-atif. Issue #1156. register-handler-taxonomy: build: @@ -918,7 +918,7 @@ services: AZURE_STORAGE_QUEUE_POSTPROCESSOR: post-processor-queue SCOPE_MT_API_URL: http://api:80 SCHEDULER_URL: http://scheduler:8080 - # pp-taxonomy (issue #1156) calls the judge service to evaluate + # pp-observations (issue #1156) calls the judge service to evaluate # observation criteria. Without this the JudgeClient falls back to # localhost:3002 and cannot reach the judge container. JUDGE_SERVICE_URL: http://judge:80 diff --git a/docs/architecture/app-design.md b/docs/architecture/app-design.md index 8f1d67c01..9ff0c010f 100644 --- a/docs/architecture/app-design.md +++ b/docs/architecture/app-design.md @@ -470,6 +470,78 @@ Profile fan-out mode is also supported for comparative runs: 3. Each expanded request resolves configuration from its variation profile; unpinned specs resolve to `latestVersion` 4. Expanded requests persist `profileId` and `profileVersionId` on each `RequestDocument` for indexing and traceability +### Submission identity and processing attempts + +Every native submit creates a stable `SubmissionDocument`, including one-request +submissions. The API generates the submission id, every request id, and every +initial run id before writing anything. It inserts the requests first, then +idempotently finalizes the submission with: + +- immutable `projectId` and ordered `requestIds`; +- one embedded current `SubmissionRunState`; +- an exact immutable vector of `{ requestId, runId }` pairs for that attempt; +- a deterministic vector fingerprint, dependency projection, blocking details, + submission-level handler status, and report pointers. + +Requests carry `submissionExpectedRequestCount`, `submissionRequestIndex`, and +the immutable `submissionInitialRunId`, then receive `submissionFinalizedAt` +only after the parent document exists. The scheduler uses those markers to +recover the exact initial vector after an interrupted final write, even if a +request has since been retried. It never adopts historical/imported requests +that only have `submissionId`; those remain view-only and non-triggering. + +The current attempt lives only at `SubmissionDocument.run`. When any request's +current run id changes, reconciliation archives the previous attempt to the +immutable `submission-runs` collection and compare-and-swaps a fresh embedded +attempt containing every request's latest run id. The next attempt id is +deterministic for `(submissionId, attemptNumber, vectorFingerprint)`, so a +rollover first claims that successor on the exact current attempt. Only the +claim winner writes the immutable archive's supersession metadata, then swaps +the embedded current attempt. A restart between claim, archive, and swap resumes +the same claimed rollover instead of creating a dangling successor or parallel +current attempt. Claimed attempts reject worker claims/completions, and only the +latest embedded vector without a rollover claim is dispatchable. Individual +retries may therefore leave useful intermediate history. +Bulk request retry places a short, expiring reconciliation hold on each affected +submission while all replacement runs are installed, then releases it so the +completed batch produces one rollover. Polling clears expired holds and repairs +vector drift, missing projections, dropped notifications, and interrupted +archive/swap operations. + +`pp-readout` is a registered `submission-handler` with +`dependencyMode: all-runs` and `dependsOn: [pp-observations]`. Reconciliation +projects the authoritative `run.handlerStatus` for every exact request/run pair: + +- `pp-observations: done` is valid evidence even when the coding outcome failed + or an observation result has `passed: false`; +- a failed `pp-observations`, or one blocked by a failed DAG ancestor, blocks + readout; +- unrelated handler failures do not block; +- completed projections are naturally preserved for request/run pairs whose + run id did not change. + +Submission queue messages are attempt-scoped: +`{ type, submissionId, submissionRunId }`. PR B's worker must claim and complete +with filters on both `submissionId` and `run._id`, call +`isCurrentSubmissionRun()` before expensive work and final publication, and +discard a message when `claimSubmissionHandler()` returns `null`. Final writes +use `completeSubmissionHandler()`, whose compare-and-swap rejects stale +publication after supersession. + +The API exposes: + +- `GET /api/v1/submissions?projectId=...` +- `GET /api/v1/submissions/:id` +- `GET /api/v1/submissions/:id/runs` +- `GET /api/v1/submissions/:id/runs/:submissionRunId` +- `POST /api/v1/submissions/:id/retry` + +Responses expose readiness, exact blocking request/run pairs, handler progress, +supersession metadata, resolved request summaries, and the current report +pointer without queue names or scheduler claim fields. Portal submit flows +navigate to Submission Detail, and the CLI mirrors list/get/history/retry +capabilities under `scope submission`. + ## Runs List Query API `GET /api/v1/requests` powers the Portal **Runs** list and `scope run list`. All @@ -693,9 +765,9 @@ Criteria are reusable evaluation rules stored in the database and optionally def - **Criterion kind** — `kind: "gate" | "observation"` distinguishes criteria that gate the agent from record-only observations. Observation criteria can optionally set `taxonomyElementId` to one of the hard-coded observation dimensions and a `subject` (`run`|`iteration`) that selects whole-run vs per-iteration evaluation; run detail derives dimension grouping by joining each `observationResults[].criterionId` (per-turn or run-level) back to the criterion metadata. - **Kind** — `kind: "gate" | "observation"` (default `gate`). **Gate** criteria steer the coding agent through the gate DAG. **Observation** criteria (issue #1156) are record-only: they produce a boolean + evidence, never feed back to the agent, never pass/fail a gate, and never enter the gate DAG. Dependencies may only reference same-kind criteria. - **Taxonomy classification** — observation criteria carry an optional `taxonomyElementId` from a hard-coded enum of the three R&A Readout quality dimensions (`dimension:idiomatic-use`, `dimension:dependency-currency`, `dimension:configuration-correctness`). Classification is static/design-time, not per-run. -- **Evaluation subject** — `subject: "run" | "iteration"` decides the evaluation granularity of an observation. **`run`** (the default for observations) is evaluated **once** against the final snapshot + the merged trajectory across all iterations and stored at run level (`run.observationResults`); **`iteration`** is evaluated per turn and stored on each turn (`turn.observationResults`). Gate criteria are always `iteration` (the API rejects `run` for gates); `subject` is honored only by the `pp-taxonomy` pipeline. The AI authoring assistant auto-detects the subject and the user can override it. +- **Evaluation subject** — `subject: "run" | "iteration"` decides the evaluation granularity of an observation. **`run`** (the default for observations) is evaluated **once** against the final snapshot + the merged trajectory across all iterations and stored at run level (`run.observationResults`); **`iteration`** is evaluated per turn and stored on each turn (`turn.observationResults`). Gate criteria are always `iteration` (the API rejects `run` for gates); `subject` is honored only by the `pp-observations` pipeline. The AI authoring assistant auto-detects the subject and the user can override it. -Observation evaluation happens off the coding-agent critical path in the `pp-taxonomy` post-processor (see [post-processing](post-processing.md)). A run carries the chosen `observations: string[]` (observation-criteria ids); **iteration-subject** results are stored per turn as `turn.observationResults: CriterionResult[]`, while **run-subject** results are stored once at run level as `run.observationResults: CriterionResult[]` (both the same shape as gate `criteriaResults`). Dimension grouping is derived at read time by joining `criterionId` → criterion `taxonomyElementId`. +Observation evaluation happens off the coding-agent critical path in the `pp-observations` post-processor (see [post-processing](post-processing.md)). A run carries the chosen `observations: string[]` (observation-criteria ids); **iteration-subject** results are stored per turn as `turn.observationResults: CriterionResult[]`, while **run-subject** results are stored once at run level as `run.observationResults: CriterionResult[]` (both the same shape as gate `criteriaResults`). Dimension grouping is derived at read time by joining `criterionId` → criterion `taxonomyElementId`. See [`ENV_VARIABLES.md`](../../scope-mt-app/ENV_VARIABLES.md) for related configuration options. diff --git a/docs/architecture/criteria-provider.md b/docs/architecture/criteria-provider.md index af6db54f6..23ae0d8ec 100644 --- a/docs/architecture/criteria-provider.md +++ b/docs/architecture/criteria-provider.md @@ -181,7 +181,7 @@ bypassed or drift between callers: default): `kind:"observation"` ⇒ `"run"`, `kind:"gate"`/legacy ⇒ `"iteration"`. Gate criteria are rejected if `subject:"run"` is supplied — `"iteration"` is their only legal value. The field is threaded through `create`/`update`/`find` and both provider backends - so the `pp-taxonomy` worker and the CLI/Portal can read it. + so the `pp-observations` worker and the CLI/Portal can read it. - **Referential integrity on delete** — deleting a criterion that still has dependents is rejected. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index a5c10eaf6..e6b179e56 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -79,7 +79,7 @@ flowchart TB | `workers/coder-acp-claude-code` | Claude Code agent via Agent Client Protocol (ACP) | | `workers/coder-acp-copilot` | GitHub Copilot agent via Agent Client Protocol (ACP) | | `workers/report-generator` | Copilot SDK worker that generates post-run markdown reports | -| `workers/post-processor` | DAG-based post-run handlers: `pp-atif` (trajectory ATIF) and `pp-taxonomy` (boolean observations via the judge, per-iteration or whole-run by criterion `subject`) | +| `workers/post-processor` | DAG-based post-run handlers: `pp-atif` (trajectory ATIF) and `pp-observations` (boolean observations via the judge, per-iteration or whole-run by criterion `subject`) | | `gateway` | AI Gateway — shared Rust TLS-intercepting proxy with plugin architecture (HAR capture, future: token refresh, rate limiting) | | `evaluations/static-prompts` | Developer-run static prompt quality and user-controlled prompt red-team suites | @@ -115,6 +115,14 @@ walkthrough, failures, cache isolation, and implemented/deferred boundary. 4. **Judge** — After the agent completes, the worker invokes the Judge to evaluate output against criteria. Results (pass/fail per criterion, scores) are persisted to CosmosDB. 5. **Snapshot** — Each iteration's workspace is snapshotted to Blob Storage for later inspection. +Every native submit also creates a stable submission that owns the complete +request set. Submission processing follows the requests' exact current run-id +vector: request retries archive and supersede the previous submission attempt, +run-level `pp-observations` completion feeds an all-runs dependency projection, +and only the embedded current submission attempt may dispatch `pp-readout`. +Portal and CLI users can inspect current/history attempts and retry current +submission processing without mutating request execution history. + > A run may execute as a sequence of **gates** (`Select → Build → Test → Run → > Deploy`), each with its own prompt, criteria subset, and iteration budget, > running stop-on-failure against the same workspace. Requests without an explicit diff --git a/docs/architecture/post-processing.md b/docs/architecture/post-processing.md index dbeec1301..4518ef391 100644 --- a/docs/architecture/post-processing.md +++ b/docs/architecture/post-processing.md @@ -56,9 +56,9 @@ Two paths trigger post-processing: The `iteration` query parameter is mandatory for the ATIF endpoints. ATIF files are also included in archive exports as `iteration-{N}.atif.trajectory.json`. -## Taxonomy Observations (`pp-taxonomy`) +## Taxonomy Observations (`pp-observations`) -The `pp-taxonomy` handler records **observation criteria** — boolean outcomes + +The `pp-observations` handler records **observation criteria** — boolean outcomes + evidence about the codebase and the agent's trajectory, **without gating or steering** the coding agent (issue #1156). It is a DAG node `dependsOn: [pp-atif]`, because some observations are only derivable from the trajectory (ATIF), not the final snapshot. @@ -89,8 +89,8 @@ Gate criteria are always `subject: "iteration"` (the API rejects `"run"` for gat 4. **Run-subject:** evaluates **once** with `snapshotUrl` = final turn's snapshot, `atifUrls` = every turn's `atifUrl` (the judge downloads + merges them into one `AgentTrajectory`), and the full `conversationHistory`. Writes the verdicts to **`run.observationResults`** (run level). - In all judge calls there is no persona, no gate, no taxonomyElementId (the classifier lives on the criterion). ATIF supersedes per-turn `toolCallsUrl`, so only ATIF is sent. - Result shape/storage mirrors gate `criteriaResults`. No new blob, no `run.taxonomyUrl`. - - **Runs exactly once per run:** the scheduler dispatches `pp-taxonomy` a single time (it never sets `message.iteration`), so the run-subject branch needs no idempotency guard. -5. Stamps `run.handlerStatus["pp-taxonomy"] = { status: "done", version: N }` and notifies the scheduler. + - **Runs exactly once per run:** the scheduler dispatches `pp-observations` a single time (it never sets `message.iteration`), so the run-subject branch needs no idempotency guard. +5. Stamps `run.handlerStatus["pp-observations"] = { status: "done", version: N }` and notifies the scheduler. Dimension grouping is **derived at read time** by joining each result's `criterionId` → criterion `taxonomyElementId` (the three R&A Readout dimensions; unset = unclassified). It never feeds back to the agent and never affects pass/fail. (#1053 later extends this worker with a richer scorecard artifact.) @@ -203,7 +203,34 @@ flowchart LR **Where it runs:** - **Docker Compose:** one `register-handler-` init service per handler (e.g. `pp-atif`) runs the generic script via `tsx`, gated on the scheduler being up. The worker waits for its registration service to complete. -- **Kubernetes:** a `register-handler-post-processor` Job runs the compiled script (`node packages/shared/dist/scripts/register-handler.js`) on each deploy. `pp-atif` and `pp-taxonomy` are registered in K8s via their respective registration Jobs (`register-handler-taxonomy` points its `HANDLER_YAML_PATH` at `handler.taxonomy.yaml`). The scheduler exposes a `Service` (`scheduler.scoped.svc.cluster.local:8080`) so Jobs and workers can reach `/handlers/register` and the notify endpoints. +- **Kubernetes:** a `register-handler-post-processor` Job runs the compiled script (`node packages/shared/dist/scripts/register-handler.js`) on each deploy. `pp-atif` and `pp-observations` are registered in K8s via their respective registration Jobs (`register-handler-taxonomy` points its `HANDLER_YAML_PATH` at `handler.taxonomy.yaml`). The scheduler exposes a `Service` (`scheduler.scoped.svc.cluster.local:8080`) so Jobs and workers can reach `/handlers/register` and the notify endpoints. + +## Submission handlers + +Submission handlers use the same `services` registration endpoint but declare +`type: submission-handler` and `dependencyMode: all-runs`. Their dependencies +name run-level DAG handlers, not other submission handlers. The scheduler +reconciles each submission's exact request/run vector, projects those named +dependencies, and enqueues only the current embedded submission attempt. + +The first consumer is `pp-readout`, which depends on `pp-observations` for every +run in the vector. Its queue contract is: + +```json +{ + "type": "readout", + "submissionId": "…", + "submissionRunId": "…" +} +``` + +Workers must use the shared `claimSubmissionHandler`, +`isCurrentSubmissionRun`, and `completeSubmissionHandler` helpers. These helpers +scope every mutation to both identifiers, providing stale-message discard, +cooperative supersession checks, and stale completion rejection. Queue delivery +remains at-least-once; the embedded current attempt is the only dispatchable +state, and the scheduler poll repairs missed notifications, stale projections, +request-vector drift, and interrupted rollover/finalization. To **add or change a handler**, edit its `handler.yaml` (bump `version`, adjust `dependsOn`, etc.) — no code or migration changes are needed for registration. diff --git a/packages/db-migrations/src/034-create-submission-indexes.test.ts b/packages/db-migrations/src/034-create-submission-indexes.test.ts new file mode 100644 index 000000000..efcd2017f --- /dev/null +++ b/packages/db-migrations/src/034-create-submission-indexes.test.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { Db } from "mongodb"; +import { describe, expect, it, vi } from "vitest"; +import { CreateSubmissionIndexes } from "./migrations/034-create-submission-indexes.js"; + +function collection() { + return { + createIndex: vi.fn(async () => "index"), + }; +} + +describe("migration 034: CreateSubmissionIndexes", () => { + it("creates submission, history, dispatch, and recovery indexes", async () => { + const submissions = collection(); + const submissionRuns = collection(); + const requests = collection(); + const db = { + collection: vi.fn((name: string) => { + if (name === "submissions") return submissions; + if (name === "submission-runs") return submissionRuns; + if (name === "requests") return requests; + throw new Error(`Unexpected collection ${name}`); + }), + } as unknown as Db; + + await new CreateSubmissionIndexes().up(db); + + expect(submissions.createIndex).toHaveBeenCalledWith( + { projectId: 1, createdAt: -1, _id: -1 }, + {}, + ); + expect(submissions.createIndex).toHaveBeenCalledWith( + { "run.handlerStatus.pp-readout.status": 1 }, + {}, + ); + expect(submissionRuns.createIndex).toHaveBeenCalledWith( + { submissionId: 1, attemptNumber: -1 }, + {}, + ); + expect(requests.createIndex).toHaveBeenCalledWith( + { submissionFinalizedAt: 1 }, + {}, + ); + expect(requests.createIndex).toHaveBeenCalledWith( + { submissionId: 1, projectId: 1, submissionRequestIndex: 1, _id: 1 }, + {}, + ); + }); +}); diff --git a/packages/db-migrations/src/migrations/034-create-submission-indexes.ts b/packages/db-migrations/src/migrations/034-create-submission-indexes.ts new file mode 100644 index 000000000..8ceb836e9 --- /dev/null +++ b/packages/db-migrations/src/migrations/034-create-submission-indexes.ts @@ -0,0 +1,67 @@ +// 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 = "034"; + +export class CreateSubmissionIndexes implements MigrationInterface { + async up(db: Db): Promise { + const submissions = db.collection("submissions"); + await ensureIndex( + submissions, + { projectId: 1, createdAt: -1, _id: -1 }, + {}, + "submissions", + TAG, + ); + await ensureIndex( + submissions, + { "run.handlerStatus.pp-readout.status": 1 }, + {}, + "submissions", + TAG, + ); + + const submissionRuns = db.collection("submission-runs"); + await ensureIndex( + submissionRuns, + { submissionId: 1 }, + {}, + "submission-runs", + TAG, + ); + await ensureIndex( + submissionRuns, + { submissionId: 1, attemptNumber: -1 }, + {}, + "submission-runs", + TAG, + ); + + const requests = db.collection("requests"); + await ensureIndex( + requests, + { submissionFinalizedAt: 1 }, + {}, + "requests", + TAG, + ); + await ensureIndex( + requests, + { submissionId: 1, projectId: 1, submissionRequestIndex: 1, _id: 1 }, + {}, + "requests", + TAG, + ); + console.log(`[${TAG}] Submission indexes complete`); + } + + async down(_db: Db): Promise { + console.log( + `[${TAG}-down] Skipping index changes - drop submission indexes manually if needed`, + ); + } +} diff --git a/packages/db-migrations/src/required-migrations.ts b/packages/db-migrations/src/required-migrations.ts index 4d0e54062..bee754391 100644 --- a/packages/db-migrations/src/required-migrations.ts +++ b/packages/db-migrations/src/required-migrations.ts @@ -46,6 +46,7 @@ export const REQUIRED_MIGRATIONS: readonly string[] = [ "031-migrate-services-to-handler-dag.ts", "032-add-handler-status-index.ts", "033-backfill-handler-status.ts", + "034-create-submission-indexes.ts", "021-backfill-criteria-kind.ts", "022-backfill-criteria-subject.ts", ]; diff --git a/packages/shared/src/criteria/criteria-store.ts b/packages/shared/src/criteria/criteria-store.ts index 7413449c9..0b47231b4 100644 --- a/packages/shared/src/criteria/criteria-store.ts +++ b/packages/shared/src/criteria/criteria-store.ts @@ -439,7 +439,7 @@ export class CriteriaStore { /** * Resolve observation-criteria ids, asserting each is an active - * kind:"observation" criterion. Used by submit validation + pp-taxonomy. + * kind:"observation" criterion. Used by submit validation + pp-observations. * Throws CriteriaValidationError on a missing or non-observation id. */ async resolveObservations(ids: string[]): Promise { diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index a6febdece..fae65d02f 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -20,6 +20,7 @@ export * from "./codebases/index.js"; export * from "./resources/index.js"; export * from "./extensions/index.js"; export * from "./projects/index.js"; +export * from "./submissions/index.js"; export * from "./agent-version.js"; export * from "./resolve-agent-version.js"; export * from "./resolve-agent-target.js"; diff --git a/packages/shared/src/judge/judge-client.ts b/packages/shared/src/judge/judge-client.ts index 7a5f85850..633fd6e8f 100644 --- a/packages/shared/src/judge/judge-client.ts +++ b/packages/shared/src/judge/judge-client.ts @@ -18,11 +18,11 @@ export interface JudgeEvaluateRequest { /** Blob URL of this iteration's captured tool calls/outputs (build/test/run output). */ toolCallsUrl?: string; /** Blob URL of this iteration's ATIF trajectory JSON. Normalized superset of - * tool calls; sent by pp-taxonomy in place of toolCallsUrl so observations + * tool calls; sent by pp-observations in place of toolCallsUrl so observations * can be evaluated against the full agent trajectory. See issue #1156. */ atifUrl?: string; /** Blob URLs of EVERY iteration's ATIF trajectory JSON, in iteration order. - * Sent by pp-taxonomy for `subject:"run"` observations so the judge merges + * Sent by pp-observations for `subject:"run"` observations so the judge merges * them into one whole-run trajectory (cross-iteration behavior is visible in a * single evaluation). Takes precedence over `atifUrl`. See issue #1156. */ atifUrls?: string[]; diff --git a/packages/shared/src/schemas/index.ts b/packages/shared/src/schemas/index.ts index cb1df8b4c..52a7bed3f 100644 --- a/packages/shared/src/schemas/index.ts +++ b/packages/shared/src/schemas/index.ts @@ -22,3 +22,4 @@ export * from "./account.js"; export * from "./feature-flag.js"; export * from "./profile.js"; export * from "./project.js"; +export * from "./submission.js"; diff --git a/packages/shared/src/schemas/request.ts b/packages/shared/src/schemas/request.ts index 5271582a5..5aefb2466 100644 --- a/packages/shared/src/schemas/request.ts +++ b/packages/shared/src/schemas/request.ts @@ -181,6 +181,10 @@ export const RequestResponseSchema = z profileId: z.string().optional(), profileVersionId: z.string().optional(), submissionId: z.string().optional(), + submissionExpectedRequestCount: z.number().int().min(1).optional(), + submissionRequestIndex: z.number().int().min(0).optional(), + submissionInitialRunId: z.string().optional(), + submissionFinalizedAt: z.coerce.date().optional(), priority: z.number().int().default(0), gates: z.array(GateConfigSchema).optional(), observations: z.array(z.string()).optional(), diff --git a/packages/shared/src/schemas/submission.ts b/packages/shared/src/schemas/submission.ts new file mode 100644 index 000000000..3558ae603 --- /dev/null +++ b/packages/shared/src/schemas/submission.ts @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { z } from "zod"; +import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; + +extendZodWithOpenApi(z); + +export const SubmissionRequestRefSchema = z + .object({ + requestId: z.string(), + runId: z.string(), + }) + .openapi("SubmissionRequestRef"); + +export const SubmissionDependencyStateSchema = z.enum([ + "waiting", + "ready", + "failed", + "blocked", +]); + +export const SubmissionDependencyProjectionSchema = z + .object({ + requestId: z.string(), + runId: z.string(), + handlerId: z.string(), + state: SubmissionDependencyStateSchema, + version: z.number().optional(), + updatedAt: z.coerce.date().optional(), + error: z.string().optional(), + }) + .openapi("SubmissionDependencyProjection"); + +export const SubmissionBlockingDetailSchema = z + .object({ + requestId: z.string(), + runId: z.string(), + handlerId: z.string(), + reason: z.enum([ + "request-missing", + "run-changed", + "dependency-not-ready", + "dependency-failed", + "dependency-blocked", + ]), + error: z.string().optional(), + }) + .openapi("SubmissionBlockingDetail"); + +export const SubmissionHandlerStatusSchema = z + .object({ + status: z.enum(["queued", "processing", "done", "failed"]), + version: z.number().optional(), + updatedAt: z.coerce.date().optional(), + error: z.string().optional(), + }) + .openapi("SubmissionHandlerStatus"); + +export const SubmissionReportPointerSchema = z + .object({ + reportId: z.string().optional(), + url: z.string().optional(), + updatedAt: z.coerce.date().optional(), + }) + .openapi("SubmissionReportPointer"); + +export const SubmissionRolloverClaimSchema = z.object({ + nextSubmissionRunId: z.string(), + vectorFingerprint: z.string(), + requests: z.array(SubmissionRequestRefSchema), + reason: z.enum(["request-vector-changed", "manual-retry"]), + claimedAt: z.coerce.date(), +}); + +export const SubmissionRunStateSchema = z + .object({ + _id: z.string(), + attemptNumber: z.number().int().min(1), + vectorFingerprint: z.string(), + requests: z.array(SubmissionRequestRefSchema), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date().optional(), + startedAt: z.coerce.date().optional(), + finishedAt: z.coerce.date().optional(), + dependencyProjection: z.array(SubmissionDependencyProjectionSchema).default([]), + handlerStatus: z.record(z.string(), SubmissionHandlerStatusSchema).optional(), + blocking: z.array(SubmissionBlockingDetailSchema).default([]), + report: SubmissionReportPointerSchema.optional(), + reconcileAfter: z.coerce.date().optional(), + rollover: SubmissionRolloverClaimSchema.optional(), + supersededAt: z.coerce.date().optional(), + supersededBySubmissionRunId: z.string().optional(), + supersededReason: z.enum(["request-vector-changed", "manual-retry"]).optional(), + }) + .openapi("SubmissionRunState"); + +export const SubmissionDocumentSchema = z + .object({ + _id: z.string(), + submissionId: z.string(), + projectId: z.string(), + requestIds: z.array(z.string()).min(1), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date().optional(), + run: SubmissionRunStateSchema, + }) + .openapi("SubmissionDocument"); + +export const SubmissionRunHistoryDocumentSchema = SubmissionRunStateSchema.extend({ + submissionId: z.string(), + projectId: z.string(), +}).openapi("SubmissionRunHistoryDocument"); + +export const SubmissionRunResponseSchema = SubmissionRunStateSchema.omit({ + reconcileAfter: true, + rollover: true, +}) + .extend({ + submissionId: z.string().optional(), + projectId: z.string().optional(), + }) + .openapi("SubmissionRunResponse"); + +export const SubmissionReadinessSchema = z.enum([ + "waiting", + "ready", + "blocked", + "processing", + "done", + "failed", +]); + +export const SubmissionRequestSummarySchema = z + .object({ + requestId: z.string(), + runId: z.string(), + task: z.string(), + status: z.string(), + outcome: z.string().optional(), + workerType: z.string(), + model: z.string().optional(), + profileId: z.string().optional(), + }) + .openapi("SubmissionRequestSummary"); + +export const SubmissionResponseSchema = z + .object({ + submissionId: z.string(), + projectId: z.string(), + requestIds: z.array(z.string()), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date().optional(), + submissionRunId: z.string(), + attemptNumber: z.number().int(), + readiness: SubmissionReadinessSchema, + requests: z.array(SubmissionRequestSummarySchema), + dependencyProjection: z.array(SubmissionDependencyProjectionSchema), + blocking: z.array(SubmissionBlockingDetailSchema), + handlerProgress: z.record(z.string(), SubmissionHandlerStatusSchema).optional(), + report: SubmissionReportPointerSchema.optional(), + supersededAt: z.coerce.date().optional(), + supersededBySubmissionRunId: z.string().optional(), + supersededReason: z.enum(["request-vector-changed", "manual-retry"]).optional(), + }) + .openapi("SubmissionResponse"); + +export const SubmissionListResponseSchema = z + .object({ + submissions: z.array(SubmissionResponseSchema), + }) + .openapi("SubmissionListResponse"); + +export const SubmissionRetryResponseSchema = z + .object({ + submissionId: z.string(), + submissionRunId: z.string(), + attemptNumber: z.number().int(), + readiness: SubmissionReadinessSchema, + blocking: z.array(SubmissionBlockingDetailSchema), + }) + .openapi("SubmissionRetryResponse"); diff --git a/packages/shared/src/scripts/register-handler.ts b/packages/shared/src/scripts/register-handler.ts index c585e9eb1..730b860f6 100644 --- a/packages/shared/src/scripts/register-handler.ts +++ b/packages/shared/src/scripts/register-handler.ts @@ -21,7 +21,7 @@ */ import { readFile } from "node:fs/promises"; import { parse as parseYaml } from "yaml"; -import type { HandlerServiceDocument } from "../types/types.js"; +import type { ServiceHandlerDocument } from "../types/types.js"; const HEALTH_RETRIES = 30; const HEALTH_DELAY_MS = 3_000; @@ -33,7 +33,7 @@ function isNonEmptyString(value: unknown): value is string { } /** Validate the parsed YAML conforms to the HandlerServiceDocument shape. */ -export function parseHandlerDocument(raw: unknown): HandlerServiceDocument { +export function parseHandlerDocument(raw: unknown): ServiceHandlerDocument { if (typeof raw !== "object" || raw === null) { throw new Error("handler.yaml must contain a mapping"); } @@ -42,8 +42,10 @@ export function parseHandlerDocument(raw: unknown): HandlerServiceDocument { if (!isNonEmptyString(doc._id)) { throw new Error("handler.yaml: '_id' is required"); } - if (doc.type !== "post-process-handler") { - throw new Error("handler.yaml: 'type' must be 'post-process-handler'"); + if (doc.type !== "post-process-handler" && doc.type !== "submission-handler") { + throw new Error( + "handler.yaml: 'type' must be 'post-process-handler' or 'submission-handler'", + ); } if (typeof doc.version !== "number" || !Number.isFinite(doc.version)) { throw new Error("handler.yaml: 'version' must be a number"); @@ -62,15 +64,27 @@ export function parseHandlerDocument(raw: unknown): HandlerServiceDocument { throw new Error("handler.yaml: 'dependsOn' must be an array of strings"); } - return { + const common = { _id: doc._id, - type: "post-process-handler", version: doc.version, queue: doc.queue, selector: doc.selector, autoBackfill: doc.autoBackfill, dependsOn: dependsOn as string[], }; + if (doc.type === "submission-handler") { + if (doc.dependencyMode !== "all-runs") { + throw new Error( + "handler.yaml: submission handlers require dependencyMode: 'all-runs'", + ); + } + return { + ...common, + type: "submission-handler", + dependencyMode: "all-runs", + }; + } + return { ...common, type: "post-process-handler" }; } async function delay(ms: number): Promise { @@ -97,7 +111,7 @@ async function waitForScheduler(schedulerUrl: string): Promise { async function postRegistration( schedulerUrl: string, - doc: HandlerServiceDocument, + doc: ServiceHandlerDocument, ): Promise { for (let attempt = 1; attempt <= POST_RETRIES; attempt++) { try { @@ -128,7 +142,7 @@ async function postRegistration( export async function registerHandlerFromYaml( yamlPath: string, schedulerUrl: string, -): Promise { +): Promise { const content = await readFile(yamlPath, "utf-8"); const doc = parseHandlerDocument(parseYaml(content)); diff --git a/packages/shared/src/submissions/index.ts b/packages/shared/src/submissions/index.ts new file mode 100644 index 000000000..5aa35026a --- /dev/null +++ b/packages/shared/src/submissions/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export * from "./submission-state.js"; +export * from "./submission-coordinator.js"; diff --git a/packages/shared/src/submissions/submission-coordinator.test.ts b/packages/shared/src/submissions/submission-coordinator.test.ts new file mode 100644 index 000000000..d44b4c12e --- /dev/null +++ b/packages/shared/src/submissions/submission-coordinator.test.ts @@ -0,0 +1,693 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, expect, it, vi } from "vitest"; +import type { Collection } from "mongodb"; +import type { + RequestDocument, + ServiceHandlerDocument, + SubmissionDocument, + SubmissionRunHistoryDocument, +} from "../types/types.js"; +import { + SubmissionCoordinator, + claimSubmissionHandler, + completeSubmissionHandler, + ensureNativeSubmission, +} from "./submission-coordinator.js"; + +function queryResult(items: T[]) { + return { + project: () => queryResult(items), + sort: () => queryResult(items), + limit: () => queryResult(items), + toArray: async () => items, + }; +} + +describe("submission coordinator persistence", () => { + it("creates a native submission idempotently with the exact initial vector", async () => { + let stored: SubmissionDocument | null = null; + const updateOne = vi.fn(async ( + _filter: unknown, + update: { $setOnInsert: SubmissionDocument }, + ) => { + stored ??= update.$setOnInsert; + return { matchedCount: stored ? 1 : 0, modifiedCount: 0 }; + }); + const findOne = vi.fn(async () => stored); + const submissions = { + updateOne, + findOne, + } as unknown as Collection; + + const first = await ensureNativeSubmission({ + collections: { submissions }, + submissionId: "submission-1", + projectId: "project-1", + requests: [ + { requestId: "request-1", runId: "run-1" }, + { requestId: "request-2", runId: "run-2" }, + ], + createdAt: new Date("2026-01-01T00:00:00Z"), + }); + const second = await ensureNativeSubmission({ + collections: { submissions }, + submissionId: "submission-1", + projectId: "project-1", + requests: [ + { requestId: "request-1", runId: "run-1" }, + { requestId: "request-2", runId: "run-2" }, + ], + createdAt: new Date("2026-01-01T00:00:00Z"), + }); + + expect(first).toEqual(second); + expect(first.requestIds).toEqual(["request-1", "request-2"]); + expect(first.run.requests).toEqual([ + { requestId: "request-1", runId: "run-1" }, + { requestId: "request-2", runId: "run-2" }, + ]); + expect(first.run._id).toBe(second.run._id); + expect(updateOne).toHaveBeenCalledTimes(2); + }); + + it("rejects an idempotency key reused with a different immutable vector", async () => { + const stored: SubmissionDocument = { + _id: "submission-1", + submissionId: "submission-1", + projectId: "project-1", + requestIds: ["request-1"], + createdAt: new Date(), + run: { + _id: "submission-run-1", + attemptNumber: 1, + vectorFingerprint: "fingerprint", + requests: [{ requestId: "request-1", runId: "run-1" }], + createdAt: new Date(), + dependencyProjection: [], + blocking: [], + }, + }; + const submissions = { + updateOne: vi.fn(async () => ({ matchedCount: 1, modifiedCount: 0 })), + findOne: vi.fn(async () => stored), + } as unknown as Collection; + + await expect( + ensureNativeSubmission({ + collections: { submissions }, + submissionId: "submission-1", + projectId: "project-1", + requests: [{ requestId: "request-1", runId: "different-run" }], + }), + ).rejects.toThrow("different immutable request vector"); + }); + + it("scopes claims and completions to the exact embedded submission run", async () => { + const findOneAndUpdate = vi.fn(async (_filter: unknown, _update: unknown) => null); + const updateOne = vi.fn(async (_filter: unknown, _update: unknown) => ({ + modifiedCount: 0, + })); + const submissions = { + findOneAndUpdate, + updateOne, + } as unknown as Collection; + + expect( + await claimSubmissionHandler({ + submissions, + submissionId: "submission-1", + submissionRunId: "stale-run", + handlerId: "pp-readout", + version: 1, + }), + ).toBeNull(); + expect(findOneAndUpdate.mock.calls[0]?.[0]).toMatchObject({ + _id: "submission-1", + "run._id": "stale-run", + "run.rollover": { $exists: false }, + }); + + expect( + await completeSubmissionHandler({ + submissions, + submissionId: "submission-1", + submissionRunId: "stale-run", + handlerId: "pp-readout", + status: "done", + version: 1, + }), + ).toBe(false); + expect(updateOne.mock.calls[0]?.[0]).toMatchObject({ + _id: "submission-1", + "run._id": "stale-run", + "run.rollover": { $exists: false }, + "run.handlerStatus.pp-readout.status": "processing", + }); + }); + + it("defers reconciliation while a bulk retry replaces its request vector", async () => { + const submission: SubmissionDocument = { + _id: "submission-1", + submissionId: "submission-1", + projectId: "project-1", + requestIds: ["request-1"], + createdAt: new Date(), + run: { + _id: "submission-run-1", + attemptNumber: 1, + vectorFingerprint: "old-vector", + requests: [{ requestId: "request-1", runId: "run-1" }], + createdAt: new Date(), + reconcileAfter: new Date(Date.now() + 60_000), + dependencyProjection: [], + blocking: [], + }, + }; + const requestsFind = vi.fn(); + const archiveInsert = vi.fn(); + const coordinator = new SubmissionCoordinator({ + requests: { + find: requestsFind, + } as unknown as Collection, + submissions: { + findOne: vi.fn(async () => submission), + } as unknown as Collection, + submissionRuns: { + insertOne: archiveInsert, + } as unknown as Collection, + services: {} as Collection, + }); + + const result = await coordinator.reconcile("submission-1"); + + expect(result?.rolled).toBe(false); + expect(requestsFind).not.toHaveBeenCalled(); + expect(archiveInsert).not.toHaveBeenCalled(); + }); + + it("resumes an interrupted drift rollover without double-rolling a manual retry", async () => { + const requestDocs: RequestDocument[] = [ + { + _id: "request-1", + projectId: "project-1", + scenario: { task: "Task", criteria: ["criterion"] }, + workerType: "worker", + createdAt: new Date(), + priority: 0, + submissionId: "submission-1", + run: { + _id: "run-2", + attemptNumber: 2, + status: "done", + outcome: "failed", + handlerStatus: { + "pp-observations": { status: "done", version: 2 }, + unrelated: { status: "failed" }, + }, + }, + }, + ]; + let submission: SubmissionDocument = { + _id: "submission-1", + submissionId: "submission-1", + projectId: "project-1", + requestIds: ["request-1"], + createdAt: new Date(), + run: { + _id: "submission-run-1", + attemptNumber: 1, + vectorFingerprint: "old-vector", + requests: [{ requestId: "request-1", runId: "run-1" }], + createdAt: new Date(), + dependencyProjection: [], + blocking: [], + }, + }; + const history: SubmissionRunHistoryDocument[] = []; + const services: ServiceHandlerDocument[] = [ + { + _id: "pp-atif", + type: "post-process-handler", + version: 1, + queue: "post-processor", + selector: "atif", + autoBackfill: true, + dependsOn: [], + }, + { + _id: "pp-observations", + type: "post-process-handler", + version: 2, + queue: "post-processor", + selector: "observations", + autoBackfill: true, + dependsOn: ["pp-atif"], + }, + { + _id: "pp-readout", + type: "submission-handler", + version: 1, + queue: "readout", + selector: "readout", + autoBackfill: false, + dependsOn: ["pp-observations"], + dependencyMode: "all-runs", + }, + ]; + const submissions = { + findOne: vi.fn(async () => structuredClone(submission)), + updateOne: vi.fn(async () => ({ matchedCount: 1, modifiedCount: 1 })), + findOneAndUpdate: vi.fn(async ( + filter: Record, + update: { $set: Record }, + ) => { + if (filter["run._id"] && filter["run._id"] !== submission.run._id) return null; + if (update.$set["run.rollover"]) { + submission = { + ...submission, + updatedAt: update.$set.updatedAt as Date, + run: { + ...submission.run, + rollover: update.$set[ + "run.rollover" + ] as SubmissionDocument["run"]["rollover"], + updatedAt: update.$set["run.updatedAt"] as Date, + }, + }; + } else if (update.$set.run) { + rollAttempts++; + if (rollAttempts === 1) return null; + submission = { + ...submission, + run: update.$set.run as SubmissionDocument["run"], + updatedAt: update.$set.updatedAt as Date, + }; + } else { + submission = { + ...submission, + updatedAt: update.$set.updatedAt as Date, + run: { + ...submission.run, + dependencyProjection: update.$set[ + "run.dependencyProjection" + ] as SubmissionDocument["run"]["dependencyProjection"], + blocking: update.$set[ + "run.blocking" + ] as SubmissionDocument["run"]["blocking"], + updatedAt: update.$set["run.updatedAt"] as Date, + }, + }; + } + return structuredClone(submission); + }), + } as unknown as Collection; + let rollAttempts = 0; + const enqueue = vi.fn(async ( + _service: ServiceHandlerDocument, + _message: { submissionId: string; submissionRunId: string; type: string }, + ) => undefined); + const archiveUpdate = vi.fn(async ( + _filter: unknown, + update: { $setOnInsert: SubmissionRunHistoryDocument }, + ) => { + if (!history.some((item) => item._id === update.$setOnInsert._id)) { + history.push(update.$setOnInsert); + } + return { matchedCount: 1, modifiedCount: 1 }; + }); + const coordinator = new SubmissionCoordinator( + { + requests: { + find: vi.fn(() => queryResult(requestDocs)), + } as unknown as Collection, + submissions, + submissionRuns: { + updateOne: archiveUpdate, + } as unknown as Collection, + services: { + find: vi.fn(() => + queryResult(services.filter((service) => service.type === "post-process-handler")), + ), + findOne: vi.fn(async () => + services.find((service) => service._id === "pp-readout") ?? null, + ), + } as unknown as Collection, + }, + enqueue, + ); + + const result = await coordinator.retryCurrent( + "submission-1", + "submission-run-1", + ); + + expect(result?.rolled).toBe(true); + expect(result?.submission.run.attemptNumber).toBe(2); + expect(result?.submission.run.requests).toEqual([ + { requestId: "request-1", runId: "run-2" }, + ]); + expect(result?.submission.run.dependencyProjection[0]).toMatchObject({ + runId: "run-2", + state: "ready", + version: 2, + }); + expect(history).toHaveLength(1); + expect(archiveUpdate).toHaveBeenCalledTimes(2); + expect(history[0]).toMatchObject({ + _id: "submission-run-1", + supersededBySubmissionRunId: result?.submission.run._id, + supersededReason: "request-vector-changed", + }); + expect(enqueue).toHaveBeenCalledTimes(1); + expect(enqueue.mock.calls[0]?.[1]).toMatchObject({ + submissionId: "submission-1", + submissionRunId: result?.submission.run._id, + }); + }); + + it("links immutable history only to CAS-winning successors under concurrent vector drift", async () => { + const request = (runId: string, attemptNumber: number): RequestDocument => ({ + _id: "request-1", + projectId: "project-1", + scenario: { task: "Task", criteria: ["criterion"] }, + workerType: "worker", + createdAt: new Date(), + priority: 0, + submissionId: "submission-1", + run: { + _id: runId, + attemptNumber, + status: "done", + }, + }); + let submission: SubmissionDocument = { + _id: "submission-1", + submissionId: "submission-1", + projectId: "project-1", + requestIds: ["request-1"], + createdAt: new Date(), + run: { + _id: "submission-run-1", + attemptNumber: 1, + vectorFingerprint: "initial", + requests: [{ requestId: "request-1", runId: "run-1" }], + createdAt: new Date(), + dependencyProjection: [], + blocking: [], + }, + }; + const history = new Map(); + const submissions = { + findOne: vi.fn(async () => structuredClone(submission)), + updateOne: vi.fn(async () => ({ matchedCount: 1, modifiedCount: 1 })), + findOneAndUpdate: vi.fn(async ( + filter: Record, + update: { $set: Record }, + ) => { + if (filter["run._id"] !== submission.run._id) return null; + if (update.$set["run.rollover"]) { + if (submission.run.rollover) return null; + submission = { + ...submission, + run: { + ...submission.run, + rollover: update.$set[ + "run.rollover" + ] as SubmissionDocument["run"]["rollover"], + }, + }; + } else if (update.$set.run) { + if ( + filter["run.rollover.nextSubmissionRunId"] !== + submission.run.rollover?.nextSubmissionRunId + ) { + return null; + } + submission = { + ...submission, + run: update.$set.run as SubmissionDocument["run"], + }; + } else { + submission = { + ...submission, + run: { + ...submission.run, + dependencyProjection: update.$set[ + "run.dependencyProjection" + ] as SubmissionDocument["run"]["dependencyProjection"], + blocking: update.$set[ + "run.blocking" + ] as SubmissionDocument["run"]["blocking"], + }, + }; + } + return structuredClone(submission); + }), + } as unknown as Collection; + const submissionRuns = { + updateOne: vi.fn(async ( + filter: { _id: string }, + update: { $setOnInsert: SubmissionRunHistoryDocument }, + ) => { + if (!history.has(filter._id)) { + history.set(filter._id, structuredClone(update.$setOnInsert)); + } + return { matchedCount: 1, modifiedCount: 1 }; + }), + } as unknown as Collection; + const services = { + find: vi.fn(() => queryResult([])), + findOne: vi.fn(async () => null), + } as unknown as Collection; + let firstCoordinatorReads = 0; + const coordinatorA = new SubmissionCoordinator({ + requests: { + find: vi.fn(() => + queryResult([ + firstCoordinatorReads++ === 0 + ? request("run-2", 2) + : request("run-3", 3), + ]), + ), + } as unknown as Collection, + submissions, + submissionRuns, + services, + }); + const coordinatorB = new SubmissionCoordinator({ + requests: { + find: vi.fn(() => queryResult([request("run-3", 3)])), + } as unknown as Collection, + submissions, + submissionRuns, + services, + }); + + await Promise.all([ + coordinatorA.reconcile("submission-1"), + coordinatorB.reconcile("submission-1"), + ]); + + expect(submission.run.requests).toEqual([ + { requestId: "request-1", runId: "run-3" }, + ]); + expect(submission.run.rollover).toBeUndefined(); + expect(history.size).toBe(2); + const knownRunIds = new Set([...history.keys(), submission.run._id]); + for (const archived of history.values()) { + expect(knownRunIds.has(archived.supersededBySubmissionRunId!)).toBe(true); + } + }); + + it("repairs an interrupted claimed rollover on the next polling reconciliation", async () => { + const requestDocument: RequestDocument = { + _id: "request-1", + projectId: "project-1", + scenario: { task: "Task", criteria: ["criterion"] }, + workerType: "worker", + createdAt: new Date(), + priority: 0, + submissionId: "submission-1", + run: { + _id: "run-2", + attemptNumber: 2, + status: "done", + }, + }; + let submission: SubmissionDocument = { + _id: "submission-1", + submissionId: "submission-1", + projectId: "project-1", + requestIds: ["request-1"], + createdAt: new Date(), + run: { + _id: "submission-run-1", + attemptNumber: 1, + vectorFingerprint: "initial", + requests: [{ requestId: "request-1", runId: "run-1" }], + createdAt: new Date(), + dependencyProjection: [], + blocking: [], + }, + }; + const history = new Map(); + let interruptReplacement = true; + const submissions = { + findOne: vi.fn(async () => structuredClone(submission)), + updateOne: vi.fn(async () => ({ matchedCount: 1, modifiedCount: 1 })), + findOneAndUpdate: vi.fn(async ( + filter: Record, + update: { $set: Record }, + ) => { + if (filter["run._id"] !== submission.run._id) return null; + if (update.$set["run.rollover"]) { + if (submission.run.rollover) return null; + submission = { + ...submission, + run: { + ...submission.run, + rollover: update.$set[ + "run.rollover" + ] as SubmissionDocument["run"]["rollover"], + }, + }; + } else if (update.$set.run) { + if (interruptReplacement) { + interruptReplacement = false; + throw new Error("simulated process interruption"); + } + submission = { + ...submission, + run: update.$set.run as SubmissionDocument["run"], + }; + } else { + submission = { + ...submission, + run: { + ...submission.run, + dependencyProjection: update.$set[ + "run.dependencyProjection" + ] as SubmissionDocument["run"]["dependencyProjection"], + blocking: update.$set[ + "run.blocking" + ] as SubmissionDocument["run"]["blocking"], + }, + }; + } + return structuredClone(submission); + }), + } as unknown as Collection; + const submissionRuns = { + updateOne: vi.fn(async ( + filter: { _id: string }, + update: { $setOnInsert: SubmissionRunHistoryDocument }, + ) => { + if (!history.has(filter._id)) { + history.set(filter._id, structuredClone(update.$setOnInsert)); + } + return { matchedCount: 1, modifiedCount: 1 }; + }), + } as unknown as Collection; + const collections = { + requests: { + find: vi.fn(() => queryResult([requestDocument])), + } as unknown as Collection, + submissions, + submissionRuns, + services: { + find: vi.fn(() => queryResult([])), + findOne: vi.fn(async () => null), + } as unknown as Collection, + }; + + await expect( + new SubmissionCoordinator(collections).reconcile("submission-1"), + ).rejects.toThrow("simulated process interruption"); + expect(submission.run.rollover).toBeDefined(); + expect(history.size).toBe(1); + + const repaired = await new SubmissionCoordinator(collections).reconcile( + "submission-1", + ); + + expect(repaired?.submission.run.attemptNumber).toBe(2); + expect(repaired?.submission.run.requests).toEqual([ + { requestId: "request-1", runId: "run-2" }, + ]); + expect(repaired?.submission.run.rollover).toBeUndefined(); + expect(history.get("submission-run-1")?.supersededBySubmissionRunId).toBe( + repaired?.submission.run._id, + ); + }); + + it("recovers an interrupted final submission write only for complete native request groups", async () => { + const requests: RequestDocument[] = [ + { + _id: "request-1", + projectId: "project-1", + scenario: { task: "Task", criteria: ["criterion"] }, + workerType: "worker", + createdAt: new Date(), + priority: 0, + submissionId: "submission-1", + submissionExpectedRequestCount: 2, + submissionRequestIndex: 0, + submissionInitialRunId: "run-1", + run: { _id: "retried-run", attemptNumber: 2, status: "pending" }, + }, + { + _id: "request-2", + projectId: "project-1", + scenario: { task: "Task", criteria: ["criterion"] }, + workerType: "worker", + createdAt: new Date(), + priority: 0, + submissionId: "submission-1", + submissionExpectedRequestCount: 2, + submissionRequestIndex: 1, + submissionInitialRunId: "run-2", + run: { _id: "run-2", attemptNumber: 1, status: "pending" }, + }, + ]; + let stored: SubmissionDocument | null = null; + const updateMany = vi.fn(async () => ({ modifiedCount: 2 })); + const coordinator = new SubmissionCoordinator({ + requests: { + find: vi.fn(() => queryResult(requests)), + updateMany, + } as unknown as Collection, + submissions: { + findOne: vi.fn(async () => stored), + updateOne: vi.fn(async ( + _filter: unknown, + update: { $setOnInsert: SubmissionDocument }, + ) => { + stored ??= update.$setOnInsert; + return { upsertedCount: 1 }; + }), + } as unknown as Collection, + submissionRuns: {} as Collection, + services: {} as Collection, + }); + + expect(await coordinator.recoverMissingNativeSubmissions()).toBe(1); + expect(stored).not.toBeNull(); + const recovered = stored as SubmissionDocument | null; + expect(recovered?.requestIds).toEqual(["request-1", "request-2"]); + expect(recovered?.run.requests).toEqual([ + { requestId: "request-1", runId: "run-1" }, + { requestId: "request-2", runId: "run-2" }, + ]); + expect(await coordinator.recoverMissingNativeSubmissions()).toBe(1); + expect(updateMany).toHaveBeenCalledTimes(2); + expect(updateMany).toHaveBeenCalledWith( + expect.objectContaining({ + submissionId: "submission-1", + submissionFinalizedAt: { $exists: false }, + }), + expect.objectContaining({ $set: { submissionFinalizedAt: expect.any(Date) } }), + ); + }); +}); diff --git a/packages/shared/src/submissions/submission-coordinator.ts b/packages/shared/src/submissions/submission-coordinator.ts new file mode 100644 index 000000000..5720a744a --- /dev/null +++ b/packages/shared/src/submissions/submission-coordinator.ts @@ -0,0 +1,573 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { v5 as uuidv5 } from "uuid"; +import type { Collection, Document } from "mongodb"; +import type { + HandlerServiceDocument, + RequestDocument, + SubmissionDocument, + SubmissionHandlerServiceDocument, + SubmissionQueueMessagePayload, + SubmissionReportPointer, + SubmissionRunHistoryDocument, + SubmissionRunState, + ServiceHandlerDocument, +} from "../types/types.js"; +import { + OBSERVATIONS_HANDLER_ID, + READOUT_HANDLER_ID, + computeSubmissionVectorFingerprint, + deriveSubmissionReadiness, + projectSubmissionDependency, +} from "./submission-state.js"; + +const SUBMISSION_RUN_NAMESPACE = "6b0f8a6a-5cda-4cb4-bcc4-79bb0ac7f54d"; + +export interface SubmissionCollections { + requests: Collection; + submissions: Collection; + submissionRuns: Collection; + services: Collection; +} + +export type EnqueueSubmission = ( + service: SubmissionHandlerServiceDocument, + message: SubmissionQueueMessagePayload, +) => Promise; + +export interface ReconcileSubmissionResult { + submission: SubmissionDocument; + readiness: ReturnType; + rolled: boolean; + dispatched: boolean; +} + +function submissionRunId( + submissionId: string, + attemptNumber: number, + vectorFingerprint: string, +): string { + return uuidv5( + `${submissionId}:${attemptNumber}:${vectorFingerprint}`, + SUBMISSION_RUN_NAMESPACE, + ); +} + +function initialSubmissionRun( + submissionId: string, + requests: Array<{ requestId: string; runId: string }>, + createdAt: Date, +): SubmissionRunState { + const vectorFingerprint = computeSubmissionVectorFingerprint(requests); + return { + _id: submissionRunId(submissionId, 1, vectorFingerprint), + attemptNumber: 1, + vectorFingerprint, + requests, + createdAt, + dependencyProjection: [], + blocking: [], + }; +} + +export async function ensureNativeSubmission(input: { + collections: Pick; + submissionId: string; + projectId: string; + requests: Array<{ requestId: string; runId: string }>; + createdAt?: Date; +}): Promise { + const requestIds = input.requests.map((request) => request.requestId); + const createdAt = input.createdAt ?? new Date(); + const doc: SubmissionDocument = { + _id: input.submissionId, + submissionId: input.submissionId, + projectId: input.projectId, + requestIds, + createdAt, + run: initialSubmissionRun(input.submissionId, input.requests, createdAt), + }; + await input.collections.submissions.updateOne( + { _id: input.submissionId } as Document, + { $setOnInsert: doc } as Document, + { upsert: true }, + ); + const persisted = await input.collections.submissions.findOne({ + _id: input.submissionId, + } as Document); + if (!persisted) { + throw new Error(`Submission ${input.submissionId} was not persisted`); + } + const sameProject = persisted.projectId === input.projectId; + const sameRequests = + persisted.requestIds.length === requestIds.length && + persisted.requestIds.every((requestId, index) => requestId === requestIds[index]); + const sameInitialVector = + persisted.run.attemptNumber > 1 || + (persisted.run.requests.length === input.requests.length && + persisted.run.requests.every((request, index) => { + const expected = input.requests[index]; + return ( + request.requestId === expected?.requestId && + request.runId === expected.runId + ); + })); + if (!sameProject || !sameRequests || !sameInitialVector) { + throw new Error( + `Submission ${input.submissionId} already exists with a different immutable request vector`, + ); + } + return persisted; +} + +async function loadPostProcessHandlers( + services: Collection, +): Promise { + const documents = await services + .find({ type: "post-process-handler" }) + .toArray(); + return documents.filter( + (document): document is HandlerServiceDocument => + document.type === "post-process-handler", + ); +} + +async function loadReadoutService( + services: Collection, +): Promise { + const document = await services.findOne({ + _id: READOUT_HANDLER_ID, + type: "submission-handler", + }); + return document?.type === "submission-handler" ? document : null; +} + +async function archiveAndRoll(input: { + collections: SubmissionCollections; + submission: SubmissionDocument; + requests: Array<{ requestId: string; runId: string }>; + reason: "request-vector-changed" | "manual-retry"; +}): Promise { + let claimed = input.submission; + if (!claimed.run.rollover) { + const claimedAt = new Date(); + const nextAttemptNumber = claimed.run.attemptNumber + 1; + const vectorFingerprint = computeSubmissionVectorFingerprint(input.requests); + const nextSubmissionRunId = submissionRunId( + claimed._id, + nextAttemptNumber, + vectorFingerprint, + ); + const claimedDocument = await input.collections.submissions.findOneAndUpdate( + { + _id: claimed._id, + "run._id": claimed.run._id, + "run.vectorFingerprint": claimed.run.vectorFingerprint, + "run.rollover": { $exists: false }, + } as Document, + { + $set: { + "run.rollover": { + nextSubmissionRunId, + vectorFingerprint, + requests: input.requests, + reason: input.reason, + claimedAt, + }, + "run.updatedAt": claimedAt, + updatedAt: claimedAt, + }, + } as Document, + { returnDocument: "after" }, + ); + if (!claimedDocument) return null; + claimed = claimedDocument; + } + const rollover = claimed.run.rollover; + if (!rollover) return null; + const { rollover: _rollover, reconcileAfter: _reconcileAfter, ...archivedRun } = + claimed.run; + const archived: SubmissionRunHistoryDocument = { + ...archivedRun, + submissionId: claimed._id, + projectId: claimed.projectId, + supersededAt: rollover.claimedAt, + supersededBySubmissionRunId: rollover.nextSubmissionRunId, + supersededReason: rollover.reason, + }; + await input.collections.submissionRuns.updateOne( + { _id: archived._id } as Document, + { $setOnInsert: archived } as Document, + { upsert: true }, + ); + + const nextRun: SubmissionRunState = { + _id: rollover.nextSubmissionRunId, + attemptNumber: claimed.run.attemptNumber + 1, + vectorFingerprint: rollover.vectorFingerprint, + requests: rollover.requests, + createdAt: rollover.claimedAt, + dependencyProjection: [], + blocking: [], + }; + const updated = await input.collections.submissions.findOneAndUpdate( + { + _id: claimed._id, + "run._id": claimed.run._id, + "run.rollover.nextSubmissionRunId": rollover.nextSubmissionRunId, + } as Document, + { + $set: { + run: nextRun, + updatedAt: rollover.claimedAt, + }, + } as Document, + { returnDocument: "after" }, + ); + return updated; +} + +export class SubmissionCoordinator { + private recoveryAfterRequestId: string | null = null; + + constructor( + private readonly collections: SubmissionCollections, + private readonly enqueue?: EnqueueSubmission, + ) {} + + async recoverMissingNativeSubmissions(limit = 30): Promise { + const filter: Document = { + submissionId: { $exists: true }, + submissionExpectedRequestCount: { $exists: true }, + submissionFinalizedAt: { $exists: false }, + deletedAt: { $exists: false }, + ...(this.recoveryAfterRequestId + ? { _id: { $gt: this.recoveryAfterRequestId } } + : {}), + }; + const candidates = await this.collections.requests + .find(filter) + .project({ + _id: 1, + projectId: 1, + submissionId: 1, + submissionExpectedRequestCount: 1, + submissionRequestIndex: 1, + submissionInitialRunId: 1, + "run._id": 1, + createdAt: 1, + }) + .sort({ _id: 1 }) + .limit(limit) + .toArray(); + this.recoveryAfterRequestId = + candidates.length === limit ? candidates.at(-1)?._id ?? null : null; + let recovered = 0; + const visited = new Set(); + for (const candidate of candidates) { + const submissionId = candidate.submissionId; + const expected = candidate.submissionExpectedRequestCount; + if (!submissionId || !expected || visited.has(submissionId)) continue; + visited.add(submissionId); + const requests = await this.collections.requests + .find({ + submissionId, + projectId: candidate.projectId, + submissionExpectedRequestCount: expected, + deletedAt: { $exists: false }, + } as Document) + .sort({ submissionRequestIndex: 1, _id: 1 }) + .toArray(); + if ( + requests.length !== expected || + requests.some( + (request, index) => + request.submissionRequestIndex !== index || + !request.submissionInitialRunId, + ) + ) { + continue; + } + await ensureNativeSubmission({ + collections: this.collections, + submissionId, + projectId: candidate.projectId, + requests: requests.map((request) => ({ + requestId: request._id, + runId: request.submissionInitialRunId!, + })), + createdAt: requests[0]?.createdAt, + }); + await this.collections.requests.updateMany( + { + submissionId, + projectId: candidate.projectId, + submissionExpectedRequestCount: expected, + submissionFinalizedAt: { $exists: false }, + } as Document, + { $set: { submissionFinalizedAt: new Date() } } as Document, + ); + recovered++; + } + return recovered; + } + + async reconcile(submissionId: string): Promise { + let rolled = false; + for (let attempt = 0; attempt < 4; attempt++) { + const submission = await this.collections.submissions.findOne({ + _id: submissionId, + } as Document); + if (!submission) return null; + if ( + submission.run.reconcileAfter && + submission.run.reconcileAfter.getTime() > Date.now() + ) { + return { + submission, + readiness: deriveSubmissionReadiness(submission.run), + rolled, + dispatched: false, + }; + } + if (submission.run.reconcileAfter) { + await this.collections.submissions.updateOne( + { _id: submissionId, "run._id": submission.run._id } as Document, + { + $unset: { "run.reconcileAfter": "" }, + $set: { updatedAt: new Date() }, + } as Document, + ); + delete submission.run.reconcileAfter; + } + if (submission.run.rollover) { + const updated = await archiveAndRoll({ + collections: this.collections, + submission, + requests: submission.run.rollover.requests, + reason: submission.run.rollover.reason, + }); + rolled = true; + if (!updated) continue; + continue; + } + + const requestDocs = await this.collections.requests + .find({ + _id: { $in: submission.requestIds }, + projectId: submission.projectId, + deletedAt: { $exists: false }, + } as Document) + .toArray(); + const byId = new Map(requestDocs.map((request) => [request._id, request])); + const latestVector = submission.requestIds.flatMap((requestId) => { + const runId = byId.get(requestId)?.run?._id; + return runId ? [{ requestId, runId }] : []; + }); + const latestFingerprint = + latestVector.length === submission.requestIds.length + ? computeSubmissionVectorFingerprint(latestVector) + : submission.run.vectorFingerprint; + + if ( + latestVector.length === submission.requestIds.length && + latestFingerprint !== submission.run.vectorFingerprint + ) { + const updated = await archiveAndRoll({ + collections: this.collections, + submission, + requests: latestVector, + reason: "request-vector-changed", + }); + rolled = true; + if (!updated) continue; + continue; + } + + const handlers = await loadPostProcessHandlers(this.collections.services); + const projected = submission.run.requests.map((ref) => + projectSubmissionDependency({ + requestId: ref.requestId, + expectedRunId: ref.runId, + run: byId.get(ref.requestId)?.run, + handlers, + handlerId: OBSERVATIONS_HANDLER_ID, + }), + ); + const dependencyProjection = projected.map((result) => result.projection); + const blocking = projected.flatMap((result) => + result.blocking ? [result.blocking] : [], + ); + const now = new Date(); + const refreshed = await this.collections.submissions.findOneAndUpdate( + { _id: submissionId, "run._id": submission.run._id } as Document, + { + $set: { + "run.dependencyProjection": dependencyProjection, + "run.blocking": blocking, + "run.updatedAt": now, + updatedAt: now, + }, + } as Document, + { returnDocument: "after" }, + ); + if (!refreshed) continue; + + let dispatched = false; + const readiness = deriveSubmissionReadiness(refreshed.run); + if (readiness === "ready" && this.enqueue) { + const service = await loadReadoutService(this.collections.services); + const existing = refreshed.run.handlerStatus?.[READOUT_HANDLER_ID]; + const eligible = + service && + (!existing || + (service.autoBackfill && + existing.status === "done" && + (existing.version ?? 0) < service.version)); + if (service && eligible) { + await this.enqueue(service, { + type: service.selector, + submissionId, + submissionRunId: refreshed.run._id, + }); + const queuedAt = new Date(); + const marked = await this.collections.submissions.updateOne( + { + _id: submissionId, + "run._id": refreshed.run._id, + [`run.handlerStatus.${READOUT_HANDLER_ID}.status`]: { + $nin: ["queued", "processing", "failed"], + }, + } as Document, + { + $set: { + [`run.handlerStatus.${READOUT_HANDLER_ID}`]: { + status: "queued", + version: service.version, + updatedAt: queuedAt, + }, + "run.updatedAt": queuedAt, + updatedAt: queuedAt, + }, + } as Document, + ); + dispatched = marked.modifiedCount === 1; + } + } + const finalSubmission = dispatched + ? await this.collections.submissions.findOne({ _id: submissionId } as Document) + : refreshed; + if (!finalSubmission) return null; + return { + submission: finalSubmission, + readiness: deriveSubmissionReadiness(finalSubmission.run), + rolled, + dispatched, + }; + } + throw new Error(`Submission ${submissionId} could not be reconciled after concurrent updates`); + } + + async retryCurrent( + submissionId: string, + expectedSubmissionRunId: string, + ): Promise { + const reconciled = await this.reconcile(submissionId); + if (!reconciled) return null; + if (reconciled.submission.run._id !== expectedSubmissionRunId) { + return reconciled; + } + if (reconciled.readiness === "blocked" || reconciled.readiness === "waiting") { + return reconciled; + } + const rolled = await archiveAndRoll({ + collections: this.collections, + submission: reconciled.submission, + requests: reconciled.submission.run.requests, + reason: "manual-retry", + }); + if (!rolled) return this.reconcile(submissionId); + return this.reconcile(submissionId); + } +} + +export async function claimSubmissionHandler(input: { + submissions: Collection; + submissionId: string; + submissionRunId: string; + handlerId: string; + version: number; +}): Promise { + const now = new Date(); + return input.submissions.findOneAndUpdate( + { + _id: input.submissionId, + "run._id": input.submissionRunId, + "run.rollover": { $exists: false }, + [`run.handlerStatus.${input.handlerId}.status`]: { $in: ["queued"] }, + } as Document, + { + $set: { + [`run.handlerStatus.${input.handlerId}`]: { + status: "processing", + version: input.version, + updatedAt: now, + }, + "run.startedAt": now, + "run.updatedAt": now, + updatedAt: now, + }, + } as Document, + { returnDocument: "after" }, + ); +} + +export async function isCurrentSubmissionRun(input: { + submissions: Collection; + submissionId: string; + submissionRunId: string; +}): Promise { + return Boolean( + await input.submissions.findOne({ + _id: input.submissionId, + "run._id": input.submissionRunId, + "run.rollover": { $exists: false }, + } as Document), + ); +} + +export async function completeSubmissionHandler(input: { + submissions: Collection; + submissionId: string; + submissionRunId: string; + handlerId: string; + status: "done" | "failed"; + version: number; + error?: string; + report?: SubmissionReportPointer; +}): Promise { + const now = new Date(); + const set: Record = { + [`run.handlerStatus.${input.handlerId}`]: { + status: input.status, + version: input.version, + updatedAt: now, + ...(input.error ? { error: input.error } : {}), + }, + "run.updatedAt": now, + "run.finishedAt": now, + updatedAt: now, + }; + if (input.report) set["run.report"] = input.report; + const result = await input.submissions.updateOne( + { + _id: input.submissionId, + "run._id": input.submissionRunId, + "run.rollover": { $exists: false }, + [`run.handlerStatus.${input.handlerId}.status`]: "processing", + } as Document, + { $set: set } as Document, + ); + return result.modifiedCount === 1; +} diff --git a/packages/shared/src/submissions/submission-state.test.ts b/packages/shared/src/submissions/submission-state.test.ts new file mode 100644 index 000000000..854558c68 --- /dev/null +++ b/packages/shared/src/submissions/submission-state.test.ts @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, expect, it } from "vitest"; +import type { HandlerServiceDocument, RunState, SubmissionRunState } from "../types/types.js"; +import { + OBSERVATIONS_HANDLER_ID, + READOUT_HANDLER_ID, + computeSubmissionVectorFingerprint, + deriveSubmissionReadiness, + evaluateNamedHandlerState, + isHandlerDagDrained, + projectSubmissionDependency, +} from "./submission-state.js"; + +const handlers: HandlerServiceDocument[] = [ + { + _id: "pp-atif", + type: "post-process-handler", + version: 1, + queue: "post-processor", + selector: "atif", + autoBackfill: true, + dependsOn: [], + }, + { + _id: OBSERVATIONS_HANDLER_ID, + type: "post-process-handler", + version: 1, + queue: "post-processor", + selector: "observations", + autoBackfill: true, + dependsOn: ["pp-atif"], + }, +]; + +function run(handlerStatus: RunState["handlerStatus"]): RunState { + return { + _id: "run-1", + attemptNumber: 1, + status: "done", + outcome: "failed", + handlerStatus, + observationResults: [ + { criterionId: "observation-1", passed: false, feedback: "evidence", evaluated: true }, + ], + }; +} + +describe("submission state helpers", () => { + it("fingerprints vectors deterministically independent of request order", () => { + expect( + computeSubmissionVectorFingerprint([ + { requestId: "b", runId: "2" }, + { requestId: "a", runId: "1" }, + ]), + ).toBe( + computeSubmissionVectorFingerprint([ + { requestId: "a", runId: "1" }, + { requestId: "b", runId: "2" }, + ]), + ); + }); + + it("treats completed observations as ready even for failed coding outcomes and passed:false evidence", () => { + const result = projectSubmissionDependency({ + requestId: "request-1", + expectedRunId: "run-1", + run: run({ + [OBSERVATIONS_HANDLER_ID]: { status: "done", version: 1 }, + unrelated: { status: "failed", error: "ignored" }, + }), + handlers, + }); + + expect(result).toEqual({ + projection: { + requestId: "request-1", + runId: "run-1", + handlerId: OBSERVATIONS_HANDLER_ID, + state: "ready", + version: 1, + }, + drifted: false, + }); + }); + + it("blocks when observations fail or are blocked by a failed dependency", () => { + expect( + evaluateNamedHandlerState( + OBSERVATIONS_HANDLER_ID, + handlers, + { [OBSERVATIONS_HANDLER_ID]: { status: "failed" } }, + ), + ).toBe("failed"); + expect( + evaluateNamedHandlerState( + OBSERVATIONS_HANDLER_ID, + handlers, + { "pp-atif": { status: "failed" } }, + ), + ).toBe("blocked"); + }); + + it("reuses named-handler DAG semantics for existing report triggering", () => { + expect( + isHandlerDagDrained(handlers, { + "pp-atif": { status: "failed" }, + }), + ).toBe(true); + expect( + isHandlerDagDrained(handlers, { + "pp-atif": { status: "processing" }, + }), + ).toBe(false); + }); + + it("detects exact run drift", () => { + const result = projectSubmissionDependency({ + requestId: "request-1", + expectedRunId: "old-run", + run: run({ [OBSERVATIONS_HANDLER_ID]: { status: "done" } }), + handlers, + }); + expect(result.drifted).toBe(true); + expect(result.blocking?.reason).toBe("run-changed"); + }); + + it("derives submission readiness from dependencies and readout progress", () => { + const base: SubmissionRunState = { + _id: "submission-run-1", + attemptNumber: 1, + vectorFingerprint: "fingerprint", + requests: [{ requestId: "request-1", runId: "run-1" }], + createdAt: new Date(), + dependencyProjection: [{ + requestId: "request-1", + runId: "run-1", + handlerId: OBSERVATIONS_HANDLER_ID, + state: "ready", + }], + blocking: [], + }; + + expect(deriveSubmissionReadiness(base)).toBe("ready"); + expect(deriveSubmissionReadiness({ + ...base, + handlerStatus: { [READOUT_HANDLER_ID]: { status: "processing" } }, + })).toBe("processing"); + expect(deriveSubmissionReadiness({ + ...base, + handlerStatus: { [READOUT_HANDLER_ID]: { status: "done" } }, + })).toBe("done"); + }); +}); diff --git a/packages/shared/src/submissions/submission-state.ts b/packages/shared/src/submissions/submission-state.ts new file mode 100644 index 000000000..cbaf44452 --- /dev/null +++ b/packages/shared/src/submissions/submission-state.ts @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { + HandlerRunStatus, + HandlerServiceDocument, + RunState, + SubmissionBlockingDetail, + SubmissionDependencyProjection, + SubmissionHandlerState, + SubmissionReadiness, + SubmissionRequestRef, + SubmissionRunState, +} from "../types/types.js"; +import { DependencyGraph } from "../graph/dependency-graph.js"; + +export const OBSERVATIONS_HANDLER_ID = "pp-observations"; +export const READOUT_HANDLER_ID = "pp-readout"; + +export function computeSubmissionVectorFingerprint( + requests: readonly SubmissionRequestRef[], +): string { + const canonical = [...requests] + .sort((left, right) => left.requestId.localeCompare(right.requestId)) + .map(({ requestId, runId }) => `${requestId}:${runId}`) + .join("\n"); + let hash = 0xcbf29ce484222325n; + for (let index = 0; index < canonical.length; index++) { + hash ^= BigInt(canonical.charCodeAt(index)); + hash = BigInt.asUintN(64, hash * 0x100000001b3n); + } + return hash.toString(16).padStart(16, "0"); +} + +export function evaluateNamedHandlerState( + handlerId: string, + handlers: readonly HandlerServiceDocument[], + handlerStatus: Record | undefined, +): SubmissionHandlerState { + const status = handlerStatus?.[handlerId]; + if (status?.status === "done") return "ready"; + if (status?.status === "failed") return "failed"; + + const graph = new DependencyGraph( + handlers.map((handler) => ({ + id: handler._id, + dependsOn: handler.dependsOn, + })), + ); + + if (!handlers.some((handler) => handler._id === handlerId)) { + return "waiting"; + } + + for (const ancestorId of graph.getAncestors(handlerId)) { + if (handlerStatus?.[ancestorId]?.status === "failed") return "blocked"; + } + return "waiting"; +} + +function isTerminalHandlerState( + handlerId: string, + graph: DependencyGraph, + handlerStatus: Record | undefined, +): boolean { + const status = handlerStatus?.[handlerId]?.status; + if (status === "done" || status === "failed") return true; + if (status !== undefined) return false; + for (const ancestorId of graph.getAncestors(handlerId)) { + if (handlerStatus?.[ancestorId]?.status === "failed") return true; + } + return false; +} + +export function isHandlerDagDrained( + handlers: readonly HandlerServiceDocument[], + handlerStatus: Record | undefined, +): boolean { + const graph = new DependencyGraph( + handlers.map((handler) => ({ + id: handler._id, + dependsOn: handler.dependsOn, + })), + ); + return handlers.every((handler) => + isTerminalHandlerState(handler._id, graph, handlerStatus), + ); +} + +export function projectSubmissionDependency(input: { + requestId: string; + expectedRunId: string; + run?: RunState; + handlers: readonly HandlerServiceDocument[]; + handlerId?: string; +}): { + projection: SubmissionDependencyProjection; + blocking?: SubmissionBlockingDetail; + drifted: boolean; +} { + const handlerId = input.handlerId ?? OBSERVATIONS_HANDLER_ID; + if (!input.run) { + return { + projection: { + requestId: input.requestId, + runId: input.expectedRunId, + handlerId, + state: "blocked", + }, + blocking: { + requestId: input.requestId, + runId: input.expectedRunId, + handlerId, + reason: "request-missing", + }, + drifted: false, + }; + } + if (input.run._id !== input.expectedRunId) { + return { + projection: { + requestId: input.requestId, + runId: input.expectedRunId, + handlerId, + state: "blocked", + }, + blocking: { + requestId: input.requestId, + runId: input.expectedRunId, + handlerId, + reason: "run-changed", + }, + drifted: true, + }; + } + + const source = input.run.handlerStatus?.[handlerId]; + const state = evaluateNamedHandlerState( + handlerId, + input.handlers, + input.run.handlerStatus, + ); + const projection: SubmissionDependencyProjection = { + requestId: input.requestId, + runId: input.expectedRunId, + handlerId, + state, + ...(source?.version !== undefined ? { version: source.version } : {}), + ...(source?.updatedAt ? { updatedAt: source.updatedAt } : {}), + ...(source?.error ? { error: source.error } : {}), + }; + if (state !== "failed" && state !== "blocked") { + return { projection, drifted: false }; + } + return { + projection, + blocking: { + requestId: input.requestId, + runId: input.expectedRunId, + handlerId, + reason: state === "failed" ? "dependency-failed" : "dependency-blocked", + ...(source?.error ? { error: source.error } : {}), + }, + drifted: false, + }; +} + +export function deriveSubmissionReadiness(run: SubmissionRunState): SubmissionReadiness { + const readout = run.handlerStatus?.[READOUT_HANDLER_ID]; + if (readout?.status === "done") return "done"; + if (readout?.status === "failed") return "failed"; + if (readout?.status === "queued" || readout?.status === "processing") { + return "processing"; + } + if (run.blocking.length > 0) return "blocked"; + if ( + run.requests.length > 0 && + run.dependencyProjection.length === run.requests.length && + run.dependencyProjection.every((dependency) => dependency.state === "ready") + ) { + return "ready"; + } + return "waiting"; +} diff --git a/packages/shared/src/types/types.ts b/packages/shared/src/types/types.ts index f12651b0b..5f8315307 100644 --- a/packages/shared/src/types/types.ts +++ b/packages/shared/src/types/types.ts @@ -204,7 +204,7 @@ export interface ConversationTurn { timestamp: Date; criteriaResults?: CriterionResult[]; // Per-criterion breakdown from DAG evaluation /** Per-iteration observation results (kind:"observation" criteria), recorded - * by the pp-taxonomy post-processor. Same shape as criteriaResults but never + * by the pp-observations post-processor. Same shape as criteriaResults but never * gates the agent. Empty/absent when no observations were selected. #1156. */ observationResults?: CriterionResult[]; harUrl?: string; // Blob storage URL to the HAR file for this turn @@ -385,6 +385,18 @@ export interface RequestDocument { profileId?: string; // FK → ProfileDocument._id (the profile lineage) profileVersionId?: string; // FK → ProfileVersionDocument._id (exact version used) submissionId?: string; // FK → SubmissionDocument._id + /** Expected native request count for interrupted SubmissionDocument finalization. + * Historical/imported requests omit it and remain view-only if their parent + * submission document is missing. */ + submissionExpectedRequestCount?: number; + /** Stable position within the native submission's immutable request vector. */ + submissionRequestIndex?: number; + /** Run id minted for the submission's first attempt. It remains unchanged + * across request retries so recovery can rebuild the exact initial vector. */ + submissionInitialRunId?: string; + /** Set after the parent SubmissionDocument has been idempotently finalized. + * Missing on interrupted native writes so the scheduler can recover them. */ + submissionFinalizedAt?: Date; /** Scheduling priority. Higher = processed first. Default: 0. */ priority: number; /** @@ -396,7 +408,7 @@ export interface RequestDocument { gates?: GateConfig[]; /** * Observation-criteria ids (kind:"observation") selected at submit. The - * pp-taxonomy post-processor evaluates exactly this set after the run + * pp-observations post-processor evaluates exactly this set after the run * completes and writes per-turn `observationResults`. Empty/absent = none. * See issue #1156. */ @@ -532,7 +544,7 @@ export interface RunState { handlerStatus?: Record; /** Whole-run observation results — one entry per selected `subject:"run"` * observation criterion, evaluated once against the final snapshot + the - * merged trajectory across all iterations by pp-taxonomy. Per-iteration + * merged trajectory across all iterations by pp-observations. Per-iteration * (`subject:"iteration"`) observations live on `turn.observationResults` * instead. Empty/absent when no run-subject observations were selected. #1156. */ observationResults?: CriterionResult[]; @@ -567,6 +579,118 @@ export interface HandlerServiceDocument { dependsOn: string[]; } +export interface SubmissionHandlerServiceDocument { + _id: string; + type: "submission-handler"; + version: number; + queue: string; + selector: string; + autoBackfill: boolean; + dependsOn: string[]; + dependencyMode: "all-runs"; +} + +export type ServiceHandlerDocument = + | HandlerServiceDocument + | SubmissionHandlerServiceDocument; + +export interface SubmissionRequestRef { + requestId: string; + runId: string; +} + +export type SubmissionHandlerState = "waiting" | "ready" | "failed" | "blocked"; + +export interface SubmissionDependencyProjection { + requestId: string; + runId: string; + handlerId: string; + state: SubmissionHandlerState; + version?: number; + updatedAt?: Date; + error?: string; +} + +export interface SubmissionBlockingDetail { + requestId: string; + runId: string; + handlerId: string; + reason: + | "request-missing" + | "run-changed" + | "dependency-not-ready" + | "dependency-failed" + | "dependency-blocked"; + error?: string; +} + +export interface SubmissionReportPointer { + reportId?: string; + url?: string; + updatedAt?: Date; +} + +export interface SubmissionRolloverClaim { + nextSubmissionRunId: string; + vectorFingerprint: string; + requests: SubmissionRequestRef[]; + reason: "request-vector-changed" | "manual-retry"; + claimedAt: Date; +} + +export interface SubmissionRunState { + _id: string; + attemptNumber: number; + vectorFingerprint: string; + requests: SubmissionRequestRef[]; + createdAt: Date; + updatedAt?: Date; + startedAt?: Date; + finishedAt?: Date; + dependencyProjection: SubmissionDependencyProjection[]; + handlerStatus?: Record; + blocking: SubmissionBlockingDetail[]; + report?: SubmissionReportPointer; + /** Internal scheduler hold used by bulk request retry so one completed + * replacement batch produces one vector rollover. Expiry self-heals crashes. */ + reconcileAfter?: Date; + /** Internal resumable rollover claim. Its CAS winner exclusively determines + * the immutable archive's successor metadata. */ + rollover?: SubmissionRolloverClaim; + supersededAt?: Date; + supersededBySubmissionRunId?: string; + supersededReason?: "request-vector-changed" | "manual-retry"; +} + +export interface SubmissionDocument { + _id: string; + submissionId: string; + projectId: string; + requestIds: string[]; + createdAt: Date; + updatedAt?: Date; + run: SubmissionRunState; +} + +export interface SubmissionRunHistoryDocument extends SubmissionRunState { + submissionId: string; + projectId: string; +} + +export type SubmissionReadiness = + | "waiting" + | "ready" + | "blocked" + | "processing" + | "done" + | "failed"; + +export interface SubmissionQueueMessagePayload { + type: string; + submissionId: string; + submissionRunId: string; +} + /** * Field names whose values move from `RequestDocument` (top-level, legacy * shape) into `RequestDocument.run: RunState` (new shape introduced for diff --git a/website/src/openapi/scope-openapi.json b/website/src/openapi/scope-openapi.json index ca95e2002..eb0e8dfd8 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"}}},"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"},"priority":{"type":"integer","default":0},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"observations":{"type":"array","items":{"type":"string"}},"gateSummaries":{"type":"array","items":{"$ref":"#/components/schemas/GateRunSummary"}},"run":{"$ref":"#/components/schemas/RunState"},"projectId":{"type":"string"}},"required":["_id","scenario","workerType","createdAt","projectId"]},"Scenario":{"type":"object","properties":{"version":{"type":"string","enum":["v1","v2"]},"task":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}}},"required":["task","criteria"]},"Persona":{"type":"object","properties":{"personality":{"type":"string","enum":["demanding","friendly"]},"experience":{"type":"string","enum":["junior","senior"]},"verbosity":{"type":"string","enum":["brief","moderate"]},"type":{"type":"string","enum":["traditional","ai_assisted","vibe"]}},"required":["personality","experience","verbosity","type"]},"ResourceBinding":{"type":"object","properties":{"ref":{"type":"string"},"revisionId":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref","revisionId","params"]},"GateConfig":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"promptId":{"type":"string"},"promptText":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}},"maxIterations":{"type":"integer","minimum":1,"maximum":50}},"required":["gate","criteria"]},"GateRunSummary":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"status":{"type":"string","enum":["passed","failed","skipped"]},"iterations":{"type":"integer","minimum":0}},"required":["gate","status","iterations"]},"RunState":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"queuedQueueName":{"type":"string"},"outcome":{"type":"string","enum":["succeeded","failed","finished"]},"result":{"type":"string"},"error":{"type":"string"},"logsUrl":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"turns":{"type":"array","items":{"$ref":"#/components/schemas/ConversationTurn"}},"workerVersion":{"type":"string"},"os":{"type":"object","properties":{"platform":{"type":"string"},"release":{"type":"string"},"arch":{"type":"string"}},"required":["platform","release","arch"]},"lastHeartbeatAt":{"type":["string","null"],"format":"date-time"},"worker":{"type":"object","properties":{"instanceId":{"type":"string"},"podName":{"type":"string"}},"required":["instanceId"]},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"setupVideoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"aiCallCount":{"type":"number"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRunOutcome"}},"mcpRegistered":{"type":"boolean"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"pausedAt":{"type":["string","null"],"format":"date-time"},"resumedAt":{"type":["string","null"],"format":"date-time"},"observationResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}}},"required":["_id","attemptNumber","status"]},"ConversationTurn":{"type":"object","properties":{"iteration":{"type":"number"},"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"codingAgentResponse":{"type":"string"},"judgeFeedback":{"type":"string"},"snapshotUrl":{"type":"string"},"passed":{"type":"boolean"},"timestamp":{"type":["string","null"],"format":"date-time"},"criteriaResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"observationResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"startedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"toolCalls":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"arguments":{"type":"object","additionalProperties":{}},"response":{"type":"string"},"timestamp":{"type":"string"}},"required":["id","name","arguments"]}},"toolCallsUrl":{"type":"string"},"toolCallCount":{"type":"number"},"aiCallCount":{"type":"number"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"chatResultUrl":{"type":"string"},"chatResultFormat":{"type":"string"}},"required":["iteration","judgeFeedback","snapshotUrl","passed","timestamp"]},"CriterionResult":{"type":"object","properties":{"criterionId":{"type":"string"},"passed":{"type":"boolean"},"feedback":{"type":"string"},"evaluated":{"type":"boolean"}},"required":["criterionId","passed","feedback","evaluated"]},"TokenUsage":{"type":"object","properties":{"promptTokens":{"type":"number"},"completionTokens":{"type":"number"},"totalTokens":{"type":"number"}},"required":["promptTokens","completionTokens","totalTokens"]},"ResourceRunOutcome":{"type":"object","properties":{"ref":{"type":"string"},"slug":{"type":"string"},"revisionId":{"type":"string"},"setupSucceeded":{"type":"boolean"},"published":{"type":"array","items":{"type":"string"}},"params":{"type":"object","additionalProperties":{"type":"string"}},"setupDurationMs":{"type":"number"},"error":{"type":"string"},"teardownRan":{"type":"boolean"}},"required":["ref","slug","revisionId","setupSucceeded","published"]},"ResourceBindingSpec":{"anyOf":[{"type":"string"},{"type":"object","properties":{"ref":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref"]}]},"CreateRequestInput":{"type":"object","properties":{"scenario":{"$ref":"#/components/schemas/Scenario"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"maxIterations":{"type":"integer","minimum":1,"maximum":50},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"profileId":{"type":"string"},"profileVariations":{"type":"array","items":{"type":"string"}},"priority":{"type":"integer"},"agentsMd":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"observations":{"type":"array","items":{"type":"string"}}},"required":["scenario"]},"RunFacetsResponse":{"type":"object","properties":{"total":{"type":"number"},"facets":{"type":"object","properties":{"workerType":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"status":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"outcome":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"model":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"os":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"priority":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"agentVersion":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"profileId":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}}},"required":["workerType","status","outcome","model","os","priority","agentVersion","profileId"]}},"required":["total","facets"]},"RunFacetBucket":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"number"}},"required":["value","count"]},"PaginatedRunsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"Cursors":{"type":"object","properties":{"next":{"type":["string","null"]},"prev":{"type":["string","null"]}},"required":["next","prev"]},"PaginatedRunGroupsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RunGroup"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"RunGroup":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"runIds":{"type":"array","items":{"type":"string"}},"aggregates":{"$ref":"#/components/schemas/GroupAggregates"},"uniform":{"$ref":"#/components/schemas/GroupUniformValues"}},"required":["key","label","runIds","aggregates","uniform"]},"GroupAggregates":{"type":"object","properties":{"count":{"type":"number"},"turns":{"$ref":"#/components/schemas/AggregateStats"},"duration":{"$ref":"#/components/schemas/AggregateStats"},"promptTokens":{"$ref":"#/components/schemas/AggregateStats"},"completionTokens":{"$ref":"#/components/schemas/AggregateStats"},"statusCounts":{"type":"object","additionalProperties":{"type":"number"}},"outcomeCounts":{"type":"object","additionalProperties":{"type":"number"}}},"required":["count","turns","duration","promptTokens","completionTokens","statusCounts","outcomeCounts"]},"AggregateStats":{"type":["object","null"],"properties":{"min":{"type":"number"},"max":{"type":"number"},"mean":{"type":"number"},"stdDev":{"type":"number"}},"required":["min","max","mean","stdDev"]},"GroupUniformValues":{"type":"object","properties":{"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":"string"},"platform":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"extensions":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"submissionId":{"type":"string"},"task":{"type":"string"}}},"BulkResubmitInput":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"count":{"type":"integer","minimum":1,"maximum":10,"default":1},"overrides":{"type":"object","properties":{"profileId":{"type":["string","null"]},"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":["string","null"]},"reasoningEffort":{"type":["string","null"]},"maxIterations":{"type":["number","null"]},"mcpServers":{"type":["array","null"],"items":{"type":"string"}},"skillRevisions":{"type":["array","null"],"items":{"type":"string"}},"extensions":{"type":["array","null"],"items":{"type":"string"}}}}},"required":["ids"]},"ReportResponse":{"type":"object","properties":{"_id":{"type":"string"},"requestId":{"type":"string"},"templateId":{"type":"string"},"reporter":{"$ref":"#/components/schemas/Reporter"},"content":{"type":"string"},"status":{"type":"string","enum":["pending","generating","completed","failed"]},"error":{"type":"string"},"insightReferences":{"type":"array","items":{"$ref":"#/components/schemas/InsightReference"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","requestId","status","createdAt","projectId"]},"Reporter":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"gitHash":{"type":"string"},"model":{"type":"string"},"agentId":{"type":"string"},"agentVersion":{"type":"string"}},"required":["id","name","gitHash","model","agentId","agentVersion"]},"InsightReference":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]},"CreateCriteriaInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"],"default":"gate"},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}},"required":["id","prompt"]},"CriteriaResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"CriteriaGraph":{"type":"object","properties":{"nodes":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphNode"}},"edges":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphEdge"}}},"required":["nodes","edges"]},"CriteriaGraphNode":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}},"required":["id","prompt"]},"CriteriaGraphEdge":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"]},"UpdateCriteriaInput":{"type":"object","properties":{"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}}},"CreatePromptFeatureInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["id","prompt"]},"PromptType":{"type":"string","enum":["select","build","test","run","deploy","agents.md"]},"PromptFeatureResult":{"type":"object","properties":{"featureId":{"type":"string"},"detected":{"type":"boolean"},"evaluated":{"type":"boolean"}},"required":["featureId","detected","evaluated"]},"SuggestedPromptFeature":{"type":"object","properties":{"suggestedId":{"type":"string"},"behavior":{"type":"string"},"prompt":{"type":"string"}},"required":["suggestedId","behavior","prompt"]},"PromptFeatureResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"UpdatePromptFeatureInput":{"type":"object","properties":{"prompt":{"type":"string"}}},"TaskPromptResponse":{"type":"object","properties":{"_id":{"type":"string"},"keyId":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"text":{"type":"string"},"contentBlobUrl":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","keyId","createdAt","projectId"]},"CreateTaskPromptInput":{"type":"object","properties":{"text":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["text"]},"PatchTaskPromptFeatureInput":{"type":"object","properties":{"detected":{"type":"boolean"}},"required":["detected"]},"CreateReportInput":{"type":"object","properties":{"requestId":{"type":"string"},"templateId":{"type":"string"}},"required":["requestId"]},"BulkCreateReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}},"templateId":{"type":"string"}},"required":["requestIds"]},"BulkReportStatusInput":{"type":"object","properties":{"reportIds":{"type":"array","items":{"type":"string"}}},"required":["reportIds"]},"BulkReportSummaryResponse":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/ReportSummary"}},"ReportSummary":{"type":"object","properties":{"total":{"type":"number"},"pending":{"type":"number"},"generating":{"type":"number"},"completed":{"type":"number"},"failed":{"type":"number"}},"required":["total","pending","generating","completed","failed"]},"BulkReportSummaryInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"TriggerReportsInput":{"type":"object","properties":{"requestId":{"type":"string"}},"required":["requestId"]},"BulkTriggerReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"InsightResponse":{"type":"object","properties":{"_id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"upvotes":{"type":"number"},"downvotes":{"type":"number"},"blocked":{"type":"boolean"},"referenceCount":{"type":"number"},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","title","description","upvotes","downvotes","blocked","referenceCount","createdBy","createdAt","projectId"]},"ProfileWithVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"version":{"$ref":"#/components/schemas/ProfileVersionResponse"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","version","projectId"]},"ProfileVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"profileId":{"type":"string"},"version":{"type":"number"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","profileId","version","workerType","model","createdAt","projectId"]},"CreateProfileInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["name","workerType","model"]},"ProfileResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","projectId"]},"UpdateProfileIdentity":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512}}},"ReportTemplateResponse":{"type":"object","properties":{"_id":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","id","name","userPrompt","createdAt","projectId"]},"ReportTemplateSystemPrompt":{"type":"object","properties":{"mode":{"type":"string","enum":["append","override"]},"content":{"type":"string"}},"required":["mode","content"]},"ReportTrigger":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["always"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["criteria"]},"criteriaIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","criteriaIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["taskPrompt"]},"taskPromptIds":{"type":"array","items":{"type":"string"}}},"required":["type","taskPromptIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["promptFeature"]},"featureIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","featureIds"]}]},"CreateReportTemplateInput":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0}},"required":["id","name","userPrompt"]},"UpdateReportTemplateInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"allOf":[{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},{"type":["object","null"]}]},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":["string","null"]},"timeoutMs":{"type":["integer","null"],"exclusiveMinimum":0}}},"AgentResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"},"versions":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","supportedModels","createdAt"]},"AgentCapabilities":{"type":"object","properties":{"supportsReasoningEffort":{"type":"boolean"},"supportsMcpServers":{"type":"boolean"},"supportsSkills":{"type":"boolean"},"supportsExtensions":{"type":"boolean"},"supportsResources":{"type":"boolean"}}},"AgentVersion":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string"},"status":{"type":"string","enum":["active","retired"]},"createdAt":{"type":["string","null"],"format":"date-time"}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","status","createdAt"]},"CreateAgentInput":{"type":"object","properties":{"_id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}},"required":["_id","name"]},"UpdateAgentInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}}},"RegisterAgentVersionInput":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string","minLength":1}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","queueName"]},"PatchAgentVersionInput":{"type":"object","properties":{"status":{"type":"string","enum":["active","retired"]}},"required":["status"]},"ModelResponse":{"type":"object","properties":{"_id":{"type":"string"},"modelId":{"type":"string"},"provider":{"type":"string"},"agentId":{"type":"string"},"firstSeenAt":{"type":["string","null"],"format":"date-time"},"lastSeenAt":{"type":["string","null"],"format":"date-time"},"disappearedAt":{"type":["string","null"],"format":"date-time"},"providerAvailableFrom":{"type":["string","null"],"format":"date-time"},"providerEndOfLife":{"type":["string","null"],"format":"date-time"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"$ref":"#/components/schemas/ModelCapabilities"}},"required":["_id","modelId","provider","agentId","firstSeenAt","lastSeenAt"]},"ModelCapabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}},"McpServerResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","type","createdAt","projectId"]},"McpServerHeader":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}},"required":["name","value"]},"UpdateMcpServerInput":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}}},"SkillResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","source","skillName","name","origin","createdAt","projectId"]},"SkillSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"source":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"installs":{"type":"number"}},"required":["id","name","source","internal"]},"SkillDiscoveryResult":{"type":"object","properties":{"skillName":{"type":"string"},"skillPath":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"existsInLibrary":{"type":"boolean"},"currentRevisionCommitSha":{"type":"string"},"latestUpstreamCommitSha":{"type":"string"},"updateAvailable":{"type":"boolean"},"lastImportedAt":{"type":"string"}},"required":["skillName","skillPath"]},"SkillRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"ref":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"skillPath":{"type":"string"},"commitHash":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"name":{"type":"string"},"description":{"type":"string"},"license":{"type":"string"},"compatibility":{"type":"string"},"allowedTools":{"type":"string"},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"content":{"type":"string"},"archiveUrl":{"type":"string"},"validationWarnings":{"type":"array","items":{"type":"string"}},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","ref","source","skillName","skillPath","commitHash","commitTimestamp","name","description","content","archiveUrl","resolvedAt","createdAt","projectId"]},"CreateSkillInput":{"type":"object","properties":{"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]}},"required":["source","skillName","name","origin"]},"CodebaseResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"defaultBranch":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","sourceType","revisionCounter","createdAt","projectId"]},"UpdateCodebaseInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"defaultBranch":{"type":"string"}}},"CodebaseRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"codebaseId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"requestedRef":{"type":"string"},"resolvedCommitSha":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"originalFilename":{"type":"string"},"contentSha256":{"type":"string"},"archiveUrl":{"type":"string"},"sizeBytes":{"type":"number"},"fileCount":{"type":"number"},"creator":{"type":"string"},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","codebaseId","slug","revisionNumber","ref","sourceType","archiveUrl","resolvedAt","createdAt","projectId"]},"ResolveCodebaseRevisionInput":{"type":"object","properties":{"requestedRef":{"type":"string"},"creator":{"type":"string"}}},"ResourceResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"latestRevisionNumber":{"type":"number"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","revisionCounter","createdAt","projectId"]},"CreateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1},"description":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["name","setup"]},"ResourceScript":{"type":"object","properties":{"sh":{"type":"string","minLength":1}}},"ResourceParameter":{"type":"object","properties":{"name":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"description":{"type":"string"},"required":{"type":"boolean","default":false},"default":{"type":"string"},"example":{"type":"string"}},"required":["name"]},"UpdateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"ResourceRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"resourceId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"contentSha256":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","resourceId","slug","revisionNumber","ref","setup","exports","contentSha256","createdAt","projectId"]},"CreateResourceRevisionInput":{"type":"object","properties":{"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["setup"]},"ExtensionResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","publisher","name","origin","createdAt","projectId"]},"ExtensionSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"publisher":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"version":{"type":"string"}},"required":["id","name","publisher","internal"]},"CreateExtensionInput":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+$"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]}},"required":["_id","publisher","name","origin"]},"UpdateExtensionInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ExtensionVersionInfo":{"type":"object","properties":{"version":{"type":"string"},"preRelease":{"type":"boolean"},"lastUpdated":{"type":"string"}},"required":["version","preRelease","lastUpdated"]},"CreateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"}},"required":["title","description"]},"UpdateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}}}},"FeatureFlagResponse":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"enabled":{"type":"boolean"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["key","label","enabled","updatedAt"]},"UpdateFeatureFlagInput":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]}},"parameters":{}},"paths":{"/api/v1/keys/preview":{"post":{"tags":["Keys"],"summary":"Preview key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}}},"/api/v1/keys":{"post":{"tags":["Keys"],"summary":"Create key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"201":{"description":"Key created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"get":{"tags":["Keys"],"summary":"List keys","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KeyResponse"}}}}}}}},"/api/v1/keys/{id}":{"get":{"tags":["Keys"],"summary":"Get key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"put":{"tags":["Keys"],"summary":"Update key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"delete":{"tags":["Keys"],"summary":"Delete key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Key deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/keys/{id}/validate":{"post":{"tags":["Keys"],"summary":"Validate key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateKeyInput"}}}},"responses":{"200":{"description":"Validation result","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Key validation result"}}}}}}},"/api/v1/accounts":{"post":{"tags":["Accounts"],"summary":"Create account","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"201":{"description":"Account created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"get":{"tags":["Accounts"],"summary":"List accounts","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccountResponse"}}}}}}}},"/api/v1/accounts/{id}":{"get":{"tags":["Accounts"],"summary":"Get account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"put":{"tags":["Accounts"],"summary":"Update account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"200":{"description":"Account updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"delete":{"tags":["Accounts"],"summary":"Delete account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Account deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/users/me":{"get":{"tags":["Users"],"summary":"Get the authenticated user's identity","description":"Read the existing authenticated Scope identity without enrollment or profile writes. Responses must not be HTTP-cached.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is not enrolled or is disabled"},"503":{"description":"Authentication service unavailable"}}},"post":{"tags":["Users"],"summary":"Enroll the authenticated user","description":"JIT-enroll the authenticated user, refresh their profile and lastLoginAt, apply bootstrap-admin rules, and warm the access cache. Use this only after an explicit IdP login; do not prefetch, poll, or automatically retry transient failures.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is disabled"},"503":{"description":"Authentication service unavailable"}}}},"/health":{"get":{"tags":["Health"],"summary":"Liveness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"version":{"type":"string"}},"required":["status","version"]}}}}}}},"/ready":{"get":{"tags":["Health"],"summary":"Readiness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"migrations":{}},"required":["status"]}}}},"503":{"description":"Service is not ready"}}}},"/about":{"get":{"tags":["System"],"summary":"API metadata","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"version":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"description":{"type":"string"},"workers":{"type":"array","items":{"type":"string"}}},"required":["name","version","buildTime","environment","description","workers"]}}}}}}},"/api/v1/version":{"get":{"tags":["System"],"summary":"Version info","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"commit":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"strictAgentCapabilities":{"type":"boolean"}},"required":["commit","buildTime","environment","strictAgentCapabilities"]}}}}}}},"/api/v1/projects":{"get":{"tags":["Projects"],"summary":"List all projects","parameters":[{"schema":{"type":"string","enum":["true","false"],"description":"When true, include soft-deleted projects in the result"},"required":false,"description":"When true, include soft-deleted projects in the result","name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProjectResponse"}}}}}}},"post":{"tags":["Projects"],"summary":"Create a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/projects/{id}":{"get":{"tags":["Projects"],"summary":"Get a project","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"patch":{"tags":["Projects"],"summary":"Update a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProjectInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"delete":{"tags":["Projects"],"summary":"Soft-delete a project","responses":{"204":{"description":"Success"},"404":{"description":"Project not found"}}}},"/api/v1/projects/{id}/restore":{"post":{"tags":["Projects"],"summary":"Restore a soft-deleted project","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}}},"/api/v1/requests":{"post":{"tags":["Requests"],"summary":"Submit request(s)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/CreateRequestInput"},{"type":"object","properties":{"count":{"type":"number","minimum":1,"maximum":10,"default":1},"skills":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"codebase":{"type":"string"}}}]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RequestResponse"},{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}]}}}}}},"get":{"tags":["Requests"],"summary":"List requests","parameters":[{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"profileId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"status","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"outcome","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"model","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"os","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"priority","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"agentVersion","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"type":"string","enum":["task","submissionId","profile"]},"required":false,"name":"groupBy","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"after","in":"query"},{"schema":{"type":"string"},"required":false,"name":"before","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"last","in":"query"},{"schema":{"type":"string","enum":["created","updated","priority","worker","status","id","duration","createdAt"]},"required":false,"name":"sortBy","in":"query"},{"schema":{"type":"string","enum":["asc","desc"]},"required":false,"name":"sortDir","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdAfter","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdBefore","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"turns","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"turnsOp","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"maxIterations","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"maxIterationsOp","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PaginatedRunsResponse"},{"$ref":"#/components/schemas/PaginatedRunGroupsResponse"}]}}}}}}},"/api/v1/requests/facets":{"get":{"tags":["Requests"],"summary":"List run filter facets","description":"Returns every distinct value and its full-dataset count per categorical filter dimension for the Runs list rail. Counts are absolute over all non-deleted runs **in the given project**: they intentionally ignore the active search, date range, iteration, and categorical selections so every selectable value stays visible with a stable count. Requires ?projectId=. Computed with parallel $group aggregations (Cosmos has no $facet) and cached per-project for a short TTL.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunFacetsResponse"}}}}}}},"/api/v1/requests/{id}":{"get":{"tags":["Requests"],"summary":"Get request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestResponse"}}}},"404":{"description":"Not found"}}},"delete":{"tags":["Requests"],"summary":"Soft-delete request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Not found"}}}},"/api/v1/analysis":{"get":{"tags":["Requests"],"summary":"Compute pass@k / success@T metrics","description":"Aggregates pass@k / success@T metrics over a single project's done runs. Requires ?projectId=.","parameters":[{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"k","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Analysis metrics"}}}}}}},"/api/v1/requests/bulk-resubmit":{"post":{"tags":["Requests"],"summary":"Bulk resubmit requests","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkResubmitInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}}}}}}},"/api/v1/requests/bulk":{"delete":{"tags":["Requests"],"summary":"Bulk soft-delete requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}},"required":["ids"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"number"}},"required":["deleted"]}}}}}}},"/api/v1/requests/archive":{"post":{"tags":["Requests"],"summary":"Download batch archive of multiple runs","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Gzipped batch archive containing individual run archives"},"400":{"description":"Invalid input"},"404":{"description":"One or more runs not found"}}}},"/api/v1/runs/upload":{"post":{"tags":["Requests"],"summary":"Import run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"}}}},"/api/v1/runs/upload-batch":{"post":{"tags":["Requests"],"summary":"Import batch run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"207":{"description":"Multi-status: 201 if all runs imported, 400 if none imported, 207 if partial"},"400":{"description":"No archive uploaded, empty archive, or all runs failed"}}}},"/api/v1/requests/{id}/reports":{"get":{"tags":["Reports"],"summary":"Get reports for request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Run not found"}}}},"/api/v1/requests/{id}/runs":{"get":{"tags":["Requests"],"summary":"List attempts for a request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RunState"}}}}},"404":{"description":"Request not found"}}}},"/api/v1/requests/{id}/runs/{runId}":{"get":{"tags":["Requests"],"summary":"Get a single attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunState"}}}},"404":{"description":"Request or run not found"}}}},"/api/v1/requests/bulk-retry":{"post":{"tags":["Requests"],"summary":"Bulk retry requests (start new attempts)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"force":{"type":"boolean"}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"retried":{"type":"integer"},"skipped":{"type":"integer"},"results":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"},"error":{"type":"string"}},"required":["requestId"]}}},"required":["retried","skipped","results"]}}}}}}},"/api/v1/requests/{id}/retry":{"post":{"tags":["Requests"],"summary":"Retry a request (start a new attempt)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"force":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"}},"required":["requestId","runId","attemptNumber"]}}}},"404":{"description":"Request not found"},"409":{"description":"Conflict — request is not in a retryable state, or a concurrent retry won the race"},"422":{"description":"Cannot retry — current run not yet terminal"}}}},"/api/v1/requests/{id}/priority":{"post":{"tags":["Requests"],"summary":"Set priority on a single request","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"priority":{"type":"integer"}},"required":["priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"priority":{"type":"number"}},"required":["id","priority"]}}}}}}},"/api/v1/requests/bulk-priority":{"post":{"tags":["Requests"],"summary":"Set priority on multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100},"priority":{"type":"integer"}},"required":["ids","priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/pause":{"post":{"tags":["Requests"],"summary":"Pause a single request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/{id}/resume":{"post":{"tags":["Requests"],"summary":"Resume a paused request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/bulk-pause":{"post":{"tags":["Requests"],"summary":"Pause multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/bulk-resume":{"post":{"tags":["Requests"],"summary":"Resume multiple paused requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/cancel":{"post":{"tags":["Requests"],"summary":"Cancel a request (marks as done/failed and signals worker to exit)","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"previousStatus":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"}},"required":["id","previousStatus","status","outcome"]}}}}}}},"/api/v1/requests/bulk-cancel":{"post":{"tags":["Requests"],"summary":"Cancel multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"cancelled":{"type":"number"},"skipped":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"cancelled":{"type":"boolean"},"previousStatus":{"type":"string"},"error":{"type":"string"}},"required":["id","cancelled"]}}},"required":["cancelled","skipped","results"]}}}}}}},"/api/v1/requests/{id}/logs":{"get":{"tags":["Requests"],"summary":"Stream request logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/logs":{"get":{"tags":["Requests"],"summary":"Stream logs for a specific attempt (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific attempt and iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/video":{"get":{"tags":["Requests"],"summary":"Download session recording","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/video":{"get":{"tags":["Requests"],"summary":"Download session recording for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/criteria/generate-prompt":{"post":{"tags":["Criteria"],"summary":"Generate criterion prompt from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"},"gates":{"type":"array","items":{"type":"string"}}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"},"suggestedId":{"type":"string"},"suggestedParents":{"type":"array","items":{"type":"string"}},"suggestedChildren":{"type":"array","items":{"type":"string"}}},"required":["prompt","suggestedId","suggestedParents","suggestedChildren"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/criteria/seed":{"post":{"tags":["Criteria"],"summary":"Seed criteria in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"criteria":{"type":"array","items":{"$ref":"#/components/schemas/CreateCriteriaInput"}}},"required":["criteria"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}},"400":{"description":"Seed batch would introduce a dependency cycle"}}}},"/api/v1/criteria":{"get":{"tags":["Criteria"],"summary":"List criteria","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","description":"Comma-separated criterion IDs to include"},"required":false,"description":"Comma-separated criterion IDs to include","name":"ids","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"When true and ids is set, also include dependency ancestors"},"required":false,"description":"When true and ids is set, also include dependency ancestors","name":"ancestors","in":"query"},{"schema":{"type":"string","enum":["gate","observation"],"description":"Filter by criterion kind"},"required":false,"description":"Filter by criterion kind","name":"kind","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaResponse"}}}}}}},"post":{"tags":["Criteria"],"summary":"Create criterion","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCriteriaInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"409":{"description":"Criterion already exists"}}}},"/api/v1/criteria/mdp":{"get":{"tags":["Criteria"],"summary":"Compute MDP transitions","parameters":[{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"since","in":"query"},{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{}}}}}}}},"/api/v1/criteria/graph":{"get":{"tags":["Criteria"],"summary":"Get criteria DAG","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaGraph"}}}}}}},"/api/v1/criteria/{id}":{"get":{"tags":["Criteria"],"summary":"Get criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"404":{"description":"Criterion not found"}}},"put":{"tags":["Criteria"],"summary":"Update criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCriteriaInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"400":{"description":"Invalid dependency reference or self-reference"},"404":{"description":"Criterion not found"}}},"delete":{"tags":["Criteria"],"summary":"Soft-delete criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Criterion not found"},"409":{"description":"Criterion has dependents"}}}},"/api/v1/prompt-features/generate-prompt":{"post":{"tags":["Prompt Features"],"summary":"Generate prompt feature from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"}},"required":["prompt"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features/seed":{"post":{"tags":["Prompt Features"],"summary":"Seed prompt features in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}},"required":["features"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}}}}},"/api/v1/prompt-features/extract-from-text":{"post":{"tags":["Prompt Features"],"summary":"Extract features from text","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string"},"model":{"type":"string"},"type":{"type":"string","enum":["select","agents.md"]}},"required":["text"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["features","cached"]}}}},"400":{"description":"Empty text string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features":{"get":{"tags":["Prompt Features"],"summary":"List features","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","enum":["select","agents.md"]},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}}}}},"post":{"tags":["Prompt Features"],"summary":"Create feature","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"409":{"description":"Feature already exists"}}}},"/api/v1/prompt-features/{id}":{"get":{"tags":["Prompt Features"],"summary":"Get feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"404":{"description":"Feature not found"}}},"put":{"tags":["Prompt Features"],"summary":"Update feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Feature not found"}}},"delete":{"tags":["Prompt Features"],"summary":"Soft-delete feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Feature not found"}}}},"/api/v1/task-prompts/generate":{"post":{"tags":["Task Prompts"],"summary":"Generate task prompts","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string"},"existingPrompt":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"tasks":{"type":"array","items":{"type":"string"}}},"required":["tasks"]}}}},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts":{"get":{"tags":["Task Prompts"],"summary":"List task prompts","parameters":[{"schema":{"type":["number","null"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":["number","null"]},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"$ref":"#/components/schemas/PromptType"},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/TaskPromptResponse"}},"total":{"type":"number"},"limit":{"type":"number"},"offset":{"type":"number"}},"required":["items","total","limit","offset"]}}}}}},"post":{"tags":["Task Prompts"],"summary":"Create or find task prompt","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTaskPromptInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Empty text string"}}}},"/api/v1/task-prompts/{id}":{"get":{"tags":["Task Prompts"],"summary":"Get task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"404":{"description":"Task prompt not found"}}},"delete":{"tags":["Task Prompts"],"summary":"Soft-delete task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/content":{"get":{"tags":["Task Prompts"],"summary":"Get resolved task prompt content","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"}},"required":["id","text"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/extract-features":{"post":{"tags":["Task Prompts"],"summary":"Extract features from task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"force","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"taskPromptId":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["taskPromptId","features","cached"]}}}},"404":{"description":"Task prompt not found"},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts/{id}/features/{featureId}":{"patch":{"tags":["Task Prompts"],"summary":"Toggle feature flag on task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"featureId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchTaskPromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Invalid detected value"},"404":{"description":"Task prompt or feature not found"}}}},"/api/v1/reports":{"post":{"tags":["Reports"],"summary":"Create report","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Run or template not found"}}},"get":{"tags":["Reports"],"summary":"List reports","parameters":[{"schema":{"type":"string"},"required":false,"name":"requestId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}}}}},"/api/v1/reports/bulk-create":{"post":{"tags":["Reports"],"summary":"Bulk create reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/bulk-status":{"post":{"tags":["Reports"],"summary":"Bulk get report statuses","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportStatusInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/bulk-summary":{"post":{"tags":["Reports"],"summary":"Bulk get report summary per run","description":"Returns aggregated report status counts per run. Only the latest report per template is counted (re-triggers are deduplicated).","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}":{"get":{"tags":["Reports"],"summary":"Get report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"404":{"description":"Report not found"}}}},"/api/v1/reports/{id}/logs":{"get":{"tags":["Reports"],"summary":"Stream report logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Report not found"}}}},"/api/v1/reports/trigger":{"post":{"tags":["Reports"],"summary":"Trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"triggered":{"type":"number"},"reports":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}},"required":["triggered","reports"]}}}},"400":{"description":"Invalid input"},"404":{"description":"Run not found"}}}},"/api/v1/reports/bulk-trigger":{"post":{"tags":["Reports"],"summary":"Bulk trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkTriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":{}}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}/insights":{"get":{"tags":["Reports"],"summary":"Get insights for report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/InsightResponse"},{"type":"object","properties":{"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}}}]}}}}},"404":{"description":"Report not found"}}},"post":{"tags":["Reports"],"summary":"Link insight to report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"isNew":{"type":"boolean"}},"required":["insightId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]}}}},"404":{"description":"Report or insight not found"},"409":{"description":"Insight already referenced by this report"}}}},"/api/v1/profiles":{"post":{"tags":["Profiles"],"summary":"Create a new profile","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"get":{"tags":["Profiles"],"summary":"List profiles","parameters":[{"schema":{"type":"string"},"required":false,"name":"workerType","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}":{"get":{"tags":["Profiles"],"summary":"Get profile with latest version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"post":{"tags":["Profiles"],"summary":"Create new profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["workerType","model"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}},"put":{"tags":["Profiles"],"summary":"Update profile identity","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileIdentity"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"}}}}}},"delete":{"tags":["Profiles"],"summary":"Delete profile","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/profiles/{profileId}/versions":{"get":{"tags":["Profiles"],"summary":"List profile versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}/versions/{version}":{"get":{"tags":["Profiles"],"summary":"Get profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"},{"schema":{"type":["number","null"]},"required":false,"name":"version","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}},"/api/v1/report-templates/default-system-prompt":{"get":{"tags":["Report Templates"],"summary":"Get default system prompt","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string"}},"required":["content"]}}}}}}},"/api/v1/report-templates/available-models":{"get":{"tags":["Report Templates"],"summary":"List models available for report generation","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"]}}}}}}}},"/api/v1/report-templates":{"get":{"tags":["Report Templates"],"summary":"List report templates","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}}}}},"post":{"tags":["Report Templates"],"summary":"Create report template","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportTemplateInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"409":{"description":"Report template already exists"}}}},"/api/v1/report-templates/{id}":{"get":{"tags":["Report Templates"],"summary":"Get report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"put":{"tags":["Report Templates"],"summary":"Update report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateReportTemplateInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"delete":{"tags":["Report Templates"],"summary":"Delete report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Report template not found"}}}},"/api/v1/agents":{"get":{"tags":["Agents"],"summary":"List agents","parameters":[{"schema":{"type":"string"},"required":false,"name":"modelProvider","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentResponse"}}}}}}},"post":{"tags":["Agents"],"summary":"Create or update agent (upsert)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"409":{"description":"Restored agent versions conflict with an active queue owner"}}}},"/api/v1/agents/{id}":{"get":{"tags":["Agents"],"summary":"Get agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"404":{"description":"Agent not found"}}},"put":{"tags":["Agents"],"summary":"Update agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAgentInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"}}},"delete":{"tags":["Agents"],"summary":"Delete agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Agent not found"}}}},"/api/v1/agents/{id}/versions":{"get":{"tags":["Agents"],"summary":"List agent versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}}}}},"404":{"description":"Agent not found"}}},"post":{"tags":["Agents"],"summary":"Register agent version (upsert)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterAgentVersionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent registration did not stabilize"}}}},"/api/v1/agents/{id}/versions/{agentVersion}":{"patch":{"tags":["Agents"],"summary":"Patch agent version","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"agentVersion","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchAgentVersionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Invalid status value"},"404":{"description":"Agent or version not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent activation did not stabilize"}}}},"/api/v1/models":{"get":{"tags":["Models"],"summary":"List models","parameters":[{"schema":{"type":"string"},"required":false,"name":"agentId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"provider","in":"query"},{"schema":{"type":"string","enum":["active","disappeared"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ModelResponse"}}}}}}}},"/api/v1/models/{id}":{"get":{"tags":["Models"],"summary":"Get model","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelResponse"}}}}}}},"/api/v1/models/sync":{"post":{"tags":["Models"],"summary":"Sync models from provider","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"agentId":{"type":"string"},"provider":{"type":"string"},"models":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"providerAvailableFrom":{"type":"string"},"providerEndOfLife":{"type":"string"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}}},"required":["id"]}},"scannedAt":{"type":"string"}},"required":["agentId","provider","models","scannedAt"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"added":{"type":"array","items":{"type":"string"}},"removed":{"type":"array","items":{"type":"string"}},"unchanged":{"type":"array","items":{"type":"string"}}},"required":["added","removed","unchanged"]}}}}}}},"/api/v1/mcp/servers":{"get":{"tags":["MCP Servers"],"summary":"List MCP servers","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"post":{"tags":["MCP Servers"],"summary":"Create MCP server","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}},"required":["_id","name","type"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"/api/v1/mcp/servers/{id}":{"get":{"tags":["MCP Servers"],"summary":"Get MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"put":{"tags":["MCP Servers"],"summary":"Update MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMcpServerInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"delete":{"tags":["MCP Servers"],"summary":"Delete MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/skills":{"get":{"tags":["Skills"],"summary":"List all skills","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"post":{"tags":["Skills"],"summary":"Create or import a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSkillInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"/api/v1/skills/search":{"get":{"tags":["Skills"],"summary":"Search skills (internal + external)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/search/external":{"get":{"tags":["Skills"],"summary":"Search external skills registry","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project."},"required":false,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/discover":{"get":{"tags":["Skills"],"summary":"Discover skills in a GitHub repository","parameters":[{"schema":{"type":"string"},"required":true,"name":"source","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillDiscoveryResult"}}}}},"400":{"description":"Missing or malformed source parameter"},"404":{"description":"Repository not found"},"502":{"description":"GitHub API error"}}}},"/api/v1/skills/{id}(*)/revisions":{"get":{"tags":["Skills"],"summary":"List skill revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}}},"404":{"description":"Skill not found"}}}},"/api/v1/skills/{id}(*)":{"get":{"tags":["Skills"],"summary":"Get skill by slug","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}},"404":{"description":"Skill not found"}}},"delete":{"tags":["Skills"],"summary":"Soft-delete a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Skill not found"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)/archive":{"get":{"tags":["Skill Revisions"],"summary":"Download skill revision archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Skill revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ref","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skill-revisions/{id}":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ID","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skills/{id}(*)/resolve":{"post":{"tags":["Skills"],"summary":"Trigger skill resolution","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebases":{"get":{"tags":["Codebases"],"summary":"List all codebases","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseResponse"}}}}}}},"post":{"tags":["Codebases"],"summary":"Create a codebase (archive codebases require the archive file)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input or missing archive"}}}},"/api/v1/codebases/{id}":{"get":{"tags":["Codebases"],"summary":"Get a codebase","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"patch":{"tags":["Codebases"],"summary":"Update a codebase","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCodebaseInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"delete":{"tags":["Codebases"],"summary":"Delete a codebase","responses":{"204":{"description":"Success"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebases/{id}/revisions":{"get":{"tags":["Codebases"],"summary":"List codebase revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}}},"404":{"description":"Codebase not found"}}},"post":{"tags":["Codebases"],"summary":"Resolve a new git codebase revision","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveCodebaseRevisionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"400":{"description":"Not a git codebase"},"404":{"description":"Codebase not found"},"502":{"description":"GitHub resolution failed"}}}},"/api/v1/codebases/{id}/upload":{"post":{"tags":["Codebases"],"summary":"Upload a codebase archive as a new revision","responses":{"201":{"description":"Success"},"400":{"description":"Missing archive or not an archive codebase"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebase-revisions/{id}/archive":{"get":{"tags":["Codebase Revisions"],"summary":"Download codebase revision archive","responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebase-revisions/{id}":{"get":{"tags":["Codebase Revisions"],"summary":"Get codebase revision by id","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources":{"get":{"tags":["Resources"],"summary":"List all resources","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceResponse"}}}}}}},"post":{"tags":["Resources"],"summary":"Create a resource and its first revision","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceInput"}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input"},"409":{"description":"Resource slug or revision ref already exists in this project"}}}},"/api/v1/resources/{id}":{"get":{"tags":["Resources"],"summary":"Get a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"patch":{"tags":["Resources"],"summary":"Update a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateResourceInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"delete":{"tags":["Resources"],"summary":"Delete a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Resource not found"}}}},"/api/v1/resources/{id}/revisions":{"get":{"tags":["Resource Revisions"],"summary":"List resource revisions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}}},"404":{"description":"Resource not found"}}},"post":{"tags":["Resource Revisions"],"summary":"Create a resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceRevisionInput"}}}},"responses":{"201":{"description":"Success"},"404":{"description":"Resource not found"},"409":{"description":"Resource revision ref already exists in this project"}}}},"/api/v1/resources/{id}/revisions/latest":{"get":{"tags":["Resource Revisions"],"summary":"Get the latest resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/{id}/revisions/{revisionNumber}":{"get":{"tags":["Resource Revisions"],"summary":"Get a resource revision by resource and revision number","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"revisionNumber","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/revisions/{id}":{"get":{"tags":["Resource Revisions"],"summary":"Get resource revision by id","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/extensions":{"get":{"tags":["Extensions"],"summary":"List all extensions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"post":{"tags":["Extensions"],"summary":"Create or import an extension","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExtensionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"/api/v1/extensions/search":{"get":{"tags":["Extensions"],"summary":"Search extensions (internal + marketplace)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/extensions/{id}":{"get":{"tags":["Extensions"],"summary":"Get extension by ID","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"put":{"tags":["Extensions"],"summary":"Update extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateExtensionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"delete":{"tags":["Extensions"],"summary":"Delete extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/extensions/{id}/versions":{"get":{"tags":["Extensions"],"summary":"List available versions for an extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"preRelease","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionVersionInfo"}}}}}}}},"/api/v1/insights":{"get":{"tags":["Insights"],"summary":"List insights","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"post":{"tags":["Insights"],"summary":"Create insight","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInsightInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"/api/v1/insights/search":{"get":{"tags":["Insights"],"summary":"Search insights","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}}},"/api/v1/insights/{id}":{"get":{"tags":["Insights"],"summary":"Get insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"put":{"tags":["Insights"],"summary":"Update insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateInsightInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"delete":{"tags":["Insights"],"summary":"Delete insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/upvote":{"post":{"tags":["Insights"],"summary":"Upvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/downvote":{"post":{"tags":["Insights"],"summary":"Downvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/block":{"post":{"tags":["Insights"],"summary":"Block insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/unblock":{"post":{"tags":["Insights"],"summary":"Unblock insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/reports":{"get":{"tags":["Insights"],"summary":"Get reports referencing insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Insight not found"}}}},"/api/v1/feature-flags":{"get":{"tags":["Feature Flags"],"summary":"List feature flags","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"/api/v1/feature-flags/{key}":{"put":{"tags":["Feature Flags"],"summary":"Update feature flag","parameters":[{"schema":{"type":"string"},"required":true,"name":"key","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeatureFlagInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"webhooks":{}} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"Scope API","version":"1.0.0","description":"REST API for the Scope platform — benchmarking AI coding agents"},"servers":[{"url":"/","description":"Current server"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Use the unchanged IdP access token, without the Bearer prefix."}},"schemas":{"KeyResponse":{"type":"object","properties":{},"additionalProperties":{}},"KeyInput":{"type":"object","properties":{},"additionalProperties":{}},"ValidateKeyInput":{"type":"object","properties":{"token":{"type":"string"}},"required":["token"]},"AccountResponse":{"type":"object","properties":{},"additionalProperties":{}},"AccountInput":{"type":"object","properties":{},"additionalProperties":{}},"UserMeResponse":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string"},"email":{"type":"string"},"displayName":{"type":"string"},"idp":{"type":"string"},"idpTenant":{"type":"string"}},"required":["id"]},"ProjectResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","createdAt"]},"CreateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"creator":{"type":"string"}},"required":["name"]},"UpdateProjectInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"SubmissionListResponse":{"type":"object","properties":{"submissions":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionResponse"}}},"required":["submissions"]},"SubmissionResponse":{"type":"object","properties":{"submissionId":{"type":"string"},"projectId":{"type":"string"},"requestIds":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"submissionRunId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["waiting","ready","blocked","processing","done","failed"]},"requests":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRequestSummary"}},"dependencyProjection":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionDependencyProjection"}},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"}},"handlerProgress":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/SubmissionHandlerStatus"}},"report":{"$ref":"#/components/schemas/SubmissionReportPointer"},"supersededAt":{"type":["string","null"],"format":"date-time"},"supersededBySubmissionRunId":{"type":"string"},"supersededReason":{"type":"string","enum":["request-vector-changed","manual-retry"]}},"required":["submissionId","projectId","requestIds","createdAt","submissionRunId","attemptNumber","readiness","requests","dependencyProjection","blocking"]},"SubmissionRequestSummary":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"task":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"},"workerType":{"type":"string"},"model":{"type":"string"},"profileId":{"type":"string"}},"required":["requestId","runId","task","status","workerType"]},"SubmissionDependencyProjection":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"state":{"type":"string","enum":["waiting","ready","failed","blocked"]},"version":{"type":"number"},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["requestId","runId","handlerId","state"]},"SubmissionBlockingDetail":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"handlerId":{"type":"string"},"reason":{"type":"string","enum":["request-missing","run-changed","dependency-not-ready","dependency-failed","dependency-blocked"]},"error":{"type":"string"}},"required":["requestId","runId","handlerId","reason"]},"SubmissionHandlerStatus":{"type":"object","properties":{"status":{"type":"string","enum":["queued","processing","done","failed"]},"version":{"type":"number"},"updatedAt":{"type":["string","null"],"format":"date-time"},"error":{"type":"string"}},"required":["status"]},"SubmissionReportPointer":{"type":"object","properties":{"reportId":{"type":"string"},"url":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"}}},"SubmissionRunResponse":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"vectorFingerprint":{"type":"string"},"requests":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRequestRef"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"dependencyProjection":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionDependencyProjection"},"default":[]},"handlerStatus":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/SubmissionHandlerStatus"}},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"},"default":[]},"report":{"$ref":"#/components/schemas/SubmissionReportPointer"},"supersededAt":{"type":["string","null"],"format":"date-time"},"supersededBySubmissionRunId":{"type":"string"},"supersededReason":{"type":"string","enum":["request-vector-changed","manual-retry"]},"submissionId":{"type":"string"},"projectId":{"type":"string"}},"required":["_id","attemptNumber","vectorFingerprint","requests","createdAt"]},"SubmissionRequestRef":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"}},"required":["requestId","runId"]},"SubmissionRetryResponse":{"type":"object","properties":{"submissionId":{"type":"string"},"submissionRunId":{"type":"string"},"attemptNumber":{"type":"integer"},"readiness":{"type":"string","enum":["waiting","ready","blocked","processing","done","failed"]},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionBlockingDetail"}}},"required":["submissionId","submissionRunId","attemptNumber","readiness","blocking"]},"RequestResponse":{"type":"object","properties":{"_id":{"type":"string"},"scenario":{"$ref":"#/components/schemas/Scenario"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"maxIterations":{"type":"number"},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"deletedAt":{"type":["string","null"],"format":"date-time"},"taskPromptId":{"type":"string"},"agentsMdPromptId":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBinding"}},"extensions":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"profileId":{"type":"string"},"profileVersionId":{"type":"string"},"submissionId":{"type":"string"},"submissionExpectedRequestCount":{"type":"integer","minimum":1},"submissionRequestIndex":{"type":"integer","minimum":0},"submissionInitialRunId":{"type":"string"},"submissionFinalizedAt":{"type":["string","null"],"format":"date-time"},"priority":{"type":"integer","default":0},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"observations":{"type":"array","items":{"type":"string"}},"gateSummaries":{"type":"array","items":{"$ref":"#/components/schemas/GateRunSummary"}},"run":{"$ref":"#/components/schemas/RunState"},"projectId":{"type":"string"}},"required":["_id","scenario","workerType","createdAt","projectId"]},"Scenario":{"type":"object","properties":{"version":{"type":"string","enum":["v1","v2"]},"task":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}}},"required":["task","criteria"]},"Persona":{"type":"object","properties":{"personality":{"type":"string","enum":["demanding","friendly"]},"experience":{"type":"string","enum":["junior","senior"]},"verbosity":{"type":"string","enum":["brief","moderate"]},"type":{"type":"string","enum":["traditional","ai_assisted","vibe"]}},"required":["personality","experience","verbosity","type"]},"ResourceBinding":{"type":"object","properties":{"ref":{"type":"string"},"revisionId":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref","revisionId","params"]},"GateConfig":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"promptId":{"type":"string"},"promptText":{"type":"string"},"criteria":{"type":"array","items":{"type":"string"}},"maxIterations":{"type":"integer","minimum":1,"maximum":50}},"required":["gate","criteria"]},"GateRunSummary":{"type":"object","properties":{"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"status":{"type":"string","enum":["passed","failed","skipped"]},"iterations":{"type":"integer","minimum":0}},"required":["gate","status","iterations"]},"RunState":{"type":"object","properties":{"_id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"queuedQueueName":{"type":"string"},"outcome":{"type":"string","enum":["succeeded","failed","finished"]},"result":{"type":"string"},"error":{"type":"string"},"logsUrl":{"type":"string"},"updatedAt":{"type":["string","null"],"format":"date-time"},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"turns":{"type":"array","items":{"$ref":"#/components/schemas/ConversationTurn"}},"workerVersion":{"type":"string"},"os":{"type":"object","properties":{"platform":{"type":"string"},"release":{"type":"string"},"arch":{"type":"string"}},"required":["platform","release","arch"]},"lastHeartbeatAt":{"type":["string","null"],"format":"date-time"},"worker":{"type":"object","properties":{"instanceId":{"type":"string"},"podName":{"type":"string"}},"required":["instanceId"]},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"setupVideoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"aiCallCount":{"type":"number"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRunOutcome"}},"mcpRegistered":{"type":"boolean"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"pausedAt":{"type":["string","null"],"format":"date-time"},"resumedAt":{"type":["string","null"],"format":"date-time"},"observationResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}}},"required":["_id","attemptNumber","status"]},"ConversationTurn":{"type":"object","properties":{"iteration":{"type":"number"},"gate":{"type":"string","enum":["select","build","test","run","deploy"]},"codingAgentResponse":{"type":"string"},"judgeFeedback":{"type":"string"},"snapshotUrl":{"type":"string"},"passed":{"type":"boolean"},"timestamp":{"type":["string","null"],"format":"date-time"},"criteriaResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"observationResults":{"type":"array","items":{"$ref":"#/components/schemas/CriterionResult"}},"harUrl":{"type":"string"},"videoUrls":{"type":"array","items":{"type":"string"}},"tokenUsage":{"$ref":"#/components/schemas/TokenUsage"},"startedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":"number"},"toolCalls":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"arguments":{"type":"object","additionalProperties":{}},"response":{"type":"string"},"timestamp":{"type":"string"}},"required":["id","name","arguments"]}},"toolCallsUrl":{"type":"string"},"toolCallCount":{"type":"number"},"aiCallCount":{"type":"number"},"rawChatUrl":{"type":"string"},"rawChatFormat":{"type":"string"},"chatResultUrl":{"type":"string"},"chatResultFormat":{"type":"string"}},"required":["iteration","judgeFeedback","snapshotUrl","passed","timestamp"]},"CriterionResult":{"type":"object","properties":{"criterionId":{"type":"string"},"passed":{"type":"boolean"},"feedback":{"type":"string"},"evaluated":{"type":"boolean"}},"required":["criterionId","passed","feedback","evaluated"]},"TokenUsage":{"type":"object","properties":{"promptTokens":{"type":"number"},"completionTokens":{"type":"number"},"totalTokens":{"type":"number"}},"required":["promptTokens","completionTokens","totalTokens"]},"ResourceRunOutcome":{"type":"object","properties":{"ref":{"type":"string"},"slug":{"type":"string"},"revisionId":{"type":"string"},"setupSucceeded":{"type":"boolean"},"published":{"type":"array","items":{"type":"string"}},"params":{"type":"object","additionalProperties":{"type":"string"}},"setupDurationMs":{"type":"number"},"error":{"type":"string"},"teardownRan":{"type":"boolean"}},"required":["ref","slug","revisionId","setupSucceeded","published"]},"ResourceBindingSpec":{"anyOf":[{"type":"string"},{"type":"object","properties":{"ref":{"type":"string"},"params":{"type":"object","additionalProperties":{"type":"string"}}},"required":["ref"]}]},"CreateRequestInput":{"type":"object","properties":{"scenario":{"$ref":"#/components/schemas/Scenario"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"maxIterations":{"type":"integer","minimum":1,"maximum":50},"personaInstructions":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"profileId":{"type":"string"},"profileVariations":{"type":"array","items":{"type":"string"}},"priority":{"type":"integer"},"agentsMd":{"type":"string"},"agentsMdParentIds":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"$ref":"#/components/schemas/GateConfig"}},"observations":{"type":"array","items":{"type":"string"}}},"required":["scenario"]},"RunFacetsResponse":{"type":"object","properties":{"total":{"type":"number"},"facets":{"type":"object","properties":{"workerType":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"status":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"outcome":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"model":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"os":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"priority":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"agentVersion":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}},"profileId":{"type":"array","items":{"$ref":"#/components/schemas/RunFacetBucket"}}},"required":["workerType","status","outcome","model","os","priority","agentVersion","profileId"]}},"required":["total","facets"]},"RunFacetBucket":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"number"}},"required":["value","count"]},"PaginatedRunsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"Cursors":{"type":"object","properties":{"next":{"type":["string","null"]},"prev":{"type":["string","null"]}},"required":["next","prev"]},"PaginatedRunGroupsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RunGroup"}},"limit":{"type":"number"},"estimatedTotal":{"type":"number"},"cursors":{"$ref":"#/components/schemas/Cursors"}},"required":["data","limit","estimatedTotal","cursors"]},"RunGroup":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"runIds":{"type":"array","items":{"type":"string"}},"aggregates":{"$ref":"#/components/schemas/GroupAggregates"},"uniform":{"$ref":"#/components/schemas/GroupUniformValues"}},"required":["key","label","runIds","aggregates","uniform"]},"GroupAggregates":{"type":"object","properties":{"count":{"type":"number"},"turns":{"$ref":"#/components/schemas/AggregateStats"},"duration":{"$ref":"#/components/schemas/AggregateStats"},"promptTokens":{"$ref":"#/components/schemas/AggregateStats"},"completionTokens":{"$ref":"#/components/schemas/AggregateStats"},"statusCounts":{"type":"object","additionalProperties":{"type":"number"}},"outcomeCounts":{"type":"object","additionalProperties":{"type":"number"}}},"required":["count","turns","duration","promptTokens","completionTokens","statusCounts","outcomeCounts"]},"AggregateStats":{"type":["object","null"],"properties":{"min":{"type":"number"},"max":{"type":"number"},"mean":{"type":"number"},"stdDev":{"type":"number"}},"required":["min","max","mean","stdDev"]},"GroupUniformValues":{"type":"object","properties":{"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":"string"},"platform":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"codebaseRevisionId":{"type":"string"},"extensions":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["pending","queued","processing","paused","done"]},"submissionId":{"type":"string"},"task":{"type":"string"}}},"BulkResubmitInput":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"count":{"type":"integer","minimum":1,"maximum":10,"default":1},"overrides":{"type":"object","properties":{"profileId":{"type":["string","null"]},"workerType":{"type":"string"},"agentVersion":{"type":"string"},"model":{"type":["string","null"]},"reasoningEffort":{"type":["string","null"]},"maxIterations":{"type":["number","null"]},"mcpServers":{"type":["array","null"],"items":{"type":"string"}},"skillRevisions":{"type":["array","null"],"items":{"type":"string"}},"extensions":{"type":["array","null"],"items":{"type":"string"}}}}},"required":["ids"]},"ReportResponse":{"type":"object","properties":{"_id":{"type":"string"},"requestId":{"type":"string"},"templateId":{"type":"string"},"reporter":{"$ref":"#/components/schemas/Reporter"},"content":{"type":"string"},"status":{"type":"string","enum":["pending","generating","completed","failed"]},"error":{"type":"string"},"insightReferences":{"type":"array","items":{"$ref":"#/components/schemas/InsightReference"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","requestId","status","createdAt","projectId"]},"Reporter":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"gitHash":{"type":"string"},"model":{"type":"string"},"agentId":{"type":"string"},"agentVersion":{"type":"string"}},"required":["id","name","gitHash","model","agentId","agentVersion"]},"InsightReference":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]},"CreateCriteriaInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"],"default":"gate"},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}},"required":["id","prompt"]},"CriteriaResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"CriteriaGraph":{"type":"object","properties":{"nodes":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphNode"}},"edges":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaGraphEdge"}}},"required":["nodes","edges"]},"CriteriaGraphNode":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}},"required":["id","prompt"]},"CriteriaGraphEdge":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"]},"UpdateCriteriaInput":{"type":"object","properties":{"prompt":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string","enum":["select","build","test","run","deploy"]}},"kind":{"type":"string","enum":["gate","observation"]},"taxonomyElementId":{"type":"string","enum":["dimension:idiomatic-use","dimension:dependency-currency","dimension:configuration-correctness"]},"subject":{"type":"string","enum":["run","iteration"]}}},"CreatePromptFeatureInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["id","prompt"]},"PromptType":{"type":"string","enum":["select","build","test","run","deploy","agents.md"]},"PromptFeatureResult":{"type":"object","properties":{"featureId":{"type":"string"},"detected":{"type":"boolean"},"evaluated":{"type":"boolean"}},"required":["featureId","detected","evaluated"]},"SuggestedPromptFeature":{"type":"object","properties":{"suggestedId":{"type":"string"},"behavior":{"type":"string"},"prompt":{"type":"string"}},"required":["suggestedId","behavior","prompt"]},"PromptFeatureResponse":{"type":"object","properties":{"id":{"type":"string"},"prompt":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["id","prompt","createdAt","projectId"]},"UpdatePromptFeatureInput":{"type":"object","properties":{"prompt":{"type":"string"}}},"TaskPromptResponse":{"type":"object","properties":{"_id":{"type":"string"},"keyId":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"},"text":{"type":"string"},"contentBlobUrl":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","keyId","createdAt","projectId"]},"CreateTaskPromptInput":{"type":"object","properties":{"text":{"type":"string"},"type":{"$ref":"#/components/schemas/PromptType"}},"required":["text"]},"PatchTaskPromptFeatureInput":{"type":"object","properties":{"detected":{"type":"boolean"}},"required":["detected"]},"CreateReportInput":{"type":"object","properties":{"requestId":{"type":"string"},"templateId":{"type":"string"}},"required":["requestId"]},"BulkCreateReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}},"templateId":{"type":"string"}},"required":["requestIds"]},"BulkReportStatusInput":{"type":"object","properties":{"reportIds":{"type":"array","items":{"type":"string"}}},"required":["reportIds"]},"BulkReportSummaryResponse":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/ReportSummary"}},"ReportSummary":{"type":"object","properties":{"total":{"type":"number"},"pending":{"type":"number"},"generating":{"type":"number"},"completed":{"type":"number"},"failed":{"type":"number"}},"required":["total","pending","generating","completed","failed"]},"BulkReportSummaryInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"TriggerReportsInput":{"type":"object","properties":{"requestId":{"type":"string"}},"required":["requestId"]},"BulkTriggerReportsInput":{"type":"object","properties":{"requestIds":{"type":"array","items":{"type":"string"}}},"required":["requestIds"]},"InsightResponse":{"type":"object","properties":{"_id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"upvotes":{"type":"number"},"downvotes":{"type":"number"},"blocked":{"type":"boolean"},"referenceCount":{"type":"number"},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","title","description","upvotes","downvotes","blocked","referenceCount","createdBy","createdAt","projectId"]},"ProfileWithVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"version":{"$ref":"#/components/schemas/ProfileVersionResponse"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","version","projectId"]},"ProfileVersionResponse":{"type":"object","properties":{"_id":{"type":"string"},"profileId":{"type":"string"},"version":{"type":"number"},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","profileId","version","workerType","model","createdAt","projectId"]},"CreateProfileInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512},"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["name","workerType","model"]},"ProfileResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"latestVersion":{"type":"number"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","name","latestVersion","createdAt","projectId"]},"UpdateProfileIdentity":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"description":{"type":"string","maxLength":512}}},"ReportTemplateResponse":{"type":"object","properties":{"_id":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","id","name","userPrompt","createdAt","projectId"]},"ReportTemplateSystemPrompt":{"type":"object","properties":{"mode":{"type":"string","enum":["append","override"]},"content":{"type":"string"}},"required":["mode","content"]},"ReportTrigger":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["always"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["criteria"]},"criteriaIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","criteriaIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["taskPrompt"]},"taskPromptIds":{"type":"array","items":{"type":"string"}}},"required":["type","taskPromptIds"]},{"type":"object","properties":{"type":{"type":"string","enum":["promptFeature"]},"featureIds":{"type":"array","items":{"type":"string"}},"match":{"type":"string","enum":["any","all"]}},"required":["type","featureIds"]}]},"CreateReportTemplateInput":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":"string"},"timeoutMs":{"type":"integer","exclusiveMinimum":0}},"required":["id","name","userPrompt"]},"UpdateReportTemplateInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"userPrompt":{"type":"string"},"systemPrompt":{"allOf":[{"$ref":"#/components/schemas/ReportTemplateSystemPrompt"},{"type":["object","null"]}]},"trigger":{"$ref":"#/components/schemas/ReportTrigger"},"model":{"type":["string","null"]},"timeoutMs":{"type":["integer","null"],"exclusiveMinimum":0}}},"AgentResponse":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"},"versions":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"}},"required":["_id","name","supportedModels","createdAt"]},"AgentCapabilities":{"type":"object","properties":{"supportsReasoningEffort":{"type":"boolean"},"supportsMcpServers":{"type":"boolean"},"supportsSkills":{"type":"boolean"},"supportsExtensions":{"type":"boolean"},"supportsResources":{"type":"boolean"}}},"AgentVersion":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string"},"status":{"type":"string","enum":["active","retired"]},"createdAt":{"type":["string","null"],"format":"date-time"}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","status","createdAt"]},"CreateAgentInput":{"type":"object","properties":{"_id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}},"required":["_id","name"]},"UpdateAgentInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"modelProvider":{"type":"string"},"supportedModels":{"type":"array","items":{"type":"string"}},"defaultModel":{"type":"string"},"available":{"type":"boolean"},"capabilities":{"$ref":"#/components/schemas/AgentCapabilities"}}},"RegisterAgentVersionInput":{"type":"object","properties":{"agentVersion":{"type":"string","minLength":1},"workerVersion":{"type":"string","minLength":1},"components":{"type":"object","additionalProperties":{"type":"string"}},"gitCommit":{"type":"string","minLength":1},"buildTime":{"type":"string","minLength":1},"imageTag":{"type":"string","minLength":1},"queueName":{"type":"string","minLength":1}},"required":["agentVersion","workerVersion","components","gitCommit","buildTime","imageTag","queueName"]},"PatchAgentVersionInput":{"type":"object","properties":{"status":{"type":"string","enum":["active","retired"]}},"required":["status"]},"ModelResponse":{"type":"object","properties":{"_id":{"type":"string"},"modelId":{"type":"string"},"provider":{"type":"string"},"agentId":{"type":"string"},"firstSeenAt":{"type":["string","null"],"format":"date-time"},"lastSeenAt":{"type":["string","null"],"format":"date-time"},"disappearedAt":{"type":["string","null"],"format":"date-time"},"providerAvailableFrom":{"type":["string","null"],"format":"date-time"},"providerEndOfLife":{"type":["string","null"],"format":"date-time"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"$ref":"#/components/schemas/ModelCapabilities"}},"required":["_id","modelId","provider","agentId","firstSeenAt","lastSeenAt"]},"ModelCapabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}},"McpServerResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","type","createdAt","projectId"]},"McpServerHeader":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}},"required":["name","value"]},"UpdateMcpServerInput":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}}},"SkillResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","source","skillName","name","origin","createdAt","projectId"]},"SkillSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"source":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"installs":{"type":"number"}},"required":["id","name","source","internal"]},"SkillDiscoveryResult":{"type":"object","properties":{"skillName":{"type":"string"},"skillPath":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"existsInLibrary":{"type":"boolean"},"currentRevisionCommitSha":{"type":"string"},"latestUpstreamCommitSha":{"type":"string"},"updateAvailable":{"type":"boolean"},"lastImportedAt":{"type":"string"}},"required":["skillName","skillPath"]},"SkillRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"ref":{"type":"string"},"source":{"type":"string"},"skillName":{"type":"string"},"skillPath":{"type":"string"},"commitHash":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"name":{"type":"string"},"description":{"type":"string"},"license":{"type":"string"},"compatibility":{"type":"string"},"allowedTools":{"type":"string"},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"content":{"type":"string"},"archiveUrl":{"type":"string"},"validationWarnings":{"type":"array","items":{"type":"string"}},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","ref","source","skillName","skillPath","commitHash","commitTimestamp","name","description","content","archiveUrl","resolvedAt","createdAt","projectId"]},"CreateSkillInput":{"type":"object","properties":{"source":{"type":"string"},"skillName":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["skills-sh","manual"]}},"required":["source","skillName","name","origin"]},"CodebaseResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"defaultBranch":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","sourceType","revisionCounter","createdAt","projectId"]},"UpdateCodebaseInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"defaultBranch":{"type":"string"}}},"CodebaseRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"codebaseId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"sourceType":{"type":"string","enum":["git","archive"]},"source":{"type":"string"},"requestedRef":{"type":"string"},"resolvedCommitSha":{"type":"string"},"commitTimestamp":{"type":["string","null"],"format":"date-time"},"originalFilename":{"type":"string"},"contentSha256":{"type":"string"},"archiveUrl":{"type":"string"},"sizeBytes":{"type":"number"},"fileCount":{"type":"number"},"creator":{"type":"string"},"resolvedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","codebaseId","slug","revisionNumber","ref","sourceType","archiveUrl","resolvedAt","createdAt","projectId"]},"ResolveCodebaseRevisionInput":{"type":"object","properties":{"requestedRef":{"type":"string"},"creator":{"type":"string"}}},"ResourceResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"revisionCounter":{"type":"number"},"latestRevisionId":{"type":"string"},"latestRevisionNumber":{"type":"number"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","name","revisionCounter","createdAt","projectId"]},"CreateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1},"description":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["name","setup"]},"ResourceScript":{"type":"object","properties":{"sh":{"type":"string","minLength":1}}},"ResourceParameter":{"type":"object","properties":{"name":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"description":{"type":"string"},"required":{"type":"boolean","default":false},"default":{"type":"string"},"example":{"type":"string"}},"required":["name"]},"UpdateResourceInput":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"}}},"ResourceRevisionResponse":{"type":"object","properties":{"_id":{"type":"string"},"resourceId":{"type":"string"},"slug":{"type":"string"},"revisionNumber":{"type":"number"},"ref":{"type":"string"},"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"contentSha256":{"type":"string"},"creator":{"type":"string"},"createdAt":{"type":["string","null"],"format":"date-time"},"deduplicated":{"type":"boolean"},"projectId":{"type":"string"}},"required":["_id","resourceId","slug","revisionNumber","ref","setup","exports","contentSha256","createdAt","projectId"]},"CreateResourceRevisionInput":{"type":"object","properties":{"setup":{"$ref":"#/components/schemas/ResourceScript"},"teardown":{"$ref":"#/components/schemas/ResourceScript"},"exports":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z_][A-Za-z0-9_]*$"}},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/ResourceParameter"}},"creator":{"type":"string"}},"required":["setup"]},"ExtensionResponse":{"type":"object","properties":{"_id":{"type":"string"},"slug":{"type":"string"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"},"deletedAt":{"type":["string","null"],"format":"date-time"},"projectId":{"type":"string"}},"required":["_id","slug","publisher","name","origin","createdAt","projectId"]},"ExtensionSearchResult":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"publisher":{"type":"string"},"description":{"type":"string"},"internal":{"type":"boolean"},"version":{"type":"string"}},"required":["id","name","publisher","internal"]},"CreateExtensionInput":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+$"},"publisher":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"origin":{"type":"string","enum":["marketplace","manual"]}},"required":["_id","publisher","name","origin"]},"UpdateExtensionInput":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ExtensionVersionInfo":{"type":"object","properties":{"version":{"type":"string"},"preRelease":{"type":"boolean"},"lastUpdated":{"type":"string"}},"required":["version","preRelease","lastUpdated"]},"CreateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"createdBy":{"type":"string","enum":["agent","user"]},"sourceReportId":{"type":"string"}},"required":["title","description"]},"UpdateInsightInput":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}}}},"FeatureFlagResponse":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"enabled":{"type":"boolean"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["key","label","enabled","updatedAt"]},"UpdateFeatureFlagInput":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]}},"parameters":{}},"paths":{"/api/v1/keys/preview":{"post":{"tags":["Keys"],"summary":"Preview key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}}},"/api/v1/keys":{"post":{"tags":["Keys"],"summary":"Create key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"201":{"description":"Key created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"get":{"tags":["Keys"],"summary":"List keys","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KeyResponse"}}}}}}}},"/api/v1/keys/{id}":{"get":{"tags":["Keys"],"summary":"Get key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"put":{"tags":["Keys"],"summary":"Update key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyInput"}}}},"responses":{"200":{"description":"Key updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyResponse"}}}}}},"delete":{"tags":["Keys"],"summary":"Delete key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Key deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/keys/{id}/validate":{"post":{"tags":["Keys"],"summary":"Validate key","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateKeyInput"}}}},"responses":{"200":{"description":"Validation result","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Key validation result"}}}}}}},"/api/v1/accounts":{"post":{"tags":["Accounts"],"summary":"Create account","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"201":{"description":"Account created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"get":{"tags":["Accounts"],"summary":"List accounts","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccountResponse"}}}}}}}},"/api/v1/accounts/{id}":{"get":{"tags":["Accounts"],"summary":"Get account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"put":{"tags":["Accounts"],"summary":"Update account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountInput"}}}},"responses":{"200":{"description":"Account updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}},"delete":{"tags":["Accounts"],"summary":"Delete account","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Account deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}}}},"/api/v1/users/me":{"get":{"tags":["Users"],"summary":"Get the authenticated user's identity","description":"Read the existing authenticated Scope identity without enrollment or profile writes. Responses must not be HTTP-cached.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is not enrolled or is disabled"},"503":{"description":"Authentication service unavailable"}}},"post":{"tags":["Users"],"summary":"Enroll the authenticated user","description":"JIT-enroll the authenticated user, refresh their profile and lastLoginAt, apply bootstrap-admin rules, and warm the access cache. Use this only after an explicit IdP login; do not prefetch, poll, or automatically retry transient failures.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserMeResponse"}}}},"401":{"description":"Not authenticated"},"403":{"description":"User is disabled"},"503":{"description":"Authentication service unavailable"}}}},"/health":{"get":{"tags":["Health"],"summary":"Liveness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"version":{"type":"string"}},"required":["status","version"]}}}}}}},"/ready":{"get":{"tags":["Health"],"summary":"Readiness probe","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"migrations":{}},"required":["status"]}}}},"503":{"description":"Service is not ready"}}}},"/about":{"get":{"tags":["System"],"summary":"API metadata","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"version":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"description":{"type":"string"},"workers":{"type":"array","items":{"type":"string"}}},"required":["name","version","buildTime","environment","description","workers"]}}}}}}},"/api/v1/version":{"get":{"tags":["System"],"summary":"Version info","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"commit":{"type":"string"},"buildTime":{"type":"string"},"environment":{"type":"string"},"strictAgentCapabilities":{"type":"boolean"}},"required":["commit","buildTime","environment","strictAgentCapabilities"]}}}}}}},"/api/v1/projects":{"get":{"tags":["Projects"],"summary":"List all projects","parameters":[{"schema":{"type":"string","enum":["true","false"],"description":"When true, include soft-deleted projects in the result"},"required":false,"description":"When true, include soft-deleted projects in the result","name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProjectResponse"}}}}}}},"post":{"tags":["Projects"],"summary":"Create a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/projects/{id}":{"get":{"tags":["Projects"],"summary":"Get a project","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"patch":{"tags":["Projects"],"summary":"Update a project","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProjectInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}},"delete":{"tags":["Projects"],"summary":"Soft-delete a project","responses":{"204":{"description":"Success"},"404":{"description":"Project not found"}}}},"/api/v1/projects/{id}/restore":{"post":{"tags":["Projects"],"summary":"Restore a soft-deleted project","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"404":{"description":"Project not found"}}}},"/api/v1/submissions":{"get":{"tags":["Submissions"],"summary":"List submissions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionListResponse"}}}}}}},"/api/v1/submissions/{id}":{"get":{"tags":["Submissions"],"summary":"Get a submission","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionResponse"}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}/runs":{"get":{"tags":["Submissions"],"summary":"List current and historical submission attempts","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"runs":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionRunResponse"}}},"required":["runs"]}}}},"404":{"description":"Submission not found"}}}},"/api/v1/submissions/{id}/runs/{submissionRunId}":{"get":{"tags":["Submissions"],"summary":"Get one submission attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"submissionRunId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionRunResponse"}}}},"404":{"description":"Submission attempt not found"}}}},"/api/v1/submissions/{id}/retry":{"post":{"tags":["Submissions"],"summary":"Retry current submission processing","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"submissionRunId":{"type":"string"}},"required":["submissionRunId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionRetryResponse"}}}},"404":{"description":"Submission not found"},"409":{"description":"Submission attempt is stale or dependencies are blocked"}}}},"/api/v1/requests":{"post":{"tags":["Requests"],"summary":"Submit request(s)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/CreateRequestInput"},{"type":"object","properties":{"count":{"type":"number","minimum":1,"maximum":10,"default":1},"skills":{"type":"array","items":{"type":"string"}},"agentVersion":{"type":"string"},"codebase":{"type":"string"}}}]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RequestResponse"},{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}]}}}}}},"get":{"tags":["Requests"],"summary":"List requests","parameters":[{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"profileId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"status","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"outcome","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"model","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"os","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"priority","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"required":false,"name":"agentVersion","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"type":"string","enum":["task","submissionId","profile"]},"required":false,"name":"groupBy","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"after","in":"query"},{"schema":{"type":"string"},"required":false,"name":"before","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"last","in":"query"},{"schema":{"type":"string","enum":["created","updated","priority","worker","status","id","duration","createdAt"]},"required":false,"name":"sortBy","in":"query"},{"schema":{"type":"string","enum":["asc","desc"]},"required":false,"name":"sortDir","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdAfter","in":"query"},{"schema":{"type":["string","null"],"format":"date-time"},"required":false,"name":"createdBefore","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"turns","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"turnsOp","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"maxIterations","in":"query"},{"schema":{"type":"string","enum":["eq","gte","lte"]},"required":false,"name":"maxIterationsOp","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PaginatedRunsResponse"},{"$ref":"#/components/schemas/PaginatedRunGroupsResponse"}]}}}}}}},"/api/v1/requests/facets":{"get":{"tags":["Requests"],"summary":"List run filter facets","description":"Returns every distinct value and its full-dataset count per categorical filter dimension for the Runs list rail. Counts are absolute over all non-deleted runs **in the given project**: they intentionally ignore the active search, date range, iteration, and categorical selections so every selectable value stays visible with a stable count. Requires ?projectId=. Computed with parallel $group aggregations (Cosmos has no $facet) and cached per-project for a short TTL.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunFacetsResponse"}}}}}}},"/api/v1/requests/{id}":{"get":{"tags":["Requests"],"summary":"Get request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestResponse"}}}},"404":{"description":"Not found"}}},"delete":{"tags":["Requests"],"summary":"Soft-delete request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Not found"}}}},"/api/v1/analysis":{"get":{"tags":["Requests"],"summary":"Compute pass@k / success@T metrics","description":"Aggregates pass@k / success@T metrics over a single project's done runs. Requires ?projectId=.","parameters":[{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"submissionId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"k","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{},"description":"Analysis metrics"}}}}}}},"/api/v1/requests/bulk-resubmit":{"post":{"tags":["Requests"],"summary":"Bulk resubmit requests","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkResubmitInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RequestResponse"}}}}}}}},"/api/v1/requests/bulk":{"delete":{"tags":["Requests"],"summary":"Bulk soft-delete requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}},"required":["ids"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"number"}},"required":["deleted"]}}}}}}},"/api/v1/requests/archive":{"post":{"tags":["Requests"],"summary":"Download batch archive of multiple runs","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Gzipped batch archive containing individual run archives"},"400":{"description":"Invalid input"},"404":{"description":"One or more runs not found"}}}},"/api/v1/runs/upload":{"post":{"tags":["Requests"],"summary":"Import run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"}}}},"/api/v1/runs/upload-batch":{"post":{"tags":["Requests"],"summary":"Import batch run archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"207":{"description":"Multi-status: 201 if all runs imported, 400 if none imported, 207 if partial"},"400":{"description":"No archive uploaded, empty archive, or all runs failed"}}}},"/api/v1/requests/{id}/reports":{"get":{"tags":["Reports"],"summary":"Get reports for request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Run not found"}}}},"/api/v1/requests/{id}/runs":{"get":{"tags":["Requests"],"summary":"List attempts for a request","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RunState"}}}}},"404":{"description":"Request not found"}}}},"/api/v1/requests/{id}/runs/{runId}":{"get":{"tags":["Requests"],"summary":"Get a single attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunState"}}}},"404":{"description":"Request or run not found"}}}},"/api/v1/requests/bulk-retry":{"post":{"tags":["Requests"],"summary":"Bulk retry requests (start new attempts)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1},"force":{"type":"boolean"}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"retried":{"type":"integer"},"skipped":{"type":"integer"},"results":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"},"error":{"type":"string"}},"required":["requestId"]}}},"required":["retried","skipped","results"]}}}}}}},"/api/v1/requests/{id}/retry":{"post":{"tags":["Requests"],"summary":"Retry a request (start a new attempt)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"force":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"string"},"runId":{"type":"string"},"attemptNumber":{"type":"integer"}},"required":["requestId","runId","attemptNumber"]}}}},"404":{"description":"Request not found"},"409":{"description":"Conflict — request is not in a retryable state, or a concurrent retry won the race"},"422":{"description":"Cannot retry — current run not yet terminal"}}}},"/api/v1/requests/{id}/priority":{"post":{"tags":["Requests"],"summary":"Set priority on a single request","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"priority":{"type":"integer"}},"required":["priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"priority":{"type":"number"}},"required":["id","priority"]}}}}}}},"/api/v1/requests/bulk-priority":{"post":{"tags":["Requests"],"summary":"Set priority on multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100},"priority":{"type":"integer"}},"required":["ids","priority"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/pause":{"post":{"tags":["Requests"],"summary":"Pause a single request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/{id}/resume":{"post":{"tags":["Requests"],"summary":"Resume a paused request","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}}}}},"/api/v1/requests/bulk-pause":{"post":{"tags":["Requests"],"summary":"Pause multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/bulk-resume":{"post":{"tags":["Requests"],"summary":"Resume multiple paused requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"number"},"skipped":{"type":"number"}},"required":["updated","skipped"]}}}}}}},"/api/v1/requests/{id}/cancel":{"post":{"tags":["Requests"],"summary":"Cancel a request (marks as done/failed and signals worker to exit)","responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"previousStatus":{"type":"string"},"status":{"type":"string"},"outcome":{"type":"string"}},"required":["id","previousStatus","status","outcome"]}}}}}}},"/api/v1/requests/bulk-cancel":{"post":{"tags":["Requests"],"summary":"Cancel multiple requests","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}},"required":["ids"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"cancelled":{"type":"number"},"skipped":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"cancelled":{"type":"boolean"},"previousStatus":{"type":"string"},"error":{"type":"string"}},"required":["id","cancelled"]}}},"required":["cancelled","skipped","results"]}}}}}}},"/api/v1/requests/{id}/logs":{"get":{"tags":["Requests"],"summary":"Stream request logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/logs":{"get":{"tags":["Requests"],"summary":"Stream logs for a specific attempt (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/har":{"get":{"tags":["Requests"],"summary":"Download HAR file for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"HAR-format JSON file"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/atif":{"get":{"tags":["Requests"],"summary":"Download ATIF trajectory file for a specific attempt and iteration","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string","description":"The iteration number (1-based)"},"required":true,"description":"The iteration number (1-based)","name":"iteration","in":"query"}],"responses":{"200":{"description":"ATIF v1.7 trajectory JSON file"},"400":{"description":"Missing or invalid iteration"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/video":{"get":{"tags":["Requests"],"summary":"Download session recording","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/video":{"get":{"tags":["Requests"],"summary":"Download session recording for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"WebM video recording (supports Range requests)"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/tool-calls":{"get":{"tags":["Requests"],"summary":"Download per-iteration tool-calls JSONL for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"JSONL stream — one ToolCall per line"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/snapshots/{iteration}":{"get":{"tags":["Requests"],"summary":"Download iteration snapshot for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"},{"schema":{"type":"string"},"required":true,"name":"iteration","in":"path"}],"responses":{"200":{"description":"Gzipped snapshot archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/requests/{id}/runs/{runId}/archive":{"get":{"tags":["Requests"],"summary":"Download full run archive for a specific attempt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"runId","in":"path"}],"responses":{"200":{"description":"Gzipped run archive"},"404":{"description":"Not found"}}}},"/api/v1/criteria/generate-prompt":{"post":{"tags":["Criteria"],"summary":"Generate criterion prompt from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"},"gates":{"type":"array","items":{"type":"string"}}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"},"suggestedId":{"type":"string"},"suggestedParents":{"type":"array","items":{"type":"string"}},"suggestedChildren":{"type":"array","items":{"type":"string"}}},"required":["prompt","suggestedId","suggestedParents","suggestedChildren"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/criteria/seed":{"post":{"tags":["Criteria"],"summary":"Seed criteria in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"criteria":{"type":"array","items":{"$ref":"#/components/schemas/CreateCriteriaInput"}}},"required":["criteria"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}},"400":{"description":"Seed batch would introduce a dependency cycle"}}}},"/api/v1/criteria":{"get":{"tags":["Criteria"],"summary":"List criteria","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","description":"Comma-separated criterion IDs to include"},"required":false,"description":"Comma-separated criterion IDs to include","name":"ids","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"When true and ids is set, also include dependency ancestors"},"required":false,"description":"When true and ids is set, also include dependency ancestors","name":"ancestors","in":"query"},{"schema":{"type":"string","enum":["gate","observation"],"description":"Filter by criterion kind"},"required":false,"description":"Filter by criterion kind","name":"kind","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CriteriaResponse"}}}}}}},"post":{"tags":["Criteria"],"summary":"Create criterion","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCriteriaInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"409":{"description":"Criterion already exists"}}}},"/api/v1/criteria/mdp":{"get":{"tags":["Criteria"],"summary":"Compute MDP transitions","parameters":[{"schema":{"type":"string"},"required":false,"name":"criteria","in":"query"},{"schema":{"type":"string"},"required":false,"name":"features","in":"query"},{"schema":{"type":"string"},"required":false,"name":"since","in":"query"},{"schema":{"type":"string"},"required":false,"name":"worker","in":"query"},{"schema":{"type":"string"},"required":false,"name":"taskPromptId","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":{}}}}}}}},"/api/v1/criteria/graph":{"get":{"tags":["Criteria"],"summary":"Get criteria DAG","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaGraph"}}}}}}},"/api/v1/criteria/{id}":{"get":{"tags":["Criteria"],"summary":"Get criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"404":{"description":"Criterion not found"}}},"put":{"tags":["Criteria"],"summary":"Update criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCriteriaInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CriteriaResponse"}}}},"400":{"description":"Invalid dependency reference or self-reference"},"404":{"description":"Criterion not found"}}},"delete":{"tags":["Criteria"],"summary":"Soft-delete criterion","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Criterion not found"},"409":{"description":"Criterion has dependents"}}}},"/api/v1/prompt-features/generate-prompt":{"post":{"tags":["Prompt Features"],"summary":"Generate prompt feature from behavior","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string"},"currentId":{"type":"string"}},"required":["behavior"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"}},"required":["prompt"]}}}},"400":{"description":"Empty behavior string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features/seed":{"post":{"tags":["Prompt Features"],"summary":"Seed prompt features in bulk","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}},"required":["features"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["seeded","errors"]}}}}}}},"/api/v1/prompt-features/extract-from-text":{"post":{"tags":["Prompt Features"],"summary":"Extract features from text","parameters":[{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","example":"00000000-0000-0000-0000-000000000000"},"required":false,"description":"Optional project scope. When provided, a slug/id is resolved within that project (slugs may repeat across projects); when omitted, resolution falls back to a legacy global lookup for backward compatibility.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string"},"model":{"type":"string"},"type":{"type":"string","enum":["select","agents.md"]}},"required":["text"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["features","cached"]}}}},"400":{"description":"Empty text string"},"503":{"description":"LLM not configured"}}}},"/api/v1/prompt-features":{"get":{"tags":["Prompt Features"],"summary":"List features","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","enum":["select","agents.md"]},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}}}}},"post":{"tags":["Prompt Features"],"summary":"Create feature","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePromptFeatureInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"409":{"description":"Feature already exists"}}}},"/api/v1/prompt-features/{id}":{"get":{"tags":["Prompt Features"],"summary":"Get feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"404":{"description":"Feature not found"}}},"put":{"tags":["Prompt Features"],"summary":"Update feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptFeatureResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Feature not found"}}},"delete":{"tags":["Prompt Features"],"summary":"Soft-delete feature","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"404":{"description":"Feature not found"}}}},"/api/v1/task-prompts/generate":{"post":{"tags":["Task Prompts"],"summary":"Generate task prompts","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string"},"existingPrompt":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"tasks":{"type":"array","items":{"type":"string"}}},"required":["tasks"]}}}},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts":{"get":{"tags":["Task Prompts"],"summary":"List task prompts","parameters":[{"schema":{"type":["number","null"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":["number","null"]},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"$ref":"#/components/schemas/PromptType"},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/TaskPromptResponse"}},"total":{"type":"number"},"limit":{"type":"number"},"offset":{"type":"number"}},"required":["items","total","limit","offset"]}}}}}},"post":{"tags":["Task Prompts"],"summary":"Create or find task prompt","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTaskPromptInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Empty text string"}}}},"/api/v1/task-prompts/{id}":{"get":{"tags":["Task Prompts"],"summary":"Get task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"404":{"description":"Task prompt not found"}}},"delete":{"tags":["Task Prompts"],"summary":"Soft-delete task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/content":{"get":{"tags":["Task Prompts"],"summary":"Get resolved task prompt content","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"}},"required":["id","text"]}}}},"404":{"description":"Task prompt not found"}}}},"/api/v1/task-prompts/{id}/extract-features":{"post":{"tags":["Task Prompts"],"summary":"Extract features from task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"force","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"taskPromptId":{"type":"string"},"features":{"type":"array","items":{"$ref":"#/components/schemas/PromptFeatureResult"}},"featuresExtractedAt":{"type":["string","null"],"format":"date-time"},"suggestedFeatures":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedPromptFeature"}},"cached":{"type":"boolean"}},"required":["taskPromptId","features","cached"]}}}},"404":{"description":"Task prompt not found"},"503":{"description":"LLM not configured"}}}},"/api/v1/task-prompts/{id}/features/{featureId}":{"patch":{"tags":["Task Prompts"],"summary":"Toggle feature flag on task prompt","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"featureId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchTaskPromptFeatureInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskPromptResponse"}}}},"400":{"description":"Invalid detected value"},"404":{"description":"Task prompt or feature not found"}}}},"/api/v1/reports":{"post":{"tags":["Reports"],"summary":"Create report","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"400":{"description":"Invalid input"},"404":{"description":"Run or template not found"}}},"get":{"tags":["Reports"],"summary":"List reports","parameters":[{"schema":{"type":"string"},"required":false,"name":"requestId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}}}}},"/api/v1/reports/bulk-create":{"post":{"tags":["Reports"],"summary":"Bulk create reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/bulk-status":{"post":{"tags":["Reports"],"summary":"Bulk get report statuses","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportStatusInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/bulk-summary":{"post":{"tags":["Reports"],"summary":"Bulk get report summary per run","description":"Returns aggregated report status counts per run. Only the latest report per template is counted (re-triggers are deduplicated).","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReportSummaryResponse"}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}":{"get":{"tags":["Reports"],"summary":"Get report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportResponse"}}}},"404":{"description":"Report not found"}}}},"/api/v1/reports/{id}/logs":{"get":{"tags":["Reports"],"summary":"Stream report logs (SSE)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Server-sent event stream of log entries"},"404":{"description":"Report not found"}}}},"/api/v1/reports/trigger":{"post":{"tags":["Reports"],"summary":"Trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"triggered":{"type":"number"},"reports":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}},"required":["triggered","reports"]}}}},"400":{"description":"Invalid input"},"404":{"description":"Run not found"}}}},"/api/v1/reports/bulk-trigger":{"post":{"tags":["Reports"],"summary":"Bulk trigger reports","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkTriggerReportsInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":{}}}}}},"400":{"description":"Invalid input"}}}},"/api/v1/reports/{id}/insights":{"get":{"tags":["Reports"],"summary":"Get insights for report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/InsightResponse"},{"type":"object","properties":{"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}}}]}}}}},"404":{"description":"Report not found"}}},"post":{"tags":["Reports"],"summary":"Link insight to report","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"isNew":{"type":"boolean"}},"required":["insightId"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"insightId":{"type":"string"},"referencedAt":{"type":["string","null"],"format":"date-time"},"isNew":{"type":"boolean"}},"required":["insightId","referencedAt","isNew"]}}}},"404":{"description":"Report or insight not found"},"409":{"description":"Insight already referenced by this report"}}}},"/api/v1/profiles":{"post":{"tags":["Profiles"],"summary":"Create a new profile","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"get":{"tags":["Profiles"],"summary":"List profiles","parameters":[{"schema":{"type":"string"},"required":false,"name":"workerType","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}":{"get":{"tags":["Profiles"],"summary":"Get profile with latest version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileWithVersionResponse"}}}}}},"post":{"tags":["Profiles"],"summary":"Create new profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"workerType":{"type":"string"},"model":{"type":"string"},"reasoningEffort":{"type":"string"},"agentVersion":{"type":"string"},"mcpServers":{"type":"array","items":{"type":"string"}},"skillRevisions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"$ref":"#/components/schemas/ResourceBindingSpec"}},"extensions":{"type":"array","items":{"type":"string"}}},"required":["workerType","model"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}},"put":{"tags":["Profiles"],"summary":"Update profile identity","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileIdentity"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"}}}}}},"delete":{"tags":["Profiles"],"summary":"Delete profile","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/profiles/{profileId}/versions":{"get":{"tags":["Profiles"],"summary":"List profile versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}}},"/api/v1/profiles/{profileId}/versions/{version}":{"get":{"tags":["Profiles"],"summary":"Get profile version","parameters":[{"schema":{"type":"string"},"required":true,"name":"profileId","in":"path"},{"schema":{"type":["number","null"]},"required":false,"name":"version","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileVersionResponse"}}}}}}},"/api/v1/report-templates/default-system-prompt":{"get":{"tags":["Report Templates"],"summary":"Get default system prompt","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string"}},"required":["content"]}}}}}}},"/api/v1/report-templates/available-models":{"get":{"tags":["Report Templates"],"summary":"List models available for report generation","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"}},"required":["modelId"]}}}}}}}},"/api/v1/report-templates":{"get":{"tags":["Report Templates"],"summary":"List report templates","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}}}}},"post":{"tags":["Report Templates"],"summary":"Create report template","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportTemplateInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"409":{"description":"Report template already exists"}}}},"/api/v1/report-templates/{id}":{"get":{"tags":["Report Templates"],"summary":"Get report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"put":{"tags":["Report Templates"],"summary":"Update report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateReportTemplateInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTemplateResponse"}}}},"404":{"description":"Report template not found"}}},"delete":{"tags":["Report Templates"],"summary":"Delete report template","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Report template not found"}}}},"/api/v1/agents":{"get":{"tags":["Agents"],"summary":"List agents","parameters":[{"schema":{"type":"string"},"required":false,"name":"modelProvider","in":"query"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentResponse"}}}}}}},"post":{"tags":["Agents"],"summary":"Create or update agent (upsert)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"409":{"description":"Restored agent versions conflict with an active queue owner"}}}},"/api/v1/agents/{id}":{"get":{"tags":["Agents"],"summary":"Get agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"404":{"description":"Agent not found"}}},"put":{"tags":["Agents"],"summary":"Update agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAgentInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"}}},"delete":{"tags":["Agents"],"summary":"Delete agent","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Agent not found"}}}},"/api/v1/agents/{id}/versions":{"get":{"tags":["Agents"],"summary":"List agent versions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentVersion"}}}}},"404":{"description":"Agent not found"}}},"post":{"tags":["Agents"],"summary":"Register agent version (upsert)","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterAgentVersionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Validation error"},"404":{"description":"Agent not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent registration did not stabilize"}}}},"/api/v1/agents/{id}/versions/{agentVersion}":{"patch":{"tags":["Agents"],"summary":"Patch agent version","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"agentVersion","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchAgentVersionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentVersion"}}}},"400":{"description":"Invalid status value"},"404":{"description":"Agent or version not found"},"409":{"description":"Queue is assigned to another agent"},"503":{"description":"Concurrent activation did not stabilize"}}}},"/api/v1/models":{"get":{"tags":["Models"],"summary":"List models","parameters":[{"schema":{"type":"string"},"required":false,"name":"agentId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"provider","in":"query"},{"schema":{"type":"string","enum":["active","disappeared"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ModelResponse"}}}}}}}},"/api/v1/models/{id}":{"get":{"tags":["Models"],"summary":"Get model","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelResponse"}}}}}}},"/api/v1/models/sync":{"post":{"tags":["Models"],"summary":"Sync models from provider","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"agentId":{"type":"string"},"provider":{"type":"string"},"models":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"providerAvailableFrom":{"type":"string"},"providerEndOfLife":{"type":"string"},"metadata":{"type":"object","additionalProperties":{}},"capabilities":{"type":"object","properties":{"reasoningEffort":{"type":"array","items":{"type":"string"}},"toolCalls":{"type":"boolean"},"vision":{"type":"boolean"},"streaming":{"type":"boolean"},"adaptiveThinking":{"type":"boolean"},"maxThinkingBudget":{"type":"number"}}}},"required":["id"]}},"scannedAt":{"type":"string"}},"required":["agentId","provider","models","scannedAt"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"added":{"type":"array","items":{"type":"string"}},"removed":{"type":"array","items":{"type":"string"}},"unchanged":{"type":"array","items":{"type":"string"}}},"required":["added","removed","unchanged"]}}}}}}},"/api/v1/mcp/servers":{"get":{"tags":["MCP Servers"],"summary":"List MCP servers","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"post":{"tags":["MCP Servers"],"summary":"Create MCP server","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"_id":{"type":"string","pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"},"name":{"type":"string"},"type":{"type":"string","enum":["sse","http","stdio"]},"url":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"env":{"type":"object","additionalProperties":{"type":"string"}},"headers":{"type":"array","items":{"$ref":"#/components/schemas/McpServerHeader"}},"sessionMode":{"type":"string","enum":["stateful","stateless"]},"version":{"type":"string"},"description":{"type":"string"}},"required":["_id","name","type"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}}},"/api/v1/mcp/servers/{id}":{"get":{"tags":["MCP Servers"],"summary":"Get MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"put":{"tags":["MCP Servers"],"summary":"Update MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMcpServerInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpServerResponse"}}}}}},"delete":{"tags":["MCP Servers"],"summary":"Delete MCP server","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/skills":{"get":{"tags":["Skills"],"summary":"List all skills","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"post":{"tags":["Skills"],"summary":"Create or import a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSkillInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}}}}},"/api/v1/skills/search":{"get":{"tags":["Skills"],"summary":"Search skills (internal + external)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/search/external":{"get":{"tags":["Skills"],"summary":"Search external skills registry","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project."},"required":false,"description":"Optional project scope. When provided, external results are de-duplicated only against skills already installed in that project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/skills/discover":{"get":{"tags":["Skills"],"summary":"Discover skills in a GitHub repository","parameters":[{"schema":{"type":"string"},"required":true,"name":"source","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillDiscoveryResult"}}}}},"400":{"description":"Missing or malformed source parameter"},"404":{"description":"Repository not found"},"502":{"description":"GitHub API error"}}}},"/api/v1/skills/{id}(*)/revisions":{"get":{"tags":["Skills"],"summary":"List skill revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}}},"404":{"description":"Skill not found"}}}},"/api/v1/skills/{id}(*)":{"get":{"tags":["Skills"],"summary":"Get skill by slug","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillResponse"}}}},"404":{"description":"Skill not found"}}},"delete":{"tags":["Skills"],"summary":"Soft-delete a skill","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Skill not found"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)/archive":{"get":{"tags":["Skill Revisions"],"summary":"Download skill revision archive","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Skill revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/skill-revisions/by-ref/{ref}(*)":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ref","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skill-revisions/{id}":{"get":{"tags":["Skill Revisions"],"summary":"Get skill revision by ID","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill revision not found"}}}},"/api/v1/skills/{id}(*)/resolve":{"post":{"tags":["Skills"],"summary":"Trigger skill resolution","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkillRevisionResponse"}}}},"404":{"description":"Skill not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebases":{"get":{"tags":["Codebases"],"summary":"List all codebases","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseResponse"}}}}}}},"post":{"tags":["Codebases"],"summary":"Create a codebase (archive codebases require the archive file)","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input or missing archive"}}}},"/api/v1/codebases/{id}":{"get":{"tags":["Codebases"],"summary":"Get a codebase","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"patch":{"tags":["Codebases"],"summary":"Update a codebase","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCodebaseInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseResponse"}}}},"404":{"description":"Codebase not found"}}},"delete":{"tags":["Codebases"],"summary":"Delete a codebase","responses":{"204":{"description":"Success"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebases/{id}/revisions":{"get":{"tags":["Codebases"],"summary":"List codebase revisions","parameters":[{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}}},"404":{"description":"Codebase not found"}}},"post":{"tags":["Codebases"],"summary":"Resolve a new git codebase revision","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveCodebaseRevisionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"400":{"description":"Not a git codebase"},"404":{"description":"Codebase not found"},"502":{"description":"GitHub resolution failed"}}}},"/api/v1/codebases/{id}/upload":{"post":{"tags":["Codebases"],"summary":"Upload a codebase archive as a new revision","responses":{"201":{"description":"Success"},"400":{"description":"Missing archive or not an archive codebase"},"404":{"description":"Codebase not found"}}}},"/api/v1/codebase-revisions/{id}/archive":{"get":{"tags":["Codebase Revisions"],"summary":"Download codebase revision archive","responses":{"200":{"description":"Binary tar.gz archive"},"404":{"description":"Revision or archive not found"},"500":{"description":"Blob storage error"}}}},"/api/v1/codebase-revisions/{id}":{"get":{"tags":["Codebase Revisions"],"summary":"Get codebase revision by id","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodebaseRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources":{"get":{"tags":["Resources"],"summary":"List all resources","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceResponse"}}}}}}},"post":{"tags":["Resources"],"summary":"Create a resource and its first revision","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceInput"}}}},"responses":{"201":{"description":"Success"},"400":{"description":"Invalid input"},"409":{"description":"Resource slug or revision ref already exists in this project"}}}},"/api/v1/resources/{id}":{"get":{"tags":["Resources"],"summary":"Get a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"patch":{"tags":["Resources"],"summary":"Update a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateResourceInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceResponse"}}}},"404":{"description":"Resource not found"}}},"delete":{"tags":["Resources"],"summary":"Delete a resource","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success"},"404":{"description":"Resource not found"}}}},"/api/v1/resources/{id}/revisions":{"get":{"tags":["Resource Revisions"],"summary":"List resource revisions","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}}},"404":{"description":"Resource not found"}}},"post":{"tags":["Resource Revisions"],"summary":"Create a resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceRevisionInput"}}}},"responses":{"201":{"description":"Success"},"404":{"description":"Resource not found"},"409":{"description":"Resource revision ref already exists in this project"}}}},"/api/v1/resources/{id}/revisions/latest":{"get":{"tags":["Resource Revisions"],"summary":"Get the latest resource revision","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/{id}/revisions/{revisionNumber}":{"get":{"tags":["Resource Revisions"],"summary":"Get a resource revision by resource and revision number","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"revisionNumber","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/resources/revisions/{id}":{"get":{"tags":["Resource Revisions"],"summary":"Get resource revision by id","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceRevisionResponse"}}}},"404":{"description":"Revision not found"}}}},"/api/v1/extensions":{"get":{"tags":["Extensions"],"summary":"List all extensions","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"post":{"tags":["Extensions"],"summary":"Create or import an extension","parameters":[{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExtensionInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}}},"/api/v1/extensions/search":{"get":{"tags":["Extensions"],"summary":"Search extensions (internal + marketplace)","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionSearchResult"}}}}},"400":{"description":"Missing query parameter"}}}},"/api/v1/extensions/{id}":{"get":{"tags":["Extensions"],"summary":"Get extension by ID","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"put":{"tags":["Extensions"],"summary":"Update extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateExtensionInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtensionResponse"}}}}}},"delete":{"tags":["Extensions"],"summary":"Delete extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/v1/extensions/{id}/versions":{"get":{"tags":["Extensions"],"summary":"List available versions for an extension","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":false,"name":"preRelease","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExtensionVersionInfo"}}}}}}}},"/api/v1/insights":{"get":{"tags":["Insights"],"summary":"List insights","parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"post":{"tags":["Insights"],"summary":"Create insight","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInsightInput"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}}}}},"/api/v1/insights/search":{"get":{"tags":["Insights"],"summary":"Search insights","parameters":[{"schema":{"type":"string"},"required":true,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"blocked","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","example":"00000000-0000-0000-0000-000000000000"},"required":true,"description":"Project scope. Required on scoped list and root-create operations; requests without a resolvable project are rejected with 400. There is no default project.","name":"projectId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InsightResponse"}}}}}}}},"/api/v1/insights/{id}":{"get":{"tags":["Insights"],"summary":"Get insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"put":{"tags":["Insights"],"summary":"Update insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateInsightInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}},"delete":{"tags":["Insights"],"summary":"Delete insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/upvote":{"post":{"tags":["Insights"],"summary":"Upvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/downvote":{"post":{"tags":["Insights"],"summary":"Downvote insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/block":{"post":{"tags":["Insights"],"summary":"Block insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/unblock":{"post":{"tags":["Insights"],"summary":"Unblock insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightResponse"}}}},"404":{"description":"Insight not found"}}}},"/api/v1/insights/{id}/reports":{"get":{"tags":["Insights"],"summary":"Get reports referencing insight","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ReportResponse"}}}}},"404":{"description":"Insight not found"}}}},"/api/v1/feature-flags":{"get":{"tags":["Feature Flags"],"summary":"List feature flags","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"/api/v1/feature-flags/{key}":{"put":{"tags":["Feature Flags"],"summary":"Update feature flag","parameters":[{"schema":{"type":"string"},"required":true,"name":"key","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeatureFlagInput"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlagResponse"}}}}}}}},"webhooks":{}} \ No newline at end of file