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
@@ -0,0 +1,547 @@
1
+ """Local worktree storage manager."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass
7
+ from datetime import UTC, datetime
8
+ from enum import Enum
9
+ from typing import Any
10
+
11
+ from gobby.storage.database import DatabaseProtocol
12
+ from gobby.utils.id import generate_prefixed_id
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class WorktreeStatus(str, Enum):
18
+ """Worktree status values."""
19
+
20
+ ACTIVE = "active"
21
+ STALE = "stale"
22
+ MERGED = "merged"
23
+ ABANDONED = "abandoned"
24
+
25
+
26
+ @dataclass
27
+ class Worktree:
28
+ """Worktree data model."""
29
+
30
+ id: str
31
+ project_id: str
32
+ task_id: str | None
33
+ branch_name: str
34
+ worktree_path: str
35
+ base_branch: str
36
+ agent_session_id: str | None
37
+ status: str
38
+ created_at: str
39
+ updated_at: str
40
+ merged_at: str | None
41
+ merge_state: str | None = None # "pending", "resolved", or None
42
+
43
+ @classmethod
44
+ def from_row(cls, row: Any) -> Worktree:
45
+ """Create Worktree from database row."""
46
+ # Handle merge_state which may not exist in older schemas
47
+ merge_state = row.get("merge_state") if hasattr(row, "get") else None
48
+ if merge_state is None:
49
+ try:
50
+ merge_state = row["merge_state"]
51
+ except (KeyError, IndexError):
52
+ pass
53
+
54
+ return cls(
55
+ id=row["id"],
56
+ project_id=row["project_id"],
57
+ task_id=row["task_id"],
58
+ branch_name=row["branch_name"],
59
+ worktree_path=row["worktree_path"],
60
+ base_branch=row["base_branch"],
61
+ agent_session_id=row["agent_session_id"],
62
+ status=row["status"],
63
+ created_at=row["created_at"],
64
+ updated_at=row["updated_at"],
65
+ merged_at=row["merged_at"],
66
+ merge_state=merge_state,
67
+ )
68
+
69
+ def to_dict(self) -> dict[str, Any]:
70
+ """Convert to dictionary."""
71
+ return {
72
+ "id": self.id,
73
+ "project_id": self.project_id,
74
+ "task_id": self.task_id,
75
+ "branch_name": self.branch_name,
76
+ "worktree_path": self.worktree_path,
77
+ "base_branch": self.base_branch,
78
+ "agent_session_id": self.agent_session_id,
79
+ "status": self.status,
80
+ "created_at": self.created_at,
81
+ "updated_at": self.updated_at,
82
+ "merged_at": self.merged_at,
83
+ "merge_state": self.merge_state,
84
+ }
85
+
86
+
87
+ class LocalWorktreeManager:
88
+ """Manager for local worktree storage."""
89
+
90
+ def __init__(self, db: DatabaseProtocol):
91
+ """Initialize with database connection."""
92
+ self.db = db
93
+
94
+ def create(
95
+ self,
96
+ project_id: str,
97
+ branch_name: str,
98
+ worktree_path: str,
99
+ base_branch: str = "main",
100
+ task_id: str | None = None,
101
+ agent_session_id: str | None = None,
102
+ ) -> Worktree:
103
+ """
104
+ Create a new worktree record.
105
+
106
+ Args:
107
+ project_id: Project ID
108
+ branch_name: Git branch name
109
+ worktree_path: Absolute path to worktree directory
110
+ base_branch: Base branch for the worktree
111
+ task_id: Optional task ID to link
112
+ agent_session_id: Optional session ID that owns this worktree
113
+
114
+ Returns:
115
+ Created Worktree instance
116
+ """
117
+ worktree_id = generate_prefixed_id("wt", length=6)
118
+ now = datetime.now(UTC).isoformat()
119
+
120
+ self.db.execute(
121
+ """
122
+ INSERT INTO worktrees (
123
+ id, project_id, task_id, branch_name, worktree_path,
124
+ base_branch, agent_session_id, status, created_at, updated_at
125
+ )
126
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
127
+ """,
128
+ (
129
+ worktree_id,
130
+ project_id,
131
+ task_id,
132
+ branch_name,
133
+ worktree_path,
134
+ base_branch,
135
+ agent_session_id,
136
+ WorktreeStatus.ACTIVE.value,
137
+ now,
138
+ now,
139
+ ),
140
+ )
141
+
142
+ return Worktree(
143
+ id=worktree_id,
144
+ project_id=project_id,
145
+ task_id=task_id,
146
+ branch_name=branch_name,
147
+ worktree_path=worktree_path,
148
+ base_branch=base_branch,
149
+ agent_session_id=agent_session_id,
150
+ status=WorktreeStatus.ACTIVE.value,
151
+ created_at=now,
152
+ updated_at=now,
153
+ merged_at=None,
154
+ )
155
+
156
+ def get(self, worktree_id: str) -> Worktree | None:
157
+ """Get worktree by ID."""
158
+ row = self.db.fetchone("SELECT * FROM worktrees WHERE id = ?", (worktree_id,))
159
+ return Worktree.from_row(row) if row else None
160
+
161
+ def get_by_path(self, worktree_path: str) -> Worktree | None:
162
+ """Get worktree by path."""
163
+ row = self.db.fetchone("SELECT * FROM worktrees WHERE worktree_path = ?", (worktree_path,))
164
+ return Worktree.from_row(row) if row else None
165
+
166
+ def get_by_branch(self, project_id: str, branch_name: str) -> Worktree | None:
167
+ """Get worktree by project and branch name."""
168
+ row = self.db.fetchone(
169
+ "SELECT * FROM worktrees WHERE project_id = ? AND branch_name = ?",
170
+ (project_id, branch_name),
171
+ )
172
+ return Worktree.from_row(row) if row else None
173
+
174
+ def get_by_task(self, task_id: str) -> Worktree | None:
175
+ """Get worktree linked to a task."""
176
+ row = self.db.fetchone("SELECT * FROM worktrees WHERE task_id = ?", (task_id,))
177
+ return Worktree.from_row(row) if row else None
178
+
179
+ def list_worktrees(
180
+ self,
181
+ project_id: str | None = None,
182
+ status: str | None = None,
183
+ agent_session_id: str | None = None,
184
+ limit: int = 50,
185
+ ) -> list[Worktree]:
186
+ """
187
+ List worktrees with optional filters.
188
+
189
+ Args:
190
+ project_id: Filter by project
191
+ status: Filter by status
192
+ agent_session_id: Filter by owning session
193
+ limit: Maximum number of results
194
+
195
+ Returns:
196
+ List of Worktree instances
197
+ """
198
+ conditions = []
199
+ params: list[Any] = []
200
+
201
+ if project_id:
202
+ conditions.append("project_id = ?")
203
+ params.append(project_id)
204
+ if status:
205
+ conditions.append("status = ?")
206
+ params.append(status)
207
+ if agent_session_id:
208
+ conditions.append("agent_session_id = ?")
209
+ params.append(agent_session_id)
210
+
211
+ where_clause = " AND ".join(conditions) if conditions else "1=1"
212
+ params.append(limit)
213
+
214
+ # nosec B608: where_clause built from hardcoded condition strings, values parameterized
215
+ rows = self.db.fetchall(
216
+ f"""
217
+ SELECT * FROM worktrees
218
+ WHERE {where_clause}
219
+ ORDER BY created_at DESC
220
+ LIMIT ?
221
+ """, # nosec B608
222
+ tuple(params),
223
+ )
224
+ return [Worktree.from_row(row) for row in rows]
225
+
226
+ # Allowlist of valid worktree column names to prevent SQL injection
227
+ _VALID_UPDATE_FIELDS = frozenset(
228
+ {
229
+ "branch_name",
230
+ "base_branch",
231
+ "worktree_path",
232
+ "status",
233
+ "agent_session_id",
234
+ "task_id",
235
+ "last_activity_at",
236
+ "updated_at",
237
+ "merged_at",
238
+ "merge_state",
239
+ }
240
+ )
241
+
242
+ def update(self, worktree_id: str, **fields: Any) -> Worktree | None:
243
+ """
244
+ Update worktree fields.
245
+
246
+ Args:
247
+ worktree_id: Worktree ID to update
248
+ **fields: Fields to update (must be valid column names)
249
+
250
+ Returns:
251
+ Updated Worktree or None if not found
252
+
253
+ Raises:
254
+ ValueError: If any field name is not in the allowlist
255
+ """
256
+ if not fields:
257
+ return self.get(worktree_id)
258
+
259
+ # Validate field names against allowlist to prevent SQL injection
260
+ invalid_fields = set(fields.keys()) - self._VALID_UPDATE_FIELDS
261
+ if invalid_fields:
262
+ raise ValueError(f"Invalid field names: {invalid_fields}")
263
+
264
+ # Add updated_at timestamp
265
+ fields["updated_at"] = datetime.now(UTC).isoformat()
266
+
267
+ # nosec B608: Fields validated against _VALID_UPDATE_FIELDS allowlist above
268
+ set_clause = ", ".join(f"{key} = ?" for key in fields.keys())
269
+ values = list(fields.values()) + [worktree_id]
270
+
271
+ self.db.execute(
272
+ f"UPDATE worktrees SET {set_clause} WHERE id = ?", # nosec B608
273
+ tuple(values),
274
+ )
275
+
276
+ return self.get(worktree_id)
277
+
278
+ def delete(self, worktree_id: str) -> bool:
279
+ """
280
+ Delete worktree record.
281
+
282
+ Args:
283
+ worktree_id: Worktree ID to delete
284
+
285
+ Returns:
286
+ True if deleted, False if not found
287
+ """
288
+ cursor = self.db.execute("DELETE FROM worktrees WHERE id = ?", (worktree_id,))
289
+ return cursor.rowcount > 0
290
+
291
+ # Status transition methods
292
+
293
+ def claim(self, worktree_id: str, session_id: str) -> Worktree | None:
294
+ """
295
+ Claim ownership of a worktree for a session.
296
+
297
+ Args:
298
+ worktree_id: Worktree ID
299
+ session_id: Session ID claiming ownership
300
+
301
+ Returns:
302
+ Updated Worktree or None if not found
303
+ """
304
+ return self.update(worktree_id, agent_session_id=session_id)
305
+
306
+ def release(self, worktree_id: str) -> Worktree | None:
307
+ """
308
+ Release ownership of a worktree.
309
+
310
+ Args:
311
+ worktree_id: Worktree ID
312
+
313
+ Returns:
314
+ Updated Worktree or None if not found
315
+ """
316
+ return self.update(worktree_id, agent_session_id=None)
317
+
318
+ def mark_stale(self, worktree_id: str) -> Worktree | None:
319
+ """
320
+ Mark worktree as stale (inactive).
321
+
322
+ Args:
323
+ worktree_id: Worktree ID
324
+
325
+ Returns:
326
+ Updated Worktree or None if not found
327
+ """
328
+ return self.update(worktree_id, status=WorktreeStatus.STALE.value)
329
+
330
+ def mark_merged(self, worktree_id: str) -> Worktree | None:
331
+ """
332
+ Mark worktree as merged.
333
+
334
+ Args:
335
+ worktree_id: Worktree ID
336
+
337
+ Returns:
338
+ Updated Worktree or None if not found
339
+ """
340
+ now = datetime.now(UTC).isoformat()
341
+ return self.update(
342
+ worktree_id,
343
+ status=WorktreeStatus.MERGED.value,
344
+ merged_at=now,
345
+ )
346
+
347
+ def mark_abandoned(self, worktree_id: str) -> Worktree | None:
348
+ """
349
+ Mark worktree as abandoned.
350
+
351
+ Args:
352
+ worktree_id: Worktree ID
353
+
354
+ Returns:
355
+ Updated Worktree or None if not found
356
+ """
357
+ return self.update(worktree_id, status=WorktreeStatus.ABANDONED.value)
358
+
359
+ def find_stale(
360
+ self,
361
+ project_id: str,
362
+ hours: int = 24,
363
+ limit: int = 50,
364
+ ) -> list[Worktree]:
365
+ """
366
+ Find worktrees that are stale (no activity for N hours).
367
+
368
+ Args:
369
+ project_id: Project ID
370
+ hours: Hours of inactivity threshold
371
+ limit: Maximum number of results
372
+
373
+ Returns:
374
+ List of stale Worktree instances
375
+ """
376
+ # Calculate cutoff time
377
+ from datetime import timedelta
378
+
379
+ cutoff = (datetime.now(UTC) - timedelta(hours=hours)).isoformat()
380
+
381
+ rows = self.db.fetchall(
382
+ """
383
+ SELECT * FROM worktrees
384
+ WHERE project_id = ?
385
+ AND status = ?
386
+ AND updated_at < ?
387
+ ORDER BY updated_at ASC
388
+ LIMIT ?
389
+ """,
390
+ (project_id, WorktreeStatus.ACTIVE.value, cutoff, limit),
391
+ )
392
+ return [Worktree.from_row(row) for row in rows]
393
+
394
+ def cleanup_stale(
395
+ self,
396
+ project_id: str,
397
+ hours: int = 24,
398
+ dry_run: bool = True,
399
+ ) -> list[Worktree]:
400
+ """
401
+ Mark stale worktrees as abandoned.
402
+
403
+ This only updates the database status. The actual git worktree
404
+ cleanup should be done by the WorktreeManager after calling this.
405
+
406
+ Args:
407
+ project_id: Project ID
408
+ hours: Hours of inactivity threshold
409
+ dry_run: If True, just return candidates without updating
410
+
411
+ Returns:
412
+ List of worktrees marked/to be marked as abandoned.
413
+ When dry_run is False, returns refreshed worktrees with updated status.
414
+ """
415
+ stale = self.find_stale(project_id, hours)
416
+
417
+ if not dry_run:
418
+ updated: list[Worktree] = []
419
+ for worktree in stale:
420
+ # mark_abandoned returns the updated Worktree
421
+ result = self.mark_abandoned(worktree.id)
422
+ if result is not None:
423
+ updated.append(result)
424
+ return updated
425
+
426
+ return stale
427
+
428
+ def count_by_status(self, project_id: str) -> dict[str, int]:
429
+ """
430
+ Get count of worktrees by status for a project.
431
+
432
+ Args:
433
+ project_id: Project ID
434
+
435
+ Returns:
436
+ Dict mapping status to count
437
+ """
438
+ rows = self.db.fetchall(
439
+ """
440
+ SELECT status, COUNT(*) as count
441
+ FROM worktrees
442
+ WHERE project_id = ?
443
+ GROUP BY status
444
+ """,
445
+ (project_id,),
446
+ )
447
+ return {row["status"]: row["count"] for row in rows}
448
+
449
+ # Merge state methods
450
+
451
+ def set_merge_state(self, worktree_id: str, merge_state: str | None) -> Worktree | None:
452
+ """
453
+ Set the merge state for a worktree.
454
+
455
+ Args:
456
+ worktree_id: Worktree ID
457
+ merge_state: Merge state ("pending", "resolved", or None)
458
+
459
+ Returns:
460
+ Updated Worktree or None if not found
461
+ """
462
+ return self.update(worktree_id, merge_state=merge_state)
463
+
464
+ def get_by_merge_state(
465
+ self,
466
+ merge_state: str,
467
+ project_id: str | None = None,
468
+ limit: int = 50,
469
+ ) -> list[Worktree]:
470
+ """
471
+ Get worktrees by merge state.
472
+
473
+ Args:
474
+ merge_state: Merge state to filter by
475
+ project_id: Optional project ID filter
476
+ limit: Maximum number of results
477
+
478
+ Returns:
479
+ List of Worktree instances with the given merge state
480
+ """
481
+ if project_id:
482
+ rows = self.db.fetchall(
483
+ """
484
+ SELECT * FROM worktrees
485
+ WHERE merge_state = ? AND project_id = ?
486
+ ORDER BY updated_at DESC
487
+ LIMIT ?
488
+ """,
489
+ (merge_state, project_id, limit),
490
+ )
491
+ else:
492
+ rows = self.db.fetchall(
493
+ """
494
+ SELECT * FROM worktrees
495
+ WHERE merge_state = ?
496
+ ORDER BY updated_at DESC
497
+ LIMIT ?
498
+ """,
499
+ (merge_state, limit),
500
+ )
501
+ return [Worktree.from_row(row) for row in rows]
502
+
503
+ def sync_with_merge_resolution(
504
+ self,
505
+ worktree_id: str,
506
+ merge_manager: Any | None = None,
507
+ strategy: str = "auto",
508
+ ) -> dict[str, Any]:
509
+ """
510
+ Sync worktree with merge resolution support.
511
+
512
+ When conflicts are detected during sync, a merge resolution
513
+ is initiated with the specified strategy.
514
+
515
+ Args:
516
+ worktree_id: Worktree ID
517
+ merge_manager: MergeResolutionManager for creating resolutions
518
+ strategy: Resolution strategy ("auto", "ai-only", "human")
519
+
520
+ Returns:
521
+ Dict with sync result and optional merge info
522
+ """
523
+ worktree = self.get(worktree_id)
524
+ if not worktree:
525
+ return {"success": False, "error": "Worktree not found"}
526
+
527
+ # Placeholder: actual sync would involve git operations
528
+ # and detection of merge conflicts
529
+
530
+ return {
531
+ "success": True,
532
+ "worktree_id": worktree_id,
533
+ "merge_initiated": False,
534
+ "message": "Sync completed without conflicts",
535
+ }
536
+
537
+ def sync(self, worktree_id: str) -> dict[str, Any]:
538
+ """
539
+ Basic sync without merge resolution.
540
+
541
+ Args:
542
+ worktree_id: Worktree ID
543
+
544
+ Returns:
545
+ Dict with sync result
546
+ """
547
+ return self.sync_with_merge_resolution(worktree_id)
gobby/sync/__init__.py ADDED
@@ -0,0 +1,29 @@
1
+ """Sync services for external integrations.
2
+
3
+ This module provides sync services that orchestrate between gobby tasks
4
+ and external services like GitHub and Linear.
5
+ """
6
+
7
+ from gobby.sync.github import (
8
+ GitHubNotFoundError,
9
+ GitHubRateLimitError,
10
+ GitHubSyncError,
11
+ GitHubSyncService,
12
+ )
13
+ from gobby.sync.linear import (
14
+ LinearNotFoundError,
15
+ LinearRateLimitError,
16
+ LinearSyncError,
17
+ LinearSyncService,
18
+ )
19
+
20
+ __all__ = [
21
+ "GitHubSyncService",
22
+ "GitHubSyncError",
23
+ "GitHubRateLimitError",
24
+ "GitHubNotFoundError",
25
+ "LinearSyncService",
26
+ "LinearSyncError",
27
+ "LinearRateLimitError",
28
+ "LinearNotFoundError",
29
+ ]