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,339 @@
1
+ """Webhook dispatcher for HTTP callouts on hook events.
2
+
3
+ This module implements config-driven HTTP webhooks that can be triggered
4
+ by hook events. It supports:
5
+ - Event filtering per endpoint
6
+ - Retry with exponential backoff
7
+ - Blocking webhooks (can_block) that can deny actions
8
+ - Async dispatch for non-blocking webhooks
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import json
15
+ import logging
16
+ from dataclasses import dataclass
17
+ from datetime import datetime
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ import httpx
21
+
22
+ if TYPE_CHECKING:
23
+ from gobby.config.app import WebhookEndpointConfig, WebhooksConfig
24
+ from gobby.hooks.events import HookEvent
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ @dataclass
30
+ class WebhookResult:
31
+ """Result of a webhook dispatch attempt."""
32
+
33
+ endpoint_name: str
34
+ success: bool
35
+ status_code: int | None = None
36
+ response_body: dict[str, Any] | None = None
37
+ error: str | None = None
38
+ attempts: int = 1
39
+ duration_ms: float = 0.0
40
+ decision: str | None = None # For blocking webhooks
41
+
42
+
43
+ class WebhookDispatcher:
44
+ """Dispatches HTTP webhooks on hook events.
45
+
46
+ The dispatcher handles:
47
+ - Matching events to configured webhook endpoints
48
+ - HTTP POST requests with JSON payloads
49
+ - Retry logic with exponential backoff
50
+ - Blocking webhooks that can influence hook decisions
51
+
52
+ Usage:
53
+ dispatcher = WebhookDispatcher(config)
54
+ results = await dispatcher.trigger(event)
55
+
56
+ # For blocking webhooks, check decision
57
+ for result in results:
58
+ if result.decision == "block":
59
+ # Handle blocked action
60
+ """
61
+
62
+ def __init__(self, config: WebhooksConfig) -> None:
63
+ """Initialize the webhook dispatcher.
64
+
65
+ Args:
66
+ config: Webhooks configuration containing endpoints and settings.
67
+ """
68
+ self.config = config
69
+ self._client: httpx.AsyncClient | None = None
70
+ self._client_lock = asyncio.Lock()
71
+
72
+ async def _get_client(self) -> httpx.AsyncClient:
73
+ """Get or create the HTTP client.
74
+
75
+ Uses double-checked locking to ensure only one client is created
76
+ even when called concurrently from multiple coroutines.
77
+ """
78
+ if self._client is None:
79
+ async with self._client_lock:
80
+ # Double-check after acquiring lock
81
+ if self._client is None:
82
+ self._client = httpx.AsyncClient(
83
+ timeout=httpx.Timeout(self.config.default_timeout),
84
+ follow_redirects=True,
85
+ )
86
+ return self._client
87
+
88
+ async def close(self) -> None:
89
+ """Close the HTTP client."""
90
+ if self._client is not None:
91
+ await self._client.aclose()
92
+ self._client = None
93
+
94
+ def _matches_event(self, endpoint: WebhookEndpointConfig, event_type: str) -> bool:
95
+ """Check if an endpoint should receive the given event type.
96
+
97
+ Args:
98
+ endpoint: The webhook endpoint configuration.
99
+ event_type: The hook event type string.
100
+
101
+ Returns:
102
+ True if the endpoint should receive this event.
103
+ """
104
+ # Empty events list means all events
105
+ if not endpoint.events:
106
+ return True
107
+
108
+ # Normalize event type for comparison (handle both formats)
109
+ # e.g., "session_start" matches "session-start" or "SESSION_START"
110
+ normalized = event_type.lower().replace("-", "_")
111
+ for configured_event in endpoint.events:
112
+ if configured_event.lower().replace("-", "_") == normalized:
113
+ return True
114
+
115
+ return False
116
+
117
+ def _build_payload(self, event: HookEvent) -> dict[str, Any]:
118
+ """Build the webhook payload from a hook event.
119
+
120
+ Args:
121
+ event: The hook event to convert to a payload.
122
+
123
+ Returns:
124
+ Dictionary payload for the webhook POST body.
125
+ """
126
+ return {
127
+ "event_type": event.event_type.value,
128
+ "session_id": event.session_id,
129
+ "source": event.source.value,
130
+ "timestamp": event.timestamp.isoformat(),
131
+ "data": event.data,
132
+ "machine_id": event.machine_id,
133
+ "cwd": event.cwd,
134
+ "project_id": event.project_id,
135
+ "task_id": event.task_id,
136
+ "metadata": event.metadata,
137
+ }
138
+
139
+ async def _dispatch_single(
140
+ self,
141
+ endpoint: WebhookEndpointConfig,
142
+ payload: dict[str, Any],
143
+ ) -> WebhookResult:
144
+ """Dispatch a webhook to a single endpoint with retry logic.
145
+
146
+ Args:
147
+ endpoint: The endpoint configuration.
148
+ payload: The JSON payload to send.
149
+
150
+ Returns:
151
+ WebhookResult with success/failure info.
152
+ """
153
+ client = await self._get_client()
154
+ start_time = datetime.now()
155
+ attempts = 0
156
+ last_error: str | None = None
157
+ delay = endpoint.retry_delay
158
+
159
+ # Build headers
160
+ headers = {
161
+ "Content-Type": "application/json",
162
+ "User-Agent": "Gobby-Webhook/1.0",
163
+ "X-Gobby-Event": payload.get("event_type", "unknown"),
164
+ }
165
+ headers.update(endpoint.headers)
166
+
167
+ while attempts <= endpoint.retry_count:
168
+ attempts += 1
169
+
170
+ try:
171
+ response = await client.post(
172
+ endpoint.url,
173
+ json=payload,
174
+ headers=headers,
175
+ timeout=endpoint.timeout,
176
+ )
177
+
178
+ duration_ms = (datetime.now() - start_time).total_seconds() * 1000
179
+
180
+ # Parse response body if JSON
181
+ response_body: dict[str, Any] | None = None
182
+ try:
183
+ response_body = response.json()
184
+ except (json.JSONDecodeError, ValueError):
185
+ pass
186
+
187
+ # Check if blocking webhook and extract decision
188
+ decision: str | None = None
189
+ if endpoint.can_block and response_body:
190
+ decision = response_body.get("decision")
191
+
192
+ # Success on 2xx status codes
193
+ if 200 <= response.status_code < 300:
194
+ logger.debug(f"Webhook {endpoint.name} succeeded: {response.status_code}")
195
+ return WebhookResult(
196
+ endpoint_name=endpoint.name,
197
+ success=True,
198
+ status_code=response.status_code,
199
+ response_body=response_body,
200
+ attempts=attempts,
201
+ duration_ms=duration_ms,
202
+ decision=decision,
203
+ )
204
+
205
+ # 4xx errors are not retryable (client error)
206
+ if 400 <= response.status_code < 500:
207
+ logger.warning(f"Webhook {endpoint.name} client error: {response.status_code}")
208
+ return WebhookResult(
209
+ endpoint_name=endpoint.name,
210
+ success=False,
211
+ status_code=response.status_code,
212
+ response_body=response_body,
213
+ error=f"HTTP {response.status_code}",
214
+ attempts=attempts,
215
+ duration_ms=duration_ms,
216
+ decision=decision,
217
+ )
218
+
219
+ # 5xx errors are retryable
220
+ last_error = f"HTTP {response.status_code}"
221
+ logger.warning(
222
+ f"Webhook {endpoint.name} server error: {response.status_code}, "
223
+ f"attempt {attempts}/{endpoint.retry_count + 1}"
224
+ )
225
+
226
+ except httpx.TimeoutException:
227
+ last_error = "Request timeout"
228
+ logger.warning(
229
+ f"Webhook {endpoint.name} timeout, "
230
+ f"attempt {attempts}/{endpoint.retry_count + 1}"
231
+ )
232
+
233
+ except httpx.ConnectError as e:
234
+ last_error = f"Connection error: {e}"
235
+ logger.warning(
236
+ f"Webhook {endpoint.name} connection error: {e}, "
237
+ f"attempt {attempts}/{endpoint.retry_count + 1}"
238
+ )
239
+
240
+ except Exception as e:
241
+ last_error = str(e)
242
+ logger.exception(f"Webhook {endpoint.name} unexpected error: {e}")
243
+
244
+ # Wait before retry with exponential backoff
245
+ if attempts <= endpoint.retry_count:
246
+ await asyncio.sleep(delay)
247
+ delay *= 2 # Exponential backoff
248
+
249
+ # All retries exhausted
250
+ duration_ms = (datetime.now() - start_time).total_seconds() * 1000
251
+ logger.error(f"Webhook {endpoint.name} failed after {attempts} attempts: {last_error}")
252
+
253
+ return WebhookResult(
254
+ endpoint_name=endpoint.name,
255
+ success=False,
256
+ error=last_error,
257
+ attempts=attempts,
258
+ duration_ms=duration_ms,
259
+ )
260
+
261
+ async def trigger(self, event: HookEvent) -> list[WebhookResult]:
262
+ """Trigger webhooks for a hook event.
263
+
264
+ Dispatches HTTP POST requests to all matching webhook endpoints.
265
+ Non-blocking webhooks are dispatched concurrently.
266
+ Blocking webhooks (can_block=True) are awaited for their decision.
267
+
268
+ Args:
269
+ event: The hook event that triggered this dispatch.
270
+
271
+ Returns:
272
+ List of WebhookResult objects for each endpoint triggered.
273
+ """
274
+ if not self.config.enabled:
275
+ return []
276
+
277
+ # Find matching endpoints
278
+ event_type = event.event_type.value
279
+ matching_endpoints = [
280
+ ep for ep in self.config.endpoints if ep.enabled and self._matches_event(ep, event_type)
281
+ ]
282
+
283
+ if not matching_endpoints:
284
+ return []
285
+
286
+ # Build payload once
287
+ payload = self._build_payload(event)
288
+
289
+ # Separate blocking and non-blocking webhooks
290
+ blocking = [ep for ep in matching_endpoints if ep.can_block]
291
+ non_blocking = [ep for ep in matching_endpoints if not ep.can_block]
292
+
293
+ results: list[WebhookResult] = []
294
+
295
+ # Dispatch blocking webhooks first (sequentially, need their decisions)
296
+ for endpoint in blocking:
297
+ result = await self._dispatch_single(endpoint, payload)
298
+ results.append(result)
299
+
300
+ # If a blocking webhook says "block", we might stop processing
301
+ # But we still dispatch all blocking webhooks to collect all decisions
302
+ if result.decision == "block":
303
+ logger.info(f"Blocking webhook {endpoint.name} returned decision: block")
304
+
305
+ # Dispatch non-blocking webhooks concurrently
306
+ if non_blocking:
307
+ if self.config.async_dispatch:
308
+ # Fire and forget for truly async dispatch
309
+ tasks = [self._dispatch_single(ep, payload) for ep in non_blocking]
310
+ non_blocking_results = await asyncio.gather(*tasks)
311
+ results.extend(non_blocking_results)
312
+ else:
313
+ # Sequential dispatch
314
+ for endpoint in non_blocking:
315
+ result = await self._dispatch_single(endpoint, payload)
316
+ results.append(result)
317
+
318
+ return results
319
+
320
+ def get_blocking_decision(self, results: list[WebhookResult]) -> tuple[str, str | None]:
321
+ """Get the overall decision from blocking webhook results.
322
+
323
+ If any blocking webhook returns "block" or "deny", the overall
324
+ decision is to block the action.
325
+
326
+ Args:
327
+ results: List of webhook results from trigger().
328
+
329
+ Returns:
330
+ Tuple of (decision, reason) where decision is "allow" or "block".
331
+ """
332
+ for result in results:
333
+ if result.decision in ("block", "deny"):
334
+ reason = None
335
+ if result.response_body:
336
+ reason = result.response_body.get("reason")
337
+ return ("block", reason)
338
+
339
+ return ("allow", None)
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: bug
3
+ description: Quickly create a bug task. Usage: /bug <title> [description]
4
+ ---
5
+
6
+ # /bug - Create Bug Task
7
+
8
+ Create a bug/defect task with the provided title and optional description.
9
+
10
+ ## Usage
11
+
12
+ ```text
13
+ /bug <title>
14
+ /bug <title> - <description>
15
+ ```
16
+
17
+ ## Examples
18
+
19
+ ```text
20
+ /bug Fix login timeout
21
+ /bug Database connection drops - Users report intermittent connection failures after 5 minutes of inactivity
22
+ ```
23
+
24
+ ## Action
25
+
26
+ Call `gobby-tasks.create_task` with:
27
+
28
+ - `title`: The bug title from user input
29
+ - `task_type`: "bug"
30
+ - `priority`: 1 (high - bugs are important)
31
+
32
+ Parse the user input:
33
+
34
+ - If input contains " - ", split into title and description
35
+ - Otherwise, use entire input as title
36
+
37
+ ```python
38
+ call_tool(
39
+ server_name="gobby-tasks",
40
+ tool_name="create_task",
41
+ arguments={
42
+ "title": "<parsed title>",
43
+ "description": "<parsed description if any>",
44
+ "task_type": "bug",
45
+ "priority": 1,
46
+ "session_id": "<session_id>" # Required - from session context
47
+ }
48
+ )
49
+ ```
50
+
51
+ After creating, confirm with the task reference (e.g., "Created bug #123: Fix login timeout").
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: chore
3
+ description: Quickly create a chore/maintenance task. Usage: /chore <title> [description]
4
+ ---
5
+
6
+ # /chore - Create Chore Task
7
+
8
+ Create a maintenance or housekeeping task with the provided title and optional description. For tasks that keep the codebase healthy but aren't features or bugs.
9
+
10
+ ## Usage
11
+
12
+ ```
13
+ /chore <title>
14
+ /chore <title> - <description>
15
+ ```
16
+
17
+ ## Examples
18
+
19
+ ```
20
+ /chore Update dependencies
21
+ /chore Clean up CI pipeline - Remove deprecated jobs and consolidate test stages
22
+ /chore Add missing type hints to utils module
23
+ /chore Rotate API keys
24
+ ```
25
+
26
+ ## Action
27
+
28
+ Call `gobby-tasks.create_task` with:
29
+ - `title`: The chore title from user input
30
+ - `task_type`: "chore"
31
+ - `priority`: 3 (low - maintenance tasks are important but rarely urgent)
32
+
33
+ Parse the user input:
34
+ - If input contains " - ", split into title and description
35
+ - Otherwise, use entire input as title
36
+
37
+ ```python
38
+ call_tool(
39
+ server_name="gobby-tasks",
40
+ tool_name="create_task",
41
+ arguments={
42
+ "title": "<parsed title>",
43
+ "description": "<parsed description if any>",
44
+ "task_type": "chore",
45
+ "priority": 3,
46
+ "session_id": "<session_id>" # Required - from session context
47
+ }
48
+ )
49
+ ```
50
+
51
+ After creating, confirm with the task reference (e.g., "Created chore #128: Update dependencies").
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: epic
3
+ description: Quickly create an epic (parent task for large features). Usage: /epic <title> [description]
4
+ ---
5
+
6
+ # /epic - Create Epic Task
7
+
8
+ Create an epic task - a parent container for a large feature or initiative that will be broken down into subtasks.
9
+
10
+ ## Usage
11
+
12
+ ```
13
+ /epic <title>
14
+ /epic <title> - <description>
15
+ ```
16
+
17
+ ## Examples
18
+
19
+ ```
20
+ /epic User authentication system
21
+ /epic API v2 migration - Migrate all endpoints from REST to GraphQL with backwards compatibility
22
+ /epic Performance optimization sprint
23
+ ```
24
+
25
+ ## Action
26
+
27
+ Call `gobby-tasks.create_task` with:
28
+ - `title`: The epic title from user input
29
+ - `task_type`: "epic"
30
+ - `priority`: 2 (medium - epics are tracked but individual subtasks drive priority)
31
+
32
+ Parse the user input:
33
+ - If input contains " - ", split into title and description
34
+ - Otherwise, use entire input as title
35
+
36
+ ```python
37
+ call_tool(
38
+ server_name="gobby-tasks",
39
+ tool_name="create_task",
40
+ arguments={
41
+ "title": "<parsed title>",
42
+ "description": "<parsed description if any>",
43
+ "task_type": "epic",
44
+ "priority": 2,
45
+ "session_id": "<session_id>" # Required - from session context
46
+ }
47
+ )
48
+ ```
49
+
50
+ After creating, confirm with the task reference and suggest next steps:
51
+ - "Created epic #127: User authentication system"
52
+ - "Use `expand_task` to break this down into subtasks."