{
  "openapi": "3.1.0",
  "info": {
    "title": "TaskForge Tickets API",
    "version": "tickets.v1",
    "description": "Submit asynchronous provider-neutral ticket-generation jobs, poll status, and fetch canonical tickets.result.v1 payloads."
  },
  "servers": [
    {
      "url": "http://127.0.0.1:8080",
      "description": "Local development"
    }
  ],
  "security": [{ "bearerApiKey": [] }],
  "paths": {
    "/tickets/v1/jobs": {
      "post": {
        "operationId": "SubmitTicketsJob",
        "summary": "Submit a ticket generation job",
        "description": "Requires scope tickets:submit. Returns 202 immediately; poll status_url until the job reaches a terminal status.",
        "tags": ["tickets"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/TicketsJobSubmitRequest" },
              "examples": {
                "canonical": {
                  "value": {
                    "schema_version": "tickets.input.v1",
                    "idempotency_key": "spec-job-uuid-or-stable-client-key",
                    "title": "Customer Billing Reconciliation Workspace",
                    "spec": "Full or condensed product/technical spec text",
                    "options": {
                      "ticket_count": 3,
                      "max_ticket_count": 6,
                      "include_epics": true,
                      "dependency_mode": "explicit",
                      "output_detail": "standard"
                    },
                    "callback_url": "https://issuehub.example.com/llm/jobs/{job_id}/callback"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TicketsJobAcceptedResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      },
      "get": {
        "operationId": "ListTicketsJobs",
        "summary": "List ticket jobs for the authenticated client",
        "description": "Requires scope tickets:read. Only jobs owned by the API key's client are returned.",
        "tags": ["tickets"],
        "responses": {
          "200": {
            "description": "Job list",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TicketsJobListResponse" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/task-generation/jobs": {
      "post": {
        "operationId": "SubmitTaskGenerationJobCompatibility",
        "summary": "Compatibility alias for SubmitTicketsJob",
        "description": "Requires scope tickets:submit. Accepts the same request body and returns the same response as POST /tickets/v1/jobs.",
        "tags": ["tickets"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/TicketsJobSubmitRequest" }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TicketsJobAcceptedResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/tickets/v1/jobs/{job_id}": {
      "parameters": [{ "$ref": "#/components/parameters/JobId" }],
      "get": {
        "operationId": "GetTicketsJob",
        "summary": "Get ticket job status",
        "description": "Requires scope tickets:read. Poll until status is completed, failed, cancelled, or expired.",
        "tags": ["tickets"],
        "responses": {
          "200": {
            "description": "Job status",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TicketsJobResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      },
      "delete": {
        "operationId": "CancelTicketsJob",
        "summary": "Cancel a ticket job",
        "description": "Requires scope tickets:cancel. Cancellation succeeds only while the job is cancellable.",
        "tags": ["tickets"],
        "responses": {
          "200": {
            "description": "Cancelled job or cancellation-requested job",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TicketsJobResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/tickets/v1/jobs/{job_id}/result": {
      "parameters": [{ "$ref": "#/components/parameters/JobId" }],
      "get": {
        "operationId": "GetTicketsJobResult",
        "summary": "Get completed ticket result",
        "description": "Requires scope tickets:read. Call after status is completed. The successful result is the canonical tickets.result.v1 payload, not a provider-specific wrapper.",
        "tags": ["tickets"],
        "responses": {
          "200": {
            "description": "Completed job result, or a terminal job status when no result exists",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/TicketsJobResultResponse" },
                    { "$ref": "#/components/schemas/TicketsJobResponse" }
                  ]
                }
              }
            }
          },
          "202": {
            "description": "Job result is not ready",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorBody" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/tickets/v1/schema": {
      "get": {
        "operationId": "GetTicketsSchema",
        "summary": "Get ticket schema metadata",
        "description": "Requires scope tickets:read. Returns version strings and the tickets.result.v1 JSON schema as a string.",
        "tags": ["tickets"],
        "responses": {
          "200": {
            "description": "Schema metadata",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TicketsSchemaResponse" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key issued in the TaskForge admin UI. Send as Authorization: Bearer <api_key>."
      }
    },
    "parameters": {
      "JobId": {
        "name": "job_id",
        "in": "path",
        "required": true,
        "schema": { "type": "string", "format": "uuid" },
        "description": "Job UUID returned by SubmitTicketsJob."
      }
    },
    "schemas": {
      "TicketsJobSubmitRequest": {
        "type": "object",
        "required": ["schema_version", "idempotency_key", "title", "spec"],
        "additionalProperties": false,
        "properties": {
          "schema_version": { "type": "string", "const": "tickets.input.v1" },
          "idempotency_key": {
            "type": "string",
            "minLength": 1,
            "description": "Stable client-generated key used to deduplicate retries for the same client and feature."
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "description": "Short name for the requested work."
          },
          "spec": {
            "type": "string",
            "minLength": 1,
            "description": "Detailed product or technical spec text to turn into provider-neutral ticket drafts."
          },
          "options": { "$ref": "#/components/schemas/TicketGenerationOptions" },
          "callback_url": {
            "type": "string",
            "format": "uri",
            "description": "Optional HTTPS callback URL. Localhost callback URLs are rejected."
          }
        }
      },
      "TicketGenerationOptions": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "ticket_count": { "type": "integer", "minimum": 1 },
          "max_ticket_count": { "type": "integer", "minimum": 1, "maximum": 50 },
          "include_epics": { "type": "boolean" },
          "dependency_mode": { "type": "string", "enum": ["explicit"] },
          "output_detail": { "type": "string", "enum": ["compact", "standard", "detailed"] }
        }
      },
      "TicketsJobAcceptedResponse": {
        "type": "object",
        "required": ["schema_version", "job_id", "status", "status_url", "result_url", "created_at"],
        "additionalProperties": false,
        "properties": {
          "schema_version": { "type": "string", "const": "tickets.job.v1" },
          "job_id": { "type": "string", "format": "uuid" },
          "status": { "$ref": "#/components/schemas/JobStatus" },
          "status_url": { "type": "string" },
          "result_url": { "type": "string" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "TicketsJobResponse": {
        "type": "object",
        "required": ["schema_version", "job_id", "status", "stage", "created_at", "updated_at", "expires_at", "error"],
        "additionalProperties": false,
        "properties": {
          "schema_version": { "type": "string", "const": "tickets.job.v1" },
          "job_id": { "type": "string", "format": "uuid" },
          "status": { "$ref": "#/components/schemas/JobStatus" },
          "stage": { "$ref": "#/components/schemas/JobStage" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" },
          "expires_at": { "type": "string", "format": "date-time" },
          "error": {
            "oneOf": [
              { "$ref": "#/components/schemas/TicketsJobError" },
              { "type": "null" }
            ]
          }
        }
      },
      "TicketsJobListResponse": {
        "type": "object",
        "required": ["jobs"],
        "additionalProperties": false,
        "properties": {
          "jobs": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/TicketsJobResponse" }
          }
        }
      },
      "TicketsJobResultResponse": { "$ref": "#/components/schemas/TicketsResult" },
      "TicketsResult": {
        "type": "object",
        "required": ["schema_version", "job_id", "source", "tickets", "risks", "open_questions", "generation"],
        "additionalProperties": false,
        "properties": {
          "schema_version": { "type": "string", "const": "tickets.result.v1" },
          "job_id": { "type": "string", "format": "uuid" },
          "source": { "$ref": "#/components/schemas/SourceSummary" },
          "tickets": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": { "$ref": "#/components/schemas/Ticket" }
          },
          "risks": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Risk" }
          },
          "open_questions": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/OpenQuestion" }
          },
          "generation": { "$ref": "#/components/schemas/GenerationMetadata" }
        }
      },
      "SourceSummary": {
        "type": "object",
        "required": ["title", "summary"],
        "additionalProperties": false,
        "properties": {
          "title": { "type": "string", "minLength": 1, "maxLength": 160 },
          "summary": { "type": "string", "minLength": 1 }
        }
      },
      "Ticket": {
        "type": "object",
        "required": [
          "external_id",
          "ticket_type",
          "title",
          "description",
          "acceptance_criteria",
          "labels",
          "dependencies",
          "story_points",
          "priority",
          "parent_external_id",
          "metadata"
        ],
        "additionalProperties": false,
        "properties": {
          "external_id": { "type": "string", "pattern": "^SPEC-[0-9]+$" },
          "ticket_type": { "type": "string", "enum": ["epic", "story", "task", "bug", "spike"] },
          "title": { "type": "string", "minLength": 1, "maxLength": 120 },
          "description": { "type": "string", "minLength": 1 },
          "acceptance_criteria": {
            "type": "array",
            "minItems": 1,
            "maxItems": 8,
            "items": { "type": "string", "minLength": 1 }
          },
          "labels": {
            "type": "array",
            "items": { "type": "string", "minLength": 1, "maxLength": 40 }
          },
          "dependencies": {
            "type": "array",
            "items": { "type": "string", "pattern": "^SPEC-[0-9]+$" }
          },
          "story_points": { "type": ["integer", "null"], "minimum": 1, "maximum": 100 },
          "priority": { "type": ["string", "null"], "enum": ["low", "medium", "high", "critical", null] },
          "parent_external_id": { "type": ["string", "null"], "pattern": "^SPEC-[0-9]+$" },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "component": { "type": "string" },
              "suggested_milestone": { "type": "string" },
              "confidence": { "type": "number", "minimum": 0, "maximum": 1 }
            }
          }
        }
      },
      "Risk": {
        "type": "object",
        "required": ["id", "description", "severity", "related_ticket_ids"],
        "additionalProperties": false,
        "properties": {
          "id": { "type": "string", "pattern": "^RISK-[0-9]+$" },
          "description": { "type": "string", "minLength": 1 },
          "severity": { "type": "string", "enum": ["low", "medium", "high", "critical"] },
          "related_ticket_ids": {
            "type": "array",
            "items": { "type": "string", "pattern": "^SPEC-[0-9]+$" }
          }
        }
      },
      "OpenQuestion": {
        "type": "object",
        "required": ["id", "question", "related_ticket_ids"],
        "additionalProperties": false,
        "properties": {
          "id": { "type": "string", "pattern": "^Q-[0-9]+$" },
          "question": { "type": "string", "minLength": 1 },
          "related_ticket_ids": {
            "type": "array",
            "items": { "type": "string", "pattern": "^SPEC-[0-9]+$" }
          }
        }
      },
      "GenerationMetadata": {
        "type": "object",
        "required": ["model", "duration_ms", "prompt_tokens", "completion_tokens", "finish_reason"],
        "additionalProperties": false,
        "properties": {
          "model": { "type": "string" },
          "duration_ms": { "type": "integer", "minimum": 0 },
          "prompt_tokens": { "type": ["integer", "null"], "minimum": 0 },
          "completion_tokens": { "type": ["integer", "null"], "minimum": 0 },
          "finish_reason": { "type": ["string", "null"] }
        }
      },
      "TicketsSchemaResponse": {
        "type": "object",
        "required": ["input_schema_version", "result_schema_version", "schema_json"],
        "additionalProperties": false,
        "properties": {
          "input_schema_version": { "type": "string", "example": "tickets.input.v1" },
          "result_schema_version": { "type": "string", "example": "tickets.result.v1" },
          "schema_json": {
            "type": "string",
            "description": "tickets.result.v1 JSON schema document encoded as a string."
          }
        }
      },
      "TicketsJobError": {
        "type": "object",
        "required": ["code", "message"],
        "additionalProperties": false,
        "properties": {
          "code": { "type": "string" },
          "message": { "type": "string" }
        }
      },
      "JobStatus": {
        "type": "string",
        "enum": ["queued", "processing", "completed", "failed", "cancel_requested", "cancelled", "expired"]
      },
      "JobStage": {
        "type": "string",
        "enum": ["accepted", "queued", "preparing_prompt", "generating_tickets", "validating_output", "completed", "failed"]
      },
      "ErrorBody": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "$ref": "#/components/schemas/ErrorPayload" }
        }
      },
      "ErrorPayload": {
        "type": "object",
        "required": ["code", "message"],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "invalid_request",
              "request_too_large",
              "unauthorized",
              "forbidden",
              "client_suspended",
              "key_expired",
              "key_revoked",
              "feature_disabled",
              "queue_full",
              "job_not_found",
              "job_cancelled",
              "ollama_unavailable",
              "model_timeout",
              "model_output_invalid",
              "storage_error",
              "callback_failed",
              "rate_limited",
              "internal_error"
            ]
          },
          "message": { "type": "string" }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid request",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      },
      "Unauthorized": {
        "description": "Missing, malformed, expired, or revoked bearer token",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      },
      "Forbidden": {
        "description": "API key is valid but does not have the required scope",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      },
      "NotFound": {
        "description": "Job not found for the authenticated client",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      },
      "Conflict": {
        "description": "Job is not cancellable",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      },
      "RateLimited": {
        "description": "Rate limit exceeded",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      },
      "InternalError": {
        "description": "Storage, model, or internal error",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      }
    }
  }
}
