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,342 @@
1
+ """
2
+ Criteria generation for task expansion.
3
+
4
+ This module provides:
5
+ - PatternCriteriaInjector: Detects patterns from labels/descriptions and injects criteria
6
+ - CriteriaGenerator: Shared criteria generator for both structured and LLM expansion
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import re
13
+ from typing import TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ from gobby.config.app import PatternCriteriaConfig, ProjectVerificationConfig
17
+ from gobby.storage.tasks import Task
18
+ from gobby.tasks.context import ExpansionContext
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class PatternCriteriaInjector:
24
+ """Injects pattern-specific validation criteria into task expansion.
25
+
26
+ Detects patterns from:
27
+ - Task labels (e.g., 'strangler-fig', 'tdd')
28
+ - Description keywords (e.g., 'using strangler fig pattern')
29
+
30
+ Then injects appropriate criteria templates with placeholders replaced
31
+ by actual verification commands from project config.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ pattern_config: PatternCriteriaConfig,
37
+ verification_config: ProjectVerificationConfig | None = None,
38
+ ):
39
+ """Initialize the injector.
40
+
41
+ Args:
42
+ pattern_config: Pattern criteria configuration with templates
43
+ verification_config: Project verification commands for placeholder substitution
44
+ """
45
+ self.pattern_config = pattern_config
46
+ self.verification_config = verification_config
47
+
48
+ def detect_patterns(
49
+ self,
50
+ task: Task,
51
+ labels: list[str] | None = None,
52
+ ) -> list[str]:
53
+ """Detect which patterns apply to a task.
54
+
55
+ Args:
56
+ task: The task to analyze
57
+ labels: Optional explicit labels (overrides task.labels)
58
+
59
+ Returns:
60
+ List of detected pattern names
61
+ """
62
+ detected: list[str] = []
63
+ task_labels = labels if labels is not None else (task.labels or [])
64
+
65
+ # Normalize labels to lowercase for matching
66
+ normalized_labels = [label.lower() for label in task_labels]
67
+
68
+ # Check labels first (direct match)
69
+ for pattern_name in self.pattern_config.patterns.keys():
70
+ if pattern_name.lower() in normalized_labels:
71
+ if pattern_name not in detected:
72
+ detected.append(pattern_name)
73
+ logger.debug(f"Detected pattern '{pattern_name}' from task labels")
74
+
75
+ # Check description keywords
76
+ description = (task.description or "").lower()
77
+ title = (task.title or "").lower()
78
+ text_to_search = f"{title} {description}"
79
+
80
+ for pattern_name, keywords in self.pattern_config.detection_keywords.items():
81
+ if pattern_name in detected:
82
+ continue # Already detected from labels
83
+
84
+ for keyword in keywords:
85
+ if keyword.lower() in text_to_search:
86
+ detected.append(pattern_name)
87
+ logger.debug(
88
+ f"Detected pattern '{pattern_name}' from keyword '{keyword}' in description"
89
+ )
90
+ break
91
+
92
+ return detected
93
+
94
+ def _get_placeholder_values(self) -> dict[str, str]:
95
+ """Get placeholder values from verification config.
96
+
97
+ Returns:
98
+ Dict mapping placeholder names to their values
99
+ """
100
+ values: dict[str, str] = {}
101
+
102
+ if self.verification_config:
103
+ if self.verification_config.unit_tests:
104
+ values["unit_tests"] = self.verification_config.unit_tests
105
+ if self.verification_config.type_check:
106
+ values["type_check"] = self.verification_config.type_check
107
+ if self.verification_config.lint:
108
+ values["lint"] = self.verification_config.lint
109
+ if self.verification_config.integration:
110
+ values["integration"] = self.verification_config.integration
111
+
112
+ # Add any custom verification commands
113
+ for name, cmd in self.verification_config.custom.items():
114
+ values[name] = cmd
115
+
116
+ return values
117
+
118
+ def _substitute_placeholders(
119
+ self,
120
+ template: str,
121
+ extra_values: dict[str, str] | None = None,
122
+ ) -> str:
123
+ """Substitute placeholders in a template string.
124
+
125
+ Args:
126
+ template: Template string with {placeholder} syntax
127
+ extra_values: Additional values to substitute
128
+
129
+ Returns:
130
+ String with placeholders replaced
131
+ """
132
+ values = self._get_placeholder_values()
133
+ if extra_values:
134
+ values.update(extra_values)
135
+
136
+ # Use a regex to find placeholders and replace only those we have values for
137
+ def replace_placeholder(match: re.Match[str]) -> str:
138
+ key = match.group(1)
139
+ return values.get(key, match.group(0)) # Keep original if no value
140
+
141
+ return re.sub(r"\{(\w+)\}", replace_placeholder, template)
142
+
143
+ def inject(
144
+ self,
145
+ task: Task,
146
+ context: ExpansionContext | None = None,
147
+ labels: list[str] | None = None,
148
+ extra_placeholders: dict[str, str] | None = None,
149
+ ) -> str:
150
+ """Generate pattern-specific criteria markdown for a task.
151
+
152
+ Args:
153
+ task: The task to generate criteria for
154
+ context: Optional expansion context (unused currently, for future extensibility)
155
+ labels: Optional explicit labels (overrides task.labels)
156
+ extra_placeholders: Additional placeholder values for substitution
157
+
158
+ Returns:
159
+ Markdown-formatted criteria string, empty if no patterns detected
160
+ """
161
+ detected_patterns = self.detect_patterns(task, labels)
162
+
163
+ if not detected_patterns:
164
+ return ""
165
+
166
+ sections: list[str] = []
167
+
168
+ for pattern_name in detected_patterns:
169
+ templates = self.pattern_config.patterns.get(pattern_name, [])
170
+ if not templates:
171
+ continue
172
+
173
+ criteria_lines: list[str] = []
174
+ for template in templates:
175
+ substituted = self._substitute_placeholders(template, extra_placeholders)
176
+ criteria_lines.append(f"- [ ] {substituted}")
177
+
178
+ if criteria_lines:
179
+ # Format pattern name nicely (e.g., "strangler-fig" -> "Strangler-Fig Pattern")
180
+ pattern_title = pattern_name.replace("-", " ").title()
181
+ section = f"## {pattern_title} Pattern Criteria\n\n" + "\n".join(criteria_lines)
182
+ sections.append(section)
183
+
184
+ return "\n\n".join(sections)
185
+
186
+ def inject_for_labels(
187
+ self,
188
+ labels: list[str],
189
+ task: Task | None = None,
190
+ extra_placeholders: dict[str, str] | None = None,
191
+ ) -> str:
192
+ """Generate criteria based on explicit labels without a full task.
193
+
194
+ This is a convenience method when you only have labels and optionally a task.
195
+
196
+ Args:
197
+ labels: List of pattern labels
198
+ task: Optional task for additional context
199
+ extra_placeholders: Additional placeholder values
200
+
201
+ Returns:
202
+ Markdown-formatted criteria string
203
+ """
204
+ if task:
205
+ return self.inject(task, labels=labels, extra_placeholders=extra_placeholders)
206
+
207
+ # Create minimal criteria from labels only
208
+ sections: list[str] = []
209
+ normalized_labels = [label.lower() for label in labels]
210
+
211
+ for pattern_name in self.pattern_config.patterns.keys():
212
+ if pattern_name.lower() not in normalized_labels:
213
+ continue
214
+
215
+ templates = self.pattern_config.patterns.get(pattern_name, [])
216
+ if not templates:
217
+ continue
218
+
219
+ criteria_lines: list[str] = []
220
+ for template in templates:
221
+ substituted = self._substitute_placeholders(template, extra_placeholders)
222
+ criteria_lines.append(f"- [ ] {substituted}")
223
+
224
+ if criteria_lines:
225
+ pattern_title = pattern_name.replace("-", " ").title()
226
+ section = f"## {pattern_title} Pattern Criteria\n\n" + "\n".join(criteria_lines)
227
+ sections.append(section)
228
+
229
+ return "\n\n".join(sections)
230
+
231
+
232
+ class CriteriaGenerator:
233
+ """Shared criteria generator for both structured and LLM expansion.
234
+
235
+ Generates validation criteria by combining:
236
+ - Pattern-specific criteria (from labels/description)
237
+ - Verification command criteria (from project config)
238
+ - File-specific criteria (when relevant files provided)
239
+
240
+ Can be used by:
241
+ - TaskExpander (LLM expansion)
242
+ - TaskHierarchyBuilder (structured expansion)
243
+ """
244
+
245
+ def __init__(
246
+ self,
247
+ pattern_config: PatternCriteriaConfig,
248
+ verification_config: ProjectVerificationConfig | None = None,
249
+ ):
250
+ """Initialize the generator.
251
+
252
+ Args:
253
+ pattern_config: Pattern criteria configuration with templates
254
+ verification_config: Project verification commands configuration
255
+ """
256
+ self.pattern_injector = PatternCriteriaInjector(
257
+ pattern_config=pattern_config,
258
+ verification_config=verification_config,
259
+ )
260
+ self.verification_config = verification_config
261
+
262
+ def generate(
263
+ self,
264
+ title: str,
265
+ description: str | None = None,
266
+ labels: list[str] | None = None,
267
+ relevant_files: list[str] | None = None,
268
+ verification_commands: dict[str, str] | None = None,
269
+ ) -> str:
270
+ """Generate validation criteria markdown.
271
+
272
+ Args:
273
+ title: Task title
274
+ description: Task description
275
+ labels: Optional labels for pattern detection
276
+ relevant_files: Optional list of relevant file paths
277
+ verification_commands: Optional verification commands override
278
+
279
+ Returns:
280
+ Markdown-formatted validation criteria string
281
+ """
282
+ criteria_parts: list[str] = []
283
+
284
+ # 1. Pattern-specific criteria from labels
285
+ if labels:
286
+ pattern_criteria = self.pattern_injector.inject_for_labels(
287
+ labels=labels,
288
+ extra_placeholders=verification_commands,
289
+ )
290
+ if pattern_criteria:
291
+ criteria_parts.append(pattern_criteria)
292
+
293
+ # 2. File-specific criteria
294
+ if relevant_files and description:
295
+ text_to_check = (title + " " + (description or "")).lower()
296
+ matching_files = [f for f in relevant_files if f.lower() in text_to_check]
297
+ if matching_files:
298
+ file_criteria = ["## File Requirements", ""]
299
+ for f in matching_files:
300
+ file_criteria.append(f"- [ ] `{f}` is correctly modified/created")
301
+ criteria_parts.append("\n".join(file_criteria))
302
+
303
+ # 3. Verification command criteria
304
+ verification = self._get_verification_commands(verification_commands)
305
+ if verification:
306
+ verification_criteria = ["## Verification", ""]
307
+ for name, cmd in verification.items():
308
+ if name in ["unit_tests", "type_check", "lint"]:
309
+ verification_criteria.append(f"- [ ] `{cmd}` passes")
310
+ if len(verification_criteria) > 2: # Has items beyond header
311
+ criteria_parts.append("\n".join(verification_criteria))
312
+
313
+ return "\n\n".join(criteria_parts) if criteria_parts else ""
314
+
315
+ def _get_verification_commands(
316
+ self,
317
+ override: dict[str, str] | None = None,
318
+ ) -> dict[str, str]:
319
+ """Get verification commands from config or override.
320
+
321
+ Args:
322
+ override: Optional override commands
323
+
324
+ Returns:
325
+ Dict of verification command name -> command
326
+ """
327
+ if override:
328
+ return override
329
+
330
+ commands: dict[str, str] = {}
331
+ if self.verification_config:
332
+ if self.verification_config.unit_tests:
333
+ commands["unit_tests"] = self.verification_config.unit_tests
334
+ if self.verification_config.type_check:
335
+ commands["type_check"] = self.verification_config.type_check
336
+ if self.verification_config.lint:
337
+ commands["lint"] = self.verification_config.lint
338
+ if self.verification_config.integration:
339
+ commands["integration"] = self.verification_config.integration
340
+ for name, cmd in self.verification_config.custom.items():
341
+ commands[name] = cmd
342
+ return commands
@@ -0,0 +1,226 @@
1
+ """Enhanced task validator with retry and escalation logic.
2
+
3
+ Provides the core validation loop for Task System V2.
4
+ """
5
+
6
+ import logging
7
+ from dataclasses import dataclass, field
8
+ from enum import Enum
9
+ from typing import TYPE_CHECKING, Any, Protocol
10
+
11
+ from gobby.tasks.validation_models import Issue
12
+
13
+ if TYPE_CHECKING:
14
+ from gobby.tasks.validation_history import ValidationHistoryManager
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class EscalationReason(Enum):
20
+ """Reasons for escalating a validation failure."""
21
+
22
+ MAX_ITERATIONS = "max_iterations"
23
+ CONSECUTIVE_ERRORS = "consecutive_errors"
24
+ RECURRING_ISSUES = "recurring_issues"
25
+
26
+
27
+ @dataclass
28
+ class ValidationResult:
29
+ """Result of a validation attempt.
30
+
31
+ Attributes:
32
+ valid: Whether the task passed validation.
33
+ iterations: Number of validation iterations performed.
34
+ feedback: Human-readable feedback from the validator.
35
+ escalated: Whether the validation was escalated.
36
+ escalation_reason: Reason for escalation if escalated.
37
+ issues: List of issues found during validation.
38
+ """
39
+
40
+ valid: bool
41
+ iterations: int
42
+ feedback: str
43
+ escalated: bool = False
44
+ escalation_reason: EscalationReason | None = None
45
+ issues: list[Issue] = field(default_factory=list)
46
+
47
+
48
+ class LLMValidator(Protocol):
49
+ """Protocol for LLM-based validators."""
50
+
51
+ async def validate(
52
+ self,
53
+ task: Any,
54
+ context: dict[str, Any] | None = None,
55
+ ) -> dict[str, Any]:
56
+ """Validate a task.
57
+
58
+ Args:
59
+ task: The task to validate.
60
+ context: Optional context for validation.
61
+
62
+ Returns:
63
+ Dictionary with keys:
64
+ - valid: bool
65
+ - feedback: str
66
+ - issues: list[dict]
67
+ """
68
+ ...
69
+
70
+
71
+ class TaskManager(Protocol):
72
+ """Protocol for task managers."""
73
+
74
+ def get_task(self, task_id: str) -> Any:
75
+ """Get a task by ID."""
76
+ ...
77
+
78
+
79
+ class EnhancedTaskValidator:
80
+ """Enhanced task validator with retry and escalation logic.
81
+
82
+ Implements a validation loop that:
83
+ - Retries validation up to max_iterations
84
+ - Tracks validation history
85
+ - Detects recurring issues
86
+ - Escalates when thresholds are exceeded
87
+ """
88
+
89
+ def __init__(
90
+ self,
91
+ task_manager: TaskManager,
92
+ history_manager: "ValidationHistoryManager",
93
+ llm_validator: LLMValidator,
94
+ max_iterations: int = 3,
95
+ error_threshold: int = 2,
96
+ ):
97
+ """Initialize EnhancedTaskValidator.
98
+
99
+ Args:
100
+ task_manager: Manager for task CRUD operations.
101
+ history_manager: Manager for validation history.
102
+ llm_validator: LLM-based validator for task validation.
103
+ max_iterations: Maximum validation attempts before escalation.
104
+ error_threshold: Consecutive errors before escalation.
105
+ """
106
+ self.task_manager = task_manager
107
+ self.history_manager = history_manager
108
+ self.llm_validator = llm_validator
109
+ self.max_iterations = max_iterations
110
+ self.error_threshold = error_threshold
111
+
112
+ async def validate_with_retry(
113
+ self,
114
+ task_id: str,
115
+ context: dict[str, Any] | None = None,
116
+ ) -> ValidationResult:
117
+ """Validate a task with retry logic.
118
+
119
+ Attempts validation up to max_iterations times. Escalates if:
120
+ - max_iterations is exceeded
121
+ - Consecutive errors exceed error_threshold
122
+ - Recurring issues are detected
123
+
124
+ Args:
125
+ task_id: ID of the task to validate.
126
+ context: Optional context for validation (e.g., git diff).
127
+
128
+ Returns:
129
+ ValidationResult with validation status and details.
130
+ """
131
+ task = self.task_manager.get_task(task_id)
132
+ consecutive_errors = 0
133
+ last_feedback = ""
134
+ last_issues: list[Issue] = []
135
+
136
+ for iteration in range(1, self.max_iterations + 1):
137
+ try:
138
+ # Attempt validation
139
+ result = await self.llm_validator.validate(task, context=context)
140
+
141
+ valid = result.get("valid", False)
142
+ feedback = result.get("feedback", "")
143
+ issues_data = result.get("issues", [])
144
+
145
+ # Parse issues
146
+ issues = []
147
+ for issue_dict in issues_data:
148
+ if isinstance(issue_dict, dict):
149
+ issues.append(Issue.from_dict(issue_dict))
150
+ elif isinstance(issue_dict, Issue):
151
+ issues.append(issue_dict)
152
+
153
+ last_feedback = feedback
154
+ last_issues = issues
155
+
156
+ # Record this iteration
157
+ status = "valid" if valid else "invalid"
158
+ self.history_manager.record_iteration(
159
+ task_id=task_id,
160
+ iteration=iteration,
161
+ status=status,
162
+ feedback=feedback,
163
+ issues=issues if issues else None,
164
+ )
165
+
166
+ # Reset consecutive errors on success
167
+ consecutive_errors = 0
168
+
169
+ if valid:
170
+ # Validation passed
171
+ return ValidationResult(
172
+ valid=True,
173
+ iterations=iteration,
174
+ feedback=feedback,
175
+ issues=issues,
176
+ )
177
+
178
+ # Check for recurring issues after recording
179
+ if self.history_manager.has_recurring_issues(task_id):
180
+ logger.info(f"Recurring issues detected for task {task_id}, escalating")
181
+ return ValidationResult(
182
+ valid=False,
183
+ iterations=iteration,
184
+ feedback=feedback,
185
+ escalated=True,
186
+ escalation_reason=EscalationReason.RECURRING_ISSUES,
187
+ issues=issues,
188
+ )
189
+
190
+ except Exception as e:
191
+ logger.warning(f"Validation error on iteration {iteration}: {e}")
192
+ consecutive_errors += 1
193
+
194
+ # Record error iteration
195
+ self.history_manager.record_iteration(
196
+ task_id=task_id,
197
+ iteration=iteration,
198
+ status="error",
199
+ feedback=str(e),
200
+ )
201
+
202
+ if consecutive_errors >= self.error_threshold:
203
+ logger.info(
204
+ f"Consecutive error threshold reached for task {task_id}, "
205
+ f"escalating after {consecutive_errors} errors"
206
+ )
207
+ return ValidationResult(
208
+ valid=False,
209
+ iterations=iteration,
210
+ feedback=f"Validation failed with {consecutive_errors} consecutive errors",
211
+ escalated=True,
212
+ escalation_reason=EscalationReason.CONSECUTIVE_ERRORS,
213
+ )
214
+
215
+ # Max iterations exceeded
216
+ logger.info(
217
+ f"Max iterations ({self.max_iterations}) exceeded for task {task_id}, escalating"
218
+ )
219
+ return ValidationResult(
220
+ valid=False,
221
+ iterations=self.max_iterations,
222
+ feedback=last_feedback,
223
+ escalated=True,
224
+ escalation_reason=EscalationReason.MAX_ITERATIONS,
225
+ issues=last_issues,
226
+ )