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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +13 -4
  4. gobby/adapters/codex_impl/__init__.py +28 -0
  5. gobby/adapters/codex_impl/adapter.py +722 -0
  6. gobby/adapters/codex_impl/client.py +679 -0
  7. gobby/adapters/codex_impl/protocol.py +20 -0
  8. gobby/adapters/codex_impl/types.py +68 -0
  9. gobby/agents/definitions.py +11 -1
  10. gobby/agents/isolation.py +395 -0
  11. gobby/agents/runner.py +8 -0
  12. gobby/agents/sandbox.py +261 -0
  13. gobby/agents/spawn.py +42 -287
  14. gobby/agents/spawn_executor.py +385 -0
  15. gobby/agents/spawners/__init__.py +24 -0
  16. gobby/agents/spawners/command_builder.py +189 -0
  17. gobby/agents/spawners/embedded.py +21 -2
  18. gobby/agents/spawners/headless.py +21 -2
  19. gobby/agents/spawners/prompt_manager.py +125 -0
  20. gobby/cli/__init__.py +6 -0
  21. gobby/cli/clones.py +419 -0
  22. gobby/cli/conductor.py +266 -0
  23. gobby/cli/install.py +4 -4
  24. gobby/cli/installers/antigravity.py +3 -9
  25. gobby/cli/installers/claude.py +15 -9
  26. gobby/cli/installers/codex.py +2 -8
  27. gobby/cli/installers/gemini.py +8 -8
  28. gobby/cli/installers/shared.py +175 -13
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/skills.py +858 -0
  31. gobby/cli/tasks/ai.py +0 -440
  32. gobby/cli/tasks/crud.py +44 -6
  33. gobby/cli/tasks/main.py +0 -4
  34. gobby/cli/tui.py +2 -2
  35. gobby/cli/utils.py +12 -5
  36. gobby/clones/__init__.py +13 -0
  37. gobby/clones/git.py +547 -0
  38. gobby/conductor/__init__.py +16 -0
  39. gobby/conductor/alerts.py +135 -0
  40. gobby/conductor/loop.py +164 -0
  41. gobby/conductor/monitors/__init__.py +11 -0
  42. gobby/conductor/monitors/agents.py +116 -0
  43. gobby/conductor/monitors/tasks.py +155 -0
  44. gobby/conductor/pricing.py +234 -0
  45. gobby/conductor/token_tracker.py +160 -0
  46. gobby/config/__init__.py +12 -97
  47. gobby/config/app.py +69 -91
  48. gobby/config/extensions.py +2 -2
  49. gobby/config/features.py +7 -130
  50. gobby/config/search.py +110 -0
  51. gobby/config/servers.py +1 -1
  52. gobby/config/skills.py +43 -0
  53. gobby/config/tasks.py +9 -41
  54. gobby/hooks/__init__.py +0 -13
  55. gobby/hooks/event_handlers.py +188 -2
  56. gobby/hooks/hook_manager.py +50 -4
  57. gobby/hooks/plugins.py +1 -1
  58. gobby/hooks/skill_manager.py +130 -0
  59. gobby/hooks/webhooks.py +1 -1
  60. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  61. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  62. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  63. gobby/llm/claude.py +22 -34
  64. gobby/llm/claude_executor.py +46 -256
  65. gobby/llm/codex_executor.py +59 -291
  66. gobby/llm/executor.py +21 -0
  67. gobby/llm/gemini.py +134 -110
  68. gobby/llm/litellm_executor.py +143 -6
  69. gobby/llm/resolver.py +98 -35
  70. gobby/mcp_proxy/importer.py +62 -4
  71. gobby/mcp_proxy/instructions.py +56 -0
  72. gobby/mcp_proxy/models.py +15 -0
  73. gobby/mcp_proxy/registries.py +68 -8
  74. gobby/mcp_proxy/server.py +33 -3
  75. gobby/mcp_proxy/services/recommendation.py +43 -11
  76. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  77. gobby/mcp_proxy/stdio.py +2 -1
  78. gobby/mcp_proxy/tools/__init__.py +0 -2
  79. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  80. gobby/mcp_proxy/tools/agents.py +31 -731
  81. gobby/mcp_proxy/tools/clones.py +518 -0
  82. gobby/mcp_proxy/tools/memory.py +3 -26
  83. gobby/mcp_proxy/tools/metrics.py +65 -1
  84. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  85. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  86. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  87. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  88. gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
  89. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  90. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  91. gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
  92. gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
  93. gobby/mcp_proxy/tools/skills/__init__.py +616 -0
  94. gobby/mcp_proxy/tools/spawn_agent.py +417 -0
  95. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  96. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  97. gobby/mcp_proxy/tools/task_sync.py +1 -1
  98. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  99. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  100. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  101. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  102. gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
  103. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  104. gobby/mcp_proxy/tools/workflows.py +1 -1
  105. gobby/mcp_proxy/tools/worktrees.py +0 -338
  106. gobby/memory/backends/__init__.py +6 -1
  107. gobby/memory/backends/mem0.py +6 -1
  108. gobby/memory/extractor.py +477 -0
  109. gobby/memory/ingestion/__init__.py +5 -0
  110. gobby/memory/ingestion/multimodal.py +221 -0
  111. gobby/memory/manager.py +73 -285
  112. gobby/memory/search/__init__.py +10 -0
  113. gobby/memory/search/coordinator.py +248 -0
  114. gobby/memory/services/__init__.py +5 -0
  115. gobby/memory/services/crossref.py +142 -0
  116. gobby/prompts/loader.py +5 -2
  117. gobby/runner.py +37 -16
  118. gobby/search/__init__.py +48 -6
  119. gobby/search/backends/__init__.py +159 -0
  120. gobby/search/backends/embedding.py +225 -0
  121. gobby/search/embeddings.py +238 -0
  122. gobby/search/models.py +148 -0
  123. gobby/search/unified.py +496 -0
  124. gobby/servers/http.py +24 -12
  125. gobby/servers/routes/admin.py +294 -0
  126. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  127. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  128. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  129. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  130. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  131. gobby/servers/routes/mcp/hooks.py +1 -1
  132. gobby/servers/routes/mcp/tools.py +48 -1317
  133. gobby/servers/websocket.py +2 -2
  134. gobby/sessions/analyzer.py +2 -0
  135. gobby/sessions/lifecycle.py +1 -1
  136. gobby/sessions/processor.py +10 -0
  137. gobby/sessions/transcripts/base.py +2 -0
  138. gobby/sessions/transcripts/claude.py +79 -10
  139. gobby/skills/__init__.py +91 -0
  140. gobby/skills/loader.py +685 -0
  141. gobby/skills/manager.py +384 -0
  142. gobby/skills/parser.py +286 -0
  143. gobby/skills/search.py +463 -0
  144. gobby/skills/sync.py +119 -0
  145. gobby/skills/updater.py +385 -0
  146. gobby/skills/validator.py +368 -0
  147. gobby/storage/clones.py +378 -0
  148. gobby/storage/database.py +1 -1
  149. gobby/storage/memories.py +43 -13
  150. gobby/storage/migrations.py +162 -201
  151. gobby/storage/sessions.py +116 -7
  152. gobby/storage/skills.py +782 -0
  153. gobby/storage/tasks/_crud.py +4 -4
  154. gobby/storage/tasks/_lifecycle.py +57 -7
  155. gobby/storage/tasks/_manager.py +14 -5
  156. gobby/storage/tasks/_models.py +8 -3
  157. gobby/sync/memories.py +40 -5
  158. gobby/sync/tasks.py +83 -6
  159. gobby/tasks/__init__.py +1 -2
  160. gobby/tasks/external_validator.py +1 -1
  161. gobby/tasks/validation.py +46 -35
  162. gobby/tools/summarizer.py +91 -10
  163. gobby/tui/api_client.py +4 -7
  164. gobby/tui/app.py +5 -3
  165. gobby/tui/screens/orchestrator.py +1 -2
  166. gobby/tui/screens/tasks.py +2 -4
  167. gobby/tui/ws_client.py +1 -1
  168. gobby/utils/daemon_client.py +2 -2
  169. gobby/utils/project_context.py +2 -3
  170. gobby/utils/status.py +13 -0
  171. gobby/workflows/actions.py +221 -1135
  172. gobby/workflows/artifact_actions.py +31 -0
  173. gobby/workflows/autonomous_actions.py +11 -0
  174. gobby/workflows/context_actions.py +93 -1
  175. gobby/workflows/detection_helpers.py +115 -31
  176. gobby/workflows/enforcement/__init__.py +47 -0
  177. gobby/workflows/enforcement/blocking.py +269 -0
  178. gobby/workflows/enforcement/commit_policy.py +283 -0
  179. gobby/workflows/enforcement/handlers.py +269 -0
  180. gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
  181. gobby/workflows/engine.py +13 -2
  182. gobby/workflows/git_utils.py +106 -0
  183. gobby/workflows/lifecycle_evaluator.py +29 -1
  184. gobby/workflows/llm_actions.py +30 -0
  185. gobby/workflows/loader.py +19 -6
  186. gobby/workflows/mcp_actions.py +20 -1
  187. gobby/workflows/memory_actions.py +154 -0
  188. gobby/workflows/safe_evaluator.py +183 -0
  189. gobby/workflows/session_actions.py +44 -0
  190. gobby/workflows/state_actions.py +60 -1
  191. gobby/workflows/stop_signal_actions.py +55 -0
  192. gobby/workflows/summary_actions.py +111 -1
  193. gobby/workflows/task_sync_actions.py +347 -0
  194. gobby/workflows/todo_actions.py +34 -1
  195. gobby/workflows/webhook_actions.py +185 -0
  196. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
  197. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
  198. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
  199. gobby/adapters/codex.py +0 -1292
  200. gobby/install/claude/commands/gobby/bug.md +0 -51
  201. gobby/install/claude/commands/gobby/chore.md +0 -51
  202. gobby/install/claude/commands/gobby/epic.md +0 -52
  203. gobby/install/claude/commands/gobby/eval.md +0 -235
  204. gobby/install/claude/commands/gobby/feat.md +0 -49
  205. gobby/install/claude/commands/gobby/nit.md +0 -52
  206. gobby/install/claude/commands/gobby/ref.md +0 -52
  207. gobby/install/codex/prompts/forget.md +0 -7
  208. gobby/install/codex/prompts/memories.md +0 -7
  209. gobby/install/codex/prompts/recall.md +0 -7
  210. gobby/install/codex/prompts/remember.md +0 -13
  211. gobby/llm/gemini_executor.py +0 -339
  212. gobby/mcp_proxy/tools/session_messages.py +0 -1056
  213. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  214. gobby/prompts/defaults/expansion/system.md +0 -119
  215. gobby/prompts/defaults/expansion/user.md +0 -48
  216. gobby/prompts/defaults/external_validation/agent.md +0 -72
  217. gobby/prompts/defaults/external_validation/external.md +0 -63
  218. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  219. gobby/prompts/defaults/external_validation/system.md +0 -6
  220. gobby/prompts/defaults/features/import_mcp.md +0 -22
  221. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  222. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  223. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  224. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  225. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  226. gobby/prompts/defaults/features/server_description.md +0 -20
  227. gobby/prompts/defaults/features/server_description_system.md +0 -6
  228. gobby/prompts/defaults/features/task_description.md +0 -31
  229. gobby/prompts/defaults/features/task_description_system.md +0 -6
  230. gobby/prompts/defaults/features/tool_summary.md +0 -17
  231. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  232. gobby/prompts/defaults/research/step.md +0 -58
  233. gobby/prompts/defaults/validation/criteria.md +0 -47
  234. gobby/prompts/defaults/validation/validate.md +0 -38
  235. gobby/storage/migrations_legacy.py +0 -1359
  236. gobby/tasks/context.py +0 -747
  237. gobby/tasks/criteria.py +0 -342
  238. gobby/tasks/expansion.py +0 -626
  239. gobby/tasks/prompts/expand.py +0 -327
  240. gobby/tasks/research.py +0 -421
  241. gobby/tasks/tdd.py +0 -352
  242. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
  243. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
  244. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
gobby/llm/resolver.py CHANGED
@@ -18,7 +18,8 @@ from typing import TYPE_CHECKING, Literal
18
18
  from gobby.llm.executor import AgentExecutor
19
19
 
20
20
  if TYPE_CHECKING:
21
- from gobby.config.app import DaemonConfig, LLMProvidersConfig
21
+ from gobby.config.app import DaemonConfig
22
+ from gobby.config.llm_providers import LLMProvidersConfig
22
23
  from gobby.workflows.definitions import WorkflowDefinition
23
24
 
24
25
  logger = logging.getLogger(__name__)
@@ -259,8 +260,13 @@ def create_executor(
259
260
  """
260
261
  Create an AgentExecutor for the given provider.
261
262
 
263
+ Routing strategy:
264
+ - api_key and adc auth modes: Route to LiteLLMExecutor for unified cost tracking
265
+ - subscription mode (Claude): Route to ClaudeExecutor (Claude Agent SDK)
266
+ - cli mode (Codex): Route to CodexExecutor (Codex CLI subprocess)
267
+
262
268
  Args:
263
- provider: Provider name (claude, gemini, litellm).
269
+ provider: Provider name (claude, gemini, litellm, codex).
264
270
  config: Optional daemon config for provider settings.
265
271
  model: Optional model override.
266
272
 
@@ -279,19 +285,40 @@ def create_executor(
279
285
  if config and config.llm_providers:
280
286
  provider_config = getattr(config.llm_providers, provider, None)
281
287
 
288
+ # Determine auth_mode from config
289
+ auth_mode = "api_key" # Default
290
+ if provider_config:
291
+ auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
292
+
282
293
  try:
283
- if provider == "claude":
294
+ # Route based on auth_mode:
295
+ # - subscription (Claude) -> ClaudeExecutor
296
+ # - cli (Codex) -> CodexExecutor
297
+ # - api_key/adc (all providers) -> LiteLLMExecutor
298
+
299
+ if provider == "claude" and auth_mode == "subscription":
300
+ # Subscription mode requires Claude Agent SDK
284
301
  return _create_claude_executor(provider_config, model)
285
- elif provider == "gemini":
286
- return _create_gemini_executor(provider_config, model)
302
+
303
+ elif provider == "codex" and auth_mode in ("subscription", "cli"):
304
+ # CLI mode requires Codex CLI subprocess
305
+ return _create_codex_executor(provider_config, model, auth_mode)
306
+
287
307
  elif provider == "litellm":
308
+ # Direct LiteLLM usage
288
309
  return _create_litellm_executor(provider_config, config, model)
289
- elif provider == "codex":
290
- return _create_codex_executor(provider_config, model)
310
+
311
+ elif auth_mode in ("api_key", "adc"):
312
+ # Route all api_key and adc modes through LiteLLM for unified cost tracking
313
+ return _create_litellm_executor_for_provider(
314
+ provider, auth_mode, provider_config, config, model
315
+ )
316
+
291
317
  else:
292
318
  raise ExecutorCreationError(
293
319
  provider,
294
- f"Unknown provider. Supported: {list(SUPPORTED_PROVIDERS)}",
320
+ f"Unknown provider/auth_mode combination: {provider}/{auth_mode}. "
321
+ f"Supported: {list(SUPPORTED_PROVIDERS)}",
295
322
  )
296
323
  except ProviderError:
297
324
  raise
@@ -303,15 +330,18 @@ def _create_claude_executor(
303
330
  provider_config: "LLMProviderConfig | None",
304
331
  model: str | None,
305
332
  ) -> AgentExecutor:
306
- """Create ClaudeExecutor with appropriate auth mode."""
333
+ """
334
+ Create ClaudeExecutor for subscription mode only.
335
+
336
+ Note: api_key mode is now routed through LiteLLMExecutor for unified cost tracking.
337
+ This function should only be called when auth_mode is "subscription".
338
+ """
307
339
  from gobby.llm.claude_executor import ClaudeExecutor
308
340
 
309
- # Determine auth mode and model from config
310
- auth_mode = "api_key"
341
+ # Subscription mode only - api_key mode routes through LiteLLM
311
342
  default_model = "claude-sonnet-4-20250514"
312
343
 
313
344
  if provider_config:
314
- auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
315
345
  # Get first model from comma-separated list if set
316
346
  models_str = getattr(provider_config, "models", None)
317
347
  if models_str:
@@ -320,46 +350,71 @@ def _create_claude_executor(
320
350
  default_model = models[0]
321
351
 
322
352
  return ClaudeExecutor(
323
- auth_mode=auth_mode, # type: ignore[arg-type]
353
+ auth_mode="subscription",
324
354
  default_model=model or default_model,
325
355
  )
326
356
 
327
357
 
328
- def _create_gemini_executor(
358
+ def _create_litellm_executor(
329
359
  provider_config: "LLMProviderConfig | None",
360
+ config: "DaemonConfig | None",
330
361
  model: str | None,
331
362
  ) -> AgentExecutor:
332
- """Create GeminiExecutor with appropriate auth mode."""
333
- from gobby.llm.gemini_executor import GeminiExecutor
363
+ """Create LiteLLMExecutor with API keys from config (direct litellm usage)."""
364
+ from gobby.llm.litellm_executor import LiteLLMExecutor
334
365
 
335
- # Determine auth mode and model from config
336
- auth_mode = "api_key"
337
- default_model = "gemini-2.0-flash"
366
+ # Determine model and API base from config
367
+ default_model = "gpt-4o-mini"
368
+ api_base = None
369
+ api_keys: dict[str, str] | None = None
338
370
 
339
371
  if provider_config:
340
- auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
341
372
  models_str = getattr(provider_config, "models", None)
342
373
  if models_str:
343
374
  models = [m.strip() for m in models_str.split(",") if m.strip()]
344
375
  if models:
345
376
  default_model = models[0]
377
+ api_base = getattr(provider_config, "api_base", None)
346
378
 
347
- return GeminiExecutor(
348
- auth_mode=auth_mode, # type: ignore[arg-type]
379
+ # Get API keys from llm_providers.api_keys
380
+ if config and config.llm_providers:
381
+ api_keys = config.llm_providers.api_keys or None
382
+
383
+ return LiteLLMExecutor(
349
384
  default_model=model or default_model,
385
+ api_base=api_base,
386
+ api_keys=api_keys,
350
387
  )
351
388
 
352
389
 
353
- def _create_litellm_executor(
390
+ def _create_litellm_executor_for_provider(
391
+ provider: str,
392
+ auth_mode: str,
354
393
  provider_config: "LLMProviderConfig | None",
355
394
  config: "DaemonConfig | None",
356
395
  model: str | None,
357
396
  ) -> AgentExecutor:
358
- """Create LiteLLMExecutor with API keys from config."""
397
+ """
398
+ Create LiteLLMExecutor configured for a specific provider's api_key/adc mode.
399
+
400
+ This routes provider-specific calls through LiteLLM for unified cost tracking:
401
+ - Claude (api_key) -> anthropic/model
402
+ - Gemini (api_key) -> gemini/model
403
+ - Gemini (adc) -> vertex_ai/model
404
+ - Codex/OpenAI (api_key) -> model (no prefix)
405
+ """
359
406
  from gobby.llm.litellm_executor import LiteLLMExecutor
360
407
 
361
- # Determine model and API base from config
362
- default_model = "gpt-4o-mini"
408
+ # Default models per provider
409
+ default_models = {
410
+ "claude": "claude-sonnet-4-20250514",
411
+ "gemini": "gemini-2.0-flash",
412
+ "codex": "gpt-4o",
413
+ "openai": "gpt-4o",
414
+ }
415
+
416
+ # Determine model from config
417
+ default_model = default_models.get(provider, "gpt-4o-mini")
363
418
  api_base = None
364
419
  api_keys: dict[str, str] | None = None
365
420
 
@@ -375,34 +430,42 @@ def _create_litellm_executor(
375
430
  if config and config.llm_providers:
376
431
  api_keys = config.llm_providers.api_keys or None
377
432
 
433
+ # Cast auth_mode to the expected literal type
434
+ litellm_auth_mode = auth_mode if auth_mode in ("api_key", "adc") else "api_key"
435
+
378
436
  return LiteLLMExecutor(
379
437
  default_model=model or default_model,
380
438
  api_base=api_base,
381
439
  api_keys=api_keys,
440
+ provider=provider, # type: ignore[arg-type]
441
+ auth_mode=litellm_auth_mode, # type: ignore[arg-type]
382
442
  )
383
443
 
384
444
 
385
445
  def _create_codex_executor(
386
446
  provider_config: "LLMProviderConfig | None",
387
447
  model: str | None,
448
+ auth_mode: str = "subscription",
388
449
  ) -> AgentExecutor:
389
450
  """
390
- Create CodexExecutor with appropriate auth mode.
451
+ Create CodexExecutor for subscription/CLI mode only.
452
+
453
+ Note: api_key mode is now routed through LiteLLMExecutor for unified cost tracking.
454
+ This function should only be called when auth_mode is "subscription" or "cli".
391
455
 
392
- Codex supports two modes with different capabilities:
393
- - api_key: OpenAI API with function calling (full tool injection)
394
- - subscription: Codex CLI with ChatGPT subscription (no custom tools)
456
+ CLI mode uses Codex CLI subprocess - no custom tool injection supported.
395
457
 
396
- See CodexExecutor docstring for detailed mode differences.
458
+ Args:
459
+ provider_config: Provider configuration.
460
+ model: Optional model override.
461
+ auth_mode: Authentication mode - "subscription" or "cli".
397
462
  """
398
463
  from gobby.llm.codex_executor import CodexExecutor
399
464
 
400
- # Determine auth mode and model from config
401
- auth_mode = "api_key"
465
+ # CLI/subscription mode only - api_key mode routes through LiteLLM
402
466
  default_model = "gpt-4o"
403
467
 
404
468
  if provider_config:
405
- auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
406
469
  models_str = getattr(provider_config, "models", None)
407
470
  if models_str:
408
471
  models = [m.strip() for m in models_str.split(",") if m.strip()]
@@ -417,7 +480,7 @@ def _create_codex_executor(
417
480
 
418
481
  # Re-export for TYPE_CHECKING
419
482
  if TYPE_CHECKING:
420
- from gobby.config.app import LLMProviderConfig
483
+ from gobby.config.llm_providers import LLMProviderConfig
421
484
 
422
485
 
423
486
  class ExecutorRegistry:
@@ -5,6 +5,8 @@ import re
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
7
  from gobby.config.app import DaemonConfig
8
+ from gobby.config.features import DEFAULT_IMPORT_MCP_SERVER_PROMPT
9
+ from gobby.prompts import PromptLoader
8
10
  from gobby.storage.database import DatabaseProtocol
9
11
  from gobby.storage.mcp import LocalMCPManager
10
12
  from gobby.storage.projects import LocalProjectManager
@@ -18,6 +20,21 @@ logger = logging.getLogger(__name__)
18
20
  # Pattern to detect placeholder secrets like <YOUR_API_KEY>
19
21
  SECRET_PLACEHOLDER_PATTERN = re.compile(r"<YOUR_[A-Z0-9_]+>")
20
22
 
23
+ DEFAULT_GITHUB_FETCH_PROMPT = """Fetch the README from this GitHub repository and extract MCP server configuration:
24
+
25
+ {github_url}
26
+
27
+ If the URL doesn't point directly to a README, try to find and fetch the README.md file.
28
+
29
+ After reading the documentation, extract the MCP server configuration as a JSON object."""
30
+
31
+ DEFAULT_SEARCH_FETCH_PROMPT = """Search for MCP server: {search_query}
32
+
33
+ Find the official documentation or GitHub repository for this MCP server.
34
+ Then fetch and read the README or installation docs.
35
+
36
+ After reading the documentation, extract the MCP server configuration as a JSON object."""
37
+
21
38
 
22
39
  class MCPServerImporter:
23
40
  """Handles importing MCP servers from various sources."""
@@ -46,6 +63,21 @@ class MCPServerImporter:
46
63
  self.mcp_client_manager = mcp_client_manager
47
64
  self.import_config = config.get_import_mcp_server_config()
48
65
 
66
+ # Initialize prompt loader
67
+ project_path = None
68
+ if current_project_id:
69
+ if project := self.project_manager.get(current_project_id):
70
+ project_path = project.repo_path
71
+
72
+ from pathlib import Path
73
+
74
+ self._loader = PromptLoader(project_dir=Path(project_path) if project_path else None)
75
+
76
+ # Register fallbacks
77
+ self._loader.register_fallback("import/github_fetch", lambda: DEFAULT_GITHUB_FETCH_PROMPT)
78
+ self._loader.register_fallback("import/search_fetch", lambda: DEFAULT_SEARCH_FETCH_PROMPT)
79
+ self._loader.register_fallback("import/system", lambda: DEFAULT_IMPORT_MCP_SERVER_PROMPT)
80
+
49
81
  async def import_from_project(
50
82
  self,
51
83
  source_project: str,
@@ -171,10 +203,23 @@ class MCPServerImporter:
171
203
  from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, TextBlock, query
172
204
 
173
205
  # Build prompt to fetch and extract config
174
- prompt = self.import_config.github_fetch_prompt.format(github_url=github_url)
206
+ prompt_path = self.import_config.github_fetch_prompt_path or "import/github_fetch"
207
+ try:
208
+ prompt = self._loader.render(prompt_path, {"github_url": github_url})
209
+ except Exception as e:
210
+ logger.warning(f"Failed to load Github fetch prompt: {e}")
211
+ prompt = DEFAULT_GITHUB_FETCH_PROMPT.format(github_url=github_url)
212
+
213
+ # Get system prompt
214
+ sys_prompt_path = self.import_config.prompt_path or "import/system"
215
+ try:
216
+ system_prompt = self._loader.render(sys_prompt_path, {})
217
+ except Exception as e:
218
+ logger.warning(f"Failed to load import system prompt: {e}")
219
+ system_prompt = DEFAULT_IMPORT_MCP_SERVER_PROMPT
175
220
 
176
221
  options = ClaudeAgentOptions(
177
- system_prompt=self.import_config.prompt,
222
+ system_prompt=system_prompt,
178
223
  max_turns=3,
179
224
  model=self.import_config.model,
180
225
  allowed_tools=["WebFetch"],
@@ -222,10 +267,23 @@ class MCPServerImporter:
222
267
  from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, TextBlock, query
223
268
 
224
269
  # Build prompt to search and extract config
225
- prompt = self.import_config.search_fetch_prompt.format(search_query=search_query)
270
+ prompt_path = self.import_config.search_fetch_prompt_path or "import/search_fetch"
271
+ try:
272
+ prompt = self._loader.render(prompt_path, {"search_query": search_query})
273
+ except Exception as e:
274
+ logger.warning(f"Failed to load search fetch prompt: {e}")
275
+ prompt = DEFAULT_SEARCH_FETCH_PROMPT.format(search_query=search_query)
276
+
277
+ # Get system prompt
278
+ sys_prompt_path = self.import_config.prompt_path or "import/system"
279
+ try:
280
+ system_prompt = self._loader.render(sys_prompt_path, {})
281
+ except Exception as e:
282
+ logger.warning(f"Failed to load import system prompt: {e}")
283
+ system_prompt = DEFAULT_IMPORT_MCP_SERVER_PROMPT
226
284
 
227
285
  options = ClaudeAgentOptions(
228
- system_prompt=self.import_config.prompt,
286
+ system_prompt=system_prompt,
229
287
  max_turns=5, # More turns for search + fetch
230
288
  model=self.import_config.model,
231
289
  allowed_tools=["WebSearch", "WebFetch"],
@@ -0,0 +1,56 @@
1
+ """Gobby MCP server instructions.
2
+
3
+ Provides XML-structured instructions that teach agents how to use Gobby correctly.
4
+ These instructions are injected into the MCP server via FastMCP's `instructions` parameter.
5
+ """
6
+
7
+
8
+ def build_gobby_instructions() -> str:
9
+ """Build XML-structured instructions for Gobby MCP server.
10
+
11
+ These instructions teach agents how to use Gobby correctly.
12
+ Every agent connecting to Gobby receives these automatically.
13
+
14
+ The instructions cover:
15
+ - Session startup sequence
16
+ - Progressive tool disclosure pattern
17
+ - Progressive skill disclosure pattern
18
+ - Critical rules for task management
19
+
20
+ Returns:
21
+ XML-structured instructions string
22
+ """
23
+ return """<gobby_system>
24
+
25
+ <startup>
26
+ At the start of EVERY session:
27
+ 1. `list_mcp_servers()` — Discover available servers
28
+ 2. `list_skills()` — Discover available skills
29
+ 3. Session ID: Look for `session_id: <uuid>` in your context.
30
+ If missing, call:
31
+ `call_tool("gobby-sessions", "get_current", {"external_id": "<your-session-id>", "source": "claude"})`
32
+
33
+ Session and task references use `#N` format (e.g., `#1`, `#42`) which is project-scoped.
34
+ </startup>
35
+
36
+ <tool_discovery>
37
+ NEVER assume tool schemas. Use progressive disclosure:
38
+ 1. `list_tools(server="...")` — Lightweight metadata (~100 tokens/tool)
39
+ 2. `get_tool_schema(server, tool)` — Full schema when needed
40
+ 3. `call_tool(server, tool, args)` — Execute
41
+ </tool_discovery>
42
+
43
+ <skill_discovery>
44
+ Skills provide detailed guidance. Use progressive disclosure:
45
+ 1. `list_skills()` — Already done at startup
46
+ 2. `get_skill(name="...")` — Full content when needed
47
+ 3. `search_skills(query="...")` — Find by task description
48
+ </skill_discovery>
49
+
50
+ <rules>
51
+ - Create/claim a task before using Edit, Write, or NotebookEdit tools
52
+ - Pass session_id to create_task (required), claim_task (required), and close_task (optional, for tracking)
53
+ - NEVER load all tool schemas upfront — use progressive disclosure
54
+ </rules>
55
+
56
+ </gobby_system>"""
gobby/mcp_proxy/models.py CHANGED
@@ -31,6 +31,21 @@ class MCPError(Exception):
31
31
  self.code = code
32
32
 
33
33
 
34
+ class ToolProxyErrorCode(str, Enum):
35
+ """Structured error codes for ToolProxyService responses.
36
+
37
+ Used by _process_tool_proxy_result to determine HTTP status codes
38
+ without fragile string matching.
39
+ """
40
+
41
+ SERVER_NOT_FOUND = "SERVER_NOT_FOUND"
42
+ SERVER_NOT_CONFIGURED = "SERVER_NOT_CONFIGURED"
43
+ TOOL_NOT_FOUND = "TOOL_NOT_FOUND"
44
+ INVALID_ARGUMENTS = "INVALID_ARGUMENTS"
45
+ EXECUTION_ERROR = "EXECUTION_ERROR"
46
+ CONNECTION_ERROR = "CONNECTION_ERROR"
47
+
48
+
34
49
  class HealthState(str, Enum):
35
50
  """Connection health state for monitoring."""
36
51
 
@@ -16,13 +16,14 @@ if TYPE_CHECKING:
16
16
  from gobby.mcp_proxy.services.tool_proxy import ToolProxyService
17
17
  from gobby.memory.manager import MemoryManager
18
18
  from gobby.sessions.manager import SessionManager
19
+ from gobby.storage.clones import LocalCloneManager
20
+ from gobby.storage.inter_session_messages import InterSessionMessageManager
19
21
  from gobby.storage.merge_resolutions import MergeResolutionManager
20
22
  from gobby.storage.session_messages import LocalSessionMessageManager
21
23
  from gobby.storage.sessions import LocalSessionManager
22
24
  from gobby.storage.tasks import LocalTaskManager
23
25
  from gobby.storage.worktrees import LocalWorktreeManager
24
26
  from gobby.sync.tasks import TaskSyncManager
25
- from gobby.tasks.expansion import TaskExpander
26
27
  from gobby.tasks.validation import TaskValidator
27
28
  from gobby.worktrees.git import WorktreeGitManager
28
29
  from gobby.worktrees.merge import MergeResolver
@@ -36,7 +37,6 @@ def setup_internal_registries(
36
37
  memory_manager: MemoryManager | None = None,
37
38
  task_manager: LocalTaskManager | None = None,
38
39
  sync_manager: TaskSyncManager | None = None,
39
- task_expander: TaskExpander | None = None,
40
40
  task_validator: TaskValidator | None = None,
41
41
  message_manager: LocalSessionMessageManager | None = None,
42
42
  local_session_manager: LocalSessionManager | None = None,
@@ -44,11 +44,13 @@ def setup_internal_registries(
44
44
  llm_service: LLMService | None = None,
45
45
  agent_runner: AgentRunner | None = None,
46
46
  worktree_storage: LocalWorktreeManager | None = None,
47
+ clone_storage: LocalCloneManager | None = None,
47
48
  git_manager: WorktreeGitManager | None = None,
48
49
  merge_storage: MergeResolutionManager | None = None,
49
50
  merge_resolver: MergeResolver | None = None,
50
51
  project_id: str | None = None,
51
52
  tool_proxy_getter: Callable[[], ToolProxyService | None] | None = None,
53
+ inter_session_message_manager: InterSessionMessageManager | None = None,
52
54
  ) -> InternalRegistryManager:
53
55
  """
54
56
  Setup internal MCP registries (tasks, messages, memory, metrics, agents, worktrees).
@@ -59,7 +61,6 @@ def setup_internal_registries(
59
61
  memory_manager: Memory manager for memory operations
60
62
  task_manager: Task storage manager
61
63
  sync_manager: Task sync manager for git sync
62
- task_expander: Task expander for AI expansion
63
64
  task_validator: Task validator for validation
64
65
  message_manager: Message storage manager
65
66
  local_session_manager: Local session manager for session CRUD
@@ -73,6 +74,7 @@ def setup_internal_registries(
73
74
  project_id: Default project ID for worktree operations
74
75
  tool_proxy_getter: Callable that returns ToolProxyService for routing
75
76
  tool calls in in-process agents. Called lazily during agent execution.
77
+ inter_session_message_manager: Inter-session message manager for agent messaging
76
78
 
77
79
  Returns:
78
80
  InternalRegistryManager containing all registries
@@ -99,7 +101,6 @@ def setup_internal_registries(
99
101
  tasks_registry = create_task_registry(
100
102
  task_manager=task_manager,
101
103
  sync_manager=sync_manager,
102
- task_expander=task_expander,
103
104
  task_validator=task_validator,
104
105
  config=_config,
105
106
  agent_runner=agent_runner,
@@ -113,7 +114,7 @@ def setup_internal_registries(
113
114
  # Initialize sessions registry (messages + session CRUD)
114
115
  # Register if either message_manager or local_session_manager is available
115
116
  if message_manager is not None or local_session_manager is not None:
116
- from gobby.mcp_proxy.tools.session_messages import create_session_messages_registry
117
+ from gobby.mcp_proxy.tools.sessions import create_session_messages_registry
117
118
 
118
119
  session_messages_registry = create_session_messages_registry(
119
120
  message_manager=message_manager,
@@ -150,20 +151,41 @@ def setup_internal_registries(
150
151
  if metrics_manager is not None:
151
152
  from gobby.mcp_proxy.tools.metrics import create_metrics_registry
152
153
 
154
+ # Get daily budget from conductor config if available
155
+ daily_budget_usd = 50.0 # Default
156
+ if _config is not None:
157
+ conductor_config = _config.conductor
158
+ if conductor_config is not None:
159
+ daily_budget_usd = conductor_config.daily_budget_usd
160
+
153
161
  metrics_registry = create_metrics_registry(
154
162
  metrics_manager=metrics_manager,
163
+ session_storage=local_session_manager,
164
+ daily_budget_usd=daily_budget_usd,
155
165
  )
156
166
  manager.add_registry(metrics_registry)
157
- logger.debug("Metrics registry initialized")
167
+ logger.debug("Metrics registry initialized with token tracking")
158
168
 
159
169
  # Initialize agents registry if agent_runner is available
160
170
  if agent_runner is not None:
171
+ from gobby.agents.registry import get_running_agent_registry
161
172
  from gobby.mcp_proxy.tools.agents import create_agents_registry
162
173
 
163
174
  agents_registry = create_agents_registry(
164
175
  runner=agent_runner,
165
- tool_proxy_getter=tool_proxy_getter,
166
176
  )
177
+
178
+ # Add inter-agent messaging tools if message manager is available
179
+ if inter_session_message_manager is not None:
180
+ from gobby.mcp_proxy.tools.agent_messaging import add_messaging_tools
181
+
182
+ add_messaging_tools(
183
+ registry=agents_registry,
184
+ message_manager=inter_session_message_manager,
185
+ agent_registry=get_running_agent_registry(),
186
+ )
187
+ logger.debug("Agent messaging tools added to agents registry")
188
+
167
189
  manager.add_registry(agents_registry)
168
190
  logger.debug("Agents registry initialized")
169
191
 
@@ -175,11 +197,35 @@ def setup_internal_registries(
175
197
  worktree_storage=worktree_storage,
176
198
  git_manager=git_manager,
177
199
  project_id=project_id,
178
- agent_runner=agent_runner,
179
200
  )
180
201
  manager.add_registry(worktrees_registry)
181
202
  logger.debug("Worktrees registry initialized")
182
203
 
204
+ # Initialize clones registry if clone_storage is available
205
+ if clone_storage is not None:
206
+ from gobby.clones.git import CloneGitManager
207
+ from gobby.mcp_proxy.tools.clones import create_clones_registry
208
+
209
+ # Create CloneGitManager from the same repo path as WorktreeGitManager
210
+ clone_git_manager = None
211
+ if git_manager is not None:
212
+ try:
213
+ clone_git_manager = CloneGitManager(git_manager.repo_path)
214
+ except Exception as e:
215
+ logger.warning(f"Failed to create CloneGitManager: {e}")
216
+
217
+ # Only create clones registry if we have a git manager
218
+ if clone_git_manager is not None:
219
+ clones_registry = create_clones_registry(
220
+ clone_storage=clone_storage,
221
+ git_manager=clone_git_manager,
222
+ project_id=project_id or "",
223
+ )
224
+ manager.add_registry(clones_registry)
225
+ logger.debug("Clones registry initialized")
226
+ else:
227
+ logger.debug("Clones registry not initialized: CloneGitManager not available")
228
+
183
229
  # Initialize merge resolution registry if merge components are available
184
230
  if merge_storage is not None and merge_resolver is not None:
185
231
  from gobby.mcp_proxy.tools.merge import create_merge_registry
@@ -204,6 +250,20 @@ def setup_internal_registries(
204
250
  manager.add_registry(hub_registry)
205
251
  logger.debug("Hub registry initialized")
206
252
 
253
+ # Initialize skills registry using the existing database from task_manager
254
+ # to avoid creating a duplicate connection that would leak
255
+ if task_manager is not None:
256
+ from gobby.mcp_proxy.tools.skills import create_skills_registry
257
+
258
+ skills_registry = create_skills_registry(
259
+ db=task_manager.db,
260
+ project_id=project_id,
261
+ )
262
+ manager.add_registry(skills_registry)
263
+ logger.debug("Skills registry initialized")
264
+ else:
265
+ logger.debug("Skills registry not initialized: task_manager is None")
266
+
207
267
  logger.info(f"Internal registries initialized: {len(manager)} registries")
208
268
  return manager
209
269
 
gobby/mcp_proxy/server.py CHANGED
@@ -2,13 +2,16 @@
2
2
  Gobby Daemon Tools MCP Server.
3
3
  """
4
4
 
5
+ import json
5
6
  import logging
6
7
  from datetime import UTC
7
8
  from typing import Any
8
9
 
9
10
  from mcp.server.fastmcp import FastMCP
11
+ from mcp.types import CallToolResult, TextContent
10
12
 
11
13
  from gobby.config.app import DaemonConfig
14
+ from gobby.mcp_proxy.instructions import build_gobby_instructions
12
15
  from gobby.mcp_proxy.manager import MCPClientManager
13
16
  from gobby.mcp_proxy.services.recommendation import RecommendationService, SearchMode
14
17
  from gobby.mcp_proxy.services.server_mgmt import ServerManagementService
@@ -96,8 +99,35 @@ class GobbyDaemonTools:
96
99
  tool_name: str,
97
100
  arguments: dict[str, Any] | None = None,
98
101
  ) -> Any:
99
- """Call a tool."""
100
- return await self.tool_proxy.call_tool(server_name, tool_name, arguments)
102
+ """Call a tool.
103
+
104
+ Returns the tool result, or a CallToolResult with isError=True if the
105
+ underlying service indicates an error. This ensures the MCP protocol
106
+ properly signals errors to LLM clients instead of returning error dicts
107
+ as successful responses.
108
+ """
109
+ result = await self.tool_proxy.call_tool(server_name, tool_name, arguments)
110
+
111
+ # Check if result indicates an error (ToolProxyService returns dict with success: False)
112
+ if isinstance(result, dict) and result.get("success") is False:
113
+ # Build helpful error message with schema hint if available
114
+ error_msg = result.get("error", "Unknown error")
115
+ hint = result.get("hint", "")
116
+ schema = result.get("schema")
117
+
118
+ parts = [f"Error: {error_msg}"]
119
+ if hint:
120
+ parts.append(f"\n{hint}")
121
+ if schema:
122
+ parts.append(f"\nCorrect schema:\n{json.dumps(schema, indent=2)}")
123
+
124
+ # Return MCP error response with isError=True
125
+ return CallToolResult(
126
+ content=[TextContent(type="text", text="\n".join(parts))],
127
+ isError=True,
128
+ )
129
+
130
+ return result
101
131
 
102
132
  async def list_tools(self, server: str, session_id: str | None = None) -> dict[str, Any]:
103
133
  """List tools for a specific server, optionally filtered by workflow phase restrictions."""
@@ -513,7 +543,7 @@ class GobbyDaemonTools:
513
543
 
514
544
  def create_mcp_server(tools_handler: GobbyDaemonTools) -> FastMCP:
515
545
  """Create the FastMCP server instance for the HTTP daemon."""
516
- mcp = FastMCP("gobby")
546
+ mcp = FastMCP("gobby", instructions=build_gobby_instructions())
517
547
 
518
548
  # System tools
519
549
  mcp.add_tool(tools_handler.status)