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,591 @@
1
+ """
2
+ Task expansion MCP tools module.
3
+
4
+ Provides tools for expanding tasks into subtasks with automatic TDD sandwich pattern:
5
+ - expand_task: Expand task into subtasks via AI (auto-applies TDD sandwich)
6
+ - analyze_complexity: Analyze task complexity
7
+ """
8
+
9
+ import asyncio
10
+ import logging
11
+ from collections.abc import Callable
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from gobby.mcp_proxy.tools.internal import InternalToolRegistry
15
+ from gobby.storage.task_dependencies import TaskDependencyManager
16
+ from gobby.storage.tasks import LocalTaskManager, Task, TaskNotFoundError
17
+
18
+ # Import shared TDD utilities
19
+ from gobby.tasks.tdd import (
20
+ TDD_CATEGORIES,
21
+ TDD_PREFIXES,
22
+ TDD_SKIP_PATTERNS,
23
+ apply_tdd_sandwich,
24
+ build_expansion_context,
25
+ should_skip_expansion,
26
+ should_skip_tdd,
27
+ )
28
+
29
+ if TYPE_CHECKING:
30
+ from gobby.tasks.expansion import TaskExpander
31
+ from gobby.tasks.validation import TaskValidator
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # Re-export for backwards compatibility
36
+ __all__ = [
37
+ "create_expansion_registry",
38
+ "should_skip_tdd",
39
+ "TDD_PREFIXES",
40
+ "TDD_SKIP_PATTERNS",
41
+ "TDD_CATEGORIES",
42
+ ]
43
+
44
+
45
+ def create_expansion_registry(
46
+ task_manager: LocalTaskManager,
47
+ task_expander: "TaskExpander | None" = None,
48
+ task_validator: "TaskValidator | None" = None,
49
+ auto_generate_on_expand: bool = True,
50
+ resolve_tdd_mode: Callable[[str | None], bool] | None = None,
51
+ ) -> InternalToolRegistry:
52
+ """
53
+ Create a registry with task expansion tools.
54
+
55
+ Args:
56
+ task_manager: LocalTaskManager instance
57
+ task_expander: TaskExpander instance (optional, required for AI expansion)
58
+ task_validator: TaskValidator instance (optional, for auto-generating criteria)
59
+ auto_generate_on_expand: Whether to auto-generate validation criteria on expand
60
+ resolve_tdd_mode: Function to resolve TDD mode from session (optional)
61
+
62
+ Returns:
63
+ InternalToolRegistry with expansion tools registered
64
+ """
65
+ # Lazy import to avoid circular dependency
66
+ from gobby.mcp_proxy.tools.tasks import resolve_task_id_for_mcp
67
+
68
+ registry = InternalToolRegistry(
69
+ name="gobby-tasks-expansion",
70
+ description="Task expansion tools - AI and structured parsing",
71
+ )
72
+
73
+ # Create helper managers
74
+ dep_manager = TaskDependencyManager(task_manager.db)
75
+
76
+ async def _apply_tdd_sandwich_wrapper(
77
+ parent_task_id: str,
78
+ impl_task_ids: list[str],
79
+ refactor_task_ids: list[str] | None = None,
80
+ ) -> dict[str, Any]:
81
+ """Wrapper to call shared apply_tdd_sandwich with local managers."""
82
+ return await apply_tdd_sandwich(
83
+ task_manager, dep_manager, parent_task_id, impl_task_ids, refactor_task_ids
84
+ )
85
+
86
+ def _find_unexpanded_epic(root_task_id: str) -> Task | None:
87
+ """Depth-first search for first unexpanded epic in the task tree.
88
+
89
+ Traverses the task tree starting from root_task_id to find the first
90
+ epic that hasn't been expanded yet (is_expanded=False).
91
+
92
+ Args:
93
+ root_task_id: Task ID to start search from
94
+
95
+ Returns:
96
+ First unexpanded epic Task, or None if all epics are expanded
97
+ """
98
+ task = task_manager.get_task(root_task_id)
99
+ if not task:
100
+ return None
101
+
102
+ # Check if this task itself is an unexpanded epic
103
+ if task.task_type == "epic" and not task.is_expanded:
104
+ return task
105
+
106
+ # Search children depth-first
107
+ children = task_manager.list_tasks(parent_task_id=root_task_id, limit=1000)
108
+ for child in children:
109
+ if child.task_type == "epic":
110
+ result = _find_unexpanded_epic(child.id)
111
+ if result:
112
+ return result
113
+
114
+ return None
115
+
116
+ def _count_unexpanded_epics(root_task_id: str) -> int:
117
+ """Count unexpanded epics in the task tree.
118
+
119
+ Args:
120
+ root_task_id: Task ID to start counting from
121
+
122
+ Returns:
123
+ Number of unexpanded epics in the tree
124
+ """
125
+ count = 0
126
+ task = task_manager.get_task(root_task_id)
127
+ if not task:
128
+ return 0
129
+
130
+ # Count this task if it's an unexpanded epic
131
+ if task.task_type == "epic" and not task.is_expanded:
132
+ count += 1
133
+
134
+ # Count children recursively
135
+ children = task_manager.list_tasks(parent_task_id=root_task_id, limit=1000)
136
+ for child in children:
137
+ count += _count_unexpanded_epics(child.id)
138
+
139
+ return count
140
+
141
+ async def _expand_single_task(
142
+ single_task_id: str,
143
+ context: str | None,
144
+ enable_web_research: bool,
145
+ enable_code_context: bool,
146
+ should_generate_validation: bool,
147
+ skip_tdd: bool = False,
148
+ force: bool = False,
149
+ session_id: str | None = None,
150
+ iterative: bool = False,
151
+ ) -> dict[str, Any]:
152
+ """Internal helper to expand a single task.
153
+
154
+ When iterative=True, supports iterative expansion of epic trees:
155
+ - If the root task is already expanded, finds the next unexpanded epic
156
+ - Returns progress info (unexpanded_epics count, complete flag)
157
+ - Call repeatedly until complete=True
158
+ """
159
+ # Resolve task reference
160
+ try:
161
+ resolved_task_id = resolve_task_id_for_mcp(task_manager, single_task_id)
162
+ except (TaskNotFoundError, ValueError) as e:
163
+ return {"error": f"Invalid task_id: {e}", "task_id": single_task_id}
164
+
165
+ if not task_expander:
166
+ return {"error": "Task expansion is not enabled", "task_id": single_task_id}
167
+
168
+ root_task = task_manager.get_task(resolved_task_id)
169
+ if not root_task:
170
+ return {"error": f"Task not found: {single_task_id}", "task_id": single_task_id}
171
+
172
+ # Auto-enable iterative mode for epics (timeout-safe cascade)
173
+ # Each call expands one epic and returns - caller loops until complete=True
174
+ if root_task.task_type == "epic" and not iterative:
175
+ iterative = True
176
+
177
+ # Iterative mode: find next unexpanded epic in tree
178
+ if iterative:
179
+ target_task = _find_unexpanded_epic(resolved_task_id)
180
+ if target_task is None:
181
+ # All epics expanded - tree is complete
182
+ return {
183
+ "complete": True,
184
+ "task_id": resolved_task_id,
185
+ "root_ref": f"#{root_task.seq_num}" if root_task.seq_num else root_task.id[:8],
186
+ "unexpanded_epics": 0,
187
+ }
188
+
189
+ # Re-fetch to get latest state and verify task should be expanded
190
+ target_task = task_manager.get_task(target_task.id)
191
+ if target_task is None:
192
+ return {"error": "Task deleted during expansion", "task_id": resolved_task_id}
193
+
194
+ # Check if task should be skipped (TDD prefixes or already expanded)
195
+ skip, reason = should_skip_expansion(target_task.title, target_task.is_expanded, force)
196
+ if skip:
197
+ logger.info(f"Skipping task {target_task.id[:8]}: {reason}")
198
+ return {
199
+ "skipped": True,
200
+ "reason": reason,
201
+ "task_id": target_task.id,
202
+ "unexpanded_epics": _count_unexpanded_epics(resolved_task_id),
203
+ }
204
+
205
+ task = target_task
206
+ else:
207
+ task = root_task
208
+
209
+ # Non-iterative mode: Check if task should be skipped
210
+ skip, reason = should_skip_expansion(task.title, task.is_expanded, force)
211
+ if skip:
212
+ if reason == "already expanded":
213
+ return {
214
+ "error": "Task already expanded (is_expanded=True). Use force=True to re-expand, or use iterative mode to expand child epics.",
215
+ "task_id": task.id,
216
+ "is_expanded": True,
217
+ }
218
+ else:
219
+ return {
220
+ "error": f"Cannot expand task: {reason}",
221
+ "task_id": task.id,
222
+ "skipped": True,
223
+ }
224
+
225
+ # Check if task is a leaf (no children) - only in non-iterative mode
226
+ existing_children = task_manager.list_tasks(parent_task_id=task.id, limit=1)
227
+ if existing_children:
228
+ return {
229
+ "error": "Task already has children. Only leaf tasks can be expanded. Use iterative mode or CLI with --cascade for parent tasks.",
230
+ "task_id": task.id,
231
+ "existing_children": len(existing_children),
232
+ }
233
+
234
+ # Build context from any stored expansion_context + user context
235
+ merged_context = build_expansion_context(task.expansion_context, context)
236
+
237
+ # Note: TDD transformation is applied separately via apply_tdd command
238
+ result = await task_expander.expand_task(
239
+ task_id=task.id,
240
+ title=task.title,
241
+ description=task.description,
242
+ context=merged_context,
243
+ enable_web_research=enable_web_research,
244
+ enable_code_context=enable_code_context,
245
+ session_id=session_id,
246
+ )
247
+
248
+ # Handle errors
249
+ if "error" in result:
250
+ return {"error": result["error"], "task_id": task.id}
251
+
252
+ # Extract subtask IDs (already created by agent via create_task tool calls)
253
+ subtask_ids = result.get("subtask_ids", [])
254
+
255
+ # Wire parent → subtask dependencies
256
+ for subtask_id in subtask_ids:
257
+ try:
258
+ dep_manager.add_dependency(
259
+ task_id=task.id, depends_on=subtask_id, dep_type="blocks"
260
+ )
261
+ except ValueError:
262
+ pass
263
+
264
+ # Fetch created subtasks for the response (include seq_num for ergonomics)
265
+ created_subtasks = []
266
+ for sid in subtask_ids:
267
+ subtask = task_manager.get_task(sid)
268
+ if subtask:
269
+ subtask_info: dict[str, Any] = {
270
+ "ref": f"#{subtask.seq_num}" if subtask.seq_num else subtask.id[:8],
271
+ "title": subtask.title,
272
+ "seq_num": subtask.seq_num,
273
+ "id": subtask.id,
274
+ }
275
+ created_subtasks.append(subtask_info)
276
+
277
+ # Auto-generate validation criteria for each subtask (when enabled)
278
+ validation_generated = 0
279
+ validation_skipped_reason = None
280
+ if should_generate_validation and subtask_ids:
281
+ if not task_validator:
282
+ validation_skipped_reason = "task_validator not configured"
283
+ else:
284
+ for sid in subtask_ids:
285
+ subtask = task_manager.get_task(sid)
286
+ if subtask and not subtask.validation_criteria and subtask.task_type != "epic":
287
+ try:
288
+ criteria = await task_validator.generate_criteria(
289
+ title=subtask.title,
290
+ description=subtask.description,
291
+ )
292
+ if criteria:
293
+ task_manager.update_task(sid, validation_criteria=criteria)
294
+ validation_generated += 1
295
+ except Exception:
296
+ pass # nosec B110 - best-effort validation generation
297
+
298
+ # Update parent task: set is_expanded and validation criteria
299
+ task_manager.update_task(
300
+ task.id,
301
+ is_expanded=True,
302
+ validation_criteria="All child tasks must be completed (status: closed).",
303
+ )
304
+
305
+ # Auto-apply TDD sandwich pattern (unless skip_tdd=True or expanding an epic)
306
+ # Creates ONE [TDD] task, renames impl tasks with [IMPL], creates ONE [REF] task
307
+ # IMPORTANT: Skip TDD when expanding epics - TDD is applied at feature task level
308
+ tdd_applied = False
309
+ tdd_categories = ("code", "config")
310
+ should_apply_tdd = not skip_tdd and subtask_ids and task.task_type != "epic"
311
+ logger.info(
312
+ f"TDD check: task={task.id[:8]} type={task.task_type} "
313
+ f"skip_tdd={skip_tdd} subtask_count={len(subtask_ids)} "
314
+ f"should_apply={should_apply_tdd}"
315
+ )
316
+ if should_apply_tdd:
317
+ # Collect code/config subtasks that should be wrapped in TDD sandwich
318
+ impl_task_ids = []
319
+ refactor_task_ids = []
320
+ for sid in subtask_ids:
321
+ subtask = task_manager.get_task(sid)
322
+ if not subtask:
323
+ continue
324
+ # Include code/config categories that aren't already TDD-formatted
325
+ if subtask.category in tdd_categories:
326
+ if not should_skip_tdd(subtask.title):
327
+ impl_task_ids.append(sid)
328
+ logger.debug(
329
+ f" TDD candidate: {subtask.id[:8]} category={subtask.category}"
330
+ )
331
+ else:
332
+ logger.debug(
333
+ f" TDD skip (pattern): {subtask.id[:8]} title={subtask.title[:40]}"
334
+ )
335
+ elif subtask.category == "refactor":
336
+ # Refactor-category tasks get [REF] prefix and wire into dependency chain
337
+ if not should_skip_tdd(subtask.title):
338
+ refactor_task_ids.append(sid)
339
+ logger.debug(
340
+ f" TDD refactor candidate: {subtask.id[:8]} category={subtask.category}"
341
+ )
342
+ else:
343
+ logger.debug(
344
+ f" TDD skip (pattern): {subtask.id[:8]} title={subtask.title[:40]}"
345
+ )
346
+ else:
347
+ logger.debug(
348
+ f" TDD skip (category): {subtask.id[:8]} category={subtask.category}"
349
+ )
350
+
351
+ # Apply TDD sandwich at parent level (wraps all impl tasks)
352
+ if impl_task_ids:
353
+ logger.info(
354
+ f"Applying TDD sandwich: {len(impl_task_ids)} code/config subtasks, "
355
+ f"{len(refactor_task_ids)} refactor subtasks"
356
+ )
357
+ tdd_result = await _apply_tdd_sandwich_wrapper(
358
+ resolved_task_id, impl_task_ids, refactor_task_ids or None
359
+ )
360
+ if tdd_result.get("success", False):
361
+ tdd_applied = True
362
+ logger.info(f"TDD sandwich applied successfully to {task.id[:8]}")
363
+ else:
364
+ logger.warning(f"TDD sandwich failed: {tdd_result}")
365
+ else:
366
+ logger.info(f"No code/config subtasks for TDD sandwich in {task.id[:8]}")
367
+
368
+ # Build response
369
+ response: dict[str, Any] = {
370
+ "task_id": task.id,
371
+ "tasks_created": len(subtask_ids),
372
+ "subtasks": created_subtasks,
373
+ "is_expanded": True,
374
+ }
375
+ # Include seq_num refs for ergonomics
376
+ if task.seq_num is not None:
377
+ response["expanded_ref"] = f"#{task.seq_num}"
378
+ # Keep legacy field for compatibility
379
+ response["parent_seq_num"] = task.seq_num
380
+ response["parent_ref"] = f"#{task.seq_num}"
381
+
382
+ # Iterative mode: include progress info
383
+ if iterative:
384
+ remaining = _count_unexpanded_epics(resolved_task_id)
385
+ response["unexpanded_epics"] = remaining
386
+ response["complete"] = remaining == 0
387
+ # Include root task ref for context
388
+ response["root_ref"] = (
389
+ f"#{root_task.seq_num}" if root_task.seq_num else root_task.id[:8]
390
+ )
391
+
392
+ if validation_generated > 0:
393
+ response["validation_criteria_generated"] = validation_generated
394
+ if validation_skipped_reason:
395
+ response["validation_skipped_reason"] = validation_skipped_reason
396
+ if tdd_applied:
397
+ response["tdd_applied"] = True
398
+ return response
399
+
400
+ @registry.tool(
401
+ name="expand_task",
402
+ description="Expand a task into smaller subtasks using AI. Supports iterative expansion of epic trees.",
403
+ )
404
+ async def expand_task(
405
+ task_id: str | None = None,
406
+ task_ids: list[str] | None = None,
407
+ context: str | None = None,
408
+ enable_web_research: bool = False,
409
+ enable_code_context: bool = True,
410
+ generate_validation: bool | None = None,
411
+ skip_tdd: bool = False,
412
+ force: bool = False,
413
+ session_id: str | None = None,
414
+ iterative: bool = False,
415
+ ) -> dict[str, Any]:
416
+ """
417
+ Expand a task into subtasks using tool-based expansion.
418
+
419
+ The expansion agent calls create_task MCP tool directly to create subtasks,
420
+ wiring dependencies via the 'blocks' parameter.
421
+
422
+ ## Iterative Mode (iterative=True)
423
+
424
+ For epic trees, call repeatedly on the root epic until complete=True:
425
+
426
+ ```python
427
+ while True:
428
+ result = expand_task(task_id="#100", iterative=True)
429
+ if result.get("complete"):
430
+ break
431
+ print(f"Expanded {result['expanded_ref']}, {result['unexpanded_epics']} remaining")
432
+ ```
433
+
434
+ Returns:
435
+ - expanded_ref: The task that was expanded (may differ from input)
436
+ - unexpanded_epics: Count of remaining unexpanded epics
437
+ - complete: True when all epics in tree are expanded
438
+
439
+ Args:
440
+ task_id: ID of single task to expand (mutually exclusive with task_ids)
441
+ task_ids: List of task IDs for batch parallel expansion
442
+ context: Additional context for expansion
443
+ enable_web_research: Whether to enable web research (default: False)
444
+ enable_code_context: Whether to enable code context gathering (default: True)
445
+ generate_validation: Whether to auto-generate validation_criteria for subtasks.
446
+ Defaults to config setting (gobby_tasks.validation.auto_generate_on_expand).
447
+ skip_tdd: Skip automatic TDD transformation for code/config subtasks
448
+ force: Re-expand even if is_expanded=True
449
+ session_id: Session ID for TDD mode resolution (optional)
450
+ iterative: Enable iterative expansion mode for epic trees. When True, finds
451
+ the next unexpanded epic in the tree and expands it. Call repeatedly
452
+ until complete=True. (default: False)
453
+
454
+ Returns:
455
+ Dictionary with expansion results. In iterative mode, includes:
456
+ - expanded_ref: Task that was expanded
457
+ - unexpanded_epics: Remaining unexpanded epics
458
+ - complete: True when tree is fully expanded
459
+ """
460
+ # Use config default if not specified
461
+ should_generate_validation = (
462
+ generate_validation if generate_validation is not None else auto_generate_on_expand
463
+ )
464
+
465
+ # Validate parameters
466
+ if task_id and task_ids:
467
+ return {
468
+ "error": "task_id and task_ids are mutually exclusive. Provide one or the other."
469
+ }
470
+ if not task_id and not task_ids:
471
+ return {"error": "Either task_id or task_ids must be provided"}
472
+ if task_ids is not None and len(task_ids) == 0:
473
+ return {"error": "task_ids list cannot be empty"}
474
+
475
+ # Single task mode
476
+ if task_id:
477
+ return await _expand_single_task(
478
+ single_task_id=task_id,
479
+ context=context,
480
+ enable_web_research=enable_web_research,
481
+ enable_code_context=enable_code_context,
482
+ should_generate_validation=should_generate_validation,
483
+ skip_tdd=skip_tdd,
484
+ force=force,
485
+ session_id=session_id,
486
+ iterative=iterative,
487
+ )
488
+
489
+ # Batch mode - process tasks in parallel
490
+ # At this point, task_ids is guaranteed to be a non-empty list (validated above)
491
+ assert task_ids is not None # nosec B101 - Type narrowing for mypy
492
+
493
+ async def expand_one(tid: str) -> dict[str, Any]:
494
+ return await _expand_single_task(
495
+ single_task_id=tid,
496
+ context=context,
497
+ enable_web_research=enable_web_research,
498
+ enable_code_context=enable_code_context,
499
+ should_generate_validation=should_generate_validation,
500
+ skip_tdd=skip_tdd,
501
+ force=force,
502
+ session_id=session_id,
503
+ iterative=iterative,
504
+ )
505
+
506
+ raw_results = await asyncio.gather(
507
+ *[expand_one(tid) for tid in task_ids], return_exceptions=True
508
+ )
509
+ # Convert exceptions to error dicts to preserve per-task success/error information
510
+ processed_results: list[dict[str, Any]] = []
511
+ for i, result in enumerate(raw_results):
512
+ if isinstance(result, BaseException):
513
+ processed_results.append(
514
+ {"error": str(result), "task_id": task_ids[i], "success": False}
515
+ )
516
+ else:
517
+ processed_results.append(result)
518
+ return {"results": processed_results}
519
+
520
+ @registry.tool(
521
+ name="analyze_complexity",
522
+ description="Analyze task complexity based on existing subtasks or description.",
523
+ )
524
+ async def analyze_complexity(task_id: str) -> dict[str, Any]:
525
+ """
526
+ Analyze task complexity.
527
+
528
+ With tool-based expansion, this now analyzes existing subtasks if present,
529
+ or estimates complexity from description length. For detailed breakdown,
530
+ use expand_task which creates subtasks directly.
531
+
532
+ Args:
533
+ task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
534
+
535
+ Returns:
536
+ Complexity analysis with score and reasoning
537
+ """
538
+ # Resolve task reference
539
+ try:
540
+ resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
541
+ except (TaskNotFoundError, ValueError) as e:
542
+ return {"error": f"Invalid task_id: {e}"}
543
+
544
+ task = task_manager.get_task(resolved_task_id)
545
+ if not task:
546
+ raise ValueError(f"Task not found: {task_id}")
547
+
548
+ # Check for existing subtasks
549
+ subtasks = task_manager.list_tasks(parent_task_id=task.id, limit=100)
550
+ subtask_count = len(subtasks)
551
+
552
+ # Simple heuristic-based complexity
553
+ if subtask_count > 0:
554
+ # Complexity based on subtask count
555
+ score = min(10, 1 + subtask_count // 2)
556
+ reasoning = f"Task has {subtask_count} subtasks"
557
+ recommended = subtask_count
558
+ else:
559
+ # Estimate from description length
560
+ desc_len = len(task.description or "")
561
+ if desc_len < 100:
562
+ score = 2
563
+ reasoning = "Short description, likely simple task"
564
+ recommended = 2
565
+ elif desc_len < 500:
566
+ score = 5
567
+ reasoning = "Medium description, moderate complexity"
568
+ recommended = 5
569
+ else:
570
+ score = 8
571
+ reasoning = "Long description, likely complex task"
572
+ recommended = 10
573
+
574
+ # Update task with complexity score
575
+ task_manager.update_task(
576
+ task.id,
577
+ complexity_score=score,
578
+ estimated_subtasks=recommended,
579
+ )
580
+
581
+ return {
582
+ "task_id": task.id,
583
+ "title": task.title,
584
+ "complexity_score": score,
585
+ "reasoning": reasoning,
586
+ "recommended_subtasks": recommended,
587
+ "existing_subtasks": subtask_count,
588
+ "note": "For detailed breakdown, use expand_task to create subtasks",
589
+ }
590
+
591
+ return registry