// Extracted from src/db/repository.ts (mechanical split — bodies unchanged).
// schema repository domain. Facade: src/db/repository.ts (Repository class).
import Database from 'better-sqlite3';
import { readFileSync } from 'fs';
import { join, dirname } from 'path';
import { logger } from '../../logger.js';
import { ensureTaskCommentFts } from '../migrate.js';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

  export function initSchema(db: Database.Database): void {
    runPreSchemaCompatibilityMigrations(db);
    const schemaPath = join(__dirname, '..', 'schema.sql');
    const schema = readFileSync(schemaPath, 'utf-8');
    db.exec(schema);
    ensureColumn(db, 'jobs', 'required_profile', "TEXT NOT NULL DEFAULT 'auto'");
    ensureColumn(db, 'jobs', 'task_class', "TEXT NOT NULL DEFAULT 'auto'");
    ensureColumn(db, 'worker_nodes', 'profile_tags', "TEXT NOT NULL DEFAULT ',auto,'");
    ensureColumn(db, 'worker_nodes', 'task_class_tags', "TEXT NOT NULL DEFAULT ',auto,'");
    ensureColumn(db, 'worker_nodes', 'available_models', 'TEXT');
    ensureColumn(db, 'worker_nodes', 'max_concurrency', 'INTEGER NOT NULL DEFAULT 1');
    ensureColumn(db, 'worker_nodes', 'last_error', 'TEXT');
    db.exec("CREATE INDEX IF NOT EXISTS idx_jobs_profile_task_class ON jobs (status, required_profile, task_class)");
    db.exec("CREATE INDEX IF NOT EXISTS idx_worker_nodes_health ON worker_nodes (enabled, healthy, last_seen_at)");
    ensureColumn(db, 'jobs', 'parent_job_id', 'TEXT');
    ensureColumn(db, 'jobs', 'subtask_depth', 'INTEGER NOT NULL DEFAULT 0');
    ensureColumn(db, 'jobs', 'wait_reason', 'TEXT');
    ensureColumn(db, 'jobs', 'continued_from_job_id', 'TEXT');
    // Phase A (multi-team GPU pool): physical backend id assigned when
    // the worker is a proxy. Sticky once set; never overwritten mid-job.
    ensureColumn(db, 'jobs', 'last_backend_id', 'TEXT');
    db.exec("CREATE INDEX IF NOT EXISTS idx_jobs_parent_job_id ON jobs (parent_job_id)");
    ensureColumn(db, 'local_tasks', 'feedback_rating', 'TEXT');
    ensureColumn(db, 'local_tasks', 'feedback_comment', 'TEXT');
    ensureColumn(db, 'local_tasks', 'feedback_tags', 'TEXT');
    ensureColumn(db, 'local_tasks', 'feedback_at', 'TEXT');
    migrateWaitingSubtasksStatus(db);
    // Auth migrations: owner_id columns
    ensureColumn(db, 'jobs', 'owner_id', 'TEXT');
    ensureColumn(db, 'local_tasks', 'owner_id', 'TEXT');
    // #142: 実行中アクティビティ表示
    ensureColumn(db, 'jobs', 'current_activity', 'TEXT');
    // abortReason 細分化: agent-loop / piece-runner が出す構造化 abort code を保持
    ensureColumn(db, 'jobs', 'abort_reason', 'TEXT');
    // #134: 共有機能
    ensureColumn(db, 'local_tasks', 'share_token', 'TEXT');
    ensureColumn(db, 'local_tasks', 'shared_at', 'TEXT');
    db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_local_tasks_share_token ON local_tasks (share_token)");

    // Ownership and visibility columns (3 tables)
    for (const table of ['local_tasks', 'scheduled_tasks', 'jobs']) {
      ensureColumn(db, table, 'owner_id', 'TEXT');
      ensureColumn(db, table, 'visibility', "TEXT NOT NULL DEFAULT 'private'");
      ensureColumn(db, table, 'visibility_scope_org_id', 'TEXT');
    }

    // User preferences
    ensureColumn(db, 'users', 'default_visibility', "TEXT NOT NULL DEFAULT 'private'");
    ensureColumn(db, 'users', 'default_visibility_org_id', 'TEXT');

    // Indexes and user_gitea_orgs table
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_local_tasks_owner_id ON local_tasks(owner_id);
      CREATE INDEX IF NOT EXISTS idx_local_tasks_visibility ON local_tasks(visibility, visibility_scope_org_id);
      CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_owner_id ON scheduled_tasks(owner_id);
      CREATE INDEX IF NOT EXISTS idx_jobs_owner_id ON jobs(owner_id);
      CREATE INDEX IF NOT EXISTS idx_jobs_visibility ON jobs(visibility, visibility_scope_org_id);

      CREATE TABLE IF NOT EXISTS user_gitea_orgs (
        user_id    TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        org_id     TEXT NOT NULL,
        org_name   TEXT NOT NULL,
        fetched_at TEXT NOT NULL DEFAULT (datetime('now')),
        PRIMARY KEY (user_id, org_id)
      );
      CREATE INDEX IF NOT EXISTS idx_user_gitea_orgs_org_id ON user_gitea_orgs(org_id);
    `);

    // スペース基盤（計画1）。schema.sql / migrate.ts と三重ミラー。
    db.exec(`
      CREATE TABLE IF NOT EXISTS spaces (
        id                      TEXT PRIMARY KEY,
        kind                    TEXT NOT NULL DEFAULT 'case',
        title                   TEXT NOT NULL,
        description             TEXT NOT NULL DEFAULT '',
        owner_id                TEXT,
        visibility              TEXT NOT NULL DEFAULT 'private',
        visibility_scope_org_id TEXT,
        status                  TEXT NOT NULL DEFAULT 'open',
        brand_color             TEXT,
        workspace_dir           TEXT,
        created_at              TEXT NOT NULL DEFAULT (datetime('now')),
        updated_at              TEXT NOT NULL DEFAULT (datetime('now'))
      );
      CREATE INDEX IF NOT EXISTS idx_spaces_owner ON spaces(owner_id);
      CREATE INDEX IF NOT EXISTS idx_spaces_visibility ON spaces(visibility, visibility_scope_org_id);
      CREATE UNIQUE INDEX IF NOT EXISTS idx_spaces_personal_owner ON spaces(owner_id) WHERE kind = 'personal';
    `);
    ensureColumn(db, 'local_tasks', 'space_id', 'TEXT');
    ensureColumn(db, 'local_tasks', 'workspace_mode', "TEXT NOT NULL DEFAULT 'persistent'");
    ensureColumn(db, 'jobs', 'space_id', 'TEXT');
    ensureColumn(db, 'scheduled_tasks', 'space_id', 'TEXT');

    // タスク横断検索用の会話コメント索引（通常テーブルのみ）。schema.sql / migrate.ts と三重ミラー。
    // FTS5 仮想テーブル＋同期トリガは src/db/migrate.ts の ensureTaskCommentFts が単一定義箇所として作成する
    // （runMigrations() を経由しない bare `new Repository()` 構築でも FTS が確実に存在するように、ここからも呼ぶ）。
    db.exec(`
      CREATE TABLE IF NOT EXISTS task_comment_index (
        comment_id  INTEGER PRIMARY KEY,
        task_id     INTEGER NOT NULL,
        author      TEXT,
        kind        TEXT,
        created_at  TEXT,
        text        TEXT NOT NULL
      );
      CREATE INDEX IF NOT EXISTS idx_tci_task ON task_comment_index(task_id);
    `);
    ensureTaskCommentFts(db);

    // スペース・メンバー（協働者）。schema.sql / migrate.ts と三重ミラー。
    // owner は members 行を持たず owner_id で判定する。
    db.exec(`
      CREATE TABLE IF NOT EXISTS space_members (
        space_id   TEXT NOT NULL,
        user_id    TEXT NOT NULL,
        role       TEXT NOT NULL DEFAULT 'editor' CHECK (role IN ('owner','editor','viewer')),
        invited_by TEXT,
        created_at TEXT NOT NULL DEFAULT (datetime('now')),
        PRIMARY KEY (space_id, user_id),
        FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE,
        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
      );
      CREATE INDEX IF NOT EXISTS idx_space_members_user ON space_members (user_id);
    `);

    // スペース・カレンダー（予定）。schema.sql / migrate.ts と三重ミラー。
    db.exec(`
      CREATE TABLE IF NOT EXISTS calendar_events (
        id             INTEGER PRIMARY KEY AUTOINCREMENT,
        space_id       TEXT NOT NULL,
        owner_id       TEXT,
        date           TEXT NOT NULL,
        end_date       TEXT,
        time           TEXT,
        end_time       TEXT,
        title          TEXT NOT NULL,
        description    TEXT,
        created_by     TEXT NOT NULL DEFAULT 'user',
        source_task_id INTEGER,
        created_at     TEXT NOT NULL DEFAULT (datetime('now')),
        updated_at     TEXT NOT NULL DEFAULT (datetime('now'))
      );
      CREATE INDEX IF NOT EXISTS idx_calendar_events_space_date ON calendar_events (space_id, date);
    `);

    // スペース招待リンク（再利用トークン）。schema.sql / migrate.ts と三重ミラー。
    db.exec(`
      CREATE TABLE IF NOT EXISTS space_invites (
        token       TEXT PRIMARY KEY,
        space_id    TEXT NOT NULL,
        role        TEXT NOT NULL DEFAULT 'viewer' CHECK (role IN ('editor','viewer')),
        created_by  TEXT,
        created_at  TEXT NOT NULL DEFAULT (datetime('now')),
        expires_at  TEXT,
        revoked_at  TEXT,
        FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE
      );
      CREATE INDEX IF NOT EXISTS idx_space_invites_space ON space_invites (space_id);
    `);
    // 公開アプリ共有リンク（read-only）。schema.sql / migrate.ts と三重ミラー。
    db.exec(`
      CREATE TABLE IF NOT EXISTS app_share_links (
        token       TEXT PRIMARY KEY,
        space_id    TEXT NOT NULL,
        app_name    TEXT NOT NULL,
        created_by  TEXT,
        created_at  TEXT NOT NULL DEFAULT (datetime('now')),
        revoked_at  TEXT,
        FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE
      );
      CREATE INDEX IF NOT EXISTS idx_app_share_links_space_app
        ON app_share_links(space_id, app_name);
    `);
    // ツール要求の記録（RequestTool 能動申告 / 未許可ツール呼び出しの受動捕捉）。
    // schema.sql / migrate.ts と三重ミラー。
    db.exec(`
      CREATE TABLE IF NOT EXISTS tool_requests (
        id            TEXT PRIMARY KEY,
        task_id       TEXT,
        job_id        TEXT,
        space_id      TEXT,
        piece_name    TEXT NOT NULL,
        movement_name TEXT NOT NULL,
        tool_name     TEXT NOT NULL,
        reason        TEXT,
        category      TEXT NOT NULL DEFAULT 'requested',
        status        TEXT NOT NULL DEFAULT 'pending',
        grant_scope   TEXT,
        decided_by    TEXT,
        created_at    TEXT NOT NULL DEFAULT (datetime('now')),
        decided_at    TEXT
      );
      CREATE INDEX IF NOT EXISTS idx_tool_requests_task ON tool_requests (task_id);
      CREATE INDEX IF NOT EXISTS idx_tool_requests_piece ON tool_requests (piece_name);
      CREATE INDEX IF NOT EXISTS idx_tool_requests_dedup ON tool_requests (job_id, movement_name, tool_name, status);
    `);
    // エージェント発 Python パッケージ要求（RequestPackage）。承認で PR2 の
    // installSpacePackages を再利用しスペース overlay へ install する。
    // schema.sql / migrate.ts と三重ミラー。
    db.exec(`
      CREATE TABLE IF NOT EXISTS package_requests (
        id              TEXT PRIMARY KEY,
        task_id         TEXT,
        job_id          TEXT,
        space_id        TEXT,
        piece_name      TEXT,
        movement_name   TEXT,
        spec            TEXT NOT NULL,
        normalized_name TEXT NOT NULL,
        reason          TEXT,
        status          TEXT NOT NULL DEFAULT 'pending',
        decided_by      TEXT,
        created_at      TEXT NOT NULL DEFAULT (datetime('now')),
        decided_at      TEXT
      );
      CREATE INDEX IF NOT EXISTS idx_package_requests_task ON package_requests (task_id);
      CREATE INDEX IF NOT EXISTS idx_package_requests_dedup ON package_requests (job_id, normalized_name, status);
    `);
    // ワークスペースファイル来歴台帳。schema.sql / migrate.ts と三重ミラー。
    db.exec(`
      CREATE TABLE IF NOT EXISTS workspace_file_provenance (
        id                        INTEGER PRIMARY KEY AUTOINCREMENT,
        space_id                  TEXT,
        workspace_path            TEXT NOT NULL,
        rel_path                  TEXT NOT NULL,
        created_by_task_id        INTEGER,
        created_by_job_id         TEXT,
        created_by_piece          TEXT,
        created_by_movement       TEXT,
        source_kind               TEXT NOT NULL,
        first_seen_at             TEXT NOT NULL,
        last_modified_by_task_id  INTEGER,
        last_modified_by_job_id   TEXT,
        last_modified_at          TEXT,
        checksum                  TEXT,
        note                      TEXT,
        UNIQUE(workspace_path, rel_path)
      );
      CREATE INDEX IF NOT EXISTS idx_wsfile_prov_ws ON workspace_file_provenance (workspace_path);
      CREATE INDEX IF NOT EXISTS idx_wsfile_prov_created ON workspace_file_provenance (workspace_path, created_by_task_id);
    `);
    // Per-space isolation for MCP servers / SSH connections (spaces foundation).
    // NULL = global/legacy (resolved globally; Phase 1 is a no-op, no consumer
    // reads this yet). Triple-path mirror: schema.sql + migrate.ts.
    ensureColumn(db, 'mcp_servers', 'space_id', 'TEXT');
    ensureColumn(db, 'ssh_connections', 'space_id', 'TEXT');
    // workstream 2: ブラウザセッションプロファイルの per-space 化。owner_id は復号
    // DEK のため保持。Triple-path mirror: schema.sql + migrate.ts。
    ensureColumn(db, 'browser_session_profiles', 'space_id', 'TEXT');
    // 計画5: 実行ログ root。NULL = 後方互換で workspace_path/logs に解決。
    ensureColumn(db, 'local_tasks', 'runtime_dir', 'TEXT');
    ensureColumn(db, 'jobs', 'runtime_dir', 'TEXT');
    // Tool-request mechanism: per-task grant overlay (JSON string[]).
    ensureColumn(db, 'local_tasks', 'granted_tools', 'TEXT');

    // Browser session persistence (2026-05) — keep in sync with schema.sql
    db.exec(`
      CREATE TABLE IF NOT EXISTS user_deks (
        user_id        TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
        encrypted_dek  BLOB NOT NULL,
        created_at     TEXT NOT NULL DEFAULT (datetime('now'))
      );
      CREATE TABLE IF NOT EXISTS browser_session_profiles (
        id                   INTEGER PRIMARY KEY AUTOINCREMENT,
        owner_id             TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        space_id             TEXT,
        label                TEXT NOT NULL,
        start_url            TEXT NOT NULL,
        match_patterns       TEXT NOT NULL DEFAULT '[]',
        storage_origins      TEXT NOT NULL DEFAULT '[]',
        logged_in_selector   TEXT,
        login_url_patterns   TEXT NOT NULL DEFAULT '[]',
        encrypted_state_blob BLOB,
        state_version        INTEGER NOT NULL DEFAULT 0,
        playwright_version   TEXT,
        status               TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','active','expired','revoked','error')),
        last_saved_at        TEXT,
        last_used_at         TEXT,
        last_validated_at    TEXT,
        last_error           TEXT,
        created_at           TEXT NOT NULL DEFAULT (datetime('now')),
        updated_at           TEXT NOT NULL DEFAULT (datetime('now'))
      );
      CREATE INDEX IF NOT EXISTS idx_bsp_owner ON browser_session_profiles(owner_id);
      CREATE INDEX IF NOT EXISTS idx_bsp_space ON browser_session_profiles(space_id);
      -- audit log: intentionally no FK — must survive deletion of referenced rows
      CREATE TABLE IF NOT EXISTS browser_session_audit (
        id              INTEGER PRIMARY KEY AUTOINCREMENT,
        ts              TEXT NOT NULL DEFAULT (datetime('now')),
        actor_user_id   TEXT,
        profile_id      INTEGER,
        owner_id        TEXT,
        action          TEXT NOT NULL CHECK (action IN ('create','save','decrypt','use','delete','expire','revoke','test','login_start','login_cancel')),
        task_id         INTEGER,
        job_id          TEXT,
        result          TEXT NOT NULL CHECK (result IN ('success','error')),
        reason          TEXT
      );
      CREATE INDEX IF NOT EXISTS idx_bsa_profile ON browser_session_audit(profile_id);
      CREATE INDEX IF NOT EXISTS idx_bsa_actor ON browser_session_audit(actor_user_id);
    `);

    ensureColumn(db, 'local_tasks', 'browser_session_profile_id', 'INTEGER REFERENCES browser_session_profiles(id) ON DELETE SET NULL');
    ensureColumn(db, 'scheduled_tasks', 'browser_session_profile_id', 'INTEGER REFERENCES browser_session_profiles(id) ON DELETE SET NULL');
    ensureColumn(db, 'jobs', 'browser_session_profile_id', 'INTEGER REFERENCES browser_session_profiles(id) ON DELETE SET NULL');

    // E: scheduled_tasks can now run a user script directly (without going
    // through the agent / LLM loop). task_kind='agent' (default) keeps the
    // pre-existing behavior; task_kind='script' uses script_name + script_params.
    ensureColumn(db, 'scheduled_tasks', 'task_kind', "TEXT NOT NULL DEFAULT 'agent' CHECK (task_kind IN ('agent','script'))");
    ensureColumn(db, 'scheduled_tasks', 'script_name', 'TEXT');
    ensureColumn(db, 'scheduled_tasks', 'script_params', 'TEXT'); // JSON-encoded object or NULL

    // F: reflection jobs — task_kind distinguishes agent jobs from reflection
    // jobs that run the self-improving-memory pipeline (no LLM task loop).
    // payload carries JSON inputs (scope, trigger metadata, etc.).
    ensureColumn(db, 'jobs', 'task_kind', "TEXT NOT NULL DEFAULT 'agent'");
    ensureColumn(db, 'jobs', 'payload', 'TEXT');

    // G: reflection piece-edit cooldown tracking.
    // reflection_piece_edits records each time the reflection pipeline writes
    // a user's custom piece. The cooldown gate in piece-writer.ts queries
    // countRecentPieceEdits to rate-limit piece rewrites.
    db.exec(`
      CREATE TABLE IF NOT EXISTS reflection_piece_edits (
        user_id     TEXT NOT NULL,
        piece_name  TEXT NOT NULL,
        snapshot_id TEXT NOT NULL,
        created_at  INTEGER NOT NULL,
        PRIMARY KEY (user_id, piece_name, created_at)
      );
      CREATE INDEX IF NOT EXISTS idx_rpe_user_piece_time
        ON reflection_piece_edits (user_id, piece_name, created_at DESC);
    `);

    // The Side Info Panel dashboard widget feature was removed (2026-06);
    // drop the legacy per-user widget table if an older DB still carries it.
    db.exec(`DROP TABLE IF EXISTS user_dashboard_widgets;`);

    // H: reflection_metrics — one row per reflection job, records outcome,
    // token usage, memory changes, and whether a piece edit was applied.
    // Used by the /api/reflection/metrics endpoint (Phase 7.2) and future
    // per-user budget enforcement (Phase 8.2).
    db.exec(`
      CREATE TABLE IF NOT EXISTS reflection_metrics (
        id                  INTEGER PRIMARY KEY AUTOINCREMENT,
        reflection_job_id   TEXT NOT NULL,
        original_job_id     TEXT,
        user_id             TEXT NOT NULL,
        piece_name          TEXT,
        outcome             TEXT NOT NULL,
        memory_changes      INTEGER NOT NULL DEFAULT 0,
        piece_edited        INTEGER NOT NULL DEFAULT 0,
        tokens_in           INTEGER NOT NULL DEFAULT 0,
        tokens_out          INTEGER NOT NULL DEFAULT 0,
        duration_ms         INTEGER NOT NULL DEFAULT 0,
        created_at          INTEGER NOT NULL
      );
      CREATE INDEX IF NOT EXISTS idx_rm_user_time
        ON reflection_metrics (user_id, created_at DESC);
    `);

    // I: AAO Gateway Phase 2a — DB-backed virtual keys.
    // Mirrors src/db/schema.sql and migrateGatewayVirtualKeys in migrate.ts;
    // all three paths must stay in sync (project_db_migration_dual_path).
    db.exec(`
      CREATE TABLE IF NOT EXISTS gateway_virtual_keys (
        id             TEXT PRIMARY KEY,
        key_hash       TEXT NOT NULL UNIQUE,
        key_prefix     TEXT NOT NULL,
        team           TEXT NOT NULL,
        allowed_models TEXT,
        source         TEXT NOT NULL DEFAULT 'admin' CHECK (source IN ('admin','config-import')),
        created_at     TEXT NOT NULL,
        created_by     TEXT,
        revoked_at     TEXT,
        revoked_by     TEXT,
        last_used_at   TEXT
      );
      CREATE INDEX IF NOT EXISTS idx_gateway_keys_hash_active
        ON gateway_virtual_keys (key_hash)
        WHERE revoked_at IS NULL;
      CREATE INDEX IF NOT EXISTS idx_gateway_keys_team
        ON gateway_virtual_keys (team);
    `);

    // I.b: AAO Gateway Phase 2b — budget / rate limit columns +
    // gateway_key_usage table. Mirrors schema.sql + migrate.ts and uses
    // the same PRAGMA-based idempotency pattern.
    ensureColumn(db, 'gateway_virtual_keys', 'tokens_budget', 'INTEGER');
    ensureColumn(db, 'gateway_virtual_keys', 'rate_limit_rpm', 'INTEGER');
    db.exec(`
      CREATE TABLE IF NOT EXISTS gateway_key_usage (
        key_id          TEXT NOT NULL REFERENCES gateway_virtual_keys(id) ON DELETE CASCADE,
        period_start    TEXT NOT NULL,
        tokens_in       INTEGER NOT NULL DEFAULT 0,
        tokens_out      INTEGER NOT NULL DEFAULT 0,
        requests        INTEGER NOT NULL DEFAULT 0,
        last_updated_at TEXT NOT NULL,
        PRIMARY KEY (key_id, period_start)
      );
      CREATE INDEX IF NOT EXISTS idx_gateway_usage_key
        ON gateway_key_usage (key_id);
    `);

    // Per-user daily LLM usage (gateway + direct). Mirrors schema.sql +
    // migrate.ts (dual-path rule). Separate lens from gateway_key_usage.
    // Spec: docs/superpowers/specs/2026-06-11-llm-usage-aggregation-design.md
    db.exec(`
      CREATE TABLE IF NOT EXISTS llm_usage_daily (
        day             TEXT    NOT NULL,
        user_id         TEXT    NOT NULL,
        source          TEXT    NOT NULL,
        model           TEXT    NOT NULL,
        route           TEXT    NOT NULL,
        tokens_in       INTEGER NOT NULL DEFAULT 0,
        tokens_out      INTEGER NOT NULL DEFAULT 0,
        requests        INTEGER NOT NULL DEFAULT 0,
        last_updated_at TEXT    NOT NULL,
        PRIMARY KEY (day, user_id, source, model, route)
      );
      CREATE INDEX IF NOT EXISTS idx_llm_usage_daily_user_day
        ON llm_usage_daily (user_id, day);
    `);

    // Usage dashboard v2: hour-grain ledger (supersedes llm_usage_daily as the
    // write target; daily kept as frozen archive). Mirrors schema.sql +
    // migrate.ts (dual-path). Backfill of the daily archive lives in migrate.ts
    // so it runs once on upgrade, not on every fresh initSchema.
    // Spec: docs/superpowers/specs/2026-06-11-usage-dashboard-v2-design.md
    db.exec(`
      CREATE TABLE IF NOT EXISTS llm_usage_hourly (
        hour            TEXT    NOT NULL,
        user_id         TEXT    NOT NULL,
        source          TEXT    NOT NULL,
        model           TEXT    NOT NULL,
        route           TEXT    NOT NULL,
        tokens_in       INTEGER NOT NULL DEFAULT 0,
        tokens_out      INTEGER NOT NULL DEFAULT 0,
        requests        INTEGER NOT NULL DEFAULT 0,
        last_updated_at TEXT    NOT NULL,
        PRIMARY KEY (hour, user_id, source, model, route)
      );
      CREATE INDEX IF NOT EXISTS idx_llm_usage_hourly_user_hour
        ON llm_usage_hourly (user_id, hour);
    `);
  }


  export function runPreSchemaCompatibilityMigrations(db: Database.Database): void {
    const hasBrowserSessionProfiles = db
      .prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ?")
      .get("table", "browser_session_profiles");
    if (!hasBrowserSessionProfiles) {
      return;
    }

    const columns = db
      .prepare("PRAGMA table_info(browser_session_profiles)")
      .all() as Array<{ name: string }>;
    if (columns.some((column) => column.name === "space_id")) {
      return;
    }

    db.exec("ALTER TABLE browser_session_profiles ADD COLUMN space_id TEXT");
  }


  export function ensureColumn(db: Database.Database, tableName: string, columnName: string, definition: string): void {
    const columns = db.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{ name: string }>;
    if (columns.some((column) => column.name === columnName)) {
      return;
    }
    db.prepare(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`).run();
  }


  export function migrateWaitingSubtasksStatus(db: Database.Database): void {
    // Check if jobs table already has waiting_subtasks in its CHECK constraint
    const tableInfo = db.prepare(
      "SELECT sql FROM sqlite_master WHERE type='table' AND name='jobs'"
    ).get() as { sql: string } | undefined;
    if (!tableInfo || tableInfo.sql.includes('waiting_subtasks')) return;

    // Recreate jobs table with updated CHECK constraint
    logger.info('Repository: migrating jobs table to support waiting_subtasks status...');
    db.transaction(() => {
      db.exec(`
        CREATE TABLE IF NOT EXISTS jobs_v2 (
          id               TEXT PRIMARY KEY,
          repo             TEXT NOT NULL,
          issue_number     INTEGER NOT NULL,
          pr_number        INTEGER,
          status           TEXT NOT NULL DEFAULT 'queued'
                           CHECK (status IN ('queued','dispatching','running','succeeded','failed','retry','cancelled','waiting_human','waiting_subtasks')),
          piece_name       TEXT NOT NULL DEFAULT 'general',
          required_profile TEXT NOT NULL DEFAULT 'auto',
          task_class       TEXT NOT NULL DEFAULT 'auto',
          current_movement TEXT,
          instruction      TEXT NOT NULL DEFAULT '',
          branch_name      TEXT,
          worktree_path    TEXT,
          attempt          INTEGER NOT NULL DEFAULT 1,
          max_attempts     INTEGER NOT NULL DEFAULT 3,
          next_retry_at    TEXT,
          error_summary    TEXT,
          resume_movement  TEXT,
          ask_count        INTEGER NOT NULL DEFAULT 0,
          worker_id        TEXT,
          parent_job_id    TEXT,
          subtask_depth    INTEGER NOT NULL DEFAULT 0,
          created_at       TEXT NOT NULL DEFAULT (datetime('now')),
          updated_at       TEXT NOT NULL DEFAULT (datetime('now'))
        );
        INSERT INTO jobs_v2
          SELECT id, repo, issue_number, pr_number, status, piece_name, required_profile, task_class,
                 current_movement, instruction, branch_name, worktree_path, attempt, max_attempts,
                 next_retry_at, error_summary, resume_movement, ask_count, worker_id,
                 NULL AS parent_job_id, 0 AS subtask_depth, created_at, updated_at
          FROM jobs;
        DROP TABLE jobs;
        ALTER TABLE jobs_v2 RENAME TO jobs;
      `);
    })();
    logger.info('Repository: jobs table migration complete');
  }
