gobby 0.2.7__py3-none-any.whl → 0.2.9__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 +1 -1
- gobby/adapters/claude_code.py +99 -61
- gobby/adapters/gemini.py +140 -38
- gobby/agents/isolation.py +130 -0
- gobby/agents/registry.py +11 -0
- gobby/agents/session.py +1 -0
- gobby/agents/spawn_executor.py +43 -13
- gobby/agents/spawners/macos.py +26 -1
- gobby/app_context.py +59 -0
- gobby/cli/__init__.py +0 -2
- gobby/cli/memory.py +185 -0
- gobby/cli/utils.py +5 -17
- gobby/clones/git.py +177 -0
- gobby/config/features.py +0 -20
- gobby/config/skills.py +31 -0
- gobby/config/tasks.py +4 -0
- gobby/hooks/event_handlers/__init__.py +155 -0
- gobby/hooks/event_handlers/_agent.py +175 -0
- gobby/hooks/event_handlers/_base.py +87 -0
- gobby/hooks/event_handlers/_misc.py +66 -0
- gobby/hooks/event_handlers/_session.py +573 -0
- gobby/hooks/event_handlers/_tool.py +196 -0
- gobby/hooks/hook_manager.py +21 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
- gobby/llm/claude.py +377 -42
- gobby/mcp_proxy/importer.py +4 -41
- gobby/mcp_proxy/instructions.py +2 -2
- gobby/mcp_proxy/manager.py +13 -3
- gobby/mcp_proxy/registries.py +35 -4
- gobby/mcp_proxy/services/recommendation.py +2 -28
- gobby/mcp_proxy/tools/agent_messaging.py +93 -44
- gobby/mcp_proxy/tools/agents.py +45 -9
- gobby/mcp_proxy/tools/artifacts.py +46 -12
- gobby/mcp_proxy/tools/sessions/_commits.py +31 -24
- gobby/mcp_proxy/tools/sessions/_crud.py +5 -5
- gobby/mcp_proxy/tools/sessions/_handoff.py +45 -41
- gobby/mcp_proxy/tools/sessions/_messages.py +35 -7
- gobby/mcp_proxy/tools/spawn_agent.py +44 -6
- gobby/mcp_proxy/tools/task_readiness.py +27 -4
- gobby/mcp_proxy/tools/tasks/_context.py +18 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +29 -14
- gobby/mcp_proxy/tools/tasks/_session.py +22 -7
- gobby/mcp_proxy/tools/workflows/__init__.py +266 -0
- gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
- gobby/mcp_proxy/tools/workflows/_import.py +112 -0
- gobby/mcp_proxy/tools/workflows/_lifecycle.py +321 -0
- gobby/mcp_proxy/tools/workflows/_query.py +207 -0
- gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
- gobby/mcp_proxy/tools/workflows/_terminal.py +139 -0
- gobby/mcp_proxy/tools/worktrees.py +32 -7
- gobby/memory/components/__init__.py +0 -0
- gobby/memory/components/ingestion.py +98 -0
- gobby/memory/components/search.py +108 -0
- gobby/memory/extractor.py +15 -1
- gobby/memory/manager.py +16 -25
- gobby/paths.py +51 -0
- gobby/prompts/loader.py +1 -35
- gobby/runner.py +36 -10
- gobby/servers/http.py +186 -149
- gobby/servers/routes/admin.py +12 -0
- gobby/servers/routes/mcp/endpoints/execution.py +15 -7
- gobby/servers/routes/mcp/endpoints/registry.py +8 -8
- gobby/servers/routes/mcp/hooks.py +50 -3
- gobby/servers/websocket.py +57 -1
- gobby/sessions/analyzer.py +4 -4
- gobby/sessions/manager.py +9 -0
- gobby/sessions/transcripts/gemini.py +100 -34
- gobby/skills/parser.py +23 -0
- gobby/skills/sync.py +5 -4
- gobby/storage/artifacts.py +19 -0
- gobby/storage/database.py +9 -2
- gobby/storage/memories.py +32 -21
- gobby/storage/migrations.py +46 -4
- gobby/storage/sessions.py +4 -2
- gobby/storage/skills.py +87 -7
- gobby/tasks/external_validator.py +4 -17
- gobby/tasks/validation.py +13 -87
- gobby/tools/summarizer.py +18 -51
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +5 -0
- gobby/workflows/context_actions.py +21 -24
- gobby/workflows/detection_helpers.py +38 -24
- gobby/workflows/enforcement/__init__.py +11 -1
- gobby/workflows/enforcement/blocking.py +109 -1
- gobby/workflows/enforcement/handlers.py +35 -1
- gobby/workflows/engine.py +96 -0
- gobby/workflows/evaluator.py +110 -0
- gobby/workflows/hooks.py +41 -0
- gobby/workflows/lifecycle_evaluator.py +2 -1
- gobby/workflows/memory_actions.py +11 -0
- gobby/workflows/safe_evaluator.py +8 -0
- gobby/workflows/summary_actions.py +123 -50
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/METADATA +1 -1
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/RECORD +99 -107
- gobby/cli/tui.py +0 -34
- gobby/hooks/event_handlers.py +0 -909
- gobby/mcp_proxy/tools/workflows.py +0 -973
- gobby/tui/__init__.py +0 -5
- gobby/tui/api_client.py +0 -278
- gobby/tui/app.py +0 -329
- gobby/tui/screens/__init__.py +0 -25
- gobby/tui/screens/agents.py +0 -333
- gobby/tui/screens/chat.py +0 -450
- gobby/tui/screens/dashboard.py +0 -377
- gobby/tui/screens/memory.py +0 -305
- gobby/tui/screens/metrics.py +0 -231
- gobby/tui/screens/orchestrator.py +0 -903
- gobby/tui/screens/sessions.py +0 -412
- gobby/tui/screens/tasks.py +0 -440
- gobby/tui/screens/workflows.py +0 -289
- gobby/tui/screens/worktrees.py +0 -174
- gobby/tui/widgets/__init__.py +0 -21
- gobby/tui/widgets/chat.py +0 -210
- gobby/tui/widgets/conductor.py +0 -104
- gobby/tui/widgets/menu.py +0 -132
- gobby/tui/widgets/message_panel.py +0 -160
- gobby/tui/widgets/review_gate.py +0 -224
- gobby/tui/widgets/task_tree.py +0 -99
- gobby/tui/widgets/token_budget.py +0 -166
- gobby/tui/ws_client.py +0 -258
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/WHEEL +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/top_level.txt +0 -0
gobby/config/skills.py
CHANGED
|
@@ -11,6 +11,37 @@ from typing import Literal
|
|
|
11
11
|
from pydantic import BaseModel, Field, field_validator
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
class HubConfig(BaseModel):
|
|
15
|
+
"""
|
|
16
|
+
Configuration for a skill hub or collection.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
type: Literal["clawdhub", "skillhub", "github-collection"] = Field(
|
|
20
|
+
...,
|
|
21
|
+
description="Type of the hub: 'clawdhub', 'skillhub', or 'github-collection'",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
base_url: str | None = Field(
|
|
25
|
+
default=None,
|
|
26
|
+
description="Base URL for the hub",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
repo: str | None = Field(
|
|
30
|
+
default=None,
|
|
31
|
+
description="GitHub repository (e.g. 'owner/repo')",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
branch: str | None = Field(
|
|
35
|
+
default=None,
|
|
36
|
+
description="Git branch to use",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
auth_key_name: str | None = Field(
|
|
40
|
+
default=None,
|
|
41
|
+
description="Environment variable name for auth key",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
14
45
|
class SkillsConfig(BaseModel):
|
|
15
46
|
"""
|
|
16
47
|
Configuration for skill injection and discovery.
|
gobby/config/tasks.py
CHANGED
|
@@ -669,6 +669,10 @@ class WorkflowConfig(BaseModel):
|
|
|
669
669
|
default_factory=lambda: ["Edit", "Write", "Update", "NotebookEdit"],
|
|
670
670
|
description="Tools that require an active task when require_task_before_edit is enabled",
|
|
671
671
|
)
|
|
672
|
+
debug_echo_context: bool = Field(
|
|
673
|
+
default=False,
|
|
674
|
+
description="Debug: echo additionalContext to system_message for terminal visibility",
|
|
675
|
+
)
|
|
672
676
|
|
|
673
677
|
@field_validator("timeout")
|
|
674
678
|
@classmethod
|
|
@@ -0,0 +1,155 @@
|
|
|
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.event_handlers._agent import AgentEventHandlerMixin
|
|
18
|
+
from gobby.hooks.event_handlers._misc import MiscEventHandlerMixin
|
|
19
|
+
from gobby.hooks.event_handlers._session import SessionEventHandlerMixin
|
|
20
|
+
from gobby.hooks.event_handlers._tool import EDIT_TOOLS, ToolEventHandlerMixin
|
|
21
|
+
from gobby.hooks.events import HookEvent, HookEventType, HookResponse
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from gobby.config.skills import SkillsConfig
|
|
25
|
+
from gobby.config.tasks import WorkflowConfig
|
|
26
|
+
from gobby.hooks.artifact_capture import ArtifactCaptureHook
|
|
27
|
+
from gobby.hooks.session_coordinator import SessionCoordinator
|
|
28
|
+
from gobby.hooks.skill_manager import HookSkillManager
|
|
29
|
+
from gobby.sessions.manager import SessionManager
|
|
30
|
+
from gobby.sessions.summary import SummaryFileGenerator
|
|
31
|
+
from gobby.storage.session_messages import LocalSessionMessageManager
|
|
32
|
+
from gobby.storage.session_tasks import SessionTaskManager
|
|
33
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
34
|
+
from gobby.storage.tasks import LocalTaskManager
|
|
35
|
+
from gobby.workflows.hooks import WorkflowHookHandler
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class EventHandlers(
|
|
39
|
+
SessionEventHandlerMixin,
|
|
40
|
+
AgentEventHandlerMixin,
|
|
41
|
+
ToolEventHandlerMixin,
|
|
42
|
+
MiscEventHandlerMixin,
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Manages event handler registration and dispatch.
|
|
46
|
+
|
|
47
|
+
Provides handler methods for all HookEventType values and a registration
|
|
48
|
+
mechanism for looking up handlers by event type.
|
|
49
|
+
|
|
50
|
+
Extracted from HookManager to separate event handling concerns.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
session_manager: SessionManager | None = None,
|
|
56
|
+
workflow_handler: WorkflowHookHandler | None = None,
|
|
57
|
+
session_storage: LocalSessionManager | None = None,
|
|
58
|
+
session_task_manager: SessionTaskManager | None = None,
|
|
59
|
+
message_processor: Any | None = None,
|
|
60
|
+
summary_file_generator: SummaryFileGenerator | None = None,
|
|
61
|
+
task_manager: LocalTaskManager | None = None,
|
|
62
|
+
session_coordinator: SessionCoordinator | None = None,
|
|
63
|
+
message_manager: LocalSessionMessageManager | None = None,
|
|
64
|
+
skill_manager: HookSkillManager | None = None,
|
|
65
|
+
skills_config: SkillsConfig | None = None,
|
|
66
|
+
artifact_capture_hook: ArtifactCaptureHook | None = None,
|
|
67
|
+
workflow_config: WorkflowConfig | None = None,
|
|
68
|
+
get_machine_id: Callable[[], str] | None = None,
|
|
69
|
+
resolve_project_id: Callable[[str | None, str | None], str] | None = None,
|
|
70
|
+
logger: logging.Logger | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Initialize EventHandlers.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
session_manager: SessionManager for session operations
|
|
77
|
+
workflow_handler: WorkflowHookHandler for lifecycle workflows
|
|
78
|
+
session_storage: LocalSessionManager for session storage
|
|
79
|
+
session_task_manager: SessionTaskManager for session-task links
|
|
80
|
+
message_processor: SessionMessageProcessor for message handling
|
|
81
|
+
summary_file_generator: SummaryFileGenerator for summaries
|
|
82
|
+
task_manager: LocalTaskManager for task operations
|
|
83
|
+
session_coordinator: SessionCoordinator for session tracking
|
|
84
|
+
message_manager: LocalSessionMessageManager for messages
|
|
85
|
+
skill_manager: HookSkillManager for skill discovery
|
|
86
|
+
skills_config: SkillsConfig for skill injection settings
|
|
87
|
+
artifact_capture_hook: ArtifactCaptureHook for capturing artifacts
|
|
88
|
+
workflow_config: WorkflowConfig for workflow settings (debug_echo_context)
|
|
89
|
+
get_machine_id: Function to get machine ID
|
|
90
|
+
resolve_project_id: Function to resolve project ID from cwd
|
|
91
|
+
logger: Optional logger instance
|
|
92
|
+
"""
|
|
93
|
+
self._session_manager = session_manager
|
|
94
|
+
self._workflow_handler = workflow_handler
|
|
95
|
+
self._session_storage = session_storage
|
|
96
|
+
self._session_task_manager = session_task_manager
|
|
97
|
+
self._message_processor = message_processor
|
|
98
|
+
self._summary_file_generator = summary_file_generator
|
|
99
|
+
self._task_manager = task_manager
|
|
100
|
+
self._session_coordinator = session_coordinator
|
|
101
|
+
self._message_manager = message_manager
|
|
102
|
+
self._skill_manager = skill_manager
|
|
103
|
+
self._skills_config = skills_config
|
|
104
|
+
self._artifact_capture_hook = artifact_capture_hook
|
|
105
|
+
self._workflow_config = workflow_config
|
|
106
|
+
self._get_machine_id = get_machine_id or (lambda: "unknown-machine")
|
|
107
|
+
self._resolve_project_id = resolve_project_id or (lambda p, c: p or "")
|
|
108
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
109
|
+
|
|
110
|
+
# Build handler map
|
|
111
|
+
self._handler_map: dict[HookEventType, Callable[[HookEvent], HookResponse]] = {
|
|
112
|
+
HookEventType.SESSION_START: self.handle_session_start,
|
|
113
|
+
HookEventType.SESSION_END: self.handle_session_end,
|
|
114
|
+
HookEventType.BEFORE_AGENT: self.handle_before_agent,
|
|
115
|
+
HookEventType.AFTER_AGENT: self.handle_after_agent,
|
|
116
|
+
HookEventType.BEFORE_TOOL: self.handle_before_tool,
|
|
117
|
+
HookEventType.AFTER_TOOL: self.handle_after_tool,
|
|
118
|
+
HookEventType.PRE_COMPACT: self.handle_pre_compact,
|
|
119
|
+
HookEventType.SUBAGENT_START: self.handle_subagent_start,
|
|
120
|
+
HookEventType.SUBAGENT_STOP: self.handle_subagent_stop,
|
|
121
|
+
HookEventType.NOTIFICATION: self.handle_notification,
|
|
122
|
+
HookEventType.BEFORE_TOOL_SELECTION: self.handle_before_tool_selection,
|
|
123
|
+
HookEventType.BEFORE_MODEL: self.handle_before_model,
|
|
124
|
+
HookEventType.AFTER_MODEL: self.handle_after_model,
|
|
125
|
+
HookEventType.PERMISSION_REQUEST: self.handle_permission_request,
|
|
126
|
+
HookEventType.STOP: self.handle_stop,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
def get_handler(
|
|
130
|
+
self, event_type: HookEventType | str
|
|
131
|
+
) -> Callable[[HookEvent], HookResponse] | None:
|
|
132
|
+
"""
|
|
133
|
+
Get handler for an event type.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
event_type: The event type to get handler for
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Handler callable or None if not found
|
|
140
|
+
"""
|
|
141
|
+
if isinstance(event_type, str):
|
|
142
|
+
try:
|
|
143
|
+
event_type = HookEventType(event_type)
|
|
144
|
+
except ValueError:
|
|
145
|
+
return None
|
|
146
|
+
return self._handler_map.get(event_type)
|
|
147
|
+
|
|
148
|
+
def get_handler_map(self) -> dict[HookEventType, Callable[[HookEvent], HookResponse]]:
|
|
149
|
+
"""
|
|
150
|
+
Get a copy of the handler map.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Copy of handler map (modifications don't affect internal state)
|
|
154
|
+
"""
|
|
155
|
+
return dict(self._handler_map)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from gobby.hooks.event_handlers._base import EventHandlersBase
|
|
4
|
+
from gobby.hooks.events import HookEvent, HookResponse, SessionSource
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AgentEventHandlerMixin(EventHandlersBase):
|
|
8
|
+
"""Mixin for handling agent-related events."""
|
|
9
|
+
|
|
10
|
+
def handle_before_agent(self, event: HookEvent) -> HookResponse:
|
|
11
|
+
"""Handle BEFORE_AGENT event (user prompt submit)."""
|
|
12
|
+
input_data = event.data
|
|
13
|
+
prompt = input_data.get("prompt", "")
|
|
14
|
+
transcript_path = input_data.get("transcript_path")
|
|
15
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
16
|
+
|
|
17
|
+
context_parts = []
|
|
18
|
+
|
|
19
|
+
if session_id:
|
|
20
|
+
self.logger.debug(f"BEFORE_AGENT: session {session_id}, prompt_len={len(prompt)}")
|
|
21
|
+
|
|
22
|
+
# Update status to active (unless /clear or /exit)
|
|
23
|
+
prompt_lower = prompt.strip().lower()
|
|
24
|
+
if prompt_lower not in ("/clear", "/exit") and self._session_manager:
|
|
25
|
+
try:
|
|
26
|
+
self._session_manager.update_session_status(session_id, "active")
|
|
27
|
+
if self._session_storage:
|
|
28
|
+
self._session_storage.reset_transcript_processed(session_id)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
self.logger.warning(f"Failed to update session status: {e}")
|
|
31
|
+
|
|
32
|
+
# Handle /clear command - lifecycle workflows handle handoff
|
|
33
|
+
if prompt_lower in ("/clear", "/exit") and transcript_path:
|
|
34
|
+
self.logger.debug(f"Detected {prompt_lower} - lifecycle workflows handle handoff")
|
|
35
|
+
|
|
36
|
+
# Execute lifecycle workflow triggers
|
|
37
|
+
if self._workflow_handler:
|
|
38
|
+
try:
|
|
39
|
+
wf_response = self._workflow_handler.handle_all_lifecycles(event)
|
|
40
|
+
if wf_response.context:
|
|
41
|
+
context_parts.append(wf_response.context)
|
|
42
|
+
if wf_response.decision != "allow":
|
|
43
|
+
return wf_response
|
|
44
|
+
except Exception as e:
|
|
45
|
+
self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
|
|
46
|
+
|
|
47
|
+
return HookResponse(
|
|
48
|
+
decision="allow",
|
|
49
|
+
context="\n\n".join(context_parts) if context_parts else None,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def handle_after_agent(self, event: HookEvent) -> HookResponse:
|
|
53
|
+
"""Handle AFTER_AGENT event."""
|
|
54
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
55
|
+
cli_source = event.source.value
|
|
56
|
+
|
|
57
|
+
context_parts = []
|
|
58
|
+
|
|
59
|
+
if session_id:
|
|
60
|
+
self.logger.debug(f"AFTER_AGENT: session {session_id}, cli={cli_source}")
|
|
61
|
+
if self._session_manager:
|
|
62
|
+
try:
|
|
63
|
+
self._session_manager.update_session_status(session_id, "paused")
|
|
64
|
+
except Exception as e:
|
|
65
|
+
self.logger.warning(f"Failed to update session status: {e}")
|
|
66
|
+
else:
|
|
67
|
+
self.logger.debug(f"AFTER_AGENT: cli={cli_source}")
|
|
68
|
+
|
|
69
|
+
# Execute lifecycle workflow triggers
|
|
70
|
+
if self._workflow_handler:
|
|
71
|
+
try:
|
|
72
|
+
wf_response = self._workflow_handler.handle_all_lifecycles(event)
|
|
73
|
+
if wf_response.context:
|
|
74
|
+
context_parts.append(wf_response.context)
|
|
75
|
+
if wf_response.decision != "allow":
|
|
76
|
+
return wf_response
|
|
77
|
+
except Exception as e:
|
|
78
|
+
self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
|
|
79
|
+
|
|
80
|
+
return HookResponse(
|
|
81
|
+
decision="allow",
|
|
82
|
+
context="\n\n".join(context_parts) if context_parts else None,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def handle_stop(self, event: HookEvent) -> HookResponse:
|
|
86
|
+
"""Handle STOP event (Claude Code only)."""
|
|
87
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
88
|
+
|
|
89
|
+
context_parts = []
|
|
90
|
+
|
|
91
|
+
if session_id:
|
|
92
|
+
self.logger.debug(f"STOP: session {session_id}")
|
|
93
|
+
if self._session_manager:
|
|
94
|
+
try:
|
|
95
|
+
self._session_manager.update_session_status(session_id, "paused")
|
|
96
|
+
except Exception as e:
|
|
97
|
+
self.logger.warning(f"Failed to update session status: {e}")
|
|
98
|
+
else:
|
|
99
|
+
self.logger.debug("STOP")
|
|
100
|
+
|
|
101
|
+
# Execute lifecycle workflow triggers
|
|
102
|
+
if self._workflow_handler:
|
|
103
|
+
try:
|
|
104
|
+
wf_response = self._workflow_handler.handle_all_lifecycles(event)
|
|
105
|
+
if wf_response.context:
|
|
106
|
+
context_parts.append(wf_response.context)
|
|
107
|
+
if wf_response.decision != "allow":
|
|
108
|
+
return wf_response
|
|
109
|
+
except Exception as e:
|
|
110
|
+
self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
|
|
111
|
+
|
|
112
|
+
return HookResponse(
|
|
113
|
+
decision="allow",
|
|
114
|
+
context="\n\n".join(context_parts) if context_parts else None,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def handle_pre_compact(self, event: HookEvent) -> HookResponse:
|
|
118
|
+
"""Handle PRE_COMPACT event.
|
|
119
|
+
|
|
120
|
+
Note: Gemini fires PreCompress constantly during normal operation,
|
|
121
|
+
unlike Claude which fires it only when approaching context limits.
|
|
122
|
+
We skip handoff logic and workflow execution for Gemini to avoid
|
|
123
|
+
excessive state changes and workflow interruptions.
|
|
124
|
+
"""
|
|
125
|
+
trigger = event.data.get("trigger", "auto")
|
|
126
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
127
|
+
|
|
128
|
+
# Skip handoff logic for Gemini - it fires PreCompress too frequently
|
|
129
|
+
if event.source == SessionSource.GEMINI:
|
|
130
|
+
self.logger.debug(f"PRE_COMPACT ({trigger}): session {session_id} [Gemini - skipped]")
|
|
131
|
+
return HookResponse(decision="allow")
|
|
132
|
+
|
|
133
|
+
if session_id:
|
|
134
|
+
self.logger.debug(f"PRE_COMPACT ({trigger}): session {session_id}")
|
|
135
|
+
# Mark session as handoff_ready so it can be found as parent after compact
|
|
136
|
+
if self._session_manager:
|
|
137
|
+
self._session_manager.update_session_status(session_id, "handoff_ready")
|
|
138
|
+
else:
|
|
139
|
+
self.logger.debug(f"PRE_COMPACT ({trigger})")
|
|
140
|
+
|
|
141
|
+
# Execute lifecycle workflows
|
|
142
|
+
if self._workflow_handler:
|
|
143
|
+
try:
|
|
144
|
+
return self._workflow_handler.handle_all_lifecycles(event)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
|
|
147
|
+
|
|
148
|
+
return HookResponse(decision="allow")
|
|
149
|
+
|
|
150
|
+
def handle_subagent_start(self, event: HookEvent) -> HookResponse:
|
|
151
|
+
"""Handle SUBAGENT_START event."""
|
|
152
|
+
input_data = event.data
|
|
153
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
154
|
+
agent_id = input_data.get("agent_id")
|
|
155
|
+
subagent_id = input_data.get("subagent_id")
|
|
156
|
+
|
|
157
|
+
log_msg = f"SUBAGENT_START: session {session_id}" if session_id else "SUBAGENT_START"
|
|
158
|
+
if agent_id:
|
|
159
|
+
log_msg += f", agent_id={agent_id}"
|
|
160
|
+
if subagent_id:
|
|
161
|
+
log_msg += f", subagent_id={subagent_id}"
|
|
162
|
+
self.logger.debug(log_msg)
|
|
163
|
+
|
|
164
|
+
return HookResponse(decision="allow")
|
|
165
|
+
|
|
166
|
+
def handle_subagent_stop(self, event: HookEvent) -> HookResponse:
|
|
167
|
+
"""Handle SUBAGENT_STOP event."""
|
|
168
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
169
|
+
|
|
170
|
+
if session_id:
|
|
171
|
+
self.logger.debug(f"SUBAGENT_STOP: session {session_id}")
|
|
172
|
+
else:
|
|
173
|
+
self.logger.debug("SUBAGENT_STOP")
|
|
174
|
+
|
|
175
|
+
return HookResponse(decision="allow")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from gobby.hooks.events import HookEvent, HookEventType, HookResponse
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from gobby.config.skills import SkillsConfig
|
|
11
|
+
from gobby.config.tasks import WorkflowConfig
|
|
12
|
+
from gobby.hooks.artifact_capture import ArtifactCaptureHook
|
|
13
|
+
from gobby.hooks.session_coordinator import SessionCoordinator
|
|
14
|
+
from gobby.hooks.skill_manager import HookSkillManager
|
|
15
|
+
from gobby.sessions.manager import SessionManager
|
|
16
|
+
from gobby.sessions.summary import SummaryFileGenerator
|
|
17
|
+
from gobby.storage.session_messages import LocalSessionMessageManager
|
|
18
|
+
from gobby.storage.session_tasks import SessionTaskManager
|
|
19
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
20
|
+
from gobby.storage.tasks import LocalTaskManager
|
|
21
|
+
from gobby.workflows.hooks import WorkflowHookHandler
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EventHandlersBase:
|
|
25
|
+
"""Base class for EventHandlers mixins with type hints for shared state."""
|
|
26
|
+
|
|
27
|
+
_session_manager: SessionManager | None
|
|
28
|
+
_workflow_handler: WorkflowHookHandler | None
|
|
29
|
+
_workflow_config: WorkflowConfig | None
|
|
30
|
+
_session_storage: LocalSessionManager | None
|
|
31
|
+
_session_task_manager: SessionTaskManager | None
|
|
32
|
+
_message_processor: Any | None
|
|
33
|
+
_summary_file_generator: SummaryFileGenerator | None
|
|
34
|
+
_task_manager: LocalTaskManager | None
|
|
35
|
+
_session_coordinator: SessionCoordinator | None
|
|
36
|
+
_message_manager: LocalSessionMessageManager | None
|
|
37
|
+
_skill_manager: HookSkillManager | None
|
|
38
|
+
_skills_config: SkillsConfig | None
|
|
39
|
+
_artifact_capture_hook: ArtifactCaptureHook | None
|
|
40
|
+
_get_machine_id: Callable[[], str]
|
|
41
|
+
_resolve_project_id: Callable[[str | None, str | None], str]
|
|
42
|
+
logger: logging.Logger
|
|
43
|
+
_handler_map: dict[HookEventType, Callable[[HookEvent], HookResponse]]
|
|
44
|
+
|
|
45
|
+
def _auto_activate_workflow(
|
|
46
|
+
self, workflow_name: str, session_id: str, project_path: str | None
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Shared method for auto-activating workflows."""
|
|
49
|
+
if not self._workflow_handler:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
result = self._workflow_handler.activate_workflow(
|
|
54
|
+
workflow_name=workflow_name,
|
|
55
|
+
session_id=session_id,
|
|
56
|
+
project_path=project_path,
|
|
57
|
+
)
|
|
58
|
+
if result.get("success"):
|
|
59
|
+
self.logger.info(
|
|
60
|
+
"Auto-activated workflow for session",
|
|
61
|
+
extra={
|
|
62
|
+
"workflow_name": workflow_name,
|
|
63
|
+
"session_id": session_id,
|
|
64
|
+
"project_path": project_path,
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
self.logger.warning(
|
|
69
|
+
"Failed to auto-activate workflow",
|
|
70
|
+
extra={
|
|
71
|
+
"workflow_name": workflow_name,
|
|
72
|
+
"session_id": session_id,
|
|
73
|
+
"project_path": project_path,
|
|
74
|
+
"error": result.get("error"),
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
self.logger.warning(
|
|
79
|
+
"Failed to auto-activate workflow",
|
|
80
|
+
extra={
|
|
81
|
+
"workflow_name": workflow_name,
|
|
82
|
+
"session_id": session_id,
|
|
83
|
+
"project_path": project_path,
|
|
84
|
+
"error": str(e),
|
|
85
|
+
},
|
|
86
|
+
exc_info=True,
|
|
87
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from gobby.hooks.event_handlers._base import EventHandlersBase
|
|
4
|
+
from gobby.hooks.events import HookEvent, HookResponse
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MiscEventHandlerMixin(EventHandlersBase):
|
|
8
|
+
"""Mixin for handling miscellaneous events."""
|
|
9
|
+
|
|
10
|
+
def handle_notification(self, event: HookEvent) -> HookResponse:
|
|
11
|
+
"""Handle NOTIFICATION event."""
|
|
12
|
+
input_data = event.data
|
|
13
|
+
notification_type = (
|
|
14
|
+
input_data.get("notification_type")
|
|
15
|
+
or input_data.get("notificationType")
|
|
16
|
+
or input_data.get("type")
|
|
17
|
+
or "general"
|
|
18
|
+
)
|
|
19
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
20
|
+
|
|
21
|
+
if session_id:
|
|
22
|
+
self.logger.debug(f"NOTIFICATION ({notification_type}): session {session_id}")
|
|
23
|
+
if self._session_manager:
|
|
24
|
+
try:
|
|
25
|
+
self._session_manager.update_session_status(session_id, "paused")
|
|
26
|
+
except Exception as e:
|
|
27
|
+
self.logger.warning(f"Failed to update session status: {e}")
|
|
28
|
+
else:
|
|
29
|
+
self.logger.debug(f"NOTIFICATION ({notification_type})")
|
|
30
|
+
|
|
31
|
+
return HookResponse(decision="allow")
|
|
32
|
+
|
|
33
|
+
def handle_permission_request(self, event: HookEvent) -> HookResponse:
|
|
34
|
+
"""Handle PERMISSION_REQUEST event (Claude Code only)."""
|
|
35
|
+
input_data = event.data
|
|
36
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
37
|
+
permission_type = input_data.get("permission_type", "unknown")
|
|
38
|
+
|
|
39
|
+
if session_id:
|
|
40
|
+
self.logger.debug(f"PERMISSION_REQUEST ({permission_type}): session {session_id}")
|
|
41
|
+
else:
|
|
42
|
+
self.logger.debug(f"PERMISSION_REQUEST ({permission_type})")
|
|
43
|
+
|
|
44
|
+
return HookResponse(decision="allow")
|
|
45
|
+
|
|
46
|
+
def handle_before_model(self, event: HookEvent) -> HookResponse:
|
|
47
|
+
"""Handle BEFORE_MODEL event (Gemini only)."""
|
|
48
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
49
|
+
|
|
50
|
+
if session_id:
|
|
51
|
+
self.logger.debug(f"BEFORE_MODEL: session {session_id}")
|
|
52
|
+
else:
|
|
53
|
+
self.logger.debug("BEFORE_MODEL")
|
|
54
|
+
|
|
55
|
+
return HookResponse(decision="allow")
|
|
56
|
+
|
|
57
|
+
def handle_after_model(self, event: HookEvent) -> HookResponse:
|
|
58
|
+
"""Handle AFTER_MODEL event (Gemini only)."""
|
|
59
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
60
|
+
|
|
61
|
+
if session_id:
|
|
62
|
+
self.logger.debug(f"AFTER_MODEL: session {session_id}")
|
|
63
|
+
else:
|
|
64
|
+
self.logger.debug("AFTER_MODEL")
|
|
65
|
+
|
|
66
|
+
return HookResponse(decision="allow")
|