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,383 @@
1
+ """Stuck detection for autonomous session management.
2
+
3
+ Provides multi-layer stuck detection for autonomous workflows:
4
+ 1. Task selection loop detection - same tasks being selected repeatedly
5
+ 2. Progress stagnation - no meaningful progress being made
6
+ 3. Tool call patterns - repeated identical tool calls
7
+ """
8
+
9
+ import ast
10
+ import json
11
+ import logging
12
+ import threading
13
+ from dataclasses import dataclass
14
+ from datetime import UTC, datetime
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ if TYPE_CHECKING:
18
+ from gobby.autonomous.progress_tracker import ProgressTracker
19
+ from gobby.storage.database import LocalDatabase
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @dataclass
25
+ class TaskSelectionEvent:
26
+ """A task selection event for loop detection."""
27
+
28
+ session_id: str
29
+ task_id: str
30
+ selected_at: datetime
31
+ context: dict[str, Any] | None = None
32
+
33
+
34
+ @dataclass
35
+ class StuckDetectionResult:
36
+ """Result of stuck detection analysis."""
37
+
38
+ is_stuck: bool
39
+ reason: str | None = None
40
+ layer: str | None = None # task_loop, progress_stagnation, tool_loop
41
+ details: dict[str, Any] | None = None
42
+ suggested_action: str | None = None # stop, change_approach, escalate
43
+
44
+
45
+ class StuckDetector:
46
+ """Multi-layer stuck detection for autonomous sessions.
47
+
48
+ The stuck detector analyzes session behavior at three levels:
49
+
50
+ Layer 1 - Task Selection Loops:
51
+ Detects when the same task(s) are being selected repeatedly
52
+ without successful completion. This indicates the agent is
53
+ unable to make progress on available work.
54
+
55
+ Layer 2 - Progress Stagnation:
56
+ Uses ProgressTracker to detect when no meaningful progress
57
+ (file modifications, commits, task completions) is occurring
58
+ despite continued activity.
59
+
60
+ Layer 3 - Tool Call Patterns:
61
+ Detects repeated identical tool calls that indicate the agent
62
+ is stuck in a loop (e.g., repeatedly reading the same file).
63
+ """
64
+
65
+ # Thresholds for loop detection
66
+ DEFAULT_TASK_LOOP_THRESHOLD = 3 # Same task selected N times = loop
67
+ DEFAULT_TASK_WINDOW_SIZE = 10 # Look at last N selections
68
+ DEFAULT_TOOL_LOOP_THRESHOLD = 5 # Same tool call N times = loop
69
+ DEFAULT_TOOL_WINDOW_SIZE = 20 # Look at last N tool calls
70
+
71
+ def __init__(
72
+ self,
73
+ db: "LocalDatabase",
74
+ progress_tracker: "ProgressTracker | None" = None,
75
+ task_loop_threshold: int | None = None,
76
+ task_window_size: int | None = None,
77
+ tool_loop_threshold: int | None = None,
78
+ tool_window_size: int | None = None,
79
+ ):
80
+ """Initialize the stuck detector.
81
+
82
+ Args:
83
+ db: Database connection for persistent storage
84
+ progress_tracker: Optional ProgressTracker for stagnation detection
85
+ task_loop_threshold: Times a task can be selected before considered stuck
86
+ task_window_size: Number of recent selections to analyze
87
+ tool_loop_threshold: Times same tool call before considered stuck
88
+ tool_window_size: Number of recent tool calls to analyze
89
+ """
90
+ self.db = db
91
+ self.progress_tracker = progress_tracker
92
+ self._lock = threading.Lock()
93
+
94
+ self.task_loop_threshold = task_loop_threshold or self.DEFAULT_TASK_LOOP_THRESHOLD
95
+ self.task_window_size = task_window_size or self.DEFAULT_TASK_WINDOW_SIZE
96
+ self.tool_loop_threshold = tool_loop_threshold or self.DEFAULT_TOOL_LOOP_THRESHOLD
97
+ self.tool_window_size = tool_window_size or self.DEFAULT_TOOL_WINDOW_SIZE
98
+
99
+ def record_task_selection(
100
+ self,
101
+ session_id: str,
102
+ task_id: str,
103
+ context: dict[str, Any] | None = None,
104
+ ) -> TaskSelectionEvent:
105
+ """Record a task selection event.
106
+
107
+ Args:
108
+ session_id: The session selecting the task
109
+ task_id: The task being selected
110
+ context: Optional context about the selection
111
+
112
+ Returns:
113
+ The created TaskSelectionEvent
114
+ """
115
+ now = datetime.now(UTC)
116
+ event = TaskSelectionEvent(
117
+ session_id=session_id,
118
+ task_id=task_id,
119
+ selected_at=now,
120
+ context=context,
121
+ )
122
+
123
+ with self._lock:
124
+ self.db.execute(
125
+ """
126
+ INSERT INTO task_selection_history (
127
+ session_id, task_id, selected_at, context
128
+ ) VALUES (?, ?, ?, ?)
129
+ """,
130
+ (
131
+ session_id,
132
+ task_id,
133
+ now.isoformat(),
134
+ str(context) if context else None,
135
+ ),
136
+ )
137
+
138
+ logger.debug(f"Recorded task selection for session {session_id}: task={task_id}")
139
+
140
+ return event
141
+
142
+ def detect_task_loop(self, session_id: str) -> StuckDetectionResult:
143
+ """Detect task selection loops.
144
+
145
+ Checks the last N task selections (task_window_size) within the past hour
146
+ to detect if any task has been selected more times than the threshold.
147
+
148
+ Args:
149
+ session_id: The session to check
150
+
151
+ Returns:
152
+ StuckDetectionResult indicating if stuck in task loop
153
+ """
154
+ from datetime import timedelta
155
+
156
+ # Compute cutoff as ISO8601 string for like-for-like comparison
157
+ cutoff = (datetime.now(UTC) - timedelta(hours=1)).isoformat()
158
+
159
+ # Get the last N task selections within the time window, then aggregate
160
+ rows = self.db.fetchall(
161
+ """
162
+ SELECT task_id, COUNT(*) as count
163
+ FROM (
164
+ SELECT task_id
165
+ FROM task_selection_history
166
+ WHERE session_id = ?
167
+ AND selected_at > ?
168
+ ORDER BY selected_at DESC
169
+ LIMIT ?
170
+ )
171
+ GROUP BY task_id
172
+ ORDER BY count DESC
173
+ """,
174
+ (session_id, cutoff, self.task_window_size),
175
+ )
176
+
177
+ if not rows:
178
+ return StuckDetectionResult(is_stuck=False)
179
+
180
+ # Check if any task has been selected too many times
181
+ for row in rows:
182
+ if row["count"] >= self.task_loop_threshold:
183
+ logger.info(
184
+ f"Session {session_id} stuck in task loop: "
185
+ f"task {row['task_id']} selected {row['count']} times"
186
+ )
187
+ return StuckDetectionResult(
188
+ is_stuck=True,
189
+ reason=f"Task '{row['task_id']}' selected {row['count']} times without completion",
190
+ layer="task_loop",
191
+ details={
192
+ "task_id": row["task_id"],
193
+ "selection_count": row["count"],
194
+ "threshold": self.task_loop_threshold,
195
+ },
196
+ suggested_action="change_approach",
197
+ )
198
+
199
+ return StuckDetectionResult(is_stuck=False)
200
+
201
+ def detect_progress_stagnation(self, session_id: str) -> StuckDetectionResult:
202
+ """Detect progress stagnation using ProgressTracker.
203
+
204
+ Args:
205
+ session_id: The session to check
206
+
207
+ Returns:
208
+ StuckDetectionResult indicating if progress is stagnant
209
+ """
210
+ if not self.progress_tracker:
211
+ return StuckDetectionResult(is_stuck=False)
212
+
213
+ summary = self.progress_tracker.get_summary(session_id)
214
+
215
+ if summary.is_stagnant:
216
+ logger.info(
217
+ f"Session {session_id} progress stagnant: "
218
+ f"{summary.stagnation_duration_seconds:.0f}s since high-value event"
219
+ )
220
+ return StuckDetectionResult(
221
+ is_stuck=True,
222
+ reason=f"No meaningful progress for {summary.stagnation_duration_seconds:.0f} seconds",
223
+ layer="progress_stagnation",
224
+ details={
225
+ "total_events": summary.total_events,
226
+ "high_value_events": summary.high_value_events,
227
+ "stagnation_duration": summary.stagnation_duration_seconds,
228
+ "last_high_value_at": (
229
+ summary.last_high_value_at.isoformat()
230
+ if summary.last_high_value_at
231
+ else None
232
+ ),
233
+ },
234
+ suggested_action="stop",
235
+ )
236
+
237
+ return StuckDetectionResult(is_stuck=False)
238
+
239
+ def detect_tool_loop(self, session_id: str) -> StuckDetectionResult:
240
+ """Detect repeated identical tool calls.
241
+
242
+ Args:
243
+ session_id: The session to check
244
+
245
+ Returns:
246
+ StuckDetectionResult indicating if stuck in tool loop
247
+ """
248
+ # Get recent tool calls from progress tracker
249
+ if not self.progress_tracker:
250
+ return StuckDetectionResult(is_stuck=False)
251
+
252
+ recent_events = self.progress_tracker.get_recent_events(session_id, self.tool_window_size)
253
+
254
+ if not recent_events:
255
+ return StuckDetectionResult(is_stuck=False)
256
+
257
+ # Count tool call patterns
258
+ tool_counts: dict[str, int] = {}
259
+ for event in recent_events:
260
+ if event.tool_name:
261
+ # Create a key from tool name and key args
262
+ key = f"{event.tool_name}:{event.details.get('tool_args_keys', [])}"
263
+ tool_counts[key] = tool_counts.get(key, 0) + 1
264
+
265
+ # Check for repeated patterns
266
+ for key, count in tool_counts.items():
267
+ if count >= self.tool_loop_threshold:
268
+ tool_name = key.split(":")[0]
269
+ logger.info(
270
+ f"Session {session_id} stuck in tool loop: {tool_name} called {count} times"
271
+ )
272
+ return StuckDetectionResult(
273
+ is_stuck=True,
274
+ reason=f"Tool '{tool_name}' called {count} times with same pattern",
275
+ layer="tool_loop",
276
+ details={
277
+ "tool_pattern": key,
278
+ "call_count": count,
279
+ "threshold": self.tool_loop_threshold,
280
+ },
281
+ suggested_action="change_approach",
282
+ )
283
+
284
+ return StuckDetectionResult(is_stuck=False)
285
+
286
+ def is_stuck(self, session_id: str) -> StuckDetectionResult:
287
+ """Run all stuck detection checks.
288
+
289
+ Checks all three layers in order of severity:
290
+ 1. Task selection loops
291
+ 2. Progress stagnation
292
+ 3. Tool call loops
293
+
294
+ Args:
295
+ session_id: The session to check
296
+
297
+ Returns:
298
+ StuckDetectionResult from first layer that detects stuck state,
299
+ or not-stuck result if all layers pass
300
+ """
301
+ # Layer 1: Task loops
302
+ result = self.detect_task_loop(session_id)
303
+ if result.is_stuck:
304
+ return result
305
+
306
+ # Layer 2: Progress stagnation
307
+ result = self.detect_progress_stagnation(session_id)
308
+ if result.is_stuck:
309
+ return result
310
+
311
+ # Layer 3: Tool loops
312
+ result = self.detect_tool_loop(session_id)
313
+ if result.is_stuck:
314
+ return result
315
+
316
+ return StuckDetectionResult(is_stuck=False)
317
+
318
+ def clear_session(self, session_id: str) -> int:
319
+ """Clear all stuck detection data for a session.
320
+
321
+ Args:
322
+ session_id: The session to clear
323
+
324
+ Returns:
325
+ Number of records cleared
326
+ """
327
+ with self._lock:
328
+ result = self.db.execute(
329
+ "DELETE FROM task_selection_history WHERE session_id = ?",
330
+ (session_id,),
331
+ )
332
+
333
+ if result.rowcount > 0:
334
+ logger.debug(
335
+ f"Cleared {result.rowcount} task selection record(s) for session {session_id}"
336
+ )
337
+
338
+ return result.rowcount
339
+
340
+ def get_selection_history(self, session_id: str, limit: int = 20) -> list[TaskSelectionEvent]:
341
+ """Get recent task selection history.
342
+
343
+ Args:
344
+ session_id: The session to get history for
345
+ limit: Maximum number of events to return
346
+
347
+ Returns:
348
+ List of recent TaskSelectionEvents
349
+ """
350
+ rows = self.db.fetchall(
351
+ """
352
+ SELECT session_id, task_id, selected_at, context
353
+ FROM task_selection_history
354
+ WHERE session_id = ?
355
+ ORDER BY selected_at DESC
356
+ LIMIT ?
357
+ """,
358
+ (session_id, limit),
359
+ )
360
+
361
+ events = []
362
+ for row in rows:
363
+ context = None
364
+ if row["context"]:
365
+ try:
366
+ context = ast.literal_eval(row["context"])
367
+ except (ValueError, SyntaxError):
368
+ try:
369
+ context = json.loads(row["context"])
370
+ except json.JSONDecodeError:
371
+ logger.warning(
372
+ f"Failed to parse context for task selection: {row['context'][:100]}"
373
+ )
374
+ context = None
375
+ events.append(
376
+ TaskSelectionEvent(
377
+ session_id=row["session_id"],
378
+ task_id=row["task_id"],
379
+ selected_at=datetime.fromisoformat(row["selected_at"]),
380
+ context=context,
381
+ )
382
+ )
383
+ return events
gobby/cli/__init__.py ADDED
@@ -0,0 +1,67 @@
1
+ """
2
+ Gobby CLI entry point.
3
+ """
4
+
5
+ import click
6
+
7
+ from gobby.config.app import load_config
8
+
9
+ from .agents import agents
10
+ from .artifacts import artifacts
11
+ from .daemon import restart, start, status, stop
12
+ from .extensions import hooks, plugins, webhooks
13
+ from .github import github
14
+ from .init import init
15
+ from .install import install, uninstall
16
+ from .linear import linear
17
+ from .mcp import mcp_server
18
+ from .mcp_proxy import mcp_proxy
19
+ from .memory import memory
20
+ from .merge import merge
21
+ from .projects import projects
22
+ from .sessions import sessions
23
+ from .tasks import tasks
24
+ from .tui import ui
25
+ from .workflows import workflows
26
+ from .worktrees import worktrees
27
+
28
+
29
+ @click.group()
30
+ @click.option(
31
+ "--config",
32
+ type=click.Path(exists=True),
33
+ help="Path to custom configuration file",
34
+ )
35
+ @click.pass_context
36
+ def cli(ctx: click.Context, config: str | None) -> None:
37
+ """Gobby - Local-first daemon for AI coding assistants."""
38
+ # Store config in context for subcommands
39
+ ctx.ensure_object(dict)
40
+ ctx.obj["config"] = load_config(config)
41
+
42
+
43
+ # Register commands
44
+ cli.add_command(start)
45
+ cli.add_command(stop)
46
+ cli.add_command(restart)
47
+ cli.add_command(status)
48
+ cli.add_command(mcp_server)
49
+ cli.add_command(init)
50
+ cli.add_command(install)
51
+ cli.add_command(uninstall)
52
+ cli.add_command(tasks)
53
+ cli.add_command(memory)
54
+ cli.add_command(sessions)
55
+ cli.add_command(agents)
56
+ cli.add_command(worktrees)
57
+ cli.add_command(mcp_proxy)
58
+ cli.add_command(projects)
59
+ cli.add_command(workflows)
60
+ cli.add_command(merge)
61
+ cli.add_command(artifacts)
62
+ cli.add_command(github)
63
+ cli.add_command(linear)
64
+ cli.add_command(hooks)
65
+ cli.add_command(plugins)
66
+ cli.add_command(webhooks)
67
+ cli.add_command(ui)
gobby/cli/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """
2
+ Entry point for python -m gobby.cli
3
+ """
4
+
5
+ from . import cli
6
+
7
+ if __name__ == "__main__":
8
+ cli()