// Package specgen builds the code-first OpenAPI document from the Go contract // types. It lives outside apispec because it imports the controllers (to // reflect their request/response shapes), and controllers import apispec (for // the 501 stub) — keeping Build here breaks that cycle. apispec only embeds and // serves the committed openapi.yaml; specgen produces it. package specgen import ( "fmt" "net/http" "reflect" "strings" jsonschema "github.com/swaggest/jsonschema-go" openapi "github.com/swaggest/openapi-go" "github.com/swaggest/openapi-go/openapi31" "github.com/aoagents/agent-orchestrator/backend/internal/httpd/controllers" "github.com/aoagents/agent-orchestrator/backend/internal/httpd/envelope" projectsvc "github.com/aoagents/agent-orchestrator/backend/internal/service/project" ) // Build reflects the Go contract types and the operation registry below into // the OpenAPI document. It is the single source of truth for the /api/v1 // contract: `cmd/genspec` writes its output to apispec/openapi.yaml (the // committed, embedded artifact) and TestBuild_MatchesEmbedded asserts the embed // equals fresh Build() output so the two can never drift. Schema facets live as // struct tags on the service.*/controllers.* types; operation metadata (path, // status codes, summaries) lives here. // // Every wire shape is reflected straight from where it is used at runtime — the // request bodies, path params, and response envelopes from controllers, the // error envelope from httpd/envelope — so the served responses and the // generated schema share one definition each. func Build() ([]byte, error) { r := openapi31.NewReflector() // Derive `required` from the idiomatic Go convention: a JSON field without // `omitempty` is required. swaggest does not infer this on its own, so the // structs stay clean (only description/enum tags) and this hook adds the // required array. nonNullableSlices drops the spurious "null" type swaggest // stamps on every Go slice. r.DefaultOptions = append(r.DefaultOptions, jsonschema.InterceptProp(requiredFromJSONTag), jsonschema.InterceptNullability(nonNullableSlices), // Clean component schema names (which become the generated TS type names): // swaggest defaults to PackageType, e.g. "ProjectProject", "EnvelopeAPIError". jsonschema.InterceptDefName(schemaName), ) r.Spec.SetTitle("Agent Orchestrator HTTP daemon") r.Spec.SetVersion("0.1.0-route-shell") r.Spec.SetDescription("Loopback-only HTTP surface served by the Go daemon. " + "Generated from Go (code-first) — do not edit by hand; run `go generate ./...`.") r.Spec.Servers = []openapi31.Server{ *(&openapi31.Server{URL: "http://127.0.0.1:3001"}).WithDescription("Local daemon (loopback only)"), } r.Spec.Tags = []openapi31.Tag{ *(&openapi31.Tag{Name: "projects"}).WithDescription( "Project registry, configuration, and lifecycle administration"), *(&openapi31.Tag{Name: "sessions"}).WithDescription( "Agent session lifecycle and messaging"), *(&openapi31.Tag{Name: "prs"}).WithDescription( "Pull-request actions (SCM lane)"), *(&openapi31.Tag{Name: "reviews"}).WithDescription( "Code-review runs and findings"), *(&openapi31.Tag{Name: "notifications"}).WithDescription( "Durable dashboard notifications"), *(&openapi31.Tag{Name: "events"}).WithDescription( "Server-sent CDC event stream with durable replay"), *(&openapi31.Tag{Name: "import"}).WithDescription( "Legacy AO project import (availability probe and run)"), } for _, op := range operations() { oc, err := r.NewOperationContext(op.method, op.path) if err != nil { return nil, fmt.Errorf("new operation %s %s: %w", op.method, op.path, err) } oc.SetID(op.id) oc.SetSummary(op.summary) oc.SetTags(op.tag) for _, param := range op.pathParams { oc.AddReqStructure(param) } if op.reqBody != nil { // AddReqStructure leaves requestBody.required absent, which // OpenAPI reads as optional. These bodies are mandatory, so force // it — otherwise validators/generators treat the body as skippable. oc.AddReqStructure(op.reqBody, openapi.WithCustomize(markRequestBodyRequired)) } for _, resp := range op.resps { opts := []openapi.ContentOption{openapi.WithHTTPStatus(resp.status)} if op.contentTypes != nil && op.contentTypes[resp.status] != "" { opts = append(opts, openapi.WithContentType(op.contentTypes[resp.status])) } oc.AddRespStructure(resp.body, opts...) } if err := r.AddOperation(oc); err != nil { return nil, fmt.Errorf("add operation %s %s: %w", op.method, op.path, err) } } return r.Spec.MarshalYAML() } // schemaName maps swaggest's default PackageType component names (e.g. // "ProjectProject", "EnvelopeAPIError") to the clean, stable schema names that // become the generated TypeScript type names. Every reflected type is listed // explicitly: an unrecognised default name is returned verbatim, so a new type // surfaces as a visibly-wrong "PackageType" name in the diff (and the drift // test) rather than silently colliding with an existing schema via a // TrimPrefix catch-all. func schemaName(_ reflect.Type, defaultName string) string { if clean, ok := schemaNames[defaultName]; ok { return clean } return defaultName } // schemaNames is the exhaustive default→clean mapping for every type reflected // by projectOperations(). Add an entry when a new contract type is introduced; // the drift test fails until the spec is regenerated, which flags the gap. var schemaNames = map[string]string{ // httpd/envelope "EnvelopeAPIError": "APIError", // domain "DomainProjectID": "ProjectID", "DomainSessionID": "SessionID", "DomainIssueID": "IssueID", "DomainSession": "Session", "DomainProjectConfig": "ProjectConfig", "DomainAgentConfig": "AgentConfig", "DomainRoleOverride": "RoleOverride", // httpd/controllers (wire envelopes) "ControllersListProjectsResponse": "ListProjectsResponse", "ControllersProjectResponse": "ProjectResponse", "ControllersGetProjectResponse": "ProjectGetResponse", "ControllersProjectOrDegraded": "ProjectOrDegraded", "ControllersListSessionsQuery": "ListSessionsQuery", "ControllersCleanupSessionsQuery": "CleanupSessionsQuery", "ControllersListSessionsResponse": "ListSessionsResponse", "ControllersSpawnSessionRequest": "SpawnSessionRequest", "ControllersSessionResponse": "SessionResponse", "ControllersSessionPreviewResponse": "SessionPreviewResponse", "ControllersSetSessionPreviewRequest": "SetSessionPreviewRequest", "ControllersRenameSessionRequest": "RenameSessionRequest", "ControllersRenameSessionResponse": "RenameSessionResponse", "ControllersRestoreSessionResponse": "RestoreSessionResponse", "ControllersCleanupSessionsResponse": "CleanupSessionsResponse", "ControllersCleanupSkippedSession": "CleanupSkippedSession", "ControllersKillSessionResponse": "KillSessionResponse", "ControllersRollbackSessionResponse": "RollbackSessionResponse", "ControllersSendSessionMessageRequest": "SendSessionMessageRequest", "ControllersSendSessionMessageResponse": "SendSessionMessageResponse", "ControllersClaimPRResponse": "ClaimPRResponse", "ControllersClaimPRRequest": "ClaimPRRequest", "ControllersSessionPRFacts": "SessionPRFacts", "ControllersSessionPRSummary": "SessionPRSummary", "ControllersSessionPRCISummary": "SessionPRCISummary", "ControllersSessionPRFailingCheck": "SessionPRFailingCheck", "ControllersSessionPRReviewSummary": "SessionPRReviewSummary", "ControllersSessionPRUnresolvedReviewer": "SessionPRUnresolvedReviewer", "ControllersSessionPRReviewCommentLink": "SessionPRReviewCommentLink", "ControllersSessionPRMergeabilitySummary": "SessionPRMergeabilitySummary", "ControllersSessionPRConflictFile": "SessionPRConflictFile", "ControllersListSessionPRsResponse": "ListSessionPRsResponse", "ControllersSetActivityRequest": "SetActivityRequest", "ControllersSetActivityResponse": "SetActivityResponse", "ControllersSpawnOrchestratorRequest": "SpawnOrchestratorRequest", "ControllersSpawnOrchestratorResponse": "SpawnOrchestratorResponse", "ControllersOrchestratorResponse": "OrchestratorResponse", "ControllersListNotificationsQuery": "ListNotificationsQuery", "ControllersNotificationStreamQuery": "NotificationStreamQuery", "ControllersNotificationIDParam": "NotificationIDParam", "ControllersNotificationTarget": "NotificationTarget", "ControllersNotificationResponse": "NotificationResponse", "ControllersListNotificationsResponse": "ListNotificationsResponse", "ControllersMarkNotificationReadRequest": "MarkNotificationReadRequest", "ControllersNotificationEnvelope": "NotificationEnvelope", "ControllersMarkAllNotificationsReadResponse": "MarkAllNotificationsReadResponse", // httpd/controllers — PR wire envelopes "ControllersMergePRResponse": "MergePRResponse", "ControllersResolveCommentsRequest": "ResolveCommentsRequest", "ControllersResolveCommentsResponse": "ResolveCommentsResponse", // httpd/controllers — review wire envelopes "ControllersListReviewsResponse": "ListReviewsResponse", "ControllersReviewRunResponse": "ReviewRunResponse", "ControllersSubmitReviewInput": "SubmitReviewInput", // domain review entities "DomainReviewRun": "ReviewRun", // httpd/controllers: import wire envelopes "ControllersImportStatusResponse": "ImportStatusResponse", "ControllersImportRunResponse": "ImportRunResponse", // legacyimport report "LegacyimportReport": "ImportReport", // service/project entities + DTOs "ProjectProject": "Project", "ProjectSummary": "ProjectSummary", "ProjectDegraded": "DegradedProject", "ProjectAddInput": "AddProjectInput", "ProjectRemoveResult": "RemoveProjectResult", "ProjectSetConfigInput": "SetProjectConfigInput", "ProjectWorkspaceRepo": "WorkspaceRepo", } // markRequestBodyRequired sets requestBody.required: true on the operation's // JSON body. swaggest leaves it absent (== optional) for AddReqStructure bodies. func markRequestBodyRequired(cor openapi.ContentOrReference) { if rb, ok := cor.(*openapi31.RequestBodyOrReference); ok && rb.RequestBody != nil { rb.RequestBody.WithRequired(true) } } // nonNullableSlices drops the "null" that swaggest unions into every Go slice // type (a nil slice marshals as JSON null). A required array field should be // `T[]`, not `T[] | null`; the handlers normalise nil to an empty slice, so // null never reaches the wire. Byte slices (base64 strings) are left alone. func nonNullableSlices(p jsonschema.InterceptNullabilityParams) { if !p.NullAdded || p.Type == nil || p.Type.Kind() != reflect.Slice { return } if p.Type.Elem().Kind() == reflect.Uint8 { return } p.Schema.TypeEns().WithSimpleTypes(jsonschema.Array) p.Schema.Type.SliceOfSimpleTypeValues = nil } // requiredFromJSONTag marks a property required when its json tag lacks // `omitempty` (the Go convention for "always present"). Runs after default // processing so ParentSchema exists; skips fields without a json tag (e.g. path // params, which swaggest marks required on their own). func requiredFromJSONTag(p jsonschema.InterceptPropParams) error { if !p.Processed || p.ParentSchema == nil { return nil } jsonTag := p.Field.Tag.Get("json") if jsonTag == "" || jsonTag == "-" { return nil } parts := strings.Split(jsonTag, ",") name := parts[0] if name == "" { name = p.Name } for _, opt := range parts[1:] { if opt == "omitempty" { return nil } } for _, existing := range p.ParentSchema.Required { if existing == name { return nil } } p.ParentSchema.Required = append(p.ParentSchema.Required, name) return nil } // --- operation registry ----------------------------------------------------- type respUnit struct { status int body any } type operation struct { method, path, id, summary string tag string pathParams []any // path/query param containers (e.g. ProjectIDParam) reqBody any // JSON request body struct, nil when the op takes none resps []respUnit contentTypes map[int]string // optional non-JSON response content types by status } func operations() []operation { ops := append([]operation{}, eventOperations()...) ops = append(ops, projectOperations()...) ops = append(ops, sessionOperations()...) ops = append(ops, prOperations()...) ops = append(ops, reviewOperations()...) ops = append(ops, notificationOperations()...) ops = append(ops, importOperations()...) return ops } // importOperations declares the 2 /import operations. Must stay 1:1 with // the routes ImportController.Register mounts (enforced by the parity test). func importOperations() []operation { return []operation{ { method: http.MethodGet, path: "/api/v1/import", id: "getImportStatus", tag: "import", summary: "Check whether a legacy AO install is available to import", resps: []respUnit{ {http.StatusOK, controllers.ImportStatusResponse{}}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodPost, path: "/api/v1/import", id: "runImport", tag: "import", summary: "Run the legacy AO project import through the daemon store", resps: []respUnit{ {http.StatusOK, controllers.ImportRunResponse{}}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, } } func notificationOperations() []operation { return []operation{ { method: http.MethodGet, path: "/api/v1/notifications", id: "listNotifications", tag: "notifications", summary: "List unread notifications", pathParams: []any{controllers.ListNotificationsQuery{}}, resps: []respUnit{ {http.StatusOK, controllers.ListNotificationsResponse{}}, {http.StatusBadRequest, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodPatch, path: "/api/v1/notifications/{id}", id: "markNotificationRead", tag: "notifications", summary: "Mark a notification read", pathParams: []any{controllers.NotificationIDParam{}}, reqBody: controllers.MarkNotificationReadRequest{}, resps: []respUnit{ {http.StatusOK, controllers.NotificationEnvelope{}}, {http.StatusBadRequest, envelope.APIError{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodPost, path: "/api/v1/notifications/read-all", id: "markAllNotificationsRead", tag: "notifications", summary: "Mark all unread notifications read", resps: []respUnit{ {http.StatusOK, controllers.MarkAllNotificationsReadResponse{}}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodGet, path: "/api/v1/notifications/stream", id: "streamNotifications", tag: "notifications", summary: "Stream created notifications", pathParams: []any{controllers.NotificationStreamQuery{}}, resps: []respUnit{ {http.StatusOK, ""}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, contentTypes: map[int]string{http.StatusOK: "text/event-stream"}, }, } } // reviewOperations declares the session-scoped /reviews operations. Must stay // 1:1 with the routes ReviewsController.Register mounts (enforced by the parity // test). func reviewOperations() []operation { return []operation{ { method: http.MethodGet, path: "/api/v1/sessions/{sessionId}/reviews", id: "listReviews", tag: "reviews", summary: "List a worker's code-review runs", pathParams: []any{controllers.SessionIDParam{}}, resps: []respUnit{ {http.StatusOK, controllers.ListReviewsResponse{}}, {http.StatusUnprocessableEntity, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodPost, path: "/api/v1/sessions/{sessionId}/reviews/trigger", id: "triggerReview", tag: "reviews", summary: "Trigger a code review of a worker's PR", pathParams: []any{controllers.SessionIDParam{}}, resps: []respUnit{ {http.StatusOK, controllers.ReviewRunResponse{}}, {http.StatusCreated, controllers.ReviewRunResponse{}}, {http.StatusUnprocessableEntity, envelope.APIError{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodPost, path: "/api/v1/sessions/{sessionId}/reviews/submit", id: "submitReview", tag: "reviews", summary: "Record a reviewer's result for a worker's PR", pathParams: []any{controllers.SessionIDParam{}}, reqBody: controllers.SubmitReviewInput{}, resps: []respUnit{ {http.StatusOK, controllers.ReviewRunResponse{}}, {http.StatusBadRequest, envelope.APIError{}}, {http.StatusUnprocessableEntity, envelope.APIError{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, } } type eventsQuery struct { After *int64 `query:"after,omitempty" minimum:"0" description:"Replay events with seq greater than this cursor. When omitted, clients may send Last-Event-ID instead."` } func eventOperations() []operation { return []operation{ { method: http.MethodGet, path: "/api/v1/events", id: "streamEvents", tag: "events", summary: "Stream CDC events with durable replay", pathParams: []any{eventsQuery{}}, resps: []respUnit{ {http.StatusOK, ""}, {status: http.StatusBadRequest, body: envelope.APIError{}}, {status: http.StatusInternalServerError, body: envelope.APIError{}}, {status: http.StatusNotImplemented, body: envelope.APIError{}}, }, contentTypes: map[int]string{http.StatusOK: "text/event-stream"}, }, } } // projectOperations declares the 4 canonical /projects operations. The set must // stay 1:1 with the routes ProjectsController.Register mounts — // TestRouteSpecParity fails the build otherwise. func projectOperations() []operation { return []operation{ { method: http.MethodGet, path: "/api/v1/projects", id: "listProjects", tag: "projects", summary: "List all registered projects (active + degraded)", resps: []respUnit{ {http.StatusOK, controllers.ListProjectsResponse{}}, {http.StatusInternalServerError, envelope.APIError{}}, }, }, { method: http.MethodPost, path: "/api/v1/projects", id: "addProject", tag: "projects", summary: "Register a new project from a git repository path", reqBody: projectsvc.AddInput{}, resps: []respUnit{ {http.StatusCreated, controllers.ProjectResponse{}}, {http.StatusBadRequest, envelope.APIError{}}, {http.StatusConflict, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, }, }, { method: http.MethodGet, path: "/api/v1/projects/{id}", id: "getProject", tag: "projects", summary: "Fetch one project; discriminates ok vs degraded", pathParams: []any{controllers.ProjectIDParam{}}, resps: []respUnit{ {http.StatusOK, controllers.GetProjectResponse{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, }, }, { method: http.MethodPut, path: "/api/v1/projects/{id}/config", id: "setProjectConfig", tag: "projects", summary: "Replace a project's per-project config", pathParams: []any{controllers.ProjectIDParam{}}, reqBody: projectsvc.SetConfigInput{}, resps: []respUnit{ {http.StatusOK, controllers.ProjectResponse{}}, {http.StatusBadRequest, envelope.APIError{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, }, }, { method: http.MethodDelete, path: "/api/v1/projects/{id}", id: "removeProject", tag: "projects", summary: "Remove a project; stops sessions, cleans workspaces, unregisters", pathParams: []any{controllers.ProjectIDParam{}}, resps: []respUnit{ {http.StatusOK, projectsvc.RemoveResult{}}, {http.StatusBadRequest, envelope.APIError{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, }, }, } } func sessionOperations() []operation { return []operation{ { method: http.MethodGet, path: "/api/v1/sessions", id: "listSessions", tag: "sessions", summary: "List sessions", pathParams: []any{controllers.ListSessionsQuery{}}, resps: []respUnit{ {http.StatusOK, controllers.ListSessionsResponse{}}, {http.StatusBadRequest, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, }, }, { method: http.MethodPost, path: "/api/v1/sessions", id: "spawnSession", tag: "sessions", summary: "Spawn a new agent session", reqBody: controllers.SpawnSessionRequest{}, resps: []respUnit{ {http.StatusCreated, controllers.SessionResponse{}}, {http.StatusBadRequest, envelope.APIError{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, }, }, { method: http.MethodGet, path: "/api/v1/sessions/{sessionId}", id: "getSession", tag: "sessions", summary: "Fetch one session", pathParams: []any{controllers.SessionIDParam{}}, resps: []respUnit{ {http.StatusOK, controllers.SessionResponse{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, }, }, { method: http.MethodGet, path: "/api/v1/sessions/{sessionId}/preview", id: "getSessionPreview", tag: "sessions", summary: "Discover a browser preview URL for a session workspace", pathParams: []any{controllers.SessionIDParam{}}, resps: []respUnit{ {http.StatusOK, controllers.SessionPreviewResponse{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodPost, path: "/api/v1/sessions/{sessionId}/preview", id: "setSessionPreview", tag: "sessions", summary: "Set (or autodetect) the browser preview URL for a session", pathParams: []any{controllers.SessionIDParam{}}, reqBody: controllers.SetSessionPreviewRequest{}, resps: []respUnit{ {http.StatusOK, controllers.SessionResponse{}}, {http.StatusBadRequest, envelope.APIError{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodDelete, path: "/api/v1/sessions/{sessionId}/preview", id: "clearSessionPreview", tag: "sessions", summary: "Clear the browser preview URL for a session", pathParams: []any{controllers.SessionIDParam{}}, resps: []respUnit{ {http.StatusOK, controllers.SessionResponse{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodGet, path: "/api/v1/sessions/{sessionId}/preview/files/*", id: "getSessionPreviewFile", tag: "sessions", summary: "Serve a static browser preview file from a session workspace", pathParams: []any{controllers.SessionIDParam{}}, resps: []respUnit{ {http.StatusOK, ""}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, contentTypes: map[int]string{http.StatusOK: "text/html"}, }, { method: http.MethodGet, path: "/api/v1/sessions/{sessionId}/pr", id: "listSessionPRs", tag: "sessions", summary: "List pull requests owned by a session", pathParams: []any{controllers.SessionIDParam{}}, resps: []respUnit{ {http.StatusOK, controllers.ListSessionPRsResponse{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodPost, path: "/api/v1/sessions/{sessionId}/pr/claim", id: "claimSessionPR", tag: "sessions", summary: "Claim an existing pull request for a session", pathParams: []any{controllers.SessionIDParam{}}, reqBody: controllers.ClaimPRRequest{}, resps: []respUnit{ {http.StatusOK, controllers.ClaimPRResponse{}}, {http.StatusBadRequest, envelope.APIError{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusConflict, envelope.APIError{}}, {http.StatusUnprocessableEntity, envelope.APIError{}}, {http.StatusServiceUnavailable, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodPatch, path: "/api/v1/sessions/{sessionId}", id: "renameSession", tag: "sessions", summary: "Rename a session display name", pathParams: []any{controllers.SessionIDParam{}}, reqBody: controllers.RenameSessionRequest{}, resps: []respUnit{ {http.StatusOK, controllers.RenameSessionResponse{}}, {http.StatusBadRequest, envelope.APIError{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodPost, path: "/api/v1/sessions/cleanup", id: "cleanupSessions", tag: "sessions", summary: "Clean up terminated session workspaces", pathParams: []any{controllers.CleanupSessionsQuery{}}, resps: []respUnit{ {http.StatusOK, controllers.CleanupSessionsResponse{}}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodPost, path: "/api/v1/sessions/{sessionId}/restore", id: "restoreSession", tag: "sessions", summary: "Restore a terminated session", pathParams: []any{controllers.SessionIDParam{}}, resps: []respUnit{ {http.StatusOK, controllers.RestoreSessionResponse{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusConflict, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, }, }, { method: http.MethodPost, path: "/api/v1/sessions/{sessionId}/kill", id: "killSession", tag: "sessions", summary: "Mark a session terminated and tear down runtime/workspace resources", pathParams: []any{controllers.SessionIDParam{}}, resps: []respUnit{ {http.StatusOK, controllers.KillSessionResponse{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusConflict, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, }, }, { method: http.MethodPost, path: "/api/v1/sessions/{sessionId}/rollback", id: "rollbackSession", tag: "sessions", summary: "Undo a partially-completed spawn (delete seed row, or kill if spawn output exists)", pathParams: []any{controllers.SessionIDParam{}}, resps: []respUnit{ {http.StatusOK, controllers.RollbackSessionResponse{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusConflict, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, }, }, { method: http.MethodPost, path: "/api/v1/sessions/{sessionId}/send", id: "sendSessionMessage", tag: "sessions", summary: "Send a message to a running session's agent", pathParams: []any{controllers.SessionIDParam{}}, reqBody: controllers.SendSessionMessageRequest{}, resps: []respUnit{ {http.StatusOK, controllers.SendSessionMessageResponse{}}, {http.StatusBadRequest, envelope.APIError{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, }, }, { method: http.MethodPost, path: "/api/v1/sessions/{sessionId}/activity", id: "setSessionActivity", tag: "sessions", summary: "Report an agent activity-state signal for a session", pathParams: []any{controllers.SessionIDParam{}}, reqBody: controllers.SetActivityRequest{}, resps: []respUnit{ {http.StatusOK, controllers.SetActivityResponse{}}, {http.StatusBadRequest, envelope.APIError{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodGet, path: "/api/v1/orchestrators", id: "listOrchestrators", tag: "sessions", summary: "List orchestrator sessions across projects", resps: []respUnit{ {http.StatusOK, controllers.ListSessionsResponse{}}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodPost, path: "/api/v1/orchestrators", id: "spawnOrchestrator", tag: "sessions", summary: "Spawn an orchestrator session", reqBody: controllers.SpawnOrchestratorRequest{}, resps: []respUnit{ {http.StatusCreated, controllers.SpawnOrchestratorResponse{}}, {http.StatusBadRequest, envelope.APIError{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodGet, path: "/api/v1/orchestrators/{id}", id: "getOrchestrator", tag: "sessions", summary: "Fetch one orchestrator session", pathParams: []any{controllers.OrchestratorIDParam{}}, resps: []respUnit{ {http.StatusOK, controllers.SessionResponse{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusInternalServerError, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, } } // prOperations declares the PR action operations. These live in the SCM lane: // the handler delegates to a PRService backed by the SCM provider. A nil // PRService (SCM not configured) returns 501 for both routes. func prOperations() []operation { return []operation{ { method: http.MethodPost, path: "/api/v1/prs/{id}/merge", id: "mergePR", tag: "prs", summary: "Squash-merge a pull request", pathParams: []any{controllers.PRIDParam{}}, resps: []respUnit{ {http.StatusOK, controllers.MergePRResponse{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusConflict, envelope.APIError{}}, {http.StatusUnprocessableEntity, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, { method: http.MethodPost, path: "/api/v1/prs/{id}/resolve-comments", id: "resolveComments", tag: "prs", summary: "Resolve review threads on a pull request", pathParams: []any{controllers.PRIDParam{}}, reqBody: nil, // body is optional: omitting it resolves all unresolved threads resps: []respUnit{ {http.StatusOK, controllers.ResolveCommentsResponse{}}, {http.StatusNotFound, envelope.APIError{}}, {http.StatusUnprocessableEntity, envelope.APIError{}}, {http.StatusNotImplemented, envelope.APIError{}}, }, }, } }