gobby 0.2.6__py3-none-any.whl → 0.2.8__py3-none-any.whl

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.
Files changed (198) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +96 -35
  4. gobby/adapters/codex_impl/__init__.py +28 -0
  5. gobby/adapters/codex_impl/adapter.py +722 -0
  6. gobby/adapters/codex_impl/client.py +679 -0
  7. gobby/adapters/codex_impl/protocol.py +20 -0
  8. gobby/adapters/codex_impl/types.py +68 -0
  9. gobby/adapters/gemini.py +140 -38
  10. gobby/agents/definitions.py +11 -1
  11. gobby/agents/isolation.py +525 -0
  12. gobby/agents/registry.py +11 -0
  13. gobby/agents/sandbox.py +261 -0
  14. gobby/agents/session.py +1 -0
  15. gobby/agents/spawn.py +42 -287
  16. gobby/agents/spawn_executor.py +415 -0
  17. gobby/agents/spawners/__init__.py +24 -0
  18. gobby/agents/spawners/command_builder.py +189 -0
  19. gobby/agents/spawners/embedded.py +21 -2
  20. gobby/agents/spawners/headless.py +21 -2
  21. gobby/agents/spawners/macos.py +26 -1
  22. gobby/agents/spawners/prompt_manager.py +125 -0
  23. gobby/cli/__init__.py +0 -2
  24. gobby/cli/install.py +4 -4
  25. gobby/cli/installers/claude.py +6 -0
  26. gobby/cli/installers/gemini.py +6 -0
  27. gobby/cli/installers/shared.py +103 -4
  28. gobby/cli/memory.py +185 -0
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/utils.py +9 -2
  31. gobby/clones/git.py +177 -0
  32. gobby/config/__init__.py +12 -97
  33. gobby/config/app.py +10 -94
  34. gobby/config/extensions.py +2 -2
  35. gobby/config/features.py +7 -130
  36. gobby/config/skills.py +31 -0
  37. gobby/config/tasks.py +4 -28
  38. gobby/hooks/__init__.py +0 -13
  39. gobby/hooks/event_handlers.py +150 -8
  40. gobby/hooks/hook_manager.py +21 -3
  41. gobby/hooks/plugins.py +1 -1
  42. gobby/hooks/webhooks.py +1 -1
  43. gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
  44. gobby/llm/resolver.py +3 -2
  45. gobby/mcp_proxy/importer.py +62 -4
  46. gobby/mcp_proxy/instructions.py +4 -2
  47. gobby/mcp_proxy/registries.py +22 -8
  48. gobby/mcp_proxy/services/recommendation.py +43 -11
  49. gobby/mcp_proxy/tools/agent_messaging.py +93 -44
  50. gobby/mcp_proxy/tools/agents.py +76 -740
  51. gobby/mcp_proxy/tools/artifacts.py +43 -9
  52. gobby/mcp_proxy/tools/clones.py +0 -385
  53. gobby/mcp_proxy/tools/memory.py +2 -2
  54. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  55. gobby/mcp_proxy/tools/sessions/_commits.py +239 -0
  56. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  57. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  58. gobby/mcp_proxy/tools/sessions/_handoff.py +503 -0
  59. gobby/mcp_proxy/tools/sessions/_messages.py +166 -0
  60. gobby/mcp_proxy/tools/skills/__init__.py +14 -29
  61. gobby/mcp_proxy/tools/spawn_agent.py +455 -0
  62. gobby/mcp_proxy/tools/tasks/_context.py +18 -0
  63. gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
  64. gobby/mcp_proxy/tools/tasks/_lifecycle.py +79 -30
  65. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
  66. gobby/mcp_proxy/tools/tasks/_session.py +22 -7
  67. gobby/mcp_proxy/tools/workflows.py +84 -34
  68. gobby/mcp_proxy/tools/worktrees.py +32 -350
  69. gobby/memory/extractor.py +15 -1
  70. gobby/memory/ingestion/__init__.py +5 -0
  71. gobby/memory/ingestion/multimodal.py +221 -0
  72. gobby/memory/manager.py +62 -283
  73. gobby/memory/search/__init__.py +10 -0
  74. gobby/memory/search/coordinator.py +248 -0
  75. gobby/memory/services/__init__.py +5 -0
  76. gobby/memory/services/crossref.py +142 -0
  77. gobby/prompts/loader.py +5 -2
  78. gobby/runner.py +13 -0
  79. gobby/servers/http.py +1 -4
  80. gobby/servers/routes/admin.py +14 -0
  81. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  82. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  83. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  84. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  85. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  86. gobby/servers/routes/mcp/hooks.py +51 -4
  87. gobby/servers/routes/mcp/tools.py +48 -1506
  88. gobby/servers/websocket.py +57 -1
  89. gobby/sessions/analyzer.py +2 -2
  90. gobby/sessions/lifecycle.py +1 -1
  91. gobby/sessions/manager.py +9 -0
  92. gobby/sessions/processor.py +10 -0
  93. gobby/sessions/transcripts/base.py +1 -0
  94. gobby/sessions/transcripts/claude.py +15 -5
  95. gobby/sessions/transcripts/gemini.py +100 -34
  96. gobby/skills/parser.py +30 -2
  97. gobby/storage/database.py +9 -2
  98. gobby/storage/memories.py +32 -21
  99. gobby/storage/migrations.py +174 -368
  100. gobby/storage/sessions.py +45 -7
  101. gobby/storage/skills.py +80 -7
  102. gobby/storage/tasks/_lifecycle.py +18 -3
  103. gobby/sync/memories.py +1 -1
  104. gobby/tasks/external_validator.py +1 -1
  105. gobby/tasks/validation.py +22 -20
  106. gobby/tools/summarizer.py +91 -10
  107. gobby/utils/project_context.py +2 -3
  108. gobby/utils/status.py +13 -0
  109. gobby/workflows/actions.py +221 -1217
  110. gobby/workflows/artifact_actions.py +31 -0
  111. gobby/workflows/autonomous_actions.py +11 -0
  112. gobby/workflows/context_actions.py +50 -1
  113. gobby/workflows/detection_helpers.py +38 -24
  114. gobby/workflows/enforcement/__init__.py +47 -0
  115. gobby/workflows/enforcement/blocking.py +281 -0
  116. gobby/workflows/enforcement/commit_policy.py +283 -0
  117. gobby/workflows/enforcement/handlers.py +269 -0
  118. gobby/workflows/enforcement/task_policy.py +542 -0
  119. gobby/workflows/engine.py +93 -0
  120. gobby/workflows/evaluator.py +110 -0
  121. gobby/workflows/git_utils.py +106 -0
  122. gobby/workflows/hooks.py +41 -0
  123. gobby/workflows/llm_actions.py +30 -0
  124. gobby/workflows/mcp_actions.py +20 -1
  125. gobby/workflows/memory_actions.py +91 -0
  126. gobby/workflows/safe_evaluator.py +191 -0
  127. gobby/workflows/session_actions.py +44 -0
  128. gobby/workflows/state_actions.py +60 -1
  129. gobby/workflows/stop_signal_actions.py +55 -0
  130. gobby/workflows/summary_actions.py +217 -51
  131. gobby/workflows/task_sync_actions.py +347 -0
  132. gobby/workflows/todo_actions.py +34 -1
  133. gobby/workflows/webhook_actions.py +185 -0
  134. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/METADATA +6 -1
  135. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/RECORD +139 -163
  136. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/WHEEL +1 -1
  137. gobby/adapters/codex.py +0 -1332
  138. gobby/cli/tui.py +0 -34
  139. gobby/install/claude/commands/gobby/bug.md +0 -51
  140. gobby/install/claude/commands/gobby/chore.md +0 -51
  141. gobby/install/claude/commands/gobby/epic.md +0 -52
  142. gobby/install/claude/commands/gobby/eval.md +0 -235
  143. gobby/install/claude/commands/gobby/feat.md +0 -49
  144. gobby/install/claude/commands/gobby/nit.md +0 -52
  145. gobby/install/claude/commands/gobby/ref.md +0 -52
  146. gobby/mcp_proxy/tools/session_messages.py +0 -1055
  147. gobby/prompts/defaults/expansion/system.md +0 -119
  148. gobby/prompts/defaults/expansion/user.md +0 -48
  149. gobby/prompts/defaults/external_validation/agent.md +0 -72
  150. gobby/prompts/defaults/external_validation/external.md +0 -63
  151. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  152. gobby/prompts/defaults/external_validation/system.md +0 -6
  153. gobby/prompts/defaults/features/import_mcp.md +0 -22
  154. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  155. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  156. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  157. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  158. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  159. gobby/prompts/defaults/features/server_description.md +0 -20
  160. gobby/prompts/defaults/features/server_description_system.md +0 -6
  161. gobby/prompts/defaults/features/task_description.md +0 -31
  162. gobby/prompts/defaults/features/task_description_system.md +0 -6
  163. gobby/prompts/defaults/features/tool_summary.md +0 -17
  164. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  165. gobby/prompts/defaults/handoff/compact.md +0 -63
  166. gobby/prompts/defaults/handoff/session_end.md +0 -57
  167. gobby/prompts/defaults/memory/extract.md +0 -61
  168. gobby/prompts/defaults/research/step.md +0 -58
  169. gobby/prompts/defaults/validation/criteria.md +0 -47
  170. gobby/prompts/defaults/validation/validate.md +0 -38
  171. gobby/storage/migrations_legacy.py +0 -1359
  172. gobby/tui/__init__.py +0 -5
  173. gobby/tui/api_client.py +0 -278
  174. gobby/tui/app.py +0 -329
  175. gobby/tui/screens/__init__.py +0 -25
  176. gobby/tui/screens/agents.py +0 -333
  177. gobby/tui/screens/chat.py +0 -450
  178. gobby/tui/screens/dashboard.py +0 -377
  179. gobby/tui/screens/memory.py +0 -305
  180. gobby/tui/screens/metrics.py +0 -231
  181. gobby/tui/screens/orchestrator.py +0 -903
  182. gobby/tui/screens/sessions.py +0 -412
  183. gobby/tui/screens/tasks.py +0 -440
  184. gobby/tui/screens/workflows.py +0 -289
  185. gobby/tui/screens/worktrees.py +0 -174
  186. gobby/tui/widgets/__init__.py +0 -21
  187. gobby/tui/widgets/chat.py +0 -210
  188. gobby/tui/widgets/conductor.py +0 -104
  189. gobby/tui/widgets/menu.py +0 -132
  190. gobby/tui/widgets/message_panel.py +0 -160
  191. gobby/tui/widgets/review_gate.py +0 -224
  192. gobby/tui/widgets/task_tree.py +0 -99
  193. gobby/tui/widgets/token_budget.py +0 -166
  194. gobby/tui/ws_client.py +0 -258
  195. gobby/workflows/task_enforcement_actions.py +0 -1343
  196. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
  197. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
  198. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
@@ -3,17 +3,27 @@
3
3
  This module handles schema migrations for the Gobby database.
4
4
 
5
5
  For new databases (version == 0):
6
- The BASELINE_SCHEMA is applied directly, jumping to version 60.
6
+ By default (use_flattened_baseline=True), BASELINE_SCHEMA_V2 is applied,
7
+ jumping directly to version 75 with a clean schema definition.
7
8
 
8
- For existing databases (0 < version < 60):
9
- Legacy migrations are imported from migrations_legacy.py and run incrementally.
9
+ To use the legacy path (use_flattened_baseline=False), the old BASELINE_SCHEMA
10
+ is applied at version 60, followed by incremental migrations.
11
+
12
+ For existing databases (0 < version < 75):
13
+ Upgrade is not supported without legacy migrations (removed).
14
+
15
+ For existing databases (version >= 75):
16
+ Any migrations in MIGRATIONS (v76+) are applied incrementally.
17
+
18
+ Troubleshooting:
19
+ If you experience issues with new database creation, ensure you are starting
20
+ fresh or have a database version >= 60.
10
21
 
11
- For all databases:
12
- Any migrations in MIGRATIONS (v61+) are applied after the baseline/legacy path.
13
22
 
14
23
  To add a new migration:
15
- 1. Add it to the MIGRATIONS list below with version = 61, 62, etc.
24
+ 1. Add it to the MIGRATIONS list below with version = 76, 77, etc.
16
25
  2. Use SQL strings for schema changes, callables for data migrations.
26
+ 3. Also add the migration to BASELINE_SCHEMA_V2 for future fresh installs.
17
27
  """
18
28
 
19
29
  import logging
@@ -23,22 +33,29 @@ from gobby.storage.database import LocalDatabase
23
33
 
24
34
  logger = logging.getLogger(__name__)
25
35
 
36
+
37
+ class MigrationUnsupportedError(Exception):
38
+ """Raised when database version is too old to migrate."""
39
+
40
+ pass
41
+
42
+
26
43
  # Migration can be SQL string or a callable that takes LocalDatabase
27
44
  MigrationAction = str | Callable[[LocalDatabase], None]
28
45
 
29
- # Baseline version - the schema state after all legacy migrations
30
- BASELINE_VERSION = 60
46
+ # Baseline version - the schema state at v78 (flattened)
47
+ # This is applied for new databases directly
48
+ BASELINE_VERSION = 78
31
49
 
32
- # Baseline schema - applied directly for new databases
33
- # This represents the final schema state after migrations 1-60
50
+ # Baseline schema - flattened from v78 production state, includes hub tracking fields
51
+ # This is applied for new databases directly
52
+ # Generated by: sqlite3 ~/.gobby/gobby-hub.db .schema
34
53
  BASELINE_SCHEMA = """
35
- -- Schema version tracking
36
54
  CREATE TABLE schema_version (
37
55
  version INTEGER PRIMARY KEY,
38
56
  applied_at TEXT NOT NULL DEFAULT (datetime('now'))
39
57
  );
40
58
 
41
- -- Projects
42
59
  CREATE TABLE projects (
43
60
  id TEXT PRIMARY KEY,
44
61
  name TEXT NOT NULL UNIQUE,
@@ -57,7 +74,6 @@ VALUES ('00000000-0000-0000-0000-000000000000', '_orphaned', NULL, datetime('now
57
74
  INSERT INTO projects (id, name, repo_path, created_at, updated_at)
58
75
  VALUES ('00000000-0000-0000-0000-000000000001', '_migrated', NULL, datetime('now'), datetime('now'));
59
76
 
60
- -- MCP Servers
61
77
  CREATE TABLE mcp_servers (
62
78
  id TEXT PRIMARY KEY,
63
79
  name TEXT NOT NULL,
@@ -78,7 +94,6 @@ CREATE INDEX idx_mcp_servers_project_id ON mcp_servers(project_id);
78
94
  CREATE INDEX idx_mcp_servers_enabled ON mcp_servers(enabled);
79
95
  CREATE UNIQUE INDEX idx_mcp_servers_name_project ON mcp_servers(name, project_id);
80
96
 
81
- -- Tools
82
97
  CREATE TABLE tools (
83
98
  id TEXT PRIMARY KEY,
84
99
  mcp_server_id TEXT NOT NULL REFERENCES mcp_servers(id) ON DELETE CASCADE,
@@ -92,7 +107,6 @@ CREATE TABLE tools (
92
107
  CREATE INDEX idx_tools_server_id ON tools(mcp_server_id);
93
108
  CREATE INDEX idx_tools_name ON tools(name);
94
109
 
95
- -- Tool embeddings for semantic search
96
110
  CREATE TABLE tool_embeddings (
97
111
  id INTEGER PRIMARY KEY AUTOINCREMENT,
98
112
  tool_id TEXT NOT NULL REFERENCES tools(id) ON DELETE CASCADE,
@@ -111,7 +125,6 @@ CREATE INDEX idx_tool_embeddings_server ON tool_embeddings(server_name);
111
125
  CREATE INDEX idx_tool_embeddings_project ON tool_embeddings(project_id);
112
126
  CREATE INDEX idx_tool_embeddings_hash ON tool_embeddings(text_hash);
113
127
 
114
- -- Tool schema hashes for incremental re-indexing
115
128
  CREATE TABLE tool_schema_hashes (
116
129
  id INTEGER PRIMARY KEY AUTOINCREMENT,
117
130
  server_name TEXT NOT NULL,
@@ -127,7 +140,6 @@ CREATE INDEX idx_schema_hashes_server ON tool_schema_hashes(server_name);
127
140
  CREATE INDEX idx_schema_hashes_project ON tool_schema_hashes(project_id);
128
141
  CREATE INDEX idx_schema_hashes_verified ON tool_schema_hashes(last_verified_at);
129
142
 
130
- -- Tool metrics
131
143
  CREATE TABLE tool_metrics (
132
144
  id TEXT PRIMARY KEY,
133
145
  project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
@@ -149,7 +161,6 @@ CREATE INDEX idx_tool_metrics_tool ON tool_metrics(tool_name);
149
161
  CREATE INDEX idx_tool_metrics_call_count ON tool_metrics(call_count DESC);
150
162
  CREATE INDEX idx_tool_metrics_last_called ON tool_metrics(last_called_at);
151
163
 
152
- -- Tool metrics daily aggregates
153
164
  CREATE TABLE tool_metrics_daily (
154
165
  id INTEGER PRIMARY KEY AUTOINCREMENT,
155
166
  project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
@@ -168,7 +179,6 @@ CREATE INDEX idx_tool_metrics_daily_project ON tool_metrics_daily(project_id);
168
179
  CREATE INDEX idx_tool_metrics_daily_date ON tool_metrics_daily(date);
169
180
  CREATE INDEX idx_tool_metrics_daily_server ON tool_metrics_daily(server_name);
170
181
 
171
- -- Agent runs
172
182
  CREATE TABLE agent_runs (
173
183
  id TEXT PRIMARY KEY,
174
184
  parent_session_id TEXT NOT NULL REFERENCES sessions(id),
@@ -192,7 +202,6 @@ CREATE INDEX idx_agent_runs_child_session ON agent_runs(child_session_id);
192
202
  CREATE INDEX idx_agent_runs_status ON agent_runs(status);
193
203
  CREATE INDEX idx_agent_runs_provider ON agent_runs(provider);
194
204
 
195
- -- Sessions
196
205
  CREATE TABLE sessions (
197
206
  id TEXT PRIMARY KEY,
198
207
  external_id TEXT NOT NULL,
@@ -221,6 +230,8 @@ CREATE TABLE sessions (
221
230
  usage_total_cost_usd REAL DEFAULT 0.0,
222
231
  terminal_context TEXT,
223
232
  seq_num INTEGER,
233
+ model TEXT,
234
+ had_edits BOOLEAN DEFAULT 0,
224
235
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
225
236
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
226
237
  );
@@ -235,10 +246,9 @@ CREATE INDEX idx_sessions_agent_depth ON sessions(agent_depth);
235
246
  CREATE INDEX idx_sessions_spawned_by ON sessions(spawned_by_agent_id);
236
247
  CREATE INDEX idx_sessions_workflow ON sessions(workflow_name);
237
248
  CREATE INDEX idx_sessions_agent_run ON sessions(agent_run_id);
238
- CREATE UNIQUE INDEX idx_sessions_seq_num ON sessions(seq_num);
249
+ CREATE UNIQUE INDEX idx_sessions_seq_num ON sessions(project_id, seq_num);
239
250
  CREATE UNIQUE INDEX idx_sessions_unique ON sessions(external_id, machine_id, source, project_id);
240
251
 
241
- -- Session messages
242
252
  CREATE TABLE session_messages (
243
253
  id INTEGER PRIMARY KEY AUTOINCREMENT,
244
254
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
@@ -259,7 +269,6 @@ CREATE INDEX idx_session_messages_role ON session_messages(role);
259
269
  CREATE INDEX idx_session_messages_timestamp ON session_messages(timestamp);
260
270
  CREATE INDEX idx_session_messages_tool ON session_messages(tool_name);
261
271
 
262
- -- Session message processing state
263
272
  CREATE TABLE session_message_state (
264
273
  session_id TEXT PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE,
265
274
  last_byte_offset INTEGER DEFAULT 0,
@@ -269,7 +278,6 @@ CREATE TABLE session_message_state (
269
278
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
270
279
  );
271
280
 
272
- -- Session artifacts with FTS
273
281
  CREATE TABLE session_artifacts (
274
282
  id TEXT PRIMARY KEY,
275
283
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
@@ -286,7 +294,6 @@ CREATE INDEX idx_session_artifacts_type ON session_artifacts(artifact_type);
286
294
  CREATE INDEX idx_session_artifacts_created ON session_artifacts(created_at);
287
295
  CREATE VIRTUAL TABLE session_artifacts_fts USING fts5(id UNINDEXED, content);
288
296
 
289
- -- Session stop signals for autonomous stop
290
297
  CREATE TABLE session_stop_signals (
291
298
  session_id TEXT PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE,
292
299
  source TEXT NOT NULL,
@@ -297,7 +304,6 @@ CREATE TABLE session_stop_signals (
297
304
  CREATE INDEX idx_stop_signals_pending ON session_stop_signals(acknowledged_at)
298
305
  WHERE acknowledged_at IS NULL;
299
306
 
300
- -- Loop progress for autonomous progress tracking
301
307
  CREATE TABLE loop_progress (
302
308
  id INTEGER PRIMARY KEY AUTOINCREMENT,
303
309
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
@@ -311,7 +317,6 @@ CREATE INDEX idx_loop_progress_session ON loop_progress(session_id, recorded_at
311
317
  CREATE INDEX idx_loop_progress_high_value ON loop_progress(session_id, is_high_value, recorded_at DESC)
312
318
  WHERE is_high_value = 1;
313
319
 
314
- -- Tasks
315
320
  CREATE TABLE tasks (
316
321
  id TEXT PRIMARY KEY,
317
322
  project_id TEXT NOT NULL REFERENCES projects(id),
@@ -374,7 +379,6 @@ CREATE INDEX idx_tasks_closed_session ON tasks(closed_in_session_id);
374
379
  CREATE UNIQUE INDEX idx_tasks_seq_num ON tasks(project_id, seq_num);
375
380
  CREATE INDEX idx_tasks_path_cache ON tasks(path_cache);
376
381
 
377
- -- Task dependencies
378
382
  CREATE TABLE task_dependencies (
379
383
  id INTEGER PRIMARY KEY AUTOINCREMENT,
380
384
  task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
@@ -386,7 +390,6 @@ CREATE TABLE task_dependencies (
386
390
  CREATE INDEX idx_deps_task ON task_dependencies(task_id);
387
391
  CREATE INDEX idx_deps_depends_on ON task_dependencies(depends_on);
388
392
 
389
- -- Session-task linkages
390
393
  CREATE TABLE session_tasks (
391
394
  id INTEGER PRIMARY KEY AUTOINCREMENT,
392
395
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
@@ -398,7 +401,6 @@ CREATE TABLE session_tasks (
398
401
  CREATE INDEX idx_session_tasks_session ON session_tasks(session_id);
399
402
  CREATE INDEX idx_session_tasks_task ON session_tasks(task_id);
400
403
 
401
- -- Task validation history
402
404
  CREATE TABLE task_validation_history (
403
405
  id INTEGER PRIMARY KEY AUTOINCREMENT,
404
406
  task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
@@ -413,7 +415,6 @@ CREATE TABLE task_validation_history (
413
415
  );
414
416
  CREATE INDEX idx_validation_history_task ON task_validation_history(task_id);
415
417
 
416
- -- Task selection history for stuck detection
417
418
  CREATE TABLE task_selection_history (
418
419
  id INTEGER PRIMARY KEY AUTOINCREMENT,
419
420
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
@@ -424,7 +425,6 @@ CREATE TABLE task_selection_history (
424
425
  CREATE INDEX idx_task_selection_session ON task_selection_history(session_id, selected_at DESC);
425
426
  CREATE INDEX idx_task_selection_task ON task_selection_history(session_id, task_id, selected_at DESC);
426
427
 
427
- -- Workflow states
428
428
  CREATE TABLE workflow_states (
429
429
  session_id TEXT PRIMARY KEY,
430
430
  workflow_name TEXT NOT NULL,
@@ -445,7 +445,6 @@ CREATE TABLE workflow_states (
445
445
  FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
446
446
  );
447
447
 
448
- -- Workflow audit log
449
448
  CREATE TABLE workflow_audit_log (
450
449
  id INTEGER PRIMARY KEY AUTOINCREMENT,
451
450
  session_id TEXT NOT NULL,
@@ -465,7 +464,6 @@ CREATE INDEX idx_audit_timestamp ON workflow_audit_log(timestamp);
465
464
  CREATE INDEX idx_audit_event_type ON workflow_audit_log(event_type);
466
465
  CREATE INDEX idx_audit_result ON workflow_audit_log(result);
467
466
 
468
- -- Memories
469
467
  CREATE TABLE memories (
470
468
  id TEXT PRIMARY KEY,
471
469
  project_id TEXT REFERENCES projects(id),
@@ -485,7 +483,6 @@ CREATE INDEX idx_memories_project ON memories(project_id);
485
483
  CREATE INDEX idx_memories_type ON memories(memory_type);
486
484
  CREATE INDEX idx_memories_importance ON memories(importance DESC);
487
485
 
488
- -- Session-memory linkages
489
486
  CREATE TABLE session_memories (
490
487
  id INTEGER PRIMARY KEY AUTOINCREMENT,
491
488
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
@@ -497,7 +494,6 @@ CREATE TABLE session_memories (
497
494
  CREATE INDEX idx_session_memories_session ON session_memories(session_id);
498
495
  CREATE INDEX idx_session_memories_memory ON session_memories(memory_id);
499
496
 
500
- -- Memory cross-references
501
497
  CREATE TABLE memory_crossrefs (
502
498
  source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
503
499
  target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
@@ -509,7 +505,6 @@ CREATE INDEX idx_crossrefs_source ON memory_crossrefs(source_id);
509
505
  CREATE INDEX idx_crossrefs_target ON memory_crossrefs(target_id);
510
506
  CREATE INDEX idx_crossrefs_similarity ON memory_crossrefs(similarity DESC);
511
507
 
512
- -- Worktrees
513
508
  CREATE TABLE worktrees (
514
509
  id TEXT PRIMARY KEY,
515
510
  project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
@@ -531,7 +526,6 @@ CREATE INDEX idx_worktrees_session ON worktrees(agent_session_id);
531
526
  CREATE UNIQUE INDEX idx_worktrees_branch ON worktrees(project_id, branch_name);
532
527
  CREATE UNIQUE INDEX idx_worktrees_path ON worktrees(worktree_path);
533
528
 
534
- -- Merge resolutions
535
529
  CREATE TABLE merge_resolutions (
536
530
  id TEXT PRIMARY KEY,
537
531
  worktree_id TEXT NOT NULL REFERENCES worktrees(id) ON DELETE CASCADE,
@@ -547,7 +541,6 @@ CREATE INDEX idx_merge_resolutions_status ON merge_resolutions(status);
547
541
  CREATE INDEX idx_merge_resolutions_source_branch ON merge_resolutions(source_branch);
548
542
  CREATE INDEX idx_merge_resolutions_target_branch ON merge_resolutions(target_branch);
549
543
 
550
- -- Merge conflicts
551
544
  CREATE TABLE merge_conflicts (
552
545
  id TEXT PRIMARY KEY,
553
546
  resolution_id TEXT NOT NULL REFERENCES merge_resolutions(id) ON DELETE CASCADE,
@@ -562,353 +555,167 @@ CREATE TABLE merge_conflicts (
562
555
  CREATE INDEX idx_merge_conflicts_resolution ON merge_conflicts(resolution_id);
563
556
  CREATE INDEX idx_merge_conflicts_file_path ON merge_conflicts(file_path);
564
557
  CREATE INDEX idx_merge_conflicts_status ON merge_conflicts(status);
565
- """
566
-
567
- # Future migrations (v61+)
568
- # Add new migrations here. Do not modify the baseline schema above.
569
-
570
-
571
- def _migrate_test_strategy_to_category(db: LocalDatabase) -> None:
572
- """Rename test_strategy column to category if it exists.
573
-
574
- This is a no-op for fresh databases that already have category in the baseline schema.
575
- Only runs the rename for databases upgraded from versions before the rename.
576
- """
577
- # Check if test_strategy column exists
578
- row = db.fetchone("SELECT sql FROM sqlite_master WHERE type='table' AND name='tasks'")
579
- if row and "test_strategy" in row["sql"].lower():
580
- db.execute("ALTER TABLE tasks RENAME COLUMN test_strategy TO category")
581
- logger.info("Renamed test_strategy column to category")
582
- else:
583
- logger.debug("test_strategy column not found (fresh database), skipping rename")
584
-
585
-
586
- def _migrate_add_agent_name(db: LocalDatabase) -> None:
587
- """Add agent_name column to tasks table for agent configuration."""
588
- # Check if agent_name column already exists (fresh database)
589
- row = db.fetchone("SELECT sql FROM sqlite_master WHERE type='table' AND name='tasks'")
590
- if row and "agent_name" not in row["sql"].lower():
591
- db.execute("ALTER TABLE tasks ADD COLUMN agent_name TEXT")
592
- logger.info("Added agent_name column to tasks table")
593
- else:
594
- logger.debug("agent_name column already exists, skipping")
595
-
596
-
597
- def _migrate_add_reference_doc(db: LocalDatabase) -> None:
598
- """Add reference_doc column to tasks table for spec traceability."""
599
- # Check if reference_doc column already exists (fresh database)
600
- row = db.fetchone("SELECT sql FROM sqlite_master WHERE type='table' AND name='tasks'")
601
- if row and "reference_doc" not in row["sql"].lower():
602
- db.execute("ALTER TABLE tasks ADD COLUMN reference_doc TEXT")
603
- logger.info("Added reference_doc column to tasks table")
604
- else:
605
- logger.debug("reference_doc column already exists, skipping")
606
-
607
-
608
- def _migrate_add_boolean_columns(db: LocalDatabase) -> None:
609
- """Add is_enriched and is_expanded columns to tasks table.
610
-
611
- Note: is_tdd_applied was previously added here but is now deprecated and
612
- removed in migration 74. This migration only adds is_enriched and is_expanded.
613
- """
614
- row = db.fetchone("SELECT sql FROM sqlite_master WHERE type='table' AND name='tasks'")
615
- if not row:
616
- return
617
-
618
- sql_lower = row["sql"].lower()
619
-
620
- # Add each column if it doesn't exist
621
- if "is_enriched" not in sql_lower:
622
- db.execute("ALTER TABLE tasks ADD COLUMN is_enriched INTEGER DEFAULT 0")
623
- logger.info("Added is_enriched column to tasks table")
624
-
625
- if "is_expanded" not in sql_lower:
626
- db.execute("ALTER TABLE tasks ADD COLUMN is_expanded INTEGER DEFAULT 0")
627
- logger.info("Added is_expanded column to tasks table")
628
-
629
-
630
- def _migrate_add_review_columns(db: LocalDatabase) -> None:
631
- """Add requires_user_review and accepted_by_user columns for review status support."""
632
- row = db.fetchone("SELECT sql FROM sqlite_master WHERE type='table' AND name='tasks'")
633
- if not row:
634
- return
635
-
636
- sql_lower = row["sql"].lower()
637
-
638
- if "requires_user_review" not in sql_lower:
639
- db.execute("ALTER TABLE tasks ADD COLUMN requires_user_review INTEGER DEFAULT 0")
640
- logger.info("Added requires_user_review column to tasks table")
641
-
642
- if "accepted_by_user" not in sql_lower:
643
- db.execute("ALTER TABLE tasks ADD COLUMN accepted_by_user INTEGER DEFAULT 0")
644
- logger.info("Added accepted_by_user column to tasks table")
645
558
 
559
+ CREATE TABLE inter_session_messages (
560
+ id TEXT PRIMARY KEY,
561
+ from_session TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
562
+ to_session TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
563
+ content TEXT NOT NULL,
564
+ priority TEXT NOT NULL DEFAULT 'normal',
565
+ sent_at TEXT NOT NULL,
566
+ read_at TEXT
567
+ );
568
+ CREATE INDEX idx_inter_session_messages_from_session ON inter_session_messages(from_session);
569
+ CREATE INDEX idx_inter_session_messages_to_session ON inter_session_messages(to_session);
570
+ CREATE INDEX idx_inter_session_messages_unread ON inter_session_messages(to_session, read_at)
571
+ WHERE read_at IS NULL;
646
572
 
647
- def _migrate_drop_is_enriched(db: LocalDatabase) -> None:
648
- """Drop deprecated is_enriched column from tasks table.
573
+ CREATE TABLE skills (
574
+ id TEXT PRIMARY KEY,
575
+ name TEXT NOT NULL,
576
+ description TEXT NOT NULL,
577
+ content TEXT NOT NULL,
578
+ version TEXT,
579
+ license TEXT,
580
+ compatibility TEXT,
581
+ allowed_tools TEXT,
582
+ metadata TEXT,
583
+ source_path TEXT,
584
+ source_type TEXT,
585
+ source_ref TEXT,
586
+ hub_name TEXT,
587
+ hub_slug TEXT,
588
+ hub_version TEXT,
589
+ enabled INTEGER DEFAULT 1,
590
+ project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
591
+ created_at TEXT NOT NULL,
592
+ updated_at TEXT NOT NULL
593
+ );
594
+ CREATE INDEX idx_skills_name ON skills(name);
595
+ CREATE INDEX idx_skills_project_id ON skills(project_id);
596
+ CREATE INDEX idx_skills_enabled ON skills(enabled);
597
+ CREATE UNIQUE INDEX idx_skills_name_project ON skills(name, project_id);
598
+ CREATE UNIQUE INDEX idx_skills_name_global ON skills(name) WHERE project_id IS NULL;
649
599
 
650
- The is_enriched flag is no longer used after the Task Expansion V3 simplification.
651
- SQLite 3.35.0+ supports ALTER TABLE DROP COLUMN.
652
- """
653
- row = db.fetchone("SELECT sql FROM sqlite_master WHERE type='table' AND name='tasks'")
654
- if not row:
655
- return
600
+ CREATE TABLE clones (
601
+ id TEXT PRIMARY KEY,
602
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
603
+ branch_name TEXT NOT NULL,
604
+ clone_path TEXT NOT NULL,
605
+ base_branch TEXT DEFAULT 'main',
606
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
607
+ agent_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
608
+ status TEXT DEFAULT 'active',
609
+ remote_url TEXT,
610
+ last_sync_at TEXT,
611
+ cleanup_after TEXT,
612
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
613
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
614
+ );
615
+ CREATE INDEX idx_clones_project ON clones(project_id);
616
+ CREATE INDEX idx_clones_status ON clones(status);
617
+ CREATE INDEX idx_clones_task ON clones(task_id);
618
+ CREATE INDEX idx_clones_session ON clones(agent_session_id);
619
+ CREATE UNIQUE INDEX idx_clones_path ON clones(clone_path);
620
+ """
656
621
 
657
- if "is_enriched" in row["sql"].lower():
658
- try:
659
- db.execute("ALTER TABLE tasks DROP COLUMN is_enriched")
660
- logger.info("Dropped is_enriched column from tasks table")
661
- except Exception as e:
662
- # SQLite < 3.35.0 doesn't support DROP COLUMN
663
- # Column will remain but be unused - not a problem
664
- logger.warning(f"Could not drop is_enriched column (SQLite < 3.35?): {e}")
622
+ # Future migrations (v61+)
623
+ # Add new migrations here. Do not modify the baseline schema above.
665
624
 
666
625
 
667
- def _migrate_add_inter_session_messages(db: LocalDatabase) -> None:
668
- """Add inter_session_messages table for parent-child agent communication.
626
+ def _migrate_session_seq_num_project_scoped(db: LocalDatabase) -> None:
627
+ """Change sessions.seq_num index from global to project-scoped.
669
628
 
670
- This table enables asynchronous messaging between agent sessions,
671
- allowing parent agents to send instructions to child agents and
672
- receive status updates back.
629
+ This allows different projects to have independent session numbering (#1, #2, etc.)
630
+ matching how tasks already work.
673
631
  """
674
- # Check if table already exists (fresh database with baseline)
632
+ # Check if the old global index exists
675
633
  row = db.fetchone(
676
- "SELECT name FROM sqlite_master WHERE type='table' AND name='inter_session_messages'"
634
+ "SELECT sql FROM sqlite_master WHERE type='index' AND name='idx_sessions_seq_num'"
677
635
  )
678
- if row:
679
- logger.debug("inter_session_messages table already exists, skipping")
636
+ if not row:
637
+ logger.debug("idx_sessions_seq_num index does not exist, skipping")
680
638
  return
681
639
 
682
- # Create the table
683
- db.execute("""
684
- CREATE TABLE inter_session_messages (
685
- id TEXT PRIMARY KEY,
686
- from_session TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
687
- to_session TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
688
- content TEXT NOT NULL,
689
- priority TEXT NOT NULL DEFAULT 'normal',
690
- sent_at TEXT NOT NULL,
691
- read_at TEXT
692
- )
693
- """)
694
-
695
- # Create indexes for efficient querying
696
- db.execute(
697
- "CREATE INDEX idx_inter_session_messages_from_session ON inter_session_messages(from_session)"
698
- )
699
- db.execute(
700
- "CREATE INDEX idx_inter_session_messages_to_session ON inter_session_messages(to_session)"
701
- )
702
- db.execute(
703
- "CREATE INDEX idx_inter_session_messages_unread ON inter_session_messages(to_session, read_at) "
704
- "WHERE read_at IS NULL"
705
- )
706
-
707
- logger.info("Created inter_session_messages table with indexes")
708
-
709
-
710
- def _migrate_add_media_column(db: LocalDatabase) -> None:
711
- """Add media column to memories table for multimodal support."""
712
- # Check if media column already exists (fresh database from baseline)
713
- row = db.fetchone("SELECT sql FROM sqlite_master WHERE type='table' AND name='memories'")
714
- if row and "media" not in row["sql"].lower():
715
- db.execute("ALTER TABLE memories ADD COLUMN media TEXT")
716
- logger.info("Added media column to memories table")
717
- else:
718
- logger.debug("media column already exists, skipping")
719
-
720
-
721
- def _migrate_add_expansion_status(db: LocalDatabase) -> None:
722
- """Add expansion_status column to tasks table for skill-based expansion."""
723
- row = db.fetchone("SELECT sql FROM sqlite_master WHERE type='table' AND name='tasks'")
724
- if row and "expansion_status" not in row["sql"].lower():
725
- db.execute("ALTER TABLE tasks ADD COLUMN expansion_status TEXT DEFAULT 'none'")
726
- logger.info("Added expansion_status column to tasks table")
727
- else:
728
- logger.debug("expansion_status column already exists, skipping")
729
-
730
-
731
- def _migrate_add_skills_table(db: LocalDatabase) -> None:
732
- """Add skills table for Agent Skills spec compliant skill storage.
733
-
734
- Skills provide structured instructions for AI agents following the
735
- Agent Skills specification (agentskills.io) with Gobby-specific extensions.
736
- """
737
- # Check if table already exists
738
- row = db.fetchone("SELECT name FROM sqlite_master WHERE type='table' AND name='skills'")
739
- if row:
740
- logger.debug("skills table already exists, skipping")
640
+ # Check if it's already project-scoped (contains 'project_id')
641
+ if "project_id" in (row["sql"] or "").lower():
642
+ logger.debug("idx_sessions_seq_num is already project-scoped, skipping")
741
643
  return
742
644
 
743
- # Create the skills table
744
- db.execute("""
745
- CREATE TABLE skills (
746
- id TEXT PRIMARY KEY,
747
- name TEXT NOT NULL,
748
- description TEXT NOT NULL,
749
- content TEXT NOT NULL,
750
- version TEXT,
751
- license TEXT,
752
- compatibility TEXT,
753
- allowed_tools TEXT,
754
- metadata TEXT,
755
- source_path TEXT,
756
- source_type TEXT,
757
- source_ref TEXT,
758
- enabled INTEGER DEFAULT 1,
759
- project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
760
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
761
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
762
- )
763
- """)
764
-
765
- # Create indexes
766
- db.execute("CREATE INDEX idx_skills_name ON skills(name)")
767
- db.execute("CREATE INDEX idx_skills_project_id ON skills(project_id)")
768
- db.execute("CREATE INDEX idx_skills_enabled ON skills(enabled)")
769
- # Unique constraint: name must be unique within a project scope
770
- db.execute("CREATE UNIQUE INDEX idx_skills_name_project ON skills(name, project_id)")
771
- # Partial unique index for global skills (project_id IS NULL)
772
- # This enforces uniqueness for global skill names since NULL != NULL in SQL
773
- db.execute(
774
- "CREATE UNIQUE INDEX idx_skills_name_global ON skills(name) WHERE project_id IS NULL"
775
- )
645
+ # Drop the old global index and create new project-scoped index atomically
646
+ with db.transaction() as conn:
647
+ conn.execute("DROP INDEX IF EXISTS idx_sessions_seq_num")
648
+ conn.execute("CREATE UNIQUE INDEX idx_sessions_seq_num ON sessions(project_id, seq_num)")
776
649
 
777
- logger.info("Created skills table with indexes")
650
+ logger.info("Changed sessions.seq_num index from global to project-scoped")
778
651
 
779
652
 
780
- def _migrate_add_skills_global_unique_index(db: LocalDatabase) -> None:
781
- """Add partial unique index for global skills (project_id IS NULL).
653
+ def _migrate_backfill_session_seq_num_per_project(db: LocalDatabase) -> None:
654
+ """Re-backfill session seq_num values to be per-project.
782
655
 
783
- This enforces uniqueness for global skill names since NULL != NULL in SQL.
784
- The existing idx_skills_name_project only enforces uniqueness within a project scope.
656
+ This migration re-numbers sessions so each project has independent numbering
657
+ starting from 1. Required after changing the index to be project-scoped.
785
658
  """
786
- # Check if index already exists (fresh database from v70+)
787
- row = db.fetchone(
788
- "SELECT name FROM sqlite_master WHERE type='index' AND name='idx_skills_name_global'"
789
- )
790
- if row:
791
- logger.debug("idx_skills_name_global index already exists, skipping")
792
- return
793
-
794
- # Check if skills table exists (might not if on old version that never created it)
795
- row = db.fetchone("SELECT name FROM sqlite_master WHERE type='table' AND name='skills'")
796
- if not row:
797
- logger.debug("skills table does not exist, skipping")
798
- return
799
-
800
- db.execute(
801
- "CREATE UNIQUE INDEX idx_skills_name_global ON skills(name) WHERE project_id IS NULL"
659
+ # Get all sessions grouped by project, ordered by created_at
660
+ sessions = db.fetchall(
661
+ """
662
+ SELECT id, project_id FROM sessions
663
+ ORDER BY project_id, created_at ASC, id ASC
664
+ """
802
665
  )
803
- logger.debug("Added idx_skills_name_global partial unique index to skills table")
804
666
 
805
-
806
- def _migrate_add_clones_table(db: LocalDatabase) -> None:
807
- """Add clones table for local git clone management.
808
-
809
- Clones are full repository copies, distinct from worktrees which share
810
- a single .git directory. This enables parallel development across machines
811
- or isolated environments.
812
- """
813
- # Check if table already exists
814
- row = db.fetchone("SELECT name FROM sqlite_master WHERE type='table' AND name='clones'")
815
- if row:
816
- logger.debug("clones table already exists, skipping")
667
+ if not sessions:
668
+ logger.debug("No sessions to re-number")
817
669
  return
818
670
 
819
- # Create the clones table
820
- db.execute("""
821
- CREATE TABLE clones (
822
- id TEXT PRIMARY KEY,
823
- project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
824
- branch_name TEXT NOT NULL,
825
- clone_path TEXT NOT NULL,
826
- base_branch TEXT DEFAULT 'main',
827
- task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
828
- agent_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
829
- status TEXT DEFAULT 'active',
830
- remote_url TEXT,
831
- last_sync_at TEXT,
832
- cleanup_after TEXT,
833
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
834
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
835
- )
836
- """)
837
-
838
- # Create indexes
839
- db.execute("CREATE INDEX idx_clones_project ON clones(project_id)")
840
- db.execute("CREATE INDEX idx_clones_status ON clones(status)")
841
- db.execute("CREATE INDEX idx_clones_task ON clones(task_id)")
842
- db.execute("CREATE INDEX idx_clones_session ON clones(agent_session_id)")
843
- db.execute("CREATE UNIQUE INDEX idx_clones_path ON clones(clone_path)")
671
+ # Wrap the entire re-numbering in a transaction for atomicity
672
+ with db.transaction() as conn:
673
+ # First, clear all seq_num values to avoid unique constraint violations
674
+ # when the existing seq_num order doesn't match created_at order
675
+ conn.execute("UPDATE sessions SET seq_num = NULL")
844
676
 
845
- logger.debug("Created clones table with indexes")
677
+ # Assign seq_num per project
678
+ current_project: str | None = None
679
+ seq_num = 0
680
+ updated = 0
846
681
 
682
+ for session in sessions:
683
+ if session["project_id"] != current_project:
684
+ current_project = session["project_id"]
685
+ seq_num = 1
686
+ else:
687
+ seq_num += 1
847
688
 
848
- def _migrate_add_model_column(db: LocalDatabase) -> None:
849
- """Add model column to sessions table for cost tracking by model.
689
+ conn.execute(
690
+ "UPDATE sessions SET seq_num = ? WHERE id = ?",
691
+ (seq_num, session["id"]),
692
+ )
693
+ updated += 1
850
694
 
851
- This enables the TokenTracker to aggregate usage by model and apply
852
- model-specific pricing for budget tracking.
853
- """
854
- row = db.fetchone("SELECT sql FROM sqlite_master WHERE type='table' AND name='sessions'")
855
- if row and "model" not in row["sql"].lower():
856
- db.execute("ALTER TABLE sessions ADD COLUMN model TEXT")
857
- logger.info("Added model column to sessions table")
858
- else:
859
- logger.debug("model column already exists, skipping")
695
+ logger.info(f"Re-numbered {updated} sessions with per-project seq_num")
860
696
 
861
697
 
862
- def _migrate_drop_is_tdd_applied(db: LocalDatabase) -> None:
863
- """Drop deprecated is_tdd_applied column from tasks table.
698
+ def _migrate_add_hub_tracking_to_skills(db: LocalDatabase) -> None:
699
+ """Add hub tracking fields to skills table.
864
700
 
865
- The is_tdd_applied flag is no longer used after removing the TDD sandwich pattern.
866
- TDD instructions are now embedded directly in task descriptions for code/config categories.
867
- SQLite 3.35.0+ supports ALTER TABLE DROP COLUMN.
701
+ Adds hub_name, hub_slug, and hub_version columns to track which hub
702
+ a skill was installed from.
868
703
  """
869
- row = db.fetchone("SELECT sql FROM sqlite_master WHERE type='table' AND name='tasks'")
870
- if not row:
871
- return
704
+ with db.transaction() as conn:
705
+ conn.execute("ALTER TABLE skills ADD COLUMN hub_name TEXT")
706
+ conn.execute("ALTER TABLE skills ADD COLUMN hub_slug TEXT")
707
+ conn.execute("ALTER TABLE skills ADD COLUMN hub_version TEXT")
872
708
 
873
- if "is_tdd_applied" in row["sql"].lower():
874
- try:
875
- db.execute("ALTER TABLE tasks DROP COLUMN is_tdd_applied")
876
- logger.info("Dropped is_tdd_applied column from tasks table")
877
- except Exception as e:
878
- # SQLite < 3.35.0 doesn't support DROP COLUMN
879
- # Column will remain but be unused - not a problem
880
- logger.warning(f"Could not drop is_tdd_applied column (SQLite < 3.35?): {e}")
709
+ logger.info("Added hub tracking fields to skills table")
881
710
 
882
711
 
883
712
  MIGRATIONS: list[tuple[int, str, MigrationAction]] = [
884
- # TDD Expansion Restructure: Rename test_strategy to category
885
- (61, "Rename test_strategy to category", _migrate_test_strategy_to_category),
886
- # TDD Expansion Restructure: Add agent_name column
887
- (62, "Add agent_name column to tasks", _migrate_add_agent_name),
888
- # TDD Expansion Restructure: Add reference_doc column
889
- (63, "Add reference_doc column to tasks", _migrate_add_reference_doc),
890
- # TDD Expansion Restructure: Add boolean columns for idempotent operations
891
- (64, "Add boolean columns to tasks", _migrate_add_boolean_columns),
892
- # Review status: Add columns for HITL review workflow
893
- (65, "Add review columns to tasks", _migrate_add_review_columns),
894
- # Task Expansion V3: Drop unused is_enriched column
895
- (66, "Drop is_enriched column from tasks", _migrate_drop_is_enriched),
896
- # Inter-session messaging: Add table for parent-child agent communication
897
- (67, "Add inter_session_messages table", _migrate_add_inter_session_messages),
898
- # Memory V3 Phase 2: Add media column for multimodal support
899
- (68, "Add media column to memories", _migrate_add_media_column),
900
- # Skill-based expansion: Add expansion_status column to tasks
901
- (69, "Add expansion_status column to tasks", _migrate_add_expansion_status),
902
- # Skills storage: Add skills table for Agent Skills spec
903
- (70, "Add skills table", _migrate_add_skills_table),
904
- # Skills: Add partial unique index for global skills
905
- (71, "Add global skills unique index", _migrate_add_skills_global_unique_index),
906
- # Local clones: Add table for git clone management
907
- (72, "Add clones table", _migrate_add_clones_table),
908
- # Token tracking: Add model column to sessions for cost tracking by model
909
- (73, "Add model column to sessions", _migrate_add_model_column),
910
- # TDD cleanup: Drop unused is_tdd_applied column from tasks
911
- (74, "Drop is_tdd_applied column from tasks", _migrate_drop_is_tdd_applied),
713
+ # Project-scoped session refs: Change seq_num index from global to project-scoped
714
+ (76, "Make sessions.seq_num project-scoped", _migrate_session_seq_num_project_scoped),
715
+ # Project-scoped session refs: Re-backfill seq_num per project
716
+ (77, "Backfill sessions.seq_num per project", _migrate_backfill_session_seq_num_per_project),
717
+ # Hub tracking: Add hub_name, hub_slug, hub_version to skills table
718
+ (78, "Add hub tracking fields to skills", _migrate_add_hub_tracking_to_skills),
912
719
  ]
913
720
 
914
721
 
@@ -922,8 +729,8 @@ def get_current_version(db: LocalDatabase) -> int:
922
729
 
923
730
 
924
731
  def _apply_baseline(db: LocalDatabase) -> None:
925
- """Apply baseline schema for new databases."""
926
- logger.info("Applying baseline schema (v60)")
732
+ """Apply baseline schema for new databases (flattened at v75)."""
733
+ logger.info("Applying baseline schema (v75)")
927
734
 
928
735
  # Execute baseline schema
929
736
  for statement in BASELINE_SCHEMA.strip().split(";"):
@@ -995,13 +802,10 @@ def run_migrations(db: LocalDatabase) -> int:
995
802
  Run pending migrations.
996
803
 
997
804
  For new databases (version == 0):
998
- Applies baseline schema directly, jumping to version 60.
999
-
1000
- For existing databases (0 < version < 60):
1001
- Imports and runs legacy migrations incrementally.
805
+ - Applies baseline schema (v75) directly.
1002
806
 
1003
- For all databases:
1004
- Runs any new migrations (v61+) after baseline/legacy path.
807
+ For existing databases:
808
+ - Runs any new migrations from v76 onwards.
1005
809
 
1006
810
  Args:
1007
811
  db: LocalDatabase instance
@@ -1013,20 +817,22 @@ def run_migrations(db: LocalDatabase) -> int:
1013
817
  total_applied = 0
1014
818
 
1015
819
  if current_version == 0:
1016
- # New database: apply baseline schema directly
820
+ # New database with flattened baseline: apply schema directly
821
+ logger.info("Using flattened baseline for new database")
1017
822
  _apply_baseline(db)
1018
823
  total_applied = 1
1019
824
  current_version = BASELINE_VERSION
1020
825
  elif current_version < BASELINE_VERSION:
1021
- # Existing database needing legacy migrations
1022
- # Lazy import to avoid loading legacy code for new databases
1023
- from gobby.storage.migrations_legacy import LEGACY_MIGRATIONS
1024
-
1025
- applied = _run_migration_list(db, current_version, LEGACY_MIGRATIONS)
1026
- total_applied += applied
1027
- current_version = get_current_version(db)
826
+ # Unsupported: Pre-v75 database without local migrations
827
+ # Since we removed legacy migrations, we can't upgrade.
828
+ msg = (
829
+ f"Database version {current_version} is older than baseline "
830
+ f"{BASELINE_VERSION}. Upgrade not supported without legacy migrations."
831
+ )
832
+ logger.error(msg)
833
+ raise MigrationUnsupportedError(msg)
1028
834
 
1029
- # Run any new migrations (v61+)
835
+ # Run any new migrations (v76+)
1030
836
  if MIGRATIONS:
1031
837
  applied = _run_migration_list(db, current_version, MIGRATIONS)
1032
838
  total_applied += applied