trekoon 0.2.0 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,12 +5,14 @@ import { runEvents } from "../commands/events";
5
5
  import { runInit } from "../commands/init";
6
6
  import { runMigrate } from "../commands/migrate";
7
7
  import { runQuickstart } from "../commands/quickstart";
8
+ import { runSession } from "../commands/session";
8
9
  import { runSkills } from "../commands/skills";
9
10
  import { runSubtask } from "../commands/subtask";
10
11
  import { runSync } from "../commands/sync";
11
12
  import { runTask } from "../commands/task";
12
13
  import { runWipe } from "../commands/wipe";
13
14
  import { failResult, okResult, renderResult } from "../io/output";
15
+ import { resolveStorageResolutionDiagnostics } from "../storage/database";
14
16
  import { type CliContext, type CliResult, type CompatibilityMode, type OutputMode } from "./command-types";
15
17
  import { CLI_VERSION } from "./version";
16
18
  import { resolveStoragePaths } from "../storage/path";
@@ -19,6 +21,7 @@ const SUPPORTED_ROOT_COMMANDS: readonly string[] = [
19
21
  "help",
20
22
  "init",
21
23
  "quickstart",
24
+ "session",
22
25
  "epic",
23
26
  "task",
24
27
  "subtask",
@@ -119,9 +122,68 @@ export function renderShellResult(result: CliResult, mode: OutputMode, compatibi
119
122
  return renderResult(result, mode, { compatibilityMode: effectiveCompatibilityMode });
120
123
  }
121
124
 
125
+ function isStringArray(value: unknown): value is string[] {
126
+ return Array.isArray(value) && value.every((entry: unknown) => typeof entry === "string");
127
+ }
128
+
129
+ function readResultStorageResolutionDiagnostics(result: CliResult) {
130
+ const data: unknown = result.data;
131
+ if (!data || typeof data !== "object") {
132
+ return null;
133
+ }
134
+
135
+ const candidate: Record<string, unknown> = data as Record<string, unknown>;
136
+ if (
137
+ typeof candidate.invocationCwd !== "string"
138
+ || typeof candidate.storageMode !== "string"
139
+ || (candidate.repoCommonDir !== null && typeof candidate.repoCommonDir !== "string")
140
+ || typeof candidate.worktreeRoot !== "string"
141
+ || typeof candidate.sharedStorageRoot !== "string"
142
+ || typeof candidate.databaseFile !== "string"
143
+ || typeof candidate.legacyStateDetected !== "boolean"
144
+ || typeof candidate.recoveryRequired !== "boolean"
145
+ || typeof candidate.recoveryStatus !== "string"
146
+ || !isStringArray(candidate.legacyDatabaseFiles)
147
+ || !isStringArray(candidate.backupFiles)
148
+ || !isStringArray(candidate.trackedStorageFiles)
149
+ || typeof candidate.autoMigratedLegacyState !== "boolean"
150
+ || (candidate.importedFromLegacyDatabase !== null && typeof candidate.importedFromLegacyDatabase !== "string")
151
+ || typeof candidate.operatorAction !== "string"
152
+ ) {
153
+ return null;
154
+ }
155
+
156
+ return {
157
+ invocationCwd: candidate.invocationCwd,
158
+ storageMode: candidate.storageMode,
159
+ repoCommonDir: candidate.repoCommonDir,
160
+ worktreeRoot: candidate.worktreeRoot,
161
+ sharedStorageRoot: candidate.sharedStorageRoot,
162
+ databaseFile: candidate.databaseFile,
163
+ legacyStateDetected: candidate.legacyStateDetected,
164
+ recoveryRequired: candidate.recoveryRequired,
165
+ recoveryStatus: candidate.recoveryStatus,
166
+ legacyDatabaseFiles: candidate.legacyDatabaseFiles,
167
+ backupFiles: candidate.backupFiles,
168
+ trackedStorageFiles: candidate.trackedStorageFiles,
169
+ autoMigratedLegacyState: candidate.autoMigratedLegacyState,
170
+ importedFromLegacyDatabase: candidate.importedFromLegacyDatabase,
171
+ operatorAction: candidate.operatorAction,
172
+ };
173
+ }
174
+
122
175
  function withStorageRootDiagnostics(result: CliResult, cwd: string): CliResult {
123
- const diagnostics = resolveStoragePaths(cwd).diagnostics;
124
- if (diagnostics.warnings.length === 0 && diagnostics.errors.length === 0) {
176
+ const paths = resolveStoragePaths(cwd);
177
+ const diagnostics = paths.diagnostics;
178
+ const resultDiagnostics = readResultStorageResolutionDiagnostics(result);
179
+ const resolutionDiagnostics = resultDiagnostics ?? resolveStorageResolutionDiagnostics(cwd);
180
+
181
+ if (
182
+ !resolutionDiagnostics.legacyStateDetected
183
+ && diagnostics.warnings.length === 0
184
+ && diagnostics.errors.length === 0
185
+ && resultDiagnostics === null
186
+ ) {
125
187
  return result;
126
188
  }
127
189
 
@@ -131,9 +193,22 @@ function withStorageRootDiagnostics(result: CliResult, cwd: string): CliResult {
131
193
  ...(result.meta ?? {}),
132
194
  storageRootDiagnostics: {
133
195
  invocationCwd: diagnostics.invocationCwd,
134
- canonicalRoot: diagnostics.canonicalRoot,
135
- warning: diagnostics.warnings[0] ?? null,
136
- error: diagnostics.errors[0] ?? null,
196
+ storageMode: diagnostics.storageMode,
197
+ repoCommonDir: diagnostics.repoCommonDir,
198
+ worktreeRoot: diagnostics.worktreeRoot,
199
+ sharedStorageRoot: diagnostics.sharedStorageRoot,
200
+ databaseFile: diagnostics.databaseFile,
201
+ legacyStateDetected: resolutionDiagnostics.legacyStateDetected,
202
+ recoveryRequired: resolutionDiagnostics.recoveryRequired,
203
+ recoveryStatus: resolutionDiagnostics.recoveryStatus,
204
+ legacyDatabaseFiles: resolutionDiagnostics.legacyDatabaseFiles,
205
+ backupFiles: resolutionDiagnostics.backupFiles,
206
+ trackedStorageFiles: resolutionDiagnostics.trackedStorageFiles,
207
+ autoMigratedLegacyState: resolutionDiagnostics.autoMigratedLegacyState,
208
+ importedFromLegacyDatabase: resolutionDiagnostics.importedFromLegacyDatabase,
209
+ operatorAction: resolutionDiagnostics.operatorAction,
210
+ warnings: diagnostics.warnings,
211
+ errors: diagnostics.errors,
137
212
  },
138
213
  },
139
214
  };
@@ -289,6 +364,9 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
289
364
  case "sync":
290
365
  result = await runSync(context);
291
366
  break;
367
+ case "session":
368
+ result = await runSession(context);
369
+ break;
292
370
  case "skills":
293
371
  result = await runSkills(context);
294
372
  break;
@@ -2,12 +2,37 @@ import { mkdirSync } from "node:fs";
2
2
 
3
3
  import { Database } from "bun:sqlite";
4
4
 
5
+ import { DomainError } from "../domain/types";
5
6
  import { migrateDatabase } from "./migrations";
6
7
  import { resolveStoragePaths, type StoragePaths } from "./path";
8
+ import {
9
+ inspectWorktreeDatabaseState,
10
+ recoverWorktreeDatabaseState,
11
+ type WorktreeRecoveryDiagnostics,
12
+ } from "./worktree-recovery";
13
+
14
+ export interface StorageResolutionDiagnostics {
15
+ readonly invocationCwd: string;
16
+ readonly storageMode: StoragePaths["storageMode"];
17
+ readonly repoCommonDir: string | null;
18
+ readonly worktreeRoot: string;
19
+ readonly sharedStorageRoot: string;
20
+ readonly databaseFile: string;
21
+ readonly legacyStateDetected: boolean;
22
+ readonly recoveryRequired: boolean;
23
+ readonly recoveryStatus: WorktreeRecoveryDiagnostics["status"];
24
+ readonly legacyDatabaseFiles: readonly string[];
25
+ readonly backupFiles: readonly string[];
26
+ readonly trackedStorageFiles: readonly string[];
27
+ readonly autoMigratedLegacyState: boolean;
28
+ readonly importedFromLegacyDatabase: string | null;
29
+ readonly operatorAction: string;
30
+ }
7
31
 
8
32
  export interface TrekoonDatabase {
9
33
  readonly db: Database;
10
34
  readonly paths: StoragePaths;
35
+ readonly diagnostics: StorageResolutionDiagnostics;
11
36
  close(): void;
12
37
  }
13
38
 
@@ -15,11 +40,71 @@ export interface OpenTrekoonDatabaseOptions {
15
40
  readonly autoMigrate?: boolean;
16
41
  }
17
42
 
43
+ function buildStorageResolutionDiagnostics(
44
+ paths: StoragePaths,
45
+ recovery: WorktreeRecoveryDiagnostics,
46
+ ): StorageResolutionDiagnostics {
47
+ const legacyStateDetected: boolean = recovery.legacyDatabaseFiles.length > 0;
48
+ const recoveryRequired: boolean =
49
+ recovery.status === "ambiguous_recovery" || recovery.status === "tracked_ignored_mismatch";
50
+
51
+ return {
52
+ invocationCwd: paths.invocationCwd,
53
+ storageMode: paths.storageMode,
54
+ repoCommonDir: paths.repoCommonDir,
55
+ worktreeRoot: paths.worktreeRoot,
56
+ sharedStorageRoot: paths.sharedStorageRoot,
57
+ databaseFile: paths.databaseFile,
58
+ legacyStateDetected,
59
+ recoveryRequired,
60
+ recoveryStatus: recovery.status,
61
+ legacyDatabaseFiles: recovery.legacyDatabaseFiles,
62
+ backupFiles: recovery.backupFiles,
63
+ trackedStorageFiles: recovery.trackedStorageFiles,
64
+ autoMigratedLegacyState: recovery.autoMigrated,
65
+ importedFromLegacyDatabase: recovery.importedFrom,
66
+ operatorAction: recovery.operatorAction,
67
+ };
68
+ }
69
+
70
+ export function resolveStorageResolutionDiagnostics(
71
+ workingDirectory: string = process.cwd(),
72
+ ): StorageResolutionDiagnostics {
73
+ const paths: StoragePaths = resolveStoragePaths(workingDirectory);
74
+
75
+ try {
76
+ return buildStorageResolutionDiagnostics(paths, inspectWorktreeDatabaseState(paths));
77
+ } catch (error) {
78
+ if (!(error instanceof DomainError)) {
79
+ throw error;
80
+ }
81
+
82
+ const details: Record<string, unknown> = error.details ?? {};
83
+ const recovery: WorktreeRecoveryDiagnostics = {
84
+ status: (details.status as WorktreeRecoveryDiagnostics["status"] | undefined) ?? "no_legacy_state",
85
+ legacyDatabaseFiles: Array.isArray(details.legacyDatabaseFiles)
86
+ ? (details.legacyDatabaseFiles as string[])
87
+ : [],
88
+ backupFiles: Array.isArray(details.backupFiles) ? (details.backupFiles as string[]) : [],
89
+ trackedStorageFiles: Array.isArray(details.trackedStorageFiles)
90
+ ? (details.trackedStorageFiles as string[])
91
+ : [],
92
+ autoMigrated: details.autoMigrated === true,
93
+ importedFrom: typeof details.importedFrom === "string" ? details.importedFrom : null,
94
+ operatorAction: typeof details.operatorAction === "string" ? details.operatorAction : error.message,
95
+ };
96
+
97
+ return buildStorageResolutionDiagnostics(paths, recovery);
98
+ }
99
+ }
100
+
18
101
  export function openTrekoonDatabase(
19
102
  workingDirectory: string = process.cwd(),
20
103
  options: OpenTrekoonDatabaseOptions = {},
21
104
  ): TrekoonDatabase {
22
105
  const paths: StoragePaths = resolveStoragePaths(workingDirectory);
106
+ const recovery: WorktreeRecoveryDiagnostics = recoverWorktreeDatabaseState(paths);
107
+ const diagnostics: StorageResolutionDiagnostics = buildStorageResolutionDiagnostics(paths, recovery);
23
108
 
24
109
  mkdirSync(paths.storageDir, { recursive: true });
25
110
 
@@ -36,6 +121,7 @@ export function openTrekoonDatabase(
36
121
  return {
37
122
  db,
38
123
  paths,
124
+ diagnostics,
39
125
  close(): void {
40
126
  db.exec("PRAGMA wal_checkpoint(PASSIVE);");
41
127
  db.close(false);
@@ -58,6 +58,43 @@ const EVENT_ARCHIVE_MIGRATION_DOWN_STATEMENTS: readonly string[] = [
58
58
  "DROP TABLE IF EXISTS event_archive;",
59
59
  ];
60
60
 
61
+ function tableHasColumn(db: Database, tableName: string, columnName: string): boolean {
62
+ const columns = db.query(`PRAGMA table_info(${tableName});`).all() as Array<{ name: string }>;
63
+ return columns.some((column) => column.name === columnName);
64
+ }
65
+
66
+ function migrateWorktreeScopedSyncMetadata(db: Database): void {
67
+ if (!tableHasColumn(db, "git_context", "metadata_scope")) {
68
+ db.exec("ALTER TABLE git_context ADD COLUMN metadata_scope TEXT NOT NULL DEFAULT 'worktree';");
69
+ }
70
+
71
+ db.exec("UPDATE git_context SET metadata_scope = 'worktree' WHERE metadata_scope IS NULL OR metadata_scope = ''; ");
72
+ db.exec("UPDATE git_context SET id = worktree_path WHERE id = 'current' AND worktree_path <> ''; ");
73
+ db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_git_context_scope_path ON git_context(metadata_scope, worktree_path);");
74
+
75
+ if (!tableHasColumn(db, "sync_cursors", "owner_scope")) {
76
+ db.exec("ALTER TABLE sync_cursors ADD COLUMN owner_scope TEXT NOT NULL DEFAULT 'worktree';");
77
+ }
78
+
79
+ if (!tableHasColumn(db, "sync_cursors", "owner_worktree_path")) {
80
+ db.exec("ALTER TABLE sync_cursors ADD COLUMN owner_worktree_path TEXT NOT NULL DEFAULT ''; ");
81
+ }
82
+
83
+ db.exec("UPDATE sync_cursors SET owner_scope = 'worktree' WHERE owner_scope IS NULL OR owner_scope = ''; ");
84
+ db.exec(`
85
+ UPDATE sync_cursors
86
+ SET owner_worktree_path = COALESCE(
87
+ NULLIF(owner_worktree_path, ''),
88
+ (SELECT worktree_path FROM git_context ORDER BY updated_at DESC LIMIT 1),
89
+ ''
90
+ );
91
+ `);
92
+ db.exec("UPDATE sync_cursors SET id = owner_worktree_path || '::' || source_branch;");
93
+ db.exec(
94
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_sync_cursors_owner ON sync_cursors(owner_scope, owner_worktree_path, source_branch);",
95
+ );
96
+ }
97
+
61
98
  interface Migration {
62
99
  readonly version: number;
63
100
  readonly name: string;
@@ -134,6 +171,17 @@ const MIGRATIONS: readonly Migration[] = [
134
171
  }
135
172
  },
136
173
  },
174
+ {
175
+ version: 4,
176
+ name: "0004_worktree_scoped_sync_metadata",
177
+ up(db: Database): void {
178
+ migrateWorktreeScopedSyncMetadata(db);
179
+ },
180
+ down(db: Database): void {
181
+ db.exec("DROP INDEX IF EXISTS idx_sync_cursors_owner;");
182
+ db.exec("DROP INDEX IF EXISTS idx_git_context_scope_path;");
183
+ },
184
+ },
137
185
  ];
138
186
 
139
187
  function migrationTableExists(db: Database): boolean {
@@ -1,12 +1,26 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { realpathSync } from "node:fs";
2
3
  import { resolve } from "node:path";
3
4
 
4
- const DB_DIRNAME = ".trekoon";
5
- const DB_FILENAME = "trekoon.db";
5
+ export const TREKOON_STORAGE_DIRNAME = ".trekoon";
6
+ export const TREKOON_DATABASE_FILENAME = "trekoon.db";
7
+
8
+ export function resolveLegacyWorktreeStorageDir(worktreeRoot: string): string {
9
+ return resolve(worktreeRoot, TREKOON_STORAGE_DIRNAME);
10
+ }
11
+
12
+ export function resolveLegacyWorktreeDatabaseFile(worktreeRoot: string): string {
13
+ return resolve(resolveLegacyWorktreeStorageDir(worktreeRoot), TREKOON_DATABASE_FILENAME);
14
+ }
15
+
16
+ export type StorageMode = "cwd" | "git_common_dir";
6
17
 
7
18
  export interface StoragePaths {
8
19
  readonly invocationCwd: string;
20
+ readonly storageMode: StorageMode;
21
+ readonly repoCommonDir: string | null;
9
22
  readonly worktreeRoot: string;
23
+ readonly sharedStorageRoot: string;
10
24
  readonly storageDir: string;
11
25
  readonly databaseFile: string;
12
26
  readonly diagnostics: StoragePathDiagnostics;
@@ -16,18 +30,26 @@ export interface StoragePathIssue {
16
30
  readonly code: string;
17
31
  readonly message: string;
18
32
  readonly invocationCwd: string;
19
- readonly canonicalRoot: string;
33
+ readonly storageMode: StorageMode;
34
+ readonly repoCommonDir: string | null;
35
+ readonly worktreeRoot: string;
36
+ readonly sharedStorageRoot: string;
37
+ readonly databaseFile: string;
20
38
  }
21
39
 
22
40
  export interface StoragePathDiagnostics {
23
41
  readonly invocationCwd: string;
24
- readonly canonicalRoot: string;
42
+ readonly storageMode: StorageMode;
43
+ readonly repoCommonDir: string | null;
44
+ readonly worktreeRoot: string;
45
+ readonly sharedStorageRoot: string;
46
+ readonly databaseFile: string;
25
47
  readonly warnings: readonly StoragePathIssue[];
26
48
  readonly errors: readonly StoragePathIssue[];
27
49
  }
28
50
 
29
- function resolveGitTopLevel(workingDirectory: string): string | null {
30
- const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
51
+ function resolveGitPath(workingDirectory: string, argument: "--git-common-dir" | "--show-toplevel"): string | null {
52
+ const result = spawnSync("git", ["rev-parse", argument], {
31
53
  cwd: workingDirectory,
32
54
  encoding: "utf8",
33
55
  stdio: ["ignore", "pipe", "ignore"],
@@ -37,41 +59,68 @@ function resolveGitTopLevel(workingDirectory: string): string | null {
37
59
  return null;
38
60
  }
39
61
 
40
- const topLevel: string = result.stdout.trim();
41
- if (!topLevel) {
62
+ const rawPath: string = result.stdout.trim();
63
+ if (!rawPath) {
42
64
  return null;
43
65
  }
44
66
 
45
- return resolve(topLevel);
67
+ return resolve(workingDirectory, rawPath);
46
68
  }
47
69
 
48
70
  export function resolveStoragePaths(workingDirectory: string = process.cwd()): StoragePaths {
49
71
  const invocationCwd: string = resolve(workingDirectory);
50
- const canonicalRoot: string = resolveGitTopLevel(invocationCwd) ?? invocationCwd;
51
- const worktreeRoot: string = canonicalRoot;
52
- const storageDir: string = resolve(worktreeRoot, DB_DIRNAME);
53
- const databaseFile: string = resolve(storageDir, DB_FILENAME);
72
+ const worktreeRoot: string = resolveGitPath(invocationCwd, "--show-toplevel") ?? invocationCwd;
73
+ const repoCommonDirRaw: string | null = resolveGitPath(invocationCwd, "--git-common-dir");
74
+ const repoCommonDir: string | null = repoCommonDirRaw ? realpathSync(repoCommonDirRaw) : null;
75
+ const storageMode: StorageMode = repoCommonDir ? "git_common_dir" : "cwd";
76
+ const sharedStorageRoot: string = repoCommonDir ? realpathSync(resolve(repoCommonDir, "..")) : invocationCwd;
77
+ const storageDir: string = resolve(sharedStorageRoot, TREKOON_STORAGE_DIRNAME);
78
+ const databaseFile: string = resolve(storageDir, TREKOON_DATABASE_FILENAME);
54
79
  const warnings: StoragePathIssue[] = [];
55
80
 
56
- if (invocationCwd !== canonicalRoot) {
57
- warnings.push({
58
- code: "storage_root_diverged_from_cwd",
59
- message: "Resolved storage root differs from invocation cwd.",
60
- invocationCwd,
61
- canonicalRoot,
62
- });
81
+ const createIssue = (code: string, message: string): StoragePathIssue => ({
82
+ code,
83
+ message,
84
+ invocationCwd,
85
+ storageMode,
86
+ repoCommonDir,
87
+ worktreeRoot,
88
+ sharedStorageRoot,
89
+ databaseFile,
90
+ });
91
+
92
+ if (invocationCwd !== worktreeRoot) {
93
+ warnings.push(
94
+ createIssue("storage_root_diverged_from_cwd", "Resolved worktree root differs from invocation cwd."),
95
+ );
96
+ }
97
+
98
+ if (sharedStorageRoot !== worktreeRoot) {
99
+ warnings.push(
100
+ createIssue(
101
+ "shared_storage_root_differs_from_worktree_root",
102
+ "Resolved shared storage root differs from worktree root.",
103
+ ),
104
+ );
63
105
  }
64
106
 
65
107
  const diagnostics: StoragePathDiagnostics = {
66
108
  invocationCwd,
67
- canonicalRoot,
109
+ storageMode,
110
+ repoCommonDir,
111
+ worktreeRoot,
112
+ sharedStorageRoot,
113
+ databaseFile,
68
114
  warnings,
69
115
  errors: [],
70
116
  };
71
117
 
72
118
  return {
73
119
  invocationCwd,
120
+ storageMode,
121
+ repoCommonDir,
74
122
  worktreeRoot,
123
+ sharedStorageRoot,
75
124
  storageDir,
76
125
  databaseFile,
77
126
  diagnostics,
@@ -76,23 +76,28 @@ export const BASE_SCHEMA_STATEMENTS: readonly string[] = [
76
76
  `
77
77
  CREATE TABLE IF NOT EXISTS git_context (
78
78
  id TEXT PRIMARY KEY,
79
+ metadata_scope TEXT NOT NULL DEFAULT 'worktree',
79
80
  worktree_path TEXT NOT NULL,
80
81
  branch_name TEXT,
81
82
  head_sha TEXT,
82
83
  created_at INTEGER NOT NULL,
83
84
  updated_at INTEGER NOT NULL,
84
- version INTEGER NOT NULL DEFAULT 1
85
+ version INTEGER NOT NULL DEFAULT 1,
86
+ UNIQUE (metadata_scope, worktree_path)
85
87
  );
86
88
  `,
87
89
  `
88
90
  CREATE TABLE IF NOT EXISTS sync_cursors (
89
91
  id TEXT PRIMARY KEY,
92
+ owner_scope TEXT NOT NULL DEFAULT 'worktree',
93
+ owner_worktree_path TEXT NOT NULL,
90
94
  source_branch TEXT NOT NULL,
91
95
  cursor_token TEXT NOT NULL,
92
96
  last_event_at INTEGER,
93
97
  created_at INTEGER NOT NULL,
94
98
  updated_at INTEGER NOT NULL,
95
- version INTEGER NOT NULL DEFAULT 1
99
+ version INTEGER NOT NULL DEFAULT 1,
100
+ UNIQUE (owner_scope, owner_worktree_path, source_branch)
96
101
  );
97
102
  `,
98
103
  `
@@ -113,5 +118,7 @@ export const BASE_SCHEMA_STATEMENTS: readonly string[] = [
113
118
  `CREATE INDEX IF NOT EXISTS idx_tasks_epic_id ON tasks(epic_id);`,
114
119
  `CREATE INDEX IF NOT EXISTS idx_subtasks_task_id ON subtasks(task_id);`,
115
120
  `CREATE INDEX IF NOT EXISTS idx_events_entity ON events(entity_kind, entity_id);`,
121
+ `CREATE INDEX IF NOT EXISTS idx_git_context_scope_path ON git_context(metadata_scope, worktree_path);`,
122
+ `CREATE INDEX IF NOT EXISTS idx_sync_cursors_owner ON sync_cursors(owner_scope, owner_worktree_path, source_branch);`,
116
123
  `CREATE INDEX IF NOT EXISTS idx_conflicts_resolution ON sync_conflicts(resolution);`,
117
124
  ];