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,522 @@
1
+ """
2
+ Task readiness MCP tools module.
3
+
4
+ Provides tools for task readiness management:
5
+ - list_ready_tasks: List tasks with no unresolved blocking dependencies
6
+ - list_blocked_tasks: List tasks that are blocked by dependencies
7
+ - suggest_next_task: Suggest the best next task based on scoring
8
+
9
+ Extracted from tasks.py using Strangler Fig pattern for code decomposition.
10
+ """
11
+
12
+ from collections.abc import Callable
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ from gobby.mcp_proxy.tools.internal import InternalToolRegistry
16
+ from gobby.storage.tasks import TaskNotFoundError
17
+ from gobby.utils.project_context import get_project_context
18
+ from gobby.workflows.state_manager import WorkflowStateManager
19
+
20
+ if TYPE_CHECKING:
21
+ from gobby.storage.tasks import LocalTaskManager
22
+
23
+ __all__ = [
24
+ "create_readiness_registry",
25
+ "is_descendant_of",
26
+ "_get_ancestry_chain",
27
+ "_compute_proximity_boost",
28
+ ]
29
+
30
+
31
+ def get_current_project_id() -> str | None:
32
+ """Get the current project ID from context."""
33
+ context = get_project_context()
34
+ return context.get("id") if context else None
35
+
36
+
37
+ def _get_ready_descendants(
38
+ task_manager: "LocalTaskManager",
39
+ parent_task_id: str,
40
+ task_type: str | None = None,
41
+ project_id: str | None = None,
42
+ ) -> list[Any]:
43
+ """
44
+ Get all ready tasks that are descendants of the given parent task.
45
+
46
+ Traverses the task hierarchy to find all tasks under parent_task_id,
47
+ then filters to only those that are ready (open with no blockers).
48
+
49
+ Args:
50
+ task_manager: LocalTaskManager instance
51
+ parent_task_id: ID of the ancestor task to filter by
52
+ task_type: Optional task type filter
53
+ project_id: Optional project ID filter
54
+
55
+ Returns:
56
+ List of ready Task objects that are descendants of parent_task_id
57
+ """
58
+ # Get all ready tasks first
59
+ all_ready = task_manager.list_ready_tasks(
60
+ task_type=task_type,
61
+ limit=200, # Get more since we'll filter
62
+ project_id=project_id,
63
+ )
64
+
65
+ if not all_ready:
66
+ return []
67
+
68
+ # Build a set of all descendant IDs by traversing the hierarchy
69
+ descendant_ids = set()
70
+ to_check = [parent_task_id]
71
+
72
+ while to_check:
73
+ current_id = to_check.pop()
74
+ # Get direct children of this task
75
+ children = task_manager.list_tasks(parent_task_id=current_id, limit=100)
76
+ for child in children:
77
+ if child.id not in descendant_ids:
78
+ descendant_ids.add(child.id)
79
+ to_check.append(child.id)
80
+
81
+ # Filter ready tasks to only descendants
82
+ return [t for t in all_ready if t.id in descendant_ids]
83
+
84
+
85
+ def is_descendant_of(
86
+ task_manager: "LocalTaskManager",
87
+ task_id: str,
88
+ ancestor_id: str,
89
+ ) -> bool:
90
+ """
91
+ Check if a task is a descendant of another task.
92
+
93
+ Traverses up the parent chain from task_id to check if
94
+ ancestor_id appears in the ancestry.
95
+
96
+ Args:
97
+ task_manager: LocalTaskManager instance
98
+ task_id: ID of the potential descendant task
99
+ ancestor_id: ID of the potential ancestor task
100
+
101
+ Returns:
102
+ True if task_id is a descendant of ancestor_id
103
+ """
104
+ if task_id == ancestor_id:
105
+ return True # A task is considered a descendant of itself
106
+
107
+ current_id: str | None = task_id
108
+ visited: set[str] = set()
109
+
110
+ while current_id and current_id not in visited:
111
+ visited.add(current_id)
112
+ task = task_manager.get_task(current_id)
113
+ if not task:
114
+ return False
115
+ if task.parent_task_id == ancestor_id:
116
+ return True
117
+ current_id = task.parent_task_id
118
+
119
+ return False
120
+
121
+
122
+ def _get_ancestry_chain(
123
+ task_manager: "LocalTaskManager",
124
+ task_id: str,
125
+ ) -> list[str]:
126
+ """
127
+ Build the ancestry chain for a task, from task up to root.
128
+
129
+ Args:
130
+ task_manager: LocalTaskManager instance
131
+ task_id: ID of the task to get ancestry for
132
+
133
+ Returns:
134
+ List of task IDs starting with task_id and ending with root ancestor.
135
+ Returns empty list if task doesn't exist.
136
+ """
137
+ chain: list[str] = []
138
+ current_id: str | None = task_id
139
+ visited: set[str] = set()
140
+
141
+ while current_id and current_id not in visited:
142
+ try:
143
+ task = task_manager.get_task(current_id)
144
+ except ValueError:
145
+ # Task doesn't exist - return chain so far or empty if just started
146
+ return chain if chain else []
147
+ visited.add(current_id)
148
+ chain.append(current_id)
149
+ current_id = task.parent_task_id
150
+
151
+ return chain
152
+
153
+
154
+ def _compute_proximity_boost(
155
+ task_ancestry: list[str],
156
+ active_ancestry: list[str],
157
+ ) -> int:
158
+ """
159
+ Compute proximity boost based on common ancestry.
160
+
161
+ The boost is higher for tasks closer to the active task in the hierarchy.
162
+ - If task is a descendant of the active task: max boost (50)
163
+ - Otherwise: max(0, 50 - (depth * 10)) where depth is distance to common ancestor
164
+
165
+ Args:
166
+ task_ancestry: Ancestry chain of the candidate task [task, parent, grandparent, ...]
167
+ active_ancestry: Ancestry chain of the active (in_progress) task
168
+
169
+ Returns:
170
+ Proximity boost score (0-50)
171
+ """
172
+ if not task_ancestry or not active_ancestry:
173
+ return 0
174
+
175
+ # Convert to sets for O(1) lookup
176
+ active_set = set(active_ancestry)
177
+ active_task_id = active_ancestry[0]
178
+
179
+ # Find first common ancestor and its depth from the task
180
+ for depth, ancestor_id in enumerate(task_ancestry):
181
+ if ancestor_id in active_set:
182
+ # If common ancestor is the active task itself, task is a descendant
183
+ # of active work - give max boost
184
+ if ancestor_id == active_task_id:
185
+ return 50
186
+ # Otherwise, use depth from common ancestor
187
+ return max(0, 50 - (depth * 10))
188
+
189
+ # No common ancestor found
190
+ return 0
191
+
192
+
193
+ class ReadinessToolRegistry(InternalToolRegistry):
194
+ """Registry for readiness tools with test-friendly get_tool method."""
195
+
196
+ def get_tool(self, name: str) -> Callable[..., Any] | None:
197
+ """Get a tool function by name (for testing)."""
198
+ tool = self._tools.get(name)
199
+ return tool.func if tool else None
200
+
201
+
202
+ def create_readiness_registry(
203
+ task_manager: "LocalTaskManager | None" = None,
204
+ ) -> ReadinessToolRegistry:
205
+ """
206
+ Create a registry with task readiness tools.
207
+
208
+ Args:
209
+ task_manager: LocalTaskManager instance (required)
210
+
211
+ Returns:
212
+ ReadinessToolRegistry with readiness tools registered
213
+ """
214
+ # Lazy import to avoid circular dependency
215
+ from gobby.mcp_proxy.tools.tasks import resolve_task_id_for_mcp
216
+
217
+ registry = ReadinessToolRegistry(
218
+ name="gobby-tasks-readiness",
219
+ description="Task readiness management tools",
220
+ )
221
+
222
+ if task_manager is None:
223
+ raise ValueError("task_manager is required")
224
+
225
+ # Create workflow state manager for session_task scoping
226
+ workflow_state_manager = WorkflowStateManager(task_manager.db)
227
+
228
+ # --- list_ready_tasks ---
229
+
230
+ def list_ready_tasks(
231
+ priority: int | None = None,
232
+ task_type: str | None = None,
233
+ assignee: str | None = None,
234
+ parent_task_id: str | None = None,
235
+ limit: int = 10,
236
+ all_projects: bool = False,
237
+ ) -> dict[str, Any]:
238
+ """List tasks that are open and have no unresolved blocking dependencies."""
239
+ # Filter by current project unless all_projects is True
240
+ project_id = None if all_projects else get_current_project_id()
241
+
242
+ # Resolve parent_task_id if it's a reference format
243
+ if parent_task_id:
244
+ try:
245
+ parent_task_id = resolve_task_id_for_mcp(task_manager, parent_task_id, project_id)
246
+ except (TaskNotFoundError, ValueError) as e:
247
+ return {"error": f"Invalid parent_task_id: {e}", "tasks": [], "count": 0}
248
+
249
+ tasks = task_manager.list_ready_tasks(
250
+ priority=priority,
251
+ task_type=task_type,
252
+ assignee=assignee,
253
+ parent_task_id=parent_task_id,
254
+ limit=limit,
255
+ project_id=project_id,
256
+ )
257
+ return {"tasks": [t.to_brief() for t in tasks], "count": len(tasks)}
258
+
259
+ registry.register(
260
+ name="list_ready_tasks",
261
+ description="List tasks that are open and have no unresolved blocking dependencies.",
262
+ input_schema={
263
+ "type": "object",
264
+ "properties": {
265
+ "priority": {
266
+ "type": "integer",
267
+ "description": "Filter by priority",
268
+ "default": None,
269
+ },
270
+ "task_type": {"type": "string", "description": "Filter by type", "default": None},
271
+ "assignee": {
272
+ "type": "string",
273
+ "description": "Filter by assignee",
274
+ "default": None,
275
+ },
276
+ "parent_task_id": {
277
+ "type": "string",
278
+ "description": "Filter by parent task (find ready subtasks): #N, N (seq_num), path (1.2.3), or UUID",
279
+ "default": None,
280
+ },
281
+ "limit": {"type": "integer", "description": "Max results", "default": 10},
282
+ "all_projects": {
283
+ "type": "boolean",
284
+ "description": "If true, list tasks from all projects instead of just the current project",
285
+ "default": False,
286
+ },
287
+ },
288
+ },
289
+ func=list_ready_tasks,
290
+ )
291
+
292
+ # --- list_blocked_tasks ---
293
+
294
+ def list_blocked_tasks(
295
+ parent_task_id: str | None = None,
296
+ limit: int = 20,
297
+ all_projects: bool = False,
298
+ ) -> dict[str, Any]:
299
+ """List tasks that are currently blocked, including what blocks them."""
300
+ # Filter by current project unless all_projects is True
301
+ project_id = None if all_projects else get_current_project_id()
302
+
303
+ # Resolve parent_task_id if it's a reference format
304
+ if parent_task_id:
305
+ try:
306
+ parent_task_id = resolve_task_id_for_mcp(task_manager, parent_task_id, project_id)
307
+ except (TaskNotFoundError, ValueError) as e:
308
+ return {"error": f"Invalid parent_task_id: {e}", "tasks": [], "count": 0}
309
+
310
+ blocked_tasks = task_manager.list_blocked_tasks(
311
+ parent_task_id=parent_task_id,
312
+ limit=limit,
313
+ project_id=project_id,
314
+ )
315
+ return {"tasks": [t.to_brief() for t in blocked_tasks], "count": len(blocked_tasks)}
316
+
317
+ registry.register(
318
+ name="list_blocked_tasks",
319
+ description="List tasks that are currently blocked by external dependencies (excludes parent tasks blocked by their own children).",
320
+ input_schema={
321
+ "type": "object",
322
+ "properties": {
323
+ "parent_task_id": {
324
+ "type": "string",
325
+ "description": "Filter by parent task (find blocked subtasks): #N, N (seq_num), path (1.2.3), or UUID",
326
+ "default": None,
327
+ },
328
+ "limit": {"type": "integer", "description": "Max results", "default": 20},
329
+ "all_projects": {
330
+ "type": "boolean",
331
+ "description": "If true, list tasks from all projects instead of just the current project",
332
+ "default": False,
333
+ },
334
+ },
335
+ },
336
+ func=list_blocked_tasks,
337
+ )
338
+
339
+ # --- suggest_next_task ---
340
+
341
+ def suggest_next_task(
342
+ task_type: str | None = None,
343
+ prefer_subtasks: bool = True,
344
+ parent_task_id: str | None = None,
345
+ session_id: str | None = None,
346
+ ) -> dict[str, Any]:
347
+ """
348
+ Suggest the best next task to work on.
349
+
350
+ Uses a scoring algorithm considering:
351
+ - Task is ready (no blockers)
352
+ - Priority (higher priority = higher score)
353
+ - Is a leaf task (subtask with no children)
354
+ - Has clear scope (complexity_score if available)
355
+ - Proximity to current in_progress task (same branch preferred)
356
+
357
+ Args:
358
+ task_type: Filter by task type (optional)
359
+ prefer_subtasks: Prefer leaf tasks over parent tasks (default: True)
360
+ parent_task_id: Filter to descendants of this task (optional).
361
+ When set, only tasks under this parent hierarchy are considered.
362
+ Use this to explicitly scope suggestions to a specific epic/feature.
363
+ session_id: Your session ID (required for MCP callers, from system context).
364
+ When provided and parent_task_id is not set, checks workflow state
365
+ for session_task variable and auto-scopes suggestions to that task's
366
+ hierarchy. Function signature is optional for TUI/internal callers.
367
+
368
+ Returns:
369
+ Suggested task with reasoning
370
+ """
371
+ # Filter by current project
372
+ project_id = get_current_project_id()
373
+
374
+ # Auto-scope to session_task if session_id is provided and parent_task_id is not set
375
+ if session_id and not parent_task_id:
376
+ workflow_state = workflow_state_manager.get_state(session_id)
377
+ if workflow_state:
378
+ session_task = workflow_state.variables.get("session_task")
379
+ if session_task and session_task != "*":
380
+ # session_task is set, use it as parent_task_id for scoping
381
+ parent_task_id = session_task
382
+
383
+ # Resolve parent_task_id if it's a reference format
384
+ if parent_task_id:
385
+ try:
386
+ parent_task_id = resolve_task_id_for_mcp(task_manager, parent_task_id, project_id)
387
+ except (TaskNotFoundError, ValueError) as e:
388
+ return {"error": f"Invalid parent_task_id: {e}", "suggestion": None}
389
+
390
+ # If parent_task_id is set, get all descendants of that task
391
+ if parent_task_id:
392
+ ready_tasks = _get_ready_descendants(
393
+ task_manager, parent_task_id, task_type, project_id
394
+ )
395
+ else:
396
+ ready_tasks = task_manager.list_ready_tasks(
397
+ task_type=task_type, limit=50, project_id=project_id
398
+ )
399
+
400
+ if not ready_tasks:
401
+ return {
402
+ "suggestion": None,
403
+ "reason": "No ready tasks found",
404
+ }
405
+
406
+ # Find current in_progress task for proximity scoring
407
+ in_progress_tasks = task_manager.list_tasks(
408
+ status="in_progress", limit=1, project_id=project_id
409
+ )
410
+ active_ancestry: list[str] = []
411
+ active_task_id: str | None = None
412
+ if in_progress_tasks:
413
+ active_task_id = in_progress_tasks[0].id
414
+ active_ancestry = _get_ancestry_chain(task_manager, active_task_id)
415
+
416
+ # Filter out in_progress tasks - we want to suggest the NEXT task, not current
417
+ ready_tasks = [t for t in ready_tasks if t.status != "in_progress"]
418
+ if not ready_tasks:
419
+ return {
420
+ "suggestion": None,
421
+ "reason": "No ready tasks found (all tasks are in_progress)",
422
+ }
423
+
424
+ # Score each task
425
+ scored = []
426
+ for task in ready_tasks:
427
+ score = 0
428
+ proximity_boost = 0
429
+
430
+ # Priority boost - use weight of 110 per level to ensure priority dominates
431
+ # This makes the gap between priority levels (110) larger than max possible
432
+ # bonuses (leaf=25 + complexity=15 + category=10 + proximity=50 = 100)
433
+ # Priority 0 (critical): 440, Priority 1 (high): 330, etc.
434
+ score += (4 - task.priority) * 110
435
+
436
+ # Check if it's a leaf task (no children)
437
+ children = task_manager.list_tasks(parent_task_id=task.id, status="open", limit=1)
438
+ is_leaf = len(children) == 0
439
+
440
+ if prefer_subtasks and is_leaf:
441
+ score += 25 # Prefer actionable leaf tasks
442
+
443
+ # Bonus for tasks with clear complexity
444
+ if task.complexity_score and task.complexity_score <= 5:
445
+ score += 15 # Prefer lower complexity tasks
446
+
447
+ # Bonus for tasks with category defined
448
+ if task.category:
449
+ score += 10
450
+
451
+ # Proximity boost based on ancestry relationship to in_progress task
452
+ if active_ancestry:
453
+ task_ancestry = _get_ancestry_chain(task_manager, task.id)
454
+ proximity_boost = _compute_proximity_boost(task_ancestry, active_ancestry)
455
+ score += proximity_boost
456
+
457
+ scored.append((task, score, is_leaf, proximity_boost))
458
+
459
+ # Sort by score descending
460
+ scored.sort(key=lambda x: x[1], reverse=True)
461
+ best_task, best_score, is_leaf, best_proximity = scored[0]
462
+
463
+ reasons = []
464
+ if best_task.priority == 0:
465
+ reasons.append("critical priority")
466
+ elif best_task.priority == 1:
467
+ reasons.append("high priority")
468
+ if is_leaf:
469
+ reasons.append("actionable leaf task")
470
+ if best_task.complexity_score and best_task.complexity_score <= 5:
471
+ reasons.append("manageable complexity")
472
+ if best_task.category:
473
+ reasons.append(f"has category ({best_task.category})")
474
+ if best_proximity > 0:
475
+ reasons.append("same branch as current work")
476
+
477
+ return {
478
+ "suggestion": best_task.to_brief(),
479
+ "score": best_score,
480
+ "reason": f"Selected because: {', '.join(reasons) if reasons else 'best available option'}",
481
+ "alternatives": [
482
+ {"ref": t.to_brief()["ref"], "title": t.title, "score": s}
483
+ for t, s, _, _ in scored[1:4] # Show top 3 alternatives
484
+ ],
485
+ }
486
+
487
+ registry.register(
488
+ name="suggest_next_task",
489
+ description="Suggest the best next task to work on based on priority, readiness, and complexity. "
490
+ "Requires session_id to check workflow's session_task variable for automatic scoping. "
491
+ "Use parent_task_id to explicitly scope suggestions to a specific epic/feature hierarchy.",
492
+ input_schema={
493
+ "type": "object",
494
+ "properties": {
495
+ "task_type": {
496
+ "type": "string",
497
+ "description": "Filter by task type (optional)",
498
+ "default": None,
499
+ },
500
+ "prefer_subtasks": {
501
+ "type": "boolean",
502
+ "description": "Prefer leaf tasks over parent tasks (default: True)",
503
+ "default": True,
504
+ },
505
+ "parent_task_id": {
506
+ "type": "string",
507
+ "description": "Filter to descendants of this task (#N, N, path, or UUID). "
508
+ "When set, only tasks under this parent hierarchy are considered. "
509
+ "Use this to scope suggestions to a specific epic/feature.",
510
+ "default": None,
511
+ },
512
+ "session_id": {
513
+ "type": "string",
514
+ "description": "Your session ID (from system context). Used to auto-scope suggestions based on workflow's session_task variable.",
515
+ },
516
+ },
517
+ "required": ["session_id"],
518
+ },
519
+ func=suggest_next_task,
520
+ )
521
+
522
+ return registry