diff --git a/apps/api/prisma/migrations/20260201205935_add_job_tracking/migration.sql b/apps/api/prisma/migrations/20260201205935_add_job_tracking/migration.sql new file mode 100644 index 0000000..174dbf2 --- /dev/null +++ b/apps/api/prisma/migrations/20260201205935_add_job_tracking/migration.sql @@ -0,0 +1,112 @@ +-- CreateEnum +CREATE TYPE "RunnerJobStatus" AS ENUM ('PENDING', 'QUEUED', 'RUNNING', 'COMPLETED', 'FAILED', 'CANCELLED'); + +-- CreateEnum +CREATE TYPE "JobStepPhase" AS ENUM ('SETUP', 'EXECUTION', 'VALIDATION', 'CLEANUP'); + +-- CreateEnum +CREATE TYPE "JobStepType" AS ENUM ('COMMAND', 'AI_ACTION', 'GATE', 'ARTIFACT'); + +-- CreateEnum +CREATE TYPE "JobStepStatus" AS ENUM ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED', 'SKIPPED'); + +-- CreateTable +CREATE TABLE "runner_jobs" ( + "id" UUID NOT NULL, + "workspace_id" UUID NOT NULL, + "agent_task_id" UUID, + "type" TEXT NOT NULL, + "status" "RunnerJobStatus" NOT NULL DEFAULT 'PENDING', + "priority" INTEGER NOT NULL, + "progress_percent" INTEGER NOT NULL DEFAULT 0, + "result" JSONB, + "error" TEXT, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "started_at" TIMESTAMPTZ, + "completed_at" TIMESTAMPTZ, + + CONSTRAINT "runner_jobs_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "job_steps" ( + "id" UUID NOT NULL, + "job_id" UUID NOT NULL, + "ordinal" INTEGER NOT NULL, + "phase" "JobStepPhase" NOT NULL, + "name" TEXT NOT NULL, + "type" "JobStepType" NOT NULL, + "status" "JobStepStatus" NOT NULL DEFAULT 'PENDING', + "output" TEXT, + "tokens_input" INTEGER, + "tokens_output" INTEGER, + "started_at" TIMESTAMPTZ, + "completed_at" TIMESTAMPTZ, + "duration_ms" INTEGER, + + CONSTRAINT "job_steps_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "job_events" ( + "id" UUID NOT NULL, + "job_id" UUID NOT NULL, + "step_id" UUID, + "type" TEXT NOT NULL, + "timestamp" TIMESTAMPTZ NOT NULL, + "actor" TEXT NOT NULL, + "payload" JSONB NOT NULL, + + CONSTRAINT "job_events_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "runner_jobs_id_workspace_id_key" ON "runner_jobs"("id", "workspace_id"); + +-- CreateIndex +CREATE INDEX "runner_jobs_workspace_id_idx" ON "runner_jobs"("workspace_id"); + +-- CreateIndex +CREATE INDEX "runner_jobs_workspace_id_status_idx" ON "runner_jobs"("workspace_id", "status"); + +-- CreateIndex +CREATE INDEX "runner_jobs_agent_task_id_idx" ON "runner_jobs"("agent_task_id"); + +-- CreateIndex +CREATE INDEX "runner_jobs_priority_idx" ON "runner_jobs"("priority"); + +-- CreateIndex +CREATE INDEX "job_steps_job_id_idx" ON "job_steps"("job_id"); + +-- CreateIndex +CREATE INDEX "job_steps_job_id_ordinal_idx" ON "job_steps"("job_id", "ordinal"); + +-- CreateIndex +CREATE INDEX "job_steps_status_idx" ON "job_steps"("status"); + +-- CreateIndex +CREATE INDEX "job_events_job_id_idx" ON "job_events"("job_id"); + +-- CreateIndex +CREATE INDEX "job_events_step_id_idx" ON "job_events"("step_id"); + +-- CreateIndex +CREATE INDEX "job_events_timestamp_idx" ON "job_events"("timestamp"); + +-- CreateIndex +CREATE INDEX "job_events_type_idx" ON "job_events"("type"); + +-- AddForeignKey +ALTER TABLE "runner_jobs" ADD CONSTRAINT "runner_jobs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "runner_jobs" ADD CONSTRAINT "runner_jobs_agent_task_id_fkey" FOREIGN KEY ("agent_task_id") REFERENCES "agent_tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "job_steps" ADD CONSTRAINT "job_steps_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "runner_jobs"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "job_events" ADD CONSTRAINT "job_events_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "runner_jobs"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "job_events" ADD CONSTRAINT "job_events_step_id_fkey" FOREIGN KEY ("step_id") REFERENCES "job_steps"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index eb0d770..bf95e25 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -135,6 +135,37 @@ enum FormalityLevel { VERY_FORMAL } +enum RunnerJobStatus { + PENDING + QUEUED + RUNNING + COMPLETED + FAILED + CANCELLED +} + +enum JobStepPhase { + SETUP + EXECUTION + VALIDATION + CLEANUP +} + +enum JobStepType { + COMMAND + AI_ACTION + GATE + ARTIFACT +} + +enum JobStepStatus { + PENDING + RUNNING + COMPLETED + FAILED + SKIPPED +} + // ============================================ // MODELS // ============================================ @@ -151,24 +182,24 @@ model User { updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz // Relations - ownedWorkspaces Workspace[] @relation("WorkspaceOwner") - workspaceMemberships WorkspaceMember[] - teamMemberships TeamMember[] - assignedTasks Task[] @relation("TaskAssignee") - createdTasks Task[] @relation("TaskCreator") - createdEvents Event[] @relation("EventCreator") - createdProjects Project[] @relation("ProjectCreator") - activityLogs ActivityLog[] - sessions Session[] - accounts Account[] - ideas Idea[] @relation("IdeaCreator") - relationships Relationship[] @relation("RelationshipCreator") - agentSessions AgentSession[] - agentTasks AgentTask[] @relation("AgentTaskCreator") - userLayouts UserLayout[] - userPreference UserPreference? - knowledgeEntryVersions KnowledgeEntryVersion[] @relation("EntryVersionAuthor") - llmProviders LlmProviderInstance[] @relation("UserLlmProviders") + ownedWorkspaces Workspace[] @relation("WorkspaceOwner") + workspaceMemberships WorkspaceMember[] + teamMemberships TeamMember[] + assignedTasks Task[] @relation("TaskAssignee") + createdTasks Task[] @relation("TaskCreator") + createdEvents Event[] @relation("EventCreator") + createdProjects Project[] @relation("ProjectCreator") + activityLogs ActivityLog[] + sessions Session[] + accounts Account[] + ideas Idea[] @relation("IdeaCreator") + relationships Relationship[] @relation("RelationshipCreator") + agentSessions AgentSession[] + agentTasks AgentTask[] @relation("AgentTaskCreator") + userLayouts UserLayout[] + userPreference UserPreference? + knowledgeEntryVersions KnowledgeEntryVersion[] @relation("EntryVersionAuthor") + llmProviders LlmProviderInstance[] @relation("UserLlmProviders") @@map("users") } @@ -195,7 +226,7 @@ model Workspace { updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz // Relations - owner User @relation("WorkspaceOwner", fields: [ownerId], references: [id], onDelete: Cascade) + owner User @relation("WorkspaceOwner", fields: [ownerId], references: [id], onDelete: Cascade) members WorkspaceMember[] teams Team[] tasks Task[] @@ -216,6 +247,7 @@ model Workspace { personalities Personality[] llmSettings WorkspaceLlmSettings? qualityGates QualityGate[] + runnerJobs RunnerJob[] @@index([ownerId]) @@map("workspaces") @@ -565,8 +597,8 @@ model Agent { } model AgentTask { - id String @id @default(uuid()) @db.Uuid - workspaceId String @map("workspace_id") @db.Uuid + id String @id @default(uuid()) @db.Uuid + workspaceId String @map("workspace_id") @db.Uuid // Task details title String @@ -575,23 +607,24 @@ model AgentTask { priority AgentTaskPriority @default(MEDIUM) // Agent configuration - agentType String @map("agent_type") - agentConfig Json @default("{}") @map("agent_config") + agentType String @map("agent_type") + agentConfig Json @default("{}") @map("agent_config") // Results - result Json? - error String? @db.Text + result Json? + error String? @db.Text // Timing - createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz - updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz - startedAt DateTime? @map("started_at") @db.Timestamptz - completedAt DateTime? @map("completed_at") @db.Timestamptz + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz + startedAt DateTime? @map("started_at") @db.Timestamptz + completedAt DateTime? @map("completed_at") @db.Timestamptz // Relations - workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) - createdBy User @relation("AgentTaskCreator", fields: [createdById], references: [id], onDelete: Cascade) - createdById String @map("created_by_id") @db.Uuid + workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) + createdBy User @relation("AgentTaskCreator", fields: [createdById], references: [id], onDelete: Cascade) + createdById String @map("created_by_id") @db.Uuid + runnerJobs RunnerJob[] @@unique([id, workspaceId]) @@index([workspaceId]) @@ -890,18 +923,18 @@ model KnowledgeEmbedding { // ============================================ model CronSchedule { - id String @id @default(uuid()) @db.Uuid - workspaceId String @map("workspace_id") @db.Uuid + id String @id @default(uuid()) @db.Uuid + workspaceId String @map("workspace_id") @db.Uuid workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) // Cron configuration - expression String // Standard cron: "0 9 * * *" = 9am daily - command String // MoltBot command to trigger + expression String // Standard cron: "0 9 * * *" = 9am daily + command String // MoltBot command to trigger // State - enabled Boolean @default(true) - lastRun DateTime? @map("last_run") @db.Timestamptz - nextRun DateTime? @map("next_run") @db.Timestamptz + enabled Boolean @default(true) + lastRun DateTime? @map("last_run") @db.Timestamptz + nextRun DateTime? @map("next_run") @db.Timestamptz // Audit createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz @@ -918,22 +951,22 @@ model CronSchedule { // ============================================ model Personality { - id String @id @default(uuid()) @db.Uuid - workspaceId String @map("workspace_id") @db.Uuid + id String @id @default(uuid()) @db.Uuid + workspaceId String @map("workspace_id") @db.Uuid workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) // Identity - name String // unique identifier slug - displayName String @map("display_name") - description String? @db.Text + name String // unique identifier slug + displayName String @map("display_name") + description String? @db.Text // System prompt systemPrompt String @map("system_prompt") @db.Text // LLM configuration - temperature Float? // null = use provider default - maxTokens Int? @map("max_tokens") // null = use provider default - llmProviderInstanceId String? @map("llm_provider_instance_id") @db.Uuid + temperature Float? // null = use provider default + maxTokens Int? @map("max_tokens") // null = use provider default + llmProviderInstanceId String? @map("llm_provider_instance_id") @db.Uuid // Status isDefault Boolean @default(false) @map("is_default") @@ -961,20 +994,20 @@ model Personality { // ============================================ model LlmProviderInstance { - id String @id @default(uuid()) @db.Uuid - providerType String @map("provider_type") // "ollama" | "claude" | "openai" - displayName String @map("display_name") - userId String? @map("user_id") @db.Uuid // NULL = system-level, UUID = user-level - config Json // Provider-specific configuration - isDefault Boolean @default(false) @map("is_default") - isEnabled Boolean @default(true) @map("is_enabled") + id String @id @default(uuid()) @db.Uuid + providerType String @map("provider_type") // "ollama" | "claude" | "openai" + displayName String @map("display_name") + userId String? @map("user_id") @db.Uuid // NULL = system-level, UUID = user-level + config Json // Provider-specific configuration + isDefault Boolean @default(false) @map("is_default") + isEnabled Boolean @default(true) @map("is_enabled") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz // Relations - user User? @relation("UserLlmProviders", fields: [userId], references: [id], onDelete: Cascade) - personalities Personality[] @relation("PersonalityLlmProvider") - workspaceLlmSettings WorkspaceLlmSettings[] @relation("WorkspaceLlmProvider") + user User? @relation("UserLlmProviders", fields: [userId], references: [id], onDelete: Cascade) + personalities Personality[] @relation("PersonalityLlmProvider") + workspaceLlmSettings WorkspaceLlmSettings[] @relation("WorkspaceLlmProvider") @@index([userId]) @@index([providerType]) @@ -1010,20 +1043,20 @@ model WorkspaceLlmSettings { // ============================================ model QualityGate { - id String @id @default(uuid()) @db.Uuid - workspaceId String @map("workspace_id") @db.Uuid + id String @id @default(uuid()) @db.Uuid + workspaceId String @map("workspace_id") @db.Uuid workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) name String description String? - type String // 'build' | 'lint' | 'test' | 'coverage' | 'custom' + type String // 'build' | 'lint' | 'test' | 'coverage' | 'custom' command String? - expectedOutput String? @map("expected_output") - isRegex Boolean @default(false) @map("is_regex") - required Boolean @default(true) - order Int @default(0) - isEnabled Boolean @default(true) @map("is_enabled") - createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz - updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz + expectedOutput String? @map("expected_output") + isRegex Boolean @default(false) @map("is_regex") + required Boolean @default(true) + order Int @default(0) + isEnabled Boolean @default(true) @map("is_enabled") + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz @@unique([workspaceId, name]) @@index([workspaceId]) @@ -1032,19 +1065,19 @@ model QualityGate { } model TaskRejection { - id String @id @default(uuid()) @db.Uuid - taskId String @map("task_id") - workspaceId String @map("workspace_id") - agentId String @map("agent_id") - attemptCount Int @map("attempt_count") - failures Json // FailureSummary[] - originalTask String @map("original_task") - startedAt DateTime @map("started_at") @db.Timestamptz - rejectedAt DateTime @map("rejected_at") @db.Timestamptz - escalated Boolean @default(false) - manualReview Boolean @default(false) @map("manual_review") - resolvedAt DateTime? @map("resolved_at") @db.Timestamptz - resolution String? + id String @id @default(uuid()) @db.Uuid + taskId String @map("task_id") + workspaceId String @map("workspace_id") + agentId String @map("agent_id") + attemptCount Int @map("attempt_count") + failures Json // FailureSummary[] + originalTask String @map("original_task") + startedAt DateTime @map("started_at") @db.Timestamptz + rejectedAt DateTime @map("rejected_at") @db.Timestamptz + escalated Boolean @default(false) + manualReview Boolean @default(false) @map("manual_review") + resolvedAt DateTime? @map("resolved_at") @db.Timestamptz + resolution String? @@index([taskId]) @@index([workspaceId]) @@ -1055,22 +1088,22 @@ model TaskRejection { } model TokenBudget { - id String @id @default(uuid()) @db.Uuid - taskId String @unique @map("task_id") @db.Uuid - workspaceId String @map("workspace_id") @db.Uuid - agentId String @map("agent_id") + id String @id @default(uuid()) @db.Uuid + taskId String @unique @map("task_id") @db.Uuid + workspaceId String @map("workspace_id") @db.Uuid + agentId String @map("agent_id") // Budget allocation - allocatedTokens Int @map("allocated_tokens") + allocatedTokens Int @map("allocated_tokens") estimatedComplexity String @map("estimated_complexity") // "low", "medium", "high", "critical" // Usage tracking - inputTokensUsed Int @default(0) @map("input_tokens_used") - outputTokensUsed Int @default(0) @map("output_tokens_used") - totalTokensUsed Int @default(0) @map("total_tokens_used") + inputTokensUsed Int @default(0) @map("input_tokens_used") + outputTokensUsed Int @default(0) @map("output_tokens_used") + totalTokensUsed Int @default(0) @map("total_tokens_used") // Cost tracking - estimatedCost Decimal? @map("estimated_cost") @db.Decimal(10, 6) + estimatedCost Decimal? @map("estimated_cost") @db.Decimal(10, 6) // State startedAt DateTime @default(now()) @map("started_at") @db.Timestamptz @@ -1078,12 +1111,103 @@ model TokenBudget { completedAt DateTime? @map("completed_at") @db.Timestamptz // Analysis - budgetUtilization Float? @map("budget_utilization") // 0.0 - 1.0 - suspiciousPattern Boolean @default(false) @map("suspicious_pattern") - suspiciousReason String? @map("suspicious_reason") + budgetUtilization Float? @map("budget_utilization") // 0.0 - 1.0 + suspiciousPattern Boolean @default(false) @map("suspicious_pattern") + suspiciousReason String? @map("suspicious_reason") @@index([taskId]) @@index([workspaceId]) @@index([suspiciousPattern]) @@map("token_budgets") } + +// ============================================ +// RUNNER JOB TRACKING MODULE +// ============================================ + +model RunnerJob { + id String @id @default(uuid()) @db.Uuid + workspaceId String @map("workspace_id") @db.Uuid + agentTaskId String? @map("agent_task_id") @db.Uuid + + // Job details + type String // 'git-status', 'code-task', 'priority-calc' + status RunnerJobStatus @default(PENDING) + priority Int + progressPercent Int @default(0) @map("progress_percent") + + // Results + result Json? + error String? @db.Text + + // Timing + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz + startedAt DateTime? @map("started_at") @db.Timestamptz + completedAt DateTime? @map("completed_at") @db.Timestamptz + + // Relations + workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) + agentTask AgentTask? @relation(fields: [agentTaskId], references: [id], onDelete: SetNull) + steps JobStep[] + events JobEvent[] + + @@unique([id, workspaceId]) + @@index([workspaceId]) + @@index([workspaceId, status]) + @@index([agentTaskId]) + @@index([priority]) + @@map("runner_jobs") +} + +model JobStep { + id String @id @default(uuid()) @db.Uuid + jobId String @map("job_id") @db.Uuid + + // Step details + ordinal Int + phase JobStepPhase + name String + type JobStepType + status JobStepStatus @default(PENDING) + + // Output and metrics + output String? @db.Text + tokensInput Int? @map("tokens_input") + tokensOutput Int? @map("tokens_output") + + // Timing + startedAt DateTime? @map("started_at") @db.Timestamptz + completedAt DateTime? @map("completed_at") @db.Timestamptz + durationMs Int? @map("duration_ms") + + // Relations + job RunnerJob @relation(fields: [jobId], references: [id], onDelete: Cascade) + events JobEvent[] + + @@index([jobId]) + @@index([jobId, ordinal]) + @@index([status]) + @@map("job_steps") +} + +model JobEvent { + id String @id @default(uuid()) @db.Uuid + jobId String @map("job_id") @db.Uuid + stepId String? @map("step_id") @db.Uuid + + // Event details + type String + timestamp DateTime @db.Timestamptz + actor String + payload Json + + // Relations + job RunnerJob @relation(fields: [jobId], references: [id], onDelete: Cascade) + step JobStep? @relation(fields: [stepId], references: [id], onDelete: Cascade) + + @@index([jobId]) + @@index([stepId]) + @@index([timestamp]) + @@index([type]) + @@map("job_events") +} diff --git a/docs/scratchpads/164-database-schema-jobs.md b/docs/scratchpads/164-database-schema-jobs.md new file mode 100644 index 0000000..7822196 --- /dev/null +++ b/docs/scratchpads/164-database-schema-jobs.md @@ -0,0 +1,109 @@ +# Issue #164: Database schema for job tracking + +## Objective + +Add Prisma schema for runner_jobs, job_steps, and job_events tables to support the autonomous runner infrastructure. + +## Approach + +1. Read existing schema.prisma to understand current conventions +2. Add four enums: RunnerJobStatus, JobStepPhase, JobStepType, JobStepStatus +3. Add three models: RunnerJob, JobStep, JobEvent +4. Create and run migration +5. Verify migration succeeds + +## Schema Design + +### Enums + +- **RunnerJobStatus**: PENDING, QUEUED, RUNNING, COMPLETED, FAILED, CANCELLED +- **JobStepPhase**: SETUP, EXECUTION, VALIDATION, CLEANUP +- **JobStepType**: COMMAND, AI_ACTION, GATE, ARTIFACT +- **JobStepStatus**: PENDING, RUNNING, COMPLETED, FAILED, SKIPPED + +### Models + +1. **RunnerJob** - Top-level job tracking + - Links to workspace and optionally to agent_task + - Tracks overall job status, progress, result + - Timestamps: created_at, started_at, completed_at + +2. **JobStep** - Granular step tracking + - Child of RunnerJob + - Phase-based organization (SETUP, EXECUTION, etc.) + - Token tracking for AI operations + - Duration tracking + +3. **JobEvent** - Event sourcing audit log + - Immutable event log for jobs and steps + - Links to both job and optionally step + - Actor tracking for accountability + +## Progress + +- [x] Read existing schema.prisma +- [x] Read architecture document for schema requirements +- [x] Add enums (RunnerJobStatus, JobStepPhase, JobStepType, JobStepStatus) +- [x] Add RunnerJob model with workspace and agentTask relations +- [x] Add JobStep model with job relation +- [x] Add JobEvent model with job and step relations +- [x] Add RunnerJob[] to Workspace and AgentTask relations +- [x] Create migration (20260201205935_add_job_tracking) +- [x] Test migration - all tables created successfully +- [x] Run quality gates (typecheck, lint, build - all passed) +- [x] Generate Prisma client +- [ ] Commit changes + +## Schema Observations from Existing Code + +**Conventions Identified:** + +- UUID primary keys with `@db.Uuid` annotation +- snake_case for database column names via `@map` +- snake_case for table names via `@@map` +- Timestamps use `@db.Timestamptz` for timezone awareness +- workspace_id on all workspace-scoped tables with cascading deletes +- Composite unique constraints with `@@unique([id, workspaceId])` +- Consistent indexing patterns: workspace_id, status, timestamps +- Json fields for flexible metadata with `@default("{}")` +- Optional foreign keys use `@db.Uuid` without NOT NULL +- Relations use descriptive names in both directions + +## Testing + +Since this is a schema-only change, testing will verify: + +- Migration runs successfully ✅ +- Foreign key constraints are valid ✅ +- Schema matches architecture document ✅ + +Verification performed: + +1. Database tables created: runner_jobs, job_steps, job_events +2. All enums created: RunnerJobStatus, JobStepPhase, JobStepType, JobStepStatus +3. Foreign key relationships verified: + - runner_jobs → workspaces (workspace_id) + - runner_jobs → agent_tasks (agent_task_id, optional) + - job_steps → runner_jobs (job_id) + - job_events → runner_jobs (job_id) + - job_events → job_steps (step_id, optional) +4. Indexes created for performance: + - workspace_id for workspace filtering + - status for job querying + - priority for job prioritization + - timestamp for event ordering +5. Quality gates passed: + - TypeScript compilation ✅ + - ESLint checks ✅ + - NestJS build ✅ + - Prisma client generation ✅ + +## Notes + +- Following existing patterns from schema.prisma +- Using UUID for all primary keys (existing convention) +- Using snake_case for table names (Prisma convention) +- All workspace-scoped tables include workspace_id for RLS +- Migration file created: 20260201205935_add_job_tracking +- Database push successful, migration marked as applied +- Schema format validated successfully