gobby 0.2.5__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 (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. gobby-0.2.5.dist-info/top_level.txt +1 -0
gobby/runner.py ADDED
@@ -0,0 +1,488 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import signal
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import uvicorn
10
+
11
+ from gobby.agents.runner import AgentRunner
12
+ from gobby.config.app import load_config
13
+ from gobby.llm import LLMService, create_llm_service
14
+ from gobby.llm.resolver import ExecutorRegistry
15
+ from gobby.mcp_proxy.manager import MCPClientManager
16
+ from gobby.memory.manager import MemoryManager
17
+ from gobby.servers.http import HTTPServer
18
+ from gobby.servers.websocket import WebSocketConfig, WebSocketServer
19
+ from gobby.sessions.lifecycle import SessionLifecycleManager
20
+ from gobby.sessions.processor import SessionMessageProcessor
21
+ from gobby.storage.database import DatabaseProtocol, LocalDatabase
22
+ from gobby.storage.mcp import LocalMCPManager
23
+ from gobby.storage.migrations import run_migrations
24
+ from gobby.storage.session_messages import LocalSessionMessageManager
25
+ from gobby.storage.session_tasks import SessionTaskManager
26
+ from gobby.storage.sessions import LocalSessionManager
27
+ from gobby.storage.tasks import LocalTaskManager
28
+ from gobby.storage.worktrees import LocalWorktreeManager
29
+ from gobby.sync.memories import MemorySyncManager
30
+ from gobby.sync.tasks import TaskSyncManager
31
+ from gobby.tasks.expansion import TaskExpander
32
+ from gobby.tasks.validation import TaskValidator
33
+ from gobby.utils.logging import setup_file_logging
34
+ from gobby.utils.machine_id import get_machine_id
35
+
36
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ class GobbyRunner:
42
+ """Runner for Gobby daemon."""
43
+
44
+ def __init__(self, config_path: Path | None = None, verbose: bool = False):
45
+ setup_file_logging(verbose=verbose)
46
+ # setup_mcp_logging(verbose=verbose) # Removed as per instruction
47
+
48
+ config_file = str(config_path) if config_path else None
49
+ self.config = load_config(config_file)
50
+ self.verbose = verbose
51
+ self.machine_id = get_machine_id()
52
+ self._shutdown_requested = False
53
+ self._metrics_cleanup_task: asyncio.Task[None] | None = None
54
+
55
+ # Initialize local storage with dual-write if in project context
56
+ self.database = self._init_database()
57
+ self.session_manager = LocalSessionManager(self.database)
58
+ self.message_manager = LocalSessionMessageManager(self.database)
59
+ self.task_manager = LocalTaskManager(self.database)
60
+ self.session_task_manager = SessionTaskManager(self.database)
61
+
62
+ # Initialize LLM Service
63
+ self.llm_service: LLMService | None = None # Added type hint
64
+ try:
65
+ self.llm_service = create_llm_service(self.config)
66
+ logger.debug(f"LLM service initialized: {self.llm_service.enabled_providers}")
67
+ except Exception as e:
68
+ logger.error(f"Failed to initialize LLM service: {e}")
69
+
70
+ # Initialize Memory Manager
71
+ self.memory_manager: MemoryManager | None = None
72
+ if hasattr(self.config, "memory"):
73
+ try:
74
+ self.memory_manager = MemoryManager(
75
+ self.database,
76
+ self.config.memory,
77
+ )
78
+ except Exception as e:
79
+ logger.error(f"Failed to initialize MemoryManager: {e}")
80
+
81
+ # MCP Proxy Manager - Initialize early for tool access
82
+ # LocalMCPManager handles server/tool storage in SQLite
83
+ self.mcp_db_manager = LocalMCPManager(self.database)
84
+
85
+ # Tool Metrics Manager for tracking call statistics
86
+ from gobby.mcp_proxy.metrics import ToolMetricsManager
87
+
88
+ self.metrics_manager = ToolMetricsManager(self.database)
89
+
90
+ # MCPClientManager loads servers from database on init
91
+ self.mcp_proxy = MCPClientManager(
92
+ mcp_db_manager=self.mcp_db_manager,
93
+ metrics_manager=self.metrics_manager,
94
+ )
95
+
96
+ # Task Sync Manager
97
+ self.task_sync_manager = TaskSyncManager(self.task_manager)
98
+ # Wire up change listener for automatic export
99
+ self.task_manager.add_change_listener(self.task_sync_manager.trigger_export)
100
+
101
+ # Initialize Memory Sync Manager (Phase 7) & Wire up listeners
102
+ self.memory_sync_manager: MemorySyncManager | None = None
103
+ if hasattr(self.config, "memory_sync") and self.config.memory_sync.enabled:
104
+ if self.memory_manager:
105
+ try:
106
+ self.memory_sync_manager = MemorySyncManager(
107
+ db=self.database,
108
+ memory_manager=self.memory_manager,
109
+ config=self.config.memory_sync,
110
+ )
111
+ # Wire up listener to trigger export on changes
112
+ self.memory_manager.storage.add_change_listener(
113
+ self.memory_sync_manager.trigger_export
114
+ )
115
+ logger.debug("MemorySyncManager initialized and listener attached")
116
+
117
+ # Force initial synchronous export
118
+ # Ensures disk state matches DB state before we start serving
119
+ try:
120
+ self.memory_sync_manager.export_sync()
121
+ logger.info("Initial memory sync export completed")
122
+ except Exception as e:
123
+ logger.warning(f"Initial memory sync failed: {e}")
124
+
125
+ except Exception as e:
126
+ logger.error(f"Failed to initialize MemorySyncManager: {e}")
127
+
128
+ # Session Message Processor (Phase 6)
129
+ # Created here and passed to HTTPServer which injects it into HookManager
130
+ self.message_processor: SessionMessageProcessor | None = None
131
+ if getattr(self.config, "message_tracking", None) and self.config.message_tracking.enabled:
132
+ self.message_processor = SessionMessageProcessor(
133
+ db=self.database,
134
+ poll_interval=self.config.message_tracking.poll_interval,
135
+ )
136
+
137
+ # Initialize Task Managers (Phase 7.1)
138
+ self.task_expander: TaskExpander | None = None
139
+ self.task_validator: TaskValidator | None = None
140
+
141
+ if self.llm_service:
142
+ gobby_tasks_config = self.config.gobby_tasks
143
+ if gobby_tasks_config.expansion.enabled:
144
+ try:
145
+ self.task_expander = TaskExpander(
146
+ llm_service=self.llm_service,
147
+ config=gobby_tasks_config.expansion,
148
+ task_manager=self.task_manager,
149
+ mcp_manager=self.mcp_proxy,
150
+ )
151
+ except Exception as e:
152
+ logger.error(f"Failed to initialize TaskExpander: {e}")
153
+
154
+ if gobby_tasks_config.validation.enabled:
155
+ try:
156
+ self.task_validator = TaskValidator(
157
+ llm_service=self.llm_service,
158
+ config=gobby_tasks_config.validation,
159
+ )
160
+ except Exception as e:
161
+ logger.error(f"Failed to initialize TaskValidator: {e}")
162
+
163
+ # Initialize Worktree Storage (Phase 7 - Subagents)
164
+ self.worktree_storage = LocalWorktreeManager(self.database)
165
+
166
+ # Initialize Agent Runner (Phase 7 - Subagents)
167
+ # Create executor registry for lazy executor creation
168
+ self.executor_registry = ExecutorRegistry(config=self.config)
169
+ self.agent_runner: AgentRunner | None = None
170
+ try:
171
+ # Pre-initialize common executors
172
+ executors = {}
173
+ for provider in ["claude", "gemini", "litellm"]:
174
+ try:
175
+ executors[provider] = self.executor_registry.get(provider=provider)
176
+ logger.info(f"Pre-initialized {provider} executor")
177
+ except Exception as e:
178
+ logger.debug(f"Could not pre-initialize {provider} executor: {e}")
179
+
180
+ self.agent_runner = AgentRunner(
181
+ db=self.database,
182
+ session_storage=self.session_manager,
183
+ executors=executors,
184
+ max_agent_depth=3,
185
+ )
186
+ logger.debug(f"AgentRunner initialized with executors: {list(executors.keys())}")
187
+ except Exception as e:
188
+ logger.error(f"Failed to initialize AgentRunner: {e}")
189
+
190
+ # Session Lifecycle Manager (background jobs for expiring and processing)
191
+ self.lifecycle_manager = SessionLifecycleManager(
192
+ db=self.database,
193
+ config=self.config.session_lifecycle,
194
+ )
195
+
196
+ # HTTP Server
197
+ self.http_server = HTTPServer(
198
+ port=self.config.daemon_port,
199
+ mcp_manager=self.mcp_proxy,
200
+ mcp_db_manager=self.mcp_db_manager,
201
+ config=self.config,
202
+ session_manager=self.session_manager,
203
+ task_manager=self.task_manager,
204
+ task_sync_manager=self.task_sync_manager,
205
+ message_manager=self.message_manager,
206
+ memory_manager=self.memory_manager,
207
+ llm_service=self.llm_service,
208
+ message_processor=self.message_processor,
209
+ memory_sync_manager=self.memory_sync_manager,
210
+ task_expander=self.task_expander,
211
+ task_validator=self.task_validator,
212
+ metrics_manager=self.metrics_manager,
213
+ agent_runner=self.agent_runner,
214
+ worktree_storage=self.worktree_storage,
215
+ )
216
+
217
+ # Ensure message_processor property is set (redundant but explicit):
218
+ self.http_server.message_processor = self.message_processor
219
+
220
+ # WebSocket Server (Optional)
221
+ self.websocket_server: WebSocketServer | None = None
222
+ if self.config.websocket and getattr(self.config.websocket, "enabled", True):
223
+ websocket_config = WebSocketConfig(
224
+ host="localhost",
225
+ port=self.config.websocket.port,
226
+ ping_interval=self.config.websocket.ping_interval,
227
+ ping_timeout=self.config.websocket.ping_timeout,
228
+ )
229
+ self.websocket_server = WebSocketServer(
230
+ config=websocket_config,
231
+ mcp_manager=self.mcp_proxy,
232
+ )
233
+ # Pass WebSocket server reference to HTTP server for broadcasting
234
+ self.http_server.websocket_server = self.websocket_server
235
+ # Also update the HTTPServer's broadcaster to use the same websocket_server
236
+ self.http_server.broadcaster.websocket_server = self.websocket_server
237
+
238
+ # Pass WebSocket server to message processor if enabled
239
+ if self.message_processor:
240
+ self.message_processor.websocket_server = self.websocket_server
241
+
242
+ # Register agent event callback for WebSocket broadcasting
243
+ self._setup_agent_event_broadcasting()
244
+
245
+ def _init_database(self) -> DatabaseProtocol:
246
+ """Initialize hub database."""
247
+ hub_db_path = Path(self.config.database_path).expanduser()
248
+
249
+ # Ensure hub db directory exists
250
+ hub_db_path.parent.mkdir(parents=True, exist_ok=True)
251
+
252
+ hub_db = LocalDatabase(hub_db_path)
253
+ run_migrations(hub_db)
254
+
255
+ logger.info(f"Database: {hub_db_path}")
256
+ return hub_db
257
+
258
+ def _setup_agent_event_broadcasting(self) -> None:
259
+ """Set up WebSocket broadcasting for agent lifecycle events."""
260
+ from gobby.agents.registry import get_running_agent_registry
261
+
262
+ if not self.websocket_server:
263
+ return
264
+
265
+ registry = get_running_agent_registry()
266
+
267
+ def broadcast_agent_event(event_type: str, run_id: str, data: dict[str, Any]) -> None:
268
+ """Broadcast agent events via WebSocket (non-blocking)."""
269
+ if not self.websocket_server:
270
+ return
271
+
272
+ def _log_broadcast_exception(task: asyncio.Task[None]) -> None:
273
+ """Log exceptions from broadcast task to avoid silent failures."""
274
+ try:
275
+ task.result()
276
+ except asyncio.CancelledError:
277
+ pass
278
+ except Exception as e:
279
+ logger.warning(f"Failed to broadcast agent event {event_type}: {e}")
280
+
281
+ # Create async task to broadcast and attach exception callback
282
+ task = asyncio.create_task(
283
+ self.websocket_server.broadcast_agent_event(
284
+ event=event_type,
285
+ run_id=run_id,
286
+ parent_session_id=data.get("parent_session_id", ""),
287
+ session_id=data.get("session_id"),
288
+ mode=data.get("mode"),
289
+ provider=data.get("provider"),
290
+ pid=data.get("pid"),
291
+ )
292
+ )
293
+ task.add_done_callback(_log_broadcast_exception)
294
+
295
+ registry.add_event_callback(broadcast_agent_event)
296
+ logger.debug("Agent event broadcasting enabled")
297
+
298
+ async def _metrics_cleanup_loop(self) -> None:
299
+ """Background loop for periodic metrics cleanup (every 24 hours)."""
300
+ interval_seconds = 24 * 60 * 60 # 24 hours
301
+
302
+ while not self._shutdown_requested:
303
+ try:
304
+ await asyncio.sleep(interval_seconds)
305
+ deleted = self.metrics_manager.cleanup_old_metrics()
306
+ if deleted > 0:
307
+ logger.info(f"Periodic metrics cleanup: removed {deleted} old entries")
308
+ except asyncio.CancelledError:
309
+ break
310
+ except Exception as e:
311
+ logger.error(f"Error in metrics cleanup loop: {e}")
312
+
313
+ def _check_memory_v2_migration(self) -> None:
314
+ """Check if Memory V2 migration is needed and log a suggestion.
315
+
316
+ Checks if there are memories in the database but few/no cross-references,
317
+ suggesting the user run `gobby memory migrate-v2`.
318
+ """
319
+ if not self.memory_manager:
320
+ return
321
+
322
+ try:
323
+ # Get memory count
324
+ memories = self.memory_manager.list_memories(limit=1)
325
+ if not memories:
326
+ # No memories, nothing to migrate
327
+ return
328
+
329
+ # Get total memory count for the threshold
330
+ all_memories = self.memory_manager.list_memories(limit=10000)
331
+ memory_count = len(all_memories)
332
+
333
+ if memory_count < 5:
334
+ # Too few memories to warrant migration check
335
+ return
336
+
337
+ # Get crossref count
338
+ from gobby.storage.memories import LocalMemoryManager
339
+
340
+ storage = LocalMemoryManager(self.database)
341
+ crossrefs = storage.get_all_crossrefs(limit=1)
342
+
343
+ if not crossrefs and memory_count >= 5:
344
+ logger.warning(
345
+ f"Memory V2 migration recommended: {memory_count} memories found "
346
+ "but no cross-references. Run 'gobby memory migrate-v2' to enable "
347
+ "semantic search and automatic memory linking."
348
+ )
349
+ except Exception as e:
350
+ # Don't fail startup on migration check errors
351
+ logger.debug(f"Memory migration check failed (non-fatal): {e}")
352
+
353
+ def _setup_signal_handlers(self) -> None:
354
+ loop = asyncio.get_running_loop()
355
+
356
+ def handle_shutdown() -> None:
357
+ logger.info("Received shutdown signal, initiating graceful shutdown...")
358
+ self._shutdown_requested = True
359
+
360
+ for sig in (signal.SIGTERM, signal.SIGINT):
361
+ loop.add_signal_handler(sig, handle_shutdown)
362
+
363
+ async def run(self) -> None:
364
+ try:
365
+ self._setup_signal_handlers()
366
+
367
+ # Connect MCP servers
368
+ try:
369
+ await asyncio.wait_for(self.mcp_proxy.connect_all(), timeout=10.0)
370
+ except TimeoutError:
371
+ logger.warning("MCP connection timed out")
372
+ except Exception as e:
373
+ logger.error(f"MCP connection failed: {e}")
374
+
375
+ # Run metrics cleanup on startup
376
+ try:
377
+ deleted = self.metrics_manager.cleanup_old_metrics()
378
+ if deleted > 0:
379
+ logger.info(f"Startup metrics cleanup: removed {deleted} old entries")
380
+ except Exception as e:
381
+ logger.warning(f"Metrics cleanup failed: {e}")
382
+
383
+ # Check for pending Memory V2 migration
384
+ self._check_memory_v2_migration()
385
+
386
+ # Start Message Processor
387
+ if self.message_processor:
388
+ await self.message_processor.start()
389
+
390
+ # Start Session Lifecycle Manager
391
+ await self.lifecycle_manager.start()
392
+
393
+ # Start periodic metrics cleanup (every 24 hours)
394
+ self._metrics_cleanup_task = asyncio.create_task(
395
+ self._metrics_cleanup_loop(),
396
+ name="metrics-cleanup",
397
+ )
398
+
399
+ # Start WebSocket server
400
+ websocket_task = None
401
+ if self.websocket_server:
402
+ websocket_task = asyncio.create_task(self.websocket_server.start())
403
+
404
+ # Start HTTP server
405
+ # nosec B104: 0.0.0.0 binding is intentional - daemon serves local network
406
+ config = uvicorn.Config(
407
+ self.http_server.app,
408
+ host="0.0.0.0", # nosec B104 - local daemon needs network access
409
+ port=self.http_server.port,
410
+ log_level="warning",
411
+ access_log=False,
412
+ )
413
+ server = uvicorn.Server(config)
414
+ server_task = asyncio.create_task(server.serve())
415
+
416
+ # Wait for shutdown
417
+ while not self._shutdown_requested:
418
+ await asyncio.sleep(0.5)
419
+
420
+ # Cleanup with timeouts to prevent hanging
421
+ server.should_exit = True
422
+ try:
423
+ await asyncio.wait_for(server_task, timeout=3.0)
424
+ except TimeoutError:
425
+ logger.warning("HTTP server shutdown timed out")
426
+
427
+ try:
428
+ await asyncio.wait_for(self.lifecycle_manager.stop(), timeout=2.0)
429
+ except TimeoutError:
430
+ logger.warning("Lifecycle manager shutdown timed out")
431
+
432
+ if self.message_processor:
433
+ try:
434
+ await asyncio.wait_for(self.message_processor.stop(), timeout=2.0)
435
+ except TimeoutError:
436
+ logger.warning("Message processor shutdown timed out")
437
+
438
+ if websocket_task:
439
+ websocket_task.cancel()
440
+ try:
441
+ await asyncio.wait_for(websocket_task, timeout=3.0)
442
+ except (asyncio.CancelledError, TimeoutError):
443
+ logger.warning("WebSocket server shutdown timed out or cancelled")
444
+
445
+ # Cancel metrics cleanup task
446
+ if self._metrics_cleanup_task and not self._metrics_cleanup_task.done():
447
+ self._metrics_cleanup_task.cancel()
448
+ try:
449
+ await asyncio.wait_for(self._metrics_cleanup_task, timeout=2.0)
450
+ except (asyncio.CancelledError, TimeoutError):
451
+ pass
452
+
453
+ try:
454
+ await asyncio.wait_for(self.mcp_proxy.disconnect_all(), timeout=3.0)
455
+ except TimeoutError:
456
+ logger.warning("MCP disconnect timed out")
457
+
458
+ logger.info("Shutdown complete")
459
+
460
+ except Exception as e:
461
+ logger.error(f"Fatal error: {e}", exc_info=True)
462
+ sys.exit(1)
463
+
464
+
465
+ async def run_gobby(config_path: Path | None = None, verbose: bool = False) -> None:
466
+ runner = GobbyRunner(config_path=config_path, verbose=verbose)
467
+ await runner.run()
468
+
469
+
470
+ def main(config_path: Path | None = None, verbose: bool = False) -> None:
471
+ try:
472
+ asyncio.run(run_gobby(config_path=config_path, verbose=verbose))
473
+ except KeyboardInterrupt:
474
+ sys.exit(0)
475
+ except Exception as e:
476
+ logger.error(f"Fatal error: {e}", exc_info=True)
477
+ sys.exit(1)
478
+
479
+
480
+ if __name__ == "__main__":
481
+ import argparse
482
+
483
+ parser = argparse.ArgumentParser(description="Run Gobby daemon")
484
+ parser.add_argument("--verbose", action="store_true", help="Enable verbose logging")
485
+ parser.add_argument("--config", type=Path, help="Path to config file")
486
+
487
+ args = parser.parse_args()
488
+ main(config_path=args.config, verbose=args.verbose)
@@ -0,0 +1,23 @@
1
+ """
2
+ Shared search backend abstraction.
3
+
4
+ Provides pluggable search backends for semantic search:
5
+ - TF-IDF (default) - Built-in local search using scikit-learn (sklearn)
6
+
7
+ Usage:
8
+ from gobby.search import SearchBackend, get_search_backend, TFIDFSearcher
9
+
10
+ backend = get_search_backend("tfidf")
11
+ backend.fit([(id, content) for id, content in items])
12
+ results = backend.search("query text", top_k=10)
13
+ """
14
+
15
+ from gobby.search.protocol import SearchBackend, SearchResult, get_search_backend
16
+ from gobby.search.tfidf import TFIDFSearcher
17
+
18
+ __all__ = [
19
+ "SearchBackend",
20
+ "SearchResult",
21
+ "TFIDFSearcher",
22
+ "get_search_backend",
23
+ ]
@@ -0,0 +1,104 @@
1
+ """Search backend protocol definition.
2
+
3
+ Defines the interface that all search backends must implement.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Protocol, runtime_checkable
9
+
10
+
11
+ @runtime_checkable
12
+ class SearchBackend(Protocol):
13
+ """
14
+ Protocol for pluggable search backends.
15
+
16
+ Backends must implement:
17
+ - fit(): Build/rebuild the search index from item contents
18
+ - search(): Find relevant items for a query
19
+ - needs_refit(): Check if index needs rebuilding
20
+
21
+ The protocol uses structural typing, so any class with these methods
22
+ will satisfy the protocol without inheritance.
23
+ """
24
+
25
+ def fit(self, items: list[tuple[str, str]]) -> None:
26
+ """
27
+ Build or rebuild the search index.
28
+
29
+ Args:
30
+ items: List of (item_id, content) tuples to index
31
+
32
+ This should be called:
33
+ - On startup to build initial index
34
+ - After bulk item operations
35
+ - When needs_refit() returns True
36
+ """
37
+ ...
38
+
39
+ def search(self, query: str, top_k: int = 10) -> list[tuple[str, float]]:
40
+ """
41
+ Search for items matching the query.
42
+
43
+ Args:
44
+ query: Search query text
45
+ top_k: Maximum number of results to return
46
+
47
+ Returns:
48
+ List of (item_id, similarity_score) tuples, sorted by
49
+ relevance (highest similarity first). Similarity scores
50
+ are typically in range [0, 1] but may vary by backend.
51
+ """
52
+ ...
53
+
54
+ def needs_refit(self) -> bool:
55
+ """
56
+ Check if the search index needs rebuilding.
57
+
58
+ Returns:
59
+ True if fit() should be called before search()
60
+ """
61
+ ...
62
+
63
+
64
+ class SearchResult:
65
+ """Result from a search query with item ID and similarity score."""
66
+
67
+ __slots__ = ("item_id", "similarity")
68
+
69
+ def __init__(self, item_id: str, similarity: float):
70
+ self.item_id = item_id
71
+ self.similarity = similarity
72
+
73
+ def __repr__(self) -> str:
74
+ return f"SearchResult(item_id={self.item_id!r}, similarity={self.similarity:.4f})"
75
+
76
+ def to_tuple(self) -> tuple[str, float]:
77
+ """Convert to (item_id, similarity) tuple for backwards compatibility."""
78
+ return (self.item_id, self.similarity)
79
+
80
+
81
+ def get_search_backend(backend_type: str, **kwargs: Any) -> SearchBackend:
82
+ """
83
+ Factory function for search backends.
84
+
85
+ Args:
86
+ backend_type: Type of backend - currently only "tfidf" is supported
87
+ **kwargs: Backend-specific configuration
88
+
89
+ Returns:
90
+ SearchBackend instance
91
+
92
+ Raises:
93
+ ValueError: If backend_type is not "tfidf"
94
+ ImportError: If required dependencies are not installed
95
+ """
96
+ from typing import cast
97
+
98
+ if backend_type == "tfidf":
99
+ from gobby.search.tfidf import TFIDFSearcher
100
+
101
+ return cast(SearchBackend, TFIDFSearcher(**kwargs))
102
+
103
+ else:
104
+ raise ValueError(f"Unknown search backend: {backend_type}. Valid options: tfidf")