gobby 0.2.7__py3-none-any.whl → 0.2.8__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/adapters/claude_code.py +96 -35
- 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/cli/__init__.py +0 -2
- gobby/cli/memory.py +185 -0
- gobby/clones/git.py +177 -0
- gobby/config/skills.py +31 -0
- gobby/hooks/event_handlers.py +109 -10
- gobby/hooks/hook_manager.py +19 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
- gobby/mcp_proxy/instructions.py +2 -2
- gobby/mcp_proxy/registries.py +21 -4
- gobby/mcp_proxy/tools/agent_messaging.py +93 -44
- gobby/mcp_proxy/tools/agents.py +45 -9
- gobby/mcp_proxy/tools/artifacts.py +43 -9
- 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/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.py +84 -34
- gobby/mcp_proxy/tools/worktrees.py +32 -7
- gobby/memory/extractor.py +15 -1
- gobby/runner.py +13 -0
- gobby/servers/routes/mcp/hooks.py +50 -3
- gobby/servers/websocket.py +57 -1
- gobby/sessions/analyzer.py +2 -2
- gobby/sessions/manager.py +9 -0
- gobby/sessions/transcripts/gemini.py +100 -34
- gobby/storage/database.py +9 -2
- gobby/storage/memories.py +32 -21
- gobby/storage/migrations.py +23 -4
- gobby/storage/sessions.py +4 -2
- gobby/storage/skills.py +43 -3
- gobby/workflows/detection_helpers.py +38 -24
- gobby/workflows/enforcement/blocking.py +13 -1
- gobby/workflows/engine.py +93 -0
- gobby/workflows/evaluator.py +110 -0
- gobby/workflows/hooks.py +41 -0
- 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.8.dist-info}/METADATA +1 -1
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/RECORD +56 -80
- gobby/cli/tui.py +0 -34
- 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.8.dist-info}/WHEEL +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
gobby/adapters/claude_code.py
CHANGED
|
@@ -104,6 +104,10 @@ class ClaudeCodeAdapter(BaseAdapter):
|
|
|
104
104
|
is_failure = hook_type == "post-tool-use-failure"
|
|
105
105
|
metadata = {"is_failure": is_failure} if is_failure else {}
|
|
106
106
|
|
|
107
|
+
# Normalize event data for CLI-agnostic processing
|
|
108
|
+
# This allows downstream code to use consistent field names
|
|
109
|
+
normalized_data = self._normalize_event_data(input_data)
|
|
110
|
+
|
|
107
111
|
return HookEvent(
|
|
108
112
|
event_type=event_type,
|
|
109
113
|
session_id=session_id,
|
|
@@ -111,10 +115,46 @@ class ClaudeCodeAdapter(BaseAdapter):
|
|
|
111
115
|
timestamp=datetime.now(UTC),
|
|
112
116
|
machine_id=input_data.get("machine_id"),
|
|
113
117
|
cwd=input_data.get("cwd"),
|
|
114
|
-
data=
|
|
118
|
+
data=normalized_data,
|
|
115
119
|
metadata=metadata,
|
|
116
120
|
)
|
|
117
121
|
|
|
122
|
+
def _normalize_event_data(self, input_data: dict[str, Any]) -> dict[str, Any]:
|
|
123
|
+
"""Normalize Claude Code event data for CLI-agnostic processing.
|
|
124
|
+
|
|
125
|
+
This method enriches the input_data with normalized fields so downstream
|
|
126
|
+
code doesn't need to handle Claude-specific formats.
|
|
127
|
+
|
|
128
|
+
Normalizations performed:
|
|
129
|
+
1. tool_input.server_name/tool_name → mcp_server/mcp_tool (for MCP calls)
|
|
130
|
+
2. tool_result → tool_output
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
input_data: Raw input data from Claude Code
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Enriched data dict with normalized fields added
|
|
137
|
+
"""
|
|
138
|
+
# Start with a copy to avoid mutating original
|
|
139
|
+
data = dict(input_data)
|
|
140
|
+
|
|
141
|
+
# Get tool info
|
|
142
|
+
tool_name = data.get("tool_name", "")
|
|
143
|
+
tool_input = data.get("tool_input", {}) or {}
|
|
144
|
+
|
|
145
|
+
# 1. Extract MCP info from nested tool_input for call_tool calls
|
|
146
|
+
if tool_name in ("call_tool", "mcp__gobby__call_tool"):
|
|
147
|
+
if "mcp_server" not in data:
|
|
148
|
+
data["mcp_server"] = tool_input.get("server_name")
|
|
149
|
+
if "mcp_tool" not in data:
|
|
150
|
+
data["mcp_tool"] = tool_input.get("tool_name")
|
|
151
|
+
|
|
152
|
+
# 2. Normalize tool_result → tool_output
|
|
153
|
+
if "tool_result" in data and "tool_output" not in data:
|
|
154
|
+
data["tool_output"] = data["tool_result"]
|
|
155
|
+
|
|
156
|
+
return data
|
|
157
|
+
|
|
118
158
|
# Map Claude Code hook types to hookEventName for hookSpecificOutput
|
|
119
159
|
HOOK_EVENT_NAME_MAP: dict[str, str] = {
|
|
120
160
|
"session-start": "SessionStart",
|
|
@@ -193,44 +233,65 @@ class ClaudeCodeAdapter(BaseAdapter):
|
|
|
193
233
|
# Add session identifiers from metadata
|
|
194
234
|
# Note: "session_id" in metadata is Gobby's internal platform session ID
|
|
195
235
|
# "external_id" in metadata is the CLI's session UUID
|
|
236
|
+
# "session_ref" is the short #N format for easier reference
|
|
237
|
+
# Token optimization: Only inject full metadata on first hook per session
|
|
196
238
|
if response.metadata:
|
|
197
239
|
gobby_session_id = response.metadata.get("session_id")
|
|
240
|
+
session_ref = response.metadata.get("session_ref")
|
|
198
241
|
external_id = response.metadata.get("external_id")
|
|
242
|
+
is_first_hook = response.metadata.get("_first_hook_for_session", False)
|
|
243
|
+
|
|
199
244
|
if gobby_session_id:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
"
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
"
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
245
|
+
if is_first_hook:
|
|
246
|
+
# First hook: inject full metadata (~60-100 tokens)
|
|
247
|
+
context_lines = []
|
|
248
|
+
if session_ref:
|
|
249
|
+
context_lines.append(
|
|
250
|
+
f"Gobby Session ID: {session_ref} (or {gobby_session_id})"
|
|
251
|
+
)
|
|
252
|
+
else:
|
|
253
|
+
context_lines.append(f"Gobby Session ID: {gobby_session_id}")
|
|
254
|
+
if external_id:
|
|
255
|
+
context_lines.append(
|
|
256
|
+
f"CLI-Specific Session ID (external_id): {external_id}"
|
|
257
|
+
)
|
|
258
|
+
if response.metadata.get("parent_session_id"):
|
|
259
|
+
context_lines.append(
|
|
260
|
+
f"parent_session_id: {response.metadata['parent_session_id']}"
|
|
261
|
+
)
|
|
262
|
+
if response.metadata.get("machine_id"):
|
|
263
|
+
context_lines.append(f"machine_id: {response.metadata['machine_id']}")
|
|
264
|
+
if response.metadata.get("project_id"):
|
|
265
|
+
context_lines.append(f"project_id: {response.metadata['project_id']}")
|
|
266
|
+
# Add terminal context (non-null values only)
|
|
267
|
+
if response.metadata.get("terminal_term_program"):
|
|
268
|
+
context_lines.append(
|
|
269
|
+
f"terminal: {response.metadata['terminal_term_program']}"
|
|
270
|
+
)
|
|
271
|
+
if response.metadata.get("terminal_tty"):
|
|
272
|
+
context_lines.append(f"tty: {response.metadata['terminal_tty']}")
|
|
273
|
+
if response.metadata.get("terminal_parent_pid"):
|
|
274
|
+
context_lines.append(
|
|
275
|
+
f"parent_pid: {response.metadata['terminal_parent_pid']}"
|
|
276
|
+
)
|
|
277
|
+
# Add terminal-specific session IDs (only one will be present)
|
|
278
|
+
for key in [
|
|
279
|
+
"terminal_iterm_session_id",
|
|
280
|
+
"terminal_term_session_id",
|
|
281
|
+
"terminal_kitty_window_id",
|
|
282
|
+
"terminal_tmux_pane",
|
|
283
|
+
"terminal_vscode_terminal_id",
|
|
284
|
+
"terminal_alacritty_socket",
|
|
285
|
+
]:
|
|
286
|
+
if response.metadata.get(key):
|
|
287
|
+
# Use friendlier names in output
|
|
288
|
+
friendly_name = key.replace("terminal_", "").replace("_", " ")
|
|
289
|
+
context_lines.append(f"{friendly_name}: {response.metadata[key]}")
|
|
290
|
+
additional_context_parts.append("\n".join(context_lines))
|
|
291
|
+
else:
|
|
292
|
+
# Subsequent hooks: inject minimal session ref only (~8 tokens)
|
|
293
|
+
if session_ref:
|
|
294
|
+
additional_context_parts.append(f"Gobby Session ID: {session_ref}")
|
|
234
295
|
|
|
235
296
|
# Build hookSpecificOutput if we have any context to inject
|
|
236
297
|
# Only include hookSpecificOutput for hook types that Claude Code's schema accepts
|
gobby/adapters/gemini.py
CHANGED
|
@@ -76,21 +76,39 @@ class GeminiAdapter(BaseAdapter):
|
|
|
76
76
|
|
|
77
77
|
# Tool name mapping: Gemini tool names -> normalized names
|
|
78
78
|
# Gemini uses different tool names than Claude Code
|
|
79
|
+
# This enables workflows to use Claude Code naming conventions
|
|
79
80
|
TOOL_MAP: dict[str, str] = {
|
|
81
|
+
# Shell/Bash
|
|
80
82
|
"run_shell_command": "Bash",
|
|
81
83
|
"RunShellCommand": "Bash",
|
|
84
|
+
"ShellTool": "Bash",
|
|
85
|
+
# File read
|
|
82
86
|
"read_file": "Read",
|
|
83
87
|
"ReadFile": "Read",
|
|
84
88
|
"ReadFileTool": "Read",
|
|
89
|
+
# File write
|
|
85
90
|
"write_file": "Write",
|
|
86
91
|
"WriteFile": "Write",
|
|
87
92
|
"WriteFileTool": "Write",
|
|
93
|
+
# File edit
|
|
88
94
|
"edit_file": "Edit",
|
|
89
95
|
"EditFile": "Edit",
|
|
90
96
|
"EditFileTool": "Edit",
|
|
97
|
+
# Search/Glob/Grep
|
|
91
98
|
"GlobTool": "Glob",
|
|
92
99
|
"GrepTool": "Grep",
|
|
93
|
-
"
|
|
100
|
+
"search_file_content": "Grep",
|
|
101
|
+
"SearchText": "Grep",
|
|
102
|
+
# MCP tools (Gobby MCP server)
|
|
103
|
+
"call_tool": "mcp__gobby__call_tool",
|
|
104
|
+
"list_mcp_servers": "mcp__gobby__list_mcp_servers",
|
|
105
|
+
"list_tools": "mcp__gobby__list_tools",
|
|
106
|
+
"get_tool_schema": "mcp__gobby__get_tool_schema",
|
|
107
|
+
"search_tools": "mcp__gobby__search_tools",
|
|
108
|
+
"recommend_tools": "mcp__gobby__recommend_tools",
|
|
109
|
+
# Skill and agent tools
|
|
110
|
+
"activate_skill": "Skill",
|
|
111
|
+
"delegate_to_agent": "Task",
|
|
94
112
|
}
|
|
95
113
|
|
|
96
114
|
def __init__(self, hook_manager: "HookManager | None" = None):
|
|
@@ -135,6 +153,55 @@ class GeminiAdapter(BaseAdapter):
|
|
|
135
153
|
"""
|
|
136
154
|
return self.TOOL_MAP.get(gemini_tool_name, gemini_tool_name)
|
|
137
155
|
|
|
156
|
+
def _normalize_event_data(self, input_data: dict[str, Any]) -> dict[str, Any]:
|
|
157
|
+
"""Normalize Gemini event data for CLI-agnostic processing.
|
|
158
|
+
|
|
159
|
+
This method enriches the input_data with normalized fields so downstream
|
|
160
|
+
code doesn't need to handle Gemini-specific formats.
|
|
161
|
+
|
|
162
|
+
Normalizations performed:
|
|
163
|
+
1. mcp_context.server_name/tool_name → mcp_server/mcp_tool (top-level)
|
|
164
|
+
2. tool_response → tool_output
|
|
165
|
+
3. function_name → tool_name (if not already present)
|
|
166
|
+
4. parameters/args → tool_input (if not already present)
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
input_data: Raw input data from Gemini CLI
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Enriched data dict with normalized fields added
|
|
173
|
+
"""
|
|
174
|
+
# Start with a copy to avoid mutating original
|
|
175
|
+
data = dict(input_data)
|
|
176
|
+
|
|
177
|
+
# 1. Flatten mcp_context to top-level mcp_server/mcp_tool
|
|
178
|
+
mcp_context = data.get("mcp_context")
|
|
179
|
+
if mcp_context and isinstance(mcp_context, dict):
|
|
180
|
+
if "mcp_server" not in data:
|
|
181
|
+
data["mcp_server"] = mcp_context.get("server_name")
|
|
182
|
+
if "mcp_tool" not in data:
|
|
183
|
+
data["mcp_tool"] = mcp_context.get("tool_name")
|
|
184
|
+
|
|
185
|
+
# 2. Normalize tool_response → tool_output
|
|
186
|
+
if "tool_response" in data and "tool_output" not in data:
|
|
187
|
+
data["tool_output"] = data["tool_response"]
|
|
188
|
+
|
|
189
|
+
# 3. Normalize function_name → tool_name
|
|
190
|
+
if "function_name" in data and "tool_name" not in data:
|
|
191
|
+
data["tool_name"] = self.normalize_tool_name(data["function_name"])
|
|
192
|
+
elif "tool_name" in data:
|
|
193
|
+
# Normalize existing tool_name
|
|
194
|
+
data["tool_name"] = self.normalize_tool_name(data["tool_name"])
|
|
195
|
+
|
|
196
|
+
# 4. Normalize parameters/args → tool_input
|
|
197
|
+
if "tool_input" not in data:
|
|
198
|
+
if "parameters" in data:
|
|
199
|
+
data["tool_input"] = data["parameters"]
|
|
200
|
+
elif "args" in data:
|
|
201
|
+
data["tool_input"] = data["args"]
|
|
202
|
+
|
|
203
|
+
return data
|
|
204
|
+
|
|
138
205
|
def translate_to_hook_event(self, native_event: dict[str, Any]) -> HookEvent:
|
|
139
206
|
"""Convert Gemini CLI native event to unified HookEvent.
|
|
140
207
|
|
|
@@ -202,6 +269,10 @@ class GeminiAdapter(BaseAdapter):
|
|
|
202
269
|
else:
|
|
203
270
|
metadata = {}
|
|
204
271
|
|
|
272
|
+
# Normalize event data for CLI-agnostic processing
|
|
273
|
+
# This allows downstream code to use consistent field names
|
|
274
|
+
normalized_data = self._normalize_event_data(input_data)
|
|
275
|
+
|
|
205
276
|
return HookEvent(
|
|
206
277
|
event_type=event_type,
|
|
207
278
|
session_id=session_id,
|
|
@@ -209,7 +280,7 @@ class GeminiAdapter(BaseAdapter):
|
|
|
209
280
|
timestamp=timestamp,
|
|
210
281
|
machine_id=machine_id,
|
|
211
282
|
cwd=input_data.get("cwd"),
|
|
212
|
-
data=
|
|
283
|
+
data=normalized_data,
|
|
213
284
|
metadata=metadata,
|
|
214
285
|
)
|
|
215
286
|
|
|
@@ -254,46 +325,77 @@ class GeminiAdapter(BaseAdapter):
|
|
|
254
325
|
if response.context:
|
|
255
326
|
hook_specific["additionalContext"] = response.context
|
|
256
327
|
|
|
257
|
-
# Add session/terminal context for
|
|
258
|
-
|
|
328
|
+
# Add session/terminal context for hooks that support additionalContext
|
|
329
|
+
# Parity with Claude Code: inject on SessionStart, BeforeAgent, BeforeTool, AfterTool
|
|
330
|
+
hooks_with_context = {"SessionStart", "BeforeAgent", "BeforeTool", "AfterTool"}
|
|
331
|
+
if hook_type in hooks_with_context and response.metadata:
|
|
259
332
|
session_id = response.metadata.get("session_id")
|
|
333
|
+
session_ref = response.metadata.get("session_ref")
|
|
334
|
+
external_id = response.metadata.get("external_id")
|
|
335
|
+
is_first_hook = response.metadata.get("_first_hook_for_session", False)
|
|
336
|
+
|
|
260
337
|
if session_id:
|
|
261
338
|
hook_event_name = self.HOOK_EVENT_NAME_MAP.get(hook_type, "Unknown")
|
|
262
|
-
|
|
263
|
-
if
|
|
264
|
-
|
|
265
|
-
|
|
339
|
+
|
|
340
|
+
if is_first_hook:
|
|
341
|
+
# First hook: inject full metadata (~60-100 tokens)
|
|
342
|
+
context_lines = []
|
|
343
|
+
if session_ref:
|
|
344
|
+
context_lines.append(f"Gobby Session ID: {session_ref} (or {session_id})")
|
|
345
|
+
else:
|
|
346
|
+
context_lines.append(f"Gobby Session ID: {session_id}")
|
|
347
|
+
if external_id:
|
|
348
|
+
context_lines.append(
|
|
349
|
+
f"CLI-Specific Session ID (external_id): {external_id}"
|
|
350
|
+
)
|
|
351
|
+
if response.metadata.get("parent_session_id"):
|
|
352
|
+
context_lines.append(
|
|
353
|
+
f"parent_session_id: {response.metadata['parent_session_id']}"
|
|
354
|
+
)
|
|
355
|
+
if response.metadata.get("machine_id"):
|
|
356
|
+
context_lines.append(f"machine_id: {response.metadata['machine_id']}")
|
|
357
|
+
if response.metadata.get("project_id"):
|
|
358
|
+
context_lines.append(f"project_id: {response.metadata['project_id']}")
|
|
359
|
+
# Add terminal context (non-null values only)
|
|
360
|
+
if response.metadata.get("terminal_term_program"):
|
|
361
|
+
context_lines.append(
|
|
362
|
+
f"terminal: {response.metadata['terminal_term_program']}"
|
|
363
|
+
)
|
|
364
|
+
if response.metadata.get("terminal_tty"):
|
|
365
|
+
context_lines.append(f"tty: {response.metadata['terminal_tty']}")
|
|
366
|
+
if response.metadata.get("terminal_parent_pid"):
|
|
367
|
+
context_lines.append(
|
|
368
|
+
f"parent_pid: {response.metadata['terminal_parent_pid']}"
|
|
369
|
+
)
|
|
370
|
+
# Add terminal-specific session IDs
|
|
371
|
+
for key in [
|
|
372
|
+
"terminal_iterm_session_id",
|
|
373
|
+
"terminal_term_session_id",
|
|
374
|
+
"terminal_kitty_window_id",
|
|
375
|
+
"terminal_tmux_pane",
|
|
376
|
+
"terminal_vscode_terminal_id",
|
|
377
|
+
"terminal_alacritty_socket",
|
|
378
|
+
]:
|
|
379
|
+
if response.metadata.get(key):
|
|
380
|
+
friendly_name = key.replace("terminal_", "").replace("_", " ")
|
|
381
|
+
context_lines.append(f"{friendly_name}: {response.metadata[key]}")
|
|
382
|
+
|
|
383
|
+
hook_specific["hookEventName"] = hook_event_name
|
|
384
|
+
# Append to existing additionalContext if present
|
|
385
|
+
existing = hook_specific.get("additionalContext", "")
|
|
386
|
+
new_context = "\n".join(context_lines)
|
|
387
|
+
hook_specific["additionalContext"] = (
|
|
388
|
+
f"{existing}\n{new_context}" if existing else new_context
|
|
266
389
|
)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if response.metadata.get("terminal_parent_pid"):
|
|
277
|
-
context_lines.append(f"parent_pid: {response.metadata['terminal_parent_pid']}")
|
|
278
|
-
# Add terminal-specific session IDs
|
|
279
|
-
for key in [
|
|
280
|
-
"terminal_iterm_session_id",
|
|
281
|
-
"terminal_term_session_id",
|
|
282
|
-
"terminal_kitty_window_id",
|
|
283
|
-
"terminal_tmux_pane",
|
|
284
|
-
"terminal_vscode_terminal_id",
|
|
285
|
-
"terminal_alacritty_socket",
|
|
286
|
-
]:
|
|
287
|
-
if response.metadata.get(key):
|
|
288
|
-
friendly_name = key.replace("terminal_", "").replace("_", " ")
|
|
289
|
-
context_lines.append(f"{friendly_name}: {response.metadata[key]}")
|
|
290
|
-
hook_specific["hookEventName"] = hook_event_name
|
|
291
|
-
# Append to existing additionalContext if present
|
|
292
|
-
existing = hook_specific.get("additionalContext", "")
|
|
293
|
-
new_context = "\n".join(context_lines)
|
|
294
|
-
hook_specific["additionalContext"] = (
|
|
295
|
-
f"{existing}\n{new_context}" if existing else new_context
|
|
296
|
-
)
|
|
390
|
+
else:
|
|
391
|
+
# Subsequent hooks: inject minimal session ref only (~8 tokens)
|
|
392
|
+
if session_ref:
|
|
393
|
+
hook_specific["hookEventName"] = hook_event_name
|
|
394
|
+
existing = hook_specific.get("additionalContext", "")
|
|
395
|
+
minimal_context = f"Gobby Session ID: {session_ref}"
|
|
396
|
+
hook_specific["additionalContext"] = (
|
|
397
|
+
f"{existing}\n{minimal_context}" if existing else minimal_context
|
|
398
|
+
)
|
|
297
399
|
|
|
298
400
|
# Handle BeforeModel-specific output (llm_request modification)
|
|
299
401
|
if hook_type == "BeforeModel" and response.modify_args:
|
gobby/agents/isolation.py
CHANGED
|
@@ -197,6 +197,13 @@ class WorktreeIsolationHandler(IsolationHandler):
|
|
|
197
197
|
task_id=config.task_id,
|
|
198
198
|
)
|
|
199
199
|
|
|
200
|
+
# Copy CLI hooks to worktree so hooks fire correctly
|
|
201
|
+
await self._copy_cli_hooks(
|
|
202
|
+
main_repo_path=self._git_manager.repo_path,
|
|
203
|
+
worktree_path=worktree_path,
|
|
204
|
+
provider=config.provider,
|
|
205
|
+
)
|
|
206
|
+
|
|
200
207
|
return IsolationContext(
|
|
201
208
|
cwd=worktree.worktree_path,
|
|
202
209
|
branch_name=worktree.branch_name,
|
|
@@ -235,6 +242,64 @@ Commit your changes to the worktree branch when done.
|
|
|
235
242
|
worktree_dir = tempfile.gettempdir()
|
|
236
243
|
return f"{worktree_dir}/gobby-worktrees/{project_name}/{safe_branch}"
|
|
237
244
|
|
|
245
|
+
async def _copy_cli_hooks(
|
|
246
|
+
self,
|
|
247
|
+
main_repo_path: str,
|
|
248
|
+
worktree_path: str,
|
|
249
|
+
provider: str,
|
|
250
|
+
) -> None:
|
|
251
|
+
"""
|
|
252
|
+
Copy CLI-specific hooks to the worktree.
|
|
253
|
+
|
|
254
|
+
Without these hooks, the spawned agent won't trigger SessionStart
|
|
255
|
+
and other lifecycle hooks, breaking Gobby integration.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
main_repo_path: Path to the main repository
|
|
259
|
+
worktree_path: Path to the newly created worktree
|
|
260
|
+
provider: CLI provider (gemini, claude, codex)
|
|
261
|
+
"""
|
|
262
|
+
import asyncio
|
|
263
|
+
import logging
|
|
264
|
+
import shutil
|
|
265
|
+
from pathlib import Path
|
|
266
|
+
|
|
267
|
+
logger = logging.getLogger(__name__)
|
|
268
|
+
|
|
269
|
+
# Map provider to CLI hook directory
|
|
270
|
+
cli_dirs = {
|
|
271
|
+
"gemini": ".gemini",
|
|
272
|
+
"claude": ".claude",
|
|
273
|
+
"codex": ".codex",
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
cli_dir = cli_dirs.get(provider)
|
|
277
|
+
if not cli_dir:
|
|
278
|
+
logger.debug(f"No CLI hooks directory defined for provider: {provider}")
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
src_path = Path(main_repo_path) / cli_dir
|
|
282
|
+
dst_path = Path(worktree_path) / cli_dir
|
|
283
|
+
|
|
284
|
+
if not src_path.exists():
|
|
285
|
+
logger.debug(f"CLI hooks directory not found in main repo: {src_path}")
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
# Copy entire CLI hooks directory (non-blocking)
|
|
290
|
+
await asyncio.to_thread(shutil.copytree, src_path, dst_path, dirs_exist_ok=True)
|
|
291
|
+
logger.info(f"Copied CLI hooks from {src_path} to {dst_path}")
|
|
292
|
+
except shutil.Error:
|
|
293
|
+
logger.warning(
|
|
294
|
+
f"Failed to copy CLI hooks: provider={provider}, src={src_path}, dst={dst_path}",
|
|
295
|
+
exc_info=True,
|
|
296
|
+
)
|
|
297
|
+
except OSError:
|
|
298
|
+
logger.warning(
|
|
299
|
+
f"Filesystem error copying CLI hooks: provider={provider}, src={src_path}, dst={dst_path}",
|
|
300
|
+
exc_info=True,
|
|
301
|
+
)
|
|
302
|
+
|
|
238
303
|
|
|
239
304
|
class CloneIsolationHandler(IsolationHandler):
|
|
240
305
|
"""
|
|
@@ -310,6 +375,13 @@ class CloneIsolationHandler(IsolationHandler):
|
|
|
310
375
|
task_id=config.task_id,
|
|
311
376
|
)
|
|
312
377
|
|
|
378
|
+
# Copy CLI hooks to clone so hooks fire correctly
|
|
379
|
+
await self._copy_cli_hooks(
|
|
380
|
+
source_repo_path=config.project_path,
|
|
381
|
+
clone_path=clone_path,
|
|
382
|
+
provider=config.provider,
|
|
383
|
+
)
|
|
384
|
+
|
|
313
385
|
return IsolationContext(
|
|
314
386
|
cwd=clone.clone_path,
|
|
315
387
|
branch_name=clone.branch_name,
|
|
@@ -348,6 +420,64 @@ Push your changes when ready to share with the original.
|
|
|
348
420
|
clone_dir = tempfile.gettempdir()
|
|
349
421
|
return f"{clone_dir}/gobby-clones/{project_name}/{safe_branch}"
|
|
350
422
|
|
|
423
|
+
async def _copy_cli_hooks(
|
|
424
|
+
self,
|
|
425
|
+
source_repo_path: str,
|
|
426
|
+
clone_path: str,
|
|
427
|
+
provider: str,
|
|
428
|
+
) -> None:
|
|
429
|
+
"""
|
|
430
|
+
Copy CLI-specific hooks to the clone.
|
|
431
|
+
|
|
432
|
+
Without these hooks, the spawned agent won't trigger SessionStart
|
|
433
|
+
and other lifecycle hooks, breaking Gobby integration.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
source_repo_path: Path to the source repository
|
|
437
|
+
clone_path: Path to the newly created clone
|
|
438
|
+
provider: CLI provider (gemini, claude, codex)
|
|
439
|
+
"""
|
|
440
|
+
import asyncio
|
|
441
|
+
import logging
|
|
442
|
+
import shutil
|
|
443
|
+
from pathlib import Path
|
|
444
|
+
|
|
445
|
+
logger = logging.getLogger(__name__)
|
|
446
|
+
|
|
447
|
+
# Map provider to CLI hook directory
|
|
448
|
+
cli_dirs = {
|
|
449
|
+
"gemini": ".gemini",
|
|
450
|
+
"claude": ".claude",
|
|
451
|
+
"codex": ".codex",
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
cli_dir = cli_dirs.get(provider)
|
|
455
|
+
if not cli_dir:
|
|
456
|
+
logger.debug(f"No CLI hooks directory defined for provider: {provider}")
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
src_path = Path(source_repo_path) / cli_dir
|
|
460
|
+
dst_path = Path(clone_path) / cli_dir
|
|
461
|
+
|
|
462
|
+
if not src_path.exists():
|
|
463
|
+
logger.debug(f"CLI hooks directory not found in source repo: {src_path}")
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
try:
|
|
467
|
+
# Copy entire CLI hooks directory (non-blocking)
|
|
468
|
+
await asyncio.to_thread(shutil.copytree, src_path, dst_path, dirs_exist_ok=True)
|
|
469
|
+
logger.info(f"Copied CLI hooks from {src_path} to {dst_path}")
|
|
470
|
+
except shutil.Error:
|
|
471
|
+
logger.warning(
|
|
472
|
+
f"Failed to copy CLI hooks: provider={provider}, src={src_path}, dst={dst_path}",
|
|
473
|
+
exc_info=True,
|
|
474
|
+
)
|
|
475
|
+
except OSError:
|
|
476
|
+
logger.warning(
|
|
477
|
+
f"Filesystem error copying CLI hooks: provider={provider}, src={src_path}, dst={dst_path}",
|
|
478
|
+
exc_info=True,
|
|
479
|
+
)
|
|
480
|
+
|
|
351
481
|
|
|
352
482
|
def get_isolation_handler(
|
|
353
483
|
mode: Literal["current", "worktree", "clone"],
|
gobby/agents/registry.py
CHANGED
|
@@ -137,6 +137,17 @@ class RunningAgentRegistry:
|
|
|
137
137
|
with self._event_callbacks_lock:
|
|
138
138
|
self._event_callbacks.append(callback)
|
|
139
139
|
|
|
140
|
+
def emit_event(self, event_type: str, run_id: str, data: dict[str, Any]) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Emit a custom event to all registered callbacks.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
event_type: Type of event (e.g., terminal_output)
|
|
146
|
+
run_id: Agent run ID
|
|
147
|
+
data: Additional event data
|
|
148
|
+
"""
|
|
149
|
+
self._emit_event(event_type, run_id, data)
|
|
150
|
+
|
|
140
151
|
def _emit_event(self, event_type: str, run_id: str, data: dict[str, Any]) -> None:
|
|
141
152
|
"""
|
|
142
153
|
Emit an event to all registered callbacks.
|