gobby 0.2.5__py3-none-any.whl → 0.2.6__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/adapters/claude_code.py +13 -4
- gobby/adapters/codex.py +43 -3
- gobby/agents/runner.py +8 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +9 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +2 -8
- gobby/cli/installers/shared.py +71 -8
- 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 +3 -3
- 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/app.py +63 -1
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +6 -14
- gobby/hooks/event_handlers.py +145 -2
- gobby/hooks/hook_manager.py +48 -2
- gobby/hooks/skill_manager.py +130 -0
- 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 +95 -33
- gobby/mcp_proxy/instructions.py +54 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -5
- gobby/mcp_proxy/server.py +33 -3
- 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/clones.py +903 -0
- gobby/mcp_proxy/tools/memory.py +1 -24
- 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/session_messages.py +1 -2
- gobby/mcp_proxy/tools/skills/__init__.py +631 -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 +60 -29
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +5 -0
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/manager.py +11 -2
- gobby/prompts/defaults/handoff/compact.md +63 -0
- gobby/prompts/defaults/handoff/session_end.md +57 -0
- gobby/prompts/defaults/memory/extract.md +61 -0
- 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 +23 -8
- gobby/servers/routes/admin.py +280 -0
- gobby/servers/routes/mcp/tools.py +241 -52
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +64 -5
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +258 -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 +180 -6
- gobby/storage/sessions.py +73 -0
- gobby/storage/skills.py +749 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +41 -6
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +39 -4
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/validation.py +24 -15
- 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/workflows/actions.py +84 -2
- gobby/workflows/context_actions.py +43 -0
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/engine.py +13 -2
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/loader.py +19 -6
- gobby/workflows/memory_actions.py +74 -0
- gobby/workflows/summary_actions.py +17 -0
- gobby/workflows/task_enforcement_actions.py +448 -6
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/METADATA +82 -21
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/RECORD +136 -107
- 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/task_expansion.py +0 -591
- 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.6.dist-info}/WHEEL +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/top_level.txt +0 -0
gobby/adapters/claude_code.py
CHANGED
|
@@ -191,11 +191,17 @@ class ClaudeCodeAdapter(BaseAdapter):
|
|
|
191
191
|
additional_context_parts.append(response.context)
|
|
192
192
|
|
|
193
193
|
# Add session identifiers from metadata
|
|
194
|
+
# Note: "session_id" in metadata is Gobby's internal platform session ID
|
|
195
|
+
# "external_id" in metadata is the CLI's session UUID
|
|
194
196
|
if response.metadata:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
gobby_session_id = response.metadata.get("session_id")
|
|
198
|
+
external_id = response.metadata.get("external_id")
|
|
199
|
+
if gobby_session_id:
|
|
197
200
|
# Build context with all available identifiers
|
|
198
|
-
|
|
201
|
+
# Use clear naming: Gobby Session ID for MCP calls, External ID for transcripts
|
|
202
|
+
context_lines = [f"Gobby Session ID: {gobby_session_id}"]
|
|
203
|
+
if external_id:
|
|
204
|
+
context_lines.append(f"External ID: {external_id}")
|
|
199
205
|
if response.metadata.get("parent_session_id"):
|
|
200
206
|
context_lines.append(
|
|
201
207
|
f"parent_session_id: {response.metadata['parent_session_id']}"
|
|
@@ -227,7 +233,10 @@ class ClaudeCodeAdapter(BaseAdapter):
|
|
|
227
233
|
additional_context_parts.append("\n".join(context_lines))
|
|
228
234
|
|
|
229
235
|
# Build hookSpecificOutput if we have any context to inject
|
|
230
|
-
|
|
236
|
+
# Only include hookSpecificOutput for hook types that Claude Code's schema accepts
|
|
237
|
+
# Valid hookEventName values: PreToolUse, UserPromptSubmit, PostToolUse
|
|
238
|
+
valid_hook_event_names = {"PreToolUse", "UserPromptSubmit", "PostToolUse"}
|
|
239
|
+
if additional_context_parts and hook_event_name in valid_hook_event_names:
|
|
231
240
|
result["hookSpecificOutput"] = {
|
|
232
241
|
"hookEventName": hook_event_name,
|
|
233
242
|
"additionalContext": "\n\n".join(additional_context_parts),
|
gobby/adapters/codex.py
CHANGED
|
@@ -775,6 +775,28 @@ class CodexAdapter(BaseAdapter):
|
|
|
775
775
|
"item/completed": HookEventType.AFTER_TOOL,
|
|
776
776
|
}
|
|
777
777
|
|
|
778
|
+
# Tool name mapping: Codex tool names -> canonical CC-style names
|
|
779
|
+
# Codex uses different tool names - normalize to Claude Code conventions
|
|
780
|
+
# so block_tools rules work across CLIs
|
|
781
|
+
TOOL_MAP: dict[str, str] = {
|
|
782
|
+
# File operations
|
|
783
|
+
"read_file": "Read",
|
|
784
|
+
"ReadFile": "Read",
|
|
785
|
+
"write_file": "Write",
|
|
786
|
+
"WriteFile": "Write",
|
|
787
|
+
"edit_file": "Edit",
|
|
788
|
+
"EditFile": "Edit",
|
|
789
|
+
# Shell
|
|
790
|
+
"run_shell_command": "Bash",
|
|
791
|
+
"RunShellCommand": "Bash",
|
|
792
|
+
"commandExecution": "Bash",
|
|
793
|
+
# Search
|
|
794
|
+
"glob": "Glob",
|
|
795
|
+
"grep": "Grep",
|
|
796
|
+
"GlobTool": "Glob",
|
|
797
|
+
"GrepTool": "Grep",
|
|
798
|
+
}
|
|
799
|
+
|
|
778
800
|
# Item types that represent tool operations
|
|
779
801
|
TOOL_ITEM_TYPES = {"commandExecution", "fileChange", "mcpToolCall"}
|
|
780
802
|
|
|
@@ -814,6 +836,19 @@ class CodexAdapter(BaseAdapter):
|
|
|
814
836
|
self._machine_id = _get_machine_id()
|
|
815
837
|
return self._machine_id
|
|
816
838
|
|
|
839
|
+
def normalize_tool_name(self, codex_tool_name: str) -> str:
|
|
840
|
+
"""Normalize Codex tool name to canonical CC-style format.
|
|
841
|
+
|
|
842
|
+
This ensures block_tools rules work consistently across CLIs.
|
|
843
|
+
|
|
844
|
+
Args:
|
|
845
|
+
codex_tool_name: Tool name from Codex CLI.
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
Normalized tool name (e.g., "Bash", "Read", "Write", "Edit").
|
|
849
|
+
"""
|
|
850
|
+
return self.TOOL_MAP.get(codex_tool_name, codex_tool_name)
|
|
851
|
+
|
|
817
852
|
def attach_to_client(self, codex_client: CodexAppServerClient) -> None:
|
|
818
853
|
"""Attach to an existing CodexAppServerClient for event handling.
|
|
819
854
|
|
|
@@ -876,14 +911,17 @@ class CodexAdapter(BaseAdapter):
|
|
|
876
911
|
thread_id = params.get("threadId", "")
|
|
877
912
|
item_id = params.get("itemId", "")
|
|
878
913
|
|
|
879
|
-
# Determine tool name from method
|
|
914
|
+
# Determine tool name from method and normalize to CC-style
|
|
880
915
|
if "commandExecution" in method:
|
|
881
|
-
|
|
916
|
+
original_tool = "commandExecution"
|
|
917
|
+
tool_name = self.normalize_tool_name(original_tool) # -> "Bash"
|
|
882
918
|
tool_input = params.get("parsedCmd", params.get("command", ""))
|
|
883
919
|
elif "fileChange" in method:
|
|
884
|
-
|
|
920
|
+
original_tool = "fileChange"
|
|
921
|
+
tool_name = "Write" # File changes are writes
|
|
885
922
|
tool_input = params.get("changes", [])
|
|
886
923
|
else:
|
|
924
|
+
original_tool = "unknown"
|
|
887
925
|
tool_name = "unknown"
|
|
888
926
|
tool_input = params
|
|
889
927
|
|
|
@@ -905,6 +943,8 @@ class CodexAdapter(BaseAdapter):
|
|
|
905
943
|
"requires_response": True,
|
|
906
944
|
"item_id": item_id,
|
|
907
945
|
"approval_method": method,
|
|
946
|
+
"original_tool_name": original_tool,
|
|
947
|
+
"normalized_tool_name": tool_name,
|
|
908
948
|
},
|
|
909
949
|
)
|
|
910
950
|
|
gobby/agents/runner.py
CHANGED
|
@@ -607,6 +607,14 @@ class AgentRunner:
|
|
|
607
607
|
else:
|
|
608
608
|
self._session_storage.update_status(child_session.id, "failed")
|
|
609
609
|
|
|
610
|
+
# Persist cost to session storage for budget tracking
|
|
611
|
+
if result.cost_info and result.cost_info.total_cost > 0:
|
|
612
|
+
self._session_storage.add_cost(child_session.id, result.cost_info.total_cost)
|
|
613
|
+
self.logger.debug(
|
|
614
|
+
f"Persisted cost ${result.cost_info.total_cost:.4f} "
|
|
615
|
+
f"for session {child_session.id}"
|
|
616
|
+
)
|
|
617
|
+
|
|
610
618
|
# Remove from in-memory tracking
|
|
611
619
|
self._untrack_running_agent(agent_run.id)
|
|
612
620
|
|
gobby/cli/__init__.py
CHANGED
|
@@ -8,6 +8,8 @@ from gobby.config.app import load_config
|
|
|
8
8
|
|
|
9
9
|
from .agents import agents
|
|
10
10
|
from .artifacts import artifacts
|
|
11
|
+
from .clones import clones
|
|
12
|
+
from .conductor import conductor
|
|
11
13
|
from .daemon import restart, start, status, stop
|
|
12
14
|
from .extensions import hooks, plugins, webhooks
|
|
13
15
|
from .github import github
|
|
@@ -20,6 +22,7 @@ from .memory import memory
|
|
|
20
22
|
from .merge import merge
|
|
21
23
|
from .projects import projects
|
|
22
24
|
from .sessions import sessions
|
|
25
|
+
from .skills import skills
|
|
23
26
|
from .tasks import tasks
|
|
24
27
|
from .tui import ui
|
|
25
28
|
from .workflows import workflows
|
|
@@ -52,6 +55,7 @@ cli.add_command(uninstall)
|
|
|
52
55
|
cli.add_command(tasks)
|
|
53
56
|
cli.add_command(memory)
|
|
54
57
|
cli.add_command(sessions)
|
|
58
|
+
cli.add_command(skills)
|
|
55
59
|
cli.add_command(agents)
|
|
56
60
|
cli.add_command(worktrees)
|
|
57
61
|
cli.add_command(mcp_proxy)
|
|
@@ -61,6 +65,8 @@ cli.add_command(merge)
|
|
|
61
65
|
cli.add_command(artifacts)
|
|
62
66
|
cli.add_command(github)
|
|
63
67
|
cli.add_command(linear)
|
|
68
|
+
cli.add_command(clones)
|
|
69
|
+
cli.add_command(conductor)
|
|
64
70
|
cli.add_command(hooks)
|
|
65
71
|
cli.add_command(plugins)
|
|
66
72
|
cli.add_command(webhooks)
|
gobby/cli/clones.py
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clone management CLI commands.
|
|
3
|
+
|
|
4
|
+
Commands for managing git clones:
|
|
5
|
+
- create: Create a new clone
|
|
6
|
+
- list: List clones
|
|
7
|
+
- spawn: Spawn an agent in a clone
|
|
8
|
+
- sync: Sync a clone with remote
|
|
9
|
+
- merge: Merge clone branch to target
|
|
10
|
+
- delete: Delete a clone
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from gobby.storage.clones import LocalCloneManager
|
|
19
|
+
from gobby.storage.database import LocalDatabase
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_clone_manager() -> LocalCloneManager:
|
|
23
|
+
"""Get initialized clone manager."""
|
|
24
|
+
db = LocalDatabase()
|
|
25
|
+
return LocalCloneManager(db)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_daemon_url() -> str:
|
|
29
|
+
"""Get daemon URL from config."""
|
|
30
|
+
from gobby.config.app import load_config
|
|
31
|
+
|
|
32
|
+
config = load_config()
|
|
33
|
+
return f"http://localhost:{config.daemon_port}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@click.group()
|
|
37
|
+
def clones() -> None:
|
|
38
|
+
"""Manage git clones for parallel development."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@clones.command("list")
|
|
43
|
+
@click.option("--status", "-s", help="Filter by status (active, stale, syncing, cleanup)")
|
|
44
|
+
@click.option("--project", "-p", "project_id", help="Filter by project ID")
|
|
45
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
46
|
+
def list_clones(
|
|
47
|
+
status: str | None,
|
|
48
|
+
project_id: str | None,
|
|
49
|
+
json_format: bool,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""List clones."""
|
|
52
|
+
manager = get_clone_manager()
|
|
53
|
+
|
|
54
|
+
clones_list = manager.list_clones(status=status, project_id=project_id)
|
|
55
|
+
|
|
56
|
+
if json_format:
|
|
57
|
+
click.echo(json.dumps([c.to_dict() for c in clones_list], indent=2, default=str))
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
if not clones_list:
|
|
61
|
+
click.echo("No clones found.")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
click.echo(f"Found {len(clones_list)} clone(s):\n")
|
|
65
|
+
for clone in clones_list:
|
|
66
|
+
status_icon = {
|
|
67
|
+
"active": "●",
|
|
68
|
+
"syncing": "↻",
|
|
69
|
+
"stale": "○",
|
|
70
|
+
"cleanup": "✗",
|
|
71
|
+
}.get(clone.status, "?")
|
|
72
|
+
|
|
73
|
+
session_info = f" (session: {clone.agent_session_id[:8]})" if clone.agent_session_id else ""
|
|
74
|
+
click.echo(
|
|
75
|
+
f"{status_icon} {clone.id} {clone.branch_name:<30} {clone.status:<10}{session_info}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@clones.command("create")
|
|
80
|
+
@click.argument("branch_name")
|
|
81
|
+
@click.argument("clone_path")
|
|
82
|
+
@click.option("--base", "-b", "base_branch", default="main", help="Base branch to clone from")
|
|
83
|
+
@click.option("--task", "-t", "task_id", help="Link clone to a task")
|
|
84
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
85
|
+
def create_clone(
|
|
86
|
+
branch_name: str,
|
|
87
|
+
clone_path: str,
|
|
88
|
+
base_branch: str,
|
|
89
|
+
task_id: str | None,
|
|
90
|
+
json_format: bool,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Create a new clone for parallel development.
|
|
93
|
+
|
|
94
|
+
Examples:
|
|
95
|
+
|
|
96
|
+
gobby clones create feature/my-feature /path/to/clone
|
|
97
|
+
|
|
98
|
+
gobby clones create bugfix/fix-123 /tmp/fix --base develop --task #47
|
|
99
|
+
"""
|
|
100
|
+
daemon_url = get_daemon_url()
|
|
101
|
+
|
|
102
|
+
arguments = {
|
|
103
|
+
"branch_name": branch_name,
|
|
104
|
+
"clone_path": clone_path,
|
|
105
|
+
"base_branch": base_branch,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if task_id:
|
|
109
|
+
arguments["task_id"] = task_id
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
response = httpx.post(
|
|
113
|
+
f"{daemon_url}/mcp/gobby-clones/tools/create_clone",
|
|
114
|
+
json=arguments,
|
|
115
|
+
timeout=300.0, # Clone can take a while
|
|
116
|
+
)
|
|
117
|
+
response.raise_for_status()
|
|
118
|
+
result = response.json()
|
|
119
|
+
except httpx.ConnectError:
|
|
120
|
+
click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
|
|
121
|
+
return
|
|
122
|
+
except httpx.HTTPStatusError as e:
|
|
123
|
+
click.echo(f"HTTP Error {e.response.status_code}: {e.response.text}", err=True)
|
|
124
|
+
return
|
|
125
|
+
except Exception as e:
|
|
126
|
+
click.echo(f"Error: {e}", err=True)
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
if json_format:
|
|
130
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
if result.get("success"):
|
|
134
|
+
clone_info = result.get("clone", {})
|
|
135
|
+
click.echo(f"Created clone: {clone_info.get('id', 'unknown')}")
|
|
136
|
+
click.echo(f" Branch: {clone_info.get('branch_name', 'unknown')}")
|
|
137
|
+
else:
|
|
138
|
+
click.echo(f"Failed to create clone: {result.get('error')}", err=True)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@clones.command("spawn")
|
|
142
|
+
@click.argument("clone_ref")
|
|
143
|
+
@click.argument("prompt")
|
|
144
|
+
@click.option(
|
|
145
|
+
"--parent-session-id",
|
|
146
|
+
"-p",
|
|
147
|
+
"parent_session_id",
|
|
148
|
+
required=True,
|
|
149
|
+
help="Parent session ID (required)",
|
|
150
|
+
)
|
|
151
|
+
@click.option("--mode", "-m", default="terminal", help="Agent mode (terminal, embedded, headless)")
|
|
152
|
+
@click.option("--workflow", "-w", help="Workflow to activate")
|
|
153
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
154
|
+
def spawn_agent(
|
|
155
|
+
clone_ref: str,
|
|
156
|
+
prompt: str,
|
|
157
|
+
parent_session_id: str,
|
|
158
|
+
mode: str,
|
|
159
|
+
workflow: str | None,
|
|
160
|
+
json_format: bool,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Spawn an agent to work in a clone.
|
|
163
|
+
|
|
164
|
+
Examples:
|
|
165
|
+
|
|
166
|
+
gobby clones spawn clone-123 "Fix the authentication bug"
|
|
167
|
+
|
|
168
|
+
gobby clones spawn clone-123 "Implement feature" --mode headless
|
|
169
|
+
"""
|
|
170
|
+
manager = get_clone_manager()
|
|
171
|
+
clone_id = resolve_clone_id(manager, clone_ref)
|
|
172
|
+
|
|
173
|
+
if not clone_id:
|
|
174
|
+
click.echo(f"Clone not found: {clone_ref}", err=True)
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
daemon_url = get_daemon_url()
|
|
178
|
+
|
|
179
|
+
arguments = {
|
|
180
|
+
"clone_id": clone_id,
|
|
181
|
+
"prompt": prompt,
|
|
182
|
+
"parent_session_id": parent_session_id,
|
|
183
|
+
"mode": mode,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if workflow:
|
|
187
|
+
arguments["workflow"] = workflow
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
response = httpx.post(
|
|
191
|
+
f"{daemon_url}/mcp/gobby-clones/tools/spawn_agent_in_clone",
|
|
192
|
+
json=arguments,
|
|
193
|
+
timeout=60.0,
|
|
194
|
+
)
|
|
195
|
+
response.raise_for_status()
|
|
196
|
+
result = response.json()
|
|
197
|
+
except httpx.ConnectError:
|
|
198
|
+
click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
|
|
199
|
+
return
|
|
200
|
+
except httpx.HTTPStatusError as e:
|
|
201
|
+
click.echo(f"HTTP Error {e.response.status_code}: {e.response.text}", err=True)
|
|
202
|
+
return
|
|
203
|
+
except Exception as e:
|
|
204
|
+
click.echo(f"Error: {e}", err=True)
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
if json_format:
|
|
208
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
if result.get("success"):
|
|
212
|
+
session_id = result.get("session_id", "unknown")
|
|
213
|
+
click.echo(f"Spawned agent in clone {clone_id}")
|
|
214
|
+
click.echo(f" Session: {session_id}")
|
|
215
|
+
else:
|
|
216
|
+
click.echo(f"Failed to spawn agent: {result.get('error')}", err=True)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@clones.command("sync")
|
|
220
|
+
@click.argument("clone_ref")
|
|
221
|
+
@click.option("--direction", "-d", default="pull", help="Sync direction (pull, push, both)")
|
|
222
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
223
|
+
def sync_clone(clone_ref: str, direction: str, json_format: bool) -> None:
|
|
224
|
+
"""Sync clone with remote.
|
|
225
|
+
|
|
226
|
+
Examples:
|
|
227
|
+
|
|
228
|
+
gobby clones sync clone-123
|
|
229
|
+
|
|
230
|
+
gobby clones sync clone-123 --direction push
|
|
231
|
+
"""
|
|
232
|
+
manager = get_clone_manager()
|
|
233
|
+
clone_id = resolve_clone_id(manager, clone_ref)
|
|
234
|
+
|
|
235
|
+
if not clone_id:
|
|
236
|
+
click.echo(f"Clone not found: {clone_ref}", err=True)
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
daemon_url = get_daemon_url()
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
response = httpx.post(
|
|
243
|
+
f"{daemon_url}/mcp/gobby-clones/tools/sync_clone",
|
|
244
|
+
json={"clone_id": clone_id, "direction": direction},
|
|
245
|
+
timeout=120.0,
|
|
246
|
+
)
|
|
247
|
+
response.raise_for_status()
|
|
248
|
+
result = response.json()
|
|
249
|
+
except httpx.ConnectError:
|
|
250
|
+
click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
|
|
251
|
+
return
|
|
252
|
+
except httpx.HTTPStatusError as e:
|
|
253
|
+
click.echo(f"HTTP Error {e.response.status_code}: {e.response.text}", err=True)
|
|
254
|
+
return
|
|
255
|
+
except Exception as e:
|
|
256
|
+
click.echo(f"Error: {e}", err=True)
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
if json_format:
|
|
260
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
if result.get("success"):
|
|
264
|
+
click.echo(f"Synced clone {clone_id}")
|
|
265
|
+
else:
|
|
266
|
+
click.echo(f"Failed to sync clone: {result.get('error')}", err=True)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@clones.command("merge")
|
|
270
|
+
@click.argument("clone_ref")
|
|
271
|
+
@click.option("--target", "-t", "target_branch", default="main", help="Target branch to merge into")
|
|
272
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
273
|
+
def merge_clone(clone_ref: str, target_branch: str, json_format: bool) -> None:
|
|
274
|
+
"""Merge clone branch to target branch.
|
|
275
|
+
|
|
276
|
+
Examples:
|
|
277
|
+
|
|
278
|
+
gobby clones merge clone-123
|
|
279
|
+
|
|
280
|
+
gobby clones merge clone-123 --target develop
|
|
281
|
+
"""
|
|
282
|
+
manager = get_clone_manager()
|
|
283
|
+
clone_id = resolve_clone_id(manager, clone_ref)
|
|
284
|
+
|
|
285
|
+
if not clone_id:
|
|
286
|
+
click.echo(f"Clone not found: {clone_ref}", err=True)
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
daemon_url = get_daemon_url()
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
response = httpx.post(
|
|
293
|
+
f"{daemon_url}/mcp/gobby-clones/tools/merge_clone_to_target",
|
|
294
|
+
json={"clone_id": clone_id, "target_branch": target_branch},
|
|
295
|
+
timeout=120.0,
|
|
296
|
+
)
|
|
297
|
+
response.raise_for_status()
|
|
298
|
+
result = response.json()
|
|
299
|
+
except httpx.ConnectError:
|
|
300
|
+
click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
|
|
301
|
+
return
|
|
302
|
+
except httpx.HTTPStatusError as e:
|
|
303
|
+
click.echo(f"HTTP Error {e.response.status_code}: {e.response.text}", err=True)
|
|
304
|
+
return
|
|
305
|
+
except Exception as e:
|
|
306
|
+
click.echo(f"Error: {e}", err=True)
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
if json_format:
|
|
310
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
if result.get("success"):
|
|
314
|
+
click.echo(f"Merged clone {clone_id} to {target_branch}")
|
|
315
|
+
else:
|
|
316
|
+
# Check for merge conflicts
|
|
317
|
+
if result.get("has_conflicts"):
|
|
318
|
+
conflicted = result.get("conflicted_files", [])
|
|
319
|
+
click.echo(f"Merge conflict in {len(conflicted)} file(s):", err=True)
|
|
320
|
+
for f in conflicted:
|
|
321
|
+
click.echo(f" {f}", err=True)
|
|
322
|
+
else:
|
|
323
|
+
click.echo(f"Failed to merge clone: {result.get('error')}", err=True)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@clones.command("delete")
|
|
327
|
+
@click.argument("clone_ref")
|
|
328
|
+
@click.option("--force", "-f", is_flag=True, help="Force delete even if active")
|
|
329
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
330
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
331
|
+
def delete_clone(clone_ref: str, force: bool, yes: bool, json_format: bool) -> None:
|
|
332
|
+
"""Delete a clone.
|
|
333
|
+
|
|
334
|
+
Examples:
|
|
335
|
+
|
|
336
|
+
gobby clones delete clone-123 --yes
|
|
337
|
+
|
|
338
|
+
gobby clones delete clone-123 --force --yes
|
|
339
|
+
"""
|
|
340
|
+
manager = get_clone_manager()
|
|
341
|
+
clone_id = resolve_clone_id(manager, clone_ref)
|
|
342
|
+
|
|
343
|
+
if not clone_id:
|
|
344
|
+
if json_format:
|
|
345
|
+
click.echo(json.dumps({"success": False, "error": f"Clone not found: {clone_ref}"}))
|
|
346
|
+
else:
|
|
347
|
+
click.echo(f"Clone not found: {clone_ref}", err=True)
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
if not yes and not json_format:
|
|
351
|
+
click.confirm("Are you sure you want to delete this clone?", abort=True)
|
|
352
|
+
|
|
353
|
+
daemon_url = get_daemon_url()
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
response = httpx.post(
|
|
357
|
+
f"{daemon_url}/mcp/gobby-clones/tools/delete_clone",
|
|
358
|
+
json={"clone_id": clone_id, "force": force},
|
|
359
|
+
timeout=60.0,
|
|
360
|
+
)
|
|
361
|
+
response.raise_for_status()
|
|
362
|
+
result = response.json()
|
|
363
|
+
except httpx.ConnectError:
|
|
364
|
+
if json_format:
|
|
365
|
+
click.echo(json.dumps({"success": False, "error": "Cannot connect to Gobby daemon"}))
|
|
366
|
+
else:
|
|
367
|
+
click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
|
|
368
|
+
return
|
|
369
|
+
except httpx.HTTPStatusError as e:
|
|
370
|
+
if json_format:
|
|
371
|
+
click.echo(
|
|
372
|
+
json.dumps(
|
|
373
|
+
{
|
|
374
|
+
"success": False,
|
|
375
|
+
"error": f"HTTP Error {e.response.status_code}",
|
|
376
|
+
"detail": e.response.text,
|
|
377
|
+
}
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
else:
|
|
381
|
+
click.echo(f"HTTP Error {e.response.status_code}: {e.response.text}", err=True)
|
|
382
|
+
return
|
|
383
|
+
except Exception as e:
|
|
384
|
+
if json_format:
|
|
385
|
+
click.echo(json.dumps({"success": False, "error": str(e)}))
|
|
386
|
+
else:
|
|
387
|
+
click.echo(f"Error: {e}", err=True)
|
|
388
|
+
return
|
|
389
|
+
|
|
390
|
+
if json_format:
|
|
391
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
if result.get("success"):
|
|
395
|
+
click.echo(f"Deleted clone: {clone_id}")
|
|
396
|
+
else:
|
|
397
|
+
click.echo(f"Failed to delete clone: {result.get('error')}", err=True)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def resolve_clone_id(manager: LocalCloneManager, clone_ref: str) -> str | None:
|
|
401
|
+
"""Resolve clone reference (UUID or prefix) to full ID."""
|
|
402
|
+
# Check for exact match first
|
|
403
|
+
if manager.get(clone_ref):
|
|
404
|
+
return clone_ref
|
|
405
|
+
|
|
406
|
+
# Try prefix match
|
|
407
|
+
all_clones = manager.list_clones()
|
|
408
|
+
matches = [c for c in all_clones if c.id.startswith(clone_ref)]
|
|
409
|
+
|
|
410
|
+
if not matches:
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
if len(matches) > 1:
|
|
414
|
+
click.echo(f"Ambiguous clone reference '{clone_ref}' matches:", err=True)
|
|
415
|
+
for c in matches:
|
|
416
|
+
click.echo(f" {c.id[:8]} ({c.branch_name})", err=True)
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
return matches[0].id
|