diff --git a/backend/app/alembic/versions/0004_add_syndarix_models.py b/backend/app/alembic/versions/0004_add_syndarix_models.py index 30c3c82..f7d8e3d 100644 --- a/backend/app/alembic/versions/0004_add_syndarix_models.py +++ b/backend/app/alembic/versions/0004_add_syndarix_models.py @@ -2,14 +2,14 @@ Revision ID: 0004 Revises: 0003 -Create Date: 2025-12-30 +Create Date: 2025-12-31 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 +- issues: Work items (epics, stories, tasks, bugs) """ from collections.abc import Sequence @@ -28,7 +28,9 @@ depends_on: str | Sequence[str] | None = None def upgrade() -> None: """Create Syndarix domain tables.""" - # Create ENUM types first + # ========================================================================= + # Create ENUM types + # ========================================================================= op.execute( """ CREATE TYPE autonomy_level AS ENUM ( @@ -64,24 +66,24 @@ def upgrade() -> None: ) """ ) + op.execute( + """ + CREATE TYPE issue_type AS ENUM ( + 'epic', 'story', 'task', 'bug' + ) + """ + ) op.execute( """ CREATE TYPE issue_status AS ENUM ( - 'open', 'in_progress', 'in_review', 'closed', 'blocked' + 'open', 'in_progress', 'in_review', 'blocked', 'closed' ) """ ) 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' + 'low', 'medium', 'high', 'critical' ) """ ) @@ -95,12 +97,14 @@ def upgrade() -> None: op.execute( """ CREATE TYPE sprint_status AS ENUM ( - 'planned', 'active', 'completed', 'cancelled' + 'planned', 'active', 'in_review', 'completed', 'cancelled' ) """ ) + # ========================================================================= # Create projects table + # ========================================================================= op.create_table( "projects", sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), @@ -152,7 +156,10 @@ def upgrade() -> None: server_default="auto", ), sa.Column( - "settings", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="{}" + "settings", + postgresql.JSONB(astext_type=sa.Text()), + nullable=False, + server_default="{}", ), sa.Column("owner_id", postgresql.UUID(as_uuid=True), nullable=True), sa.Column( @@ -168,11 +175,10 @@ def upgrade() -> None: server_default=sa.text("now()"), ), sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint( - ["owner_id"], ["users.id"], ondelete="SET NULL" - ), + sa.ForeignKeyConstraint(["owner_id"], ["users.id"], ondelete="SET NULL"), sa.UniqueConstraint("slug"), ) + # Single column indexes op.create_index("ix_projects_name", "projects", ["name"]) op.create_index("ix_projects_slug", "projects", ["slug"]) op.create_index("ix_projects_status", "projects", ["status"]) @@ -180,6 +186,7 @@ def upgrade() -> None: 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"]) + # Composite indexes op.create_index("ix_projects_slug_status", "projects", ["slug", "status"]) op.create_index("ix_projects_owner_status", "projects", ["owner_id", "status"]) op.create_index( @@ -189,13 +196,25 @@ def upgrade() -> None: "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("name", sa.String(255), nullable=False), + sa.Column("slug", sa.String(255), nullable=False), sa.Column("description", sa.Text(), nullable=True), + # Areas of expertise (e.g., ["python", "fastapi", "databases"]) + sa.Column( + "expertise", + postgresql.JSONB(astext_type=sa.Text()), + nullable=False, + server_default="[]", + ), + # System prompt defining personality and behavior (required) + sa.Column("personality_prompt", sa.Text(), nullable=False), + # LLM model configuration sa.Column("primary_model", sa.String(100), nullable=False), sa.Column( "fallback_models", @@ -203,16 +222,23 @@ def upgrade() -> None: nullable=False, server_default="[]", ), - sa.Column("system_prompt", sa.Text(), nullable=True), - sa.Column("personality_prompt", sa.Text(), nullable=True), + # Model parameters (temperature, max_tokens, etc.) sa.Column( - "capabilities", + "model_params", + postgresql.JSONB(astext_type=sa.Text()), + nullable=False, + server_default="{}", + ), + # MCP servers this agent can connect to + sa.Column( + "mcp_servers", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="[]", ), + # Tool permissions configuration sa.Column( - "default_config", + "tool_permissions", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="{}", @@ -233,12 +259,17 @@ def upgrade() -> None: sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("slug"), ) + # Single column indexes 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"]) + # Composite indexes + op.create_index("ix_agent_types_slug_active", "agent_types", ["slug", "is_active"]) + op.create_index("ix_agent_types_name_active", "agent_types", ["name", "is_active"]) + # ========================================================================= # Create agent_instances table + # ========================================================================= op.create_table( "agent_instances", sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), @@ -260,17 +291,28 @@ def upgrade() -> None: server_default="idle", ), sa.Column("current_task", sa.Text(), nullable=True), + # Short-term memory (conversation context, recent decisions) sa.Column( - "config_overrides", + "short_term_memory", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="{}", ), + # Reference to long-term memory in vector store + sa.Column("long_term_memory_ref", sa.String(500), nullable=True), + # Session ID for active MCP connections + sa.Column("session_id", sa.String(255), nullable=True), + # Activity tracking + sa.Column("last_activity_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("terminated_at", sa.DateTime(timezone=True), nullable=True), + # Usage metrics + sa.Column("tasks_completed", sa.Integer(), nullable=False, server_default="0"), + sa.Column("tokens_used", sa.BigInteger(), nullable=False, server_default="0"), sa.Column( - "metadata", - postgresql.JSONB(astext_type=sa.Text()), + "cost_incurred", + sa.Numeric(precision=10, scale=4), nullable=False, - server_default="{}", + server_default="0", ), sa.Column( "created_at", @@ -290,12 +332,21 @@ def upgrade() -> None: ), sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"), ) + # Single column indexes 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_session_id", "agent_instances", ["session_id"]) + op.create_index( + "ix_agent_instances_last_activity_at", "agent_instances", ["last_activity_at"] + ) + op.create_index( + "ix_agent_instances_terminated_at", "agent_instances", ["terminated_at"] + ) + # Composite indexes op.create_index( "ix_agent_instances_project_status", "agent_instances", @@ -306,22 +357,30 @@ def upgrade() -> None: "agent_instances", ["agent_type_id", "status"], ) + op.create_index( + "ix_agent_instances_project_type", + "agent_instances", + ["project_id", "agent_type_id"], + ) + # ========================================================================= # 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("name", sa.String(255), 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("start_date", sa.Date(), nullable=False), + sa.Column("end_date", sa.Date(), nullable=False), sa.Column( "status", sa.Enum( "planned", "active", + "in_review", "completed", "cancelled", name="sprint_status", @@ -348,29 +407,53 @@ def upgrade() -> None: 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"]) + # Single column indexes op.create_index("ix_sprints_project_id", "sprints", ["project_id"]) + op.create_index("ix_sprints_status", "sprints", ["status"]) + op.create_index("ix_sprints_start_date", "sprints", ["start_date"]) + op.create_index("ix_sprints_end_date", "sprints", ["end_date"]) + # Composite indexes op.create_index("ix_sprints_project_status", "sprints", ["project_id", "status"]) + op.create_index("ix_sprints_project_number", "sprints", ["project_id", "number"]) + op.create_index("ix_sprints_date_range", "sprints", ["start_date", "end_date"]) + # ========================================================================= # 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), + # Parent issue for hierarchy (Epic -> Story -> Task) + sa.Column("parent_id", postgresql.UUID(as_uuid=True), nullable=True), + # Issue type (epic, story, task, bug) + sa.Column( + "type", + sa.Enum( + "epic", + "story", + "task", + "bug", + name="issue_type", + create_type=False, + ), + nullable=False, + server_default="task", + ), + # Reporter (who created this issue) + sa.Column("reporter_id", postgresql.UUID(as_uuid=True), nullable=True), + # Issue content sa.Column("title", sa.String(500), nullable=False), - sa.Column("description", sa.Text(), nullable=True), + sa.Column("body", sa.Text(), nullable=False, server_default=""), + # Status and priority sa.Column( "status", sa.Enum( "open", "in_progress", "in_review", - "closed", "blocked", + "closed", name="issue_status", create_type=False, ), @@ -380,33 +463,37 @@ def upgrade() -> None: sa.Column( "priority", sa.Enum( - "critical", "high", "medium", "low", name="issue_priority", create_type=False + "low", + "medium", + "high", + "critical", + name="issue_priority", + create_type=False, ), nullable=False, server_default="medium", ), - sa.Column("story_points", sa.Integer(), nullable=True), + # Labels for categorization 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), + # Assignment - agent or human (mutually exclusive) + sa.Column("assigned_agent_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("human_assignee", sa.String(255), nullable=True), + # Sprint association + sa.Column("sprint_id", postgresql.UUID(as_uuid=True), nullable=True), + # Estimation + sa.Column("story_points", sa.Integer(), nullable=True), + sa.Column("due_date", sa.Date(), nullable=True), + # External tracker integration (String for flexibility) + sa.Column("external_tracker_type", sa.String(50), nullable=True), + sa.Column("external_issue_id", sa.String(255), nullable=True), + sa.Column("remote_url", sa.String(1000), nullable=True), + sa.Column("external_issue_number", sa.Integer(), nullable=True), + # Sync status sa.Column( "sync_status", sa.Enum( @@ -417,15 +504,13 @@ def upgrade() -> None: name="sync_status", create_type=False, ), - nullable=True, + nullable=False, + server_default="synced", ), 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("external_updated_at", sa.DateTime(timezone=True), nullable=True), + # Lifecycle + sa.Column("closed_at", sa.DateTime(timezone=True), nullable=True), sa.Column( "created_at", sa.DateTime(timezone=True), @@ -440,29 +525,43 @@ def upgrade() -> None: ), sa.PrimaryKeyConstraint("id"), sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["parent_id"], ["issues.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"]) + # Single column indexes + op.create_index("ix_issues_project_id", "issues", ["project_id"]) + op.create_index("ix_issues_parent_id", "issues", ["parent_id"]) + op.create_index("ix_issues_type", "issues", ["type"]) + op.create_index("ix_issues_reporter_id", "issues", ["reporter_id"]) 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_human_assignee", "issues", ["human_assignee"]) + op.create_index("ix_issues_sprint_id", "issues", ["sprint_id"]) + op.create_index("ix_issues_due_date", "issues", ["due_date"]) + op.create_index("ix_issues_external_tracker_type", "issues", ["external_tracker_type"]) op.create_index("ix_issues_sync_status", "issues", ["sync_status"]) + op.create_index("ix_issues_closed_at", "issues", ["closed_at"]) + # Composite indexes op.create_index("ix_issues_project_status", "issues", ["project_id", "status"]) + op.create_index("ix_issues_project_priority", "issues", ["project_id", "priority"]) + op.create_index("ix_issues_project_sprint", "issues", ["project_id", "sprint_id"]) + op.create_index("ix_issues_project_type", "issues", ["project_id", "type"]) + op.create_index("ix_issues_project_agent", "issues", ["project_id", "assigned_agent_id"]) op.create_index( "ix_issues_project_status_priority", "issues", ["project_id", "status", "priority"], ) op.create_index( - "ix_issues_external", + "ix_issues_external_tracker_id", "issues", - ["project_id", "external_tracker", "external_id"], + ["external_tracker_type", "external_issue_id"], ) @@ -478,9 +577,9 @@ def downgrade() -> None: # 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 issue_type") op.execute("DROP TYPE IF EXISTS agent_status") op.execute("DROP TYPE IF EXISTS client_mode") op.execute("DROP TYPE IF EXISTS project_complexity")