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,223 @@
1
+ import logging
2
+ import sqlite3
3
+ from dataclasses import dataclass
4
+ from datetime import UTC, datetime
5
+ from typing import Any, Literal
6
+
7
+ from gobby.storage.database import DatabaseProtocol
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ DependencyType = Literal["blocks", "related", "discovered-from"]
12
+
13
+
14
+ @dataclass
15
+ class TaskDependency:
16
+ id: int
17
+ task_id: str
18
+ depends_on: str
19
+ dep_type: DependencyType
20
+ created_at: str
21
+
22
+ @classmethod
23
+ def from_row(cls, row: sqlite3.Row) -> "TaskDependency":
24
+ return cls(
25
+ id=row["id"],
26
+ task_id=row["task_id"],
27
+ depends_on=row["depends_on"],
28
+ dep_type=row["dep_type"],
29
+ created_at=row["created_at"],
30
+ )
31
+
32
+ def to_dict(self) -> dict[str, Any]:
33
+ """Convert TaskDependency to dictionary."""
34
+ return {
35
+ "id": self.id,
36
+ "task_id": self.task_id,
37
+ "depends_on": self.depends_on,
38
+ "dep_type": self.dep_type,
39
+ "created_at": self.created_at,
40
+ }
41
+
42
+
43
+ class DependencyCycleError(Exception):
44
+ """Raised when a dependency cycle is detected."""
45
+
46
+ pass
47
+
48
+
49
+ class TaskDependencyManager:
50
+ def __init__(self, db: DatabaseProtocol):
51
+ self.db = db
52
+
53
+ def add_dependency(
54
+ self, task_id: str, depends_on: str, dep_type: DependencyType = "blocks"
55
+ ) -> TaskDependency:
56
+ """Add a dependency."""
57
+ if task_id == depends_on:
58
+ raise ValueError("Task cannot depend on itself")
59
+
60
+ # For 'blocks', prevent cycles
61
+ if dep_type == "blocks" and self._would_create_cycle(task_id, depends_on):
62
+ raise DependencyCycleError(
63
+ f"Adding dependency {task_id} blocks {depends_on} would create a cycle"
64
+ )
65
+
66
+ now = datetime.now(UTC).isoformat()
67
+
68
+ with self.db.transaction() as conn:
69
+ cursor = conn.execute(
70
+ "INSERT INTO task_dependencies (task_id, depends_on, dep_type, created_at) VALUES (?, ?, ?, ?)",
71
+ (task_id, depends_on, dep_type, now),
72
+ )
73
+ dep_id = cursor.lastrowid
74
+
75
+ if dep_id is None:
76
+ raise ValueError("Failed to retrieve dependency ID")
77
+
78
+ return TaskDependency(dep_id, task_id, depends_on, dep_type, now)
79
+
80
+ def remove_dependency(self, task_id: str, depends_on: str) -> bool:
81
+ """Remove a dependency."""
82
+ with self.db.transaction() as conn:
83
+ cursor = conn.execute(
84
+ "DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?",
85
+ (task_id, depends_on),
86
+ )
87
+ deleted: bool = cursor.rowcount > 0
88
+ return deleted
89
+
90
+ def get_blockers(self, task_id: str) -> list[TaskDependency]:
91
+ """Get tasks that block this task (task_id depends on X)."""
92
+ rows = self.db.fetchall(
93
+ "SELECT * FROM task_dependencies WHERE task_id = ? AND dep_type = 'blocks'",
94
+ (task_id,),
95
+ )
96
+ return [TaskDependency.from_row(row) for row in rows]
97
+
98
+ def get_blocking(self, task_id: str) -> list[TaskDependency]:
99
+ """Get tasks that this task blocks (X depends on task_id)."""
100
+ rows = self.db.fetchall(
101
+ "SELECT * FROM task_dependencies WHERE depends_on = ? AND dep_type = 'blocks'",
102
+ (task_id,),
103
+ )
104
+ return [TaskDependency.from_row(row) for row in rows]
105
+
106
+ def get_all_dependencies(self, task_id: str) -> list[TaskDependency]:
107
+ """Get all dependencies for a task (outgoing edges)."""
108
+ rows = self.db.fetchall(
109
+ "SELECT * FROM task_dependencies WHERE task_id = ?",
110
+ (task_id,),
111
+ )
112
+ return [TaskDependency.from_row(row) for row in rows]
113
+
114
+ def _would_create_cycle(self, task_id: str, depends_on: str) -> bool:
115
+ """
116
+ Check if adding edge task_id -> depends_on creates a cycle.
117
+ This implies exists path depends_on -> ... -> task_id.
118
+ """
119
+ visited = set()
120
+ stack = [depends_on]
121
+
122
+ while stack:
123
+ current = stack.pop()
124
+ if current == task_id:
125
+ return True
126
+
127
+ if current in visited:
128
+ continue
129
+ visited.add(current)
130
+
131
+ deps = self.db.fetchall(
132
+ "SELECT depends_on FROM task_dependencies WHERE task_id = ? AND dep_type = 'blocks'",
133
+ (current,),
134
+ )
135
+ for row in deps:
136
+ stack.append(row["depends_on"])
137
+
138
+ return False
139
+
140
+ def get_dependency_tree(
141
+ self,
142
+ task_id: str,
143
+ direction: Literal["blockers", "blocking", "both"] = "both",
144
+ max_depth: int = 10,
145
+ ) -> dict[str, Any]:
146
+ """
147
+ Get dependency tree.
148
+ direction:
149
+ - blockers: tasks that task_id depends on (upstream)
150
+ - blocking: tasks that depend on task_id (downstream)
151
+ - both: both
152
+ """
153
+ result: dict[str, Any] = {"id": task_id}
154
+
155
+ if max_depth <= 0:
156
+ result["_truncated"] = True
157
+ return result
158
+
159
+ if direction in ("blockers", "both"):
160
+ blockers = self.get_blockers(task_id)
161
+ if blockers:
162
+ result["blockers"] = [
163
+ self.get_dependency_tree(
164
+ b.depends_on, direction="blockers", max_depth=max_depth - 1
165
+ )
166
+ for b in blockers
167
+ ]
168
+
169
+ if direction in ("blocking", "both"):
170
+ blocking = self.get_blocking(task_id)
171
+ if blocking:
172
+ # blocking contains deps where depends_on = task_id. The other end is task_id.
173
+ # Use b.task_id (the task that is blocked).
174
+ result["blocking"] = [
175
+ self.get_dependency_tree(
176
+ b.task_id, direction="blocking", max_depth=max_depth - 1
177
+ )
178
+ for b in blocking
179
+ ]
180
+
181
+ return result
182
+
183
+ def check_cycles(self) -> list[list[str]]:
184
+ """Detect all cycles in 'blocks' dependencies. Returns list of cycles (list of task IDs)."""
185
+ rows = self.db.fetchall(
186
+ "SELECT task_id, depends_on FROM task_dependencies WHERE dep_type = 'blocks'"
187
+ )
188
+ graph: dict[str, list[str]] = {}
189
+ for row in rows:
190
+ u, v = row["task_id"], row["depends_on"]
191
+ graph.setdefault(u, []).append(v)
192
+ graph.setdefault(v, [])
193
+
194
+ cycles = []
195
+ visited = set()
196
+ path = []
197
+ path_set = set()
198
+
199
+ def dfs(u: str) -> None:
200
+ visited.add(u)
201
+ path.append(u)
202
+ path_set.add(u)
203
+
204
+ for v in graph.get(u, []):
205
+ if v in path_set:
206
+ # Cycle found
207
+ # cycle is from v to ... to u to v
208
+ try:
209
+ idx = path.index(v)
210
+ cycles.append(path[idx:].copy())
211
+ except ValueError:
212
+ pass
213
+ elif v not in visited:
214
+ dfs(v)
215
+
216
+ path_set.remove(u)
217
+ path.pop()
218
+
219
+ for node in list(graph.keys()):
220
+ if node not in visited:
221
+ dfs(node)
222
+
223
+ return cycles
@@ -0,0 +1,42 @@
1
+ """Task storage module.
2
+
3
+ This package provides task management functionality including:
4
+ - Task dataclass and serialization
5
+ - LocalTaskManager for CRUD operations
6
+ - Task ID generation and resolution
7
+ - Hierarchical ordering utilities
8
+
9
+ All public symbols are re-exported for backward compatibility.
10
+ """
11
+
12
+ from gobby.storage.tasks._id import generate_task_id
13
+ from gobby.storage.tasks._manager import LocalTaskManager
14
+ from gobby.storage.tasks._models import (
15
+ PRIORITY_MAP,
16
+ UNSET,
17
+ VALID_CATEGORIES,
18
+ Task,
19
+ TaskIDCollisionError,
20
+ TaskNotFoundError,
21
+ normalize_priority,
22
+ validate_category,
23
+ )
24
+ from gobby.storage.tasks._ordering import order_tasks_hierarchically
25
+
26
+ __all__ = [
27
+ # Core classes
28
+ "Task",
29
+ "LocalTaskManager",
30
+ # Exceptions
31
+ "TaskIDCollisionError",
32
+ "TaskNotFoundError",
33
+ # Functions
34
+ "generate_task_id",
35
+ "validate_category",
36
+ "normalize_priority",
37
+ "order_tasks_hierarchically",
38
+ # Constants
39
+ "PRIORITY_MAP",
40
+ "VALID_CATEGORIES",
41
+ "UNSET",
42
+ ]
@@ -0,0 +1,180 @@
1
+ """Task aggregate operations.
2
+
3
+ This module provides aggregate operations for task counts and statistics:
4
+ - count_tasks: Count tasks with optional filters
5
+ - count_by_status: Count tasks grouped by status
6
+ - count_ready_tasks: Count tasks ready to work on
7
+ - count_blocked_tasks: Count tasks blocked by dependencies
8
+ """
9
+
10
+ from typing import Any
11
+
12
+ from gobby.storage.database import DatabaseProtocol
13
+
14
+
15
+ def count_tasks(
16
+ db: DatabaseProtocol,
17
+ project_id: str | None = None,
18
+ status: str | None = None,
19
+ ) -> int:
20
+ """Count tasks with optional filters.
21
+
22
+ Args:
23
+ db: Database protocol instance
24
+ project_id: Filter by project
25
+ status: Filter by status
26
+
27
+ Returns:
28
+ Count of matching tasks
29
+ """
30
+ query = "SELECT COUNT(*) as count FROM tasks WHERE 1=1"
31
+ params: list[Any] = []
32
+
33
+ if project_id:
34
+ query += " AND project_id = ?"
35
+ params.append(project_id)
36
+ if status:
37
+ query += " AND status = ?"
38
+ params.append(status)
39
+
40
+ result = db.fetchone(query, tuple(params))
41
+ return result["count"] if result else 0
42
+
43
+
44
+ def count_by_status(
45
+ db: DatabaseProtocol,
46
+ project_id: str | None = None,
47
+ ) -> dict[str, int]:
48
+ """Count tasks grouped by status.
49
+
50
+ Args:
51
+ db: Database protocol instance
52
+ project_id: Optional project filter
53
+
54
+ Returns:
55
+ Dictionary mapping status to count
56
+ """
57
+ query = "SELECT status, COUNT(*) as count FROM tasks"
58
+ params: list[Any] = []
59
+
60
+ if project_id:
61
+ query += " WHERE project_id = ?"
62
+ params.append(project_id)
63
+
64
+ query += " GROUP BY status"
65
+
66
+ rows = db.fetchall(query, tuple(params))
67
+ return {row["status"]: row["count"] for row in rows}
68
+
69
+
70
+ def count_ready_tasks(
71
+ db: DatabaseProtocol,
72
+ project_id: str | None = None,
73
+ ) -> int:
74
+ """Count tasks that are ready (open or in_progress) and not blocked.
75
+
76
+ A task is ready if it has no external blocking dependencies.
77
+ Excludes parent tasks blocked by their own descendants (completion block, not work block).
78
+
79
+ Args:
80
+ db: Database protocol instance
81
+ project_id: Optional project filter
82
+
83
+ Returns:
84
+ Count of ready tasks
85
+ """
86
+ # Uses the same descendant-aware predicate as list_ready_tasks.
87
+ # The is_descendant_of check uses a recursive CTE to walk up the blocker's
88
+ # ancestor chain and check if the blocked task (t.id) appears anywhere.
89
+ query = """
90
+ SELECT COUNT(*) as count FROM tasks t
91
+ WHERE t.status IN ('open', 'in_progress')
92
+ AND NOT EXISTS (
93
+ SELECT 1 FROM task_dependencies d
94
+ JOIN tasks blocker ON d.depends_on = blocker.id
95
+ WHERE d.task_id = t.id
96
+ AND d.dep_type = 'blocks'
97
+ -- Blocker is unresolved if not closed AND not in review without requiring user review
98
+ AND NOT (
99
+ blocker.status = 'closed'
100
+ OR (blocker.status = 'review' AND blocker.requires_user_review = 0)
101
+ )
102
+ -- Exclude ancestor blocked by any descendant (completion block, not work block)
103
+ -- Check if t.id appears anywhere in blocker's ancestor chain
104
+ AND NOT EXISTS (
105
+ WITH RECURSIVE ancestors AS (
106
+ SELECT blocker.parent_task_id AS ancestor_id
107
+ UNION ALL
108
+ SELECT p.parent_task_id
109
+ FROM tasks p
110
+ JOIN ancestors a ON p.id = a.ancestor_id
111
+ WHERE p.parent_task_id IS NOT NULL
112
+ )
113
+ SELECT 1 FROM ancestors WHERE ancestor_id = t.id
114
+ )
115
+ )
116
+ """
117
+ params: list[Any] = []
118
+
119
+ if project_id:
120
+ query += " AND t.project_id = ?"
121
+ params.append(project_id)
122
+
123
+ result = db.fetchone(query, tuple(params))
124
+ return result["count"] if result else 0
125
+
126
+
127
+ def count_blocked_tasks(
128
+ db: DatabaseProtocol,
129
+ project_id: str | None = None,
130
+ ) -> int:
131
+ """Count tasks that are blocked by at least one external blocking dependency.
132
+
133
+ Excludes parent tasks blocked by their own descendants (completion block, not work block).
134
+
135
+ Args:
136
+ db: Database protocol instance
137
+ project_id: Optional project filter
138
+
139
+ Returns:
140
+ Count of blocked tasks
141
+ """
142
+ # Uses the same descendant-aware predicate as list_ready_tasks.
143
+ # The is_descendant_of check uses a recursive CTE to walk up the blocker's
144
+ # ancestor chain and check if the blocked task (t.id) appears anywhere.
145
+ query = """
146
+ SELECT COUNT(*) as count FROM tasks t
147
+ WHERE t.status = 'open'
148
+ AND EXISTS (
149
+ SELECT 1 FROM task_dependencies d
150
+ JOIN tasks blocker ON d.depends_on = blocker.id
151
+ WHERE d.task_id = t.id
152
+ AND d.dep_type = 'blocks'
153
+ -- Blocker is unresolved if not closed AND not in review without requiring user review
154
+ AND NOT (
155
+ blocker.status = 'closed'
156
+ OR (blocker.status = 'review' AND blocker.requires_user_review = 0)
157
+ )
158
+ -- Exclude ancestor blocked by any descendant (completion block, not work block)
159
+ -- Check if t.id appears anywhere in blocker's ancestor chain
160
+ AND NOT EXISTS (
161
+ WITH RECURSIVE ancestors AS (
162
+ SELECT blocker.parent_task_id AS ancestor_id
163
+ UNION ALL
164
+ SELECT p.parent_task_id
165
+ FROM tasks p
166
+ JOIN ancestors a ON p.id = a.ancestor_id
167
+ WHERE p.parent_task_id IS NOT NULL
168
+ )
169
+ SELECT 1 FROM ancestors WHERE ancestor_id = t.id
170
+ )
171
+ )
172
+ """
173
+ params: list[Any] = []
174
+
175
+ if project_id:
176
+ query += " AND t.project_id = ?"
177
+ params.append(project_id)
178
+
179
+ result = db.fetchone(query, tuple(params))
180
+ return result["count"] if result else 0