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,308 @@
1
+ """Windows terminal spawners: Windows Terminal, cmd, PowerShell, and WSL."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import platform
7
+ import shlex
8
+ import shutil
9
+ import subprocess # nosec B404 - subprocess needed for terminal spawning
10
+ from pathlib import Path
11
+
12
+ from gobby.agents.spawners.base import SpawnResult, TerminalSpawnerBase, TerminalType
13
+ from gobby.agents.tty_config import get_tty_config
14
+
15
+ __all__ = ["WindowsTerminalSpawner", "CmdSpawner", "PowerShellSpawner", "WSLSpawner"]
16
+
17
+
18
+ class WindowsTerminalSpawner(TerminalSpawnerBase):
19
+ """Spawner for Windows Terminal."""
20
+
21
+ @property
22
+ def terminal_type(self) -> TerminalType:
23
+ return TerminalType.WINDOWS_TERMINAL
24
+
25
+ def is_available(self) -> bool:
26
+ if platform.system() != "Windows":
27
+ return False
28
+ config = get_tty_config().get_terminal_config("windows-terminal")
29
+ if not config.enabled:
30
+ return False
31
+ command = config.command or "wt"
32
+ return shutil.which(command) is not None
33
+
34
+ def spawn(
35
+ self,
36
+ command: list[str],
37
+ cwd: str | Path,
38
+ env: dict[str, str] | None = None,
39
+ title: str | None = None,
40
+ ) -> SpawnResult:
41
+ try:
42
+ tty_config = get_tty_config().get_terminal_config("windows-terminal")
43
+ cli_command = tty_config.command or "wt"
44
+ args = [cli_command, "-d", str(cwd)]
45
+ # Add extra options from config
46
+ args.extend(tty_config.options)
47
+ if title:
48
+ args.extend(["--title", title])
49
+ args.extend(["--", *command])
50
+
51
+ spawn_env = os.environ.copy()
52
+ if env:
53
+ spawn_env.update(env)
54
+
55
+ process = subprocess.Popen( # nosec B603 - args built from config
56
+ args,
57
+ env=spawn_env,
58
+ creationflags=getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0),
59
+ )
60
+
61
+ return SpawnResult(
62
+ success=True,
63
+ message=f"Spawned Windows Terminal with PID {process.pid}",
64
+ pid=process.pid,
65
+ terminal_type=self.terminal_type.value,
66
+ )
67
+
68
+ except Exception as e:
69
+ return SpawnResult(
70
+ success=False,
71
+ message=f"Failed to spawn Windows Terminal: {e}",
72
+ error=str(e),
73
+ )
74
+
75
+
76
+ class CmdSpawner(TerminalSpawnerBase):
77
+ """Spawner for Windows cmd.exe."""
78
+
79
+ @property
80
+ def terminal_type(self) -> TerminalType:
81
+ return TerminalType.CMD
82
+
83
+ def is_available(self) -> bool:
84
+ if platform.system() != "Windows":
85
+ return False
86
+ config = get_tty_config().get_terminal_config("cmd")
87
+ return config.enabled
88
+
89
+ def spawn(
90
+ self,
91
+ command: list[str],
92
+ cwd: str | Path,
93
+ env: dict[str, str] | None = None,
94
+ title: str | None = None,
95
+ ) -> SpawnResult:
96
+ try:
97
+ # Build the inner command as a list and convert safely with list2cmdline
98
+ # This properly escapes all arguments to prevent command injection
99
+ cd_cmd = ["cd", "/d", str(cwd)]
100
+ # Build full command list: cd /d path && original_command
101
+ # list2cmdline handles proper escaping for Windows
102
+ inner_cmd = subprocess.list2cmdline(cd_cmd) + " && " + subprocess.list2cmdline(command)
103
+
104
+ args = ["cmd", "/c", "start"]
105
+ if title:
106
+ # Title must be quoted if it contains spaces
107
+ args.append(subprocess.list2cmdline([title]))
108
+ # Use empty title if none provided (required for start command when path is quoted)
109
+ else:
110
+ args.append('""')
111
+ # Pass the inner command as a single argument to cmd /k
112
+ args.extend(["cmd", "/k", inner_cmd])
113
+
114
+ spawn_env = os.environ.copy()
115
+ if env:
116
+ spawn_env.update(env)
117
+
118
+ process = subprocess.Popen( # nosec B603 - args built from config
119
+ args,
120
+ env=spawn_env,
121
+ creationflags=getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0),
122
+ )
123
+
124
+ return SpawnResult(
125
+ success=True,
126
+ message=f"Spawned cmd.exe with PID {process.pid}",
127
+ pid=process.pid,
128
+ terminal_type=self.terminal_type.value,
129
+ )
130
+
131
+ except Exception as e:
132
+ return SpawnResult(
133
+ success=False,
134
+ message=f"Failed to spawn cmd.exe: {e}",
135
+ error=str(e),
136
+ )
137
+
138
+
139
+ class PowerShellSpawner(TerminalSpawnerBase):
140
+ """Spawner for Windows PowerShell."""
141
+
142
+ @property
143
+ def terminal_type(self) -> TerminalType:
144
+ return TerminalType.POWERSHELL
145
+
146
+ def is_available(self) -> bool:
147
+ if platform.system() != "Windows":
148
+ return False
149
+ config = get_tty_config().get_terminal_config("powershell")
150
+ if not config.enabled:
151
+ return False
152
+ # Check for pwsh (PowerShell Core) first, then powershell (Windows PowerShell)
153
+ command = config.command or "pwsh"
154
+ if shutil.which(command) is not None:
155
+ return True
156
+ # Fall back to Windows PowerShell
157
+ return shutil.which("powershell") is not None
158
+
159
+ def spawn(
160
+ self,
161
+ command: list[str],
162
+ cwd: str | Path,
163
+ env: dict[str, str] | None = None,
164
+ title: str | None = None,
165
+ ) -> SpawnResult:
166
+ try:
167
+ tty_config = get_tty_config().get_terminal_config("powershell")
168
+ # Prefer pwsh (PowerShell Core) over powershell (Windows PowerShell)
169
+ cli_command = tty_config.command or "pwsh"
170
+ if shutil.which(cli_command) is None:
171
+ cli_command = "powershell"
172
+
173
+ # Build the inner command to run
174
+ # PowerShell requires special escaping for the -Command parameter
175
+ inner_cmd = subprocess.list2cmdline(command)
176
+
177
+ # Escape values for PowerShell single-quoted strings (double any single quotes)
178
+ safe_cwd = "'" + str(cwd).replace("'", "''") + "'"
179
+ safe_inner_cmd = inner_cmd.replace("'", "''")
180
+
181
+ # Build PowerShell command:
182
+ # Start-Process spawns a new window, -WorkingDirectory sets cwd
183
+ # -NoExit keeps the window open after command completes
184
+ ps_script = f"Set-Location -Path {safe_cwd}; {safe_inner_cmd}"
185
+
186
+ args = ["cmd", "/c", "start", "", cli_command]
187
+ # Add extra options from config
188
+ args.extend(tty_config.options)
189
+ if title:
190
+ # Escape title for PowerShell
191
+ safe_title = "'" + title.replace("'", "''") + "'"
192
+ args.extend(["-Title", safe_title])
193
+ args.extend(["-NoExit", "-Command", ps_script])
194
+
195
+ spawn_env = os.environ.copy()
196
+ if env:
197
+ spawn_env.update(env)
198
+
199
+ process = subprocess.Popen( # nosec B603 - args built from config
200
+ args,
201
+ env=spawn_env,
202
+ creationflags=getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0),
203
+ )
204
+
205
+ return SpawnResult(
206
+ success=True,
207
+ message=f"Spawned PowerShell with PID {process.pid}",
208
+ pid=process.pid,
209
+ terminal_type=self.terminal_type.value,
210
+ )
211
+
212
+ except Exception as e:
213
+ return SpawnResult(
214
+ success=False,
215
+ message=f"Failed to spawn PowerShell: {e}",
216
+ error=str(e),
217
+ )
218
+
219
+
220
+ class WSLSpawner(TerminalSpawnerBase):
221
+ """Spawner for Windows Subsystem for Linux (WSL2)."""
222
+
223
+ @property
224
+ def terminal_type(self) -> TerminalType:
225
+ return TerminalType.WSL
226
+
227
+ def is_available(self) -> bool:
228
+ if platform.system() != "Windows":
229
+ return False
230
+ config = get_tty_config().get_terminal_config("wsl")
231
+ if not config.enabled:
232
+ return False
233
+ command = config.command or "wsl"
234
+ return shutil.which(command) is not None
235
+
236
+ def spawn(
237
+ self,
238
+ command: list[str],
239
+ cwd: str | Path,
240
+ env: dict[str, str] | None = None,
241
+ title: str | None = None,
242
+ ) -> SpawnResult:
243
+ try:
244
+ tty_config = get_tty_config().get_terminal_config("wsl")
245
+ cli_command = tty_config.command or "wsl"
246
+
247
+ # Convert Windows path to WSL path if needed
248
+ # e.g., C:\Users\foo -> /mnt/c/Users/foo
249
+ cwd_str = str(cwd)
250
+ if len(cwd_str) >= 2 and cwd_str[1] == ":":
251
+ # Windows absolute path - convert to WSL format
252
+ drive = cwd_str[0].lower()
253
+ wsl_path = f"/mnt/{drive}{cwd_str[2:].replace(chr(92), '/')}"
254
+ else:
255
+ wsl_path = cwd_str
256
+
257
+ # Build the command to run inside WSL
258
+ # Escape for bash shell inside WSL
259
+ inner_parts = [shlex.quote(part) for part in command]
260
+ inner_cmd = " ".join(inner_parts)
261
+ wsl_script = f"cd {shlex.quote(wsl_path)} && {inner_cmd}"
262
+
263
+ # Build environment exports for WSL
264
+ env_exports = ""
265
+ if env:
266
+ exports = []
267
+ for k, v in env.items():
268
+ if k.isidentifier():
269
+ exports.append(f"export {k}={shlex.quote(v)}")
270
+ if exports:
271
+ env_exports = " && ".join(exports) + " && "
272
+
273
+ full_script = env_exports + wsl_script
274
+
275
+ # Use cmd /c start to spawn in a new console window
276
+ args = ["cmd", "/c", "start"]
277
+ if title:
278
+ args.append(subprocess.list2cmdline([title]))
279
+ else:
280
+ args.append('""')
281
+ args.extend([cli_command])
282
+ # Add extra options from config (e.g., -d for distribution)
283
+ args.extend(tty_config.options)
284
+ args.extend(["--", "bash", "-c", full_script])
285
+
286
+ spawn_env = os.environ.copy()
287
+ # Note: env vars passed via spawn_env won't reach WSL directly
288
+ # They're handled via the bash -c script above
289
+
290
+ process = subprocess.Popen( # nosec B603 - args built from config
291
+ args,
292
+ env=spawn_env,
293
+ creationflags=getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0),
294
+ )
295
+
296
+ return SpawnResult(
297
+ success=True,
298
+ message=f"Spawned WSL with PID {process.pid}",
299
+ pid=process.pid,
300
+ terminal_type=self.terminal_type.value,
301
+ )
302
+
303
+ except Exception as e:
304
+ return SpawnResult(
305
+ success=False,
306
+ message=f"Failed to spawn WSL: {e}",
307
+ error=str(e),
308
+ )
@@ -0,0 +1,319 @@
1
+ """
2
+ Terminal spawner configuration.
3
+
4
+ Loads terminal preferences and customizations from ~/.gobby/tty_config.yaml,
5
+ allowing users to reorder preferences, customize app paths, and add options.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import platform
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import yaml
15
+ from pydantic import BaseModel, Field
16
+
17
+
18
+ class TerminalConfig(BaseModel):
19
+ """Configuration for a specific terminal emulator."""
20
+
21
+ app_path: str | None = Field(
22
+ default=None,
23
+ description="macOS app bundle path (e.g., /Applications/Ghostty.app)",
24
+ )
25
+ command: str | None = Field(
26
+ default=None,
27
+ description="CLI command name for shutil.which() (e.g., ghostty, kitty)",
28
+ )
29
+ options: list[str] = Field(
30
+ default_factory=list,
31
+ description="Extra command-line options to pass to the terminal",
32
+ )
33
+ enabled: bool = Field(
34
+ default=True,
35
+ description="Whether this terminal is enabled for use",
36
+ )
37
+
38
+
39
+ class PlatformPreferences(BaseModel):
40
+ """Terminal preference order by platform."""
41
+
42
+ macos: list[str] = Field(
43
+ default_factory=lambda: [
44
+ "ghostty",
45
+ "iterm",
46
+ "kitty",
47
+ "alacritty",
48
+ "terminal.app",
49
+ "tmux", # Multiplexer (last resort)
50
+ ],
51
+ description="Terminal preference order for macOS",
52
+ )
53
+ linux: list[str] = Field(
54
+ default_factory=lambda: [
55
+ "ghostty",
56
+ "kitty",
57
+ "gnome-terminal",
58
+ "konsole",
59
+ "alacritty",
60
+ "tmux", # Multiplexer (last resort)
61
+ ],
62
+ description="Terminal preference order for Linux",
63
+ )
64
+ windows: list[str] = Field(
65
+ default_factory=lambda: [
66
+ "windows-terminal",
67
+ "powershell",
68
+ "alacritty",
69
+ "wsl",
70
+ "cmd",
71
+ ],
72
+ description="Terminal preference order for Windows",
73
+ )
74
+
75
+
76
+ # Default terminal configurations (can be overridden in config file)
77
+ DEFAULT_TERMINAL_CONFIGS: dict[str, dict[str, Any]] = {
78
+ "ghostty": {
79
+ "app_path": "/Applications/Ghostty.app",
80
+ "command": "ghostty",
81
+ },
82
+ "iterm": {
83
+ "app_path": "/Applications/iTerm.app",
84
+ },
85
+ "terminal.app": {
86
+ "app_path": "/System/Applications/Utilities/Terminal.app",
87
+ },
88
+ "kitty": {
89
+ "app_path": "/Applications/kitty.app",
90
+ "command": "kitty",
91
+ "options": ["-o", "confirm_os_window_close=0"],
92
+ },
93
+ "alacritty": {
94
+ "command": "alacritty",
95
+ },
96
+ "gnome-terminal": {
97
+ "command": "gnome-terminal",
98
+ },
99
+ "konsole": {
100
+ "command": "konsole",
101
+ },
102
+ "windows-terminal": {
103
+ "command": "wt",
104
+ },
105
+ "cmd": {
106
+ # Built-in on Windows, no command needed
107
+ },
108
+ "powershell": {
109
+ # pwsh (PowerShell Core) is preferred, falls back to powershell (Windows PowerShell)
110
+ "command": "pwsh",
111
+ },
112
+ "wsl": {
113
+ "command": "wsl",
114
+ # Options can specify distribution: ["-d", "Ubuntu"]
115
+ },
116
+ "tmux": {
117
+ "command": "tmux",
118
+ # Options can set socket name, config file, etc.
119
+ },
120
+ }
121
+
122
+
123
+ class TTYConfig(BaseModel):
124
+ """Terminal spawner configuration."""
125
+
126
+ preferences: PlatformPreferences = Field(
127
+ default_factory=PlatformPreferences,
128
+ description="Terminal preference order by platform",
129
+ )
130
+ terminals: dict[str, TerminalConfig] = Field(
131
+ default_factory=dict,
132
+ description="Terminal-specific configurations (merged with defaults)",
133
+ )
134
+
135
+ def get_terminal_config(self, terminal_name: str) -> TerminalConfig:
136
+ """
137
+ Get configuration for a specific terminal.
138
+
139
+ Merges user config with defaults, with user config taking precedence.
140
+
141
+ Args:
142
+ terminal_name: Name of the terminal (e.g., 'ghostty', 'iterm')
143
+
144
+ Returns:
145
+ TerminalConfig with merged settings
146
+ """
147
+ # Start with defaults
148
+ defaults = DEFAULT_TERMINAL_CONFIGS.get(terminal_name, {})
149
+ config_dict = dict(defaults)
150
+
151
+ # Merge user config if present
152
+ if terminal_name in self.terminals:
153
+ user_config = self.terminals[terminal_name].model_dump(exclude_none=True)
154
+ # For options, extend rather than replace
155
+ if "options" in user_config and "options" in config_dict:
156
+ config_dict["options"] = config_dict["options"] + user_config["options"]
157
+ del user_config["options"]
158
+ config_dict.update(user_config)
159
+
160
+ return TerminalConfig(**config_dict)
161
+
162
+ def get_preferences(self) -> list[str]:
163
+ """
164
+ Get terminal preference order for current platform.
165
+
166
+ Returns:
167
+ List of terminal names in preference order
168
+ """
169
+ system = platform.system()
170
+ if system == "Darwin":
171
+ return self.preferences.macos
172
+ elif system == "Windows":
173
+ return self.preferences.windows
174
+ else:
175
+ return self.preferences.linux
176
+
177
+
178
+ def load_tty_config(config_path: str | Path | None = None) -> TTYConfig:
179
+ """
180
+ Load terminal configuration from YAML file.
181
+
182
+ Args:
183
+ config_path: Path to config file (default: ~/.gobby/tty_config.yaml)
184
+
185
+ Returns:
186
+ TTYConfig instance (with defaults if file doesn't exist)
187
+ """
188
+ if config_path is None:
189
+ config_path = Path.home() / ".gobby" / "tty_config.yaml"
190
+ else:
191
+ config_path = Path(config_path).expanduser()
192
+
193
+ if not config_path.exists():
194
+ return TTYConfig()
195
+
196
+ try:
197
+ with open(config_path) as f:
198
+ data = yaml.safe_load(f)
199
+ return TTYConfig(**(data or {}))
200
+ except Exception:
201
+ # Fall back to defaults on any error
202
+ return TTYConfig()
203
+
204
+
205
+ def generate_default_tty_config(config_path: str | Path | None = None) -> Path:
206
+ """
207
+ Generate default terminal configuration file.
208
+
209
+ Args:
210
+ config_path: Path to config file (default: ~/.gobby/tty_config.yaml)
211
+
212
+ Returns:
213
+ Path to the created config file
214
+ """
215
+ if config_path is None:
216
+ config_path = Path.home() / ".gobby" / "tty_config.yaml"
217
+ else:
218
+ config_path = Path(config_path).expanduser()
219
+
220
+ config_path.parent.mkdir(parents=True, exist_ok=True)
221
+
222
+ # Generate example config with comments
223
+ config_content = """# Terminal spawner configuration for Gobby
224
+ # See: https://github.com/GobbyAI/gobby/docs/terminal-config.md
225
+
226
+ # Terminal preference order by platform (first available is used)
227
+ preferences:
228
+ macos:
229
+ - ghostty
230
+ - iterm
231
+ - kitty
232
+ - alacritty
233
+ - terminal.app
234
+ - tmux
235
+ linux:
236
+ - ghostty
237
+ - kitty
238
+ - gnome-terminal
239
+ - konsole
240
+ - alacritty
241
+ - tmux
242
+ windows:
243
+ - windows-terminal
244
+ - powershell
245
+ - alacritty
246
+ - wsl
247
+ - cmd
248
+
249
+ # Terminal-specific configurations (overrides defaults)
250
+ # Uncomment and modify as needed:
251
+ #
252
+ # terminals:
253
+ # ghostty:
254
+ # app_path: /Applications/Ghostty.app # macOS app bundle path
255
+ # command: ghostty # CLI command (Linux/other)
256
+ # enabled: true # Set to false to skip this terminal
257
+ #
258
+ # kitty:
259
+ # app_path: /Applications/kitty.app
260
+ # command: kitty
261
+ # options: # Extra command-line options
262
+ # - "-o"
263
+ # - "confirm_os_window_close=0"
264
+ #
265
+ # iterm:
266
+ # app_path: /Applications/iTerm.app
267
+ #
268
+ # powershell:
269
+ # command: pwsh # Use 'powershell' for Windows PowerShell
270
+ #
271
+ # wsl:
272
+ # command: wsl
273
+ # options: # Specify WSL distribution
274
+ # - "-d"
275
+ # - "Ubuntu"
276
+ #
277
+ # tmux:
278
+ # command: tmux
279
+ # options: # Use specific socket/config
280
+ # - "-L"
281
+ # - "gobby"
282
+ """
283
+
284
+ with open(config_path, "w") as f:
285
+ f.write(config_content)
286
+
287
+ # Set restrictive permissions
288
+ config_path.chmod(0o600)
289
+
290
+ return config_path
291
+
292
+
293
+ # Global cached config instance
294
+ _config: TTYConfig | None = None
295
+
296
+
297
+ def get_tty_config() -> TTYConfig:
298
+ """
299
+ Get the terminal configuration (cached).
300
+
301
+ Returns:
302
+ TTYConfig instance
303
+ """
304
+ global _config
305
+ if _config is None:
306
+ _config = load_tty_config()
307
+ return _config
308
+
309
+
310
+ def reload_tty_config() -> TTYConfig:
311
+ """
312
+ Reload terminal configuration from disk.
313
+
314
+ Returns:
315
+ New TTYConfig instance
316
+ """
317
+ global _config
318
+ _config = load_tty_config()
319
+ return _config
@@ -0,0 +1,32 @@
1
+ """Autonomous execution infrastructure for Gobby.
2
+
3
+ This module provides infrastructure for autonomous task execution including:
4
+ - Stop signal management for graceful shutdown
5
+ - Progress tracking for detecting stagnation
6
+ - Stuck detection for breaking out of loops
7
+ """
8
+
9
+ from gobby.autonomous.progress_tracker import (
10
+ ProgressEvent,
11
+ ProgressSummary,
12
+ ProgressTracker,
13
+ ProgressType,
14
+ )
15
+ from gobby.autonomous.stop_registry import StopRegistry, StopSignal
16
+ from gobby.autonomous.stuck_detector import (
17
+ StuckDetectionResult,
18
+ StuckDetector,
19
+ TaskSelectionEvent,
20
+ )
21
+
22
+ __all__ = [
23
+ "ProgressEvent",
24
+ "ProgressSummary",
25
+ "ProgressTracker",
26
+ "ProgressType",
27
+ "StopRegistry",
28
+ "StopSignal",
29
+ "StuckDetectionResult",
30
+ "StuckDetector",
31
+ "TaskSelectionEvent",
32
+ ]