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,301 @@
1
+ """Validation helpers for task lifecycle operations.
2
+
3
+ Provides validation functions used by close_task to verify tasks
4
+ can be closed (commit checks, child completion, LLM validation).
5
+ """
6
+
7
+ import logging
8
+ from dataclasses import dataclass
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from gobby.mcp_proxy.tools.tasks._helpers import SKIP_REASONS
12
+ from gobby.storage.tasks import Task
13
+
14
+ if TYPE_CHECKING:
15
+ from gobby.config.tasks import TaskValidationConfig
16
+ from gobby.mcp_proxy.tools.tasks._context import RegistryContext
17
+ from gobby.storage.tasks import LocalTaskManager
18
+ from gobby.tasks.validation import TaskValidator
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ @dataclass
24
+ class ValidationResult:
25
+ """Result of validation checks."""
26
+
27
+ can_close: bool
28
+ error_type: str | None = None
29
+ message: str | None = None
30
+ extra: dict[str, Any] | None = None
31
+
32
+
33
+ def validate_commit_requirements(
34
+ task: Task,
35
+ reason: str,
36
+ no_commit_needed: bool,
37
+ override_justification: str | None,
38
+ ) -> ValidationResult:
39
+ """Check if task meets commit requirements for closing.
40
+
41
+ Args:
42
+ task: The task to validate
43
+ reason: Reason for closing
44
+ no_commit_needed: If True, allow closing without commits
45
+ override_justification: Justification for skipping commit check
46
+
47
+ Returns:
48
+ ValidationResult indicating if task can be closed
49
+ """
50
+ # Skip commit check for certain close reasons that imply no work was done
51
+ requires_commit_check = reason.lower() not in SKIP_REASONS
52
+
53
+ if requires_commit_check and not task.commits:
54
+ # No commits linked - require explicit acknowledgment
55
+ if no_commit_needed:
56
+ if not override_justification:
57
+ return ValidationResult(
58
+ can_close=False,
59
+ error_type="justification_required",
60
+ message=(
61
+ "When no_commit_needed=True, you must provide "
62
+ "override_justification explaining why no commit was needed."
63
+ ),
64
+ )
65
+ # Allowed to proceed - agent confirmed no commit needed
66
+ else:
67
+ return ValidationResult(
68
+ can_close=False,
69
+ error_type="no_commits_linked",
70
+ message=(
71
+ "Cannot close task: no commits are linked. Either:\n"
72
+ "1. Commit your changes and use link_commit() or include [task_id] in commit message\n"
73
+ "2. Set no_commit_needed=True with override_justification if this task didn't require code changes"
74
+ ),
75
+ )
76
+
77
+ return ValidationResult(can_close=True)
78
+
79
+
80
+ def validate_parent_task(
81
+ ctx: "RegistryContext",
82
+ task_id: str,
83
+ ) -> ValidationResult:
84
+ """Check if a parent task's children are all closed.
85
+
86
+ Args:
87
+ ctx: Registry context
88
+ task_id: The parent task ID
89
+
90
+ Returns:
91
+ ValidationResult indicating if parent can be closed
92
+ """
93
+ children = ctx.task_manager.list_tasks(parent_task_id=task_id, limit=1000)
94
+
95
+ if children:
96
+ open_children = [c for c in children if c.status != "closed"]
97
+ if open_children:
98
+ open_titles = [f"- {c.id}: {c.title}" for c in open_children[:5]]
99
+ remaining = len(open_children) - 5 if len(open_children) > 5 else 0
100
+ feedback = f"Cannot close: {len(open_children)} child tasks still open:\n"
101
+ feedback += "\n".join(open_titles)
102
+ if remaining > 0:
103
+ feedback += f"\n... and {remaining} more"
104
+ return ValidationResult(
105
+ can_close=False,
106
+ error_type="validation_failed",
107
+ message=feedback,
108
+ extra={"open_children": [c.id for c in open_children]},
109
+ )
110
+
111
+ return ValidationResult(can_close=True)
112
+
113
+
114
+ def gather_validation_context(
115
+ task: Task,
116
+ changes_summary: str | None,
117
+ repo_path: str | None,
118
+ task_manager: "LocalTaskManager",
119
+ ) -> tuple[str | None, str | None]:
120
+ """Gather context for LLM validation.
121
+
122
+ Uses provided changes_summary or auto-fetches via smart context gathering.
123
+
124
+ Args:
125
+ task: The task to validate
126
+ changes_summary: Optional user-provided summary
127
+ repo_path: Path to the repository
128
+ task_manager: LocalTaskManager for fetching task diff
129
+
130
+ Returns:
131
+ Tuple of (validation_context, raw_diff)
132
+ """
133
+ from gobby.tasks.commits import get_task_diff, summarize_diff_for_validation
134
+
135
+ validation_context = changes_summary
136
+ raw_diff = None
137
+
138
+ if not validation_context:
139
+ # First try commit-based diff if task has linked commits
140
+ if task.commits:
141
+ try:
142
+ # Don't include uncommitted changes - they're likely unrelated to this task
143
+ # The linked commits ARE the work for this task
144
+ diff_result = get_task_diff(
145
+ task_id=task.id,
146
+ task_manager=task_manager,
147
+ include_uncommitted=False,
148
+ cwd=repo_path,
149
+ )
150
+ if diff_result.diff:
151
+ raw_diff = diff_result.diff
152
+ # Use smart summarization to ensure all files are visible
153
+ summarized_diff = summarize_diff_for_validation(raw_diff)
154
+ validation_context = (
155
+ f"Commit-based diff ({len(diff_result.commits)} commits, "
156
+ f"{diff_result.file_count} files):\n\n{summarized_diff}"
157
+ )
158
+ else:
159
+ logger.warning(
160
+ f"get_task_diff returned empty for task {task.id} "
161
+ f"with commits {task.commits}"
162
+ )
163
+ except Exception as e:
164
+ logger.warning(f"get_task_diff failed for task {task.id}: {e}")
165
+
166
+ # Fall back to smart context ONLY if no linked commits
167
+ # (uncommitted changes are unrelated if we have specific commits linked)
168
+ if not validation_context and not task.commits:
169
+ from gobby.tasks.validation import get_validation_context_smart
170
+
171
+ # Smart context gathering: uncommitted changes + multi-commit window + file analysis
172
+ smart_context = get_validation_context_smart(
173
+ task_title=task.title,
174
+ validation_criteria=task.validation_criteria,
175
+ task_description=task.description,
176
+ cwd=repo_path,
177
+ )
178
+ if smart_context:
179
+ validation_context = f"Validation context:\n\n{smart_context}"
180
+
181
+ return validation_context, raw_diff
182
+
183
+
184
+ async def validate_leaf_task_with_llm(
185
+ task: Task,
186
+ task_validator: "TaskValidator",
187
+ validation_context: str,
188
+ raw_diff: str | None,
189
+ ctx: "RegistryContext",
190
+ resolved_id: str,
191
+ validation_config: "TaskValidationConfig | None",
192
+ ) -> ValidationResult:
193
+ """Run LLM validation on a leaf task.
194
+
195
+ Args:
196
+ task: The task to validate
197
+ task_validator: The validator instance
198
+ validation_context: Context for validation
199
+ raw_diff: Raw diff for doc-only check
200
+ ctx: Registry context
201
+ resolved_id: Resolved task ID
202
+ validation_config: Validation configuration
203
+
204
+ Returns:
205
+ ValidationResult indicating if task can be closed
206
+ """
207
+ from gobby.tasks.commits import is_doc_only_diff
208
+
209
+ # Auto-skip LLM validation for doc-only changes
210
+ if raw_diff and is_doc_only_diff(raw_diff):
211
+ logger.info(f"Skipping LLM validation for task {task.id}: doc-only changes")
212
+ ctx.task_manager.update_task(
213
+ resolved_id,
214
+ validation_status="valid",
215
+ validation_feedback="Auto-validated: documentation-only changes",
216
+ )
217
+ return ValidationResult(can_close=True)
218
+
219
+ # Run LLM validation
220
+ result = await task_validator.validate_task(
221
+ task_id=task.id,
222
+ title=task.title,
223
+ description=task.description,
224
+ changes_summary=validation_context,
225
+ validation_criteria=task.validation_criteria,
226
+ category=task.category,
227
+ )
228
+
229
+ # Store validation result regardless of pass/fail
230
+ ctx.task_manager.update_task(
231
+ resolved_id,
232
+ validation_status=result.status,
233
+ validation_feedback=result.feedback,
234
+ )
235
+
236
+ if result.status != "valid":
237
+ # Block closing on invalid or pending (error during validation)
238
+ return ValidationResult(
239
+ can_close=False,
240
+ error_type="validation_failed",
241
+ message=result.feedback or "Validation did not pass",
242
+ extra={"validation_status": result.status},
243
+ )
244
+
245
+ # Run external validation if enabled (after internal validation passes)
246
+ if validation_config and validation_config.use_external_validator:
247
+ from gobby.tasks.external_validator import run_external_validation
248
+
249
+ external_result = await run_external_validation(
250
+ config=validation_config,
251
+ llm_service=task_validator.llm_service,
252
+ task={
253
+ "id": task.id,
254
+ "title": task.title,
255
+ "description": task.description,
256
+ "validation_criteria": task.validation_criteria,
257
+ },
258
+ changes_context=validation_context,
259
+ agent_runner=ctx.agent_runner,
260
+ )
261
+
262
+ if external_result.status not in ("valid", "skipped"):
263
+ # Block closing on external validation failure
264
+ return ValidationResult(
265
+ can_close=False,
266
+ error_type="external_validation_failed",
267
+ message=external_result.summary,
268
+ extra={
269
+ "validation_status": external_result.status,
270
+ "issues": [issue.to_dict() for issue in external_result.issues],
271
+ },
272
+ )
273
+
274
+ return ValidationResult(can_close=True)
275
+
276
+
277
+ def determine_close_outcome(
278
+ task: Task,
279
+ skip_validation: bool,
280
+ no_commit_needed: bool,
281
+ override_justification: str | None,
282
+ ) -> tuple[bool, bool]:
283
+ """Determine the close outcome for a task.
284
+
285
+ Args:
286
+ task: The task being closed
287
+ skip_validation: Whether validation was skipped
288
+ no_commit_needed: Whether commit was not needed
289
+ override_justification: Justification for override
290
+
291
+ Returns:
292
+ Tuple of (route_to_review, store_override)
293
+ """
294
+ # Determine if override should be stored
295
+ store_override = skip_validation or no_commit_needed
296
+
297
+ # Route to review if task requires user review OR override was used
298
+ # This ensures tasks with HITL flag or skipped validation go through human review
299
+ route_to_review = bool(task.requires_user_review or (override_justification and store_override))
300
+
301
+ return route_to_review, store_override
@@ -0,0 +1,55 @@
1
+ """Task ID resolution for MCP tools.
2
+
3
+ Provides resolve_task_id_for_mcp() which resolves various task reference
4
+ formats (#N, N, path, UUID) to task UUIDs.
5
+ """
6
+
7
+ from gobby.mcp_proxy.tools.tasks._helpers import _is_path_format
8
+ from gobby.storage.tasks import LocalTaskManager, TaskNotFoundError
9
+ from gobby.utils.project_context import get_project_context
10
+
11
+
12
+ def resolve_task_id_for_mcp(
13
+ task_manager: LocalTaskManager,
14
+ task_id: str,
15
+ project_id: str | None = None,
16
+ ) -> str:
17
+ """Resolve a task reference to its UUID for MCP tools.
18
+
19
+ Supports multiple reference formats:
20
+ - #N: Project-scoped seq_num (e.g., #1, #47) - requires project_id
21
+ - N: Bare numeric seq_num (e.g., 1, 47) - requires project_id
22
+ - 1.2.3: Path cache format - requires project_id
23
+ - UUID: Direct UUID lookup (validated to exist)
24
+
25
+ Args:
26
+ task_manager: The task manager
27
+ task_id: Task reference in any supported format
28
+ project_id: Project ID for scoped lookups (#N and path formats).
29
+ If not provided, will try to get from project context.
30
+
31
+ Returns:
32
+ The resolved task UUID
33
+
34
+ Raises:
35
+ TaskNotFoundError: If the reference cannot be resolved
36
+ ValueError: If the format is invalid
37
+ """
38
+ # Get project_id from context if not provided
39
+ if project_id is None:
40
+ ctx = get_project_context()
41
+ project_id = ctx.get("id") if ctx else None
42
+
43
+ # Check for #N format or path format (requires project_id)
44
+ if project_id and (task_id.startswith("#") or _is_path_format(task_id)):
45
+ return task_manager.resolve_task_reference(task_id, project_id)
46
+
47
+ # Check for bare numeric string (seq_num without #)
48
+ if project_id and task_id.isdigit():
49
+ return task_manager.resolve_task_reference(f"#{task_id}", project_id)
50
+
51
+ # UUID format: validate it exists by trying to get it
52
+ task = task_manager.get_task(task_id)
53
+ if task is None:
54
+ raise TaskNotFoundError(f"Task with UUID '{task_id}' not found")
55
+ return task_id
@@ -0,0 +1,215 @@
1
+ """Search operations for task management.
2
+
3
+ Provides semantic search tools for tasks using TF-IDF.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from gobby.mcp_proxy.tools.internal import InternalToolRegistry
9
+ from gobby.mcp_proxy.tools.tasks._context import RegistryContext
10
+ from gobby.mcp_proxy.tools.tasks._resolution import resolve_task_id_for_mcp
11
+ from gobby.utils.project_context import get_project_context
12
+
13
+
14
+ def create_search_registry(ctx: RegistryContext) -> InternalToolRegistry:
15
+ """Create a registry with task search tools.
16
+
17
+ Args:
18
+ ctx: Shared registry context
19
+
20
+ Returns:
21
+ InternalToolRegistry with search tools registered
22
+ """
23
+ registry = InternalToolRegistry(
24
+ name="gobby-tasks-search",
25
+ description="Task search operations",
26
+ )
27
+
28
+ def search_tasks(
29
+ query: str,
30
+ status: str | list[str] | None = None,
31
+ task_type: str | None = None,
32
+ priority: int | None = None,
33
+ parent_task_id: str | None = None,
34
+ category: str | None = None,
35
+ limit: int = 20,
36
+ min_score: float = 0.0,
37
+ all_projects: bool = False,
38
+ ) -> dict[str, Any]:
39
+ """Search tasks using semantic TF-IDF search.
40
+
41
+ Performs semantic search on task title, description, labels, and type.
42
+ Results are ranked by relevance and can be filtered by status, type, etc.
43
+
44
+ Args:
45
+ query: Search query text (required). Natural language query.
46
+ status: Filter by status (open, in_progress, review, closed).
47
+ Can be a single status or comma-separated list.
48
+ task_type: Filter by task type (task, bug, feature, epic)
49
+ priority: Filter by priority (1=High, 2=Medium, 3=Low)
50
+ parent_task_id: Filter by parent task ID (UUID, #N, or N format)
51
+ category: Filter by task category
52
+ limit: Maximum number of results (default: 20)
53
+ min_score: Minimum similarity score 0.0-1.0 (default: 0.0)
54
+ all_projects: If true, search all projects instead of current project
55
+
56
+ Returns:
57
+ Dict with matching tasks and their similarity scores
58
+ """
59
+ if not query or not query.strip():
60
+ return {"error": "Query is required", "tasks": [], "count": 0}
61
+
62
+ # Get current project context unless all_projects
63
+ project_id = None
64
+ if not all_projects:
65
+ project_ctx = get_project_context()
66
+ if project_ctx and project_ctx.get("id"):
67
+ project_id = project_ctx["id"]
68
+
69
+ # Handle comma-separated status string
70
+ status_filter: str | list[str] | None = status
71
+ if isinstance(status, str) and "," in status:
72
+ status_filter = [s.strip() for s in status.split(",")]
73
+
74
+ # Resolve parent_task_id if provided (#N, N, or UUID -> UUID)
75
+ resolved_parent_id = None
76
+ if parent_task_id:
77
+ try:
78
+ resolved_parent_id = resolve_task_id_for_mcp(
79
+ ctx.task_manager, parent_task_id, project_id
80
+ )
81
+ except Exception:
82
+ return {
83
+ "error": f"Invalid parent_task_id: {parent_task_id}",
84
+ "tasks": [],
85
+ "count": 0,
86
+ }
87
+
88
+ # Perform search
89
+ results = ctx.task_manager.search_tasks(
90
+ query=query.strip(),
91
+ project_id=project_id,
92
+ status=status_filter,
93
+ task_type=task_type,
94
+ priority=priority,
95
+ parent_task_id=resolved_parent_id,
96
+ category=category,
97
+ limit=limit,
98
+ min_score=min_score,
99
+ )
100
+
101
+ return {
102
+ "tasks": [
103
+ {
104
+ **task.to_brief(),
105
+ "score": round(score, 4),
106
+ }
107
+ for task, score in results
108
+ ],
109
+ "count": len(results),
110
+ "query": query.strip(),
111
+ }
112
+
113
+ registry.register(
114
+ name="search_tasks",
115
+ description="Search tasks using semantic TF-IDF search. Returns tasks ranked by relevance to the query.",
116
+ input_schema={
117
+ "type": "object",
118
+ "properties": {
119
+ "query": {
120
+ "type": "string",
121
+ "description": "Search query text. Natural language query to find matching tasks.",
122
+ },
123
+ "status": {
124
+ "oneOf": [
125
+ {"type": "string"},
126
+ {"type": "array", "items": {"type": "string"}},
127
+ ],
128
+ "description": "Filter by status. Can be single status or comma-separated list (e.g., 'open,in_progress')",
129
+ "default": None,
130
+ },
131
+ "task_type": {
132
+ "type": "string",
133
+ "description": "Filter by task type (task, bug, feature, epic)",
134
+ "default": None,
135
+ },
136
+ "priority": {
137
+ "type": "integer",
138
+ "description": "Filter by priority (1=High, 2=Medium, 3=Low)",
139
+ "default": None,
140
+ },
141
+ "parent_task_id": {
142
+ "type": "string",
143
+ "description": "Filter by parent task ID (UUID, #N, or N format)",
144
+ "default": None,
145
+ },
146
+ "category": {
147
+ "type": "string",
148
+ "description": "Filter by task category",
149
+ "default": None,
150
+ },
151
+ "limit": {
152
+ "type": "integer",
153
+ "description": "Maximum number of results to return",
154
+ "default": 20,
155
+ },
156
+ "min_score": {
157
+ "type": "number",
158
+ "description": "Minimum similarity score threshold (0.0-1.0)",
159
+ "default": 0.0,
160
+ },
161
+ "all_projects": {
162
+ "type": "boolean",
163
+ "description": "If true, search all projects instead of just the current project",
164
+ "default": False,
165
+ },
166
+ },
167
+ "required": ["query"],
168
+ },
169
+ func=search_tasks,
170
+ )
171
+
172
+ def reindex_tasks(all_projects: bool = False) -> dict[str, Any]:
173
+ """Force rebuild of the task search index.
174
+
175
+ Use this to refresh the search index after bulk operations
176
+ or if search results seem stale.
177
+
178
+ Args:
179
+ all_projects: If true, reindex all projects instead of current project
180
+
181
+ Returns:
182
+ Dict with index statistics
183
+ """
184
+ # Get current project context unless all_projects
185
+ project_id = None
186
+ if not all_projects:
187
+ project_ctx = get_project_context()
188
+ if project_ctx and project_ctx.get("id"):
189
+ project_id = project_ctx["id"]
190
+
191
+ stats = ctx.task_manager.reindex_search(project_id)
192
+
193
+ return {
194
+ "success": True,
195
+ "message": f"Search index rebuilt with {stats.get('item_count', 0)} tasks",
196
+ "stats": stats,
197
+ }
198
+
199
+ registry.register(
200
+ name="reindex_tasks",
201
+ description="Force rebuild of the task search index. Use after bulk operations or if search seems stale.",
202
+ input_schema={
203
+ "type": "object",
204
+ "properties": {
205
+ "all_projects": {
206
+ "type": "boolean",
207
+ "description": "If true, reindex all projects instead of just the current project",
208
+ "default": False,
209
+ },
210
+ },
211
+ },
212
+ func=reindex_tasks,
213
+ )
214
+
215
+ return registry
@@ -0,0 +1,125 @@
1
+ """Session integration tools for task management.
2
+
3
+ Provides tools for linking tasks to sessions and querying task-session
4
+ relationships.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from gobby.mcp_proxy.tools.internal import InternalToolRegistry
10
+ from gobby.mcp_proxy.tools.tasks._context import RegistryContext
11
+ from gobby.mcp_proxy.tools.tasks._resolution import resolve_task_id_for_mcp
12
+ from gobby.storage.tasks import TaskNotFoundError
13
+
14
+
15
+ def create_session_registry(ctx: RegistryContext) -> InternalToolRegistry:
16
+ """Create a registry with session-task integration tools.
17
+
18
+ Args:
19
+ ctx: Shared registry context
20
+
21
+ Returns:
22
+ InternalToolRegistry with session tools registered
23
+ """
24
+ registry = InternalToolRegistry(
25
+ name="gobby-tasks-session",
26
+ description="Task-session integration tools",
27
+ )
28
+
29
+ def link_task_to_session(
30
+ task_id: str,
31
+ session_id: str | None = None,
32
+ action: str = "worked_on",
33
+ ) -> dict[str, Any]:
34
+ """Link a task to a session."""
35
+ if not session_id:
36
+ return {"error": "session_id is required"}
37
+
38
+ try:
39
+ resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
40
+ except (TaskNotFoundError, ValueError) as e:
41
+ return {"error": str(e)}
42
+
43
+ try:
44
+ ctx.session_task_manager.link_task(session_id, resolved_id, action)
45
+ return {}
46
+ except ValueError as e:
47
+ return {"error": str(e)}
48
+
49
+ registry.register(
50
+ name="link_task_to_session",
51
+ description="Link a task to a session.",
52
+ input_schema={
53
+ "type": "object",
54
+ "properties": {
55
+ "task_id": {
56
+ "type": "string",
57
+ "description": "Task reference: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID",
58
+ },
59
+ "session_id": {
60
+ "type": "string",
61
+ "description": "Session ID (optional, defaults to linking context if available)",
62
+ "default": None,
63
+ },
64
+ "action": {
65
+ "type": "string",
66
+ "description": "Relationship type (worked_on, discovered, mentioned, closed)",
67
+ "default": "worked_on",
68
+ },
69
+ },
70
+ "required": ["task_id"],
71
+ },
72
+ func=link_task_to_session,
73
+ )
74
+
75
+ def get_session_tasks(session_id: str) -> dict[str, Any]:
76
+ """Get all tasks associated with a session."""
77
+ tasks = ctx.session_task_manager.get_session_tasks(session_id)
78
+ return {"session_id": session_id, "tasks": tasks}
79
+
80
+ registry.register(
81
+ name="get_session_tasks",
82
+ description="Get all tasks associated with a session.",
83
+ input_schema={
84
+ "type": "object",
85
+ "properties": {
86
+ "session_id": {"type": "string", "description": "Session ID"},
87
+ },
88
+ "required": ["session_id"],
89
+ },
90
+ func=get_session_tasks,
91
+ )
92
+
93
+ def get_task_sessions(task_id: str) -> dict[str, Any]:
94
+ """Get all sessions that touched a task."""
95
+ try:
96
+ resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
97
+ except (TaskNotFoundError, ValueError) as e:
98
+ return {"error": str(e)}
99
+ task = ctx.task_manager.get_task(resolved_id)
100
+ sessions = ctx.session_task_manager.get_task_sessions(resolved_id)
101
+ # Handle case where task is not found (shouldn't happen after resolve, but be defensive)
102
+ ref = f"#{task.seq_num}" if task and task.seq_num else resolved_id[:8]
103
+ return {
104
+ "ref": ref,
105
+ "task_id": resolved_id,
106
+ "sessions": sessions,
107
+ }
108
+
109
+ registry.register(
110
+ name="get_task_sessions",
111
+ description="Get all sessions that touched a task.",
112
+ input_schema={
113
+ "type": "object",
114
+ "properties": {
115
+ "task_id": {
116
+ "type": "string",
117
+ "description": "Task reference: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID",
118
+ },
119
+ },
120
+ "required": ["task_id"],
121
+ },
122
+ func=get_task_sessions,
123
+ )
124
+
125
+ return registry