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.
Files changed (125) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/claude_code.py +99 -61
  3. gobby/adapters/gemini.py +140 -38
  4. gobby/agents/isolation.py +130 -0
  5. gobby/agents/registry.py +11 -0
  6. gobby/agents/session.py +1 -0
  7. gobby/agents/spawn_executor.py +43 -13
  8. gobby/agents/spawners/macos.py +26 -1
  9. gobby/app_context.py +59 -0
  10. gobby/cli/__init__.py +0 -2
  11. gobby/cli/memory.py +185 -0
  12. gobby/cli/utils.py +5 -17
  13. gobby/clones/git.py +177 -0
  14. gobby/config/features.py +0 -20
  15. gobby/config/skills.py +31 -0
  16. gobby/config/tasks.py +4 -0
  17. gobby/hooks/event_handlers/__init__.py +155 -0
  18. gobby/hooks/event_handlers/_agent.py +175 -0
  19. gobby/hooks/event_handlers/_base.py +87 -0
  20. gobby/hooks/event_handlers/_misc.py +66 -0
  21. gobby/hooks/event_handlers/_session.py +573 -0
  22. gobby/hooks/event_handlers/_tool.py +196 -0
  23. gobby/hooks/hook_manager.py +21 -1
  24. gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
  25. gobby/llm/claude.py +377 -42
  26. gobby/mcp_proxy/importer.py +4 -41
  27. gobby/mcp_proxy/instructions.py +2 -2
  28. gobby/mcp_proxy/manager.py +13 -3
  29. gobby/mcp_proxy/registries.py +35 -4
  30. gobby/mcp_proxy/services/recommendation.py +2 -28
  31. gobby/mcp_proxy/tools/agent_messaging.py +93 -44
  32. gobby/mcp_proxy/tools/agents.py +45 -9
  33. gobby/mcp_proxy/tools/artifacts.py +46 -12
  34. gobby/mcp_proxy/tools/sessions/_commits.py +31 -24
  35. gobby/mcp_proxy/tools/sessions/_crud.py +5 -5
  36. gobby/mcp_proxy/tools/sessions/_handoff.py +45 -41
  37. gobby/mcp_proxy/tools/sessions/_messages.py +35 -7
  38. gobby/mcp_proxy/tools/spawn_agent.py +44 -6
  39. gobby/mcp_proxy/tools/task_readiness.py +27 -4
  40. gobby/mcp_proxy/tools/tasks/_context.py +18 -0
  41. gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
  42. gobby/mcp_proxy/tools/tasks/_lifecycle.py +29 -14
  43. gobby/mcp_proxy/tools/tasks/_session.py +22 -7
  44. gobby/mcp_proxy/tools/workflows/__init__.py +266 -0
  45. gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
  46. gobby/mcp_proxy/tools/workflows/_import.py +112 -0
  47. gobby/mcp_proxy/tools/workflows/_lifecycle.py +321 -0
  48. gobby/mcp_proxy/tools/workflows/_query.py +207 -0
  49. gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
  50. gobby/mcp_proxy/tools/workflows/_terminal.py +139 -0
  51. gobby/mcp_proxy/tools/worktrees.py +32 -7
  52. gobby/memory/components/__init__.py +0 -0
  53. gobby/memory/components/ingestion.py +98 -0
  54. gobby/memory/components/search.py +108 -0
  55. gobby/memory/extractor.py +15 -1
  56. gobby/memory/manager.py +16 -25
  57. gobby/paths.py +51 -0
  58. gobby/prompts/loader.py +1 -35
  59. gobby/runner.py +36 -10
  60. gobby/servers/http.py +186 -149
  61. gobby/servers/routes/admin.py +12 -0
  62. gobby/servers/routes/mcp/endpoints/execution.py +15 -7
  63. gobby/servers/routes/mcp/endpoints/registry.py +8 -8
  64. gobby/servers/routes/mcp/hooks.py +50 -3
  65. gobby/servers/websocket.py +57 -1
  66. gobby/sessions/analyzer.py +4 -4
  67. gobby/sessions/manager.py +9 -0
  68. gobby/sessions/transcripts/gemini.py +100 -34
  69. gobby/skills/parser.py +23 -0
  70. gobby/skills/sync.py +5 -4
  71. gobby/storage/artifacts.py +19 -0
  72. gobby/storage/database.py +9 -2
  73. gobby/storage/memories.py +32 -21
  74. gobby/storage/migrations.py +46 -4
  75. gobby/storage/sessions.py +4 -2
  76. gobby/storage/skills.py +87 -7
  77. gobby/tasks/external_validator.py +4 -17
  78. gobby/tasks/validation.py +13 -87
  79. gobby/tools/summarizer.py +18 -51
  80. gobby/utils/status.py +13 -0
  81. gobby/workflows/actions.py +5 -0
  82. gobby/workflows/context_actions.py +21 -24
  83. gobby/workflows/detection_helpers.py +38 -24
  84. gobby/workflows/enforcement/__init__.py +11 -1
  85. gobby/workflows/enforcement/blocking.py +109 -1
  86. gobby/workflows/enforcement/handlers.py +35 -1
  87. gobby/workflows/engine.py +96 -0
  88. gobby/workflows/evaluator.py +110 -0
  89. gobby/workflows/hooks.py +41 -0
  90. gobby/workflows/lifecycle_evaluator.py +2 -1
  91. gobby/workflows/memory_actions.py +11 -0
  92. gobby/workflows/safe_evaluator.py +8 -0
  93. gobby/workflows/summary_actions.py +123 -50
  94. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/METADATA +1 -1
  95. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/RECORD +99 -107
  96. gobby/cli/tui.py +0 -34
  97. gobby/hooks/event_handlers.py +0 -909
  98. gobby/mcp_proxy/tools/workflows.py +0 -973
  99. gobby/tui/__init__.py +0 -5
  100. gobby/tui/api_client.py +0 -278
  101. gobby/tui/app.py +0 -329
  102. gobby/tui/screens/__init__.py +0 -25
  103. gobby/tui/screens/agents.py +0 -333
  104. gobby/tui/screens/chat.py +0 -450
  105. gobby/tui/screens/dashboard.py +0 -377
  106. gobby/tui/screens/memory.py +0 -305
  107. gobby/tui/screens/metrics.py +0 -231
  108. gobby/tui/screens/orchestrator.py +0 -903
  109. gobby/tui/screens/sessions.py +0 -412
  110. gobby/tui/screens/tasks.py +0 -440
  111. gobby/tui/screens/workflows.py +0 -289
  112. gobby/tui/screens/worktrees.py +0 -174
  113. gobby/tui/widgets/__init__.py +0 -21
  114. gobby/tui/widgets/chat.py +0 -210
  115. gobby/tui/widgets/conductor.py +0 -104
  116. gobby/tui/widgets/menu.py +0 -132
  117. gobby/tui/widgets/message_panel.py +0 -160
  118. gobby/tui/widgets/review_gate.py +0 -224
  119. gobby/tui/widgets/task_tree.py +0 -99
  120. gobby/tui/widgets/token_budget.py +0 -166
  121. gobby/tui/ws_client.py +0 -258
  122. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/WHEEL +0 -0
  123. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/entry_points.txt +0 -0
  124. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/licenses/LICENSE.md +0 -0
  125. {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")