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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. gobby/adapters/claude_code.py +13 -4
  2. gobby/adapters/codex.py +43 -3
  3. gobby/agents/runner.py +8 -0
  4. gobby/cli/__init__.py +6 -0
  5. gobby/cli/clones.py +419 -0
  6. gobby/cli/conductor.py +266 -0
  7. gobby/cli/installers/antigravity.py +3 -9
  8. gobby/cli/installers/claude.py +9 -9
  9. gobby/cli/installers/codex.py +2 -8
  10. gobby/cli/installers/gemini.py +2 -8
  11. gobby/cli/installers/shared.py +71 -8
  12. gobby/cli/skills.py +858 -0
  13. gobby/cli/tasks/ai.py +0 -440
  14. gobby/cli/tasks/crud.py +44 -6
  15. gobby/cli/tasks/main.py +0 -4
  16. gobby/cli/tui.py +2 -2
  17. gobby/cli/utils.py +3 -3
  18. gobby/clones/__init__.py +13 -0
  19. gobby/clones/git.py +547 -0
  20. gobby/conductor/__init__.py +16 -0
  21. gobby/conductor/alerts.py +135 -0
  22. gobby/conductor/loop.py +164 -0
  23. gobby/conductor/monitors/__init__.py +11 -0
  24. gobby/conductor/monitors/agents.py +116 -0
  25. gobby/conductor/monitors/tasks.py +155 -0
  26. gobby/conductor/pricing.py +234 -0
  27. gobby/conductor/token_tracker.py +160 -0
  28. gobby/config/app.py +63 -1
  29. gobby/config/search.py +110 -0
  30. gobby/config/servers.py +1 -1
  31. gobby/config/skills.py +43 -0
  32. gobby/config/tasks.py +6 -14
  33. gobby/hooks/event_handlers.py +145 -2
  34. gobby/hooks/hook_manager.py +48 -2
  35. gobby/hooks/skill_manager.py +130 -0
  36. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  37. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  38. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  39. gobby/llm/claude.py +22 -34
  40. gobby/llm/claude_executor.py +46 -256
  41. gobby/llm/codex_executor.py +59 -291
  42. gobby/llm/executor.py +21 -0
  43. gobby/llm/gemini.py +134 -110
  44. gobby/llm/litellm_executor.py +143 -6
  45. gobby/llm/resolver.py +95 -33
  46. gobby/mcp_proxy/instructions.py +54 -0
  47. gobby/mcp_proxy/models.py +15 -0
  48. gobby/mcp_proxy/registries.py +68 -5
  49. gobby/mcp_proxy/server.py +33 -3
  50. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  51. gobby/mcp_proxy/stdio.py +2 -1
  52. gobby/mcp_proxy/tools/__init__.py +0 -2
  53. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  54. gobby/mcp_proxy/tools/clones.py +903 -0
  55. gobby/mcp_proxy/tools/memory.py +1 -24
  56. gobby/mcp_proxy/tools/metrics.py +65 -1
  57. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  58. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  59. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  60. gobby/mcp_proxy/tools/session_messages.py +1 -2
  61. gobby/mcp_proxy/tools/skills/__init__.py +631 -0
  62. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  63. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  64. gobby/mcp_proxy/tools/task_sync.py +1 -1
  65. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  66. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  67. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  68. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  69. gobby/mcp_proxy/tools/tasks/_lifecycle.py +60 -29
  70. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  71. gobby/mcp_proxy/tools/workflows.py +1 -1
  72. gobby/mcp_proxy/tools/worktrees.py +5 -0
  73. gobby/memory/backends/__init__.py +6 -1
  74. gobby/memory/backends/mem0.py +6 -1
  75. gobby/memory/extractor.py +477 -0
  76. gobby/memory/manager.py +11 -2
  77. gobby/prompts/defaults/handoff/compact.md +63 -0
  78. gobby/prompts/defaults/handoff/session_end.md +57 -0
  79. gobby/prompts/defaults/memory/extract.md +61 -0
  80. gobby/runner.py +37 -16
  81. gobby/search/__init__.py +48 -6
  82. gobby/search/backends/__init__.py +159 -0
  83. gobby/search/backends/embedding.py +225 -0
  84. gobby/search/embeddings.py +238 -0
  85. gobby/search/models.py +148 -0
  86. gobby/search/unified.py +496 -0
  87. gobby/servers/http.py +23 -8
  88. gobby/servers/routes/admin.py +280 -0
  89. gobby/servers/routes/mcp/tools.py +241 -52
  90. gobby/servers/websocket.py +2 -2
  91. gobby/sessions/analyzer.py +2 -0
  92. gobby/sessions/transcripts/base.py +1 -0
  93. gobby/sessions/transcripts/claude.py +64 -5
  94. gobby/skills/__init__.py +91 -0
  95. gobby/skills/loader.py +685 -0
  96. gobby/skills/manager.py +384 -0
  97. gobby/skills/parser.py +258 -0
  98. gobby/skills/search.py +463 -0
  99. gobby/skills/sync.py +119 -0
  100. gobby/skills/updater.py +385 -0
  101. gobby/skills/validator.py +368 -0
  102. gobby/storage/clones.py +378 -0
  103. gobby/storage/database.py +1 -1
  104. gobby/storage/memories.py +43 -13
  105. gobby/storage/migrations.py +180 -6
  106. gobby/storage/sessions.py +73 -0
  107. gobby/storage/skills.py +749 -0
  108. gobby/storage/tasks/_crud.py +4 -4
  109. gobby/storage/tasks/_lifecycle.py +41 -6
  110. gobby/storage/tasks/_manager.py +14 -5
  111. gobby/storage/tasks/_models.py +8 -3
  112. gobby/sync/memories.py +39 -4
  113. gobby/sync/tasks.py +83 -6
  114. gobby/tasks/__init__.py +1 -2
  115. gobby/tasks/validation.py +24 -15
  116. gobby/tui/api_client.py +4 -7
  117. gobby/tui/app.py +5 -3
  118. gobby/tui/screens/orchestrator.py +1 -2
  119. gobby/tui/screens/tasks.py +2 -4
  120. gobby/tui/ws_client.py +1 -1
  121. gobby/utils/daemon_client.py +2 -2
  122. gobby/workflows/actions.py +84 -2
  123. gobby/workflows/context_actions.py +43 -0
  124. gobby/workflows/detection_helpers.py +115 -31
  125. gobby/workflows/engine.py +13 -2
  126. gobby/workflows/lifecycle_evaluator.py +29 -1
  127. gobby/workflows/loader.py +19 -6
  128. gobby/workflows/memory_actions.py +74 -0
  129. gobby/workflows/summary_actions.py +17 -0
  130. gobby/workflows/task_enforcement_actions.py +448 -6
  131. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/METADATA +82 -21
  132. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/RECORD +136 -107
  133. gobby/install/codex/prompts/forget.md +0 -7
  134. gobby/install/codex/prompts/memories.md +0 -7
  135. gobby/install/codex/prompts/recall.md +0 -7
  136. gobby/install/codex/prompts/remember.md +0 -13
  137. gobby/llm/gemini_executor.py +0 -339
  138. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  139. gobby/tasks/context.py +0 -747
  140. gobby/tasks/criteria.py +0 -342
  141. gobby/tasks/expansion.py +0 -626
  142. gobby/tasks/prompts/expand.py +0 -327
  143. gobby/tasks/research.py +0 -421
  144. gobby/tasks/tdd.py +0 -352
  145. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/WHEEL +0 -0
  146. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/entry_points.txt +0 -0
  147. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/licenses/LICENSE.md +0 -0
  148. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/top_level.txt +0 -0
gobby/llm/resolver.py CHANGED
@@ -259,8 +259,13 @@ def create_executor(
259
259
  """
260
260
  Create an AgentExecutor for the given provider.
261
261
 
262
+ Routing strategy:
263
+ - api_key and adc auth modes: Route to LiteLLMExecutor for unified cost tracking
264
+ - subscription mode (Claude): Route to ClaudeExecutor (Claude Agent SDK)
265
+ - cli mode (Codex): Route to CodexExecutor (Codex CLI subprocess)
266
+
262
267
  Args:
263
- provider: Provider name (claude, gemini, litellm).
268
+ provider: Provider name (claude, gemini, litellm, codex).
264
269
  config: Optional daemon config for provider settings.
265
270
  model: Optional model override.
266
271
 
@@ -279,19 +284,40 @@ def create_executor(
279
284
  if config and config.llm_providers:
280
285
  provider_config = getattr(config.llm_providers, provider, None)
281
286
 
287
+ # Determine auth_mode from config
288
+ auth_mode = "api_key" # Default
289
+ if provider_config:
290
+ auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
291
+
282
292
  try:
283
- if provider == "claude":
293
+ # Route based on auth_mode:
294
+ # - subscription (Claude) -> ClaudeExecutor
295
+ # - cli (Codex) -> CodexExecutor
296
+ # - api_key/adc (all providers) -> LiteLLMExecutor
297
+
298
+ if provider == "claude" and auth_mode == "subscription":
299
+ # Subscription mode requires Claude Agent SDK
284
300
  return _create_claude_executor(provider_config, model)
285
- elif provider == "gemini":
286
- return _create_gemini_executor(provider_config, model)
301
+
302
+ elif provider == "codex" and auth_mode in ("subscription", "cli"):
303
+ # CLI mode requires Codex CLI subprocess
304
+ return _create_codex_executor(provider_config, model, auth_mode)
305
+
287
306
  elif provider == "litellm":
307
+ # Direct LiteLLM usage
288
308
  return _create_litellm_executor(provider_config, config, model)
289
- elif provider == "codex":
290
- return _create_codex_executor(provider_config, model)
309
+
310
+ elif auth_mode in ("api_key", "adc"):
311
+ # Route all api_key and adc modes through LiteLLM for unified cost tracking
312
+ return _create_litellm_executor_for_provider(
313
+ provider, auth_mode, provider_config, config, model
314
+ )
315
+
291
316
  else:
292
317
  raise ExecutorCreationError(
293
318
  provider,
294
- f"Unknown provider. Supported: {list(SUPPORTED_PROVIDERS)}",
319
+ f"Unknown provider/auth_mode combination: {provider}/{auth_mode}. "
320
+ f"Supported: {list(SUPPORTED_PROVIDERS)}",
295
321
  )
296
322
  except ProviderError:
297
323
  raise
@@ -303,15 +329,18 @@ def _create_claude_executor(
303
329
  provider_config: "LLMProviderConfig | None",
304
330
  model: str | None,
305
331
  ) -> AgentExecutor:
306
- """Create ClaudeExecutor with appropriate auth mode."""
332
+ """
333
+ Create ClaudeExecutor for subscription mode only.
334
+
335
+ Note: api_key mode is now routed through LiteLLMExecutor for unified cost tracking.
336
+ This function should only be called when auth_mode is "subscription".
337
+ """
307
338
  from gobby.llm.claude_executor import ClaudeExecutor
308
339
 
309
- # Determine auth mode and model from config
310
- auth_mode = "api_key"
340
+ # Subscription mode only - api_key mode routes through LiteLLM
311
341
  default_model = "claude-sonnet-4-20250514"
312
342
 
313
343
  if provider_config:
314
- auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
315
344
  # Get first model from comma-separated list if set
316
345
  models_str = getattr(provider_config, "models", None)
317
346
  if models_str:
@@ -320,46 +349,71 @@ def _create_claude_executor(
320
349
  default_model = models[0]
321
350
 
322
351
  return ClaudeExecutor(
323
- auth_mode=auth_mode, # type: ignore[arg-type]
352
+ auth_mode="subscription",
324
353
  default_model=model or default_model,
325
354
  )
326
355
 
327
356
 
328
- def _create_gemini_executor(
357
+ def _create_litellm_executor(
329
358
  provider_config: "LLMProviderConfig | None",
359
+ config: "DaemonConfig | None",
330
360
  model: str | None,
331
361
  ) -> AgentExecutor:
332
- """Create GeminiExecutor with appropriate auth mode."""
333
- from gobby.llm.gemini_executor import GeminiExecutor
362
+ """Create LiteLLMExecutor with API keys from config (direct litellm usage)."""
363
+ from gobby.llm.litellm_executor import LiteLLMExecutor
334
364
 
335
- # Determine auth mode and model from config
336
- auth_mode = "api_key"
337
- default_model = "gemini-2.0-flash"
365
+ # Determine model and API base from config
366
+ default_model = "gpt-4o-mini"
367
+ api_base = None
368
+ api_keys: dict[str, str] | None = None
338
369
 
339
370
  if provider_config:
340
- auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
341
371
  models_str = getattr(provider_config, "models", None)
342
372
  if models_str:
343
373
  models = [m.strip() for m in models_str.split(",") if m.strip()]
344
374
  if models:
345
375
  default_model = models[0]
376
+ api_base = getattr(provider_config, "api_base", None)
346
377
 
347
- return GeminiExecutor(
348
- auth_mode=auth_mode, # type: ignore[arg-type]
378
+ # Get API keys from llm_providers.api_keys
379
+ if config and config.llm_providers:
380
+ api_keys = config.llm_providers.api_keys or None
381
+
382
+ return LiteLLMExecutor(
349
383
  default_model=model or default_model,
384
+ api_base=api_base,
385
+ api_keys=api_keys,
350
386
  )
351
387
 
352
388
 
353
- def _create_litellm_executor(
389
+ def _create_litellm_executor_for_provider(
390
+ provider: str,
391
+ auth_mode: str,
354
392
  provider_config: "LLMProviderConfig | None",
355
393
  config: "DaemonConfig | None",
356
394
  model: str | None,
357
395
  ) -> AgentExecutor:
358
- """Create LiteLLMExecutor with API keys from config."""
396
+ """
397
+ Create LiteLLMExecutor configured for a specific provider's api_key/adc mode.
398
+
399
+ This routes provider-specific calls through LiteLLM for unified cost tracking:
400
+ - Claude (api_key) -> anthropic/model
401
+ - Gemini (api_key) -> gemini/model
402
+ - Gemini (adc) -> vertex_ai/model
403
+ - Codex/OpenAI (api_key) -> model (no prefix)
404
+ """
359
405
  from gobby.llm.litellm_executor import LiteLLMExecutor
360
406
 
361
- # Determine model and API base from config
362
- default_model = "gpt-4o-mini"
407
+ # Default models per provider
408
+ default_models = {
409
+ "claude": "claude-sonnet-4-20250514",
410
+ "gemini": "gemini-2.0-flash",
411
+ "codex": "gpt-4o",
412
+ "openai": "gpt-4o",
413
+ }
414
+
415
+ # Determine model from config
416
+ default_model = default_models.get(provider, "gpt-4o-mini")
363
417
  api_base = None
364
418
  api_keys: dict[str, str] | None = None
365
419
 
@@ -375,34 +429,42 @@ def _create_litellm_executor(
375
429
  if config and config.llm_providers:
376
430
  api_keys = config.llm_providers.api_keys or None
377
431
 
432
+ # Cast auth_mode to the expected literal type
433
+ litellm_auth_mode = auth_mode if auth_mode in ("api_key", "adc") else "api_key"
434
+
378
435
  return LiteLLMExecutor(
379
436
  default_model=model or default_model,
380
437
  api_base=api_base,
381
438
  api_keys=api_keys,
439
+ provider=provider, # type: ignore[arg-type]
440
+ auth_mode=litellm_auth_mode, # type: ignore[arg-type]
382
441
  )
383
442
 
384
443
 
385
444
  def _create_codex_executor(
386
445
  provider_config: "LLMProviderConfig | None",
387
446
  model: str | None,
447
+ auth_mode: str = "subscription",
388
448
  ) -> AgentExecutor:
389
449
  """
390
- Create CodexExecutor with appropriate auth mode.
450
+ Create CodexExecutor for subscription/CLI mode only.
451
+
452
+ Note: api_key mode is now routed through LiteLLMExecutor for unified cost tracking.
453
+ This function should only be called when auth_mode is "subscription" or "cli".
391
454
 
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)
455
+ CLI mode uses Codex CLI subprocess - no custom tool injection supported.
395
456
 
396
- See CodexExecutor docstring for detailed mode differences.
457
+ Args:
458
+ provider_config: Provider configuration.
459
+ model: Optional model override.
460
+ auth_mode: Authentication mode - "subscription" or "cli".
397
461
  """
398
462
  from gobby.llm.codex_executor import CodexExecutor
399
463
 
400
- # Determine auth mode and model from config
401
- auth_mode = "api_key"
464
+ # CLI/subscription mode only - api_key mode routes through LiteLLM
402
465
  default_model = "gpt-4o"
403
466
 
404
467
  if provider_config:
405
- auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
406
468
  models_str = getattr(provider_config, "models", None)
407
469
  if models_str:
408
470
  models = [m.strip() for m in models_str.split(",") if m.strip()]
@@ -0,0 +1,54 @@
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
+ </startup>
33
+
34
+ <tool_discovery>
35
+ NEVER assume tool schemas. Use progressive disclosure:
36
+ 1. `list_tools(server="...")` — Lightweight metadata (~100 tokens/tool)
37
+ 2. `get_tool_schema(server, tool)` — Full schema when needed
38
+ 3. `call_tool(server, tool, args)` — Execute
39
+ </tool_discovery>
40
+
41
+ <skill_discovery>
42
+ Skills provide detailed guidance. Use progressive disclosure:
43
+ 1. `list_skills()` — Already done at startup
44
+ 2. `get_skill(name="...")` — Full content when needed
45
+ 3. `search_skills(query="...")` — Find by task description
46
+ </skill_discovery>
47
+
48
+ <rules>
49
+ - Create/claim a task before using Edit, Write, or NotebookEdit tools
50
+ - Pass session_id to create_task (required), claim_task (required), and close_task (optional, for tracking)
51
+ - NEVER load all tool schemas upfront — use progressive disclosure
52
+ </rules>
53
+
54
+ </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,
@@ -150,20 +151,42 @@ 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
176
  tool_proxy_getter=tool_proxy_getter,
166
177
  )
178
+
179
+ # Add inter-agent messaging tools if message manager is available
180
+ if inter_session_message_manager is not None:
181
+ from gobby.mcp_proxy.tools.agent_messaging import add_messaging_tools
182
+
183
+ add_messaging_tools(
184
+ registry=agents_registry,
185
+ message_manager=inter_session_message_manager,
186
+ agent_registry=get_running_agent_registry(),
187
+ )
188
+ logger.debug("Agent messaging tools added to agents registry")
189
+
167
190
  manager.add_registry(agents_registry)
168
191
  logger.debug("Agents registry initialized")
169
192
 
@@ -180,6 +203,32 @@ def setup_internal_registries(
180
203
  manager.add_registry(worktrees_registry)
181
204
  logger.debug("Worktrees registry initialized")
182
205
 
206
+ # Initialize clones registry if clone_storage is available
207
+ if clone_storage is not None:
208
+ from gobby.clones.git import CloneGitManager
209
+ from gobby.mcp_proxy.tools.clones import create_clones_registry
210
+
211
+ # Create CloneGitManager from the same repo path as WorktreeGitManager
212
+ clone_git_manager = None
213
+ if git_manager is not None:
214
+ try:
215
+ clone_git_manager = CloneGitManager(git_manager.repo_path)
216
+ except Exception as e:
217
+ logger.warning(f"Failed to create CloneGitManager: {e}")
218
+
219
+ # Only create clones registry if we have a git manager
220
+ if clone_git_manager is not None:
221
+ clones_registry = create_clones_registry(
222
+ clone_storage=clone_storage,
223
+ git_manager=clone_git_manager,
224
+ project_id=project_id or "",
225
+ agent_runner=agent_runner,
226
+ )
227
+ manager.add_registry(clones_registry)
228
+ logger.debug("Clones registry initialized")
229
+ else:
230
+ logger.debug("Clones registry not initialized: CloneGitManager not available")
231
+
183
232
  # Initialize merge resolution registry if merge components are available
184
233
  if merge_storage is not None and merge_resolver is not None:
185
234
  from gobby.mcp_proxy.tools.merge import create_merge_registry
@@ -204,6 +253,20 @@ def setup_internal_registries(
204
253
  manager.add_registry(hub_registry)
205
254
  logger.debug("Hub registry initialized")
206
255
 
256
+ # Initialize skills registry using the existing database from task_manager
257
+ # to avoid creating a duplicate connection that would leak
258
+ if task_manager is not None:
259
+ from gobby.mcp_proxy.tools.skills import create_skills_registry
260
+
261
+ skills_registry = create_skills_registry(
262
+ db=task_manager.db,
263
+ project_id=project_id,
264
+ )
265
+ manager.add_registry(skills_registry)
266
+ logger.debug("Skills registry initialized")
267
+ else:
268
+ logger.debug("Skills registry not initialized: task_manager is None")
269
+
207
270
  logger.info(f"Internal registries initialized: {len(manager)} registries")
208
271
  return manager
209
272
 
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)
@@ -4,7 +4,7 @@ import logging
4
4
  from typing import TYPE_CHECKING, Any
5
5
 
6
6
  from gobby.mcp_proxy.manager import MCPClientManager
7
- from gobby.mcp_proxy.models import MCPError
7
+ from gobby.mcp_proxy.models import MCPError, ToolProxyErrorCode
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from gobby.mcp_proxy.services.fallback import ToolFallbackResolver
@@ -74,6 +74,70 @@ class ToolProxyService:
74
74
 
75
75
  return errors
76
76
 
77
+ def _is_argument_error(self, error_message: str) -> bool:
78
+ """Detect if error message suggests invalid arguments.
79
+
80
+ Used to determine whether to include tool schema in error response
81
+ to help the caller self-correct.
82
+ """
83
+ indicators = [
84
+ "parameter",
85
+ "argument",
86
+ "required",
87
+ "missing",
88
+ "invalid",
89
+ "unknown",
90
+ "expected",
91
+ "type error",
92
+ "validation",
93
+ "schema",
94
+ "property",
95
+ "field",
96
+ "400",
97
+ "422",
98
+ "-32602", # JSON-RPC invalid params error code
99
+ ]
100
+ error_lower = error_message.lower()
101
+ return any(indicator in error_lower for indicator in indicators)
102
+
103
+ def _classify_error(self, error_message: str, exception: Exception) -> str:
104
+ """Classify an error into a structured error code.
105
+
106
+ Used to provide structured error codes that consumers can rely on
107
+ instead of fragile string matching.
108
+
109
+ Args:
110
+ error_message: The error message string
111
+ exception: The original exception
112
+
113
+ Returns:
114
+ ToolProxyErrorCode value as string
115
+ """
116
+ error_lower = error_message.lower()
117
+
118
+ # Check for server not found/configured errors
119
+ if "server" in error_lower:
120
+ if "not found" in error_lower:
121
+ return ToolProxyErrorCode.SERVER_NOT_FOUND.value
122
+ if "not configured" in error_lower:
123
+ return ToolProxyErrorCode.SERVER_NOT_CONFIGURED.value
124
+
125
+ # Check for tool not found
126
+ if "tool" in error_lower and "not found" in error_lower:
127
+ return ToolProxyErrorCode.TOOL_NOT_FOUND.value
128
+
129
+ # Check for argument/validation errors
130
+ if self._is_argument_error(error_message):
131
+ return ToolProxyErrorCode.INVALID_ARGUMENTS.value
132
+
133
+ # Check for connection errors
134
+ connection_indicators = ["connection", "timeout", "refused", "unreachable", "circuit"]
135
+ if any(ind in error_lower for ind in connection_indicators):
136
+ return ToolProxyErrorCode.CONNECTION_ERROR.value
137
+
138
+ # Default to execution error
139
+ return ToolProxyErrorCode.EXECUTION_ERROR.value
140
+
77
141
  async def list_tools(
78
142
  self,
79
143
  server_name: str,
@@ -193,10 +257,26 @@ class ToolProxyService:
193
257
  response: dict[str, Any] = {
194
258
  "success": False,
195
259
  "error": error_message,
260
+ "error_code": self._classify_error(error_message, e),
196
261
  "server_name": server_name,
197
262
  "tool_name": tool_name,
198
263
  }
199
264
 
265
+ # Enrich with schema if error looks like an argument validation error
266
+ if self._is_argument_error(error_message):
267
+ try:
268
+ schema_result = await self.get_tool_schema(server_name, tool_name)
269
+ if schema_result.get("success"):
270
+ input_schema = schema_result.get("tool", {}).get("inputSchema", {})
271
+ if input_schema:
272
+ response["hint"] = (
273
+ "This appears to be an argument error. "
274
+ "Schema provided for self-correction."
275
+ )
276
+ response["schema"] = input_schema
277
+ except Exception as schema_error:
278
+ logger.debug(f"Could not fetch schema for error enrichment: {schema_error}")
279
+
200
280
  # Get fallback suggestions if resolver is available
201
281
  if self._fallback_resolver:
202
282
  try:
gobby/mcp_proxy/stdio.py CHANGED
@@ -22,6 +22,7 @@ from gobby.mcp_proxy.daemon_control import (
22
22
  start_daemon_process,
23
23
  stop_daemon_process,
24
24
  )
25
+ from gobby.mcp_proxy.instructions import build_gobby_instructions
25
26
  from gobby.mcp_proxy.registries import setup_internal_registries
26
27
 
27
28
  __all__ = [
@@ -264,7 +265,7 @@ def create_stdio_mcp_server() -> FastMCP:
264
265
  _ = setup_internal_registries(config, session_manager, memory_manager)
265
266
 
266
267
  # Initialize MCP server and daemon proxy
267
- mcp = FastMCP("gobby")
268
+ mcp = FastMCP("gobby", instructions=build_gobby_instructions())
268
269
  proxy = DaemonProxy(config.daemon_port)
269
270
 
270
271
  register_proxy_tools(mcp, proxy)
@@ -7,7 +7,6 @@ Provides factory functions for creating tool registries.
7
7
  # Main task registry (facade that merges all task-related registries)
8
8
  # Extracted task module registries (for direct use or testing)
9
9
  from gobby.mcp_proxy.tools.task_dependencies import create_dependency_registry
10
- from gobby.mcp_proxy.tools.task_expansion import create_expansion_registry
11
10
  from gobby.mcp_proxy.tools.task_github import create_github_sync_registry
12
11
  from gobby.mcp_proxy.tools.task_readiness import create_readiness_registry
13
12
  from gobby.mcp_proxy.tools.task_sync import create_sync_registry
@@ -19,7 +18,6 @@ __all__ = [
19
18
  "create_task_registry",
20
19
  # Extracted registries
21
20
  "create_dependency_registry",
22
- "create_expansion_registry",
23
21
  "create_github_sync_registry",
24
22
  "create_readiness_registry",
25
23
  "create_sync_registry",