gobby 0.2.5__py3-none-any.whl → 0.2.7__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 (244) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +13 -4
  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/agents/definitions.py +11 -1
  10. gobby/agents/isolation.py +395 -0
  11. gobby/agents/runner.py +8 -0
  12. gobby/agents/sandbox.py +261 -0
  13. gobby/agents/spawn.py +42 -287
  14. gobby/agents/spawn_executor.py +385 -0
  15. gobby/agents/spawners/__init__.py +24 -0
  16. gobby/agents/spawners/command_builder.py +189 -0
  17. gobby/agents/spawners/embedded.py +21 -2
  18. gobby/agents/spawners/headless.py +21 -2
  19. gobby/agents/spawners/prompt_manager.py +125 -0
  20. gobby/cli/__init__.py +6 -0
  21. gobby/cli/clones.py +419 -0
  22. gobby/cli/conductor.py +266 -0
  23. gobby/cli/install.py +4 -4
  24. gobby/cli/installers/antigravity.py +3 -9
  25. gobby/cli/installers/claude.py +15 -9
  26. gobby/cli/installers/codex.py +2 -8
  27. gobby/cli/installers/gemini.py +8 -8
  28. gobby/cli/installers/shared.py +175 -13
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/skills.py +858 -0
  31. gobby/cli/tasks/ai.py +0 -440
  32. gobby/cli/tasks/crud.py +44 -6
  33. gobby/cli/tasks/main.py +0 -4
  34. gobby/cli/tui.py +2 -2
  35. gobby/cli/utils.py +12 -5
  36. gobby/clones/__init__.py +13 -0
  37. gobby/clones/git.py +547 -0
  38. gobby/conductor/__init__.py +16 -0
  39. gobby/conductor/alerts.py +135 -0
  40. gobby/conductor/loop.py +164 -0
  41. gobby/conductor/monitors/__init__.py +11 -0
  42. gobby/conductor/monitors/agents.py +116 -0
  43. gobby/conductor/monitors/tasks.py +155 -0
  44. gobby/conductor/pricing.py +234 -0
  45. gobby/conductor/token_tracker.py +160 -0
  46. gobby/config/__init__.py +12 -97
  47. gobby/config/app.py +69 -91
  48. gobby/config/extensions.py +2 -2
  49. gobby/config/features.py +7 -130
  50. gobby/config/search.py +110 -0
  51. gobby/config/servers.py +1 -1
  52. gobby/config/skills.py +43 -0
  53. gobby/config/tasks.py +9 -41
  54. gobby/hooks/__init__.py +0 -13
  55. gobby/hooks/event_handlers.py +188 -2
  56. gobby/hooks/hook_manager.py +50 -4
  57. gobby/hooks/plugins.py +1 -1
  58. gobby/hooks/skill_manager.py +130 -0
  59. gobby/hooks/webhooks.py +1 -1
  60. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  61. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  62. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  63. gobby/llm/claude.py +22 -34
  64. gobby/llm/claude_executor.py +46 -256
  65. gobby/llm/codex_executor.py +59 -291
  66. gobby/llm/executor.py +21 -0
  67. gobby/llm/gemini.py +134 -110
  68. gobby/llm/litellm_executor.py +143 -6
  69. gobby/llm/resolver.py +98 -35
  70. gobby/mcp_proxy/importer.py +62 -4
  71. gobby/mcp_proxy/instructions.py +56 -0
  72. gobby/mcp_proxy/models.py +15 -0
  73. gobby/mcp_proxy/registries.py +68 -8
  74. gobby/mcp_proxy/server.py +33 -3
  75. gobby/mcp_proxy/services/recommendation.py +43 -11
  76. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  77. gobby/mcp_proxy/stdio.py +2 -1
  78. gobby/mcp_proxy/tools/__init__.py +0 -2
  79. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  80. gobby/mcp_proxy/tools/agents.py +31 -731
  81. gobby/mcp_proxy/tools/clones.py +518 -0
  82. gobby/mcp_proxy/tools/memory.py +3 -26
  83. gobby/mcp_proxy/tools/metrics.py +65 -1
  84. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  85. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  86. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  87. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  88. gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
  89. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  90. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  91. gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
  92. gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
  93. gobby/mcp_proxy/tools/skills/__init__.py +616 -0
  94. gobby/mcp_proxy/tools/spawn_agent.py +417 -0
  95. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  96. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  97. gobby/mcp_proxy/tools/task_sync.py +1 -1
  98. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  99. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  100. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  101. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  102. gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
  103. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  104. gobby/mcp_proxy/tools/workflows.py +1 -1
  105. gobby/mcp_proxy/tools/worktrees.py +0 -338
  106. gobby/memory/backends/__init__.py +6 -1
  107. gobby/memory/backends/mem0.py +6 -1
  108. gobby/memory/extractor.py +477 -0
  109. gobby/memory/ingestion/__init__.py +5 -0
  110. gobby/memory/ingestion/multimodal.py +221 -0
  111. gobby/memory/manager.py +73 -285
  112. gobby/memory/search/__init__.py +10 -0
  113. gobby/memory/search/coordinator.py +248 -0
  114. gobby/memory/services/__init__.py +5 -0
  115. gobby/memory/services/crossref.py +142 -0
  116. gobby/prompts/loader.py +5 -2
  117. gobby/runner.py +37 -16
  118. gobby/search/__init__.py +48 -6
  119. gobby/search/backends/__init__.py +159 -0
  120. gobby/search/backends/embedding.py +225 -0
  121. gobby/search/embeddings.py +238 -0
  122. gobby/search/models.py +148 -0
  123. gobby/search/unified.py +496 -0
  124. gobby/servers/http.py +24 -12
  125. gobby/servers/routes/admin.py +294 -0
  126. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  127. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  128. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  129. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  130. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  131. gobby/servers/routes/mcp/hooks.py +1 -1
  132. gobby/servers/routes/mcp/tools.py +48 -1317
  133. gobby/servers/websocket.py +2 -2
  134. gobby/sessions/analyzer.py +2 -0
  135. gobby/sessions/lifecycle.py +1 -1
  136. gobby/sessions/processor.py +10 -0
  137. gobby/sessions/transcripts/base.py +2 -0
  138. gobby/sessions/transcripts/claude.py +79 -10
  139. gobby/skills/__init__.py +91 -0
  140. gobby/skills/loader.py +685 -0
  141. gobby/skills/manager.py +384 -0
  142. gobby/skills/parser.py +286 -0
  143. gobby/skills/search.py +463 -0
  144. gobby/skills/sync.py +119 -0
  145. gobby/skills/updater.py +385 -0
  146. gobby/skills/validator.py +368 -0
  147. gobby/storage/clones.py +378 -0
  148. gobby/storage/database.py +1 -1
  149. gobby/storage/memories.py +43 -13
  150. gobby/storage/migrations.py +162 -201
  151. gobby/storage/sessions.py +116 -7
  152. gobby/storage/skills.py +782 -0
  153. gobby/storage/tasks/_crud.py +4 -4
  154. gobby/storage/tasks/_lifecycle.py +57 -7
  155. gobby/storage/tasks/_manager.py +14 -5
  156. gobby/storage/tasks/_models.py +8 -3
  157. gobby/sync/memories.py +40 -5
  158. gobby/sync/tasks.py +83 -6
  159. gobby/tasks/__init__.py +1 -2
  160. gobby/tasks/external_validator.py +1 -1
  161. gobby/tasks/validation.py +46 -35
  162. gobby/tools/summarizer.py +91 -10
  163. gobby/tui/api_client.py +4 -7
  164. gobby/tui/app.py +5 -3
  165. gobby/tui/screens/orchestrator.py +1 -2
  166. gobby/tui/screens/tasks.py +2 -4
  167. gobby/tui/ws_client.py +1 -1
  168. gobby/utils/daemon_client.py +2 -2
  169. gobby/utils/project_context.py +2 -3
  170. gobby/utils/status.py +13 -0
  171. gobby/workflows/actions.py +221 -1135
  172. gobby/workflows/artifact_actions.py +31 -0
  173. gobby/workflows/autonomous_actions.py +11 -0
  174. gobby/workflows/context_actions.py +93 -1
  175. gobby/workflows/detection_helpers.py +115 -31
  176. gobby/workflows/enforcement/__init__.py +47 -0
  177. gobby/workflows/enforcement/blocking.py +269 -0
  178. gobby/workflows/enforcement/commit_policy.py +283 -0
  179. gobby/workflows/enforcement/handlers.py +269 -0
  180. gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
  181. gobby/workflows/engine.py +13 -2
  182. gobby/workflows/git_utils.py +106 -0
  183. gobby/workflows/lifecycle_evaluator.py +29 -1
  184. gobby/workflows/llm_actions.py +30 -0
  185. gobby/workflows/loader.py +19 -6
  186. gobby/workflows/mcp_actions.py +20 -1
  187. gobby/workflows/memory_actions.py +154 -0
  188. gobby/workflows/safe_evaluator.py +183 -0
  189. gobby/workflows/session_actions.py +44 -0
  190. gobby/workflows/state_actions.py +60 -1
  191. gobby/workflows/stop_signal_actions.py +55 -0
  192. gobby/workflows/summary_actions.py +111 -1
  193. gobby/workflows/task_sync_actions.py +347 -0
  194. gobby/workflows/todo_actions.py +34 -1
  195. gobby/workflows/webhook_actions.py +185 -0
  196. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
  197. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
  198. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
  199. gobby/adapters/codex.py +0 -1292
  200. gobby/install/claude/commands/gobby/bug.md +0 -51
  201. gobby/install/claude/commands/gobby/chore.md +0 -51
  202. gobby/install/claude/commands/gobby/epic.md +0 -52
  203. gobby/install/claude/commands/gobby/eval.md +0 -235
  204. gobby/install/claude/commands/gobby/feat.md +0 -49
  205. gobby/install/claude/commands/gobby/nit.md +0 -52
  206. gobby/install/claude/commands/gobby/ref.md +0 -52
  207. gobby/install/codex/prompts/forget.md +0 -7
  208. gobby/install/codex/prompts/memories.md +0 -7
  209. gobby/install/codex/prompts/recall.md +0 -7
  210. gobby/install/codex/prompts/remember.md +0 -13
  211. gobby/llm/gemini_executor.py +0 -339
  212. gobby/mcp_proxy/tools/session_messages.py +0 -1056
  213. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  214. gobby/prompts/defaults/expansion/system.md +0 -119
  215. gobby/prompts/defaults/expansion/user.md +0 -48
  216. gobby/prompts/defaults/external_validation/agent.md +0 -72
  217. gobby/prompts/defaults/external_validation/external.md +0 -63
  218. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  219. gobby/prompts/defaults/external_validation/system.md +0 -6
  220. gobby/prompts/defaults/features/import_mcp.md +0 -22
  221. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  222. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  223. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  224. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  225. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  226. gobby/prompts/defaults/features/server_description.md +0 -20
  227. gobby/prompts/defaults/features/server_description_system.md +0 -6
  228. gobby/prompts/defaults/features/task_description.md +0 -31
  229. gobby/prompts/defaults/features/task_description_system.md +0 -6
  230. gobby/prompts/defaults/features/tool_summary.md +0 -17
  231. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  232. gobby/prompts/defaults/research/step.md +0 -58
  233. gobby/prompts/defaults/validation/criteria.md +0 -47
  234. gobby/prompts/defaults/validation/validate.md +0 -38
  235. gobby/storage/migrations_legacy.py +0 -1359
  236. gobby/tasks/context.py +0 -747
  237. gobby/tasks/criteria.py +0 -342
  238. gobby/tasks/expansion.py +0 -626
  239. gobby/tasks/prompts/expand.py +0 -327
  240. gobby/tasks/research.py +0 -421
  241. gobby/tasks/tdd.py +0 -352
  242. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
  243. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
  244. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
gobby/cli/tasks/ai.py CHANGED
@@ -380,340 +380,6 @@ def _generate_criteria_for_all(manager: LocalTaskManager) -> None:
380
380
  )
381
381
 
382
382
 
383
- def _find_unexpanded_epic(manager: LocalTaskManager, root_task_id: str) -> Task | None:
384
- """Depth-first search for first unexpanded epic in the task tree."""
385
- task = manager.get_task(root_task_id)
386
- if not task:
387
- return None
388
-
389
- # Check if this task itself is an unexpanded epic
390
- if task.task_type == "epic" and not task.is_expanded:
391
- return task
392
-
393
- # Search children depth-first
394
- children = manager.list_tasks(parent_task_id=root_task_id, limit=1000)
395
- for child in children:
396
- if child.task_type == "epic":
397
- result = _find_unexpanded_epic(manager, child.id)
398
- if result:
399
- return result
400
-
401
- return None
402
-
403
-
404
- def _count_unexpanded_epics(manager: LocalTaskManager, root_task_id: str) -> int:
405
- """Count unexpanded epics in the task tree."""
406
- count = 0
407
- task = manager.get_task(root_task_id)
408
- if not task:
409
- return 0
410
-
411
- # Count this task if it's an unexpanded epic
412
- if task.task_type == "epic" and not task.is_expanded:
413
- count += 1
414
-
415
- # Count children recursively
416
- children = manager.list_tasks(parent_task_id=root_task_id, limit=1000)
417
- for child in children:
418
- count += _count_unexpanded_epics(manager, child.id)
419
-
420
- return count
421
-
422
-
423
- @click.command("expand")
424
- @click.argument("task_refs", nargs=-1, required=True, metavar="TASKS...")
425
- @click.option("--context", "-c", help="Additional context for expansion")
426
- @click.option(
427
- "--web-research/--no-web-research",
428
- default=False,
429
- help="Enable/disable agentic web research",
430
- )
431
- @click.option(
432
- "--code-context/--no-code-context",
433
- default=True,
434
- help="Enable/disable codebase context gathering",
435
- )
436
- @click.option(
437
- "--cascade", is_flag=True, help="Iteratively expand all child epics (default for epics)"
438
- )
439
- @click.option("--force", "-f", is_flag=True, help="Re-expand already expanded tasks")
440
- @click.option("--project", "-p", "project_name", help="Project name or ID")
441
- def expand_task_cmd(
442
- task_refs: tuple[str, ...],
443
- context: str | None,
444
- web_research: bool,
445
- code_context: bool,
446
- cascade: bool,
447
- force: bool,
448
- project_name: str | None,
449
- ) -> None:
450
- """Expand a task tree. Runs iteratively until complete.
451
-
452
- TASKS can be: #N (e.g., #1, #47), comma-separated (#1,#2,#3), or UUIDs.
453
-
454
- For epics, automatically expands all child epics iteratively (--cascade).
455
- Use --no-cascade to expand only the root task.
456
-
457
- Examples:
458
- gobby tasks expand #42 # Expands #42 and all child epics
459
- gobby tasks expand #42 --force # Re-expand even if already expanded
460
- """
461
- import asyncio
462
-
463
- from gobby.cli.tasks._utils import parse_task_refs
464
- from gobby.cli.utils import get_active_session_id
465
- from gobby.config.app import load_config
466
- from gobby.llm import LLMService
467
- from gobby.storage.task_dependencies import TaskDependencyManager
468
- from gobby.tasks.expansion import TaskExpander
469
- from gobby.tasks.tdd import (
470
- TDD_CATEGORIES,
471
- apply_tdd_sandwich,
472
- build_expansion_context,
473
- should_skip_expansion,
474
- should_skip_tdd,
475
- )
476
- from gobby.tasks.validation import TaskValidator
477
-
478
- # Parse task references
479
- refs = parse_task_refs(task_refs)
480
- if not refs:
481
- click.echo("Error: No task references provided", err=True)
482
- return
483
-
484
- manager = get_task_manager()
485
-
486
- # Resolve all tasks
487
- root_tasks: list[Task] = []
488
- for ref in refs:
489
- task = resolve_task_id(manager, ref)
490
- if not task:
491
- continue
492
- root_tasks.append(task)
493
-
494
- if not root_tasks:
495
- click.echo("No valid tasks to expand.", err=True)
496
- return
497
-
498
- # Initialize services
499
- try:
500
- config = load_config()
501
- if not config.gobby_tasks.expansion.enabled:
502
- click.echo("Error: Task expansion is disabled in config.", err=True)
503
- return
504
-
505
- llm_service = LLMService(config)
506
- expander = TaskExpander(
507
- config.gobby_tasks.expansion, llm_service, manager, mcp_manager=None
508
- )
509
- dep_manager = TaskDependencyManager(manager.db)
510
- validator = TaskValidator(config.gobby_tasks.validation, llm_service)
511
- auto_generate_validation = config.gobby_tasks.validation.auto_generate_on_expand
512
-
513
- except Exception as e:
514
- click.echo(f"Error initializing services: {e}", err=True)
515
- return
516
-
517
- async def _post_expansion_processing(task: Task, subtask_ids: list[str]) -> dict[str, Any]:
518
- """Apply MCP-parity post-expansion processing.
519
-
520
- - Wire parent → subtask blocking dependencies
521
- - Apply TDD sandwich for code/config subtasks (non-epic only)
522
- - Generate validation criteria for subtasks
523
- """
524
- result: dict[str, Any] = {"tdd_applied": False, "validation_generated": 0}
525
-
526
- if not subtask_ids:
527
- return result
528
-
529
- # 1. Wire parent → subtask blocking dependencies
530
- for subtask_id in subtask_ids:
531
- try:
532
- dep_manager.add_dependency(
533
- task_id=task.id, depends_on=subtask_id, dep_type="blocks"
534
- )
535
- except ValueError:
536
- pass # Dependency already exists
537
-
538
- # 2. Apply TDD sandwich (non-epic tasks with code/config subtasks)
539
- if task.task_type != "epic":
540
- impl_task_ids = []
541
- for sid in subtask_ids:
542
- subtask = manager.get_task(sid)
543
- if subtask and subtask.category in TDD_CATEGORIES:
544
- if not should_skip_tdd(subtask.title):
545
- impl_task_ids.append(sid)
546
-
547
- if impl_task_ids:
548
- tdd_result = await apply_tdd_sandwich(manager, dep_manager, task.id, impl_task_ids)
549
- if tdd_result.get("success"):
550
- result["tdd_applied"] = True
551
-
552
- # 3. Generate validation criteria for subtasks
553
- if auto_generate_validation:
554
- for sid in subtask_ids:
555
- subtask = manager.get_task(sid)
556
- if subtask and not subtask.validation_criteria and subtask.task_type != "epic":
557
- try:
558
- criteria = await validator.generate_criteria(
559
- title=subtask.title,
560
- description=subtask.description,
561
- )
562
- if criteria:
563
- manager.update_task(sid, validation_criteria=criteria)
564
- result["validation_generated"] += 1
565
- except Exception:
566
- pass # nosec B110 - best effort validation generation
567
-
568
- # 4. Update parent task: set is_expanded and validation criteria
569
- manager.update_task(
570
- task.id,
571
- is_expanded=True,
572
- validation_criteria="All child tasks must be completed (status: closed).",
573
- )
574
-
575
- return result
576
-
577
- # Get current session ID for expansion context
578
- session_id = get_active_session_id()
579
-
580
- # Process each root task
581
- total_iterations = 0
582
- total_subtasks = 0
583
-
584
- for root_task in root_tasks:
585
- root_ref = f"#{root_task.seq_num}" if root_task.seq_num else root_task.id[:8]
586
-
587
- # For epics, default to cascade mode
588
- should_cascade = cascade or root_task.task_type == "epic"
589
-
590
- if should_cascade:
591
- # Iterative expansion mode
592
- click.echo(f"Expanding {root_ref}: {root_task.title[:50]}...")
593
- if web_research:
594
- click.echo(" • Web research enabled")
595
- if code_context:
596
- click.echo(" • Code context enabled")
597
-
598
- iteration = 0
599
- while True:
600
- iteration += 1
601
-
602
- # Find next unexpanded epic
603
- target = _find_unexpanded_epic(manager, root_task.id)
604
-
605
- if target is None:
606
- click.echo(f"✓ Expansion complete after {iteration - 1} iterations")
607
- break
608
-
609
- # Re-fetch to get latest state
610
- target = manager.get_task(target.id)
611
- if target is None:
612
- click.echo(" Task deleted during expansion", err=True)
613
- break
614
-
615
- # Check if task should be skipped (TDD prefixes or already expanded)
616
- skip, reason = should_skip_expansion(target.title, target.is_expanded, force)
617
- if skip:
618
- target_ref = f"#{target.seq_num}" if target.seq_num else target.id[:8]
619
- click.echo(f" Skipping {target_ref}: {reason}")
620
- continue
621
-
622
- target_ref = f"#{target.seq_num}" if target.seq_num else target.id[:8]
623
- click.echo(f"[{iteration}] Expanding {target_ref}: {target.title[:40]}...")
624
-
625
- # Build merged context from stored expansion_context + user context
626
- merged_context = build_expansion_context(target.expansion_context, context)
627
-
628
- try:
629
- result = asyncio.run(
630
- expander.expand_task(
631
- task_id=target.id,
632
- title=target.title,
633
- description=target.description,
634
- context=merged_context,
635
- enable_web_research=web_research,
636
- enable_code_context=code_context,
637
- session_id=session_id,
638
- )
639
- )
640
- except Exception as e:
641
- click.echo(f" Error: {e}", err=True)
642
- break
643
-
644
- if "error" in result:
645
- click.echo(f" Error: {result['error']}", err=True)
646
- break
647
-
648
- subtasks = result.get("subtask_ids", [])
649
- click.echo(f" → Created {len(subtasks)} subtasks")
650
- total_subtasks += len(subtasks)
651
-
652
- # Apply post-expansion processing (deps, TDD sandwich, validation)
653
- post_result = asyncio.run(_post_expansion_processing(target, subtasks))
654
- if post_result.get("tdd_applied"):
655
- click.echo(" → Applied TDD sandwich")
656
- if post_result.get("validation_generated", 0) > 0:
657
- click.echo(
658
- f" → Generated {post_result['validation_generated']} validation criteria"
659
- )
660
-
661
- remaining = _count_unexpanded_epics(manager, root_task.id)
662
- if remaining > 0:
663
- click.echo(f" → {remaining} epic(s) remaining")
664
-
665
- total_iterations += iteration - 1
666
-
667
- else:
668
- # Single task expansion (non-cascade)
669
- skip, reason = should_skip_expansion(root_task.title, root_task.is_expanded, force)
670
- if skip:
671
- click.echo(f"Skipping {root_ref}: {reason}")
672
- continue
673
-
674
- click.echo(f"Expanding {root_ref}: {root_task.title[:50]}...")
675
-
676
- # Build merged context from stored expansion_context + user context
677
- merged_context = build_expansion_context(root_task.expansion_context, context)
678
-
679
- try:
680
- result = asyncio.run(
681
- expander.expand_task(
682
- task_id=root_task.id,
683
- title=root_task.title,
684
- description=root_task.description,
685
- context=merged_context,
686
- enable_web_research=web_research,
687
- enable_code_context=code_context,
688
- session_id=session_id,
689
- )
690
- )
691
- except Exception as e:
692
- click.echo(f" Error: {e}", err=True)
693
- continue
694
-
695
- if "error" in result:
696
- click.echo(f" Error: {result['error']}", err=True)
697
- continue
698
-
699
- subtasks = result.get("subtask_ids", [])
700
- click.echo(f" Created {len(subtasks)} subtasks")
701
-
702
- # Apply post-expansion processing (deps, TDD sandwich, validation)
703
- post_result = asyncio.run(_post_expansion_processing(root_task, subtasks))
704
- if post_result.get("tdd_applied"):
705
- click.echo(" → Applied TDD sandwich")
706
- if post_result.get("validation_generated", 0) > 0:
707
- click.echo(
708
- f" → Generated {post_result['validation_generated']} validation criteria"
709
- )
710
- total_subtasks += len(subtasks)
711
- total_iterations += 1
712
-
713
- if len(root_tasks) > 1:
714
- click.echo(f"\nTotal: {total_subtasks} subtasks across {total_iterations} expansions")
715
-
716
-
717
383
  @click.command("complexity")
718
384
  @click.argument("task_id", required=False)
719
385
  @click.option("--all", "analyze_all", is_flag=True, help="Analyze all pending tasks")
@@ -829,112 +495,6 @@ def _analyze_task_complexity(manager: LocalTaskManager, task: Task) -> dict[str,
829
495
  }
830
496
 
831
497
 
832
- @click.command("expand-all")
833
- @click.option("--max", "-m", "max_tasks", default=5, help="Maximum tasks to expand")
834
- @click.option("--min-complexity", default=1, help="Only expand tasks with complexity >= this")
835
- @click.option("--type", "task_type", help="Filter by task type")
836
- @click.option("--web-research/--no-web-research", default=False, help="Enable web research")
837
- @click.option("--dry-run", "-d", is_flag=True, help="Show what would be expanded without doing it")
838
- def expand_all_cmd(
839
- max_tasks: int,
840
- min_complexity: int,
841
- task_type: str | None,
842
- web_research: bool,
843
- dry_run: bool,
844
- ) -> None:
845
- """Expand all unexpanded tasks (tasks without subtasks)."""
846
- import asyncio
847
-
848
- from gobby.config.app import load_config
849
- from gobby.llm import LLMService
850
- from gobby.tasks.expansion import TaskExpander
851
-
852
- manager = get_task_manager()
853
-
854
- # Find tasks without children
855
- all_tasks = manager.list_tasks(status="open", task_type=task_type, limit=100)
856
-
857
- unexpanded = []
858
- for t in all_tasks:
859
- children = manager.list_tasks(parent_task_id=t.id, limit=1)
860
- if not children:
861
- if t.complexity_score is None or t.complexity_score >= min_complexity:
862
- unexpanded.append(t)
863
-
864
- to_expand = unexpanded[:max_tasks]
865
-
866
- if not to_expand:
867
- click.echo("No unexpanded tasks found matching criteria.")
868
- return
869
-
870
- if dry_run:
871
- click.echo(f"Would expand {len(to_expand)} tasks:")
872
- for t in to_expand:
873
- score = t.complexity_score or "?"
874
- click.echo(f" {t.id[:12]} | Complexity: {score} | {t.title[:50]}")
875
- return
876
-
877
- # Initialize services
878
- try:
879
- config = load_config()
880
- if not config.gobby_tasks.expansion.enabled:
881
- click.echo("Error: Task expansion is disabled in config.", err=True)
882
- return
883
-
884
- llm_service = LLMService(config)
885
- expander = TaskExpander(
886
- config.gobby_tasks.expansion, llm_service, manager, mcp_manager=None
887
- )
888
- except Exception as e:
889
- click.echo(f"Error initializing services: {e}", err=True)
890
- return
891
-
892
- click.echo(f"Expanding {len(to_expand)} tasks...")
893
-
894
- async def expand_tasks() -> list[dict[str, Any]]:
895
- results = []
896
- for task in to_expand:
897
- click.echo(f"\nExpanding: {task.title[:60]}...")
898
- try:
899
- result = await expander.expand_task(
900
- task_id=task.id,
901
- title=task.title,
902
- description=task.description,
903
- enable_web_research=web_research,
904
- enable_code_context=True,
905
- )
906
- subtask_ids = result.get("subtask_ids", [])
907
- results.append(
908
- {
909
- "task_id": task.id,
910
- "title": task.title,
911
- "subtasks_created": len(subtask_ids),
912
- "status": "success" if not result.get("error") else "error",
913
- "error": result.get("error"),
914
- }
915
- )
916
- if result.get("error"):
917
- click.echo(f" Error: {result['error']}")
918
- else:
919
- click.echo(f" Created {len(subtask_ids)} subtasks")
920
- except Exception as e:
921
- results.append(
922
- {
923
- "task_id": task.id,
924
- "title": task.title,
925
- "status": "error",
926
- "error": str(e),
927
- }
928
- )
929
- click.echo(f" Error: {e}")
930
- return results
931
-
932
- results = asyncio.run(expand_tasks())
933
-
934
- success_count = len([r for r in results if r["status"] == "success"])
935
- click.echo(f"\nExpanded {success_count}/{len(results)} tasks successfully.")
936
-
937
-
938
498
  @click.command("suggest")
939
499
  @click.option("--type", "-t", "task_type", help="Filter by task type")
940
500
  @click.option("--no-prefer-subtasks", is_flag=True, help="Don't prefer leaf tasks over parents")
gobby/cli/tasks/crud.py CHANGED
@@ -320,8 +320,21 @@ def task_stats(project_ref: str | None, json_format: bool) -> None:
320
320
  @click.option("--description", "-d", help="Task description")
321
321
  @click.option("--priority", "-p", type=int, default=2, help="Priority (1=High, 2=Med, 3=Low)")
322
322
  @click.option("--type", "-t", "task_type", default="task", help="Task type")
323
- def create_task(title: str, description: str | None, priority: int, task_type: str) -> None:
324
- """Create a new task."""
323
+ @click.option("--depends-on", "-D", multiple=True, help="Task(s) this task depends on (#N, UUID)")
324
+ def create_task(
325
+ title: str,
326
+ description: str | None,
327
+ priority: int,
328
+ task_type: str,
329
+ depends_on: tuple[str, ...],
330
+ ) -> None:
331
+ """Create a new task.
332
+
333
+ Examples:
334
+ gobby tasks create "Fix bug"
335
+ gobby tasks create "Implement feature" --depends-on "#1"
336
+ gobby tasks create "Final review" -D "#1" -D "#2"
337
+ """
325
338
  project_ctx = get_project_context()
326
339
  if not project_ctx or "id" not in project_ctx:
327
340
  click.echo("Error: Not in a gobby project or project.json missing 'id'.", err=True)
@@ -343,6 +356,22 @@ def create_task(title: str, description: str | None, priority: int, task_type: s
343
356
  else:
344
357
  click.echo(f"Created task {task_ref}: {task.title}")
345
358
 
359
+ # Handle depends_on
360
+ if depends_on:
361
+ from gobby.storage.task_dependencies import TaskDependencyManager
362
+
363
+ dep_manager = TaskDependencyManager(manager.db)
364
+ for blocker_ref in depends_on:
365
+ try:
366
+ blocker = resolve_task_id(manager, blocker_ref)
367
+ if blocker:
368
+ # blocker blocks task (task depends on blocker)
369
+ dep_manager.add_dependency(blocker.id, task.id, "blocks")
370
+ blocker_display = f"#{blocker.seq_num}" if blocker.seq_num else blocker.id[:8]
371
+ click.echo(f" → depends on {blocker_display}")
372
+ except Exception as e:
373
+ click.echo(f" Warning: Could not add dependency on '{blocker_ref}': {e}", err=True)
374
+
346
375
 
347
376
  @click.command("show")
348
377
  @click.argument("task_id", metavar="TASK")
@@ -537,9 +566,12 @@ def reopen_task_cmd(task_id: str, reason: str | None) -> None:
537
566
 
538
567
  @click.command("delete")
539
568
  @click.argument("task_refs", nargs=-1, required=True, metavar="TASKS...")
540
- @click.option("--cascade", "-c", is_flag=True, help="Delete child tasks")
569
+ @click.option("--cascade", "-c", is_flag=True, help="Delete child tasks and dependent tasks")
570
+ @click.option(
571
+ "--unlink", "-u", is_flag=True, help="Remove dependency links but preserve dependent tasks"
572
+ )
541
573
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
542
- def delete_task(task_refs: tuple[str, ...], cascade: bool, yes: bool) -> None:
574
+ def delete_task(task_refs: tuple[str, ...], cascade: bool, unlink: bool, yes: bool) -> None:
543
575
  """Delete one or more tasks.
544
576
 
545
577
  TASKS can be: #N (e.g., #1, #47), comma-separated (#1,#2,#3), or UUIDs.
@@ -549,6 +581,7 @@ def delete_task(task_refs: tuple[str, ...], cascade: bool, yes: bool) -> None:
549
581
  gobby tasks delete #42
550
582
  gobby tasks delete #42,#43,#44 --cascade
551
583
  gobby tasks delete #42 #43 #44 --yes
584
+ gobby tasks delete #42 --unlink
552
585
  """
553
586
  from gobby.cli.tasks._utils import parse_task_refs
554
587
 
@@ -576,13 +609,18 @@ def delete_task(task_refs: tuple[str, ...], cascade: bool, yes: bool) -> None:
576
609
  deleted = 0
577
610
  for ref, resolved in resolved_tasks:
578
611
  try:
579
- manager.delete_task(resolved.id, cascade=cascade)
612
+ manager.delete_task(resolved.id, cascade=cascade, unlink=unlink)
580
613
  click.echo(f"Deleted task {resolved.id}")
581
614
  deleted += 1
582
615
  except ValueError as e:
583
616
  msg = str(e)
584
- if "has children" in msg and "cascade=True" in msg:
617
+ if "has children" in msg:
585
618
  msg = f"Task {ref} has children. Use --cascade to delete with all subtasks."
619
+ elif "dependent task(s)" in msg:
620
+ msg = (
621
+ f"Task {ref} has dependent tasks. "
622
+ f"Use --cascade to delete them, or --unlink to preserve them."
623
+ )
586
624
  click.echo(f"Error: {msg}", err=True)
587
625
 
588
626
  if len(resolved_tasks) > 1:
gobby/cli/tasks/main.py CHANGED
@@ -14,8 +14,6 @@ from gobby.cli.tasks._utils import (
14
14
  )
15
15
  from gobby.cli.tasks.ai import (
16
16
  complexity_cmd,
17
- expand_all_cmd,
18
- expand_task_cmd,
19
17
  generate_criteria_cmd,
20
18
  suggest_cmd,
21
19
  validate_task_cmd,
@@ -65,9 +63,7 @@ tasks.add_command(validation_history_cmd)
65
63
  # Register AI-powered commands from extracted module
66
64
  tasks.add_command(validate_task_cmd)
67
65
  tasks.add_command(generate_criteria_cmd)
68
- tasks.add_command(expand_task_cmd)
69
66
  tasks.add_command(complexity_cmd)
70
- tasks.add_command(expand_all_cmd)
71
67
  tasks.add_command(suggest_cmd)
72
68
 
73
69
  # Register search commands
gobby/cli/tui.py CHANGED
@@ -7,14 +7,14 @@ import click
7
7
  @click.option(
8
8
  "--port",
9
9
  "-p",
10
- default=8765,
10
+ default=60887,
11
11
  help="Daemon HTTP port",
12
12
  show_default=True,
13
13
  )
14
14
  @click.option(
15
15
  "--ws-port",
16
16
  "-w",
17
- default=8766,
17
+ default=60888,
18
18
  help="Daemon WebSocket port",
19
19
  show_default=True,
20
20
  )
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:
@@ -263,7 +270,7 @@ def kill_all_gobby_daemons() -> int:
263
270
 
264
271
  Detection methods:
265
272
  1. Matches gobby.runner (the main daemon process)
266
- 2. Matches processes listening on daemon ports (8765/8766)
273
+ 2. Matches processes listening on daemon ports (60887/60888)
267
274
 
268
275
  Returns:
269
276
  Number of processes killed
@@ -275,8 +282,8 @@ def kill_all_gobby_daemons() -> int:
275
282
  ws_port = config.websocket.port
276
283
  except Exception:
277
284
  # Fallback to defaults if config can't be loaded
278
- http_port = 8765
279
- ws_port = 8766
285
+ http_port = 60887
286
+ ws_port = 60888
280
287
 
281
288
  killed_count = 0
282
289
  current_pid = os.getpid()
@@ -0,0 +1,13 @@
1
+ """Clone management for parallel development.
2
+
3
+ This module provides git clone operations, distinct from worktrees.
4
+ Clones are full repository copies while worktrees share a single .git directory.
5
+ """
6
+
7
+ from gobby.clones.git import CloneGitManager, CloneStatus, GitOperationResult
8
+
9
+ __all__ = [
10
+ "CloneGitManager",
11
+ "CloneStatus",
12
+ "GitOperationResult",
13
+ ]