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,685 @@
1
+ """
2
+ CRUD commands for task management.
3
+ """
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ import click
9
+
10
+ from gobby.cli.tasks._utils import (
11
+ collect_ancestors,
12
+ compute_tree_prefixes,
13
+ format_task_header,
14
+ format_task_row,
15
+ get_claimed_task_ids,
16
+ get_task_manager,
17
+ normalize_status,
18
+ resolve_task_id,
19
+ sort_tasks_for_tree,
20
+ )
21
+ from gobby.cli.utils import resolve_project_ref
22
+ from gobby.utils.project_context import get_project_context
23
+
24
+
25
+ @click.command("list")
26
+ @click.option(
27
+ "--status",
28
+ "-s",
29
+ help="Filter by status (open, in_progress, review, closed, blocked). Comma-separated for multiple.",
30
+ )
31
+ @click.option(
32
+ "--active",
33
+ is_flag=True,
34
+ help="Shorthand for --status open,in_progress (all active work)",
35
+ )
36
+ @click.option("--project", "-p", "project_ref", help="Filter by project (name or UUID)")
37
+ @click.option("--assignee", "-a", help="Filter by assignee")
38
+ @click.option(
39
+ "--ready", is_flag=True, help="Show only ready tasks (open/in_progress with no blocking deps)"
40
+ )
41
+ @click.option(
42
+ "--blocked", is_flag=True, help="Show only blocked tasks (open with unresolved blockers)"
43
+ )
44
+ @click.option("--limit", "-l", default=50, help="Max tasks to show")
45
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
46
+ def list_tasks(
47
+ status: str | None,
48
+ active: bool,
49
+ project_ref: str | None,
50
+ assignee: str | None,
51
+ ready: bool,
52
+ blocked: bool,
53
+ limit: int,
54
+ json_format: bool,
55
+ ) -> None:
56
+ """List tasks."""
57
+ if ready and blocked:
58
+ click.echo("Error: --ready and --blocked are mutually exclusive.", err=True)
59
+ return
60
+
61
+ if active and status:
62
+ click.echo("Error: --active and --status are mutually exclusive.", err=True)
63
+ return
64
+
65
+ # Parse comma-separated statuses or use --active shorthand
66
+ # Normalize hyphen-separated status names (e.g., in-progress -> in_progress)
67
+ status_filter: str | list[str] | None = None
68
+ if active:
69
+ status_filter = ["open", "in_progress"]
70
+ elif status:
71
+ if "," in status:
72
+ status_filter = [normalize_status(s.strip()) for s in status.split(",")]
73
+ else:
74
+ status_filter = normalize_status(status)
75
+
76
+ project_id = resolve_project_ref(project_ref)
77
+
78
+ manager = get_task_manager()
79
+
80
+ if ready:
81
+ # Use ready task detection (open/in_progress tasks with no unresolved blocking dependencies)
82
+ tasks_list = manager.list_ready_tasks(
83
+ project_id=project_id,
84
+ assignee=assignee,
85
+ limit=limit,
86
+ )
87
+ label = "ready tasks"
88
+ empty_msg = "No ready tasks found."
89
+ elif blocked:
90
+ # Show tasks that are blocked by unresolved dependencies
91
+ tasks_list = manager.list_blocked_tasks(
92
+ project_id=project_id,
93
+ limit=limit,
94
+ )
95
+ label = "blocked tasks"
96
+ empty_msg = "No blocked tasks found."
97
+ else:
98
+ tasks_list = manager.list_tasks(
99
+ project_id=project_id,
100
+ status=status_filter,
101
+ assignee=assignee,
102
+ limit=limit,
103
+ )
104
+ label = "tasks"
105
+ empty_msg = "No tasks found."
106
+
107
+ if json_format:
108
+ click.echo(json.dumps([t.to_dict() for t in tasks_list], indent=2, default=str))
109
+ return
110
+
111
+ if not tasks_list:
112
+ click.echo(empty_msg)
113
+ return
114
+
115
+ # For filtered views, include ancestors for proper tree hierarchy
116
+ primary_ids: set[str] | None = None
117
+ display_tasks = tasks_list
118
+ if ready or blocked or status_filter:
119
+ display_tasks, primary_ids = collect_ancestors(tasks_list, manager)
120
+
121
+ # Sort for proper tree display order
122
+ display_tasks = sort_tasks_for_tree(display_tasks)
123
+
124
+ # Get tasks claimed by active sessions for indicator display
125
+ claimed_ids = get_claimed_task_ids()
126
+
127
+ click.echo(f"Found {len(tasks_list)} {label}:")
128
+ click.echo(format_task_header())
129
+ prefixes = compute_tree_prefixes(display_tasks, primary_ids)
130
+ for task in display_tasks:
131
+ prefix_info = prefixes.get(task.id, ("", True))
132
+ tree_prefix, is_primary = prefix_info
133
+ click.echo(
134
+ format_task_row(
135
+ task,
136
+ tree_prefix=tree_prefix,
137
+ is_primary=is_primary,
138
+ claimed_task_ids=claimed_ids,
139
+ )
140
+ )
141
+
142
+
143
+ @click.command("ready")
144
+ @click.option("--limit", "-n", default=10, help="Max results")
145
+ @click.option("--project", "-p", "project_ref", help="Filter by project (name or UUID)")
146
+ @click.option("--priority", type=int, help="Filter by priority")
147
+ @click.option("--type", "-t", "task_type", help="Filter by type")
148
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
149
+ @click.option("--flat", is_flag=True, help="Flat list without tree hierarchy")
150
+ def ready_tasks(
151
+ limit: int,
152
+ project_ref: str | None,
153
+ priority: int | None,
154
+ task_type: str | None,
155
+ json_format: bool,
156
+ flat: bool,
157
+ ) -> None:
158
+ """List tasks with no unresolved blocking dependencies."""
159
+ project_id = resolve_project_ref(project_ref)
160
+ manager = get_task_manager()
161
+ tasks_list = manager.list_ready_tasks(
162
+ project_id=project_id,
163
+ priority=priority,
164
+ task_type=task_type,
165
+ limit=limit,
166
+ )
167
+
168
+ if json_format:
169
+ click.echo(json.dumps([t.to_dict() for t in tasks_list], indent=2, default=str))
170
+ return
171
+
172
+ if not tasks_list:
173
+ click.echo("No ready tasks found.")
174
+ return
175
+
176
+ # Get tasks claimed by active sessions for indicator display
177
+ claimed_ids = get_claimed_task_ids()
178
+
179
+ click.echo(f"Found {len(tasks_list)} ready tasks:")
180
+ click.echo(format_task_header())
181
+
182
+ if flat:
183
+ # Simple flat list without tree structure
184
+ for task in tasks_list:
185
+ click.echo(format_task_row(task, claimed_task_ids=claimed_ids))
186
+ else:
187
+ # Include ancestors for proper tree hierarchy
188
+ display_tasks, primary_ids = collect_ancestors(tasks_list, manager)
189
+ display_tasks = sort_tasks_for_tree(display_tasks)
190
+ prefixes = compute_tree_prefixes(display_tasks, primary_ids)
191
+ for task in display_tasks:
192
+ prefix_info = prefixes.get(task.id, ("", True))
193
+ tree_prefix, is_primary = prefix_info
194
+ click.echo(
195
+ format_task_row(
196
+ task,
197
+ tree_prefix=tree_prefix,
198
+ is_primary=is_primary,
199
+ claimed_task_ids=claimed_ids,
200
+ )
201
+ )
202
+
203
+
204
+ @click.command("blocked")
205
+ @click.option("--limit", "-n", default=20, help="Max results")
206
+ @click.option("--project", "-p", "project_ref", help="Filter by project (name or UUID)")
207
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
208
+ def blocked_tasks(limit: int, project_ref: str | None, json_format: bool) -> None:
209
+ """List blocked tasks with what blocks them."""
210
+ from gobby.storage.task_dependencies import TaskDependencyManager
211
+
212
+ project_id = resolve_project_ref(project_ref)
213
+ manager = get_task_manager()
214
+ dep_manager = TaskDependencyManager(manager.db)
215
+ blocked_list = manager.list_blocked_tasks(project_id=project_id, limit=limit)
216
+
217
+ if json_format:
218
+ # Build detailed structure for JSON output
219
+ result = []
220
+ for task in blocked_list:
221
+ tree = dep_manager.get_dependency_tree(task.id)
222
+ result.append(
223
+ {
224
+ "task": task.to_dict(),
225
+ "blocked_by": tree.get("blockers", []),
226
+ }
227
+ )
228
+ click.echo(json.dumps(result, indent=2, default=str))
229
+ return
230
+
231
+ if not blocked_list:
232
+ click.echo("No blocked tasks found.")
233
+ return
234
+
235
+ click.echo(f"Found {len(blocked_list)} blocked tasks:")
236
+ for task in blocked_list:
237
+ tree = dep_manager.get_dependency_tree(task.id)
238
+ blocker_ids = tree.get("blockers", [])
239
+ click.echo(f"\n○ {task.id[:8]}: {task.title}")
240
+ if blocker_ids:
241
+ click.echo(" Blocked by:")
242
+ for b in blocker_ids:
243
+ blocker_id = b.get("id") if isinstance(b, dict) else b
244
+ if not blocker_id or not isinstance(blocker_id, str):
245
+ continue
246
+
247
+ # Explicit cast to satisfy linter
248
+ bid: str = blocker_id
249
+
250
+ try:
251
+ blocker_task = manager.get_task(bid)
252
+ status_icon = "✓" if blocker_task.status == "closed" else "○"
253
+ click.echo(f" {status_icon} {bid[:8]}: {blocker_task.title}")
254
+ except Exception:
255
+ click.echo(f" ? {bid[:8]}: (not found)")
256
+
257
+
258
+ @click.command("stats")
259
+ @click.option("--project", "-p", "project_ref", help="Filter by project (name or UUID)")
260
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
261
+ def task_stats(project_ref: str | None, json_format: bool) -> None:
262
+ """Show task statistics."""
263
+ project_id = resolve_project_ref(project_ref)
264
+ manager = get_task_manager()
265
+
266
+ # Get counts by status
267
+ all_tasks = manager.list_tasks(project_id=project_id, limit=10000)
268
+ total = len(all_tasks)
269
+ by_status = {"open": 0, "in_progress": 0, "review": 0, "closed": 0}
270
+ by_priority = {1: 0, 2: 0, 3: 0}
271
+ by_type: dict[str, int] = {}
272
+
273
+ for task in all_tasks:
274
+ by_status[task.status] = by_status.get(task.status, 0) + 1
275
+ if task.priority:
276
+ by_priority[task.priority] = by_priority.get(task.priority, 0) + 1
277
+ if task.task_type:
278
+ by_type[task.task_type] = by_type.get(task.task_type, 0) + 1
279
+
280
+ # Get ready and blocked counts
281
+ ready_count = len(manager.list_ready_tasks(project_id=project_id, limit=10000))
282
+ blocked_count = len(manager.list_blocked_tasks(project_id=project_id, limit=10000))
283
+
284
+ stats = {
285
+ "total": total,
286
+ "by_status": by_status,
287
+ "by_priority": {
288
+ "high": by_priority.get(1, 0),
289
+ "medium": by_priority.get(2, 0),
290
+ "low": by_priority.get(3, 0),
291
+ },
292
+ "by_type": by_type,
293
+ "ready": ready_count,
294
+ "blocked": blocked_count,
295
+ }
296
+
297
+ if json_format:
298
+ click.echo(json.dumps(stats, indent=2))
299
+ return
300
+
301
+ click.echo("Task Statistics:")
302
+ click.echo(f" Total: {total}")
303
+ click.echo(f" Open: {by_status.get('open', 0)}")
304
+ click.echo(f" In Progress: {by_status.get('in_progress', 0)}")
305
+ click.echo(f" Review: {by_status.get('review', 0)}")
306
+ click.echo(f" Closed: {by_status.get('closed', 0)}")
307
+ click.echo(f"\n Ready (no blockers): {ready_count}")
308
+ click.echo(f" Blocked: {blocked_count}")
309
+ click.echo(f"\n High Priority: {by_priority.get(1, 0)}")
310
+ click.echo(f" Medium Priority: {by_priority.get(2, 0)}")
311
+ click.echo(f" Low Priority: {by_priority.get(3, 0)}")
312
+ if by_type:
313
+ click.echo("\n By Type:")
314
+ for t, count in sorted(by_type.items(), key=lambda x: -x[1]):
315
+ click.echo(f" {t}: {count}")
316
+
317
+
318
+ @click.command("create")
319
+ @click.argument("title")
320
+ @click.option("--description", "-d", help="Task description")
321
+ @click.option("--priority", "-p", type=int, default=2, help="Priority (1=High, 2=Med, 3=Low)")
322
+ @click.option("--type", "-t", "task_type", default="task", help="Task type")
323
+ def create_task(title: str, description: str | None, priority: int, task_type: str) -> None:
324
+ """Create a new task."""
325
+ project_ctx = get_project_context()
326
+ if not project_ctx or "id" not in project_ctx:
327
+ click.echo("Error: Not in a gobby project or project.json missing 'id'.", err=True)
328
+ return
329
+
330
+ manager = get_task_manager()
331
+ task = manager.create_task(
332
+ project_id=project_ctx["id"],
333
+ title=title,
334
+ description=description,
335
+ priority=priority,
336
+ task_type=task_type,
337
+ )
338
+ task_ref = f"#{task.seq_num}" if task.seq_num else task.id[:8]
339
+ project_name = project_ctx.get("name") if project_ctx else None
340
+
341
+ if project_name and task.seq_num:
342
+ click.echo(f"Created task {project_name}-#{task.seq_num}: {task.title}")
343
+ else:
344
+ click.echo(f"Created task {task_ref}: {task.title}")
345
+
346
+
347
+ @click.command("show")
348
+ @click.argument("task_id", metavar="TASK")
349
+ def show_task(task_id: str) -> None:
350
+ """Show details for a task.
351
+
352
+ TASK can be: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID.
353
+ """
354
+ manager = get_task_manager()
355
+ task = resolve_task_id(manager, task_id)
356
+
357
+ if not task:
358
+ return
359
+
360
+ click.echo(f"Task: {task.title}")
361
+ click.echo(f"ID: {task.id}")
362
+ if task.seq_num:
363
+ click.echo(f"Ref: #{task.seq_num}")
364
+ click.echo(f"Status: {task.status}")
365
+ click.echo(f"Priority: {task.priority}")
366
+ click.echo(f"Type: {task.task_type}")
367
+ click.echo(f"Created: {task.created_at}")
368
+ click.echo(f"Updated: {task.updated_at}")
369
+ if task.assignee:
370
+ click.echo(f"Assignee: {task.assignee}")
371
+ if task.labels:
372
+ click.echo(f"Labels: {', '.join(task.labels)}")
373
+ if task.description:
374
+ click.echo(f"\n{task.description}")
375
+
376
+
377
+ @click.command("update")
378
+ @click.argument("task_id", metavar="TASK")
379
+ @click.option("--title", "-T", help="New title")
380
+ @click.option("--status", "-s", help="New status")
381
+ @click.option("--priority", type=int, help="New priority")
382
+ @click.option("--assignee", "-a", help="New assignee")
383
+ @click.option("--parent", "parent_task_id", help="Parent task (#N, path, or UUID)")
384
+ def update_task(
385
+ task_id: str,
386
+ title: str | None,
387
+ status: str | None,
388
+ priority: int | None,
389
+ assignee: str | None,
390
+ parent_task_id: str | None,
391
+ ) -> None:
392
+ """Update a task.
393
+
394
+ TASK can be: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID.
395
+ """
396
+ manager = get_task_manager()
397
+ resolved = resolve_task_id(manager, task_id)
398
+ if not resolved:
399
+ return
400
+
401
+ # Resolve parent task ID if provided
402
+ resolved_parent_id = None
403
+ if parent_task_id:
404
+ resolved_parent = resolve_task_id(manager, parent_task_id)
405
+ if not resolved_parent:
406
+ return
407
+ resolved_parent_id = resolved_parent.id
408
+
409
+ # Only pass parameters that were explicitly provided (not None)
410
+ # to avoid setting NOT NULL fields to NULL
411
+ kwargs: dict[str, Any] = {}
412
+ if title is not None:
413
+ kwargs["title"] = title
414
+ if status is not None:
415
+ kwargs["status"] = status
416
+ if priority is not None:
417
+ kwargs["priority"] = priority
418
+ if assignee is not None:
419
+ kwargs["assignee"] = assignee
420
+ if resolved_parent_id is not None:
421
+ kwargs["parent_task_id"] = resolved_parent_id
422
+
423
+ task = manager.update_task(resolved.id, **kwargs)
424
+
425
+ # Use standardized ref
426
+ task_ref = f"#{task.seq_num}" if task.seq_num else task.id[:8]
427
+ click.echo(f"Updated task {task_ref}")
428
+
429
+
430
+ @click.command("close")
431
+ @click.argument("task_ids", metavar="TASK", nargs=-1, required=True)
432
+ @click.option("--reason", "-r", default="completed", help="Reason for closing")
433
+ @click.option("--skip-validation", is_flag=True, help="Skip validation checks")
434
+ @click.option("--force", "-f", is_flag=True, help="Alias for --skip-validation")
435
+ def close_task_cmd(
436
+ task_ids: tuple[str, ...], reason: str, skip_validation: bool, force: bool
437
+ ) -> None:
438
+ """Close one or more tasks.
439
+
440
+ TASK can be: #N (e.g., #1, #47), seq_num (e.g., 47), path (e.g., 1.2.3), or UUID.
441
+ Multiple tasks can be specified separated by spaces or commas.
442
+
443
+ Examples:
444
+ gobby tasks close #42
445
+ gobby tasks close 42 43 44
446
+ gobby tasks close abc123,#45,46
447
+
448
+ Parent tasks require all children to be closed first.
449
+ Use --skip-validation or --force for wont_fix, duplicate, etc.
450
+ """
451
+ manager = get_task_manager()
452
+ skip = skip_validation or force
453
+
454
+ # Expand comma-separated values into individual IDs
455
+ expanded_ids: list[str] = []
456
+ for task_id in task_ids:
457
+ if "," in task_id:
458
+ expanded_ids.extend(part.strip() for part in task_id.split(",") if part.strip())
459
+ else:
460
+ expanded_ids.append(task_id)
461
+
462
+ closed_count = 0
463
+ failed_count = 0
464
+
465
+ for task_id in expanded_ids:
466
+ resolved = resolve_task_id(manager, task_id)
467
+ if not resolved:
468
+ failed_count += 1
469
+ continue
470
+
471
+ if not skip:
472
+ # Check if task has children (is a parent task)
473
+ children = manager.list_tasks(parent_task_id=resolved.id, limit=1000)
474
+
475
+ if children:
476
+ # Parent task: must have all children closed
477
+ open_children = [c for c in children if c.status != "closed"]
478
+ if open_children:
479
+ task_ref = f"#{resolved.seq_num}" if resolved.seq_num else resolved.id[:8]
480
+ click.echo(
481
+ f"Cannot close {task_ref}: {len(open_children)} child tasks still open",
482
+ err=True,
483
+ )
484
+ failed_count += 1
485
+ continue
486
+
487
+ task = manager.close_task(resolved.id, reason=reason)
488
+
489
+ # Use standardized ref
490
+ task_ref = f"#{task.seq_num}" if task.seq_num else task.id[:8]
491
+ click.echo(f"Closed task {task_ref} ({reason})")
492
+ closed_count += 1
493
+
494
+ # Summary if multiple tasks were processed
495
+ if len(expanded_ids) > 1:
496
+ if failed_count > 0:
497
+ click.echo(f"\nClosed {closed_count}/{len(expanded_ids)} tasks ({failed_count} failed)")
498
+ else:
499
+ click.echo(f"\nClosed {closed_count} tasks")
500
+
501
+
502
+ @click.command("reopen")
503
+ @click.argument("task_id", metavar="TASK")
504
+ @click.option("--reason", "-r", default=None, help="Reason for reopening")
505
+ def reopen_task_cmd(task_id: str, reason: str | None) -> None:
506
+ """Reopen a closed or review task.
507
+
508
+ TASK can be: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID.
509
+
510
+ Sets status back to 'open', clears closed_at/closed_reason, and resets
511
+ accepted_by_user to false.
512
+ """
513
+ manager = get_task_manager()
514
+ resolved = resolve_task_id(manager, task_id)
515
+ if not resolved:
516
+ return
517
+
518
+ # Use standardized ref for errors
519
+ resolved_ref = f"#{resolved.seq_num}" if resolved.seq_num else resolved.id[:8]
520
+
521
+ if resolved.status not in ("closed", "review"):
522
+ click.echo(
523
+ f"Task {resolved_ref} is not closed or in review (status: {resolved.status})", err=True
524
+ )
525
+ return
526
+
527
+ task = manager.reopen_task(resolved.id, reason=reason)
528
+
529
+ # Use standardized ref
530
+ task_ref = f"#{task.seq_num}" if task.seq_num else task.id[:8]
531
+
532
+ if reason:
533
+ click.echo(f"Reopened task {task_ref} ({reason})")
534
+ else:
535
+ click.echo(f"Reopened task {task_ref}")
536
+
537
+
538
+ @click.command("delete")
539
+ @click.argument("task_refs", nargs=-1, required=True, metavar="TASKS...")
540
+ @click.option("--cascade", "-c", is_flag=True, help="Delete child tasks")
541
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
542
+ def delete_task(task_refs: tuple[str, ...], cascade: bool, yes: bool) -> None:
543
+ """Delete one or more tasks.
544
+
545
+ TASKS can be: #N (e.g., #1, #47), comma-separated (#1,#2,#3), or UUIDs.
546
+ Multiple tasks can be specified separated by spaces or commas.
547
+
548
+ Examples:
549
+ gobby tasks delete #42
550
+ gobby tasks delete #42,#43,#44 --cascade
551
+ gobby tasks delete #42 #43 #44 --yes
552
+ """
553
+ from gobby.cli.tasks._utils import parse_task_refs
554
+
555
+ manager = get_task_manager()
556
+
557
+ # Parse and resolve all task refs
558
+ all_refs = parse_task_refs(task_refs)
559
+ resolved_tasks = []
560
+ for ref in all_refs:
561
+ resolved = resolve_task_id(manager, ref)
562
+ if resolved:
563
+ resolved_tasks.append((ref, resolved))
564
+
565
+ if not resolved_tasks:
566
+ return
567
+
568
+ # Confirm deletion
569
+ if not yes:
570
+ task_list = ", ".join(ref for ref, _ in resolved_tasks)
571
+ if not click.confirm(f"Delete {len(resolved_tasks)} task(s): {task_list}?"):
572
+ click.echo("Cancelled.")
573
+ return
574
+
575
+ # Delete tasks
576
+ deleted = 0
577
+ for ref, resolved in resolved_tasks:
578
+ try:
579
+ manager.delete_task(resolved.id, cascade=cascade)
580
+ click.echo(f"Deleted task {resolved.id}")
581
+ deleted += 1
582
+ except ValueError as e:
583
+ msg = str(e)
584
+ if "has children" in msg and "cascade=True" in msg:
585
+ msg = f"Task {ref} has children. Use --cascade to delete with all subtasks."
586
+ click.echo(f"Error: {msg}", err=True)
587
+
588
+ if len(resolved_tasks) > 1:
589
+ click.echo(f"\nDeleted {deleted}/{len(resolved_tasks)} tasks.")
590
+
591
+
592
+ @click.command("de-escalate")
593
+ @click.argument("task_id", metavar="TASK")
594
+ @click.option("--reason", "-r", required=True, help="Reason for de-escalation")
595
+ @click.option("--reset-validation", is_flag=True, help="Reset validation fail count")
596
+ def de_escalate_cmd(task_id: str, reason: str, reset_validation: bool) -> None:
597
+ """Return an escalated task to open status.
598
+
599
+ TASK can be: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID.
600
+
601
+ Use after human intervention resolves the issue that caused escalation.
602
+ """
603
+ manager = get_task_manager()
604
+ resolved = resolve_task_id(manager, task_id)
605
+ if not resolved:
606
+ return
607
+
608
+ if resolved.status != "escalated":
609
+ click.echo(
610
+ f"Task {resolved.id[:8]} is not escalated (status: {resolved.status})",
611
+ err=True,
612
+ )
613
+ return
614
+
615
+ # Build update kwargs
616
+ update_kwargs: dict[str, str | int | None] = {
617
+ "status": "open",
618
+ "escalated_at": None,
619
+ "escalation_reason": None,
620
+ }
621
+ if reset_validation:
622
+ update_kwargs["validation_fail_count"] = 0
623
+
624
+ manager.update_task(resolved.id, **update_kwargs)
625
+ click.echo(f"De-escalated task {resolved.id[:8]} ({reason})")
626
+ if reset_validation:
627
+ click.echo(" Validation fail count reset to 0")
628
+
629
+
630
+ @click.command("validation-history")
631
+ @click.argument("task_id", metavar="TASK")
632
+ @click.option("--clear", is_flag=True, help="Clear validation history")
633
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
634
+ def validation_history_cmd(task_id: str, clear: bool, json_format: bool) -> None:
635
+ """View or clear validation history for a task.
636
+
637
+ TASK can be: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID.
638
+ """
639
+ from gobby.tasks.validation_history import ValidationHistoryManager
640
+
641
+ manager = get_task_manager()
642
+ resolved = resolve_task_id(manager, task_id)
643
+ if not resolved:
644
+ return
645
+
646
+ history_manager = ValidationHistoryManager(manager.db)
647
+
648
+ if clear:
649
+ history_manager.clear_history(resolved.id)
650
+ manager.update_task(resolved.id, validation_fail_count=0)
651
+ click.echo(f"Cleared validation history for {resolved.id[:8]}")
652
+ return
653
+
654
+ iterations = history_manager.get_iteration_history(resolved.id)
655
+
656
+ if json_format:
657
+ result = {
658
+ "task_id": resolved.id,
659
+ "iterations": [
660
+ {
661
+ "iteration": it.iteration,
662
+ "status": it.status,
663
+ "feedback": it.feedback,
664
+ "issues": [i.to_dict() for i in (it.issues or [])],
665
+ "created_at": it.created_at,
666
+ }
667
+ for it in iterations
668
+ ],
669
+ }
670
+ click.echo(json.dumps(result, indent=2, default=str))
671
+ return
672
+
673
+ if not iterations:
674
+ click.echo(f"No validation history for task {resolved.id[:8]}")
675
+ return
676
+
677
+ click.echo(f"Validation history for {resolved.id[:8]}:")
678
+ for it in iterations:
679
+ click.echo(f"\n Iteration {it.iteration}: {it.status}")
680
+ if it.feedback:
681
+ feedback_preview = it.feedback[:100] + "..." if len(it.feedback) > 100 else it.feedback
682
+ click.echo(f" Feedback: {feedback_preview}")
683
+ if it.issues:
684
+ click.echo(f" Issues: {len(it.issues)}")
685
+ click.echo(f" Created: {it.created_at}")