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/sync/github.py ADDED
@@ -0,0 +1,333 @@
1
+ """GitHub sync service that orchestrates between gobby tasks and GitHub.
2
+
3
+ This service delegates all GitHub operations to the official GitHub MCP server
4
+ (@modelcontextprotocol/server-github), avoiding custom API client code.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from typing import TYPE_CHECKING, Any, cast
11
+
12
+ from gobby.integrations.github import GitHubIntegration
13
+
14
+ if TYPE_CHECKING:
15
+ from gobby.mcp_proxy.manager import MCPClientManager
16
+ from gobby.storage.tasks import LocalTaskManager
17
+
18
+ __all__ = [
19
+ "GitHubSyncService",
20
+ "GitHubSyncError",
21
+ "GitHubRateLimitError",
22
+ "GitHubNotFoundError",
23
+ ]
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class GitHubSyncError(Exception):
29
+ """Base exception for GitHub sync errors."""
30
+
31
+ pass
32
+
33
+
34
+ class GitHubRateLimitError(GitHubSyncError):
35
+ """Raised when GitHub API rate limit is exceeded.
36
+
37
+ Attributes:
38
+ reset_at: Unix timestamp when rate limit resets.
39
+ """
40
+
41
+ def __init__(self, message: str, reset_at: int | None = None) -> None:
42
+ super().__init__(message)
43
+ self.reset_at = reset_at
44
+
45
+
46
+ class GitHubNotFoundError(GitHubSyncError):
47
+ """Raised when a GitHub resource is not found.
48
+
49
+ Attributes:
50
+ resource: Type of resource (e.g., "issue", "repo", "pr").
51
+ resource_id: Identifier of the missing resource.
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ message: str,
57
+ resource: str | None = None,
58
+ resource_id: int | str | None = None,
59
+ ) -> None:
60
+ super().__init__(message)
61
+ self.resource = resource
62
+ self.resource_id = resource_id
63
+
64
+
65
+ class GitHubSyncService:
66
+ """Service for syncing gobby tasks with GitHub issues and PRs.
67
+
68
+ This service orchestrates bidirectional sync between gobby tasks and GitHub:
69
+ - Import GitHub issues as gobby tasks
70
+ - Sync task updates back to GitHub issues
71
+ - Create PRs from completed tasks
72
+
73
+ All GitHub operations are delegated to the official GitHub MCP server.
74
+
75
+ Attributes:
76
+ mcp_manager: MCPClientManager for accessing GitHub MCP server.
77
+ task_manager: LocalTaskManager for gobby task CRUD.
78
+ project_id: Gobby project ID for creating tasks.
79
+ github_repo: Default GitHub repo in "owner/repo" format.
80
+ github: GitHubIntegration instance for availability checks.
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ mcp_manager: MCPClientManager,
86
+ task_manager: LocalTaskManager,
87
+ project_id: str,
88
+ github_repo: str | None = None,
89
+ ) -> None:
90
+ """Initialize GitHubSyncService.
91
+
92
+ Args:
93
+ mcp_manager: MCPClientManager for GitHub MCP server access.
94
+ task_manager: LocalTaskManager for gobby task operations.
95
+ project_id: Gobby project ID for creating tasks.
96
+ github_repo: Default GitHub repo in "owner/repo" format.
97
+ """
98
+ self.mcp_manager = mcp_manager
99
+ self.task_manager = task_manager
100
+ self.project_id = project_id
101
+ self.github_repo = github_repo
102
+ self.github = GitHubIntegration(mcp_manager)
103
+
104
+ def is_available(self) -> bool:
105
+ """Check if GitHub MCP server is available.
106
+
107
+ Returns:
108
+ True if GitHub MCP server is available, False otherwise.
109
+ """
110
+ return self.github.is_available()
111
+
112
+ async def import_github_issues(
113
+ self,
114
+ repo: str,
115
+ labels: list[str] | None = None,
116
+ state: str = "open",
117
+ ) -> list[dict[str, Any]]:
118
+ """Import GitHub issues as gobby tasks.
119
+
120
+ Fetches issues from GitHub via the MCP server and creates corresponding
121
+ gobby tasks with linked github_issue_number and github_repo fields.
122
+
123
+ Args:
124
+ repo: GitHub repo in "owner/repo" format.
125
+ labels: Optional list of labels to filter issues.
126
+ state: Issue state to filter ("open", "closed", "all").
127
+
128
+ Returns:
129
+ List of created task dictionaries.
130
+
131
+ Raises:
132
+ RuntimeError: If GitHub MCP server is unavailable.
133
+ """
134
+ self.github.require_available()
135
+
136
+ # Call GitHub MCP to list issues
137
+ args: dict[str, Any] = {"owner": repo.split("/")[0], "repo": repo.split("/")[1]}
138
+ if labels:
139
+ args["labels"] = ",".join(labels)
140
+ if state:
141
+ args["state"] = state
142
+
143
+ result = await self.mcp_manager.call_tool(
144
+ server_name="github",
145
+ tool_name="list_issues",
146
+ arguments=args,
147
+ )
148
+
149
+ issues = result.get("issues", [])
150
+ created_tasks = []
151
+
152
+ for issue in issues:
153
+ # Create gobby task linked to GitHub issue
154
+ task = self.task_manager.create_task(
155
+ project_id=self.project_id,
156
+ title=issue.get("title", "Untitled Issue"),
157
+ description=issue.get("body", ""),
158
+ github_issue_number=issue.get("number"),
159
+ github_repo=repo,
160
+ )
161
+ created_tasks.append(task.to_dict())
162
+
163
+ logger.info(f"Imported {len(created_tasks)} issues from {repo}")
164
+ return created_tasks
165
+
166
+ async def sync_task_to_github(self, task_id: str) -> dict[str, Any]:
167
+ """Sync a gobby task to its linked GitHub issue.
168
+
169
+ Updates the GitHub issue title and body to match the task.
170
+
171
+ Args:
172
+ task_id: ID of the task to sync.
173
+
174
+ Returns:
175
+ Result from GitHub MCP update_issue call.
176
+
177
+ Raises:
178
+ RuntimeError: If GitHub MCP server is unavailable.
179
+ ValueError: If task has no linked GitHub issue.
180
+ """
181
+ self.github.require_available()
182
+
183
+ task = self.task_manager.get_task(task_id)
184
+
185
+ if not task.github_issue_number:
186
+ raise ValueError(
187
+ f"Task {task_id} has no linked GitHub issue. Set github_issue_number to sync."
188
+ )
189
+
190
+ repo = task.github_repo or self.github_repo
191
+ if not repo:
192
+ raise ValueError(
193
+ f"Task {task_id} has no github_repo set and no default repo configured."
194
+ )
195
+
196
+ owner, repo_name = repo.split("/")
197
+
198
+ result = await self.mcp_manager.call_tool(
199
+ server_name="github",
200
+ tool_name="update_issue",
201
+ arguments={
202
+ "owner": owner,
203
+ "repo": repo_name,
204
+ "issue_number": task.github_issue_number,
205
+ "title": task.title,
206
+ "body": task.description or "",
207
+ },
208
+ )
209
+
210
+ # Validate response
211
+ if result is None or not isinstance(result, dict):
212
+ raise GitHubSyncError(
213
+ f"Invalid response from GitHub MCP when updating issue "
214
+ f"#{task.github_issue_number}: expected dict, got {type(result).__name__}"
215
+ )
216
+
217
+ logger.info(f"Synced task {task_id} to GitHub issue #{task.github_issue_number}")
218
+ return cast(dict[str, Any], result)
219
+
220
+ async def create_pr_for_task(
221
+ self,
222
+ task_id: str,
223
+ head_branch: str,
224
+ base_branch: str = "main",
225
+ draft: bool = False,
226
+ ) -> dict[str, Any]:
227
+ """Create a GitHub PR for a task.
228
+
229
+ Creates a pull request on GitHub and links it to the task.
230
+
231
+ Args:
232
+ task_id: ID of the task to create PR for.
233
+ head_branch: Branch containing the changes.
234
+ base_branch: Branch to merge into (default: "main").
235
+ draft: Whether to create as draft PR.
236
+
237
+ Returns:
238
+ Result from GitHub MCP create_pull_request call.
239
+
240
+ Raises:
241
+ RuntimeError: If GitHub MCP server is unavailable.
242
+ """
243
+ self.github.require_available()
244
+
245
+ task = self.task_manager.get_task(task_id)
246
+
247
+ repo = task.github_repo or self.github_repo
248
+ if not repo:
249
+ raise ValueError(
250
+ f"Task {task_id} has no github_repo set and no default repo configured."
251
+ )
252
+
253
+ owner, repo_name = repo.split("/")
254
+
255
+ # Create PR via GitHub MCP
256
+ result = await self.mcp_manager.call_tool(
257
+ server_name="github",
258
+ tool_name="create_pull_request",
259
+ arguments={
260
+ "owner": owner,
261
+ "repo": repo_name,
262
+ "title": task.title,
263
+ "body": task.description or "",
264
+ "head": head_branch,
265
+ "base": base_branch,
266
+ "draft": draft,
267
+ },
268
+ )
269
+
270
+ # Update task with PR number if available
271
+ result_dict = cast(dict[str, Any], result)
272
+ pr_number = result_dict.get("number")
273
+ if pr_number:
274
+ self.task_manager.update_task(
275
+ task_id,
276
+ github_pr_number=pr_number,
277
+ github_repo=repo,
278
+ )
279
+ logger.info(f"Created PR #{pr_number} for task {task_id}")
280
+
281
+ return result_dict
282
+
283
+ def map_gobby_labels_to_github(
284
+ self,
285
+ gobby_labels: list[str],
286
+ prefix: str = "",
287
+ ) -> list[str]:
288
+ """Map gobby labels to GitHub label format.
289
+
290
+ Args:
291
+ gobby_labels: List of gobby label strings.
292
+ prefix: Optional prefix to add to each label.
293
+
294
+ Returns:
295
+ List of GitHub-formatted labels.
296
+ """
297
+ if not gobby_labels:
298
+ return []
299
+
300
+ github_labels = []
301
+ for label in gobby_labels:
302
+ if prefix:
303
+ github_labels.append(f"{prefix}{label}")
304
+ else:
305
+ github_labels.append(label)
306
+
307
+ return github_labels
308
+
309
+ def map_github_labels_to_gobby(
310
+ self,
311
+ github_labels: list[str],
312
+ strip_prefix: str = "",
313
+ ) -> list[str]:
314
+ """Map GitHub labels to gobby label format.
315
+
316
+ Args:
317
+ github_labels: List of GitHub label strings.
318
+ strip_prefix: Optional prefix to strip from each label.
319
+
320
+ Returns:
321
+ List of gobby-formatted labels.
322
+ """
323
+ if not github_labels:
324
+ return []
325
+
326
+ gobby_labels = []
327
+ for label in github_labels:
328
+ if strip_prefix and label.startswith(strip_prefix):
329
+ gobby_labels.append(label[len(strip_prefix) :])
330
+ else:
331
+ gobby_labels.append(label)
332
+
333
+ return gobby_labels
gobby/sync/linear.py ADDED
@@ -0,0 +1,304 @@
1
+ """Linear sync service that orchestrates between gobby tasks and Linear.
2
+
3
+ This service delegates all Linear operations to the official Linear MCP server,
4
+ avoiding custom API client code.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from typing import TYPE_CHECKING, Any, cast
11
+
12
+ from gobby.integrations.linear import LinearIntegration
13
+
14
+ if TYPE_CHECKING:
15
+ from gobby.mcp_proxy.manager import MCPClientManager
16
+ from gobby.storage.tasks import LocalTaskManager
17
+
18
+ __all__ = [
19
+ "LinearSyncService",
20
+ "LinearSyncError",
21
+ "LinearRateLimitError",
22
+ "LinearNotFoundError",
23
+ ]
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class LinearSyncError(Exception):
29
+ """Base exception for Linear sync errors."""
30
+
31
+ pass
32
+
33
+
34
+ class LinearRateLimitError(LinearSyncError):
35
+ """Raised when Linear API rate limit is exceeded.
36
+
37
+ Attributes:
38
+ reset_at: Unix timestamp when rate limit resets.
39
+ """
40
+
41
+ def __init__(self, message: str, reset_at: int | None = None) -> None:
42
+ super().__init__(message)
43
+ self.reset_at = reset_at
44
+
45
+
46
+ class LinearNotFoundError(LinearSyncError):
47
+ """Raised when a Linear resource is not found.
48
+
49
+ Attributes:
50
+ resource: Type of resource (e.g., "issue", "team", "project").
51
+ resource_id: Identifier of the missing resource.
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ message: str,
57
+ resource: str | None = None,
58
+ resource_id: str | None = None,
59
+ ) -> None:
60
+ super().__init__(message)
61
+ self.resource = resource
62
+ self.resource_id = resource_id
63
+
64
+
65
+ class LinearSyncService:
66
+ """Service for syncing gobby tasks with Linear issues.
67
+
68
+ This service orchestrates bidirectional sync between gobby tasks and Linear:
69
+ - Import Linear issues as gobby tasks
70
+ - Sync task updates back to Linear issues
71
+ - Create new issues from gobby tasks
72
+
73
+ All Linear operations are delegated to the official Linear MCP server.
74
+
75
+ Attributes:
76
+ mcp_manager: MCPClientManager for accessing Linear MCP server.
77
+ task_manager: LocalTaskManager for gobby task CRUD.
78
+ project_id: Gobby project ID for creating tasks.
79
+ linear_team_id: Default Linear team ID for creating issues.
80
+ linear: LinearIntegration instance for availability checks.
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ mcp_manager: MCPClientManager,
86
+ task_manager: LocalTaskManager,
87
+ project_id: str,
88
+ linear_team_id: str | None = None,
89
+ ) -> None:
90
+ """Initialize LinearSyncService.
91
+
92
+ Args:
93
+ mcp_manager: MCPClientManager for Linear MCP server access.
94
+ task_manager: LocalTaskManager for gobby task operations.
95
+ project_id: Gobby project ID for creating tasks.
96
+ linear_team_id: Default Linear team ID for creating issues.
97
+ """
98
+ self.mcp_manager = mcp_manager
99
+ self.task_manager = task_manager
100
+ self.project_id = project_id
101
+ self.linear_team_id = linear_team_id
102
+ self.linear = LinearIntegration(mcp_manager)
103
+
104
+ def is_available(self) -> bool:
105
+ """Check if Linear MCP server is available.
106
+
107
+ Returns:
108
+ True if Linear MCP server is available, False otherwise.
109
+ """
110
+ return self.linear.is_available()
111
+
112
+ async def import_linear_issues(
113
+ self,
114
+ team_id: str | None = None,
115
+ state: str | None = None,
116
+ labels: list[str] | None = None,
117
+ ) -> list[dict[str, Any]]:
118
+ """Import Linear issues as gobby tasks.
119
+
120
+ Fetches issues from Linear via the MCP server and creates corresponding
121
+ gobby tasks with linked linear_issue_id and linear_team_id fields.
122
+
123
+ Args:
124
+ team_id: Linear team ID to filter issues. Uses default if not provided.
125
+ state: Issue state to filter (e.g., "In Progress", "Todo").
126
+ labels: Optional list of labels to filter issues.
127
+
128
+ Returns:
129
+ List of created task dictionaries.
130
+
131
+ Raises:
132
+ RuntimeError: If Linear MCP server is unavailable.
133
+ ValueError: If no team_id is provided and no default configured.
134
+ """
135
+ self.linear.require_available()
136
+
137
+ effective_team_id = team_id or self.linear_team_id
138
+ if not effective_team_id:
139
+ raise ValueError("No team_id provided and no default linear_team_id configured.")
140
+
141
+ # Build filter arguments for Linear MCP
142
+ args: dict[str, Any] = {"teamId": effective_team_id}
143
+ if state:
144
+ args["state"] = state
145
+ if labels:
146
+ args["labels"] = labels
147
+
148
+ result = await self.mcp_manager.call_tool(
149
+ server_name="linear",
150
+ tool_name="list_issues",
151
+ arguments=args,
152
+ )
153
+
154
+ issues = result.get("issues", [])
155
+ created_tasks = []
156
+
157
+ for issue in issues:
158
+ # Create gobby task linked to Linear issue
159
+ task = self.task_manager.create_task(
160
+ project_id=self.project_id,
161
+ title=issue.get("title", "Untitled Issue"),
162
+ description=issue.get("description", ""),
163
+ linear_issue_id=issue.get("id"),
164
+ linear_team_id=effective_team_id,
165
+ )
166
+ created_tasks.append(task.to_dict())
167
+
168
+ logger.info(f"Imported {len(created_tasks)} issues from Linear team {effective_team_id}")
169
+ return created_tasks
170
+
171
+ async def sync_task_to_linear(self, task_id: str) -> dict[str, Any]:
172
+ """Sync a gobby task to its linked Linear issue.
173
+
174
+ Updates the Linear issue title and description to match the task.
175
+
176
+ Args:
177
+ task_id: ID of the task to sync.
178
+
179
+ Returns:
180
+ Result from Linear MCP update_issue call.
181
+
182
+ Raises:
183
+ RuntimeError: If Linear MCP server is unavailable.
184
+ ValueError: If task has no linked Linear issue.
185
+ """
186
+ self.linear.require_available()
187
+
188
+ task = self.task_manager.get_task(task_id)
189
+
190
+ if not task.linear_issue_id:
191
+ raise ValueError(
192
+ f"Task {task_id} has no linked Linear issue. Set linear_issue_id to sync."
193
+ )
194
+
195
+ result = await self.mcp_manager.call_tool(
196
+ server_name="linear",
197
+ tool_name="update_issue",
198
+ arguments={
199
+ "issueId": task.linear_issue_id,
200
+ "title": task.title,
201
+ "description": task.description or "",
202
+ },
203
+ )
204
+
205
+ # Validate response
206
+ if result is None or not isinstance(result, dict):
207
+ raise LinearSyncError(
208
+ f"Invalid response from Linear MCP when updating issue "
209
+ f"{task.linear_issue_id}: expected dict, got {type(result).__name__}"
210
+ )
211
+
212
+ logger.info(f"Synced task {task_id} to Linear issue {task.linear_issue_id}")
213
+ return cast(dict[str, Any], result)
214
+
215
+ async def create_issue_for_task(
216
+ self,
217
+ task_id: str,
218
+ team_id: str | None = None,
219
+ ) -> dict[str, Any]:
220
+ """Create a Linear issue from a gobby task.
221
+
222
+ Creates an issue on Linear and links it to the task.
223
+
224
+ Args:
225
+ task_id: ID of the task to create issue for.
226
+ team_id: Linear team ID for the issue. Uses default if not provided.
227
+
228
+ Returns:
229
+ Result from Linear MCP create_issue call.
230
+
231
+ Raises:
232
+ RuntimeError: If Linear MCP server is unavailable.
233
+ ValueError: If no team_id is provided and no default configured.
234
+ """
235
+ self.linear.require_available()
236
+
237
+ task = self.task_manager.get_task(task_id)
238
+
239
+ effective_team_id = team_id or task.linear_team_id or self.linear_team_id
240
+ if not effective_team_id:
241
+ raise ValueError(f"Task {task_id} has no linear_team_id set and no default configured.")
242
+
243
+ # Create issue via Linear MCP
244
+ result = await self.mcp_manager.call_tool(
245
+ server_name="linear",
246
+ tool_name="create_issue",
247
+ arguments={
248
+ "teamId": effective_team_id,
249
+ "title": task.title,
250
+ "description": task.description or "",
251
+ },
252
+ )
253
+
254
+ # Update task with Linear issue ID if available
255
+ result_dict = cast(dict[str, Any], result)
256
+ issue_id = result_dict.get("id")
257
+ if issue_id:
258
+ self.task_manager.update_task(
259
+ task_id,
260
+ linear_issue_id=issue_id,
261
+ linear_team_id=effective_team_id,
262
+ )
263
+ logger.info(f"Created Linear issue {issue_id} for task {task_id}")
264
+
265
+ return result_dict
266
+
267
+ def map_gobby_status_to_linear(self, gobby_status: str) -> str:
268
+ """Map gobby task status to Linear issue state.
269
+
270
+ Args:
271
+ gobby_status: Gobby task status.
272
+
273
+ Returns:
274
+ Linear issue state name.
275
+ """
276
+ status_map = {
277
+ "open": "Todo",
278
+ "in_progress": "In Progress",
279
+ "closed": "Done",
280
+ "failed": "Canceled",
281
+ "escalated": "In Review",
282
+ "needs_decomposition": "Backlog",
283
+ }
284
+ return status_map.get(gobby_status, "Todo")
285
+
286
+ def map_linear_status_to_gobby(self, linear_state: str) -> str:
287
+ """Map Linear issue state to gobby task status.
288
+
289
+ Args:
290
+ linear_state: Linear issue state name.
291
+
292
+ Returns:
293
+ Gobby task status.
294
+ """
295
+ state_map = {
296
+ "Todo": "open",
297
+ "In Progress": "in_progress",
298
+ "Done": "closed",
299
+ "Canceled": "closed",
300
+ "In Review": "in_progress",
301
+ "Backlog": "open",
302
+ "Triage": "open",
303
+ }
304
+ return state_map.get(linear_state, "open")