"""Add Syndarix models Revision ID: 0004 Revises: 0003 Create Date: 2025-12-30 This migration creates the core Syndarix domain tables: - projects: Client engagement projects - agent_types: Agent template configurations - agent_instances: Spawned agent instances assigned to projects - issues: Work items (stories, tasks, bugs) - sprints: Sprint containers for issues """ from collections.abc import Sequence import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. revision: str = "0004" down_revision: str | None = "0003" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def upgrade() -> None: """Create Syndarix domain tables.""" # Create ENUM types first op.execute( """ CREATE TYPE autonomy_level AS ENUM ( 'full_control', 'milestone', 'autonomous' ) """ ) op.execute( """ CREATE TYPE project_status AS ENUM ( 'active', 'paused', 'completed', 'archived' ) """ ) op.execute( """ CREATE TYPE project_complexity AS ENUM ( 'script', 'simple', 'medium', 'complex' ) """ ) op.execute( """ CREATE TYPE client_mode AS ENUM ( 'technical', 'auto' ) """ ) op.execute( """ CREATE TYPE agent_status AS ENUM ( 'idle', 'working', 'waiting', 'paused', 'terminated' ) """ ) op.execute( """ CREATE TYPE issue_status AS ENUM ( 'open', 'in_progress', 'in_review', 'closed', 'blocked' ) """ ) op.execute( """ CREATE TYPE issue_priority AS ENUM ( 'critical', 'high', 'medium', 'low' ) """ ) op.execute( """ CREATE TYPE external_tracker_type AS ENUM ( 'gitea', 'github', 'gitlab', 'jira' ) """ ) op.execute( """ CREATE TYPE sync_status AS ENUM ( 'synced', 'pending', 'conflict', 'error' ) """ ) op.execute( """ CREATE TYPE sprint_status AS ENUM ( 'planned', 'active', 'completed', 'cancelled' ) """ ) # Create projects table op.create_table( "projects", sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("name", sa.String(255), nullable=False), sa.Column("slug", sa.String(255), nullable=False), sa.Column("description", sa.Text(), nullable=True), sa.Column( "autonomy_level", sa.Enum( "full_control", "milestone", "autonomous", name="autonomy_level", create_type=False, ), nullable=False, server_default="milestone", ), sa.Column( "status", sa.Enum( "active", "paused", "completed", "archived", name="project_status", create_type=False, ), nullable=False, server_default="active", ), sa.Column( "complexity", sa.Enum( "script", "simple", "medium", "complex", name="project_complexity", create_type=False, ), nullable=False, server_default="medium", ), sa.Column( "client_mode", sa.Enum("technical", "auto", name="client_mode", create_type=False), nullable=False, server_default="auto", ), sa.Column( "settings", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="{}" ), sa.Column("owner_id", postgresql.UUID(as_uuid=True), nullable=True), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.Column( "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.PrimaryKeyConstraint("id"), sa.ForeignKeyConstraint( ["owner_id"], ["users.id"], ondelete="SET NULL" ), sa.UniqueConstraint("slug"), ) op.create_index("ix_projects_name", "projects", ["name"]) op.create_index("ix_projects_slug", "projects", ["slug"]) op.create_index("ix_projects_status", "projects", ["status"]) op.create_index("ix_projects_autonomy_level", "projects", ["autonomy_level"]) op.create_index("ix_projects_complexity", "projects", ["complexity"]) op.create_index("ix_projects_client_mode", "projects", ["client_mode"]) op.create_index("ix_projects_owner_id", "projects", ["owner_id"]) op.create_index("ix_projects_slug_status", "projects", ["slug", "status"]) op.create_index("ix_projects_owner_status", "projects", ["owner_id", "status"]) op.create_index( "ix_projects_autonomy_status", "projects", ["autonomy_level", "status"] ) op.create_index( "ix_projects_complexity_status", "projects", ["complexity", "status"] ) # Create agent_types table op.create_table( "agent_types", sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("name", sa.String(100), nullable=False), sa.Column("slug", sa.String(100), nullable=False), sa.Column("description", sa.Text(), nullable=True), sa.Column("primary_model", sa.String(100), nullable=False), sa.Column( "fallback_models", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="[]", ), sa.Column("system_prompt", sa.Text(), nullable=True), sa.Column("personality_prompt", sa.Text(), nullable=True), sa.Column( "capabilities", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="[]", ), sa.Column( "default_config", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="{}", ), sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.Column( "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("slug"), ) op.create_index("ix_agent_types_name", "agent_types", ["name"]) op.create_index("ix_agent_types_slug", "agent_types", ["slug"]) op.create_index("ix_agent_types_is_active", "agent_types", ["is_active"]) op.create_index("ix_agent_types_primary_model", "agent_types", ["primary_model"]) # Create agent_instances table op.create_table( "agent_instances", sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("agent_type_id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("name", sa.String(100), nullable=False), sa.Column( "status", sa.Enum( "idle", "working", "waiting", "paused", "terminated", name="agent_status", create_type=False, ), nullable=False, server_default="idle", ), sa.Column("current_task", sa.Text(), nullable=True), sa.Column( "config_overrides", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="{}", ), sa.Column( "metadata", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="{}", ), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.Column( "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.PrimaryKeyConstraint("id"), sa.ForeignKeyConstraint( ["agent_type_id"], ["agent_types.id"], ondelete="RESTRICT" ), sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"), ) op.create_index("ix_agent_instances_name", "agent_instances", ["name"]) op.create_index("ix_agent_instances_status", "agent_instances", ["status"]) op.create_index( "ix_agent_instances_agent_type_id", "agent_instances", ["agent_type_id"] ) op.create_index("ix_agent_instances_project_id", "agent_instances", ["project_id"]) op.create_index( "ix_agent_instances_project_status", "agent_instances", ["project_id", "status"], ) op.create_index( "ix_agent_instances_type_status", "agent_instances", ["agent_type_id", "status"], ) # Create sprints table (before issues for FK reference) op.create_table( "sprints", sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("name", sa.String(100), nullable=False), sa.Column("number", sa.Integer(), nullable=False), sa.Column("goal", sa.Text(), nullable=True), sa.Column("start_date", sa.Date(), nullable=True), sa.Column("end_date", sa.Date(), nullable=True), sa.Column( "status", sa.Enum( "planned", "active", "completed", "cancelled", name="sprint_status", create_type=False, ), nullable=False, server_default="planned", ), sa.Column("planned_points", sa.Integer(), nullable=True), sa.Column("velocity", sa.Integer(), nullable=True), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.Column( "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.PrimaryKeyConstraint("id"), sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"), sa.UniqueConstraint("project_id", "number", name="uq_sprint_project_number"), ) op.create_index("ix_sprints_name", "sprints", ["name"]) op.create_index("ix_sprints_number", "sprints", ["number"]) op.create_index("ix_sprints_status", "sprints", ["status"]) op.create_index("ix_sprints_project_id", "sprints", ["project_id"]) op.create_index("ix_sprints_project_status", "sprints", ["project_id", "status"]) # Create issues table op.create_table( "issues", sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("sprint_id", postgresql.UUID(as_uuid=True), nullable=True), sa.Column("assigned_agent_id", postgresql.UUID(as_uuid=True), nullable=True), sa.Column("title", sa.String(500), nullable=False), sa.Column("description", sa.Text(), nullable=True), sa.Column( "status", sa.Enum( "open", "in_progress", "in_review", "closed", "blocked", name="issue_status", create_type=False, ), nullable=False, server_default="open", ), sa.Column( "priority", sa.Enum( "critical", "high", "medium", "low", name="issue_priority", create_type=False ), nullable=False, server_default="medium", ), sa.Column("story_points", sa.Integer(), nullable=True), sa.Column( "labels", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="[]", ), sa.Column( "external_tracker", sa.Enum( "gitea", "github", "gitlab", "jira", name="external_tracker_type", create_type=False, ), nullable=True, ), sa.Column("external_id", sa.String(255), nullable=True), sa.Column("external_url", sa.String(2048), nullable=True), sa.Column("external_number", sa.Integer(), nullable=True), sa.Column( "sync_status", sa.Enum( "synced", "pending", "conflict", "error", name="sync_status", create_type=False, ), nullable=True, ), sa.Column("last_synced_at", sa.DateTime(timezone=True), nullable=True), sa.Column( "metadata", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="{}", ), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.Column( "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.PrimaryKeyConstraint("id"), sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(["sprint_id"], ["sprints.id"], ondelete="SET NULL"), sa.ForeignKeyConstraint( ["assigned_agent_id"], ["agent_instances.id"], ondelete="SET NULL" ), ) op.create_index("ix_issues_title", "issues", ["title"]) op.create_index("ix_issues_status", "issues", ["status"]) op.create_index("ix_issues_priority", "issues", ["priority"]) op.create_index("ix_issues_project_id", "issues", ["project_id"]) op.create_index("ix_issues_sprint_id", "issues", ["sprint_id"]) op.create_index("ix_issues_assigned_agent_id", "issues", ["assigned_agent_id"]) op.create_index("ix_issues_external_tracker", "issues", ["external_tracker"]) op.create_index("ix_issues_sync_status", "issues", ["sync_status"]) op.create_index("ix_issues_project_status", "issues", ["project_id", "status"]) op.create_index( "ix_issues_project_status_priority", "issues", ["project_id", "status", "priority"], ) op.create_index( "ix_issues_external", "issues", ["project_id", "external_tracker", "external_id"], ) def downgrade() -> None: """Drop Syndarix domain tables.""" # Drop tables in reverse order (respecting FK constraints) op.drop_table("issues") op.drop_table("sprints") op.drop_table("agent_instances") op.drop_table("agent_types") op.drop_table("projects") # Drop ENUM types op.execute("DROP TYPE IF EXISTS sprint_status") op.execute("DROP TYPE IF EXISTS sync_status") op.execute("DROP TYPE IF EXISTS external_tracker_type") op.execute("DROP TYPE IF EXISTS issue_priority") op.execute("DROP TYPE IF EXISTS issue_status") op.execute("DROP TYPE IF EXISTS agent_status") op.execute("DROP TYPE IF EXISTS client_mode") op.execute("DROP TYPE IF EXISTS project_complexity") op.execute("DROP TYPE IF EXISTS project_status") op.execute("DROP TYPE IF EXISTS autonomy_level")