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
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Internal MCP tools for Gobby Clone Management.
|
|
3
|
+
|
|
4
|
+
Exposes functionality for:
|
|
5
|
+
- Creating git clones for isolated development
|
|
6
|
+
- Managing clone lifecycle (get, list, delete)
|
|
7
|
+
- Syncing clones with remote repositories
|
|
8
|
+
|
|
9
|
+
These tools are registered with the InternalToolRegistry and accessed
|
|
10
|
+
via the downstream proxy pattern (call_tool, list_tools, get_tool_schema).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
17
|
+
|
|
18
|
+
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from gobby.clones.git import CloneGitManager
|
|
22
|
+
from gobby.storage.clones import LocalCloneManager
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_clones_registry(
|
|
28
|
+
clone_storage: LocalCloneManager,
|
|
29
|
+
git_manager: CloneGitManager,
|
|
30
|
+
project_id: str,
|
|
31
|
+
) -> InternalToolRegistry:
|
|
32
|
+
"""
|
|
33
|
+
Create the gobby-clones MCP server registry.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
clone_storage: Clone storage manager for CRUD operations
|
|
37
|
+
git_manager: Git manager for clone operations
|
|
38
|
+
project_id: Default project ID for new clones
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
InternalToolRegistry with clone management tools
|
|
42
|
+
"""
|
|
43
|
+
registry = InternalToolRegistry(
|
|
44
|
+
name="gobby-clones",
|
|
45
|
+
description="Git clone management for isolated development",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# ===== create_clone =====
|
|
49
|
+
async def create_clone(
|
|
50
|
+
branch_name: str,
|
|
51
|
+
clone_path: str,
|
|
52
|
+
remote_url: str | None = None,
|
|
53
|
+
task_id: str | None = None,
|
|
54
|
+
base_branch: str = "main",
|
|
55
|
+
depth: int = 1,
|
|
56
|
+
) -> dict[str, Any]:
|
|
57
|
+
"""
|
|
58
|
+
Create a new git clone.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
branch_name: Branch to clone
|
|
62
|
+
clone_path: Path where clone will be created
|
|
63
|
+
remote_url: Remote URL (defaults to origin of parent repo)
|
|
64
|
+
task_id: Optional task ID to link
|
|
65
|
+
base_branch: Base branch for the clone
|
|
66
|
+
depth: Clone depth (default: 1 for shallow)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Dict with clone info or error
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
# Get remote URL if not provided
|
|
73
|
+
if not remote_url:
|
|
74
|
+
remote_url = git_manager.get_remote_url()
|
|
75
|
+
if not remote_url:
|
|
76
|
+
return {
|
|
77
|
+
"success": False,
|
|
78
|
+
"error": "No remote URL provided and could not get from repository",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Create the clone
|
|
82
|
+
result = git_manager.shallow_clone(
|
|
83
|
+
remote_url=remote_url,
|
|
84
|
+
clone_path=clone_path,
|
|
85
|
+
branch=branch_name,
|
|
86
|
+
depth=depth,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if not result.success:
|
|
90
|
+
return {
|
|
91
|
+
"success": False,
|
|
92
|
+
"error": f"Clone failed: {result.error or result.message}",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Store clone record
|
|
96
|
+
clone = clone_storage.create(
|
|
97
|
+
project_id=project_id,
|
|
98
|
+
branch_name=branch_name,
|
|
99
|
+
clone_path=clone_path,
|
|
100
|
+
base_branch=base_branch,
|
|
101
|
+
task_id=task_id,
|
|
102
|
+
remote_url=remote_url,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
"success": True,
|
|
107
|
+
"clone": clone.to_dict(),
|
|
108
|
+
"message": f"Created clone at {clone_path}",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.error(f"Error creating clone: {e}")
|
|
113
|
+
return {
|
|
114
|
+
"success": False,
|
|
115
|
+
"error": str(e),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
registry.register(
|
|
119
|
+
name="create_clone",
|
|
120
|
+
description="Create a new git clone for isolated development",
|
|
121
|
+
input_schema={
|
|
122
|
+
"type": "object",
|
|
123
|
+
"properties": {
|
|
124
|
+
"branch_name": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"description": "Branch to clone",
|
|
127
|
+
},
|
|
128
|
+
"clone_path": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"description": "Path where clone will be created",
|
|
131
|
+
},
|
|
132
|
+
"remote_url": {
|
|
133
|
+
"type": "string",
|
|
134
|
+
"description": "Remote URL (defaults to origin of parent repo)",
|
|
135
|
+
},
|
|
136
|
+
"task_id": {
|
|
137
|
+
"type": "string",
|
|
138
|
+
"description": "Optional task ID to link",
|
|
139
|
+
},
|
|
140
|
+
"base_branch": {
|
|
141
|
+
"type": "string",
|
|
142
|
+
"description": "Base branch for the clone",
|
|
143
|
+
"default": "main",
|
|
144
|
+
},
|
|
145
|
+
"depth": {
|
|
146
|
+
"type": "integer",
|
|
147
|
+
"description": "Clone depth (default: 1 for shallow)",
|
|
148
|
+
"default": 1,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
"required": ["branch_name", "clone_path"],
|
|
152
|
+
},
|
|
153
|
+
func=create_clone,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# ===== get_clone =====
|
|
157
|
+
async def get_clone(clone_id: str) -> dict[str, Any]:
|
|
158
|
+
"""
|
|
159
|
+
Get clone by ID.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
clone_id: Clone ID
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Dict with clone info or error
|
|
166
|
+
"""
|
|
167
|
+
clone = clone_storage.get(clone_id)
|
|
168
|
+
if not clone:
|
|
169
|
+
return {
|
|
170
|
+
"success": False,
|
|
171
|
+
"error": f"Clone not found: {clone_id}",
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"success": True,
|
|
176
|
+
"clone": clone.to_dict(),
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
registry.register(
|
|
180
|
+
name="get_clone",
|
|
181
|
+
description="Get clone by ID",
|
|
182
|
+
input_schema={
|
|
183
|
+
"type": "object",
|
|
184
|
+
"properties": {
|
|
185
|
+
"clone_id": {
|
|
186
|
+
"type": "string",
|
|
187
|
+
"description": "Clone ID",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
"required": ["clone_id"],
|
|
191
|
+
},
|
|
192
|
+
func=get_clone,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# ===== list_clones =====
|
|
196
|
+
async def list_clones(
|
|
197
|
+
status: str | None = None,
|
|
198
|
+
limit: int = 50,
|
|
199
|
+
) -> dict[str, Any]:
|
|
200
|
+
"""
|
|
201
|
+
List clones with optional filters.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
status: Filter by status (active, syncing, stale, cleanup)
|
|
205
|
+
limit: Maximum number of results
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Dict with list of clones
|
|
209
|
+
"""
|
|
210
|
+
clones = clone_storage.list_clones(
|
|
211
|
+
project_id=project_id,
|
|
212
|
+
status=status,
|
|
213
|
+
limit=limit,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
"success": True,
|
|
218
|
+
"clones": [c.to_dict() for c in clones],
|
|
219
|
+
"count": len(clones),
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
registry.register(
|
|
223
|
+
name="list_clones",
|
|
224
|
+
description="List clones with optional status filter",
|
|
225
|
+
input_schema={
|
|
226
|
+
"type": "object",
|
|
227
|
+
"properties": {
|
|
228
|
+
"status": {
|
|
229
|
+
"type": "string",
|
|
230
|
+
"description": "Filter by status (active, syncing, stale, cleanup)",
|
|
231
|
+
"enum": ["active", "syncing", "stale", "cleanup"],
|
|
232
|
+
},
|
|
233
|
+
"limit": {
|
|
234
|
+
"type": "integer",
|
|
235
|
+
"description": "Maximum number of results",
|
|
236
|
+
"default": 50,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
func=list_clones,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# ===== delete_clone =====
|
|
244
|
+
async def delete_clone(
|
|
245
|
+
clone_id: str,
|
|
246
|
+
force: bool = False,
|
|
247
|
+
) -> dict[str, Any]:
|
|
248
|
+
"""
|
|
249
|
+
Delete a clone.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
clone_id: Clone ID to delete
|
|
253
|
+
force: Force deletion even if there are uncommitted changes
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Dict with success status
|
|
257
|
+
"""
|
|
258
|
+
clone = clone_storage.get(clone_id)
|
|
259
|
+
if not clone:
|
|
260
|
+
return {
|
|
261
|
+
"success": False,
|
|
262
|
+
"error": f"Clone not found: {clone_id}",
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# Store clone info for potential rollback
|
|
266
|
+
clone_path = clone.clone_path
|
|
267
|
+
|
|
268
|
+
# Delete the database record first (can be rolled back more easily)
|
|
269
|
+
try:
|
|
270
|
+
clone_storage.delete(clone_id)
|
|
271
|
+
except Exception as e:
|
|
272
|
+
logger.error(f"Failed to delete clone record {clone_id}: {e}")
|
|
273
|
+
return {
|
|
274
|
+
"success": False,
|
|
275
|
+
"error": f"Failed to delete clone record: {e}",
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# Delete the files
|
|
279
|
+
result = git_manager.delete_clone(clone_path, force=force)
|
|
280
|
+
if not result.success:
|
|
281
|
+
# Rollback: recreate the clone record since file deletion failed
|
|
282
|
+
logger.error(
|
|
283
|
+
f"Failed to delete clone files for {clone_id}, "
|
|
284
|
+
f"attempting to restore record: {result.error or result.message}"
|
|
285
|
+
)
|
|
286
|
+
try:
|
|
287
|
+
clone_storage.create(
|
|
288
|
+
project_id=clone.project_id,
|
|
289
|
+
branch_name=clone.branch_name,
|
|
290
|
+
clone_path=clone_path,
|
|
291
|
+
base_branch=clone.base_branch,
|
|
292
|
+
task_id=clone.task_id,
|
|
293
|
+
remote_url=clone.remote_url,
|
|
294
|
+
)
|
|
295
|
+
logger.info(f"Restored clone record for {clone_id} after file deletion failure")
|
|
296
|
+
except Exception as restore_error:
|
|
297
|
+
logger.error(
|
|
298
|
+
f"Failed to restore clone record {clone_id}: {restore_error}. "
|
|
299
|
+
f"Clone is now orphaned in database."
|
|
300
|
+
)
|
|
301
|
+
return {
|
|
302
|
+
"success": False,
|
|
303
|
+
"error": f"Failed to delete clone files: {result.error or result.message}",
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
"success": True,
|
|
308
|
+
"message": f"Deleted clone {clone_id}",
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
registry.register(
|
|
312
|
+
name="delete_clone",
|
|
313
|
+
description="Delete a clone and its files",
|
|
314
|
+
input_schema={
|
|
315
|
+
"type": "object",
|
|
316
|
+
"properties": {
|
|
317
|
+
"clone_id": {
|
|
318
|
+
"type": "string",
|
|
319
|
+
"description": "Clone ID to delete",
|
|
320
|
+
},
|
|
321
|
+
"force": {
|
|
322
|
+
"type": "boolean",
|
|
323
|
+
"description": "Force deletion even with uncommitted changes",
|
|
324
|
+
"default": False,
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
"required": ["clone_id"],
|
|
328
|
+
},
|
|
329
|
+
func=delete_clone,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# ===== sync_clone =====
|
|
333
|
+
async def sync_clone(
|
|
334
|
+
clone_id: str,
|
|
335
|
+
direction: Literal["pull", "push", "both"] = "pull",
|
|
336
|
+
) -> dict[str, Any]:
|
|
337
|
+
"""
|
|
338
|
+
Sync a clone with its remote.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
clone_id: Clone ID to sync
|
|
342
|
+
direction: Sync direction (pull, push, or both)
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Dict with sync result
|
|
346
|
+
"""
|
|
347
|
+
clone = clone_storage.get(clone_id)
|
|
348
|
+
if not clone:
|
|
349
|
+
return {
|
|
350
|
+
"success": False,
|
|
351
|
+
"error": f"Clone not found: {clone_id}",
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# Mark as syncing
|
|
355
|
+
clone_storage.mark_syncing(clone_id)
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
result = git_manager.sync_clone(
|
|
359
|
+
clone_path=clone.clone_path,
|
|
360
|
+
direction=direction,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
if result.success:
|
|
364
|
+
# Record successful sync and mark as active
|
|
365
|
+
clone_storage.record_sync(clone_id)
|
|
366
|
+
clone_storage.update(clone_id, status="active")
|
|
367
|
+
return {
|
|
368
|
+
"success": True,
|
|
369
|
+
"message": f"Synced clone {clone_id} ({direction})",
|
|
370
|
+
}
|
|
371
|
+
else:
|
|
372
|
+
return {
|
|
373
|
+
"success": False,
|
|
374
|
+
"error": f"Sync failed: {result.error or result.message}",
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
except Exception as e:
|
|
378
|
+
return {
|
|
379
|
+
"success": False,
|
|
380
|
+
"error": str(e),
|
|
381
|
+
}
|
|
382
|
+
finally:
|
|
383
|
+
# Ensure status is reset to active if record_sync didn't complete
|
|
384
|
+
clone = clone_storage.get(clone_id)
|
|
385
|
+
if clone and clone.status == "syncing":
|
|
386
|
+
clone_storage.update(clone_id, status="active")
|
|
387
|
+
|
|
388
|
+
registry.register(
|
|
389
|
+
name="sync_clone",
|
|
390
|
+
description="Sync a clone with its remote repository",
|
|
391
|
+
input_schema={
|
|
392
|
+
"type": "object",
|
|
393
|
+
"properties": {
|
|
394
|
+
"clone_id": {
|
|
395
|
+
"type": "string",
|
|
396
|
+
"description": "Clone ID to sync",
|
|
397
|
+
},
|
|
398
|
+
"direction": {
|
|
399
|
+
"type": "string",
|
|
400
|
+
"description": "Sync direction",
|
|
401
|
+
"enum": ["pull", "push", "both"],
|
|
402
|
+
"default": "pull",
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
"required": ["clone_id"],
|
|
406
|
+
},
|
|
407
|
+
func=sync_clone,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# ===== merge_clone_to_target =====
|
|
411
|
+
async def merge_clone_to_target(
|
|
412
|
+
clone_id: str,
|
|
413
|
+
target_branch: str = "main",
|
|
414
|
+
) -> dict[str, Any]:
|
|
415
|
+
"""
|
|
416
|
+
Merge clone branch to target branch in main repository.
|
|
417
|
+
|
|
418
|
+
Performs:
|
|
419
|
+
1. Push clone changes to remote (sync_clone push)
|
|
420
|
+
2. Fetch branch in main repo
|
|
421
|
+
3. Attempt merge to target branch
|
|
422
|
+
|
|
423
|
+
On success, sets cleanup_after to 7 days from now.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
clone_id: Clone ID to merge
|
|
427
|
+
target_branch: Target branch to merge into (default: main)
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Dict with merge result and conflict info if any
|
|
431
|
+
"""
|
|
432
|
+
from datetime import UTC, datetime, timedelta
|
|
433
|
+
|
|
434
|
+
clone = clone_storage.get(clone_id)
|
|
435
|
+
if not clone:
|
|
436
|
+
return {
|
|
437
|
+
"success": False,
|
|
438
|
+
"error": f"Clone not found: {clone_id}",
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
# Step 1: Push clone changes to remote
|
|
442
|
+
clone_storage.mark_syncing(clone_id)
|
|
443
|
+
sync_result = git_manager.sync_clone(
|
|
444
|
+
clone_path=clone.clone_path,
|
|
445
|
+
direction="push",
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
if not sync_result.success:
|
|
449
|
+
clone_storage.update(clone_id, status="active")
|
|
450
|
+
return {
|
|
451
|
+
"success": False,
|
|
452
|
+
"error": f"Sync failed: {sync_result.error or sync_result.message}",
|
|
453
|
+
"step": "sync",
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
clone_storage.record_sync(clone_id)
|
|
457
|
+
|
|
458
|
+
# Step 2: Merge in main repo
|
|
459
|
+
merge_result = git_manager.merge_branch(
|
|
460
|
+
source_branch=clone.branch_name,
|
|
461
|
+
target_branch=target_branch,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
if not merge_result.success:
|
|
465
|
+
# Check for conflicts
|
|
466
|
+
if merge_result.error == "merge_conflict":
|
|
467
|
+
conflicted_files = merge_result.output.split("\n") if merge_result.output else []
|
|
468
|
+
return {
|
|
469
|
+
"success": False,
|
|
470
|
+
"has_conflicts": True,
|
|
471
|
+
"conflicted_files": conflicted_files,
|
|
472
|
+
"error": merge_result.message,
|
|
473
|
+
"step": "merge",
|
|
474
|
+
"message": (
|
|
475
|
+
f"Merge conflicts detected in {len(conflicted_files)} files. "
|
|
476
|
+
"Use gobby-merge tools to resolve."
|
|
477
|
+
),
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
"success": False,
|
|
482
|
+
"has_conflicts": False,
|
|
483
|
+
"error": merge_result.error or merge_result.message,
|
|
484
|
+
"step": "merge",
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
# Step 3: Success - set cleanup_after
|
|
488
|
+
cleanup_after = (datetime.now(UTC) + timedelta(days=7)).isoformat()
|
|
489
|
+
clone_storage.update(clone_id, cleanup_after=cleanup_after)
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
"success": True,
|
|
493
|
+
"message": f"Successfully merged {clone.branch_name} into {target_branch}",
|
|
494
|
+
"cleanup_after": cleanup_after,
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
registry.register(
|
|
498
|
+
name="merge_clone_to_target",
|
|
499
|
+
description="Merge clone branch to target branch in main repository",
|
|
500
|
+
input_schema={
|
|
501
|
+
"type": "object",
|
|
502
|
+
"properties": {
|
|
503
|
+
"clone_id": {
|
|
504
|
+
"type": "string",
|
|
505
|
+
"description": "Clone ID to merge",
|
|
506
|
+
},
|
|
507
|
+
"target_branch": {
|
|
508
|
+
"type": "string",
|
|
509
|
+
"description": "Target branch to merge into",
|
|
510
|
+
"default": "main",
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
"required": ["clone_id"],
|
|
514
|
+
},
|
|
515
|
+
func=merge_clone_to_target,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
return registry
|
gobby/mcp_proxy/tools/memory.py
CHANGED
|
@@ -3,7 +3,7 @@ Internal MCP tools for Gobby Memory System.
|
|
|
3
3
|
|
|
4
4
|
Exposes functionality for:
|
|
5
5
|
- Creating memories (create_memory)
|
|
6
|
-
- Searching memories (search_memories
|
|
6
|
+
- Searching memories (search_memories)
|
|
7
7
|
- Deleting memories (delete_memory)
|
|
8
8
|
- Listing memories (list_memories)
|
|
9
9
|
- Getting memory details (get_memory)
|
|
@@ -144,29 +144,6 @@ def create_memory_registry(
|
|
|
144
144
|
except Exception as e:
|
|
145
145
|
return {"success": False, "error": str(e)}
|
|
146
146
|
|
|
147
|
-
# Backward compatibility alias for recall_memory -> search_memories
|
|
148
|
-
@registry.tool(
|
|
149
|
-
name="recall_memory",
|
|
150
|
-
description="[DEPRECATED: Use search_memories] Search memories based on query.",
|
|
151
|
-
)
|
|
152
|
-
def recall_memory(
|
|
153
|
-
query: str | None = None,
|
|
154
|
-
limit: int = 10,
|
|
155
|
-
min_importance: float | None = None,
|
|
156
|
-
tags_all: list[str] | None = None,
|
|
157
|
-
tags_any: list[str] | None = None,
|
|
158
|
-
tags_none: list[str] | None = None,
|
|
159
|
-
) -> dict[str, Any]:
|
|
160
|
-
"""Deprecated alias for search_memories. Use search_memories instead."""
|
|
161
|
-
return search_memories( # type: ignore[no-any-return]
|
|
162
|
-
query=query,
|
|
163
|
-
limit=limit,
|
|
164
|
-
min_importance=min_importance,
|
|
165
|
-
tags_all=tags_all,
|
|
166
|
-
tags_any=tags_any,
|
|
167
|
-
tags_none=tags_none,
|
|
168
|
-
)
|
|
169
|
-
|
|
170
147
|
@registry.tool(
|
|
171
148
|
name="delete_memory",
|
|
172
149
|
description="Delete a memory by ID.",
|
|
@@ -278,7 +255,7 @@ def create_memory_registry(
|
|
|
278
255
|
name="get_related_memories",
|
|
279
256
|
description="Get memories related to a specific memory via cross-references.",
|
|
280
257
|
)
|
|
281
|
-
def get_related_memories(
|
|
258
|
+
async def get_related_memories(
|
|
282
259
|
memory_id: str,
|
|
283
260
|
limit: int = 5,
|
|
284
261
|
min_similarity: float = 0.0,
|
|
@@ -295,7 +272,7 @@ def create_memory_registry(
|
|
|
295
272
|
min_similarity: Minimum similarity threshold (0.0-1.0)
|
|
296
273
|
"""
|
|
297
274
|
try:
|
|
298
|
-
memories = memory_manager.get_related(
|
|
275
|
+
memories = await memory_manager.get_related(
|
|
299
276
|
memory_id=memory_id,
|
|
300
277
|
limit=limit,
|
|
301
278
|
min_similarity=min_similarity,
|
gobby/mcp_proxy/tools/metrics.py
CHANGED
|
@@ -11,20 +11,34 @@ via the downstream proxy pattern (call_tool).
|
|
|
11
11
|
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
from gobby.conductor.token_tracker import SessionTokenTracker
|
|
14
15
|
from gobby.mcp_proxy.metrics import ToolMetricsManager
|
|
15
16
|
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
def create_metrics_registry(
|
|
19
|
+
def create_metrics_registry(
|
|
20
|
+
metrics_manager: ToolMetricsManager,
|
|
21
|
+
session_storage: Any | None = None,
|
|
22
|
+
daily_budget_usd: float = 50.0,
|
|
23
|
+
) -> InternalToolRegistry:
|
|
19
24
|
"""
|
|
20
25
|
Create a metrics tool registry with all metrics-related tools.
|
|
21
26
|
|
|
22
27
|
Args:
|
|
23
28
|
metrics_manager: ToolMetricsManager instance
|
|
29
|
+
session_storage: Optional LocalSessionManager for token/cost tracking
|
|
30
|
+
daily_budget_usd: Daily budget limit for token tracking (default: $50)
|
|
24
31
|
|
|
25
32
|
Returns:
|
|
26
33
|
InternalToolRegistry with metrics tools registered
|
|
27
34
|
"""
|
|
35
|
+
# Create token tracker if session storage is provided
|
|
36
|
+
token_tracker: SessionTokenTracker | None = None
|
|
37
|
+
if session_storage is not None:
|
|
38
|
+
token_tracker = SessionTokenTracker(
|
|
39
|
+
session_storage=session_storage,
|
|
40
|
+
daily_budget_usd=daily_budget_usd,
|
|
41
|
+
)
|
|
28
42
|
registry = InternalToolRegistry(
|
|
29
43
|
name="gobby-metrics",
|
|
30
44
|
description="Tool metrics - query call counts, success rates, latency",
|
|
@@ -280,4 +294,54 @@ def create_metrics_registry(metrics_manager: ToolMetricsManager) -> InternalTool
|
|
|
280
294
|
except Exception as e:
|
|
281
295
|
return {"success": False, "error": str(e)}
|
|
282
296
|
|
|
297
|
+
# Token/cost tracking tools (only available if session_storage provided)
|
|
298
|
+
@registry.tool(
|
|
299
|
+
name="get_usage_report",
|
|
300
|
+
description="Get token and cost usage report for a specified time period.",
|
|
301
|
+
)
|
|
302
|
+
def get_usage_report(days: int = 1) -> dict[str, Any]:
|
|
303
|
+
"""
|
|
304
|
+
Get usage report including token counts and costs.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
days: Number of days to look back (default: 1 = today)
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Dictionary with usage summary
|
|
311
|
+
"""
|
|
312
|
+
if token_tracker is None:
|
|
313
|
+
return {"success": False, "error": "Token tracking not configured"}
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
summary = token_tracker.get_usage_summary(days=days)
|
|
317
|
+
return {
|
|
318
|
+
"success": True,
|
|
319
|
+
"usage": summary,
|
|
320
|
+
}
|
|
321
|
+
except Exception as e:
|
|
322
|
+
return {"success": False, "error": str(e)}
|
|
323
|
+
|
|
324
|
+
@registry.tool(
|
|
325
|
+
name="get_budget_status",
|
|
326
|
+
description="Get current daily budget status including used amount and remaining budget.",
|
|
327
|
+
)
|
|
328
|
+
def get_budget_status() -> dict[str, Any]:
|
|
329
|
+
"""
|
|
330
|
+
Get current budget status for today.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Dictionary with budget info
|
|
334
|
+
"""
|
|
335
|
+
if token_tracker is None:
|
|
336
|
+
return {"success": False, "error": "Token tracking not configured"}
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
status = token_tracker.get_budget_status()
|
|
340
|
+
return {
|
|
341
|
+
"success": True,
|
|
342
|
+
"budget": status,
|
|
343
|
+
}
|
|
344
|
+
except Exception as e:
|
|
345
|
+
return {"success": False, "error": str(e)}
|
|
346
|
+
|
|
283
347
|
return registry
|
|
@@ -5,6 +5,7 @@ Contains decomposed orchestration functionality:
|
|
|
5
5
|
- monitor: Status monitoring tools (get_orchestration_status, poll_agent_status)
|
|
6
6
|
- review: Review workflow tools (spawn_review_agent, process_completed_agents)
|
|
7
7
|
- cleanup: Cleanup tools (cleanup_reviewed_worktrees, cleanup_stale_worktrees)
|
|
8
|
+
- wait: Blocking wait tools (wait_for_task, wait_for_any_task, wait_for_all_tasks)
|
|
8
9
|
- utils: Shared utilities
|
|
9
10
|
"""
|
|
10
11
|
|
|
@@ -13,11 +14,13 @@ from gobby.mcp_proxy.tools.orchestration.monitor import register_monitor
|
|
|
13
14
|
from gobby.mcp_proxy.tools.orchestration.orchestrate import register_orchestrator
|
|
14
15
|
from gobby.mcp_proxy.tools.orchestration.review import register_reviewer
|
|
15
16
|
from gobby.mcp_proxy.tools.orchestration.utils import get_current_project_id
|
|
17
|
+
from gobby.mcp_proxy.tools.orchestration.wait import register_wait
|
|
16
18
|
|
|
17
19
|
__all__ = [
|
|
18
20
|
"register_cleanup",
|
|
19
21
|
"register_monitor",
|
|
20
22
|
"register_orchestrator",
|
|
21
23
|
"register_reviewer",
|
|
24
|
+
"register_wait",
|
|
22
25
|
"get_current_project_id",
|
|
23
26
|
]
|