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
gobby/cli/memory.py CHANGED
@@ -282,6 +282,191 @@ def export_memories(
282
282
  click.echo(markdown)
283
283
 
284
284
 
285
+ @memory.command("dedupe")
286
+ @click.option("--dry-run", is_flag=True, help="Show duplicates without deleting")
287
+ @click.pass_context
288
+ def dedupe_memories(ctx: click.Context, dry_run: bool) -> None:
289
+ """Remove duplicate memories (same content, different IDs).
290
+
291
+ Identifies memories with identical content but different IDs (caused by
292
+ project_id variations) and removes duplicates, keeping the earliest one.
293
+
294
+ Examples:
295
+
296
+ gobby memory dedupe --dry-run # Preview duplicates
297
+
298
+ gobby memory dedupe # Remove duplicates
299
+ """
300
+ manager = get_memory_manager(ctx)
301
+
302
+ # Get all memories
303
+ memories = manager.list_memories(limit=10000)
304
+
305
+ if not memories:
306
+ click.echo("No memories found.")
307
+ return
308
+
309
+ # Group by normalized content
310
+ content_groups: dict[str, list[tuple[str, str, str | None]]] = {}
311
+ for m in memories:
312
+ normalized = m.content.strip()
313
+ if normalized not in content_groups:
314
+ content_groups[normalized] = []
315
+ content_groups[normalized].append((m.id, m.created_at, m.project_id))
316
+
317
+ # Find duplicates
318
+ duplicates_to_delete: list[str] = []
319
+ duplicate_count = 0
320
+
321
+ for content, entries in content_groups.items():
322
+ if len(entries) > 1:
323
+ duplicate_count += len(entries) - 1
324
+ # Sort by created_at to keep earliest
325
+ entries.sort(key=lambda x: x[1])
326
+ keeper = entries[0]
327
+ to_delete = entries[1:]
328
+
329
+ if dry_run:
330
+ click.echo(f"\nDuplicate content ({len(entries)} copies):")
331
+ click.echo(f" Content: {content[:80]}{'...' if len(content) > 80 else ''}")
332
+ click.echo(f" Keep: {keeper[0][:12]} (created: {keeper[1][:19]})")
333
+ for d in to_delete:
334
+ click.echo(f" Delete: {d[0][:12]} (created: {d[1][:19]}, project: {d[2]})")
335
+ else:
336
+ for d in to_delete:
337
+ duplicates_to_delete.append(d[0])
338
+
339
+ if dry_run:
340
+ click.echo(f"\nFound {duplicate_count} duplicate memories.")
341
+ click.echo("Run without --dry-run to delete them.")
342
+ else:
343
+ # Delete duplicates
344
+ deleted = 0
345
+ for memory_id in duplicates_to_delete:
346
+ if manager.forget(memory_id):
347
+ deleted += 1
348
+
349
+ click.echo(f"Deleted {deleted} duplicate memories.")
350
+
351
+
352
+ @memory.command("fix-null-project")
353
+ @click.option("--dry-run", is_flag=True, help="Show affected memories without updating")
354
+ @click.pass_context
355
+ def fix_null_project(ctx: click.Context, dry_run: bool) -> None:
356
+ """Fix memories with NULL project_id from their source session.
357
+
358
+ Finds memories with source_type='session' and NULL project_id, then
359
+ looks up the source session to get the correct project_id.
360
+
361
+ Examples:
362
+
363
+ gobby memory fix-null-project --dry-run # Preview changes
364
+
365
+ gobby memory fix-null-project # Apply fixes
366
+ """
367
+ from gobby.storage.sessions import LocalSessionManager
368
+
369
+ db = LocalDatabase()
370
+ session_mgr = LocalSessionManager(db)
371
+
372
+ # Find memories with NULL project_id and session source
373
+ rows = db.fetchall(
374
+ """
375
+ SELECT id, content, source_session_id
376
+ FROM memories
377
+ WHERE project_id IS NULL AND source_type = 'session' AND source_session_id IS NOT NULL
378
+ """,
379
+ (),
380
+ )
381
+
382
+ if not rows:
383
+ click.echo("No memories with NULL project_id from sessions found.")
384
+ return
385
+
386
+ click.echo(f"Found {len(rows)} memories with NULL project_id from sessions.")
387
+
388
+ fixed = 0
389
+ for row in rows:
390
+ memory_id = row["id"]
391
+ session_id = row["source_session_id"]
392
+ content_preview = row["content"][:50] if row["content"] else ""
393
+
394
+ # Look up session to get project_id
395
+ session = session_mgr.get(session_id)
396
+ if session and session.project_id:
397
+ if dry_run:
398
+ click.echo(
399
+ f" Would fix {memory_id[:12]}: set project_id={session.project_id[:12]}"
400
+ )
401
+ click.echo(f" Content: {content_preview}...")
402
+ else:
403
+ # Update the memory's project_id
404
+ with db.transaction() as conn:
405
+ conn.execute(
406
+ "UPDATE memories SET project_id = ? WHERE id = ?",
407
+ (session.project_id, memory_id),
408
+ )
409
+ fixed += 1
410
+ else:
411
+ if dry_run:
412
+ click.echo(
413
+ f" Cannot fix {memory_id[:12]}: session {session_id} not found or has no project_id"
414
+ )
415
+
416
+ if dry_run:
417
+ click.echo(f"\nWould fix {fixed} memories. Run without --dry-run to apply.")
418
+ else:
419
+ click.echo(f"Fixed {fixed} memories with project_id from their source sessions.")
420
+
421
+
422
+ @memory.command("backup")
423
+ @click.option(
424
+ "--output",
425
+ "-o",
426
+ "output_path",
427
+ type=click.Path(),
428
+ help="Output file path (default: .gobby/memories.jsonl)",
429
+ )
430
+ @click.pass_context
431
+ def backup_memories(ctx: click.Context, output_path: str | None) -> None:
432
+ """Backup memories to JSONL file.
433
+
434
+ Exports all memories to a JSONL file for backup/disaster recovery.
435
+ This runs synchronously and can be used even when the daemon is not running.
436
+
437
+ Examples:
438
+
439
+ gobby memory backup # Export to .gobby/memories.jsonl
440
+
441
+ gobby memory backup -o ~/backups/mem.jsonl # Export to custom path
442
+ """
443
+ from pathlib import Path
444
+
445
+ from gobby.config.persistence import MemorySyncConfig
446
+ from gobby.sync.memories import MemoryBackupManager
447
+
448
+ manager = get_memory_manager(ctx)
449
+
450
+ # Create a backup manager with custom or default path
451
+ if output_path:
452
+ export_path = Path(output_path)
453
+ else:
454
+ export_path = Path(".gobby/memories.jsonl")
455
+
456
+ config = MemorySyncConfig(enabled=True, export_path=export_path)
457
+ backup_mgr = MemoryBackupManager(
458
+ db=manager.db,
459
+ memory_manager=manager,
460
+ config=config,
461
+ )
462
+
463
+ count = backup_mgr.backup_sync()
464
+ if count > 0:
465
+ click.echo(f"Backed up {count} memories to {export_path}")
466
+ else:
467
+ click.echo("No memories to backup.")
468
+
469
+
285
470
  def resolve_memory_id(manager: MemoryManager, memory_ref: str) -> str:
286
471
  """Resolve memory reference (UUID or prefix) to full ID."""
287
472
  # Try exact match first
gobby/cli/sessions.py CHANGED
@@ -387,7 +387,7 @@ def create_handoff(
387
387
  import time
388
388
  from pathlib import Path
389
389
 
390
- from gobby.mcp_proxy.tools.session_messages import _format_handoff_markdown
390
+ from gobby.mcp_proxy.tools.sessions._handoff import _format_handoff_markdown
391
391
  from gobby.sessions.analyzer import TranscriptAnalyzer
392
392
 
393
393
  manager = get_session_manager()
gobby/cli/utils.py CHANGED
@@ -118,7 +118,7 @@ def get_active_session_id(db: LocalDatabase | None = None) -> str | None:
118
118
  db.close()
119
119
 
120
120
 
121
- def resolve_session_id(session_ref: str | None) -> str:
121
+ def resolve_session_id(session_ref: str | None, project_id: str | None = None) -> str:
122
122
  """
123
123
  Resolve session reference to UUID.
124
124
 
@@ -126,6 +126,8 @@ def resolve_session_id(session_ref: str | None) -> str:
126
126
 
127
127
  Args:
128
128
  session_ref: User input string (UUID, #N, N, prefix) or None
129
+ project_id: Project ID for project-scoped #N lookup.
130
+ If not provided, auto-detected from current project context.
129
131
 
130
132
  Returns:
131
133
  Resolved UUID string
@@ -142,10 +144,15 @@ def resolve_session_id(session_ref: str | None) -> str:
142
144
  raise click.ClickException("No active session found. Specify --session.")
143
145
  return active_id
144
146
 
147
+ # Get project_id from context if not provided
148
+ if not project_id:
149
+ ctx = get_project_context()
150
+ project_id = ctx.get("id") if ctx else None
151
+
145
152
  # Use SessionManager for resolution logic
146
153
  manager = LocalSessionManager(db)
147
154
  try:
148
- return manager.resolve_session_reference(session_ref)
155
+ return manager.resolve_session_reference(session_ref, project_id)
149
156
  except ValueError as e:
150
157
  raise click.ClickException(str(e)) from None
151
158
  finally:
gobby/clones/git.py CHANGED
@@ -231,6 +231,89 @@ class CloneGitManager:
231
231
  error=str(e),
232
232
  )
233
233
 
234
+ def full_clone(
235
+ self,
236
+ remote_url: str,
237
+ clone_path: str | Path,
238
+ branch: str = "main",
239
+ ) -> GitOperationResult:
240
+ """
241
+ Create a full (non-shallow) clone of a repository.
242
+
243
+ Args:
244
+ remote_url: URL of the remote repository (HTTPS or SSH)
245
+ clone_path: Path where clone will be created
246
+ branch: Branch to clone
247
+
248
+ Returns:
249
+ GitOperationResult with success status and message
250
+ """
251
+ clone_path = Path(clone_path)
252
+
253
+ # Check if path already exists
254
+ if clone_path.exists():
255
+ return GitOperationResult(
256
+ success=False,
257
+ message=f"Path already exists: {clone_path}",
258
+ )
259
+
260
+ # Ensure parent directory exists
261
+ clone_path.parent.mkdir(parents=True, exist_ok=True)
262
+
263
+ try:
264
+ # Build clone command without --depth (full clone)
265
+ cmd = [
266
+ "git",
267
+ "clone",
268
+ "-b",
269
+ branch,
270
+ remote_url,
271
+ str(clone_path),
272
+ ]
273
+
274
+ # Sanitize URL in command before logging to avoid exposing credentials
275
+ safe_cmd = cmd.copy()
276
+ safe_cmd[safe_cmd.index(remote_url)] = _sanitize_url(remote_url)
277
+ logger.debug(f"Running: {' '.join(safe_cmd)}")
278
+
279
+ result = subprocess.run( # nosec B603 B607 - cmd built from hardcoded git arguments
280
+ cmd,
281
+ capture_output=True,
282
+ text=True,
283
+ timeout=600, # 10 minutes for full clone
284
+ )
285
+
286
+ if result.returncode == 0:
287
+ return GitOperationResult(
288
+ success=True,
289
+ message=f"Successfully cloned to {clone_path}",
290
+ output=result.stdout,
291
+ )
292
+ else:
293
+ return GitOperationResult(
294
+ success=False,
295
+ message=f"Clone failed: {result.stderr}",
296
+ error=result.stderr,
297
+ )
298
+
299
+ except subprocess.TimeoutExpired:
300
+ # Clean up partial clone
301
+ if clone_path.exists():
302
+ shutil.rmtree(clone_path, ignore_errors=True)
303
+ return GitOperationResult(
304
+ success=False,
305
+ message="Git clone timed out",
306
+ )
307
+ except Exception as e:
308
+ # Clean up partial clone
309
+ if clone_path.exists():
310
+ shutil.rmtree(clone_path, ignore_errors=True)
311
+ return GitOperationResult(
312
+ success=False,
313
+ message=f"Error cloning repository: {e}",
314
+ error=str(e),
315
+ )
316
+
234
317
  def sync_clone(
235
318
  self,
236
319
  clone_path: str | Path,
@@ -422,6 +505,100 @@ class CloneGitManager:
422
505
  logger.error(f"Error getting clone status: {e}")
423
506
  return None
424
507
 
508
+ def create_clone(
509
+ self,
510
+ clone_path: str | Path,
511
+ branch_name: str,
512
+ base_branch: str = "main",
513
+ shallow: bool = True,
514
+ ) -> GitOperationResult:
515
+ """
516
+ Create a clone for isolated work.
517
+
518
+ This is the high-level API used by CloneIsolationHandler.
519
+ It gets the remote URL from the current repository and creates
520
+ either a shallow or full clone at the specified path.
521
+
522
+ Args:
523
+ clone_path: Path where clone will be created
524
+ branch_name: Branch to create/checkout in the clone
525
+ base_branch: Base branch to clone from (default: main)
526
+ shallow: Whether to create a shallow clone (default: True)
527
+
528
+ Returns:
529
+ GitOperationResult with success status and message
530
+ """
531
+ # Get remote URL from current repo
532
+ remote_url = self.get_remote_url()
533
+ if not remote_url:
534
+ return GitOperationResult(
535
+ success=False,
536
+ message="Could not get remote URL from repository",
537
+ error="no_remote_url",
538
+ )
539
+
540
+ # Create clone (shallow or full based on parameter)
541
+ if shallow:
542
+ result = self.shallow_clone(
543
+ remote_url=remote_url,
544
+ clone_path=clone_path,
545
+ branch=base_branch,
546
+ depth=1,
547
+ )
548
+ else:
549
+ result = self.full_clone(
550
+ remote_url=remote_url,
551
+ clone_path=clone_path,
552
+ branch=base_branch,
553
+ )
554
+
555
+ if not result.success:
556
+ return result
557
+
558
+ # If branch_name differs from base_branch, create and checkout the new branch
559
+ if branch_name != base_branch:
560
+ try:
561
+ # Create new branch from base
562
+ create_result = self._run_git(
563
+ ["checkout", "-b", branch_name],
564
+ cwd=clone_path,
565
+ timeout=30,
566
+ )
567
+ if create_result.returncode != 0:
568
+ # Clean up the clone on branch creation failure
569
+ try:
570
+ if Path(clone_path).exists():
571
+ shutil.rmtree(clone_path)
572
+ except Exception as cleanup_err:
573
+ logger.warning(
574
+ f"Failed to clean up clone after branch creation failure: {cleanup_err}"
575
+ )
576
+ return GitOperationResult(
577
+ success=False,
578
+ message=f"Failed to create branch {branch_name}: {create_result.stderr}",
579
+ error=create_result.stderr,
580
+ )
581
+ except Exception as e:
582
+ # Clean up the clone on exception
583
+ try:
584
+ if Path(clone_path).exists():
585
+ shutil.rmtree(clone_path)
586
+ except Exception as cleanup_err:
587
+ logger.warning(
588
+ f"Failed to clean up clone after branch creation error: {cleanup_err}"
589
+ )
590
+ return GitOperationResult(
591
+ success=False,
592
+ message=f"Error creating branch: {e}",
593
+ error=str(e),
594
+ )
595
+
596
+ return GitOperationResult(
597
+ success=True,
598
+ message=f"Successfully created clone at {clone_path} on branch {branch_name}",
599
+ output=result.output,
600
+ )
601
+
425
602
  def merge_branch(
426
603
  self,
427
604
  source_branch: str,
gobby/config/__init__.py CHANGED
@@ -14,116 +14,31 @@ Module structure:
14
14
  - extensions.py: Hook extension configs (webhooks, plugins)
15
15
  - sessions.py: Session lifecycle and tracking configs
16
16
  - features.py: MCP proxy feature configs (code execution, tool recommendation)
17
+
18
+ Import from submodules directly for specific configs:
19
+ from gobby.config.tasks import TaskValidationConfig
20
+ from gobby.config.extensions import WebhooksConfig
21
+
22
+ Import from this package for app-level items:
23
+ from gobby.config import DaemonConfig, load_config
17
24
  """
18
25
 
19
26
  # Core configuration and utilities from app.py
20
27
  from gobby.config.app import (
21
28
  DaemonConfig,
22
29
  expand_env_vars,
30
+ generate_default_config,
23
31
  load_config,
32
+ load_yaml,
24
33
  save_config,
25
34
  )
26
35
 
27
- # Extension configs
28
- from gobby.config.extensions import (
29
- HookExtensionsConfig,
30
- PluginItemConfig,
31
- PluginsConfig,
32
- WebhookEndpointConfig,
33
- WebhooksConfig,
34
- WebSocketBroadcastConfig,
35
- )
36
-
37
- # Feature configs
38
- from gobby.config.features import (
39
- ImportMCPServerConfig,
40
- MetricsConfig,
41
- ProjectVerificationConfig,
42
- RecommendToolsConfig,
43
- ToolSummarizerConfig,
44
- )
45
-
46
- # LLM provider configs
47
- from gobby.config.llm_providers import (
48
- LLMProviderConfig,
49
- LLMProvidersConfig,
50
- )
51
-
52
- # Logging configs
53
- from gobby.config.logging import LoggingSettings
54
-
55
- # Persistence configs
56
- from gobby.config.persistence import (
57
- MemoryConfig,
58
- MemorySyncConfig,
59
- )
60
-
61
- # Server configs
62
- from gobby.config.servers import (
63
- MCPClientProxyConfig,
64
- WebSocketSettings,
65
- )
66
-
67
- # Session configs
68
- from gobby.config.sessions import (
69
- ContextInjectionConfig,
70
- MessageTrackingConfig,
71
- SessionLifecycleConfig,
72
- SessionSummaryConfig,
73
- TitleSynthesisConfig,
74
- )
75
-
76
- # Task configs
77
- from gobby.config.tasks import (
78
- CompactHandoffConfig,
79
- GobbyTasksConfig,
80
- PatternCriteriaConfig,
81
- TaskExpansionConfig,
82
- TaskValidationConfig,
83
- WorkflowConfig,
84
- )
85
-
86
36
  __all__ = [
87
- # Core
37
+ # Core app-level exports only
88
38
  "DaemonConfig",
89
39
  "expand_env_vars",
40
+ "generate_default_config",
90
41
  "load_config",
42
+ "load_yaml",
91
43
  "save_config",
92
- # Extension configs
93
- "HookExtensionsConfig",
94
- "PluginItemConfig",
95
- "PluginsConfig",
96
- "WebhookEndpointConfig",
97
- "WebhooksConfig",
98
- "WebSocketBroadcastConfig",
99
- # Feature configs
100
- "ImportMCPServerConfig",
101
- "MetricsConfig",
102
- "ProjectVerificationConfig",
103
- "RecommendToolsConfig",
104
- "ToolSummarizerConfig",
105
- # LLM provider configs
106
- "LLMProviderConfig",
107
- "LLMProvidersConfig",
108
- # Logging configs
109
- "LoggingSettings",
110
- # Persistence configs
111
- "MemoryConfig",
112
- "MemorySyncConfig",
113
- # Server configs
114
- "MCPClientProxyConfig",
115
- "WebSocketSettings",
116
- # Session configs
117
- "ContextInjectionConfig",
118
- "MessageTrackingConfig",
119
- "SessionLifecycleConfig",
120
- "SessionSummaryConfig",
121
- "TitleSynthesisConfig",
122
- # Task configs
123
- "CompactHandoffConfig",
124
- "GobbyTasksConfig",
125
- "PatternCriteriaConfig",
126
- "TaskExpansionConfig",
127
- "TaskValidationConfig",
128
- "WorkflowConfig",
129
44
  ]
gobby/config/app.py CHANGED
@@ -15,14 +15,8 @@ from typing import Any
15
15
  import yaml
16
16
  from pydantic import BaseModel, Field, field_validator
17
17
 
18
- from gobby.config.extensions import (
19
- HookExtensionsConfig,
20
- PluginItemConfig,
21
- PluginsConfig,
22
- WebhookEndpointConfig,
23
- WebhooksConfig,
24
- WebSocketBroadcastConfig,
25
- )
18
+ # Internal imports for DaemonConfig fields - NOT re-exported
19
+ from gobby.config.extensions import HookExtensionsConfig
26
20
  from gobby.config.features import (
27
21
  ImportMCPServerConfig,
28
22
  MetricsConfig,
@@ -31,14 +25,9 @@ from gobby.config.features import (
31
25
  TaskDescriptionConfig,
32
26
  ToolSummarizerConfig,
33
27
  )
34
-
35
- # Re-export from extracted modules (Strangler Fig pattern for backwards compatibility)
36
- from gobby.config.llm_providers import LLMProviderConfig, LLMProvidersConfig
28
+ from gobby.config.llm_providers import LLMProvidersConfig
37
29
  from gobby.config.logging import LoggingSettings
38
- from gobby.config.persistence import (
39
- MemoryConfig,
40
- MemorySyncConfig,
41
- )
30
+ from gobby.config.persistence import MemoryConfig, MemorySyncConfig
42
31
  from gobby.config.search import SearchConfig
43
32
  from gobby.config.servers import MCPClientProxyConfig, WebSocketSettings
44
33
  from gobby.config.sessions import (
@@ -50,61 +39,10 @@ from gobby.config.sessions import (
50
39
  TitleSynthesisConfig,
51
40
  )
52
41
  from gobby.config.skills import SkillsConfig
53
- from gobby.config.tasks import (
54
- CompactHandoffConfig,
55
- GobbyTasksConfig,
56
- PatternCriteriaConfig,
57
- TaskExpansionConfig,
58
- TaskValidationConfig,
59
- WorkflowConfig,
60
- )
42
+ from gobby.config.tasks import CompactHandoffConfig, GobbyTasksConfig, WorkflowConfig
61
43
 
62
- # Explicit exports for mypy (re-exported symbols from submodules)
63
44
  __all__ = [
64
- # From gobby.config.extensions
65
- "HookExtensionsConfig",
66
- "PluginItemConfig",
67
- "PluginsConfig",
68
- "WebhookEndpointConfig",
69
- "WebhooksConfig",
70
- "WebSocketBroadcastConfig",
71
- # From gobby.config.features
72
- "ImportMCPServerConfig",
73
- "MetricsConfig",
74
- "ProjectVerificationConfig",
75
- "RecommendToolsConfig",
76
- "TaskDescriptionConfig",
77
- "ToolSummarizerConfig",
78
- # From gobby.config.llm_providers
79
- "LLMProviderConfig",
80
- "LLMProvidersConfig",
81
- # From gobby.config.logging
82
- "LoggingSettings",
83
- # From gobby.config.persistence
84
- "MemoryConfig",
85
- "MemorySyncConfig",
86
- # From gobby.config.search
87
- "SearchConfig",
88
- # From gobby.config.servers
89
- "MCPClientProxyConfig",
90
- "WebSocketSettings",
91
- # From gobby.config.skills
92
- "SkillsConfig",
93
- # From gobby.config.sessions
94
- "ArtifactHandoffConfig",
95
- "ContextInjectionConfig",
96
- "MessageTrackingConfig",
97
- "SessionLifecycleConfig",
98
- "SessionSummaryConfig",
99
- "TitleSynthesisConfig",
100
- # From gobby.config.tasks
101
- "CompactHandoffConfig",
102
- "GobbyTasksConfig",
103
- "PatternCriteriaConfig",
104
- "TaskExpansionConfig",
105
- "TaskValidationConfig",
106
- "WorkflowConfig",
107
- # Local definitions
45
+ # Local definitions only - no re-exports
108
46
  "ConductorConfig",
109
47
  "DaemonConfig",
110
48
  "expand_env_vars",
@@ -184,32 +122,6 @@ def expand_env_vars(content: str) -> str:
184
122
  return ENV_VAR_PATTERN.sub(replace_match, content)
185
123
 
186
124
 
187
- # WebSocketSettings moved to gobby.config.servers (re-exported above)
188
- # LoggingSettings moved to gobby.config.logging (re-exported above)
189
- # CompactHandoffConfig moved to gobby.config.tasks (re-exported above)
190
-
191
- # ContextInjectionConfig, SessionSummaryConfig, TitleSynthesisConfig,
192
- # MessageTrackingConfig, SessionLifecycleConfig
193
- # moved to gobby.config.sessions (re-exported above)
194
-
195
- # ToolSummarizerConfig, RecommendToolsConfig, ImportMCPServerConfig,
196
- # MetricsConfig, ProjectVerificationConfig
197
- # moved to gobby.config.features (re-exported above)
198
-
199
- # WebSocketBroadcastConfig, WebhookEndpointConfig, WebhooksConfig,
200
- # PluginItemConfig, PluginsConfig, HookExtensionsConfig
201
- # moved to gobby.config.extensions (re-exported above)
202
-
203
- # PatternCriteriaConfig, TaskExpansionConfig, TaskValidationConfig, WorkflowConfig,
204
- # GobbyTasksConfig, CompactHandoffConfig
205
- # moved to gobby.config.tasks (re-exported above)
206
-
207
- # MCPClientProxyConfig moved to gobby.config.servers (re-exported above)
208
- # LLMProviderConfig and LLMProvidersConfig moved to gobby.config.llm_providers (re-exported above)
209
- # MemoryConfig, MemorySyncConfig
210
- # moved to gobby.config.persistence (re-exported above)
211
-
212
-
213
125
  class DaemonConfig(BaseModel):
214
126
  """
215
127
  Main configuration for Gobby daemon.
@@ -243,6 +155,10 @@ class DaemonConfig(BaseModel):
243
155
  default="~/.gobby/gobby-hub.db",
244
156
  description="Path to hub database for cross-project queries.",
245
157
  )
158
+ use_flattened_baseline: bool = Field(
159
+ default=True,
160
+ description="Use flattened V2 baseline schema (v75) for new databases instead of legacy migrations.",
161
+ )
246
162
 
247
163
  # Sub-configs
248
164
  websocket: WebSocketSettings = Field(