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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +13 -4
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +395 -0
- gobby/agents/runner.py +8 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +385 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +15 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +8 -8
- gobby/cli/installers/shared.py +175 -13
- gobby/cli/sessions.py +1 -1
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +12 -5
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +69 -91
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +9 -41
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +188 -2
- gobby/hooks/hook_manager.py +50 -4
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/skill_manager.py +130 -0
- gobby/hooks/webhooks.py +1 -1
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +98 -35
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +56 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -8
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +518 -0
- gobby/mcp_proxy/tools/memory.py +3 -26
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
- gobby/mcp_proxy/tools/skills/__init__.py +616 -0
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -338
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +73 -285
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/runner.py +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +24 -12
- gobby/servers/routes/admin.py +294 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +1 -1
- gobby/servers/routes/mcp/tools.py +48 -1317
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +2 -0
- gobby/sessions/transcripts/claude.py +79 -10
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +286 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +162 -201
- gobby/storage/sessions.py +116 -7
- gobby/storage/skills.py +782 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +57 -7
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +40 -5
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +46 -35
- gobby/tools/summarizer.py +91 -10
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1135
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +93 -1
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +269 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
- gobby/workflows/engine.py +13 -2
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/loader.py +19 -6
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +154 -0
- gobby/workflows/safe_evaluator.py +183 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +111 -1
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1292
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/session_messages.py +0 -1056
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- gobby/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {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
|
-
|
|
324
|
-
|
|
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
|
|
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=
|
|
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=
|
|
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 (
|
|
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 =
|
|
279
|
-
ws_port =
|
|
285
|
+
http_port = 60887
|
|
286
|
+
ws_port = 60888
|
|
280
287
|
|
|
281
288
|
killed_count = 0
|
|
282
289
|
current_pid = os.getpid()
|
gobby/clones/__init__.py
ADDED
|
@@ -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
|
+
]
|