""" Kin — SQLite database schema and connection management. All tables from DESIGN.md section 3.5 State Management. """ import sqlite3 from pathlib import Path DB_PATH = Path(__file__).parent.parent / "kin.db" SCHEMA = """ -- Проекты (центральный реестр) CREATE TABLE IF NOT EXISTS projects ( id TEXT PRIMARY KEY, name TEXT NOT NULL, path TEXT CHECK (path IS NOT NULL OR project_type = 'operations'), tech_stack JSON, status TEXT DEFAULT 'active', priority INTEGER DEFAULT 5, pm_prompt TEXT, claude_md_path TEXT, forgejo_repo TEXT, language TEXT DEFAULT 'ru', execution_mode TEXT NOT NULL DEFAULT 'review', deploy_command TEXT, project_type TEXT DEFAULT 'development', ssh_host TEXT, ssh_user TEXT, ssh_key_path TEXT, ssh_proxy_jump TEXT, description TEXT, autocommit_enabled INTEGER DEFAULT 0, obsidian_vault_path TEXT, worktrees_enabled INTEGER DEFAULT 0, auto_test_enabled INTEGER DEFAULT 0, test_command TEXT DEFAULT 'make test', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Задачи (привязаны к проекту) CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id), title TEXT NOT NULL, status TEXT DEFAULT 'pending', priority INTEGER DEFAULT 5, assigned_role TEXT, parent_task_id TEXT REFERENCES tasks(id), brief JSON, spec JSON, review JSON, test_result JSON, security_result JSON, forgejo_issue_id INTEGER, execution_mode TEXT, blocked_reason TEXT, blocked_at DATETIME, blocked_agent_role TEXT, blocked_pipeline_step TEXT, dangerously_skipped BOOLEAN DEFAULT 0, revise_comment TEXT, revise_count INTEGER DEFAULT 0, revise_target_role TEXT DEFAULT NULL, labels JSON, category TEXT DEFAULT NULL, telegram_sent BOOLEAN DEFAULT 0, acceptance_criteria TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Решения и грабли (внешняя память PM-агента) CREATE TABLE IF NOT EXISTS decisions ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL REFERENCES projects(id), task_id TEXT REFERENCES tasks(id), type TEXT NOT NULL, category TEXT, title TEXT NOT NULL, description TEXT NOT NULL, tags JSON, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Логи агентов (дебаг, обучение, cost tracking) CREATE TABLE IF NOT EXISTS agent_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL REFERENCES projects(id), task_id TEXT REFERENCES tasks(id), agent_role TEXT NOT NULL, session_id TEXT, action TEXT NOT NULL, input_summary TEXT, output_summary TEXT, tokens_used INTEGER, model TEXT, cost_usd REAL, success BOOLEAN, error_message TEXT, duration_seconds INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Модули проекта (карта для PM) CREATE TABLE IF NOT EXISTS modules ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL REFERENCES projects(id), name TEXT NOT NULL, type TEXT NOT NULL, path TEXT NOT NULL, description TEXT, owner_role TEXT, dependencies JSON, UNIQUE(project_id, name) ); -- Фазы исследования нового проекта (research workflow KIN-059) CREATE TABLE IF NOT EXISTS project_phases ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL REFERENCES projects(id), role TEXT NOT NULL, phase_order INTEGER NOT NULL, status TEXT DEFAULT 'pending', task_id TEXT REFERENCES tasks(id), revise_count INTEGER DEFAULT 0, revise_comment TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_phases_project ON project_phases(project_id, phase_order); -- Pipelines (история запусков) CREATE TABLE IF NOT EXISTS pipelines ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL REFERENCES tasks(id), project_id TEXT NOT NULL REFERENCES projects(id), route_type TEXT NOT NULL, steps JSON NOT NULL, status TEXT DEFAULT 'running', total_cost_usd REAL, total_tokens INTEGER, total_duration_seconds INTEGER, parent_pipeline_id INTEGER REFERENCES pipelines(id), department TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, completed_at DATETIME ); -- Межотдельные handoff-ы (KIN-098) CREATE TABLE IF NOT EXISTS department_handoffs ( id INTEGER PRIMARY KEY AUTOINCREMENT, pipeline_id INTEGER NOT NULL REFERENCES pipelines(id), task_id TEXT NOT NULL REFERENCES tasks(id), from_department TEXT NOT NULL, to_department TEXT, artifacts JSON, decisions_made JSON, blockers JSON, status TEXT DEFAULT 'pending', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_handoffs_pipeline ON department_handoffs(pipeline_id); CREATE INDEX IF NOT EXISTS idx_handoffs_task ON department_handoffs(task_id); -- Post-pipeline хуки CREATE TABLE IF NOT EXISTS hooks ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL REFERENCES projects(id), name TEXT NOT NULL, event TEXT NOT NULL, trigger_module_path TEXT, trigger_module_type TEXT, command TEXT NOT NULL, working_dir TEXT, timeout_seconds INTEGER DEFAULT 120, enabled INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now')) ); -- Лог выполнений хуков CREATE TABLE IF NOT EXISTS hook_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, hook_id INTEGER NOT NULL REFERENCES hooks(id), project_id TEXT NOT NULL REFERENCES projects(id), task_id TEXT, success INTEGER NOT NULL, exit_code INTEGER, output TEXT, error TEXT, duration_seconds REAL, created_at TEXT DEFAULT (datetime('now')) ); -- Аудит-лог опасных операций (dangerously-skip-permissions) CREATE TABLE IF NOT EXISTS audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, task_id TEXT REFERENCES tasks(id), step_id TEXT, event_type TEXT NOT NULL DEFAULT 'dangerous_skip', reason TEXT, project_id TEXT REFERENCES projects(id) ); CREATE INDEX IF NOT EXISTS idx_audit_log_task ON audit_log(task_id); CREATE INDEX IF NOT EXISTS idx_audit_log_event ON audit_log(event_type, timestamp); -- Кросс-проектные зависимости CREATE TABLE IF NOT EXISTS project_links ( id INTEGER PRIMARY KEY AUTOINCREMENT, from_project TEXT NOT NULL REFERENCES projects(id), to_project TEXT NOT NULL REFERENCES projects(id), type TEXT NOT NULL, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Тикеты от пользователей CREATE TABLE IF NOT EXISTS support_tickets ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL REFERENCES projects(id), source TEXT NOT NULL, client_id TEXT, client_message TEXT NOT NULL, classification TEXT, guard_result TEXT, guard_reason TEXT, anamnesis JSON, task_id TEXT REFERENCES tasks(id), response TEXT, response_approved BOOLEAN DEFAULT FALSE, status TEXT DEFAULT 'new', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, resolved_at DATETIME ); -- Настройки бота для каждого проекта CREATE TABLE IF NOT EXISTS support_bot_config ( project_id TEXT PRIMARY KEY REFERENCES projects(id), telegram_bot_token TEXT, welcome_message TEXT, faq JSON, auto_reply BOOLEAN DEFAULT FALSE, require_approval BOOLEAN DEFAULT TRUE, brand_voice TEXT, forbidden_topics JSON, escalation_keywords JSON ); -- Среды развёртывания проекта (prod/dev серверы) CREATE TABLE IF NOT EXISTS project_environments ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL REFERENCES projects(id), name TEXT NOT NULL, host TEXT NOT NULL, port INTEGER DEFAULT 22, username TEXT NOT NULL, auth_type TEXT NOT NULL DEFAULT 'password', auth_value TEXT, is_installed INTEGER NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(project_id, name) ); CREATE INDEX IF NOT EXISTS idx_environments_project ON project_environments(project_id); -- Индексы CREATE INDEX IF NOT EXISTS idx_tasks_project_status ON tasks(project_id, status); CREATE INDEX IF NOT EXISTS idx_decisions_project ON decisions(project_id); CREATE INDEX IF NOT EXISTS idx_decisions_tags ON decisions(tags); CREATE INDEX IF NOT EXISTS idx_agent_logs_project ON agent_logs(project_id, created_at); CREATE INDEX IF NOT EXISTS idx_agent_logs_cost ON agent_logs(project_id, cost_usd); CREATE INDEX IF NOT EXISTS idx_tickets_project ON support_tickets(project_id, status); CREATE INDEX IF NOT EXISTS idx_tickets_client ON support_tickets(client_id); -- Чат-сообщения (KIN-OBS-012) CREATE TABLE IF NOT EXISTS chat_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL REFERENCES projects(id), role TEXT NOT NULL, content TEXT NOT NULL, message_type TEXT DEFAULT 'text', task_id TEXT REFERENCES tasks(id), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_chat_messages_project ON chat_messages(project_id, created_at); -- Вложения задач (KIN-090) CREATE TABLE IF NOT EXISTS task_attachments ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, filename TEXT NOT NULL, path TEXT NOT NULL, mime_type TEXT NOT NULL, size INTEGER NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_task_attachments_task ON task_attachments(task_id); """ def get_connection(db_path: Path = DB_PATH) -> sqlite3.Connection: conn = sqlite3.connect(str(db_path)) conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA foreign_keys=ON") conn.row_factory = sqlite3.Row return conn def _migrate(conn: sqlite3.Connection): """Run migrations for existing databases.""" # Check if language column exists on projects proj_cols = {r[1] for r in conn.execute("PRAGMA table_info(projects)").fetchall()} if "language" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN language TEXT DEFAULT 'ru'") conn.commit() if "execution_mode" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN execution_mode TEXT NOT NULL DEFAULT 'review'") conn.commit() # Check if execution_mode column exists on tasks task_cols = {r[1] for r in conn.execute("PRAGMA table_info(tasks)").fetchall()} if "execution_mode" not in task_cols: conn.execute("ALTER TABLE tasks ADD COLUMN execution_mode TEXT") conn.commit() if "blocked_reason" not in task_cols: conn.execute("ALTER TABLE tasks ADD COLUMN blocked_reason TEXT") conn.commit() if "autocommit_enabled" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN autocommit_enabled INTEGER DEFAULT 0") conn.commit() if "dangerously_skipped" not in task_cols: conn.execute("ALTER TABLE tasks ADD COLUMN dangerously_skipped BOOLEAN DEFAULT 0") conn.commit() if "revise_comment" not in task_cols: conn.execute("ALTER TABLE tasks ADD COLUMN revise_comment TEXT") conn.commit() if "category" not in task_cols: conn.execute("ALTER TABLE tasks ADD COLUMN category TEXT DEFAULT NULL") conn.commit() if "blocked_at" not in task_cols: conn.execute("ALTER TABLE tasks ADD COLUMN blocked_at DATETIME") conn.commit() if "blocked_agent_role" not in task_cols: conn.execute("ALTER TABLE tasks ADD COLUMN blocked_agent_role TEXT") conn.commit() if "blocked_pipeline_step" not in task_cols: conn.execute("ALTER TABLE tasks ADD COLUMN blocked_pipeline_step TEXT") conn.commit() if "telegram_sent" not in task_cols: conn.execute("ALTER TABLE tasks ADD COLUMN telegram_sent BOOLEAN DEFAULT 0") conn.commit() if "acceptance_criteria" not in task_cols: conn.execute("ALTER TABLE tasks ADD COLUMN acceptance_criteria TEXT") conn.commit() if "revise_count" not in task_cols: conn.execute("ALTER TABLE tasks ADD COLUMN revise_count INTEGER DEFAULT 0") conn.commit() if "labels" not in task_cols: conn.execute("ALTER TABLE tasks ADD COLUMN labels JSON DEFAULT NULL") conn.commit() if "revise_target_role" not in task_cols: conn.execute("ALTER TABLE tasks ADD COLUMN revise_target_role TEXT DEFAULT NULL") conn.commit() if "obsidian_vault_path" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN obsidian_vault_path TEXT") conn.commit() if "worktrees_enabled" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN worktrees_enabled INTEGER DEFAULT 0") conn.commit() if "auto_test_enabled" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN auto_test_enabled INTEGER DEFAULT 0") conn.commit() if "deploy_command" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN deploy_command TEXT") conn.commit() if "project_type" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN project_type TEXT DEFAULT 'development'") conn.commit() if "ssh_host" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN ssh_host TEXT") conn.commit() if "ssh_user" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN ssh_user TEXT") conn.commit() if "ssh_key_path" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN ssh_key_path TEXT") conn.commit() if "ssh_proxy_jump" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN ssh_proxy_jump TEXT") conn.commit() if "description" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN description TEXT") conn.commit() # Migrate audit_log + project_phases tables existing_tables = {r[0] for r in conn.execute( "SELECT name FROM sqlite_master WHERE type='table'" ).fetchall()} if "project_environments" not in existing_tables: conn.executescript(""" CREATE TABLE IF NOT EXISTS project_environments ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL REFERENCES projects(id), name TEXT NOT NULL, host TEXT NOT NULL, port INTEGER DEFAULT 22, username TEXT NOT NULL, auth_type TEXT NOT NULL DEFAULT 'password', auth_value TEXT, is_installed INTEGER NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(project_id, name) ); CREATE INDEX IF NOT EXISTS idx_environments_project ON project_environments(project_id); """) conn.commit() # Migrate project_environments: old schema used label/login/credential, # new schema uses name/username/auth_value (KIN-087 column rename). env_cols = {r[1] for r in conn.execute("PRAGMA table_info(project_environments)").fetchall()} if "name" not in env_cols and "label" in env_cols: conn.executescript(""" PRAGMA foreign_keys=OFF; CREATE TABLE project_environments_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL REFERENCES projects(id), name TEXT NOT NULL, host TEXT NOT NULL, port INTEGER DEFAULT 22, username TEXT NOT NULL, auth_type TEXT NOT NULL DEFAULT 'password', auth_value TEXT, is_installed INTEGER NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(project_id, name) ); INSERT INTO project_environments_new SELECT id, project_id, label, host, port, login, auth_type, credential, is_installed, created_at, updated_at FROM project_environments; DROP TABLE project_environments; ALTER TABLE project_environments_new RENAME TO project_environments; CREATE INDEX IF NOT EXISTS idx_environments_project ON project_environments(project_id); PRAGMA foreign_keys=ON; """) conn.commit() if "project_phases" not in existing_tables: conn.executescript(""" CREATE TABLE IF NOT EXISTS project_phases ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL REFERENCES projects(id), role TEXT NOT NULL, phase_order INTEGER NOT NULL, status TEXT DEFAULT 'pending', task_id TEXT REFERENCES tasks(id), revise_count INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_phases_project ON project_phases(project_id, phase_order); """) conn.commit() # Migrate project_phases columns (table may already exist without revise_comment) phase_cols = {r[1] for r in conn.execute("PRAGMA table_info(project_phases)").fetchall()} if "revise_comment" not in phase_cols: conn.execute("ALTER TABLE project_phases ADD COLUMN revise_comment TEXT") conn.commit() if "audit_log" not in existing_tables: conn.executescript(""" CREATE TABLE IF NOT EXISTS audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, task_id TEXT REFERENCES tasks(id), step_id TEXT, event_type TEXT NOT NULL DEFAULT 'dangerous_skip', reason TEXT, project_id TEXT REFERENCES projects(id) ); CREATE INDEX IF NOT EXISTS idx_audit_log_task ON audit_log(task_id); CREATE INDEX IF NOT EXISTS idx_audit_log_event ON audit_log(event_type, timestamp); """) conn.commit() # Migrate columns that must exist before table recreation (KIN-UI-002) # These columns are referenced in the INSERT SELECT below but were not added # by any prior ALTER TABLE in this chain — causing OperationalError on minimal schemas. if "tech_stack" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN tech_stack JSON DEFAULT NULL") conn.commit() if "priority" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN priority INTEGER DEFAULT 5") conn.commit() if "pm_prompt" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN pm_prompt TEXT DEFAULT NULL") conn.commit() if "claude_md_path" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN claude_md_path TEXT DEFAULT NULL") conn.commit() if "forgejo_repo" not in proj_cols: conn.execute("ALTER TABLE projects ADD COLUMN forgejo_repo TEXT DEFAULT NULL") conn.commit() if "created_at" not in proj_cols: # SQLite ALTER TABLE does not allow non-constant defaults like CURRENT_TIMESTAMP conn.execute("ALTER TABLE projects ADD COLUMN created_at DATETIME DEFAULT NULL") conn.commit() # Migrate projects.path from NOT NULL to nullable (KIN-ARCH-003) # SQLite doesn't support ALTER COLUMN, so we recreate the table. path_col_rows = conn.execute("PRAGMA table_info(projects)").fetchall() path_col = next((r for r in path_col_rows if r[1] == "path"), None) if path_col and path_col[3] == 1: # notnull == 1, migration needed conn.executescript(""" PRAGMA foreign_keys=OFF; CREATE TABLE projects_new ( id TEXT PRIMARY KEY, name TEXT NOT NULL, path TEXT CHECK (path IS NOT NULL OR project_type = 'operations'), tech_stack JSON, status TEXT DEFAULT 'active', priority INTEGER DEFAULT 5, pm_prompt TEXT, claude_md_path TEXT, forgejo_repo TEXT, language TEXT DEFAULT 'ru', execution_mode TEXT NOT NULL DEFAULT 'review', deploy_command TEXT, project_type TEXT DEFAULT 'development', ssh_host TEXT, ssh_user TEXT, ssh_key_path TEXT, ssh_proxy_jump TEXT, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, autocommit_enabled INTEGER DEFAULT 0, obsidian_vault_path TEXT ); INSERT INTO projects_new SELECT id, name, path, tech_stack, status, priority, pm_prompt, claude_md_path, forgejo_repo, language, execution_mode, deploy_command, project_type, ssh_host, ssh_user, ssh_key_path, ssh_proxy_jump, description, created_at, autocommit_enabled, obsidian_vault_path FROM projects; DROP TABLE projects; ALTER TABLE projects_new RENAME TO projects; PRAGMA foreign_keys=ON; """) if "chat_messages" not in existing_tables: conn.executescript(""" CREATE TABLE IF NOT EXISTS chat_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL REFERENCES projects(id), role TEXT NOT NULL, content TEXT NOT NULL, message_type TEXT DEFAULT 'text', task_id TEXT REFERENCES tasks(id), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_chat_messages_project ON chat_messages(project_id, created_at); """) conn.commit() if "task_attachments" not in existing_tables: conn.executescript(""" CREATE TABLE IF NOT EXISTS task_attachments ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, filename TEXT NOT NULL, path TEXT NOT NULL, mime_type TEXT NOT NULL, size INTEGER NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_task_attachments_task ON task_attachments(task_id); """) conn.commit() # Migrate pipelines: add parent_pipeline_id and department columns (KIN-098) # Guard: table may not exist in legacy schemas without pipelines (old test fixtures) if "pipelines" in existing_tables: pipeline_cols = {r[1] for r in conn.execute("PRAGMA table_info(pipelines)").fetchall()} if "parent_pipeline_id" not in pipeline_cols: conn.execute("ALTER TABLE pipelines ADD COLUMN parent_pipeline_id INTEGER REFERENCES pipelines(id)") conn.commit() if "department" not in pipeline_cols: conn.execute("ALTER TABLE pipelines ADD COLUMN department TEXT") conn.commit() if "pid" not in pipeline_cols: conn.execute("ALTER TABLE pipelines ADD COLUMN pid INTEGER") conn.commit() # Create department_handoffs table (KIN-098) if "department_handoffs" not in existing_tables: conn.executescript(""" CREATE TABLE IF NOT EXISTS department_handoffs ( id INTEGER PRIMARY KEY AUTOINCREMENT, pipeline_id INTEGER NOT NULL REFERENCES pipelines(id), task_id TEXT NOT NULL REFERENCES tasks(id), from_department TEXT NOT NULL, to_department TEXT, artifacts JSON, decisions_made JSON, blockers JSON, status TEXT DEFAULT 'pending', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_handoffs_pipeline ON department_handoffs(pipeline_id); CREATE INDEX IF NOT EXISTS idx_handoffs_task ON department_handoffs(task_id); """) conn.commit() # Add test_command column to projects (KIN-ARCH-008) projects_cols = {row["name"] for row in conn.execute("PRAGMA table_info(projects)")} if "test_command" not in projects_cols: conn.execute("ALTER TABLE projects ADD COLUMN test_command TEXT DEFAULT 'make test'") conn.commit() # Rename legacy 'auto' → 'auto_complete' (KIN-063) conn.execute( "UPDATE projects SET execution_mode = 'auto_complete' WHERE execution_mode = 'auto'" ) conn.execute( "UPDATE tasks SET execution_mode = 'auto_complete' WHERE execution_mode = 'auto'" ) conn.commit() # Fix orphaned dept_sub pipelines left in 'running' state due to double-create bug # (KIN-ARCH-012): before the fix, _execute_department_head_step created a pipeline # that was never updated, leaving ghost 'running' records in prod DB. conn.execute( "UPDATE pipelines SET status = 'failed' WHERE route_type = 'dept_sub' AND status = 'running'" ) conn.commit() def _seed_default_hooks(conn: sqlite3.Connection): """Seed default hooks for the kin project (idempotent). Creates rebuild-frontend hook only when: - project 'kin' exists in the projects table - the hook doesn't already exist (no duplicate) Also updates existing hooks to the correct command/config if outdated. """ kin_row = conn.execute( "SELECT path FROM projects WHERE id = 'kin'" ).fetchone() if not kin_row or not kin_row["path"]: return _PROJECT_PATH = kin_row["path"].rstrip("/") _REBUILD_SCRIPT = f"{_PROJECT_PATH}/scripts/rebuild-frontend.sh" _REBUILD_TRIGGER = "web/frontend/*" _REBUILD_WORKDIR = _PROJECT_PATH exists = conn.execute( "SELECT 1 FROM hooks" " WHERE project_id = 'kin'" " AND name = 'rebuild-frontend'" " AND event = 'pipeline_completed'" ).fetchone() if not exists: conn.execute( """INSERT INTO hooks (project_id, name, event, trigger_module_path, command, working_dir, timeout_seconds, enabled) VALUES ('kin', 'rebuild-frontend', 'pipeline_completed', ?, ?, ?, 300, 1)""", (_REBUILD_TRIGGER, _REBUILD_SCRIPT, _REBUILD_WORKDIR), ) else: # Migrate existing hook: set trigger_module_path, correct command, working_dir conn.execute( """UPDATE hooks SET trigger_module_path = ?, command = ?, working_dir = ?, timeout_seconds = 300 WHERE project_id = 'kin' AND name = 'rebuild-frontend'""", (_REBUILD_TRIGGER, _REBUILD_SCRIPT, _REBUILD_WORKDIR), ) conn.commit() # Enable autocommit for kin project (opt-in, idempotent) conn.execute( "UPDATE projects SET autocommit_enabled=1 WHERE id='kin' AND autocommit_enabled=0" ) conn.commit() def init_db(db_path: Path = DB_PATH) -> sqlite3.Connection: conn = get_connection(db_path) conn.executescript(SCHEMA) conn.commit() _migrate(conn) _seed_default_hooks(conn) return conn if __name__ == "__main__": conn = init_db() tables = conn.execute( "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" ).fetchall() print(f"Initialized {len(tables)} tables:") for t in tables: print(f" - {t['name']}") conn.close()