feat(#164): Add database schema for job tracking

Add Prisma schema for runner jobs, job steps, and job events to support
the autonomous runner infrastructure (M4.2).

Enums added:
- 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 added:
- RunnerJob: Top-level job tracking linked to workspace and agent_tasks
- JobStep: Granular step tracking within jobs with phase organization
- JobEvent: Immutable event sourcing audit log for jobs and steps

Foreign key relationships:
- runner_jobs → workspaces (workspace_id, CASCADE)
- runner_jobs → agent_tasks (agent_task_id, SET NULL)
- job_steps → runner_jobs (job_id, CASCADE)
- job_events → runner_jobs (job_id, CASCADE)
- job_events → job_steps (step_id, CASCADE)

Indexes added for performance on workspace_id, status, priority, timestamp.

Migration: 20260201205935_add_job_tracking

Quality gates passed: typecheck, lint, build

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 21:01:57 -06:00
parent e09950f225
commit 65b1dad64f
3 changed files with 437 additions and 92 deletions

View File

@@ -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;

View File

@@ -135,6 +135,37 @@ enum FormalityLevel {
VERY_FORMAL 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 // MODELS
// ============================================ // ============================================
@@ -151,24 +182,24 @@ model User {
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
// Relations // Relations
ownedWorkspaces Workspace[] @relation("WorkspaceOwner") ownedWorkspaces Workspace[] @relation("WorkspaceOwner")
workspaceMemberships WorkspaceMember[] workspaceMemberships WorkspaceMember[]
teamMemberships TeamMember[] teamMemberships TeamMember[]
assignedTasks Task[] @relation("TaskAssignee") assignedTasks Task[] @relation("TaskAssignee")
createdTasks Task[] @relation("TaskCreator") createdTasks Task[] @relation("TaskCreator")
createdEvents Event[] @relation("EventCreator") createdEvents Event[] @relation("EventCreator")
createdProjects Project[] @relation("ProjectCreator") createdProjects Project[] @relation("ProjectCreator")
activityLogs ActivityLog[] activityLogs ActivityLog[]
sessions Session[] sessions Session[]
accounts Account[] accounts Account[]
ideas Idea[] @relation("IdeaCreator") ideas Idea[] @relation("IdeaCreator")
relationships Relationship[] @relation("RelationshipCreator") relationships Relationship[] @relation("RelationshipCreator")
agentSessions AgentSession[] agentSessions AgentSession[]
agentTasks AgentTask[] @relation("AgentTaskCreator") agentTasks AgentTask[] @relation("AgentTaskCreator")
userLayouts UserLayout[] userLayouts UserLayout[]
userPreference UserPreference? userPreference UserPreference?
knowledgeEntryVersions KnowledgeEntryVersion[] @relation("EntryVersionAuthor") knowledgeEntryVersions KnowledgeEntryVersion[] @relation("EntryVersionAuthor")
llmProviders LlmProviderInstance[] @relation("UserLlmProviders") llmProviders LlmProviderInstance[] @relation("UserLlmProviders")
@@map("users") @@map("users")
} }
@@ -195,7 +226,7 @@ model Workspace {
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
// Relations // Relations
owner User @relation("WorkspaceOwner", fields: [ownerId], references: [id], onDelete: Cascade) owner User @relation("WorkspaceOwner", fields: [ownerId], references: [id], onDelete: Cascade)
members WorkspaceMember[] members WorkspaceMember[]
teams Team[] teams Team[]
tasks Task[] tasks Task[]
@@ -216,6 +247,7 @@ model Workspace {
personalities Personality[] personalities Personality[]
llmSettings WorkspaceLlmSettings? llmSettings WorkspaceLlmSettings?
qualityGates QualityGate[] qualityGates QualityGate[]
runnerJobs RunnerJob[]
@@index([ownerId]) @@index([ownerId])
@@map("workspaces") @@map("workspaces")
@@ -565,8 +597,8 @@ model Agent {
} }
model AgentTask { model AgentTask {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
workspaceId String @map("workspace_id") @db.Uuid workspaceId String @map("workspace_id") @db.Uuid
// Task details // Task details
title String title String
@@ -575,23 +607,24 @@ model AgentTask {
priority AgentTaskPriority @default(MEDIUM) priority AgentTaskPriority @default(MEDIUM)
// Agent configuration // Agent configuration
agentType String @map("agent_type") agentType String @map("agent_type")
agentConfig Json @default("{}") @map("agent_config") agentConfig Json @default("{}") @map("agent_config")
// Results // Results
result Json? result Json?
error String? @db.Text error String? @db.Text
// Timing // Timing
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
startedAt DateTime? @map("started_at") @db.Timestamptz startedAt DateTime? @map("started_at") @db.Timestamptz
completedAt DateTime? @map("completed_at") @db.Timestamptz completedAt DateTime? @map("completed_at") @db.Timestamptz
// Relations // Relations
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
createdBy User @relation("AgentTaskCreator", fields: [createdById], references: [id], onDelete: Cascade) createdBy User @relation("AgentTaskCreator", fields: [createdById], references: [id], onDelete: Cascade)
createdById String @map("created_by_id") @db.Uuid createdById String @map("created_by_id") @db.Uuid
runnerJobs RunnerJob[]
@@unique([id, workspaceId]) @@unique([id, workspaceId])
@@index([workspaceId]) @@index([workspaceId])
@@ -890,18 +923,18 @@ model KnowledgeEmbedding {
// ============================================ // ============================================
model CronSchedule { model CronSchedule {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
workspaceId String @map("workspace_id") @db.Uuid workspaceId String @map("workspace_id") @db.Uuid
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
// Cron configuration // Cron configuration
expression String // Standard cron: "0 9 * * *" = 9am daily expression String // Standard cron: "0 9 * * *" = 9am daily
command String // MoltBot command to trigger command String // MoltBot command to trigger
// State // State
enabled Boolean @default(true) enabled Boolean @default(true)
lastRun DateTime? @map("last_run") @db.Timestamptz lastRun DateTime? @map("last_run") @db.Timestamptz
nextRun DateTime? @map("next_run") @db.Timestamptz nextRun DateTime? @map("next_run") @db.Timestamptz
// Audit // Audit
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
@@ -918,22 +951,22 @@ model CronSchedule {
// ============================================ // ============================================
model Personality { model Personality {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
workspaceId String @map("workspace_id") @db.Uuid workspaceId String @map("workspace_id") @db.Uuid
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
// Identity // Identity
name String // unique identifier slug name String // unique identifier slug
displayName String @map("display_name") displayName String @map("display_name")
description String? @db.Text description String? @db.Text
// System prompt // System prompt
systemPrompt String @map("system_prompt") @db.Text systemPrompt String @map("system_prompt") @db.Text
// LLM configuration // LLM configuration
temperature Float? // null = use provider default temperature Float? // null = use provider default
maxTokens Int? @map("max_tokens") // null = use provider default maxTokens Int? @map("max_tokens") // null = use provider default
llmProviderInstanceId String? @map("llm_provider_instance_id") @db.Uuid llmProviderInstanceId String? @map("llm_provider_instance_id") @db.Uuid
// Status // Status
isDefault Boolean @default(false) @map("is_default") isDefault Boolean @default(false) @map("is_default")
@@ -961,20 +994,20 @@ model Personality {
// ============================================ // ============================================
model LlmProviderInstance { model LlmProviderInstance {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
providerType String @map("provider_type") // "ollama" | "claude" | "openai" providerType String @map("provider_type") // "ollama" | "claude" | "openai"
displayName String @map("display_name") displayName String @map("display_name")
userId String? @map("user_id") @db.Uuid // NULL = system-level, UUID = user-level userId String? @map("user_id") @db.Uuid // NULL = system-level, UUID = user-level
config Json // Provider-specific configuration config Json // Provider-specific configuration
isDefault Boolean @default(false) @map("is_default") isDefault Boolean @default(false) @map("is_default")
isEnabled Boolean @default(true) @map("is_enabled") isEnabled Boolean @default(true) @map("is_enabled")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
// Relations // Relations
user User? @relation("UserLlmProviders", fields: [userId], references: [id], onDelete: Cascade) user User? @relation("UserLlmProviders", fields: [userId], references: [id], onDelete: Cascade)
personalities Personality[] @relation("PersonalityLlmProvider") personalities Personality[] @relation("PersonalityLlmProvider")
workspaceLlmSettings WorkspaceLlmSettings[] @relation("WorkspaceLlmProvider") workspaceLlmSettings WorkspaceLlmSettings[] @relation("WorkspaceLlmProvider")
@@index([userId]) @@index([userId])
@@index([providerType]) @@index([providerType])
@@ -1010,20 +1043,20 @@ model WorkspaceLlmSettings {
// ============================================ // ============================================
model QualityGate { model QualityGate {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
workspaceId String @map("workspace_id") @db.Uuid workspaceId String @map("workspace_id") @db.Uuid
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
name String name String
description String? description String?
type String // 'build' | 'lint' | 'test' | 'coverage' | 'custom' type String // 'build' | 'lint' | 'test' | 'coverage' | 'custom'
command String? command String?
expectedOutput String? @map("expected_output") expectedOutput String? @map("expected_output")
isRegex Boolean @default(false) @map("is_regex") isRegex Boolean @default(false) @map("is_regex")
required Boolean @default(true) required Boolean @default(true)
order Int @default(0) order Int @default(0)
isEnabled Boolean @default(true) @map("is_enabled") isEnabled Boolean @default(true) @map("is_enabled")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
@@unique([workspaceId, name]) @@unique([workspaceId, name])
@@index([workspaceId]) @@index([workspaceId])
@@ -1032,19 +1065,19 @@ model QualityGate {
} }
model TaskRejection { model TaskRejection {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
taskId String @map("task_id") taskId String @map("task_id")
workspaceId String @map("workspace_id") workspaceId String @map("workspace_id")
agentId String @map("agent_id") agentId String @map("agent_id")
attemptCount Int @map("attempt_count") attemptCount Int @map("attempt_count")
failures Json // FailureSummary[] failures Json // FailureSummary[]
originalTask String @map("original_task") originalTask String @map("original_task")
startedAt DateTime @map("started_at") @db.Timestamptz startedAt DateTime @map("started_at") @db.Timestamptz
rejectedAt DateTime @map("rejected_at") @db.Timestamptz rejectedAt DateTime @map("rejected_at") @db.Timestamptz
escalated Boolean @default(false) escalated Boolean @default(false)
manualReview Boolean @default(false) @map("manual_review") manualReview Boolean @default(false) @map("manual_review")
resolvedAt DateTime? @map("resolved_at") @db.Timestamptz resolvedAt DateTime? @map("resolved_at") @db.Timestamptz
resolution String? resolution String?
@@index([taskId]) @@index([taskId])
@@index([workspaceId]) @@index([workspaceId])
@@ -1055,22 +1088,22 @@ model TaskRejection {
} }
model TokenBudget { model TokenBudget {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
taskId String @unique @map("task_id") @db.Uuid taskId String @unique @map("task_id") @db.Uuid
workspaceId String @map("workspace_id") @db.Uuid workspaceId String @map("workspace_id") @db.Uuid
agentId String @map("agent_id") agentId String @map("agent_id")
// Budget allocation // Budget allocation
allocatedTokens Int @map("allocated_tokens") allocatedTokens Int @map("allocated_tokens")
estimatedComplexity String @map("estimated_complexity") // "low", "medium", "high", "critical" estimatedComplexity String @map("estimated_complexity") // "low", "medium", "high", "critical"
// Usage tracking // Usage tracking
inputTokensUsed Int @default(0) @map("input_tokens_used") inputTokensUsed Int @default(0) @map("input_tokens_used")
outputTokensUsed Int @default(0) @map("output_tokens_used") outputTokensUsed Int @default(0) @map("output_tokens_used")
totalTokensUsed Int @default(0) @map("total_tokens_used") totalTokensUsed Int @default(0) @map("total_tokens_used")
// Cost tracking // Cost tracking
estimatedCost Decimal? @map("estimated_cost") @db.Decimal(10, 6) estimatedCost Decimal? @map("estimated_cost") @db.Decimal(10, 6)
// State // State
startedAt DateTime @default(now()) @map("started_at") @db.Timestamptz startedAt DateTime @default(now()) @map("started_at") @db.Timestamptz
@@ -1078,12 +1111,103 @@ model TokenBudget {
completedAt DateTime? @map("completed_at") @db.Timestamptz completedAt DateTime? @map("completed_at") @db.Timestamptz
// Analysis // Analysis
budgetUtilization Float? @map("budget_utilization") // 0.0 - 1.0 budgetUtilization Float? @map("budget_utilization") // 0.0 - 1.0
suspiciousPattern Boolean @default(false) @map("suspicious_pattern") suspiciousPattern Boolean @default(false) @map("suspicious_pattern")
suspiciousReason String? @map("suspicious_reason") suspiciousReason String? @map("suspicious_reason")
@@index([taskId]) @@index([taskId])
@@index([workspaceId]) @@index([workspaceId])
@@index([suspiciousPattern]) @@index([suspiciousPattern])
@@map("token_budgets") @@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")
}

View File

@@ -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