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,343 @@
1
+ """Gemini CLI adapter for hook translation.
2
+
3
+ This adapter translates between Gemini CLI's native hook format and the unified
4
+ HookEvent/HookResponse models.
5
+
6
+ Gemini CLI Hook Types (11 total):
7
+ - SessionStart, SessionEnd: Session lifecycle
8
+ - BeforeAgent, AfterAgent: Agent turn lifecycle
9
+ - BeforeTool, AfterTool: Tool execution lifecycle
10
+ - BeforeToolSelection: Before tool selection (Gemini-only)
11
+ - BeforeModel, AfterModel: Model call lifecycle (Gemini-only)
12
+ - PreCompress: Context compression (maps to PRE_COMPACT)
13
+ - Notification: System notifications
14
+
15
+ Key differences from Claude Code:
16
+ - Uses PascalCase hook names (SessionStart vs session-start)
17
+ - Uses `hook_event_name` field instead of `hook_type`
18
+ - Has BeforeToolSelection, BeforeModel, AfterModel (not in Claude)
19
+ - Missing PermissionRequest, SubagentStart, SubagentStop (Claude-only)
20
+ - Different tool names (RunShellCommand vs Bash)
21
+ """
22
+
23
+ import platform
24
+ import uuid
25
+ from datetime import UTC, datetime
26
+ from typing import TYPE_CHECKING, Any
27
+
28
+ from gobby.adapters.base import BaseAdapter
29
+ from gobby.hooks.events import HookEvent, HookEventType, HookResponse, SessionSource
30
+
31
+ if TYPE_CHECKING:
32
+ from gobby.hooks.hook_manager import HookManager
33
+
34
+
35
+ class GeminiAdapter(BaseAdapter):
36
+ """Adapter for Gemini CLI hook translation.
37
+
38
+ This adapter:
39
+ 1. Translates Gemini CLI's PascalCase hook payloads to unified HookEvent
40
+ 2. Translates HookResponse back to Gemini CLI's expected format
41
+ 3. Calls HookManager.handle() with unified HookEvent model
42
+ """
43
+
44
+ source = SessionSource.GEMINI
45
+
46
+ # Event type mapping: Gemini CLI hook names -> unified HookEventType
47
+ # Gemini CLI uses PascalCase hook names in the payload's "hook_event_name" field
48
+ EVENT_MAP: dict[str, HookEventType] = {
49
+ "SessionStart": HookEventType.SESSION_START,
50
+ "SessionEnd": HookEventType.SESSION_END,
51
+ "BeforeAgent": HookEventType.BEFORE_AGENT,
52
+ "AfterAgent": HookEventType.AFTER_AGENT,
53
+ "BeforeTool": HookEventType.BEFORE_TOOL,
54
+ "AfterTool": HookEventType.AFTER_TOOL,
55
+ "BeforeToolSelection": HookEventType.BEFORE_TOOL_SELECTION, # Gemini-only
56
+ "BeforeModel": HookEventType.BEFORE_MODEL, # Gemini-only
57
+ "AfterModel": HookEventType.AFTER_MODEL, # Gemini-only
58
+ "PreCompress": HookEventType.PRE_COMPACT, # Gemini calls it PreCompress
59
+ "Notification": HookEventType.NOTIFICATION,
60
+ }
61
+
62
+ # Reverse mapping for response translation
63
+ HOOK_EVENT_NAME_MAP: dict[str, str] = {
64
+ "session_start": "SessionStart",
65
+ "session_end": "SessionEnd",
66
+ "before_agent": "BeforeAgent",
67
+ "after_agent": "AfterAgent",
68
+ "before_tool": "BeforeTool",
69
+ "after_tool": "AfterTool",
70
+ "before_tool_selection": "BeforeToolSelection",
71
+ "before_model": "BeforeModel",
72
+ "after_model": "AfterModel",
73
+ "pre_compact": "PreCompress",
74
+ "notification": "Notification",
75
+ }
76
+
77
+ # Tool name mapping: Gemini tool names -> normalized names
78
+ # Gemini uses different tool names than Claude Code
79
+ TOOL_MAP: dict[str, str] = {
80
+ "run_shell_command": "Bash",
81
+ "RunShellCommand": "Bash",
82
+ "read_file": "Read",
83
+ "ReadFile": "Read",
84
+ "ReadFileTool": "Read",
85
+ "write_file": "Write",
86
+ "WriteFile": "Write",
87
+ "WriteFileTool": "Write",
88
+ "edit_file": "Edit",
89
+ "EditFile": "Edit",
90
+ "EditFileTool": "Edit",
91
+ "GlobTool": "Glob",
92
+ "GrepTool": "Grep",
93
+ "ShellTool": "Bash",
94
+ }
95
+
96
+ def __init__(self, hook_manager: "HookManager | None" = None):
97
+ """Initialize the Gemini CLI adapter.
98
+
99
+ Args:
100
+ hook_manager: Reference to HookManager for handling events.
101
+ If None, the adapter can only translate (not handle events).
102
+ """
103
+ self._hook_manager = hook_manager
104
+ # Cache machine_id since Gemini doesn't always send it
105
+ self._machine_id: str | None = None
106
+
107
+ def _get_machine_id(self) -> str:
108
+ """Get or generate a machine identifier.
109
+
110
+ Gemini CLI doesn't always send machine_id, so we generate one
111
+ based on the platform node (hostname/MAC address).
112
+
113
+ Returns:
114
+ A stable machine identifier.
115
+ """
116
+ if self._machine_id is None:
117
+ # Use platform.node() which returns hostname or MAC-based ID
118
+ node = platform.node()
119
+ if node:
120
+ # Create a deterministic UUID from the node name
121
+ self._machine_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, node))
122
+ else:
123
+ # Fallback to a random UUID (less ideal but works)
124
+ self._machine_id = str(uuid.uuid4())
125
+ return self._machine_id
126
+
127
+ def normalize_tool_name(self, gemini_tool_name: str) -> str:
128
+ """Normalize Gemini tool name to standard format.
129
+
130
+ Args:
131
+ gemini_tool_name: Tool name from Gemini CLI.
132
+
133
+ Returns:
134
+ Normalized tool name (e.g., "Bash", "Read", "Write").
135
+ """
136
+ return self.TOOL_MAP.get(gemini_tool_name, gemini_tool_name)
137
+
138
+ def translate_to_hook_event(self, native_event: dict[str, Any]) -> HookEvent:
139
+ """Convert Gemini CLI native event to unified HookEvent.
140
+
141
+ Gemini CLI payloads have the structure:
142
+ {
143
+ "hook_event_name": "SessionStart", # PascalCase hook name
144
+ "session_id": "abc123", # Session identifier
145
+ "cwd": "/path/to/project",
146
+ "timestamp": "2025-01-15T10:30:00Z", # ISO timestamp
147
+ # ... other hook-specific fields
148
+ }
149
+
150
+ Note: The hook_dispatcher.py wraps this in:
151
+ {
152
+ "source": "gemini",
153
+ "hook_type": "SessionStart",
154
+ "input_data": {...} # The actual Gemini payload
155
+ }
156
+
157
+ Args:
158
+ native_event: Raw payload from Gemini CLI's hook_dispatcher.py
159
+
160
+ Returns:
161
+ Unified HookEvent with normalized fields.
162
+ """
163
+ # Extract from dispatcher wrapper format (matches Claude's structure)
164
+ hook_type = native_event.get("hook_type", "")
165
+ input_data = native_event.get("input_data", {})
166
+
167
+ # If input_data is empty, the native_event might BE the input_data
168
+ # (for direct Gemini calls without dispatcher wrapper)
169
+ if not input_data and "hook_event_name" in native_event:
170
+ input_data = native_event
171
+ hook_type = native_event.get("hook_event_name", "")
172
+
173
+ # Map Gemini hook type to unified event type
174
+ # Fall back to NOTIFICATION for unknown types (fail-open)
175
+ event_type = self.EVENT_MAP.get(hook_type, HookEventType.NOTIFICATION)
176
+
177
+ # Extract session_id
178
+ session_id = input_data.get("session_id", "")
179
+
180
+ # Parse timestamp if present (Gemini uses ISO format)
181
+ timestamp_str = input_data.get("timestamp")
182
+ if timestamp_str:
183
+ try:
184
+ timestamp = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
185
+ except (ValueError, AttributeError):
186
+ timestamp = datetime.now(UTC)
187
+ else:
188
+ timestamp = datetime.now(UTC)
189
+
190
+ # Get machine_id (Gemini might not send it)
191
+ machine_id = input_data.get("machine_id") or self._get_machine_id()
192
+
193
+ # Normalize tool name if present (for tool-related hooks)
194
+ if "tool_name" in input_data:
195
+ original_tool = input_data.get("tool_name", "")
196
+ normalized_tool = self.normalize_tool_name(original_tool)
197
+ # Store both for logging/debugging
198
+ metadata = {
199
+ "original_tool_name": original_tool,
200
+ "normalized_tool_name": normalized_tool,
201
+ }
202
+ else:
203
+ metadata = {}
204
+
205
+ return HookEvent(
206
+ event_type=event_type,
207
+ session_id=session_id,
208
+ source=self.source,
209
+ timestamp=timestamp,
210
+ machine_id=machine_id,
211
+ cwd=input_data.get("cwd"),
212
+ data=input_data,
213
+ metadata=metadata,
214
+ )
215
+
216
+ def translate_from_hook_response(
217
+ self, response: HookResponse, hook_type: str | None = None
218
+ ) -> dict[str, Any]:
219
+ """Convert HookResponse to Gemini CLI's expected format.
220
+
221
+ Gemini CLI expects responses in this format:
222
+ {
223
+ "decision": "allow" | "deny", # Whether to allow the action
224
+ "reason": "...", # Optional reason for decision
225
+ "hookSpecificOutput": { # Hook-specific response data
226
+ "additionalContext": "...", # Context to inject
227
+ "llm_request": {...}, # For BeforeModel hooks
228
+ "toolConfig": {...} # For BeforeToolSelection hooks
229
+ }
230
+ }
231
+
232
+ Exit codes: 0 = allow, 2 = deny (handled by dispatcher)
233
+
234
+ Args:
235
+ response: Unified HookResponse from HookManager.
236
+ hook_type: Original Gemini CLI hook type (e.g., "SessionStart")
237
+ Used to format hookSpecificOutput appropriately.
238
+
239
+ Returns:
240
+ Dict in Gemini CLI's expected format.
241
+ """
242
+ result: dict[str, Any] = {
243
+ "decision": response.decision,
244
+ }
245
+
246
+ # Add reason if present
247
+ if response.reason:
248
+ result["reason"] = response.reason
249
+
250
+ # Build hookSpecificOutput based on hook type
251
+ hook_specific: dict[str, Any] = {}
252
+
253
+ # Add context injection if present
254
+ if response.context:
255
+ hook_specific["additionalContext"] = response.context
256
+
257
+ # Add session/terminal context for SessionStart only
258
+ if hook_type == "SessionStart" and response.metadata:
259
+ session_id = response.metadata.get("session_id")
260
+ if session_id:
261
+ hook_event_name = self.HOOK_EVENT_NAME_MAP.get(hook_type, "Unknown")
262
+ context_lines = [f"session_id: {session_id}"]
263
+ if response.metadata.get("parent_session_id"):
264
+ context_lines.append(
265
+ f"parent_session_id: {response.metadata['parent_session_id']}"
266
+ )
267
+ if response.metadata.get("machine_id"):
268
+ context_lines.append(f"machine_id: {response.metadata['machine_id']}")
269
+ if response.metadata.get("project_id"):
270
+ context_lines.append(f"project_id: {response.metadata['project_id']}")
271
+ # Add terminal context (non-null values only)
272
+ if response.metadata.get("terminal_term_program"):
273
+ context_lines.append(f"terminal: {response.metadata['terminal_term_program']}")
274
+ if response.metadata.get("terminal_tty"):
275
+ context_lines.append(f"tty: {response.metadata['terminal_tty']}")
276
+ if response.metadata.get("terminal_parent_pid"):
277
+ context_lines.append(f"parent_pid: {response.metadata['terminal_parent_pid']}")
278
+ # Add terminal-specific session IDs
279
+ for key in [
280
+ "terminal_iterm_session_id",
281
+ "terminal_term_session_id",
282
+ "terminal_kitty_window_id",
283
+ "terminal_tmux_pane",
284
+ "terminal_vscode_terminal_id",
285
+ "terminal_alacritty_socket",
286
+ ]:
287
+ if response.metadata.get(key):
288
+ friendly_name = key.replace("terminal_", "").replace("_", " ")
289
+ context_lines.append(f"{friendly_name}: {response.metadata[key]}")
290
+ hook_specific["hookEventName"] = hook_event_name
291
+ # Append to existing additionalContext if present
292
+ existing = hook_specific.get("additionalContext", "")
293
+ new_context = "\n".join(context_lines)
294
+ hook_specific["additionalContext"] = (
295
+ f"{existing}\n{new_context}" if existing else new_context
296
+ )
297
+
298
+ # Handle BeforeModel-specific output (llm_request modification)
299
+ if hook_type == "BeforeModel" and response.modify_args:
300
+ hook_specific["llm_request"] = response.modify_args
301
+
302
+ # Handle BeforeToolSelection-specific output (toolConfig modification)
303
+ if hook_type == "BeforeToolSelection" and response.modify_args:
304
+ hook_specific["toolConfig"] = response.modify_args
305
+
306
+ # Only add hookSpecificOutput if there's content
307
+ if hook_specific:
308
+ result["hookSpecificOutput"] = hook_specific
309
+
310
+ # Add system message if present (user-visible notification)
311
+ if response.system_message:
312
+ result["systemMessage"] = response.system_message
313
+
314
+ return result
315
+
316
+ def handle_native(
317
+ self, native_event: dict[str, Any], hook_manager: "HookManager"
318
+ ) -> dict[str, Any]:
319
+ """Main entry point for HTTP endpoint.
320
+
321
+ Translates native Gemini CLI event, processes through HookManager,
322
+ and returns response in Gemini's expected format.
323
+
324
+ Args:
325
+ native_event: Raw payload from Gemini CLI's hook_dispatcher.py
326
+ hook_manager: HookManager instance for processing.
327
+
328
+ Returns:
329
+ Response dict in Gemini CLI's expected format.
330
+ """
331
+ # Translate to unified HookEvent
332
+ hook_event = self.translate_to_hook_event(native_event)
333
+
334
+ # Get original hook type for response formatting
335
+ hook_type = native_event.get("hook_type", "")
336
+ if not hook_type:
337
+ hook_type = native_event.get("input_data", {}).get("hook_event_name", "")
338
+
339
+ # Process through HookManager
340
+ hook_response = hook_manager.handle(hook_event)
341
+
342
+ # Translate response back to Gemini format
343
+ return self.translate_from_hook_response(hook_response, hook_type=hook_type)
@@ -0,0 +1,37 @@
1
+ """
2
+ Gobby Agents Module.
3
+
4
+ This module provides the subagent spawning system, enabling agents to spawn
5
+ independent subagents that can use any LLM provider and follow workflows.
6
+
7
+ Components:
8
+ - AgentRunner: Orchestrates agent execution with workflow integration
9
+ - Session management: Creates and links child sessions to parents
10
+ - Terminal spawning: Launches agents in separate terminal windows
11
+
12
+ Usage:
13
+ from gobby.agents import AgentRunner, AgentConfig
14
+
15
+ runner = AgentRunner(db, session_storage, executors)
16
+ result = await runner.run(AgentConfig(
17
+ prompt="Review the auth changes",
18
+ parent_session_id="sess-123",
19
+ project_id="proj-abc",
20
+ machine_id="machine-1",
21
+ source="claude",
22
+ provider="claude",
23
+ ))
24
+ """
25
+
26
+ from gobby.agents.registry import RunningAgent
27
+ from gobby.agents.runner import AgentConfig, AgentRunContext, AgentRunner
28
+ from gobby.agents.session import ChildSessionConfig, ChildSessionManager
29
+
30
+ __all__ = [
31
+ "AgentConfig",
32
+ "AgentRunContext",
33
+ "AgentRunner",
34
+ "RunningAgent",
35
+ "ChildSessionConfig",
36
+ "ChildSessionManager",
37
+ ]
@@ -0,0 +1,120 @@
1
+ """Codex session ID capture utility.
2
+
3
+ Captures Codex's session_id from the startup banner before launching interactive mode.
4
+ This is necessary because we need the session_id to:
5
+ 1. Link to Gobby sessions (external_id)
6
+ 2. Resume functionality with `codex resume {session_id}`
7
+ 3. MCP tool calls that require session context
8
+ """
9
+
10
+ import asyncio
11
+ import logging
12
+ import re
13
+ from dataclasses import dataclass
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Regex to match "session id: {uuid}" in startup banner
18
+ SESSION_ID_PATTERN = re.compile(r"^session id:\s*([0-9a-f-]+)$", re.IGNORECASE)
19
+
20
+
21
+ @dataclass
22
+ class CodexSessionInfo:
23
+ """Captured Codex session information."""
24
+
25
+ session_id: str
26
+ model: str | None = None
27
+ workdir: str | None = None
28
+
29
+
30
+ async def capture_codex_session_id(
31
+ timeout: float = 30.0,
32
+ ) -> CodexSessionInfo:
33
+ """Capture Codex's session_id via preflight exec call.
34
+
35
+ Launches Codex with minimal command (`codex exec "exit"`),
36
+ parses the startup banner for session_id, then returns.
37
+
38
+ The Codex banner format is:
39
+ OpenAI Codex v0.80.0 (research preview)
40
+ --------
41
+ workdir: /path/to/dir
42
+ model: gpt-5.2-codex
43
+ ...
44
+ session id: 019bbaea-3e0f-7d61-afc4-56a9456c2c7d
45
+ --------
46
+
47
+ Args:
48
+ timeout: Max seconds to wait for exec to complete (default 30s
49
+ to account for model loading time)
50
+
51
+ Returns:
52
+ CodexSessionInfo with captured session_id and metadata
53
+
54
+ Raises:
55
+ asyncio.TimeoutError: If exec doesn't complete within timeout
56
+ ValueError: If session_id not found in output
57
+ FileNotFoundError: If codex CLI is not installed
58
+ """
59
+ logger.debug("Starting Codex preflight to capture session_id")
60
+
61
+ try:
62
+ proc = await asyncio.create_subprocess_exec(
63
+ "codex",
64
+ "exec",
65
+ "exit",
66
+ stdout=asyncio.subprocess.PIPE,
67
+ stderr=asyncio.subprocess.PIPE,
68
+ )
69
+ except FileNotFoundError as e:
70
+ raise FileNotFoundError(
71
+ "Codex CLI not found. Install from: https://github.com/openai/codex"
72
+ ) from e
73
+
74
+ try:
75
+ stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
76
+ except TimeoutError:
77
+ proc.kill()
78
+ await proc.wait()
79
+ raise
80
+
81
+ # Codex outputs the startup banner (including session id) to stderr
82
+ output = stderr.decode()
83
+ session_id: str | None = None
84
+ model: str | None = None
85
+ workdir: str | None = None
86
+
87
+ # Parse the startup banner
88
+ for line in output.splitlines():
89
+ line = line.strip()
90
+
91
+ # Match session id
92
+ match = SESSION_ID_PATTERN.match(line)
93
+ if match:
94
+ session_id = match.group(1)
95
+ continue
96
+
97
+ # Extract model if present
98
+ if line.startswith("model:"):
99
+ model = line.split(":", 1)[1].strip()
100
+ continue
101
+
102
+ # Extract workdir if present
103
+ if line.startswith("workdir:"):
104
+ workdir = line.split(":", 1)[1].strip()
105
+ continue
106
+
107
+ if not session_id:
108
+ # Log the output for debugging
109
+ logger.error(f"Failed to parse Codex output:\n{output}")
110
+ if stderr:
111
+ logger.error(f"Codex stderr:\n{stderr.decode()}")
112
+ raise ValueError("No session id found in Codex output")
113
+
114
+ logger.debug(f"Captured Codex session_id: {session_id}, model: {model}, workdir: {workdir}")
115
+
116
+ return CodexSessionInfo(
117
+ session_id=session_id,
118
+ model=model,
119
+ workdir=workdir,
120
+ )
@@ -0,0 +1,112 @@
1
+ """
2
+ Constants for agent spawning and terminal mode.
3
+
4
+ This module defines environment variables used to pass context to
5
+ spawned terminal processes. When an agent spawns a child in terminal
6
+ mode, these environment variables are set in the child process.
7
+ """
8
+
9
+ # ============================================================================
10
+ # Terminal Mode Environment Variables
11
+ # ============================================================================
12
+ # These environment variables are set when spawning a terminal-mode agent.
13
+ # The child CLI process reads these to pick up its prepared state.
14
+ # ============================================================================
15
+
16
+ # Session identifier for the pre-created child session
17
+ # The spawned CLI uses this to connect to its session via hooks
18
+ GOBBY_SESSION_ID = "GOBBY_SESSION_ID"
19
+
20
+ # Parent session identifier for context resolution
21
+ # Used to look up parent session for context injection
22
+ GOBBY_PARENT_SESSION_ID = "GOBBY_PARENT_SESSION_ID"
23
+
24
+ # Agent run record identifier
25
+ # Links the terminal process back to its agent_runs record
26
+ GOBBY_AGENT_RUN_ID = "GOBBY_AGENT_RUN_ID"
27
+
28
+ # Workflow name to activate on session start
29
+ # The hook reads this and activates the workflow for the session
30
+ GOBBY_WORKFLOW_NAME = "GOBBY_WORKFLOW_NAME"
31
+
32
+ # Project identifier for the session
33
+ # Used for project-scoped operations
34
+ GOBBY_PROJECT_ID = "GOBBY_PROJECT_ID"
35
+
36
+ # Current agent nesting depth
37
+ # 0 = human-initiated, 1+ = agent-spawned
38
+ GOBBY_AGENT_DEPTH = "GOBBY_AGENT_DEPTH"
39
+
40
+ # Maximum allowed agent depth
41
+ # Prevents infinite nesting
42
+ GOBBY_MAX_AGENT_DEPTH = "GOBBY_MAX_AGENT_DEPTH"
43
+
44
+ # Initial prompt for the agent (short prompts only)
45
+ # For longer prompts, use GOBBY_PROMPT_FILE instead
46
+ GOBBY_PROMPT = "GOBBY_PROMPT"
47
+
48
+ # Path to file containing initial prompt (for long prompts)
49
+ # Takes precedence over GOBBY_PROMPT if both are set
50
+ GOBBY_PROMPT_FILE = "GOBBY_PROMPT_FILE"
51
+
52
+
53
+ def get_terminal_env_vars(
54
+ session_id: str,
55
+ parent_session_id: str,
56
+ agent_run_id: str,
57
+ project_id: str,
58
+ workflow_name: str | None = None,
59
+ agent_depth: int = 1,
60
+ max_agent_depth: int = 3,
61
+ prompt: str | None = None,
62
+ prompt_file: str | None = None,
63
+ ) -> dict[str, str]:
64
+ """
65
+ Build environment variables dict for spawning a terminal-mode agent.
66
+
67
+ Args:
68
+ session_id: The pre-created child session ID.
69
+ parent_session_id: The parent session ID for context resolution.
70
+ agent_run_id: The agent run record ID.
71
+ project_id: The project ID.
72
+ workflow_name: Optional workflow to activate.
73
+ agent_depth: Current nesting depth (default: 1).
74
+ max_agent_depth: Maximum allowed depth (default: 3).
75
+ prompt: Optional short prompt (for inline passing).
76
+ prompt_file: Optional path to file containing prompt (for long prompts).
77
+
78
+ Returns:
79
+ Dict of environment variable name to value.
80
+ """
81
+ env = {
82
+ GOBBY_SESSION_ID: session_id,
83
+ GOBBY_PARENT_SESSION_ID: parent_session_id,
84
+ GOBBY_AGENT_RUN_ID: agent_run_id,
85
+ GOBBY_PROJECT_ID: project_id,
86
+ GOBBY_AGENT_DEPTH: str(agent_depth),
87
+ GOBBY_MAX_AGENT_DEPTH: str(max_agent_depth),
88
+ }
89
+
90
+ if workflow_name:
91
+ env[GOBBY_WORKFLOW_NAME] = workflow_name
92
+
93
+ if prompt_file:
94
+ env[GOBBY_PROMPT_FILE] = prompt_file
95
+ elif prompt:
96
+ env[GOBBY_PROMPT] = prompt
97
+
98
+ return env
99
+
100
+
101
+ # List of all environment variable names for documentation
102
+ ALL_TERMINAL_ENV_VARS = [
103
+ GOBBY_SESSION_ID,
104
+ GOBBY_PARENT_SESSION_ID,
105
+ GOBBY_AGENT_RUN_ID,
106
+ GOBBY_WORKFLOW_NAME,
107
+ GOBBY_PROJECT_ID,
108
+ GOBBY_AGENT_DEPTH,
109
+ GOBBY_MAX_AGENT_DEPTH,
110
+ GOBBY_PROMPT,
111
+ GOBBY_PROMPT_FILE,
112
+ ]