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,517 @@
1
+ """Lifecycle operations for task management.
2
+
3
+ Provides task lifecycle tools: close, reopen, delete, and label management.
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._helpers import SKIP_REASONS
11
+ from gobby.mcp_proxy.tools.tasks._lifecycle_validation import (
12
+ determine_close_outcome,
13
+ gather_validation_context,
14
+ validate_commit_requirements,
15
+ validate_leaf_task_with_llm,
16
+ validate_parent_task,
17
+ )
18
+ from gobby.mcp_proxy.tools.tasks._resolution import resolve_task_id_for_mcp
19
+ from gobby.storage.tasks import TaskNotFoundError
20
+ from gobby.storage.worktrees import LocalWorktreeManager
21
+
22
+
23
+ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
24
+ """Create a registry with task lifecycle tools.
25
+
26
+ Args:
27
+ ctx: Shared registry context
28
+
29
+ Returns:
30
+ InternalToolRegistry with lifecycle tools registered
31
+ """
32
+ registry = InternalToolRegistry(
33
+ name="gobby-tasks-lifecycle",
34
+ description="Task lifecycle operations",
35
+ )
36
+
37
+ async def close_task(
38
+ task_id: str,
39
+ reason: str = "completed",
40
+ changes_summary: str | None = None,
41
+ skip_validation: bool = False,
42
+ session_id: str | None = None,
43
+ override_justification: str | None = None,
44
+ no_commit_needed: bool = False,
45
+ commit_sha: str | None = None,
46
+ ) -> dict[str, Any]:
47
+ """Close a task with validation.
48
+
49
+ For parent tasks: automatically checks all children are closed.
50
+ For leaf tasks: optionally validates with LLM if changes_summary provided.
51
+
52
+ Args:
53
+ task_id: Task reference (#N, path, or UUID)
54
+ reason: Reason for closing
55
+ changes_summary: Summary of changes (enables LLM validation for leaf tasks)
56
+ skip_validation: Skip all validation checks
57
+ session_id: Session ID where task is being closed (auto-links to session)
58
+ override_justification: Why agent bypassed validation (stored for audit).
59
+ Also used to explain why no commit was needed when no_commit_needed=True.
60
+ no_commit_needed: Set to True for tasks that don't produce code changes
61
+ (research, planning, documentation review). Requires override_justification.
62
+ commit_sha: Git commit SHA to link before closing. Convenience for link + close in one call.
63
+
64
+ Returns:
65
+ Closed task or error with validation feedback
66
+ """
67
+ # Resolve task reference (supports #N, path, UUID formats)
68
+ try:
69
+ resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
70
+ except TaskNotFoundError as e:
71
+ return {"error": str(e)}
72
+ except ValueError as e:
73
+ return {"error": str(e)}
74
+
75
+ task = ctx.task_manager.get_task(resolved_id)
76
+ if not task:
77
+ return {"error": f"Task {task_id} not found"}
78
+
79
+ # Link commit if provided (convenience for link + close in one call)
80
+ if commit_sha:
81
+ task = ctx.task_manager.link_commit(resolved_id, commit_sha)
82
+
83
+ # Get project repo_path for git commands
84
+ repo_path = ctx.get_project_repo_path(task.project_id)
85
+ cwd = repo_path or "."
86
+
87
+ # Check for linked commits (unless task type doesn't require commits)
88
+ commit_result = validate_commit_requirements(
89
+ task, reason, no_commit_needed, override_justification
90
+ )
91
+ if not commit_result.can_close:
92
+ return {
93
+ "error": commit_result.error_type,
94
+ "message": commit_result.message,
95
+ }
96
+
97
+ # Auto-skip validation for certain close reasons
98
+ should_skip = skip_validation or reason.lower() in SKIP_REASONS
99
+
100
+ if not should_skip:
101
+ # Check if task has children (is a parent task)
102
+ parent_result = validate_parent_task(ctx, resolved_id)
103
+ if not parent_result.can_close:
104
+ response = {
105
+ "error": parent_result.error_type,
106
+ "message": parent_result.message,
107
+ }
108
+ if parent_result.extra:
109
+ response.update(parent_result.extra)
110
+ return response
111
+
112
+ # Check for leaf task with validation criteria
113
+ children = ctx.task_manager.list_tasks(parent_task_id=resolved_id, limit=1)
114
+ is_leaf = len(children) == 0
115
+
116
+ if is_leaf and ctx.task_validator and task.validation_criteria:
117
+ # Gather validation context
118
+ validation_context, raw_diff = gather_validation_context(
119
+ task, changes_summary, repo_path, ctx.task_manager
120
+ )
121
+
122
+ if validation_context:
123
+ # Run LLM validation
124
+ llm_result = await validate_leaf_task_with_llm(
125
+ task=task,
126
+ task_validator=ctx.task_validator,
127
+ validation_context=validation_context,
128
+ raw_diff=raw_diff,
129
+ ctx=ctx,
130
+ resolved_id=resolved_id,
131
+ validation_config=ctx.validation_config,
132
+ )
133
+ if not llm_result.can_close:
134
+ response = {
135
+ "error": llm_result.error_type,
136
+ "message": llm_result.message,
137
+ }
138
+ if llm_result.extra:
139
+ response.update(llm_result.extra)
140
+ return response
141
+
142
+ # Determine close outcome
143
+ route_to_review, store_override = determine_close_outcome(
144
+ task, skip_validation, no_commit_needed, override_justification
145
+ )
146
+
147
+ # Get git commit SHA (best-effort, dynamic short format for consistency)
148
+ from gobby.utils.git import run_git_command
149
+
150
+ current_commit_sha = run_git_command(["git", "rev-parse", "--short", "HEAD"], cwd=cwd)
151
+
152
+ if route_to_review:
153
+ # Route to review status instead of closing
154
+ # Task stays in review until user explicitly closes
155
+ ctx.task_manager.update_task(
156
+ resolved_id,
157
+ status="review",
158
+ validation_override_reason=override_justification if store_override else None,
159
+ )
160
+
161
+ # Auto-link session if provided
162
+ if session_id:
163
+ try:
164
+ ctx.session_task_manager.link_task(session_id, resolved_id, "review")
165
+ except Exception:
166
+ pass # nosec B110 - best-effort linking
167
+
168
+ return {
169
+ "routed_to_review": True,
170
+ "message": (
171
+ "Task routed to review status. "
172
+ + (
173
+ "Reason: requires user review before closing."
174
+ if task.requires_user_review
175
+ else "Reason: validation was overridden, human review recommended."
176
+ )
177
+ ),
178
+ "task_id": resolved_id,
179
+ }
180
+
181
+ # All checks passed - close the task with session and commit tracking
182
+ ctx.task_manager.close_task(
183
+ resolved_id,
184
+ reason=reason,
185
+ closed_in_session_id=session_id,
186
+ closed_commit_sha=current_commit_sha,
187
+ validation_override_reason=override_justification if store_override else None,
188
+ )
189
+
190
+ # Auto-link session if provided
191
+ if session_id:
192
+ try:
193
+ ctx.session_task_manager.link_task(session_id, resolved_id, "closed")
194
+ except Exception:
195
+ pass # nosec B110 - best-effort linking, don't fail the close
196
+
197
+ # Update worktree status based on closure reason (case-insensitive)
198
+ try:
199
+ reason_normalized = reason.lower()
200
+ worktree_manager = LocalWorktreeManager(ctx.task_manager.db)
201
+ wt = worktree_manager.get_by_task(resolved_id)
202
+ if wt:
203
+ if reason_normalized in (
204
+ "wont_fix",
205
+ "obsolete",
206
+ "duplicate",
207
+ "already_implemented",
208
+ ):
209
+ worktree_manager.mark_abandoned(wt.id)
210
+ elif reason_normalized == "completed":
211
+ worktree_manager.mark_merged(wt.id)
212
+ except Exception:
213
+ pass # nosec B110 - best-effort worktree update, don't fail the close
214
+
215
+ return {}
216
+
217
+ registry.register(
218
+ name="close_task",
219
+ description="Close a task. Commits should use [#N] format (e.g., git commit -m '[#42] feat: add feature'). Requires commits to be linked (auto-detected from commit message or use link_commit). Parent tasks require all children closed. Leaf tasks validate with LLM. Validation auto-skipped for: duplicate, already_implemented, wont_fix, obsolete.",
220
+ input_schema={
221
+ "type": "object",
222
+ "properties": {
223
+ "task_id": {
224
+ "type": "string",
225
+ "description": "Task reference: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID",
226
+ },
227
+ "reason": {
228
+ "type": "string",
229
+ "description": 'Reason for closing. Use "duplicate", "already_implemented", "wont_fix", or "obsolete" to auto-skip validation and commit check.',
230
+ "default": "completed",
231
+ },
232
+ "changes_summary": {
233
+ "type": "string",
234
+ "description": "Summary of changes made. If provided for leaf tasks, triggers LLM validation before close.",
235
+ "default": None,
236
+ },
237
+ "skip_validation": {
238
+ "type": "boolean",
239
+ "description": (
240
+ "Skip LLM validation even when task has validation_criteria. "
241
+ "USE THIS when: validation fails due to truncated diff, validator misses context, "
242
+ "or you've manually verified completion. Provide override_justification explaining why."
243
+ ),
244
+ "default": False,
245
+ },
246
+ "session_id": {
247
+ "type": "string",
248
+ "description": "Your session ID (from system context). Pass this to track which session closed the task.",
249
+ "default": None,
250
+ },
251
+ "override_justification": {
252
+ "type": "string",
253
+ "description": (
254
+ "Justification for bypassing validation or commit check. "
255
+ "Required when skip_validation=True or no_commit_needed=True. "
256
+ "Example: 'Validation saw truncated diff - verified via git show that commit includes all changes'"
257
+ ),
258
+ "default": None,
259
+ },
260
+ "no_commit_needed": {
261
+ "type": "boolean",
262
+ "description": (
263
+ "ONLY for tasks with NO code changes (pure research, planning, documentation review). "
264
+ "Do NOT use this to bypass validation when a commit exists - use skip_validation instead. "
265
+ "Requires override_justification."
266
+ ),
267
+ "default": False,
268
+ },
269
+ "commit_sha": {
270
+ "type": "string",
271
+ "description": "Git commit SHA to link before closing. Convenience for commit + close in one call.",
272
+ "default": None,
273
+ },
274
+ },
275
+ "required": ["task_id"],
276
+ },
277
+ func=close_task,
278
+ )
279
+
280
+ def reopen_task(task_id: str, reason: str | None = None) -> dict[str, Any]:
281
+ """Reopen a closed or review task.
282
+
283
+ Args:
284
+ task_id: Task reference (#N, path, or UUID)
285
+ reason: Optional reason for reopening
286
+
287
+ Returns:
288
+ Reopened task or error. Resets accepted_by_user to false.
289
+ """
290
+ try:
291
+ resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
292
+ except (TaskNotFoundError, ValueError) as e:
293
+ return {"error": str(e)}
294
+
295
+ try:
296
+ ctx.task_manager.reopen_task(resolved_id, reason=reason)
297
+
298
+ # Reactivate any associated worktrees that were marked merged/abandoned
299
+ try:
300
+ from gobby.storage.worktrees import WorktreeStatus
301
+
302
+ worktree_manager = LocalWorktreeManager(ctx.task_manager.db)
303
+ wt = worktree_manager.get_by_task(resolved_id)
304
+ if wt and wt.status in (
305
+ WorktreeStatus.MERGED.value,
306
+ WorktreeStatus.ABANDONED.value,
307
+ ):
308
+ worktree_manager.update(wt.id, status=WorktreeStatus.ACTIVE.value)
309
+ except Exception:
310
+ pass # nosec B110 - best-effort worktree update
311
+
312
+ return {}
313
+ except ValueError as e:
314
+ return {"error": str(e)}
315
+
316
+ registry.register(
317
+ name="reopen_task",
318
+ description="Reopen a closed task. Clears closed_at, closed_reason, and closed_in_session_id. Optionally appends a reopen reason to the description.",
319
+ input_schema={
320
+ "type": "object",
321
+ "properties": {
322
+ "task_id": {
323
+ "type": "string",
324
+ "description": "Task reference to reopen: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID",
325
+ },
326
+ "reason": {
327
+ "type": "string",
328
+ "description": "Optional reason for reopening the task",
329
+ "default": None,
330
+ },
331
+ },
332
+ "required": ["task_id"],
333
+ },
334
+ func=reopen_task,
335
+ )
336
+
337
+ def delete_task(task_id: str, cascade: bool = True) -> dict[str, Any]:
338
+ """Delete a task and its children by default."""
339
+ try:
340
+ resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
341
+ except (TaskNotFoundError, ValueError) as e:
342
+ return {"error": str(e)}
343
+
344
+ # Get task before deleting to capture seq_num for ref
345
+ task = ctx.task_manager.get_task(resolved_id)
346
+ if not task:
347
+ return {"error": f"Task {task_id} not found"}
348
+ ref = f"#{task.seq_num}" if task.seq_num else resolved_id[:8]
349
+
350
+ deleted = ctx.task_manager.delete_task(resolved_id, cascade=cascade)
351
+ if not deleted:
352
+ return {"error": f"Task {task_id} not found"}
353
+
354
+ return {
355
+ "ref": ref,
356
+ "deleted_task_id": resolved_id, # UUID at end
357
+ }
358
+
359
+ registry.register(
360
+ name="delete_task",
361
+ description="Delete a task and its subtasks.",
362
+ input_schema={
363
+ "type": "object",
364
+ "properties": {
365
+ "task_id": {
366
+ "type": "string",
367
+ "description": "Task reference: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID",
368
+ },
369
+ "cascade": {
370
+ "type": "boolean",
371
+ "description": "If True, delete all child tasks as well. Defaults to True.",
372
+ "default": True,
373
+ },
374
+ },
375
+ "required": ["task_id"],
376
+ },
377
+ func=delete_task,
378
+ )
379
+
380
+ def add_label(task_id: str, label: str) -> dict[str, Any]:
381
+ """Add a label to a task."""
382
+ try:
383
+ resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
384
+ except (TaskNotFoundError, ValueError) as e:
385
+ return {"error": str(e)}
386
+ task = ctx.task_manager.add_label(resolved_id, label)
387
+ if not task:
388
+ return {"error": f"Task {task_id} not found"}
389
+ return {}
390
+
391
+ registry.register(
392
+ name="add_label",
393
+ description="Add a label to a task.",
394
+ input_schema={
395
+ "type": "object",
396
+ "properties": {
397
+ "task_id": {
398
+ "type": "string",
399
+ "description": "Task reference: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID",
400
+ },
401
+ "label": {"type": "string", "description": "Label to add"},
402
+ },
403
+ "required": ["task_id", "label"],
404
+ },
405
+ func=add_label,
406
+ )
407
+
408
+ def remove_label(task_id: str, label: str) -> dict[str, Any]:
409
+ """Remove a label from a task."""
410
+ try:
411
+ resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
412
+ except (TaskNotFoundError, ValueError) as e:
413
+ return {"error": str(e)}
414
+ task = ctx.task_manager.remove_label(resolved_id, label)
415
+ if not task:
416
+ return {"error": f"Task {task_id} not found"}
417
+ return {}
418
+
419
+ registry.register(
420
+ name="remove_label",
421
+ description="Remove a label from a task.",
422
+ input_schema={
423
+ "type": "object",
424
+ "properties": {
425
+ "task_id": {
426
+ "type": "string",
427
+ "description": "Task reference: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID",
428
+ },
429
+ "label": {"type": "string", "description": "Label to remove"},
430
+ },
431
+ "required": ["task_id", "label"],
432
+ },
433
+ func=remove_label,
434
+ )
435
+
436
+ def claim_task(
437
+ task_id: str,
438
+ session_id: str,
439
+ force: bool = False,
440
+ ) -> dict[str, Any]:
441
+ """Claim a task for the current session.
442
+
443
+ Combines setting the assignee and marking as in_progress in a single
444
+ atomic operation. Detects conflicts when another session has already
445
+ claimed the task.
446
+
447
+ Args:
448
+ task_id: Task reference (#N, path, or UUID)
449
+ session_id: Session ID claiming the task
450
+ force: Override existing claim by another session (default: False)
451
+
452
+ Returns:
453
+ Empty dict on success, or error dict with conflict information.
454
+ """
455
+ # Resolve task reference (supports #N, path, UUID formats)
456
+ try:
457
+ resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
458
+ except TaskNotFoundError as e:
459
+ return {"error": str(e)}
460
+ except ValueError as e:
461
+ return {"error": str(e)}
462
+
463
+ task = ctx.task_manager.get_task(resolved_id)
464
+ if not task:
465
+ return {"error": f"Task {task_id} not found"}
466
+
467
+ # Check if already claimed by another session
468
+ if task.assignee and task.assignee != session_id and not force:
469
+ return {
470
+ "error": "Task already claimed by another session",
471
+ "claimed_by": task.assignee,
472
+ "message": f"Task is already claimed by session '{task.assignee}'. Use force=True to override.",
473
+ }
474
+
475
+ # Update task with assignee and status in single atomic call
476
+ updated = ctx.task_manager.update_task(
477
+ resolved_id,
478
+ assignee=session_id,
479
+ status="in_progress",
480
+ )
481
+ if not updated:
482
+ return {"error": f"Failed to claim task {task_id}"}
483
+
484
+ # Link task to session (best-effort, don't fail the claim if this fails)
485
+ try:
486
+ ctx.session_task_manager.link_task(session_id, resolved_id, "claimed")
487
+ except Exception:
488
+ pass # nosec B110 - best-effort linking
489
+
490
+ return {}
491
+
492
+ registry.register(
493
+ name="claim_task",
494
+ description="Claim a task for your session. Sets assignee to session_id and status to in_progress. Detects conflicts if already claimed by another session.",
495
+ input_schema={
496
+ "type": "object",
497
+ "properties": {
498
+ "task_id": {
499
+ "type": "string",
500
+ "description": "Task reference: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID",
501
+ },
502
+ "session_id": {
503
+ "type": "string",
504
+ "description": "Your session ID (from system context). The session claiming the task.",
505
+ },
506
+ "force": {
507
+ "type": "boolean",
508
+ "description": "Override existing claim by another session (default: False)",
509
+ "default": False,
510
+ },
511
+ },
512
+ "required": ["task_id", "session_id"],
513
+ },
514
+ func=claim_task,
515
+ )
516
+
517
+ return registry