gobby 0.2.5__py3-none-any.whl → 0.2.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +13 -4
  4. gobby/adapters/codex_impl/__init__.py +28 -0
  5. gobby/adapters/codex_impl/adapter.py +722 -0
  6. gobby/adapters/codex_impl/client.py +679 -0
  7. gobby/adapters/codex_impl/protocol.py +20 -0
  8. gobby/adapters/codex_impl/types.py +68 -0
  9. gobby/agents/definitions.py +11 -1
  10. gobby/agents/isolation.py +395 -0
  11. gobby/agents/runner.py +8 -0
  12. gobby/agents/sandbox.py +261 -0
  13. gobby/agents/spawn.py +42 -287
  14. gobby/agents/spawn_executor.py +385 -0
  15. gobby/agents/spawners/__init__.py +24 -0
  16. gobby/agents/spawners/command_builder.py +189 -0
  17. gobby/agents/spawners/embedded.py +21 -2
  18. gobby/agents/spawners/headless.py +21 -2
  19. gobby/agents/spawners/prompt_manager.py +125 -0
  20. gobby/cli/__init__.py +6 -0
  21. gobby/cli/clones.py +419 -0
  22. gobby/cli/conductor.py +266 -0
  23. gobby/cli/install.py +4 -4
  24. gobby/cli/installers/antigravity.py +3 -9
  25. gobby/cli/installers/claude.py +15 -9
  26. gobby/cli/installers/codex.py +2 -8
  27. gobby/cli/installers/gemini.py +8 -8
  28. gobby/cli/installers/shared.py +175 -13
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/skills.py +858 -0
  31. gobby/cli/tasks/ai.py +0 -440
  32. gobby/cli/tasks/crud.py +44 -6
  33. gobby/cli/tasks/main.py +0 -4
  34. gobby/cli/tui.py +2 -2
  35. gobby/cli/utils.py +12 -5
  36. gobby/clones/__init__.py +13 -0
  37. gobby/clones/git.py +547 -0
  38. gobby/conductor/__init__.py +16 -0
  39. gobby/conductor/alerts.py +135 -0
  40. gobby/conductor/loop.py +164 -0
  41. gobby/conductor/monitors/__init__.py +11 -0
  42. gobby/conductor/monitors/agents.py +116 -0
  43. gobby/conductor/monitors/tasks.py +155 -0
  44. gobby/conductor/pricing.py +234 -0
  45. gobby/conductor/token_tracker.py +160 -0
  46. gobby/config/__init__.py +12 -97
  47. gobby/config/app.py +69 -91
  48. gobby/config/extensions.py +2 -2
  49. gobby/config/features.py +7 -130
  50. gobby/config/search.py +110 -0
  51. gobby/config/servers.py +1 -1
  52. gobby/config/skills.py +43 -0
  53. gobby/config/tasks.py +9 -41
  54. gobby/hooks/__init__.py +0 -13
  55. gobby/hooks/event_handlers.py +188 -2
  56. gobby/hooks/hook_manager.py +50 -4
  57. gobby/hooks/plugins.py +1 -1
  58. gobby/hooks/skill_manager.py +130 -0
  59. gobby/hooks/webhooks.py +1 -1
  60. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  61. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  62. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  63. gobby/llm/claude.py +22 -34
  64. gobby/llm/claude_executor.py +46 -256
  65. gobby/llm/codex_executor.py +59 -291
  66. gobby/llm/executor.py +21 -0
  67. gobby/llm/gemini.py +134 -110
  68. gobby/llm/litellm_executor.py +143 -6
  69. gobby/llm/resolver.py +98 -35
  70. gobby/mcp_proxy/importer.py +62 -4
  71. gobby/mcp_proxy/instructions.py +56 -0
  72. gobby/mcp_proxy/models.py +15 -0
  73. gobby/mcp_proxy/registries.py +68 -8
  74. gobby/mcp_proxy/server.py +33 -3
  75. gobby/mcp_proxy/services/recommendation.py +43 -11
  76. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  77. gobby/mcp_proxy/stdio.py +2 -1
  78. gobby/mcp_proxy/tools/__init__.py +0 -2
  79. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  80. gobby/mcp_proxy/tools/agents.py +31 -731
  81. gobby/mcp_proxy/tools/clones.py +518 -0
  82. gobby/mcp_proxy/tools/memory.py +3 -26
  83. gobby/mcp_proxy/tools/metrics.py +65 -1
  84. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  85. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  86. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  87. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  88. gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
  89. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  90. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  91. gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
  92. gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
  93. gobby/mcp_proxy/tools/skills/__init__.py +616 -0
  94. gobby/mcp_proxy/tools/spawn_agent.py +417 -0
  95. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  96. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  97. gobby/mcp_proxy/tools/task_sync.py +1 -1
  98. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  99. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  100. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  101. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  102. gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
  103. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  104. gobby/mcp_proxy/tools/workflows.py +1 -1
  105. gobby/mcp_proxy/tools/worktrees.py +0 -338
  106. gobby/memory/backends/__init__.py +6 -1
  107. gobby/memory/backends/mem0.py +6 -1
  108. gobby/memory/extractor.py +477 -0
  109. gobby/memory/ingestion/__init__.py +5 -0
  110. gobby/memory/ingestion/multimodal.py +221 -0
  111. gobby/memory/manager.py +73 -285
  112. gobby/memory/search/__init__.py +10 -0
  113. gobby/memory/search/coordinator.py +248 -0
  114. gobby/memory/services/__init__.py +5 -0
  115. gobby/memory/services/crossref.py +142 -0
  116. gobby/prompts/loader.py +5 -2
  117. gobby/runner.py +37 -16
  118. gobby/search/__init__.py +48 -6
  119. gobby/search/backends/__init__.py +159 -0
  120. gobby/search/backends/embedding.py +225 -0
  121. gobby/search/embeddings.py +238 -0
  122. gobby/search/models.py +148 -0
  123. gobby/search/unified.py +496 -0
  124. gobby/servers/http.py +24 -12
  125. gobby/servers/routes/admin.py +294 -0
  126. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  127. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  128. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  129. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  130. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  131. gobby/servers/routes/mcp/hooks.py +1 -1
  132. gobby/servers/routes/mcp/tools.py +48 -1317
  133. gobby/servers/websocket.py +2 -2
  134. gobby/sessions/analyzer.py +2 -0
  135. gobby/sessions/lifecycle.py +1 -1
  136. gobby/sessions/processor.py +10 -0
  137. gobby/sessions/transcripts/base.py +2 -0
  138. gobby/sessions/transcripts/claude.py +79 -10
  139. gobby/skills/__init__.py +91 -0
  140. gobby/skills/loader.py +685 -0
  141. gobby/skills/manager.py +384 -0
  142. gobby/skills/parser.py +286 -0
  143. gobby/skills/search.py +463 -0
  144. gobby/skills/sync.py +119 -0
  145. gobby/skills/updater.py +385 -0
  146. gobby/skills/validator.py +368 -0
  147. gobby/storage/clones.py +378 -0
  148. gobby/storage/database.py +1 -1
  149. gobby/storage/memories.py +43 -13
  150. gobby/storage/migrations.py +162 -201
  151. gobby/storage/sessions.py +116 -7
  152. gobby/storage/skills.py +782 -0
  153. gobby/storage/tasks/_crud.py +4 -4
  154. gobby/storage/tasks/_lifecycle.py +57 -7
  155. gobby/storage/tasks/_manager.py +14 -5
  156. gobby/storage/tasks/_models.py +8 -3
  157. gobby/sync/memories.py +40 -5
  158. gobby/sync/tasks.py +83 -6
  159. gobby/tasks/__init__.py +1 -2
  160. gobby/tasks/external_validator.py +1 -1
  161. gobby/tasks/validation.py +46 -35
  162. gobby/tools/summarizer.py +91 -10
  163. gobby/tui/api_client.py +4 -7
  164. gobby/tui/app.py +5 -3
  165. gobby/tui/screens/orchestrator.py +1 -2
  166. gobby/tui/screens/tasks.py +2 -4
  167. gobby/tui/ws_client.py +1 -1
  168. gobby/utils/daemon_client.py +2 -2
  169. gobby/utils/project_context.py +2 -3
  170. gobby/utils/status.py +13 -0
  171. gobby/workflows/actions.py +221 -1135
  172. gobby/workflows/artifact_actions.py +31 -0
  173. gobby/workflows/autonomous_actions.py +11 -0
  174. gobby/workflows/context_actions.py +93 -1
  175. gobby/workflows/detection_helpers.py +115 -31
  176. gobby/workflows/enforcement/__init__.py +47 -0
  177. gobby/workflows/enforcement/blocking.py +269 -0
  178. gobby/workflows/enforcement/commit_policy.py +283 -0
  179. gobby/workflows/enforcement/handlers.py +269 -0
  180. gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
  181. gobby/workflows/engine.py +13 -2
  182. gobby/workflows/git_utils.py +106 -0
  183. gobby/workflows/lifecycle_evaluator.py +29 -1
  184. gobby/workflows/llm_actions.py +30 -0
  185. gobby/workflows/loader.py +19 -6
  186. gobby/workflows/mcp_actions.py +20 -1
  187. gobby/workflows/memory_actions.py +154 -0
  188. gobby/workflows/safe_evaluator.py +183 -0
  189. gobby/workflows/session_actions.py +44 -0
  190. gobby/workflows/state_actions.py +60 -1
  191. gobby/workflows/stop_signal_actions.py +55 -0
  192. gobby/workflows/summary_actions.py +111 -1
  193. gobby/workflows/task_sync_actions.py +347 -0
  194. gobby/workflows/todo_actions.py +34 -1
  195. gobby/workflows/webhook_actions.py +185 -0
  196. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
  197. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
  198. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
  199. gobby/adapters/codex.py +0 -1292
  200. gobby/install/claude/commands/gobby/bug.md +0 -51
  201. gobby/install/claude/commands/gobby/chore.md +0 -51
  202. gobby/install/claude/commands/gobby/epic.md +0 -52
  203. gobby/install/claude/commands/gobby/eval.md +0 -235
  204. gobby/install/claude/commands/gobby/feat.md +0 -49
  205. gobby/install/claude/commands/gobby/nit.md +0 -52
  206. gobby/install/claude/commands/gobby/ref.md +0 -52
  207. gobby/install/codex/prompts/forget.md +0 -7
  208. gobby/install/codex/prompts/memories.md +0 -7
  209. gobby/install/codex/prompts/recall.md +0 -7
  210. gobby/install/codex/prompts/remember.md +0 -13
  211. gobby/llm/gemini_executor.py +0 -339
  212. gobby/mcp_proxy/tools/session_messages.py +0 -1056
  213. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  214. gobby/prompts/defaults/expansion/system.md +0 -119
  215. gobby/prompts/defaults/expansion/user.md +0 -48
  216. gobby/prompts/defaults/external_validation/agent.md +0 -72
  217. gobby/prompts/defaults/external_validation/external.md +0 -63
  218. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  219. gobby/prompts/defaults/external_validation/system.md +0 -6
  220. gobby/prompts/defaults/features/import_mcp.md +0 -22
  221. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  222. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  223. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  224. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  225. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  226. gobby/prompts/defaults/features/server_description.md +0 -20
  227. gobby/prompts/defaults/features/server_description_system.md +0 -6
  228. gobby/prompts/defaults/features/task_description.md +0 -31
  229. gobby/prompts/defaults/features/task_description_system.md +0 -6
  230. gobby/prompts/defaults/features/tool_summary.md +0 -17
  231. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  232. gobby/prompts/defaults/research/step.md +0 -58
  233. gobby/prompts/defaults/validation/criteria.md +0 -47
  234. gobby/prompts/defaults/validation/validate.md +0 -38
  235. gobby/storage/migrations_legacy.py +0 -1359
  236. gobby/tasks/context.py +0 -747
  237. gobby/tasks/criteria.py +0 -342
  238. gobby/tasks/expansion.py +0 -626
  239. gobby/tasks/prompts/expand.py +0 -327
  240. gobby/tasks/research.py +0 -421
  241. gobby/tasks/tdd.py +0 -352
  242. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
  243. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
  244. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
@@ -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
@@ -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, formerly recall_memory)
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,
@@ -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(metrics_manager: ToolMetricsManager) -> InternalToolRegistry:
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
  ]