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,843 @@
1
+ """
2
+ Validation MCP tools for Gobby Task System.
3
+
4
+ Extracted from tasks.py using Strangler Fig pattern.
5
+
6
+ Exposes functionality for:
7
+ - Task validation (validate_task, generate_validation_criteria)
8
+ - Validation status (get_validation_status, reset_validation_count)
9
+ - Validation history (get_validation_history, get_recurring_issues, clear_validation_history)
10
+ - De-escalation (de_escalate_task)
11
+ - QA loop (validate_and_fix, run_fix_attempt)
12
+
13
+ These tools are registered with the InternalToolRegistry and accessed
14
+ via the downstream proxy pattern (call_tool, list_tools, get_tool_schema).
15
+ """
16
+
17
+ import logging
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ from gobby.mcp_proxy.tools.internal import InternalToolRegistry
21
+ from gobby.storage.tasks import LocalTaskManager, TaskNotFoundError
22
+ from gobby.tasks.validation import TaskValidator
23
+ from gobby.tasks.validation_history import ValidationHistoryManager
24
+
25
+ if TYPE_CHECKING:
26
+ from gobby.agents.runner import AgentRunner
27
+ from gobby.storage.projects import LocalProjectManager
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ def create_validation_registry(
33
+ task_manager: LocalTaskManager,
34
+ task_validator: TaskValidator | None = None,
35
+ project_manager: "LocalProjectManager | None" = None,
36
+ get_project_repo_path: Any = None,
37
+ agent_runner: "AgentRunner | None" = None,
38
+ ) -> InternalToolRegistry:
39
+ """
40
+ Create a validation tool registry with all validation-related tools.
41
+
42
+ Args:
43
+ task_manager: LocalTaskManager instance
44
+ task_validator: TaskValidator instance (optional, enables LLM validation)
45
+ project_manager: LocalProjectManager instance (optional)
46
+ get_project_repo_path: Callable to get repo path from project ID (optional)
47
+ agent_runner: AgentRunner instance (optional, enables fix agent spawning)
48
+
49
+ Returns:
50
+ InternalToolRegistry with all validation tools registered
51
+ """
52
+ # Lazy import to avoid circular dependency
53
+ from gobby.mcp_proxy.tools.tasks import resolve_task_id_for_mcp
54
+
55
+ registry = InternalToolRegistry(
56
+ name="gobby-tasks-validation",
57
+ description="Task validation tools - validate, criteria, history",
58
+ )
59
+
60
+ # Create helper managers
61
+ validation_history_manager = ValidationHistoryManager(task_manager.db)
62
+
63
+ @registry.tool(
64
+ name="validate_task",
65
+ description="Validate if a task is completed. Auto-gathers context from recent commits and relevant files if changes_summary not provided.",
66
+ )
67
+ async def validate_task(
68
+ task_id: str,
69
+ changes_summary: str | None = None,
70
+ context_files: list[str] | None = None,
71
+ ) -> dict[str, Any]:
72
+ """
73
+ Validate task completion.
74
+
75
+ For parent tasks (tasks with children), validation checks if all children are closed.
76
+ For leaf tasks, uses LLM-based validation against criteria.
77
+
78
+ If changes_summary is not provided for leaf tasks, uses smart context gathering:
79
+ 1. Current uncommitted changes (staged + unstaged)
80
+ 2. Multi-commit window (last 10 commits)
81
+ 3. File-based analysis (reads files mentioned in criteria)
82
+
83
+ Args:
84
+ task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
85
+ changes_summary: Summary of changes made (optional - auto-gathered if not provided)
86
+ context_files: List of file paths to read for context (optional)
87
+
88
+ Returns:
89
+ Validation result
90
+ """
91
+ # Resolve task reference
92
+ try:
93
+ resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
94
+ except (TaskNotFoundError, ValueError) as e:
95
+ return {"error": f"Invalid task_id: {e}"}
96
+
97
+ task = task_manager.get_task(resolved_task_id)
98
+ if not task:
99
+ return {"error": f"Task not found: {task_id}"}
100
+
101
+ # Check if task has children (is a parent task)
102
+ children = task_manager.list_tasks(parent_task_id=task.id, limit=1000)
103
+
104
+ if children:
105
+ # Parent task: validate based on child completion
106
+ open_children = [c for c in children if c.status != "closed"]
107
+ all_closed = len(open_children) == 0
108
+
109
+ from gobby.tasks.validation import ValidationResult
110
+
111
+ if all_closed:
112
+ result = ValidationResult(
113
+ status="valid",
114
+ feedback=f"All {len(children)} child tasks are completed.",
115
+ )
116
+ else:
117
+ open_titles = [f"- {c.id}: {c.title}" for c in open_children[:5]]
118
+ remaining = len(open_children) - 5 if len(open_children) > 5 else 0
119
+ feedback = f"{len(open_children)} of {len(children)} child tasks still open:\n"
120
+ feedback += "\n".join(open_titles)
121
+ if remaining > 0:
122
+ feedback += f"\n... and {remaining} more"
123
+ result = ValidationResult(status="invalid", feedback=feedback)
124
+ else:
125
+ # Leaf task: use LLM-based validation
126
+ if not task_validator:
127
+ raise RuntimeError("Task validation is not enabled")
128
+
129
+ # Use provided changes_summary or auto-gather via smart context
130
+ validation_context = changes_summary
131
+ if not validation_context:
132
+ from gobby.tasks.validation import get_validation_context_smart
133
+
134
+ # Get project repo_path for git commands
135
+ repo_path = None
136
+ if get_project_repo_path and task.project_id:
137
+ repo_path = get_project_repo_path(task.project_id)
138
+
139
+ smart_context = get_validation_context_smart(
140
+ task_title=task.title,
141
+ validation_criteria=task.validation_criteria,
142
+ task_description=task.description,
143
+ cwd=repo_path,
144
+ )
145
+ if smart_context:
146
+ validation_context = f"Validation context:\n\n{smart_context}"
147
+
148
+ if not validation_context:
149
+ raise ValueError(
150
+ "No changes found for validation. Either provide changes_summary "
151
+ "or ensure there are uncommitted changes or recent commits."
152
+ )
153
+
154
+ result = await task_validator.validate_task(
155
+ task_id=task.id,
156
+ title=task.title,
157
+ description=task.description,
158
+ changes_summary=validation_context,
159
+ validation_criteria=task.validation_criteria,
160
+ context_files=context_files,
161
+ category=task.category,
162
+ )
163
+
164
+ # Record validation iteration to history
165
+ # Calculate iteration number based on fail count (current fail count + 1 for this attempt)
166
+ current_fail_count = task.validation_fail_count or 0
167
+ iteration_number = current_fail_count + 1
168
+
169
+ # Determine validator type and context type
170
+ validator_type = "parent_completion" if children else "llm"
171
+ context_type = "child_status" if children else "smart_context"
172
+ context_summary = (
173
+ f"{len(children)} children checked" if children else "Auto-gathered from git/files"
174
+ )
175
+
176
+ validation_history_manager.record_iteration(
177
+ task_id=task.id,
178
+ iteration=iteration_number,
179
+ status=result.status,
180
+ feedback=result.feedback,
181
+ issues=None, # ValidationResult from validation.py doesn't have issues
182
+ context_type=context_type,
183
+ context_summary=context_summary,
184
+ validator_type=validator_type,
185
+ )
186
+
187
+ # Update validation status
188
+ updates: dict[str, Any] = {
189
+ "validation_status": result.status,
190
+ "validation_feedback": result.feedback,
191
+ }
192
+
193
+ MAX_RETRIES = 3
194
+
195
+ if result.status == "valid":
196
+ # Success: Close task
197
+ task_manager.close_task(task.id, reason="Completed via validation")
198
+ elif result.status == "invalid":
199
+ # Failure: Increment fail count
200
+ current_fail_count = task.validation_fail_count or 0
201
+ new_fail_count = current_fail_count + 1
202
+ updates["validation_fail_count"] = new_fail_count
203
+
204
+ feedback_str = result.feedback or "Validation failed (no feedback provided)."
205
+
206
+ if new_fail_count < MAX_RETRIES:
207
+ # Create subtask to fix issues
208
+ fix_task = task_manager.create_task(
209
+ project_id=task.project_id,
210
+ title=f"Fix validation failures for {task.title}",
211
+ description=f"Validation failed with feedback:\n{feedback_str}\n\nPlease fix the issues and re-validate.",
212
+ parent_task_id=task.id,
213
+ priority=1, # High priority fix
214
+ task_type="bug",
215
+ )
216
+ updates["validation_feedback"] = (
217
+ feedback_str + f"\n\nCreated fix task: {fix_task.id}"
218
+ )
219
+ else:
220
+ # Exceeded retries: Mark as failed
221
+ updates["status"] = "failed"
222
+ updates["validation_feedback"] = (
223
+ feedback_str + f"\n\nExceeded max retries ({MAX_RETRIES}). Marked as failed."
224
+ )
225
+
226
+ task_manager.update_task(task.id, **updates)
227
+
228
+ return {
229
+ "is_valid": result.status == "valid",
230
+ "feedback": result.feedback,
231
+ "status": result.status,
232
+ "fail_count": updates.get("validation_fail_count", task.validation_fail_count),
233
+ }
234
+
235
+ @registry.tool(
236
+ name="get_validation_status",
237
+ description="Get validation details for a task.",
238
+ )
239
+ def get_validation_status(task_id: str) -> dict[str, Any]:
240
+ """
241
+ Get validation details.
242
+
243
+ Args:
244
+ task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
245
+
246
+ Returns:
247
+ Validation details
248
+ """
249
+ # Resolve task reference
250
+ try:
251
+ resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
252
+ except (TaskNotFoundError, ValueError) as e:
253
+ return {"error": f"Invalid task_id: {e}"}
254
+
255
+ task = task_manager.get_task(resolved_task_id)
256
+ if not task:
257
+ raise ValueError(f"Task not found: {task_id}")
258
+
259
+ return {
260
+ "task_id": task.id,
261
+ "validation_status": task.validation_status,
262
+ "validation_feedback": task.validation_feedback,
263
+ "validation_criteria": task.validation_criteria,
264
+ "validation_fail_count": task.validation_fail_count,
265
+ "use_external_validator": task.use_external_validator,
266
+ }
267
+
268
+ @registry.tool(
269
+ name="reset_validation_count",
270
+ description="Reset validation failure count for a task.",
271
+ )
272
+ def reset_validation_count(task_id: str) -> dict[str, Any]:
273
+ """
274
+ Reset validation failure count.
275
+
276
+ Args:
277
+ task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
278
+
279
+ Returns:
280
+ Updated task details
281
+ """
282
+ # Resolve task reference
283
+ try:
284
+ resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
285
+ except (TaskNotFoundError, ValueError) as e:
286
+ return {"error": f"Invalid task_id: {e}"}
287
+
288
+ task = task_manager.get_task(resolved_task_id)
289
+ if not task:
290
+ raise ValueError(f"Task not found: {task_id}")
291
+
292
+ updated_task = task_manager.update_task(task.id, validation_fail_count=0)
293
+ return {
294
+ "task_id": updated_task.id,
295
+ "validation_fail_count": updated_task.validation_fail_count,
296
+ "message": "Validation failure count reset to 0",
297
+ }
298
+
299
+ @registry.tool(
300
+ name="get_validation_history",
301
+ description="Get full validation history for a task, including all iterations, feedback, and issues.",
302
+ )
303
+ def get_validation_history(task_id: str) -> dict[str, Any]:
304
+ """
305
+ Get validation history for a task.
306
+
307
+ Returns all validation iterations with their status, feedback, and issues.
308
+
309
+ Args:
310
+ task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
311
+
312
+ Returns:
313
+ Validation history with all iterations
314
+ """
315
+ # Resolve task reference
316
+ try:
317
+ resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
318
+ except (TaskNotFoundError, ValueError) as e:
319
+ return {"error": f"Invalid task_id: {e}"}
320
+
321
+ task = task_manager.get_task(resolved_task_id)
322
+ if not task:
323
+ raise ValueError(f"Task {task_id} not found")
324
+
325
+ history = validation_history_manager.get_iteration_history(task.id)
326
+
327
+ # Convert iterations to serializable format
328
+ history_dicts = []
329
+ for iteration in history:
330
+ iter_dict: dict[str, Any] = {
331
+ "iteration": iteration.iteration,
332
+ "status": iteration.status,
333
+ "feedback": iteration.feedback,
334
+ "issues": [i.to_dict() for i in (iteration.issues or [])],
335
+ "context_type": iteration.context_type,
336
+ "context_summary": iteration.context_summary,
337
+ "validator_type": iteration.validator_type,
338
+ "created_at": iteration.created_at,
339
+ }
340
+ history_dicts.append(iter_dict)
341
+
342
+ return {
343
+ "task_id": task_id,
344
+ "history": history_dicts,
345
+ "total_iterations": len(history_dicts),
346
+ }
347
+
348
+ @registry.tool(
349
+ name="get_recurring_issues",
350
+ description="Analyze validation history for recurring issues that keep appearing across iterations.",
351
+ )
352
+ def get_recurring_issues(
353
+ task_id: str,
354
+ threshold: int = 2,
355
+ ) -> dict[str, Any]:
356
+ """
357
+ Get recurring issues analysis for a task.
358
+
359
+ Finds issues that appear multiple times across validation iterations.
360
+
361
+ Args:
362
+ task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
363
+ threshold: Minimum occurrences to consider an issue recurring (default: 2)
364
+
365
+ Returns:
366
+ Recurring issues analysis with grouped issues and counts
367
+ """
368
+ # Resolve task reference
369
+ try:
370
+ resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
371
+ except (TaskNotFoundError, ValueError) as e:
372
+ return {"error": f"Invalid task_id: {e}"}
373
+
374
+ task = task_manager.get_task(resolved_task_id)
375
+ if not task:
376
+ return {"error": f"Task {task_id} not found"}
377
+
378
+ summary = validation_history_manager.get_recurring_issue_summary(
379
+ task.id, threshold=threshold
380
+ )
381
+
382
+ has_recurring = validation_history_manager.has_recurring_issues(
383
+ task.id, threshold=threshold
384
+ )
385
+
386
+ return {
387
+ "task_id": task.id,
388
+ "recurring_issues": summary["recurring_issues"],
389
+ "total_iterations": summary["total_iterations"],
390
+ "has_recurring": has_recurring,
391
+ }
392
+
393
+ @registry.tool(
394
+ name="clear_validation_history",
395
+ description="Clear all validation history for a task. Use after major changes that invalidate previous feedback.",
396
+ )
397
+ def clear_validation_history(
398
+ task_id: str,
399
+ reason: str | None = None,
400
+ ) -> dict[str, Any]:
401
+ """
402
+ Clear validation history for a fresh start.
403
+
404
+ Removes all validation iterations and resets the fail count.
405
+
406
+ Args:
407
+ task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
408
+ reason: Optional reason for clearing history
409
+
410
+ Returns:
411
+ Confirmation of cleared history
412
+ """
413
+ # Resolve task reference
414
+ try:
415
+ resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
416
+ except (TaskNotFoundError, ValueError) as e:
417
+ return {"error": f"Invalid task_id: {e}"}
418
+
419
+ task = task_manager.get_task(resolved_task_id)
420
+ if not task:
421
+ return {"error": f"Task {task_id} not found"}
422
+
423
+ # Get count before clearing for response
424
+ history = validation_history_manager.get_iteration_history(task.id)
425
+ iterations_count = len(history)
426
+
427
+ # Clear history
428
+ validation_history_manager.clear_history(task.id)
429
+
430
+ # Also reset validation fail count
431
+ task_manager.update_task(task.id, validation_fail_count=0)
432
+
433
+ return {
434
+ "task_id": task.id,
435
+ "cleared": True,
436
+ "iterations_cleared": iterations_count,
437
+ "reason": reason,
438
+ }
439
+
440
+ @registry.tool(
441
+ name="de_escalate_task",
442
+ description="Return an escalated task to open status after human intervention resolves the issue.",
443
+ )
444
+ def de_escalate_task(
445
+ task_id: str,
446
+ reason: str,
447
+ reset_validation: bool = False,
448
+ ) -> dict[str, Any]:
449
+ """
450
+ De-escalate a task back to open status.
451
+
452
+ Args:
453
+ task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
454
+ reason: Reason for de-escalation (required)
455
+ reset_validation: Also reset validation fail count (default: False)
456
+
457
+ Returns:
458
+ Updated task details
459
+ """
460
+ # Resolve task reference
461
+ try:
462
+ resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
463
+ except (TaskNotFoundError, ValueError) as e:
464
+ return {"error": f"Invalid task_id: {e}"}
465
+
466
+ task = task_manager.get_task(resolved_task_id)
467
+ if not task:
468
+ return {"error": f"Task {task_id} not found"}
469
+
470
+ if task.status != "escalated":
471
+ return {"error": f"Task {task_id} is not escalated (current status: {task.status})"}
472
+
473
+ # Build update kwargs
474
+ update_kwargs: dict[str, Any] = {
475
+ "status": "open",
476
+ "escalated_at": None,
477
+ "escalation_reason": None,
478
+ }
479
+
480
+ if reset_validation:
481
+ update_kwargs["validation_fail_count"] = 0
482
+
483
+ updated_task = task_manager.update_task(task.id, **update_kwargs)
484
+
485
+ return {
486
+ "task_id": updated_task.id,
487
+ "status": updated_task.status,
488
+ "escalated_at": updated_task.escalated_at,
489
+ "escalation_reason": updated_task.escalation_reason,
490
+ "de_escalation_reason": reason,
491
+ "validation_reset": reset_validation,
492
+ }
493
+
494
+ @registry.tool(
495
+ name="generate_validation_criteria",
496
+ description="Generate validation criteria for a task using AI. Updates the task with the generated criteria.",
497
+ )
498
+ async def generate_validation_criteria(task_id: str) -> dict[str, Any]:
499
+ """
500
+ Generate validation criteria for a task using AI.
501
+
502
+ For parent tasks (tasks with children), sets criteria to "All child tasks completed".
503
+ For leaf tasks, uses LLM to generate criteria from title/description.
504
+
505
+ Args:
506
+ task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
507
+
508
+ Returns:
509
+ Generated criteria and updated task info
510
+ """
511
+ # Resolve task reference
512
+ try:
513
+ resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
514
+ except (TaskNotFoundError, ValueError) as e:
515
+ return {"error": f"Invalid task_id: {e}"}
516
+
517
+ task = task_manager.get_task(resolved_task_id)
518
+ if not task:
519
+ raise ValueError(f"Task not found: {task_id}")
520
+
521
+ if task.validation_criteria:
522
+ return {
523
+ "task_id": task.id,
524
+ "validation_criteria": task.validation_criteria,
525
+ "generated": False,
526
+ "message": "Task already has validation criteria",
527
+ }
528
+
529
+ # Check if task has children (is a parent task)
530
+ children = task_manager.list_tasks(parent_task_id=task.id, limit=1)
531
+ criteria: str | None
532
+
533
+ if children:
534
+ # Parent task: criteria is child completion
535
+ criteria = "All child tasks must be completed (status: closed)."
536
+ else:
537
+ # Leaf task: use LLM to generate criteria
538
+ if not task_validator:
539
+ raise RuntimeError("Task validation is not enabled")
540
+
541
+ criteria = await task_validator.generate_criteria(
542
+ title=task.title,
543
+ description=task.description,
544
+ labels=task.labels,
545
+ )
546
+
547
+ if not criteria:
548
+ return {
549
+ "task_id": task.id,
550
+ "validation_criteria": None,
551
+ "generated": False,
552
+ "error": "Failed to generate criteria",
553
+ }
554
+
555
+ # Update task with generated criteria
556
+ task_manager.update_task(task.id, validation_criteria=criteria)
557
+
558
+ return {
559
+ "task_id": task.id,
560
+ "validation_criteria": criteria,
561
+ "generated": True,
562
+ "is_parent_task": len(children) > 0,
563
+ }
564
+
565
+ @registry.tool(
566
+ name="run_fix_attempt",
567
+ description="Spawn a fix agent to address validation issues. Returns when the fix attempt completes.",
568
+ )
569
+ async def run_fix_attempt(
570
+ task_id: str,
571
+ issues: list[str] | None = None,
572
+ timeout: float = 120.0,
573
+ max_turns: int = 10,
574
+ ) -> dict[str, Any]:
575
+ """
576
+ Spawn an agent to fix validation issues for a task.
577
+
578
+ The fix agent is given the original task context plus validation
579
+ failure details to guide its fixes.
580
+
581
+ Args:
582
+ task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
583
+ issues: List of specific issues to fix (uses validation_feedback if not provided)
584
+ timeout: Max time for fix attempt in seconds (default: 120)
585
+ max_turns: Max agent turns (default: 10)
586
+
587
+ Returns:
588
+ Dict with success status and fix details
589
+ """
590
+ # Resolve task reference
591
+ try:
592
+ resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
593
+ except (TaskNotFoundError, ValueError) as e:
594
+ return {"error": f"Invalid task_id: {e}"}
595
+
596
+ if not agent_runner:
597
+ return {
598
+ "success": False,
599
+ "error": "Agent runner not configured - cannot spawn fix agent",
600
+ }
601
+
602
+ task = task_manager.get_task(resolved_task_id)
603
+ if not task:
604
+ return {"success": False, "error": f"Task not found: {task_id}"}
605
+
606
+ # Get issues from parameter or task validation feedback
607
+ issues_text = ""
608
+ if issues:
609
+ issues_text = "\n".join(f"- {issue}" for issue in issues)
610
+ elif task.validation_feedback:
611
+ issues_text = task.validation_feedback
612
+ else:
613
+ return {
614
+ "success": False,
615
+ "error": "No issues provided and no validation feedback on task",
616
+ }
617
+
618
+ # Get repo path for context
619
+ repo_path = None
620
+ if get_project_repo_path and task.project_id:
621
+ repo_path = get_project_repo_path(task.project_id)
622
+
623
+ # Build fix prompt
624
+ fix_prompt = f"""You are fixing validation failures for a task.
625
+
626
+ ## Original Task
627
+ **Title:** {task.title}
628
+ **Description:** {task.description or "No description provided."}
629
+
630
+ ## Validation Criteria
631
+ {task.validation_criteria or "No specific criteria - use task description."}
632
+
633
+ ## Validation Failures
634
+ {issues_text}
635
+
636
+ ## Instructions
637
+ 1. Read the relevant files to understand the current state
638
+ 2. Fix the issues listed above
639
+ 3. Ensure all validation criteria pass after your fixes
640
+ 4. Do NOT create new tasks - fix the issues directly
641
+
642
+ Focus on fixing ONLY the listed issues. Do not make unrelated changes.
643
+ """
644
+
645
+ try:
646
+ from gobby.agents.runner import AgentConfig
647
+
648
+ config = AgentConfig(
649
+ prompt=fix_prompt,
650
+ parent_session_id=None, # No parent session for fix agent
651
+ project_id=task.project_id,
652
+ machine_id=None, # Will be inferred
653
+ source="claude",
654
+ workflow=None, # No workflow - direct execution
655
+ task=None, # Don't claim the task
656
+ mode="headless",
657
+ timeout=timeout,
658
+ max_turns=max_turns,
659
+ project_path=repo_path,
660
+ )
661
+
662
+ # Run the fix agent
663
+ result = await agent_runner.run(config)
664
+
665
+ # Record the fix attempt
666
+ iteration = (task.validation_fail_count or 0) + 1
667
+ validation_history_manager.record_iteration(
668
+ task_id=task.id,
669
+ iteration=iteration,
670
+ status="fix_attempted",
671
+ feedback=f"Fix agent completed with status: {result.status}",
672
+ issues=None,
673
+ context_type="fix_agent",
674
+ context_summary=f"Fix attempt {iteration}",
675
+ validator_type="fix_agent",
676
+ )
677
+
678
+ return {
679
+ "success": True,
680
+ "task_id": task_id,
681
+ "fix_status": result.status,
682
+ "agent_output": result.output,
683
+ "agent_turns": result.turns_used,
684
+ }
685
+
686
+ except Exception as e:
687
+ logger.exception(f"Fix attempt failed for task {task_id}")
688
+ return {
689
+ "success": False,
690
+ "error": f"Fix agent failed: {e!s}",
691
+ }
692
+
693
+ @registry.tool(
694
+ name="validate_and_fix",
695
+ description="Run validation loop with automatic fix attempts. Validates, spawns fix agent if needed, re-validates.",
696
+ )
697
+ async def validate_and_fix(
698
+ task_id: str,
699
+ max_retries: int = 3,
700
+ auto_fix: bool = True,
701
+ fix_timeout: float = 120.0,
702
+ ) -> dict[str, Any]:
703
+ """
704
+ Run validation loop with automatic fix attempts.
705
+
706
+ 1. Validate task completion
707
+ 2. If failed and auto_fix=True:
708
+ - Spawn fix agent to address issues
709
+ - Re-validate after fix
710
+ - Repeat up to max_retries
711
+ 3. If still failing after retries:
712
+ - Create fix subtask with failure details
713
+ - Mark task status = 'failed'
714
+
715
+ Args:
716
+ task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
717
+ max_retries: Maximum fix attempts before giving up (default: 3)
718
+ auto_fix: Whether to attempt automatic fixes (default: True)
719
+ fix_timeout: Timeout per fix attempt in seconds (default: 120)
720
+
721
+ Returns:
722
+ Validation result with loop history
723
+ """
724
+ # Resolve task reference
725
+ try:
726
+ resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
727
+ except (TaskNotFoundError, ValueError) as e:
728
+ return {"error": f"Invalid task_id: {e}"}
729
+
730
+ task = task_manager.get_task(resolved_task_id)
731
+ if not task:
732
+ return {"success": False, "error": f"Task not found: {task_id}"}
733
+
734
+ # Check if task has children (parent tasks use child completion validation)
735
+ children = task_manager.list_tasks(parent_task_id=task.id, limit=1)
736
+ if children:
737
+ # For parent tasks, just run regular validation (no fix loop)
738
+ result = await validate_task(task.id)
739
+ return {
740
+ "success": True,
741
+ "task_id": task.id,
742
+ "is_parent_task": True,
743
+ "validation_result": result,
744
+ }
745
+
746
+ loop_history: list[dict[str, Any]] = []
747
+ current_retry = 0
748
+
749
+ while current_retry < max_retries:
750
+ # Run validation
751
+ validation_result = await validate_task(task.id)
752
+ loop_history.append(
753
+ {
754
+ "iteration": current_retry + 1,
755
+ "action": "validate",
756
+ "result": validation_result,
757
+ }
758
+ )
759
+
760
+ if validation_result.get("is_valid"):
761
+ # Success! Task is closed by validate_task
762
+ return {
763
+ "success": True,
764
+ "task_id": task.id,
765
+ "is_valid": True,
766
+ "iterations": current_retry + 1,
767
+ "loop_history": loop_history,
768
+ "message": "Task validated successfully",
769
+ }
770
+
771
+ # Validation failed - attempt fix if enabled and agent runner available
772
+ if not auto_fix:
773
+ break
774
+
775
+ if not agent_runner:
776
+ loop_history.append(
777
+ {
778
+ "iteration": current_retry + 1,
779
+ "action": "fix_skipped",
780
+ "reason": "Agent runner not configured",
781
+ }
782
+ )
783
+ break
784
+
785
+ # Spawn fix agent
786
+ fix_result = await run_fix_attempt(
787
+ task_id=task.id,
788
+ timeout=fix_timeout,
789
+ )
790
+ loop_history.append(
791
+ {
792
+ "iteration": current_retry + 1,
793
+ "action": "fix_attempt",
794
+ "result": fix_result,
795
+ }
796
+ )
797
+
798
+ if not fix_result.get("success"):
799
+ # Fix agent failed to run
800
+ logger.warning(f"Fix attempt {current_retry + 1} failed: {fix_result.get('error')}")
801
+
802
+ current_retry += 1
803
+
804
+ # All retries exhausted - mark as failed
805
+ final_feedback = f"QA loop exhausted after {current_retry} fix attempts."
806
+ if loop_history:
807
+ last_validation = next(
808
+ (h for h in reversed(loop_history) if h.get("action") == "validate"),
809
+ None,
810
+ )
811
+ if last_validation and last_validation.get("result", {}).get("feedback"):
812
+ final_feedback += (
813
+ f"\n\nLast validation feedback:\n{last_validation['result']['feedback']}"
814
+ )
815
+
816
+ # Create fix subtask for manual intervention
817
+ fix_task = task_manager.create_task(
818
+ project_id=task.project_id,
819
+ title=f"[Manual Fix] {task.title}",
820
+ description=f"Automatic fix attempts failed.\n\n{final_feedback}",
821
+ parent_task_id=task.id,
822
+ priority=1,
823
+ task_type="bug",
824
+ )
825
+
826
+ # Mark task as failed
827
+ task_manager.update_task(
828
+ task.id,
829
+ status="failed",
830
+ validation_feedback=final_feedback,
831
+ )
832
+
833
+ return {
834
+ "success": False,
835
+ "task_id": task.id,
836
+ "is_valid": False,
837
+ "iterations": current_retry,
838
+ "loop_history": loop_history,
839
+ "fix_subtask_id": fix_task.id,
840
+ "message": f"Validation failed after {current_retry} fix attempts. Created fix subtask: {fix_task.id}",
841
+ }
842
+
843
+ return registry