gobby 0.2.8__py3-none-any.whl → 0.2.11__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 (168) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +6 -0
  3. gobby/adapters/base.py +11 -2
  4. gobby/adapters/claude_code.py +5 -28
  5. gobby/adapters/codex_impl/adapter.py +38 -43
  6. gobby/adapters/copilot.py +324 -0
  7. gobby/adapters/cursor.py +373 -0
  8. gobby/adapters/gemini.py +2 -26
  9. gobby/adapters/windsurf.py +359 -0
  10. gobby/agents/definitions.py +162 -2
  11. gobby/agents/isolation.py +33 -1
  12. gobby/agents/pty_reader.py +192 -0
  13. gobby/agents/registry.py +10 -1
  14. gobby/agents/runner.py +24 -8
  15. gobby/agents/sandbox.py +8 -3
  16. gobby/agents/session.py +4 -0
  17. gobby/agents/spawn.py +9 -2
  18. gobby/agents/spawn_executor.py +49 -61
  19. gobby/agents/spawners/command_builder.py +4 -4
  20. gobby/app_context.py +64 -0
  21. gobby/cli/__init__.py +4 -0
  22. gobby/cli/install.py +259 -4
  23. gobby/cli/installers/__init__.py +12 -0
  24. gobby/cli/installers/copilot.py +242 -0
  25. gobby/cli/installers/cursor.py +244 -0
  26. gobby/cli/installers/shared.py +3 -0
  27. gobby/cli/installers/windsurf.py +242 -0
  28. gobby/cli/pipelines.py +639 -0
  29. gobby/cli/sessions.py +3 -1
  30. gobby/cli/skills.py +209 -0
  31. gobby/cli/tasks/crud.py +6 -5
  32. gobby/cli/tasks/search.py +1 -1
  33. gobby/cli/ui.py +116 -0
  34. gobby/cli/utils.py +5 -17
  35. gobby/cli/workflows.py +38 -17
  36. gobby/config/app.py +5 -0
  37. gobby/config/features.py +0 -20
  38. gobby/config/skills.py +23 -2
  39. gobby/config/tasks.py +4 -0
  40. gobby/hooks/broadcaster.py +9 -0
  41. gobby/hooks/event_handlers/__init__.py +155 -0
  42. gobby/hooks/event_handlers/_agent.py +175 -0
  43. gobby/hooks/event_handlers/_base.py +92 -0
  44. gobby/hooks/event_handlers/_misc.py +66 -0
  45. gobby/hooks/event_handlers/_session.py +487 -0
  46. gobby/hooks/event_handlers/_tool.py +196 -0
  47. gobby/hooks/events.py +48 -0
  48. gobby/hooks/hook_manager.py +27 -3
  49. gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
  50. gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
  51. gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
  52. gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
  53. gobby/llm/__init__.py +14 -1
  54. gobby/llm/claude.py +594 -43
  55. gobby/llm/service.py +149 -0
  56. gobby/mcp_proxy/importer.py +4 -41
  57. gobby/mcp_proxy/instructions.py +9 -27
  58. gobby/mcp_proxy/manager.py +13 -3
  59. gobby/mcp_proxy/models.py +1 -0
  60. gobby/mcp_proxy/registries.py +66 -5
  61. gobby/mcp_proxy/server.py +6 -2
  62. gobby/mcp_proxy/services/recommendation.py +2 -28
  63. gobby/mcp_proxy/services/tool_filter.py +7 -0
  64. gobby/mcp_proxy/services/tool_proxy.py +19 -1
  65. gobby/mcp_proxy/stdio.py +37 -21
  66. gobby/mcp_proxy/tools/agents.py +7 -0
  67. gobby/mcp_proxy/tools/artifacts.py +3 -3
  68. gobby/mcp_proxy/tools/hub.py +30 -1
  69. gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
  70. gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
  71. gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
  72. gobby/mcp_proxy/tools/orchestration/review.py +17 -4
  73. gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
  74. gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
  75. gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
  76. gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
  77. gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
  78. gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
  79. gobby/mcp_proxy/tools/skills/__init__.py +184 -30
  80. gobby/mcp_proxy/tools/spawn_agent.py +229 -14
  81. gobby/mcp_proxy/tools/task_readiness.py +27 -4
  82. gobby/mcp_proxy/tools/tasks/_context.py +8 -0
  83. gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
  84. gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
  85. gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
  86. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
  87. gobby/mcp_proxy/tools/tasks/_search.py +1 -1
  88. gobby/mcp_proxy/tools/workflows/__init__.py +273 -0
  89. gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
  90. gobby/mcp_proxy/tools/workflows/_import.py +112 -0
  91. gobby/mcp_proxy/tools/workflows/_lifecycle.py +332 -0
  92. gobby/mcp_proxy/tools/workflows/_query.py +226 -0
  93. gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
  94. gobby/mcp_proxy/tools/workflows/_terminal.py +175 -0
  95. gobby/mcp_proxy/tools/worktrees.py +54 -15
  96. gobby/memory/components/__init__.py +0 -0
  97. gobby/memory/components/ingestion.py +98 -0
  98. gobby/memory/components/search.py +108 -0
  99. gobby/memory/context.py +5 -5
  100. gobby/memory/manager.py +16 -25
  101. gobby/paths.py +51 -0
  102. gobby/prompts/loader.py +1 -35
  103. gobby/runner.py +131 -16
  104. gobby/servers/http.py +193 -150
  105. gobby/servers/routes/__init__.py +2 -0
  106. gobby/servers/routes/admin.py +56 -0
  107. gobby/servers/routes/mcp/endpoints/execution.py +33 -32
  108. gobby/servers/routes/mcp/endpoints/registry.py +8 -8
  109. gobby/servers/routes/mcp/hooks.py +10 -1
  110. gobby/servers/routes/pipelines.py +227 -0
  111. gobby/servers/websocket.py +314 -1
  112. gobby/sessions/analyzer.py +89 -3
  113. gobby/sessions/manager.py +5 -5
  114. gobby/sessions/transcripts/__init__.py +3 -0
  115. gobby/sessions/transcripts/claude.py +5 -0
  116. gobby/sessions/transcripts/codex.py +5 -0
  117. gobby/sessions/transcripts/gemini.py +5 -0
  118. gobby/skills/hubs/__init__.py +25 -0
  119. gobby/skills/hubs/base.py +234 -0
  120. gobby/skills/hubs/claude_plugins.py +328 -0
  121. gobby/skills/hubs/clawdhub.py +289 -0
  122. gobby/skills/hubs/github_collection.py +465 -0
  123. gobby/skills/hubs/manager.py +263 -0
  124. gobby/skills/hubs/skillhub.py +342 -0
  125. gobby/skills/parser.py +23 -0
  126. gobby/skills/sync.py +5 -4
  127. gobby/storage/artifacts.py +19 -0
  128. gobby/storage/memories.py +4 -4
  129. gobby/storage/migrations.py +118 -3
  130. gobby/storage/pipelines.py +367 -0
  131. gobby/storage/sessions.py +23 -4
  132. gobby/storage/skills.py +48 -8
  133. gobby/storage/tasks/_aggregates.py +2 -2
  134. gobby/storage/tasks/_lifecycle.py +4 -4
  135. gobby/storage/tasks/_models.py +7 -1
  136. gobby/storage/tasks/_queries.py +3 -3
  137. gobby/sync/memories.py +4 -3
  138. gobby/tasks/commits.py +48 -17
  139. gobby/tasks/external_validator.py +4 -17
  140. gobby/tasks/validation.py +13 -87
  141. gobby/tools/summarizer.py +18 -51
  142. gobby/utils/status.py +13 -0
  143. gobby/workflows/actions.py +80 -0
  144. gobby/workflows/context_actions.py +265 -27
  145. gobby/workflows/definitions.py +119 -1
  146. gobby/workflows/detection_helpers.py +23 -11
  147. gobby/workflows/enforcement/__init__.py +11 -1
  148. gobby/workflows/enforcement/blocking.py +96 -0
  149. gobby/workflows/enforcement/handlers.py +35 -1
  150. gobby/workflows/enforcement/task_policy.py +18 -0
  151. gobby/workflows/engine.py +26 -4
  152. gobby/workflows/evaluator.py +8 -5
  153. gobby/workflows/lifecycle_evaluator.py +59 -27
  154. gobby/workflows/loader.py +567 -30
  155. gobby/workflows/lobster_compat.py +147 -0
  156. gobby/workflows/pipeline_executor.py +801 -0
  157. gobby/workflows/pipeline_state.py +172 -0
  158. gobby/workflows/pipeline_webhooks.py +206 -0
  159. gobby/workflows/premature_stop.py +5 -0
  160. gobby/worktrees/git.py +135 -20
  161. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
  162. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/RECORD +166 -122
  163. gobby/hooks/event_handlers.py +0 -1008
  164. gobby/mcp_proxy/tools/workflows.py +0 -1023
  165. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
  166. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
  167. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
  168. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
@@ -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,92 @@
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,
47
+ workflow_name: str,
48
+ session_id: str,
49
+ project_path: str | None,
50
+ variables: dict[str, Any] | None = None,
51
+ ) -> None:
52
+ """Shared method for auto-activating workflows."""
53
+ if not self._workflow_handler:
54
+ return
55
+
56
+ try:
57
+ result = self._workflow_handler.activate_workflow(
58
+ workflow_name=workflow_name,
59
+ session_id=session_id,
60
+ project_path=project_path,
61
+ variables=variables,
62
+ )
63
+ if result.get("success"):
64
+ self.logger.info(
65
+ "Auto-activated workflow for session",
66
+ extra={
67
+ "workflow_name": workflow_name,
68
+ "session_id": session_id,
69
+ "project_path": project_path,
70
+ },
71
+ )
72
+ else:
73
+ self.logger.warning(
74
+ "Failed to auto-activate workflow",
75
+ extra={
76
+ "workflow_name": workflow_name,
77
+ "session_id": session_id,
78
+ "project_path": project_path,
79
+ "error": result.get("error"),
80
+ },
81
+ )
82
+ except Exception as e:
83
+ self.logger.warning(
84
+ "Failed to auto-activate workflow",
85
+ extra={
86
+ "workflow_name": workflow_name,
87
+ "session_id": session_id,
88
+ "project_path": project_path,
89
+ "error": str(e),
90
+ },
91
+ exc_info=True,
92
+ )
@@ -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")