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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
gobby/hooks/webhooks.py
ADDED
|
@@ -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."
|