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,300 @@
1
+ """Task models, exceptions, and constants.
2
+
3
+ This module contains:
4
+ - Task dataclass with serialization methods
5
+ - Task-related exceptions
6
+ - Priority and category constants
7
+ - Validation and normalization helpers
8
+ """
9
+
10
+ import json
11
+ import sqlite3
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Literal
14
+
15
+ # Priority name to numeric value mapping
16
+ PRIORITY_MAP = {"backlog": 4, "low": 3, "medium": 2, "high": 1, "critical": 0}
17
+
18
+ # Valid task categories (enum-like constraint)
19
+ VALID_CATEGORIES: frozenset[str] = frozenset(
20
+ {
21
+ "code", # Implementation tasks
22
+ "config", # Configuration file changes
23
+ "docs", # Documentation tasks
24
+ "test", # Test infrastructure tasks (fixtures, helpers)
25
+ "refactor", # Refactoring tasks (including updating existing tests)
26
+ "research", # Investigation/exploration tasks
27
+ "planning", # Design/architecture tasks
28
+ "manual", # Manual functional testing (observe output)
29
+ }
30
+ )
31
+
32
+ # Sentinel for unset optional parameters
33
+ UNSET: Any = object()
34
+
35
+
36
+ def validate_category(category: str | None) -> str | None:
37
+ """Validate and normalize a category value.
38
+
39
+ Args:
40
+ category: Category string to validate (case-insensitive)
41
+
42
+ Returns:
43
+ Normalized lowercase category if valid, None otherwise
44
+ """
45
+ if category is None:
46
+ return None
47
+ normalized = category.lower().strip()
48
+ return normalized if normalized in VALID_CATEGORIES else None
49
+
50
+
51
+ def normalize_priority(priority: int | str | None) -> int:
52
+ """Convert priority to numeric value for sorting."""
53
+ if priority is None:
54
+ return 999
55
+ if isinstance(priority, str):
56
+ # Check if it's a named priority
57
+ if priority.lower() in PRIORITY_MAP:
58
+ return PRIORITY_MAP[priority.lower()]
59
+ # Try to parse as int
60
+ try:
61
+ return int(priority)
62
+ except ValueError:
63
+ return 999
64
+ return int(priority)
65
+
66
+
67
+ class TaskIDCollisionError(Exception):
68
+ """Raised when a unique task ID cannot be generated."""
69
+
70
+ pass
71
+
72
+
73
+ class TaskNotFoundError(Exception):
74
+ """Raised when a task reference cannot be resolved to an existing task."""
75
+
76
+ pass
77
+
78
+
79
+ @dataclass
80
+ class Task:
81
+ id: str
82
+ project_id: str
83
+ title: str
84
+ status: Literal[
85
+ "open", "in_progress", "review", "closed", "failed", "escalated", "needs_decomposition"
86
+ ]
87
+ priority: int
88
+ task_type: str # bug, feature, task, epic, chore
89
+ created_at: str
90
+ updated_at: str
91
+ # Optional fields
92
+ description: str | None = None
93
+ parent_task_id: str | None = None
94
+ created_in_session_id: str | None = None
95
+ closed_in_session_id: str | None = None
96
+ closed_commit_sha: str | None = None
97
+ closed_at: str | None = None
98
+ assignee: str | None = None
99
+ labels: list[str] | None = None
100
+ closed_reason: str | None = None
101
+ validation_status: Literal["pending", "valid", "invalid"] | None = None
102
+ validation_feedback: str | None = None
103
+ category: str | None = None
104
+ complexity_score: int | None = None
105
+ estimated_subtasks: int | None = None
106
+ expansion_context: str | None = None
107
+ validation_criteria: str | None = None
108
+ use_external_validator: bool = False
109
+ validation_fail_count: int = 0
110
+ validation_override_reason: str | None = None # Why agent bypassed validation
111
+ # Workflow integration fields
112
+ workflow_name: str | None = None
113
+ verification: str | None = None
114
+ sequence_order: int | None = None
115
+ # Commit linking
116
+ commits: list[str] | None = None
117
+ # Escalation fields
118
+ escalated_at: str | None = None
119
+ escalation_reason: str | None = None
120
+ # GitHub integration fields
121
+ github_issue_number: int | None = None
122
+ github_pr_number: int | None = None
123
+ github_repo: str | None = None
124
+ # Linear integration fields
125
+ linear_issue_id: str | None = None
126
+ linear_team_id: str | None = None
127
+ # Human-friendly ID fields (task renumbering)
128
+ seq_num: int | None = None
129
+ path_cache: str | None = None
130
+ # Agent configuration
131
+ agent_name: str | None = None # Subagent config file to use for this task
132
+ # Spec traceability
133
+ reference_doc: str | None = None # Path to source specification document
134
+ # Processing flags for idempotent operations
135
+ is_expanded: bool = False # Subtasks have been created
136
+ is_tdd_applied: bool = False # TDD pairs have been generated
137
+ # Review status fields (HITL support)
138
+ requires_user_review: bool = False # Task requires user sign-off before closing
139
+ accepted_by_user: bool = False # Set True when user moves review → closed
140
+ # Dependency fields (populated on demand, not stored in tasks table)
141
+ blocked_by: set[str] = field(default_factory=set)
142
+
143
+ @classmethod
144
+ def from_row(cls, row: sqlite3.Row) -> "Task":
145
+ """Convert database row to Task object."""
146
+ labels_json = row["labels"]
147
+ labels = json.loads(labels_json) if labels_json else []
148
+
149
+ # Handle optional columns that might not exist yet if migration pending
150
+ keys = row.keys()
151
+
152
+ return cls(
153
+ id=row["id"],
154
+ project_id=row["project_id"],
155
+ title=row["title"],
156
+ status=row["status"],
157
+ priority=normalize_priority(row["priority"]),
158
+ task_type=row["task_type"],
159
+ created_at=row["created_at"],
160
+ updated_at=row["updated_at"],
161
+ description=row["description"],
162
+ parent_task_id=row["parent_task_id"],
163
+ created_in_session_id=(
164
+ row["created_in_session_id"]
165
+ if "created_in_session_id" in keys
166
+ else (
167
+ row["discovered_in_session_id"] if "discovered_in_session_id" in keys else None
168
+ )
169
+ ),
170
+ closed_in_session_id=(
171
+ row["closed_in_session_id"] if "closed_in_session_id" in keys else None
172
+ ),
173
+ closed_commit_sha=row["closed_commit_sha"] if "closed_commit_sha" in keys else None,
174
+ closed_at=row["closed_at"] if "closed_at" in keys else None,
175
+ assignee=row["assignee"],
176
+ labels=labels,
177
+ closed_reason=row["closed_reason"],
178
+ validation_status=row["validation_status"] if "validation_status" in keys else None,
179
+ validation_feedback=(
180
+ row["validation_feedback"] if "validation_feedback" in keys else None
181
+ ),
182
+ category=row["category"] if "category" in keys else None,
183
+ complexity_score=row["complexity_score"] if "complexity_score" in keys else None,
184
+ estimated_subtasks=row["estimated_subtasks"] if "estimated_subtasks" in keys else None,
185
+ expansion_context=row["expansion_context"] if "expansion_context" in keys else None,
186
+ validation_criteria=(
187
+ row["validation_criteria"] if "validation_criteria" in keys else None
188
+ ),
189
+ use_external_validator=(
190
+ bool(row["use_external_validator"]) if "use_external_validator" in keys else False
191
+ ),
192
+ validation_fail_count=(
193
+ row["validation_fail_count"] if "validation_fail_count" in keys else 0
194
+ ),
195
+ validation_override_reason=(
196
+ row["validation_override_reason"] if "validation_override_reason" in keys else None
197
+ ),
198
+ workflow_name=row["workflow_name"] if "workflow_name" in keys else None,
199
+ verification=row["verification"] if "verification" in keys else None,
200
+ sequence_order=row["sequence_order"] if "sequence_order" in keys else None,
201
+ commits=json.loads(row["commits"]) if "commits" in keys and row["commits"] else None,
202
+ escalated_at=row["escalated_at"] if "escalated_at" in keys else None,
203
+ escalation_reason=row["escalation_reason"] if "escalation_reason" in keys else None,
204
+ github_issue_number=(
205
+ row["github_issue_number"] if "github_issue_number" in keys else None
206
+ ),
207
+ github_pr_number=row["github_pr_number"] if "github_pr_number" in keys else None,
208
+ github_repo=row["github_repo"] if "github_repo" in keys else None,
209
+ linear_issue_id=row["linear_issue_id"] if "linear_issue_id" in keys else None,
210
+ linear_team_id=row["linear_team_id"] if "linear_team_id" in keys else None,
211
+ seq_num=row["seq_num"] if "seq_num" in keys else None,
212
+ path_cache=row["path_cache"] if "path_cache" in keys else None,
213
+ agent_name=row["agent_name"] if "agent_name" in keys else None,
214
+ reference_doc=row["reference_doc"] if "reference_doc" in keys else None,
215
+ is_expanded=bool(row["is_expanded"]) if "is_expanded" in keys else False,
216
+ is_tdd_applied=bool(row["is_tdd_applied"]) if "is_tdd_applied" in keys else False,
217
+ requires_user_review=(
218
+ bool(row["requires_user_review"]) if "requires_user_review" in keys else False
219
+ ),
220
+ accepted_by_user=(
221
+ bool(row["accepted_by_user"]) if "accepted_by_user" in keys else False
222
+ ),
223
+ )
224
+
225
+ def to_dict(self) -> dict[str, Any]:
226
+ """Convert Task to dictionary."""
227
+ return {
228
+ "ref": f"#{self.seq_num}" if self.seq_num else self.id[:8],
229
+ "project_id": self.project_id,
230
+ "title": self.title,
231
+ "status": self.status,
232
+ "priority": self.priority,
233
+ "type": self.task_type, # Use 'type' for API compatibility
234
+ "created_at": self.created_at,
235
+ "updated_at": self.updated_at,
236
+ "description": self.description,
237
+ "parent_task_id": self.parent_task_id,
238
+ "created_in_session_id": self.created_in_session_id,
239
+ "closed_in_session_id": self.closed_in_session_id,
240
+ "closed_commit_sha": self.closed_commit_sha,
241
+ "closed_at": self.closed_at,
242
+ "assignee": self.assignee,
243
+ "labels": self.labels,
244
+ "closed_reason": self.closed_reason,
245
+ "validation_status": self.validation_status,
246
+ "validation_feedback": self.validation_feedback,
247
+ "category": self.category,
248
+ "complexity_score": self.complexity_score,
249
+ "estimated_subtasks": self.estimated_subtasks,
250
+ "expansion_context": self.expansion_context,
251
+ "validation_criteria": self.validation_criteria,
252
+ "use_external_validator": self.use_external_validator,
253
+ "validation_fail_count": self.validation_fail_count,
254
+ "validation_override_reason": self.validation_override_reason,
255
+ "workflow_name": self.workflow_name,
256
+ "verification": self.verification,
257
+ "sequence_order": self.sequence_order,
258
+ "commits": self.commits,
259
+ "escalated_at": self.escalated_at,
260
+ "escalation_reason": self.escalation_reason,
261
+ "github_issue_number": self.github_issue_number,
262
+ "github_pr_number": self.github_pr_number,
263
+ "github_repo": self.github_repo,
264
+ "linear_issue_id": self.linear_issue_id,
265
+ "linear_team_id": self.linear_team_id,
266
+ "seq_num": self.seq_num,
267
+ "path_cache": self.path_cache,
268
+ "agent_name": self.agent_name,
269
+ "reference_doc": self.reference_doc,
270
+ "is_expanded": self.is_expanded,
271
+ "is_tdd_applied": self.is_tdd_applied,
272
+ "requires_user_review": self.requires_user_review,
273
+ "accepted_by_user": self.accepted_by_user,
274
+ "id": self.id, # UUID at end for backwards compat
275
+ }
276
+
277
+ def to_brief(self) -> dict[str, Any]:
278
+ """Convert Task to brief discovery format for list operations.
279
+
280
+ Returns only essential fields needed for task discovery.
281
+ Use get_task() with to_dict() for full task details.
282
+
283
+ This follows the progressive disclosure pattern used for MCP tools:
284
+ - list_tasks() returns brief format (8 fields)
285
+ - get_task() returns full format (33 fields)
286
+ """
287
+ return {
288
+ "ref": f"#{self.seq_num}" if self.seq_num else self.id[:8],
289
+ "title": self.title,
290
+ "status": self.status,
291
+ "priority": self.priority,
292
+ "type": self.task_type,
293
+ "parent_task_id": self.parent_task_id,
294
+ "created_at": self.created_at,
295
+ "updated_at": self.updated_at,
296
+ "seq_num": self.seq_num,
297
+ "path_cache": self.path_cache,
298
+ "requires_user_review": self.requires_user_review,
299
+ "id": self.id, # UUID at end for backwards compat
300
+ }
@@ -0,0 +1,119 @@
1
+ """Hierarchical task ordering utilities.
2
+
3
+ This module provides functions for ordering tasks hierarchically,
4
+ with parents appearing before their children and siblings sorted
5
+ topologically by dependencies.
6
+ """
7
+
8
+ from gobby.storage.tasks._models import Task, normalize_priority
9
+
10
+
11
+ def order_tasks_hierarchically(tasks: list[Task]) -> list[Task]:
12
+ """
13
+ Reorder tasks so parents appear before their children.
14
+
15
+ The ordering is: parent -> children (recursively), then next parent -> children, etc.
16
+ Root tasks (no parent) are sorted by priority ASC, then created_at ASC.
17
+ Children are sorted by priority ASC, then created_at ASC within their parent.
18
+
19
+ Returns a new list with tasks ordered hierarchically.
20
+ """
21
+ if not tasks:
22
+ return []
23
+
24
+ # Build lookup structures
25
+ task_by_id: dict[str, Task] = {t.id: t for t in tasks}
26
+ children_by_parent: dict[str | None, list[Task]] = {}
27
+
28
+ for task in tasks:
29
+ parent_id = task.parent_task_id
30
+ # Only group under parent if parent is in the result set
31
+ if parent_id and parent_id not in task_by_id:
32
+ parent_id = None
33
+ if parent_id not in children_by_parent:
34
+ children_by_parent[parent_id] = []
35
+ children_by_parent[parent_id].append(task)
36
+
37
+ def sort_siblings(siblings: list[Task]) -> list[Task]:
38
+ """Sort siblings topologically with priority tie-breaking."""
39
+ if not siblings:
40
+ return []
41
+
42
+ # 1. Build local dependency graph for these siblings
43
+ sibling_ids = {t.id for t in siblings}
44
+ graph: dict[str, list[str]] = {t.id: [] for t in siblings}
45
+ in_degree: dict[str, int] = {t.id: 0 for t in siblings}
46
+
47
+ for task in siblings:
48
+ # Check who blocks this task (Local dependencies only)
49
+ # task.blocked_by contains IDs of tasks that block 'task'
50
+ # If A blocks B, we want A -> B order.
51
+ # So graph edge is A -> B.
52
+ # task.blocked_by = {A} means B depends on A.
53
+
54
+ for blocker_id in task.blocked_by:
55
+ if blocker_id in sibling_ids:
56
+ graph[blocker_id].append(task.id)
57
+ in_degree[task.id] += 1
58
+
59
+ # 2. Initialize queue with tasks having 0 in-degree (no local blockers)
60
+ # We want to process high priority tasks first among available ones.
61
+ # Priority 0 is highest, so valid sort key is (priority, created_at).
62
+ # We sort the initial list to ensure deterministic order for stable sort
63
+ queue = [t for t in siblings if in_degree[t.id] == 0]
64
+ # Sort queue by priority/created_at so we pop high priority first
65
+ queue.sort(key=lambda t: (normalize_priority(t.priority), t.created_at))
66
+
67
+ sorted_siblings: list[Task] = []
68
+
69
+ while queue:
70
+ # Pop the first (highest priority available)
71
+ current = queue.pop(0)
72
+ sorted_siblings.append(current)
73
+
74
+ # Decrease in-degree of neighbors
75
+ neighbors = graph[current.id]
76
+ # Neighbors might become available. Collect them.
77
+ newly_available = []
78
+ for neighbor_id in neighbors:
79
+ in_degree[neighbor_id] -= 1
80
+ if in_degree[neighbor_id] == 0:
81
+ newly_available.append(task_by_id[neighbor_id])
82
+
83
+ # Sort newly available nodes by priority and add to queue
84
+ # We need to re-sort queue every time or insert in order.
85
+ # Since N is small (siblings usually < 50), simple re-sort of queue is fine.
86
+ newly_available.sort(key=lambda t: (normalize_priority(t.priority), t.created_at))
87
+
88
+ # Add newly available to queue. We want to maintain global order in queue
89
+ # based on priority.
90
+ # Merging two sorted lists is O(N).
91
+ queue.extend(newly_available)
92
+ queue.sort(key=lambda t: (normalize_priority(t.priority), t.created_at))
93
+
94
+ # Check for cycles (remaining nodes with >0 in-degree)
95
+ if len(sorted_siblings) < len(siblings):
96
+ # Cycle detected. Append remaining nodes sorted by priority.
97
+ remaining = [t for t in siblings if t not in sorted_siblings]
98
+ remaining.sort(key=lambda t: (normalize_priority(t.priority), t.created_at))
99
+ sorted_siblings.extend(remaining)
100
+
101
+ return sorted_siblings
102
+
103
+ # Sort children within each parent group
104
+ for parent_id, children in children_by_parent.items():
105
+ children_by_parent[parent_id] = sort_siblings(children)
106
+
107
+ # Build result with DFS traversal
108
+ result: list[Task] = []
109
+
110
+ def add_with_children(task: Task) -> None:
111
+ result.append(task)
112
+ for child in children_by_parent.get(task.id, []):
113
+ add_with_children(child)
114
+
115
+ # Start with root tasks (no parent or parent not in result set)
116
+ for root_task in children_by_parent.get(None, []):
117
+ add_with_children(root_task)
118
+
119
+ return result
@@ -0,0 +1,110 @@
1
+ """Path cache computation and management utilities.
2
+
3
+ This module provides functions for computing and updating task path caches,
4
+ which represent the hierarchical position of a task as a dotted seq_num path.
5
+ """
6
+
7
+ import logging
8
+ from datetime import UTC, datetime
9
+
10
+ from gobby.storage.database import DatabaseProtocol
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def compute_path_cache(db: DatabaseProtocol, task_id: str) -> str | None:
16
+ """Compute the hierarchical path for a task.
17
+
18
+ Traverses up the parent chain to build a dotted path from seq_nums.
19
+ Format: 'ancestor_seq.parent_seq.task_seq' (e.g., '1.3.47')
20
+
21
+ Args:
22
+ db: Database protocol instance
23
+ task_id: The task ID to compute path for
24
+
25
+ Returns:
26
+ Dotted path string (e.g., '1.3.47'), or None if task not found
27
+ or any task in the chain is missing a seq_num.
28
+ """
29
+ # Build path by walking up parent chain
30
+ path_parts: list[str] = []
31
+ current_id: str | None = task_id
32
+
33
+ # Safety limit to prevent infinite loops (max 100 levels deep)
34
+ max_depth = 100
35
+ depth = 0
36
+
37
+ while current_id and depth < max_depth:
38
+ row = db.fetchone(
39
+ "SELECT seq_num, parent_task_id FROM tasks WHERE id = ?",
40
+ (current_id,),
41
+ )
42
+ if not row:
43
+ # Task not found
44
+ return None
45
+
46
+ seq_num = row["seq_num"]
47
+ if seq_num is None:
48
+ # seq_num not yet assigned
49
+ return None
50
+
51
+ path_parts.append(str(seq_num))
52
+ current_id = row["parent_task_id"]
53
+ depth += 1
54
+
55
+ if depth >= max_depth:
56
+ logger.warning(f"Task {task_id} exceeded max depth ({max_depth}) when computing path")
57
+ return None
58
+
59
+ # Reverse to get root-to-leaf order
60
+ path_parts.reverse()
61
+ return ".".join(path_parts)
62
+
63
+
64
+ def update_path_cache(db: DatabaseProtocol, task_id: str) -> str | None:
65
+ """Compute and store the path_cache for a task.
66
+
67
+ Args:
68
+ db: Database protocol instance
69
+ task_id: The task ID to update
70
+
71
+ Returns:
72
+ The computed path, or None if computation failed
73
+ """
74
+ path = compute_path_cache(db, task_id)
75
+ if path is not None:
76
+ now = datetime.now(UTC).isoformat()
77
+ db.execute(
78
+ "UPDATE tasks SET path_cache = ?, updated_at = ? WHERE id = ?",
79
+ (path, now, task_id),
80
+ )
81
+ return path
82
+
83
+
84
+ def update_descendant_paths(db: DatabaseProtocol, task_id: str) -> int:
85
+ """Update path_cache for a task and all its descendants.
86
+
87
+ Use this after reparenting a task to cascade path updates.
88
+
89
+ Args:
90
+ db: Database protocol instance
91
+ task_id: The root task ID to start updating from
92
+
93
+ Returns:
94
+ Number of tasks updated
95
+ """
96
+ updated_count = 0
97
+
98
+ # Update the task itself
99
+ if update_path_cache(db, task_id):
100
+ updated_count += 1
101
+
102
+ # Find and update all descendants (recursive)
103
+ children = db.fetchall(
104
+ "SELECT id FROM tasks WHERE parent_task_id = ?",
105
+ (task_id,),
106
+ )
107
+ for child in children:
108
+ updated_count += update_descendant_paths(db, child["id"])
109
+
110
+ return updated_count