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,343 @@
1
+ """Task query operations.
2
+
3
+ This module provides query operations for listing and filtering tasks:
4
+ - list_tasks: General task listing with filters
5
+ - list_ready_tasks: Tasks ready to work on (not blocked)
6
+ - list_blocked_tasks: Tasks blocked by dependencies
7
+ - list_workflow_tasks: Tasks associated with a workflow
8
+ """
9
+
10
+ from typing import Any
11
+
12
+ from gobby.storage.database import DatabaseProtocol
13
+ from gobby.storage.tasks._models import Task
14
+ from gobby.storage.tasks._ordering import order_tasks_hierarchically
15
+
16
+
17
+ def list_tasks(
18
+ db: DatabaseProtocol,
19
+ project_id: str | None = None,
20
+ status: str | list[str] | None = None,
21
+ priority: int | None = None,
22
+ assignee: str | None = None,
23
+ task_type: str | None = None,
24
+ label: str | None = None,
25
+ parent_task_id: str | None = None,
26
+ title_like: str | None = None,
27
+ limit: int = 50,
28
+ offset: int = 0,
29
+ ) -> list[Task]:
30
+ """List tasks with filtering.
31
+
32
+ Args:
33
+ db: Database protocol instance
34
+ project_id: Filter by project
35
+ status: Filter by status. Can be a single status string, a list of statuses,
36
+ or None to include all statuses.
37
+ priority: Filter by priority
38
+ assignee: Filter by assignee
39
+ task_type: Filter by task type
40
+ label: Filter by label
41
+ parent_task_id: Filter by parent task
42
+ title_like: Filter by title (partial match)
43
+ limit: Maximum tasks to return
44
+ offset: Pagination offset
45
+
46
+ Results are ordered hierarchically: parents appear before their children,
47
+ with siblings sorted by priority ASC, then created_at ASC.
48
+ """
49
+ query = "SELECT * FROM tasks WHERE 1=1"
50
+ params: list[Any] = []
51
+
52
+ if project_id:
53
+ query += " AND project_id = ?"
54
+ params.append(project_id)
55
+ if status:
56
+ if isinstance(status, list):
57
+ placeholders = ", ".join("?" for _ in status)
58
+ query += f" AND status IN ({placeholders})"
59
+ params.extend(status)
60
+ else:
61
+ query += " AND status = ?"
62
+ params.append(status)
63
+ if priority:
64
+ query += " AND priority = ?"
65
+ params.append(priority)
66
+ if assignee:
67
+ query += " AND assignee = ?"
68
+ params.append(assignee)
69
+ if task_type:
70
+ query += " AND task_type = ?"
71
+ params.append(task_type)
72
+ if label:
73
+ # tasks.labels is a JSON list. We use json_each to find if the label is in the list.
74
+ query += " AND EXISTS (SELECT 1 FROM json_each(tasks.labels) WHERE value = ?)"
75
+ params.append(label)
76
+ if parent_task_id:
77
+ query += " AND parent_task_id = ?"
78
+ params.append(parent_task_id)
79
+ if title_like:
80
+ query += " AND title LIKE ?"
81
+ params.append(f"%{title_like}%")
82
+
83
+ # Fetch with base ordering, then apply hierarchical sort in Python
84
+ query += " ORDER BY priority ASC, created_at ASC LIMIT ? OFFSET ?"
85
+ params.extend([limit, offset])
86
+
87
+ rows = db.fetchall(query, tuple(params))
88
+ tasks = [Task.from_row(row) for row in rows]
89
+
90
+ # Bulk fetch dependencies for these tasks to support topological sort
91
+ if tasks:
92
+ task_ids = [t.id for t in tasks]
93
+ placeholders = ", ".join("?" for _ in task_ids)
94
+ # nosec B608: placeholders are just '?' characters, values parameterized
95
+ dep_rows = db.fetchall(
96
+ f"SELECT task_id, depends_on FROM task_dependencies WHERE dep_type = 'blocks' AND task_id IN ({placeholders})", # nosec B608
97
+ tuple(task_ids),
98
+ )
99
+
100
+ # Map by task_id -> set of blockers
101
+ blockers_map: dict[str, set[str]] = {}
102
+ for row in dep_rows:
103
+ tid = row["task_id"]
104
+ blocker = row["depends_on"]
105
+ if tid not in blockers_map:
106
+ blockers_map[tid] = set()
107
+ blockers_map[tid].add(blocker)
108
+
109
+ # Populate task objects
110
+ for task in tasks:
111
+ if task.id in blockers_map:
112
+ task.blocked_by = blockers_map[task.id]
113
+
114
+ return order_tasks_hierarchically(tasks)
115
+
116
+
117
+ def list_ready_tasks(
118
+ db: DatabaseProtocol,
119
+ project_id: str | None = None,
120
+ priority: int | None = None,
121
+ task_type: str | None = None,
122
+ assignee: str | None = None,
123
+ parent_task_id: str | None = None,
124
+ limit: int = 50,
125
+ offset: int = 0,
126
+ ) -> list[Task]:
127
+ """List tasks that are ready to work on (open or in_progress) and not blocked.
128
+
129
+ A task is ready if:
130
+ 1. It is open or in_progress
131
+ 2. It has no open blocking dependencies
132
+ 3. Its parent (if any) is also ready (recursive check up the chain)
133
+
134
+ Note: in_progress tasks are included because they represent active work
135
+ that should remain visible in the ready queue.
136
+
137
+ Results are ordered hierarchically: parents appear before their children,
138
+ with siblings sorted by priority ASC, then created_at ASC.
139
+
140
+ Note: The limit is applied AFTER hierarchical ordering to ensure coherent
141
+ tree structures. We fetch all ready tasks, order them hierarchically,
142
+ then return the first N tasks in tree traversal order.
143
+ """
144
+ # Use recursive CTE to find tasks with ready parent chains
145
+ query = """
146
+ WITH RECURSIVE ready_tasks AS (
147
+ -- Base case: open/in_progress tasks with no parent and no external blocking deps
148
+ SELECT t.id FROM tasks t
149
+ WHERE t.status IN ('open', 'in_progress')
150
+ AND t.parent_task_id IS NULL
151
+ AND NOT EXISTS (
152
+ SELECT 1 FROM task_dependencies d
153
+ JOIN tasks blocker ON d.depends_on = blocker.id
154
+ WHERE d.task_id = t.id
155
+ AND d.dep_type = 'blocks'
156
+ -- Blocker is unresolved if not closed AND not in review without requiring user review
157
+ AND NOT (
158
+ blocker.status = 'closed'
159
+ OR (blocker.status = 'review' AND blocker.requires_user_review = 0)
160
+ )
161
+ -- Exclude ancestor blocked by any descendant (completion block, not work block)
162
+ AND NOT EXISTS (
163
+ WITH RECURSIVE ancestors AS (
164
+ SELECT blocker.parent_task_id AS ancestor_id
165
+ UNION ALL
166
+ SELECT p.parent_task_id
167
+ FROM tasks p
168
+ JOIN ancestors a ON p.id = a.ancestor_id
169
+ WHERE p.parent_task_id IS NOT NULL
170
+ )
171
+ SELECT 1 FROM ancestors WHERE ancestor_id = t.id
172
+ )
173
+ )
174
+
175
+ UNION ALL
176
+
177
+ -- Recursive case: open/in_progress tasks whose parent is ready and no external blocking deps
178
+ SELECT t.id FROM tasks t
179
+ JOIN ready_tasks rt ON t.parent_task_id = rt.id
180
+ WHERE t.status IN ('open', 'in_progress')
181
+ AND NOT EXISTS (
182
+ SELECT 1 FROM task_dependencies d
183
+ JOIN tasks blocker ON d.depends_on = blocker.id
184
+ WHERE d.task_id = t.id
185
+ AND d.dep_type = 'blocks'
186
+ -- Blocker is unresolved if not closed AND not in review without requiring user review
187
+ AND NOT (
188
+ blocker.status = 'closed'
189
+ OR (blocker.status = 'review' AND blocker.requires_user_review = 0)
190
+ )
191
+ -- Exclude ancestor blocked by any descendant (completion block, not work block)
192
+ AND NOT EXISTS (
193
+ WITH RECURSIVE ancestors AS (
194
+ SELECT blocker.parent_task_id AS ancestor_id
195
+ UNION ALL
196
+ SELECT p.parent_task_id
197
+ FROM tasks p
198
+ JOIN ancestors a ON p.id = a.ancestor_id
199
+ WHERE p.parent_task_id IS NOT NULL
200
+ )
201
+ SELECT 1 FROM ancestors WHERE ancestor_id = t.id
202
+ )
203
+ )
204
+ )
205
+ SELECT t.* FROM tasks t
206
+ JOIN ready_tasks rt ON t.id = rt.id
207
+ WHERE 1=1
208
+ """
209
+ params: list[Any] = []
210
+
211
+ if project_id:
212
+ query += " AND t.project_id = ?"
213
+ params.append(project_id)
214
+ if priority:
215
+ query += " AND t.priority = ?"
216
+ params.append(priority)
217
+ if task_type:
218
+ query += " AND t.task_type = ?"
219
+ params.append(task_type)
220
+ if assignee:
221
+ query += " AND t.assignee = ?"
222
+ params.append(assignee)
223
+ if parent_task_id:
224
+ query += " AND t.parent_task_id = ?"
225
+ params.append(parent_task_id)
226
+
227
+ # Fetch all matching tasks (no SQL limit) so we can order hierarchically first
228
+ internal_limit = 1000
229
+ query += " ORDER BY t.priority ASC, t.created_at ASC LIMIT ?"
230
+ params.append(internal_limit)
231
+
232
+ rows = db.fetchall(query, tuple(params))
233
+ tasks = [Task.from_row(row) for row in rows]
234
+
235
+ # Order hierarchically, then apply user's limit/offset
236
+ ordered = order_tasks_hierarchically(tasks)
237
+ return ordered[offset : offset + limit] if limit else ordered
238
+
239
+
240
+ def list_blocked_tasks(
241
+ db: DatabaseProtocol,
242
+ project_id: str | None = None,
243
+ parent_task_id: str | None = None,
244
+ limit: int = 50,
245
+ offset: int = 0,
246
+ ) -> list[Task]:
247
+ """List tasks that are blocked by at least one open blocking dependency.
248
+
249
+ Only considers "external" blockers - excludes parent tasks being blocked
250
+ by their own descendants (which is a "completion" block, not a "work" block).
251
+
252
+ Results are ordered hierarchically: parents appear before their children,
253
+ with siblings sorted by priority ASC, then created_at ASC.
254
+
255
+ Note: The limit is applied AFTER hierarchical ordering to ensure coherent
256
+ tree structures.
257
+ """
258
+ query = """
259
+ SELECT t.* FROM tasks t
260
+ WHERE t.status = 'open'
261
+ AND EXISTS (
262
+ SELECT 1 FROM task_dependencies d
263
+ JOIN tasks blocker ON d.depends_on = blocker.id
264
+ WHERE d.task_id = t.id
265
+ AND d.dep_type = 'blocks'
266
+ -- Blocker is unresolved if not closed AND not in review without requiring user review
267
+ AND NOT (
268
+ blocker.status = 'closed'
269
+ OR (blocker.status = 'review' AND blocker.requires_user_review = 0)
270
+ )
271
+ -- Exclude ancestor blocked by any descendant (completion block, not work block)
272
+ AND NOT EXISTS (
273
+ WITH RECURSIVE ancestors AS (
274
+ SELECT blocker.parent_task_id AS ancestor_id
275
+ UNION ALL
276
+ SELECT p.parent_task_id
277
+ FROM tasks p
278
+ JOIN ancestors a ON p.id = a.ancestor_id
279
+ WHERE p.parent_task_id IS NOT NULL
280
+ )
281
+ SELECT 1 FROM ancestors WHERE ancestor_id = t.id
282
+ )
283
+ )
284
+ """
285
+ params: list[Any] = []
286
+
287
+ if project_id:
288
+ query += " AND t.project_id = ?"
289
+ params.append(project_id)
290
+ if parent_task_id:
291
+ query += " AND t.parent_task_id = ?"
292
+ params.append(parent_task_id)
293
+
294
+ # Fetch all matching tasks (no SQL limit) so we can order hierarchically first
295
+ internal_limit = 1000
296
+ query += " ORDER BY t.priority ASC, t.created_at ASC LIMIT ?"
297
+ params.append(internal_limit)
298
+
299
+ rows = db.fetchall(query, tuple(params))
300
+ tasks = [Task.from_row(row) for row in rows]
301
+
302
+ # Order hierarchically, then apply user's limit/offset
303
+ ordered = order_tasks_hierarchically(tasks)
304
+ return ordered[offset : offset + limit] if limit else ordered
305
+
306
+
307
+ def list_workflow_tasks(
308
+ db: DatabaseProtocol,
309
+ workflow_name: str,
310
+ project_id: str | None = None,
311
+ status: str | None = None,
312
+ limit: int = 100,
313
+ offset: int = 0,
314
+ ) -> list[Task]:
315
+ """List tasks associated with a workflow, ordered by sequence_order.
316
+
317
+ Args:
318
+ db: Database protocol instance
319
+ workflow_name: The workflow name to filter by
320
+ project_id: Optional project ID filter
321
+ status: Optional status filter ('open', 'in_progress', 'closed')
322
+ limit: Maximum tasks to return
323
+ offset: Pagination offset
324
+
325
+ Returns:
326
+ List of tasks ordered by sequence_order (nulls last), then created_at
327
+ """
328
+ query = "SELECT * FROM tasks WHERE workflow_name = ?"
329
+ params: list[Any] = [workflow_name]
330
+
331
+ if project_id:
332
+ query += " AND project_id = ?"
333
+ params.append(project_id)
334
+ if status:
335
+ query += " AND status = ?"
336
+ params.append(status)
337
+
338
+ # Order by sequence_order (nulls last), then created_at
339
+ query += " ORDER BY COALESCE(sequence_order, 999999) ASC, created_at ASC LIMIT ? OFFSET ?"
340
+ params.extend([limit, offset])
341
+
342
+ rows = db.fetchall(query, tuple(params))
343
+ return [Task.from_row(row) for row in rows]
@@ -0,0 +1,143 @@
1
+ """Task search module using TF-IDF.
2
+
3
+ Provides semantic search capabilities for tasks using the shared TF-IDF backend.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from gobby.search import TFIDFSearcher
12
+
13
+ if TYPE_CHECKING:
14
+ from gobby.storage.tasks._models import Task
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def build_searchable_content(task: Task) -> str:
20
+ """
21
+ Build searchable text content from a task.
22
+
23
+ Combines title + description + labels into a single searchable string.
24
+ This ensures all relevant text is indexed for search.
25
+
26
+ Args:
27
+ task: Task object to extract content from
28
+
29
+ Returns:
30
+ Concatenated searchable text
31
+ """
32
+ parts: list[str] = []
33
+
34
+ # Title is always present and most important
35
+ if task.title:
36
+ parts.append(task.title)
37
+
38
+ # Description provides additional context
39
+ if task.description:
40
+ parts.append(task.description)
41
+
42
+ # Labels can contain useful keywords
43
+ if task.labels:
44
+ parts.append(" ".join(task.labels))
45
+
46
+ # Task type can be useful for filtering
47
+ if task.task_type:
48
+ parts.append(task.task_type)
49
+
50
+ # Category can help with domain filtering
51
+ if task.category:
52
+ parts.append(task.category)
53
+
54
+ return " ".join(parts)
55
+
56
+
57
+ class TaskSearcher:
58
+ """
59
+ TF-IDF based search for tasks.
60
+
61
+ Wraps the generic TFIDFSearcher with task-specific content building.
62
+ Supports lazy fitting and dirty tracking for efficient reindexing.
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ ngram_range: tuple[int, int] = (1, 2),
68
+ max_features: int = 10000,
69
+ min_df: int = 1,
70
+ stop_words: str | None = "english",
71
+ refit_threshold: int = 10,
72
+ ):
73
+ """
74
+ Initialize task searcher.
75
+
76
+ Args:
77
+ ngram_range: Min/max n-gram sizes for tokenization
78
+ max_features: Maximum vocabulary size
79
+ min_df: Minimum document frequency for inclusion
80
+ stop_words: Language for stop words (None to disable)
81
+ refit_threshold: Number of updates before automatic refit
82
+ """
83
+ self._searcher = TFIDFSearcher(
84
+ ngram_range=ngram_range,
85
+ max_features=max_features,
86
+ min_df=min_df,
87
+ stop_words=stop_words,
88
+ refit_threshold=refit_threshold,
89
+ )
90
+ self._dirty = True
91
+
92
+ def fit(self, tasks: list[Task]) -> None:
93
+ """
94
+ Build the search index from tasks.
95
+
96
+ Args:
97
+ tasks: List of Task objects to index
98
+ """
99
+ if not tasks:
100
+ self._searcher.fit([])
101
+ self._dirty = False
102
+ return
103
+
104
+ # Build (task_id, content) tuples
105
+ items = [(task.id, build_searchable_content(task)) for task in tasks]
106
+
107
+ self._searcher.fit(items)
108
+ self._dirty = False
109
+ logger.info(f"Task search index built with {len(tasks)} tasks")
110
+
111
+ def search(self, query: str, top_k: int = 20) -> list[tuple[str, float]]:
112
+ """
113
+ Search for tasks matching the query.
114
+
115
+ Args:
116
+ query: Search query text
117
+ top_k: Maximum number of results to return
118
+
119
+ Returns:
120
+ List of (task_id, similarity_score) tuples, sorted by
121
+ similarity descending.
122
+ """
123
+ return self._searcher.search(query, top_k)
124
+
125
+ def needs_refit(self) -> bool:
126
+ """Check if the index needs rebuilding."""
127
+ return self._dirty or self._searcher.needs_refit()
128
+
129
+ def mark_dirty(self) -> None:
130
+ """Mark the index as needing a refit."""
131
+ self._dirty = True
132
+ self._searcher.mark_update()
133
+
134
+ def get_stats(self) -> dict[str, Any]:
135
+ """Get statistics about the search index."""
136
+ stats = self._searcher.get_stats()
137
+ stats["dirty"] = self._dirty
138
+ return stats
139
+
140
+ def clear(self) -> None:
141
+ """Clear the search index."""
142
+ self._searcher.clear()
143
+ self._dirty = True