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
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Event handlers module for hook event processing.
|
|
3
|
+
|
|
4
|
+
This module is extracted from hook_manager.py using Strangler Fig pattern.
|
|
5
|
+
It provides centralized event handler registration and dispatch.
|
|
6
|
+
|
|
7
|
+
Classes:
|
|
8
|
+
EventHandlers: Manages event handler registration and dispatch.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
from gobby.hooks.events import HookEvent, HookEventType, HookResponse
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from gobby.hooks.session_coordinator import SessionCoordinator
|
|
21
|
+
from gobby.sessions.manager import SessionManager
|
|
22
|
+
from gobby.sessions.summary import SummaryFileGenerator
|
|
23
|
+
from gobby.storage.session_messages import LocalSessionMessageManager
|
|
24
|
+
from gobby.storage.session_tasks import SessionTaskManager
|
|
25
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
26
|
+
from gobby.storage.tasks import LocalTaskManager
|
|
27
|
+
from gobby.workflows.hooks import WorkflowHookHandler
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EventHandlers:
|
|
31
|
+
"""
|
|
32
|
+
Manages event handler registration and dispatch.
|
|
33
|
+
|
|
34
|
+
Provides handler methods for all HookEventType values and a registration
|
|
35
|
+
mechanism for looking up handlers by event type.
|
|
36
|
+
|
|
37
|
+
Extracted from HookManager to separate event handling concerns.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
session_manager: SessionManager | None = None,
|
|
43
|
+
workflow_handler: WorkflowHookHandler | None = None,
|
|
44
|
+
session_storage: LocalSessionManager | None = None,
|
|
45
|
+
session_task_manager: SessionTaskManager | None = None,
|
|
46
|
+
message_processor: Any | None = None,
|
|
47
|
+
summary_file_generator: SummaryFileGenerator | None = None,
|
|
48
|
+
task_manager: LocalTaskManager | None = None,
|
|
49
|
+
session_coordinator: SessionCoordinator | None = None,
|
|
50
|
+
message_manager: LocalSessionMessageManager | None = None,
|
|
51
|
+
get_machine_id: Callable[[], str] | None = None,
|
|
52
|
+
resolve_project_id: Callable[[str | None, str | None], str] | None = None,
|
|
53
|
+
logger: logging.Logger | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Initialize EventHandlers.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
session_manager: SessionManager for session operations
|
|
60
|
+
workflow_handler: WorkflowHookHandler for lifecycle workflows
|
|
61
|
+
session_storage: LocalSessionManager for session storage
|
|
62
|
+
session_task_manager: SessionTaskManager for session-task links
|
|
63
|
+
message_processor: SessionMessageProcessor for message handling
|
|
64
|
+
summary_file_generator: SummaryFileGenerator for summaries
|
|
65
|
+
task_manager: LocalTaskManager for task operations
|
|
66
|
+
session_coordinator: SessionCoordinator for session tracking
|
|
67
|
+
message_manager: LocalSessionMessageManager for messages
|
|
68
|
+
get_machine_id: Function to get machine ID
|
|
69
|
+
resolve_project_id: Function to resolve project ID from cwd
|
|
70
|
+
logger: Optional logger instance
|
|
71
|
+
"""
|
|
72
|
+
self._session_manager = session_manager
|
|
73
|
+
self._workflow_handler = workflow_handler
|
|
74
|
+
self._session_storage = session_storage
|
|
75
|
+
self._session_task_manager = session_task_manager
|
|
76
|
+
self._message_processor = message_processor
|
|
77
|
+
self._summary_file_generator = summary_file_generator
|
|
78
|
+
self._task_manager = task_manager
|
|
79
|
+
self._session_coordinator = session_coordinator
|
|
80
|
+
self._message_manager = message_manager
|
|
81
|
+
self._get_machine_id = get_machine_id or (lambda: "unknown-machine")
|
|
82
|
+
self._resolve_project_id = resolve_project_id or (lambda p, c: p or "")
|
|
83
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
84
|
+
|
|
85
|
+
# Build handler map
|
|
86
|
+
self._handler_map: dict[HookEventType, Callable[[HookEvent], HookResponse]] = {
|
|
87
|
+
HookEventType.SESSION_START: self.handle_session_start,
|
|
88
|
+
HookEventType.SESSION_END: self.handle_session_end,
|
|
89
|
+
HookEventType.BEFORE_AGENT: self.handle_before_agent,
|
|
90
|
+
HookEventType.AFTER_AGENT: self.handle_after_agent,
|
|
91
|
+
HookEventType.BEFORE_TOOL: self.handle_before_tool,
|
|
92
|
+
HookEventType.AFTER_TOOL: self.handle_after_tool,
|
|
93
|
+
HookEventType.PRE_COMPACT: self.handle_pre_compact,
|
|
94
|
+
HookEventType.SUBAGENT_START: self.handle_subagent_start,
|
|
95
|
+
HookEventType.SUBAGENT_STOP: self.handle_subagent_stop,
|
|
96
|
+
HookEventType.NOTIFICATION: self.handle_notification,
|
|
97
|
+
HookEventType.BEFORE_TOOL_SELECTION: self.handle_before_tool_selection,
|
|
98
|
+
HookEventType.BEFORE_MODEL: self.handle_before_model,
|
|
99
|
+
HookEventType.AFTER_MODEL: self.handle_after_model,
|
|
100
|
+
HookEventType.PERMISSION_REQUEST: self.handle_permission_request,
|
|
101
|
+
HookEventType.STOP: self.handle_stop,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def get_handler(
|
|
105
|
+
self, event_type: HookEventType | str
|
|
106
|
+
) -> Callable[[HookEvent], HookResponse] | None:
|
|
107
|
+
"""
|
|
108
|
+
Get handler for an event type.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
event_type: The event type to get handler for
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Handler callable or None if not found
|
|
115
|
+
"""
|
|
116
|
+
if isinstance(event_type, str):
|
|
117
|
+
try:
|
|
118
|
+
event_type = HookEventType(event_type)
|
|
119
|
+
except ValueError:
|
|
120
|
+
return None
|
|
121
|
+
return self._handler_map.get(event_type)
|
|
122
|
+
|
|
123
|
+
def get_handler_map(self) -> dict[HookEventType, Callable[[HookEvent], HookResponse]]:
|
|
124
|
+
"""
|
|
125
|
+
Get a copy of the handler map.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Copy of handler map (modifications don't affect internal state)
|
|
129
|
+
"""
|
|
130
|
+
return dict(self._handler_map)
|
|
131
|
+
|
|
132
|
+
# ==================== SESSION HANDLERS ====================
|
|
133
|
+
|
|
134
|
+
def handle_session_start(self, event: HookEvent) -> HookResponse:
|
|
135
|
+
"""
|
|
136
|
+
Handle SESSION_START event.
|
|
137
|
+
|
|
138
|
+
Register session and execute session-handoff workflow.
|
|
139
|
+
"""
|
|
140
|
+
external_id = event.session_id
|
|
141
|
+
input_data = event.data
|
|
142
|
+
transcript_path = input_data.get("transcript_path")
|
|
143
|
+
cli_source = event.source.value
|
|
144
|
+
cwd = input_data.get("cwd")
|
|
145
|
+
session_source = input_data.get("source", "startup")
|
|
146
|
+
|
|
147
|
+
# Resolve project_id (auto-creates if needed)
|
|
148
|
+
project_id = self._resolve_project_id(input_data.get("project_id"), cwd)
|
|
149
|
+
# Always use Gobby's machine_id for cross-CLI consistency
|
|
150
|
+
machine_id = self._get_machine_id()
|
|
151
|
+
|
|
152
|
+
self.logger.debug(
|
|
153
|
+
f"SESSION_START: cli={cli_source}, project={project_id}, source={session_source}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Step 0: Check if this is a pre-created session (terminal mode agent)
|
|
157
|
+
# When we spawn an agent in terminal mode, we pass --session-id <internal_id>
|
|
158
|
+
# to Claude, so external_id here might actually be our internal session ID
|
|
159
|
+
existing_session = None
|
|
160
|
+
if self._session_storage:
|
|
161
|
+
try:
|
|
162
|
+
# Try to find by internal ID first (terminal mode case)
|
|
163
|
+
existing_session = self._session_storage.get(external_id)
|
|
164
|
+
if existing_session:
|
|
165
|
+
self.logger.info(
|
|
166
|
+
f"Found pre-created session {external_id}, updating instead of creating"
|
|
167
|
+
)
|
|
168
|
+
# Update the session with actual runtime info
|
|
169
|
+
self._session_storage.update(
|
|
170
|
+
session_id=existing_session.id,
|
|
171
|
+
jsonl_path=transcript_path,
|
|
172
|
+
status="active",
|
|
173
|
+
)
|
|
174
|
+
# Return early with the pre-created session's context
|
|
175
|
+
session_id: str | None = existing_session.id
|
|
176
|
+
parent_session_id = existing_session.parent_session_id
|
|
177
|
+
|
|
178
|
+
# Track registered session
|
|
179
|
+
if transcript_path and self._session_coordinator:
|
|
180
|
+
try:
|
|
181
|
+
self._session_coordinator.register_session(external_id)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
self.logger.error(f"Failed to setup session tracking: {e}")
|
|
184
|
+
|
|
185
|
+
# Start the agent run if this is a terminal-mode agent session
|
|
186
|
+
if existing_session.agent_run_id and self._session_coordinator:
|
|
187
|
+
try:
|
|
188
|
+
self._session_coordinator.start_agent_run(existing_session.agent_run_id)
|
|
189
|
+
except Exception as e:
|
|
190
|
+
self.logger.warning(f"Failed to start agent run: {e}")
|
|
191
|
+
|
|
192
|
+
# Update event metadata
|
|
193
|
+
event.metadata["_platform_session_id"] = session_id
|
|
194
|
+
|
|
195
|
+
# Register with Message Processor
|
|
196
|
+
if self._message_processor and transcript_path:
|
|
197
|
+
try:
|
|
198
|
+
self._message_processor.register_session(
|
|
199
|
+
session_id, transcript_path, source=cli_source
|
|
200
|
+
)
|
|
201
|
+
except Exception as e:
|
|
202
|
+
self.logger.warning(f"Failed to register with message processor: {e}")
|
|
203
|
+
|
|
204
|
+
# Execute lifecycle workflows
|
|
205
|
+
context_parts = [""]
|
|
206
|
+
wf_response = HookResponse(decision="allow", context="")
|
|
207
|
+
if self._workflow_handler:
|
|
208
|
+
try:
|
|
209
|
+
wf_response = self._workflow_handler.handle_all_lifecycles(event)
|
|
210
|
+
if wf_response.context:
|
|
211
|
+
context_parts.append(wf_response.context)
|
|
212
|
+
except Exception as e:
|
|
213
|
+
self.logger.warning(f"Workflow error: {e}")
|
|
214
|
+
|
|
215
|
+
# Build system message (terminal display only)
|
|
216
|
+
system_message = "\nSession enhanced by gobby."
|
|
217
|
+
if parent_session_id:
|
|
218
|
+
context_parts.append(f"Parent session: {parent_session_id}")
|
|
219
|
+
if wf_response.system_message:
|
|
220
|
+
system_message += f"\n\n{wf_response.system_message}"
|
|
221
|
+
|
|
222
|
+
return HookResponse(
|
|
223
|
+
decision="allow",
|
|
224
|
+
context="\n".join(context_parts) if context_parts else None,
|
|
225
|
+
system_message=system_message,
|
|
226
|
+
metadata={
|
|
227
|
+
"session_id": session_id,
|
|
228
|
+
"parent_session_id": parent_session_id,
|
|
229
|
+
"machine_id": machine_id,
|
|
230
|
+
"project_id": existing_session.project_id,
|
|
231
|
+
"external_id": external_id,
|
|
232
|
+
"task_id": event.task_id,
|
|
233
|
+
"is_pre_created": True,
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
self.logger.debug(f"No pre-created session found: {e}")
|
|
238
|
+
|
|
239
|
+
# Step 1: Find parent session if this is a handoff (source='clear' only)
|
|
240
|
+
parent_session_id = None
|
|
241
|
+
if session_source == "clear" and self._session_storage:
|
|
242
|
+
try:
|
|
243
|
+
parent = self._session_storage.find_parent(
|
|
244
|
+
machine_id=machine_id,
|
|
245
|
+
project_id=project_id,
|
|
246
|
+
source=cli_source,
|
|
247
|
+
status="handoff_ready",
|
|
248
|
+
)
|
|
249
|
+
if parent:
|
|
250
|
+
parent_session_id = parent.id
|
|
251
|
+
self.logger.debug(f"Found parent session: {parent_session_id}")
|
|
252
|
+
except Exception as e:
|
|
253
|
+
self.logger.warning(f"Error finding parent session: {e}")
|
|
254
|
+
|
|
255
|
+
# Step 2: Register new session with parent if found
|
|
256
|
+
# Extract terminal context (injected by hook_dispatcher for terminal correlation)
|
|
257
|
+
terminal_context = input_data.get("terminal_context")
|
|
258
|
+
session_id = None
|
|
259
|
+
if self._session_manager:
|
|
260
|
+
session_id = self._session_manager.register_session(
|
|
261
|
+
external_id=external_id,
|
|
262
|
+
machine_id=machine_id,
|
|
263
|
+
project_id=project_id,
|
|
264
|
+
parent_session_id=parent_session_id,
|
|
265
|
+
jsonl_path=transcript_path,
|
|
266
|
+
source=cli_source,
|
|
267
|
+
project_path=cwd,
|
|
268
|
+
terminal_context=terminal_context,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Step 2b: Mark parent session as expired after successful handoff
|
|
272
|
+
if parent_session_id and self._session_manager:
|
|
273
|
+
try:
|
|
274
|
+
self._session_manager.mark_session_expired(parent_session_id)
|
|
275
|
+
self.logger.debug(f"Marked parent session {parent_session_id} as expired")
|
|
276
|
+
except Exception as e:
|
|
277
|
+
self.logger.warning(f"Failed to mark parent session as expired: {e}")
|
|
278
|
+
|
|
279
|
+
# Step 3: Track registered session
|
|
280
|
+
if transcript_path and self._session_coordinator:
|
|
281
|
+
try:
|
|
282
|
+
self._session_coordinator.register_session(external_id)
|
|
283
|
+
except Exception as e:
|
|
284
|
+
self.logger.error(f"Failed to setup session tracking: {e}", exc_info=True)
|
|
285
|
+
|
|
286
|
+
# Step 4: Update event metadata with the newly registered session_id
|
|
287
|
+
event.metadata["_platform_session_id"] = session_id
|
|
288
|
+
if parent_session_id:
|
|
289
|
+
event.metadata["_parent_session_id"] = parent_session_id
|
|
290
|
+
|
|
291
|
+
# Step 5: Register with Message Processor
|
|
292
|
+
if self._message_processor and transcript_path and session_id:
|
|
293
|
+
try:
|
|
294
|
+
self._message_processor.register_session(
|
|
295
|
+
session_id, transcript_path, source=cli_source
|
|
296
|
+
)
|
|
297
|
+
except Exception as e:
|
|
298
|
+
self.logger.warning(f"Failed to register session with message processor: {e}")
|
|
299
|
+
|
|
300
|
+
# Step 6: Execute lifecycle workflows
|
|
301
|
+
context_parts = [""]
|
|
302
|
+
wf_response = HookResponse(decision="allow", context="")
|
|
303
|
+
if self._workflow_handler:
|
|
304
|
+
try:
|
|
305
|
+
wf_response = self._workflow_handler.handle_all_lifecycles(event)
|
|
306
|
+
if wf_response.context:
|
|
307
|
+
context_parts.append(wf_response.context)
|
|
308
|
+
except Exception as e:
|
|
309
|
+
self.logger.warning(f"Workflow error: {e}")
|
|
310
|
+
|
|
311
|
+
if parent_session_id:
|
|
312
|
+
context_parts.append(f"Parent session: {parent_session_id}")
|
|
313
|
+
|
|
314
|
+
# Build system message (terminal display only)
|
|
315
|
+
system_message = "\nSession enhanced by gobby."
|
|
316
|
+
if wf_response.system_message:
|
|
317
|
+
system_message += f"\n\n{wf_response.system_message}"
|
|
318
|
+
|
|
319
|
+
# Inject active task context if available
|
|
320
|
+
if event.task_id:
|
|
321
|
+
task_title = event.metadata.get("_task_title", "Unknown Task")
|
|
322
|
+
context_parts.append("\n## Active Task Context\n")
|
|
323
|
+
context_parts.append(f"You are working on task: {task_title} ({event.task_id})")
|
|
324
|
+
|
|
325
|
+
# Build metadata with terminal context (filter out nulls)
|
|
326
|
+
metadata: dict[str, Any] = {
|
|
327
|
+
"session_id": session_id,
|
|
328
|
+
"parent_session_id": parent_session_id,
|
|
329
|
+
"machine_id": machine_id,
|
|
330
|
+
"project_id": project_id,
|
|
331
|
+
"external_id": external_id,
|
|
332
|
+
"task_id": event.task_id,
|
|
333
|
+
}
|
|
334
|
+
if terminal_context:
|
|
335
|
+
# Only include non-null terminal values
|
|
336
|
+
for key, value in terminal_context.items():
|
|
337
|
+
if value is not None:
|
|
338
|
+
metadata[f"terminal_{key}"] = value
|
|
339
|
+
|
|
340
|
+
return HookResponse(
|
|
341
|
+
decision="allow",
|
|
342
|
+
context="\n".join(context_parts) if context_parts else None,
|
|
343
|
+
system_message=system_message,
|
|
344
|
+
metadata=metadata,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def handle_session_end(self, event: HookEvent) -> HookResponse:
|
|
348
|
+
"""Handle SESSION_END event."""
|
|
349
|
+
from gobby.tasks.commits import auto_link_commits
|
|
350
|
+
|
|
351
|
+
external_id = event.session_id
|
|
352
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
353
|
+
|
|
354
|
+
if session_id:
|
|
355
|
+
self.logger.debug(f"SESSION_END: session {session_id}")
|
|
356
|
+
else:
|
|
357
|
+
self.logger.warning(f"SESSION_END: session_id not found for external_id={external_id}")
|
|
358
|
+
|
|
359
|
+
# If not in mapping, query database
|
|
360
|
+
if not session_id and external_id and self._session_manager:
|
|
361
|
+
self.logger.debug(f"external_id {external_id} not in mapping, querying database")
|
|
362
|
+
# Resolve context for lookup
|
|
363
|
+
machine_id = self._get_machine_id()
|
|
364
|
+
cwd = event.data.get("cwd")
|
|
365
|
+
project_id = self._resolve_project_id(event.data.get("project_id"), cwd)
|
|
366
|
+
# Lookup with full composite key
|
|
367
|
+
session_id = self._session_manager.lookup_session_id(
|
|
368
|
+
external_id,
|
|
369
|
+
source=event.source.value,
|
|
370
|
+
machine_id=machine_id,
|
|
371
|
+
project_id=project_id,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Ensure session_id is available in event metadata for workflow actions
|
|
375
|
+
if session_id and not event.metadata.get("_platform_session_id"):
|
|
376
|
+
event.metadata["_platform_session_id"] = session_id
|
|
377
|
+
|
|
378
|
+
# Execute lifecycle workflow triggers
|
|
379
|
+
if self._workflow_handler:
|
|
380
|
+
try:
|
|
381
|
+
self._workflow_handler.handle_all_lifecycles(event)
|
|
382
|
+
except Exception as e:
|
|
383
|
+
self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
|
|
384
|
+
|
|
385
|
+
# Auto-link commits made during this session to tasks
|
|
386
|
+
if session_id and self._session_storage and self._task_manager:
|
|
387
|
+
try:
|
|
388
|
+
session = self._session_storage.get(session_id)
|
|
389
|
+
if session:
|
|
390
|
+
cwd = event.data.get("cwd")
|
|
391
|
+
link_result = auto_link_commits(
|
|
392
|
+
task_manager=self._task_manager,
|
|
393
|
+
since=session.created_at,
|
|
394
|
+
cwd=cwd,
|
|
395
|
+
)
|
|
396
|
+
if link_result.total_linked > 0:
|
|
397
|
+
self.logger.info(
|
|
398
|
+
f"Auto-linked {link_result.total_linked} commits to tasks: "
|
|
399
|
+
f"{list(link_result.linked_tasks.keys())}"
|
|
400
|
+
)
|
|
401
|
+
except Exception as e:
|
|
402
|
+
self.logger.warning(f"Failed to auto-link session commits: {e}")
|
|
403
|
+
|
|
404
|
+
# Complete agent run if this is a terminal-mode agent session
|
|
405
|
+
if session_id and self._session_storage and self._session_coordinator:
|
|
406
|
+
try:
|
|
407
|
+
session = self._session_storage.get(session_id)
|
|
408
|
+
if session and session.agent_run_id:
|
|
409
|
+
self._session_coordinator.complete_agent_run(session)
|
|
410
|
+
except Exception as e:
|
|
411
|
+
self.logger.warning(f"Failed to complete agent run: {e}")
|
|
412
|
+
|
|
413
|
+
# Generate independent session summary file
|
|
414
|
+
if self._summary_file_generator:
|
|
415
|
+
try:
|
|
416
|
+
summary_input = {
|
|
417
|
+
"session_id": external_id,
|
|
418
|
+
"transcript_path": event.data.get("transcript_path"),
|
|
419
|
+
}
|
|
420
|
+
self._summary_file_generator.generate_session_summary(
|
|
421
|
+
session_id=session_id or external_id,
|
|
422
|
+
input_data=summary_input,
|
|
423
|
+
)
|
|
424
|
+
except Exception as e:
|
|
425
|
+
self.logger.error(f"Failed to generate failover summary: {e}")
|
|
426
|
+
|
|
427
|
+
# Unregister from message processor
|
|
428
|
+
if self._message_processor and (session_id or external_id):
|
|
429
|
+
try:
|
|
430
|
+
target_id = session_id or external_id
|
|
431
|
+
self._message_processor.unregister_session(target_id)
|
|
432
|
+
except Exception as e:
|
|
433
|
+
self.logger.warning(f"Failed to unregister session from message processor: {e}")
|
|
434
|
+
|
|
435
|
+
return HookResponse(decision="allow")
|
|
436
|
+
|
|
437
|
+
# ==================== AGENT HANDLERS ====================
|
|
438
|
+
|
|
439
|
+
def handle_before_agent(self, event: HookEvent) -> HookResponse:
|
|
440
|
+
"""Handle BEFORE_AGENT event (user prompt submit)."""
|
|
441
|
+
input_data = event.data
|
|
442
|
+
prompt = input_data.get("prompt", "")
|
|
443
|
+
transcript_path = input_data.get("transcript_path")
|
|
444
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
445
|
+
|
|
446
|
+
context_parts = []
|
|
447
|
+
|
|
448
|
+
if session_id:
|
|
449
|
+
self.logger.debug(f"BEFORE_AGENT: session {session_id}")
|
|
450
|
+
self.logger.debug(f" Prompt: {prompt[:100]}...")
|
|
451
|
+
|
|
452
|
+
# Update status to active (unless /clear or /exit)
|
|
453
|
+
prompt_lower = prompt.strip().lower()
|
|
454
|
+
if prompt_lower not in ("/clear", "/exit") and self._session_manager:
|
|
455
|
+
try:
|
|
456
|
+
self._session_manager.update_session_status(session_id, "active")
|
|
457
|
+
if self._session_storage:
|
|
458
|
+
self._session_storage.reset_transcript_processed(session_id)
|
|
459
|
+
except Exception as e:
|
|
460
|
+
self.logger.warning(f"Failed to update session status: {e}")
|
|
461
|
+
|
|
462
|
+
# Handle /clear command - lifecycle workflows handle handoff
|
|
463
|
+
if prompt_lower in ("/clear", "/exit") and transcript_path:
|
|
464
|
+
self.logger.debug(f"Detected {prompt_lower} - lifecycle workflows handle handoff")
|
|
465
|
+
|
|
466
|
+
# Execute lifecycle workflow triggers
|
|
467
|
+
if self._workflow_handler:
|
|
468
|
+
try:
|
|
469
|
+
wf_response = self._workflow_handler.handle_all_lifecycles(event)
|
|
470
|
+
if wf_response.context:
|
|
471
|
+
context_parts.append(wf_response.context)
|
|
472
|
+
if wf_response.decision != "allow":
|
|
473
|
+
return wf_response
|
|
474
|
+
except Exception as e:
|
|
475
|
+
self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
|
|
476
|
+
|
|
477
|
+
return HookResponse(
|
|
478
|
+
decision="allow",
|
|
479
|
+
context="\n\n".join(context_parts) if context_parts else None,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
def handle_after_agent(self, event: HookEvent) -> HookResponse:
|
|
483
|
+
"""Handle AFTER_AGENT event."""
|
|
484
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
485
|
+
cli_source = event.source.value
|
|
486
|
+
|
|
487
|
+
if session_id:
|
|
488
|
+
self.logger.debug(f"AFTER_AGENT: session {session_id}, cli={cli_source}")
|
|
489
|
+
if self._session_manager:
|
|
490
|
+
try:
|
|
491
|
+
self._session_manager.update_session_status(session_id, "paused")
|
|
492
|
+
except Exception as e:
|
|
493
|
+
self.logger.warning(f"Failed to update session status: {e}")
|
|
494
|
+
else:
|
|
495
|
+
self.logger.debug(f"AFTER_AGENT: cli={cli_source}")
|
|
496
|
+
|
|
497
|
+
# Execute lifecycle workflow triggers
|
|
498
|
+
if self._workflow_handler:
|
|
499
|
+
try:
|
|
500
|
+
wf_response = self._workflow_handler.handle_all_lifecycles(event)
|
|
501
|
+
if wf_response.decision != "allow":
|
|
502
|
+
return wf_response
|
|
503
|
+
if wf_response.context:
|
|
504
|
+
return wf_response
|
|
505
|
+
except Exception as e:
|
|
506
|
+
self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
|
|
507
|
+
|
|
508
|
+
return HookResponse(decision="allow")
|
|
509
|
+
|
|
510
|
+
# ==================== TOOL HANDLERS ====================
|
|
511
|
+
|
|
512
|
+
def handle_before_tool(self, event: HookEvent) -> HookResponse:
|
|
513
|
+
"""Handle BEFORE_TOOL event."""
|
|
514
|
+
input_data = event.data
|
|
515
|
+
tool_name = input_data.get("tool_name", "unknown")
|
|
516
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
517
|
+
|
|
518
|
+
if session_id:
|
|
519
|
+
self.logger.debug(f"BEFORE_TOOL: {tool_name}, session {session_id}")
|
|
520
|
+
else:
|
|
521
|
+
self.logger.debug(f"BEFORE_TOOL: {tool_name}")
|
|
522
|
+
|
|
523
|
+
context_parts = []
|
|
524
|
+
|
|
525
|
+
# Execute lifecycle workflow triggers
|
|
526
|
+
if self._workflow_handler:
|
|
527
|
+
try:
|
|
528
|
+
wf_response = self._workflow_handler.handle_all_lifecycles(event)
|
|
529
|
+
if wf_response.context:
|
|
530
|
+
context_parts.append(wf_response.context)
|
|
531
|
+
if wf_response.decision != "allow":
|
|
532
|
+
return wf_response
|
|
533
|
+
except Exception as e:
|
|
534
|
+
self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
|
|
535
|
+
|
|
536
|
+
return HookResponse(
|
|
537
|
+
decision="allow",
|
|
538
|
+
context="\n\n".join(context_parts) if context_parts else None,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
def handle_after_tool(self, event: HookEvent) -> HookResponse:
|
|
542
|
+
"""Handle AFTER_TOOL event."""
|
|
543
|
+
input_data = event.data
|
|
544
|
+
tool_name = input_data.get("tool_name", "unknown")
|
|
545
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
546
|
+
is_failure = event.metadata.get("is_failure", False)
|
|
547
|
+
|
|
548
|
+
status = "FAIL" if is_failure else "OK"
|
|
549
|
+
if session_id:
|
|
550
|
+
self.logger.debug(f"AFTER_TOOL [{status}]: {tool_name}, session {session_id}")
|
|
551
|
+
else:
|
|
552
|
+
self.logger.debug(f"AFTER_TOOL [{status}]: {tool_name}")
|
|
553
|
+
|
|
554
|
+
context_parts = []
|
|
555
|
+
|
|
556
|
+
# Execute lifecycle workflow triggers
|
|
557
|
+
if self._workflow_handler:
|
|
558
|
+
try:
|
|
559
|
+
wf_response = self._workflow_handler.handle_all_lifecycles(event)
|
|
560
|
+
if wf_response.context:
|
|
561
|
+
context_parts.append(wf_response.context)
|
|
562
|
+
if wf_response.decision != "allow":
|
|
563
|
+
return wf_response
|
|
564
|
+
except Exception as e:
|
|
565
|
+
self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
|
|
566
|
+
|
|
567
|
+
return HookResponse(
|
|
568
|
+
decision="allow",
|
|
569
|
+
context="\n\n".join(context_parts) if context_parts else None,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
# ==================== STOP HANDLER ====================
|
|
573
|
+
|
|
574
|
+
def handle_stop(self, event: HookEvent) -> HookResponse:
|
|
575
|
+
"""Handle STOP event (Claude Code only)."""
|
|
576
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
577
|
+
cli_source = event.source.value
|
|
578
|
+
|
|
579
|
+
self.logger.debug(f"STOP: session {session_id}, cli={cli_source}")
|
|
580
|
+
|
|
581
|
+
# Execute lifecycle workflow triggers for on_stop
|
|
582
|
+
if self._workflow_handler:
|
|
583
|
+
try:
|
|
584
|
+
wf_response = self._workflow_handler.handle_all_lifecycles(event)
|
|
585
|
+
if wf_response.decision != "allow":
|
|
586
|
+
return wf_response
|
|
587
|
+
if wf_response.context:
|
|
588
|
+
return wf_response
|
|
589
|
+
except Exception as e:
|
|
590
|
+
self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
|
|
591
|
+
|
|
592
|
+
return HookResponse(decision="allow")
|
|
593
|
+
|
|
594
|
+
# ==================== COMPACT HANDLER ====================
|
|
595
|
+
|
|
596
|
+
def handle_pre_compact(self, event: HookEvent) -> HookResponse:
|
|
597
|
+
"""Handle PRE_COMPACT event."""
|
|
598
|
+
trigger = event.data.get("trigger", "auto")
|
|
599
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
600
|
+
|
|
601
|
+
if session_id:
|
|
602
|
+
self.logger.debug(f"PRE_COMPACT ({trigger}): session {session_id}")
|
|
603
|
+
# Mark session as handoff_ready so it can be found as parent after compact
|
|
604
|
+
if self._session_manager:
|
|
605
|
+
self._session_manager.update_session_status(session_id, "handoff_ready")
|
|
606
|
+
else:
|
|
607
|
+
self.logger.debug(f"PRE_COMPACT ({trigger})")
|
|
608
|
+
|
|
609
|
+
# Execute lifecycle workflows
|
|
610
|
+
if self._workflow_handler:
|
|
611
|
+
try:
|
|
612
|
+
return self._workflow_handler.handle_all_lifecycles(event)
|
|
613
|
+
except Exception as e:
|
|
614
|
+
self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
|
|
615
|
+
|
|
616
|
+
return HookResponse(decision="allow")
|
|
617
|
+
|
|
618
|
+
# ==================== SUBAGENT HANDLERS ====================
|
|
619
|
+
|
|
620
|
+
def handle_subagent_start(self, event: HookEvent) -> HookResponse:
|
|
621
|
+
"""Handle SUBAGENT_START event."""
|
|
622
|
+
input_data = event.data
|
|
623
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
624
|
+
agent_id = input_data.get("agent_id")
|
|
625
|
+
subagent_id = input_data.get("subagent_id")
|
|
626
|
+
|
|
627
|
+
log_msg = f"SUBAGENT_START: session {session_id}" if session_id else "SUBAGENT_START"
|
|
628
|
+
if agent_id:
|
|
629
|
+
log_msg += f", agent_id={agent_id}"
|
|
630
|
+
if subagent_id:
|
|
631
|
+
log_msg += f", subagent_id={subagent_id}"
|
|
632
|
+
self.logger.debug(log_msg)
|
|
633
|
+
|
|
634
|
+
return HookResponse(decision="allow")
|
|
635
|
+
|
|
636
|
+
def handle_subagent_stop(self, event: HookEvent) -> HookResponse:
|
|
637
|
+
"""Handle SUBAGENT_STOP event."""
|
|
638
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
639
|
+
|
|
640
|
+
if session_id:
|
|
641
|
+
self.logger.debug(f"SUBAGENT_STOP: session {session_id}")
|
|
642
|
+
else:
|
|
643
|
+
self.logger.debug("SUBAGENT_STOP")
|
|
644
|
+
|
|
645
|
+
return HookResponse(decision="allow")
|
|
646
|
+
|
|
647
|
+
# ==================== NOTIFICATION HANDLER ====================
|
|
648
|
+
|
|
649
|
+
def handle_notification(self, event: HookEvent) -> HookResponse:
|
|
650
|
+
"""Handle NOTIFICATION event."""
|
|
651
|
+
input_data = event.data
|
|
652
|
+
notification_type = (
|
|
653
|
+
input_data.get("notification_type")
|
|
654
|
+
or input_data.get("notificationType")
|
|
655
|
+
or input_data.get("type")
|
|
656
|
+
or "general"
|
|
657
|
+
)
|
|
658
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
659
|
+
|
|
660
|
+
if session_id:
|
|
661
|
+
self.logger.debug(f"NOTIFICATION ({notification_type}): session {session_id}")
|
|
662
|
+
if self._session_manager:
|
|
663
|
+
try:
|
|
664
|
+
self._session_manager.update_session_status(session_id, "paused")
|
|
665
|
+
except Exception as e:
|
|
666
|
+
self.logger.warning(f"Failed to update session status: {e}")
|
|
667
|
+
else:
|
|
668
|
+
self.logger.debug(f"NOTIFICATION ({notification_type})")
|
|
669
|
+
|
|
670
|
+
return HookResponse(decision="allow")
|
|
671
|
+
|
|
672
|
+
# ==================== PERMISSION HANDLER ====================
|
|
673
|
+
|
|
674
|
+
def handle_permission_request(self, event: HookEvent) -> HookResponse:
|
|
675
|
+
"""Handle PERMISSION_REQUEST event (Claude Code only)."""
|
|
676
|
+
input_data = event.data
|
|
677
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
678
|
+
permission_type = input_data.get("permission_type", "unknown")
|
|
679
|
+
|
|
680
|
+
if session_id:
|
|
681
|
+
self.logger.debug(f"PERMISSION_REQUEST ({permission_type}): session {session_id}")
|
|
682
|
+
else:
|
|
683
|
+
self.logger.debug(f"PERMISSION_REQUEST ({permission_type})")
|
|
684
|
+
|
|
685
|
+
return HookResponse(decision="allow")
|
|
686
|
+
|
|
687
|
+
# ==================== GEMINI-ONLY HANDLERS ====================
|
|
688
|
+
|
|
689
|
+
def handle_before_tool_selection(self, event: HookEvent) -> HookResponse:
|
|
690
|
+
"""Handle BEFORE_TOOL_SELECTION event (Gemini only)."""
|
|
691
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
692
|
+
|
|
693
|
+
if session_id:
|
|
694
|
+
self.logger.debug(f"BEFORE_TOOL_SELECTION: session {session_id}")
|
|
695
|
+
else:
|
|
696
|
+
self.logger.debug("BEFORE_TOOL_SELECTION")
|
|
697
|
+
|
|
698
|
+
return HookResponse(decision="allow")
|
|
699
|
+
|
|
700
|
+
def handle_before_model(self, event: HookEvent) -> HookResponse:
|
|
701
|
+
"""Handle BEFORE_MODEL event (Gemini only)."""
|
|
702
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
703
|
+
|
|
704
|
+
if session_id:
|
|
705
|
+
self.logger.debug(f"BEFORE_MODEL: session {session_id}")
|
|
706
|
+
else:
|
|
707
|
+
self.logger.debug("BEFORE_MODEL")
|
|
708
|
+
|
|
709
|
+
return HookResponse(decision="allow")
|
|
710
|
+
|
|
711
|
+
def handle_after_model(self, event: HookEvent) -> HookResponse:
|
|
712
|
+
"""Handle AFTER_MODEL event (Gemini only)."""
|
|
713
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
714
|
+
|
|
715
|
+
if session_id:
|
|
716
|
+
self.logger.debug(f"AFTER_MODEL: session {session_id}")
|
|
717
|
+
else:
|
|
718
|
+
self.logger.debug("AFTER_MODEL")
|
|
719
|
+
|
|
720
|
+
return HookResponse(decision="allow")
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
__all__ = ["EventHandlers"]
|