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,399 @@
1
+ """
2
+ Webhook action executor for workflows.
3
+
4
+ Executes HTTP requests as workflow actions with retry logic,
5
+ variable interpolation, and response capture.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import json
12
+ import logging
13
+ import re
14
+ import time
15
+ from collections.abc import Callable, Coroutine
16
+ from dataclasses import dataclass, field
17
+ from typing import Any
18
+
19
+ import aiohttp
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @dataclass
25
+ class WebhookResult:
26
+ """Result of a webhook execution."""
27
+
28
+ success: bool
29
+ status_code: int | None = None
30
+ body: str | None = None
31
+ headers: dict[str, str] | None = None
32
+ error: str | None = None
33
+
34
+ def json_body(self) -> dict[str, Any] | None:
35
+ """Parse body as JSON.
36
+
37
+ Returns:
38
+ Parsed JSON dict, or None if body is not valid JSON.
39
+ """
40
+ if not self.body:
41
+ return None
42
+ try:
43
+ result: dict[str, Any] = json.loads(self.body)
44
+ return result
45
+ except json.JSONDecodeError:
46
+ return None
47
+
48
+
49
+ @dataclass
50
+ class RetryConfig:
51
+ """Configuration for retry behavior."""
52
+
53
+ max_attempts: int = 3
54
+ backoff_seconds: float = 1.0
55
+ retry_on_status: list[int] = field(default_factory=lambda: [429, 500, 502, 503, 504])
56
+
57
+
58
+ class WebhookExecutor:
59
+ """Executes webhook HTTP requests from workflows.
60
+
61
+ Handles URL resolution, variable interpolation, retries,
62
+ and response capture.
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ template_engine: Any | None = None,
68
+ webhook_registry: dict[str, dict[str, Any]] | None = None,
69
+ secrets: dict[str, str] | None = None,
70
+ ):
71
+ """Initialize the executor.
72
+
73
+ Args:
74
+ template_engine: Optional template engine for variable interpolation.
75
+ webhook_registry: Dict mapping webhook_id to config (url, headers, etc.).
76
+ secrets: Dict of secret values for ${secrets.VAR} interpolation.
77
+ """
78
+ self.template_engine = template_engine
79
+ self.webhook_registry = webhook_registry or {}
80
+ self.secrets = secrets or {}
81
+
82
+ async def execute(
83
+ self,
84
+ url: str,
85
+ method: str = "POST",
86
+ headers: dict[str, str] | None = None,
87
+ payload: dict[str, Any] | str | None = None,
88
+ timeout: int = 30,
89
+ retry_config: dict[str, Any] | None = None,
90
+ context: dict[str, Any] | None = None,
91
+ on_success: Callable[[WebhookResult], Coroutine[Any, Any, None]] | None = None,
92
+ on_failure: Callable[[WebhookResult], Coroutine[Any, Any, None]] | None = None,
93
+ ) -> WebhookResult:
94
+ """Execute a webhook HTTP request.
95
+
96
+ Args:
97
+ url: Target URL for the request.
98
+ method: HTTP method (GET, POST, PUT, PATCH, DELETE).
99
+ headers: Request headers (supports ${secrets.VAR} interpolation).
100
+ payload: Request body as dict or string.
101
+ timeout: Request timeout in seconds.
102
+ retry_config: Optional retry configuration dict.
103
+ context: Context dict for variable interpolation.
104
+ on_success: Async callback for successful (2xx) response.
105
+ on_failure: Async callback after all retries exhausted.
106
+
107
+ Returns:
108
+ WebhookResult with response data or error.
109
+ """
110
+ headers = headers or {}
111
+ context = context or {}
112
+
113
+ # Interpolate secrets in headers
114
+ interpolated_headers = self._interpolate_secrets(headers)
115
+
116
+ # Interpolate context in payload
117
+ interpolated_payload = self._interpolate_payload(payload, context)
118
+
119
+ # Parse retry config
120
+ retry = self._parse_retry_config(retry_config)
121
+
122
+ # Execute with retry logic
123
+ result = await self._execute_with_retry(
124
+ url=url,
125
+ method=method,
126
+ headers=interpolated_headers,
127
+ payload=interpolated_payload,
128
+ timeout=timeout,
129
+ retry=retry,
130
+ )
131
+
132
+ # Call appropriate handler
133
+ if result.success and on_success:
134
+ await on_success(result)
135
+ elif not result.success and on_failure:
136
+ await on_failure(result)
137
+
138
+ return result
139
+
140
+ async def execute_by_webhook_id(
141
+ self,
142
+ webhook_id: str,
143
+ payload: dict[str, Any] | str | None = None,
144
+ method: str | None = None,
145
+ headers: dict[str, str] | None = None,
146
+ timeout: int | None = None,
147
+ context: dict[str, Any] | None = None,
148
+ retry_config: dict[str, Any] | None = None,
149
+ on_success: Callable[[WebhookResult], Coroutine[Any, Any, None]] | None = None,
150
+ on_failure: Callable[[WebhookResult], Coroutine[Any, Any, None]] | None = None,
151
+ ) -> WebhookResult:
152
+ """Execute a webhook by looking up its ID in the registry.
153
+
154
+ Args:
155
+ webhook_id: ID of the webhook in the registry.
156
+ payload: Request body.
157
+ method: Override HTTP method from registry.
158
+ headers: Additional headers (merged with registry headers).
159
+ timeout: Override timeout from registry.
160
+ context: Context for variable interpolation.
161
+ retry_config: Optional retry configuration dict.
162
+ on_success: Async callback for successful (2xx) response.
163
+ on_failure: Async callback after all retries exhausted.
164
+
165
+ Returns:
166
+ WebhookResult with response data or error.
167
+
168
+ Raises:
169
+ ValueError: If webhook_id not found in registry.
170
+ """
171
+ if webhook_id not in self.webhook_registry:
172
+ raise ValueError(f"webhook_id '{webhook_id}' not found in registry")
173
+
174
+ config = self.webhook_registry[webhook_id]
175
+ url = config.get("url")
176
+ if not url:
177
+ raise ValueError(f"webhook_id '{webhook_id}' has no URL configured")
178
+
179
+ # Merge headers (registry defaults + provided overrides)
180
+ merged_headers = dict(config.get("headers", {}))
181
+ if headers:
182
+ merged_headers.update(headers)
183
+
184
+ return await self.execute(
185
+ url=url,
186
+ method=method or config.get("method", "POST"),
187
+ headers=merged_headers,
188
+ payload=payload,
189
+ timeout=timeout or config.get("timeout", 30),
190
+ context=context,
191
+ retry_config=retry_config,
192
+ on_success=on_success,
193
+ on_failure=on_failure,
194
+ )
195
+
196
+ def _interpolate_secrets(self, headers: dict[str, str]) -> dict[str, str]:
197
+ """Interpolate ${secrets.VAR} in header values.
198
+
199
+ Args:
200
+ headers: Headers dict with potential secret references.
201
+
202
+ Returns:
203
+ Headers with secrets interpolated.
204
+
205
+ Raises:
206
+ ValueError: If a referenced secret is not found in self.secrets.
207
+ """
208
+ result = {}
209
+ pattern = re.compile(r"\$\{secrets\.(\w+)\}")
210
+
211
+ for key, value in headers.items():
212
+ if isinstance(value, str):
213
+ # Find all secret references in the value
214
+ matches = pattern.findall(value)
215
+ for secret_name in matches:
216
+ if secret_name not in self.secrets:
217
+ raise ValueError(
218
+ f"Missing secret '{secret_name}' referenced in header '{key}'"
219
+ )
220
+ # Replace all secrets with their values
221
+ result[key] = pattern.sub(
222
+ lambda m: self.secrets[m.group(1)],
223
+ value,
224
+ )
225
+ else:
226
+ result[key] = value
227
+
228
+ return result
229
+
230
+ def _interpolate_payload(
231
+ self,
232
+ payload: dict[str, Any] | str | None,
233
+ context: dict[str, Any],
234
+ ) -> dict[str, Any] | str | None:
235
+ """Interpolate context variables in payload.
236
+
237
+ Args:
238
+ payload: Payload to interpolate.
239
+ context: Context dict for variable values.
240
+
241
+ Returns:
242
+ Interpolated payload.
243
+ """
244
+ if payload is None:
245
+ return None
246
+
247
+ if self.template_engine and isinstance(payload, str):
248
+ rendered: str = self.template_engine.render(payload, context)
249
+ return rendered
250
+
251
+ # For dicts, we could deep-interpolate, but for now just return as-is
252
+ # since the tests expect the executor to handle the interpolation
253
+ return payload
254
+
255
+ def _parse_retry_config(self, config: dict[str, Any] | None) -> RetryConfig:
256
+ """Parse retry configuration from dict.
257
+
258
+ Args:
259
+ config: Retry config dict or None.
260
+
261
+ Returns:
262
+ RetryConfig instance.
263
+ """
264
+ if not config:
265
+ return RetryConfig(max_attempts=1) # No retry by default
266
+
267
+ return RetryConfig(
268
+ max_attempts=config.get("max_attempts", 3),
269
+ backoff_seconds=config.get("backoff_seconds", 1.0),
270
+ retry_on_status=config.get("retry_on_status", [429, 500, 502, 503, 504]),
271
+ )
272
+
273
+ async def _execute_with_retry(
274
+ self,
275
+ url: str,
276
+ method: str,
277
+ headers: dict[str, str],
278
+ payload: dict[str, Any] | str | None,
279
+ timeout: int,
280
+ retry: RetryConfig,
281
+ ) -> WebhookResult:
282
+ """Execute request with retry logic.
283
+
284
+ Args:
285
+ url: Target URL.
286
+ method: HTTP method.
287
+ headers: Request headers.
288
+ payload: Request body.
289
+ timeout: Timeout in seconds.
290
+ retry: Retry configuration.
291
+
292
+ Returns:
293
+ WebhookResult with response or error.
294
+ """
295
+ last_error: str | None = None
296
+ last_status: int | None = None
297
+
298
+ for attempt in range(retry.max_attempts):
299
+ if attempt > 0:
300
+ # Exponential backoff
301
+ delay = retry.backoff_seconds * (2 ** (attempt - 1))
302
+ logger.debug(f"Webhook retry {attempt + 1}/{retry.max_attempts}, backoff {delay}s")
303
+ await asyncio.sleep(delay)
304
+
305
+ try:
306
+ start_time = time.time()
307
+ result = await self._make_request(
308
+ url=url,
309
+ method=method,
310
+ headers=headers,
311
+ payload=payload,
312
+ timeout=timeout,
313
+ )
314
+ elapsed = time.time() - start_time
315
+ logger.debug(f"Webhook {method} {url} -> {result.status_code} ({elapsed:.2f}s)")
316
+
317
+ if result.success:
318
+ return result
319
+
320
+ # Check if we should retry
321
+ if result.status_code and result.status_code in retry.retry_on_status:
322
+ last_error = f"HTTP {result.status_code}"
323
+ last_status = result.status_code
324
+ continue # Retry
325
+
326
+ # Non-retryable error
327
+ return result
328
+
329
+ except TimeoutError:
330
+ last_error = f"Timeout after {timeout}s"
331
+ logger.debug(f"Webhook timeout: {url}")
332
+ continue # Retry on timeout
333
+
334
+ except aiohttp.ClientError as e:
335
+ last_error = str(e)
336
+ logger.debug(f"Webhook connection error: {url} - {e}")
337
+ continue # Retry on aiohttp client errors
338
+
339
+ # All retries exhausted
340
+ return WebhookResult(
341
+ success=False,
342
+ status_code=last_status,
343
+ body=None,
344
+ headers=None,
345
+ error=last_error or "Unknown error",
346
+ )
347
+
348
+ async def _make_request(
349
+ self,
350
+ url: str,
351
+ method: str,
352
+ headers: dict[str, str],
353
+ payload: dict[str, Any] | str | None,
354
+ timeout: int,
355
+ ) -> WebhookResult:
356
+ """Make a single HTTP request.
357
+
358
+ Args:
359
+ url: Target URL.
360
+ method: HTTP method.
361
+ headers: Request headers.
362
+ payload: Request body.
363
+ timeout: Timeout in seconds.
364
+
365
+ Returns:
366
+ WebhookResult with response data.
367
+ """
368
+ client_timeout = aiohttp.ClientTimeout(total=timeout)
369
+
370
+ async with aiohttp.ClientSession(timeout=client_timeout) as session:
371
+ # Prepare request kwargs
372
+ kwargs: dict[str, Any] = {
373
+ "method": method,
374
+ "url": url,
375
+ "headers": headers,
376
+ }
377
+
378
+ # Add payload
379
+ if payload is not None:
380
+ if isinstance(payload, dict):
381
+ kwargs["json"] = payload
382
+ else:
383
+ kwargs["data"] = payload
384
+
385
+ async with session.request(**kwargs) as response:
386
+ body = await response.text()
387
+
388
+ # Convert headers to dict
389
+ response_headers = dict(response.headers)
390
+
391
+ success = 200 <= response.status < 300
392
+
393
+ return WebhookResult(
394
+ success=success,
395
+ status_code=response.status,
396
+ body=body,
397
+ headers=response_headers,
398
+ error=None if success else f"HTTP {response.status}",
399
+ )
@@ -0,0 +1,5 @@
1
+ """Worktree management module."""
2
+
3
+ from gobby.worktrees.git import WorktreeGitManager
4
+
5
+ __all__ = ["WorktreeGitManager"]