gobby 0.2.8__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 +3 -26
- gobby/app_context.py +59 -0
- gobby/cli/utils.py +5 -17
- gobby/config/features.py +0 -20
- 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 +2 -0
- gobby/llm/claude.py +377 -42
- gobby/mcp_proxy/importer.py +4 -41
- gobby/mcp_proxy/manager.py +13 -3
- gobby/mcp_proxy/registries.py +14 -0
- gobby/mcp_proxy/services/recommendation.py +2 -28
- gobby/mcp_proxy/tools/artifacts.py +3 -3
- gobby/mcp_proxy/tools/task_readiness.py +27 -4
- 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/memory/components/__init__.py +0 -0
- gobby/memory/components/ingestion.py +98 -0
- gobby/memory/components/search.py +108 -0
- gobby/memory/manager.py +16 -25
- gobby/paths.py +51 -0
- gobby/prompts/loader.py +1 -35
- gobby/runner.py +23 -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/sessions/analyzer.py +2 -2
- gobby/skills/parser.py +23 -0
- gobby/skills/sync.py +5 -4
- gobby/storage/artifacts.py +19 -0
- gobby/storage/migrations.py +25 -2
- gobby/storage/skills.py +47 -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/enforcement/__init__.py +11 -1
- gobby/workflows/enforcement/blocking.py +96 -0
- gobby/workflows/enforcement/handlers.py +35 -1
- gobby/workflows/engine.py +6 -3
- gobby/workflows/lifecycle_evaluator.py +2 -1
- {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/METADATA +1 -1
- {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/RECORD +61 -45
- gobby/hooks/event_handlers.py +0 -1008
- gobby/mcp_proxy/tools/workflows.py +0 -1023
- {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/WHEEL +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from gobby.hooks.event_handlers._base import EventHandlersBase
|
|
6
|
+
from gobby.hooks.events import HookEvent, HookResponse
|
|
7
|
+
|
|
8
|
+
EDIT_TOOLS = {
|
|
9
|
+
"write_file",
|
|
10
|
+
"replace",
|
|
11
|
+
"edit_file",
|
|
12
|
+
"notebook_edit",
|
|
13
|
+
"edit",
|
|
14
|
+
"write",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ToolEventHandlerMixin(EventHandlersBase):
|
|
19
|
+
"""Mixin for handling tool-related events."""
|
|
20
|
+
|
|
21
|
+
def handle_before_tool(self, event: HookEvent) -> HookResponse:
|
|
22
|
+
"""Handle BEFORE_TOOL event."""
|
|
23
|
+
input_data = event.data
|
|
24
|
+
tool_name = input_data.get("tool_name", "unknown")
|
|
25
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
26
|
+
|
|
27
|
+
if session_id:
|
|
28
|
+
self.logger.debug(f"BEFORE_TOOL: {tool_name}, session {session_id}")
|
|
29
|
+
else:
|
|
30
|
+
self.logger.debug(f"BEFORE_TOOL: {tool_name}")
|
|
31
|
+
|
|
32
|
+
context_parts = []
|
|
33
|
+
|
|
34
|
+
# Execute lifecycle workflow triggers
|
|
35
|
+
if self._workflow_handler:
|
|
36
|
+
try:
|
|
37
|
+
wf_response = self._workflow_handler.handle_all_lifecycles(event)
|
|
38
|
+
if wf_response.context:
|
|
39
|
+
context_parts.append(wf_response.context)
|
|
40
|
+
if wf_response.decision != "allow":
|
|
41
|
+
return wf_response
|
|
42
|
+
except Exception as e:
|
|
43
|
+
self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
|
|
44
|
+
|
|
45
|
+
return HookResponse(
|
|
46
|
+
decision="allow",
|
|
47
|
+
context="\n\n".join(context_parts) if context_parts else None,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def handle_after_tool(self, event: HookEvent) -> HookResponse:
|
|
51
|
+
"""Handle AFTER_TOOL event."""
|
|
52
|
+
input_data = event.data
|
|
53
|
+
tool_name = input_data.get("tool_name", "unknown")
|
|
54
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
55
|
+
is_failure = event.metadata.get("is_failure", False)
|
|
56
|
+
|
|
57
|
+
status = "FAIL" if is_failure else "OK"
|
|
58
|
+
if session_id:
|
|
59
|
+
self.logger.debug(f"AFTER_TOOL [{status}]: {tool_name}, session {session_id}")
|
|
60
|
+
|
|
61
|
+
# Track edits for session high-water mark
|
|
62
|
+
# Only if tool succeeded, matches edit tools, and session has claimed a task
|
|
63
|
+
# Skip .gobby/ internal files (tasks.jsonl, memories.jsonl, etc.)
|
|
64
|
+
tool_input = input_data.get("tool_input", {})
|
|
65
|
+
|
|
66
|
+
# Capture artifacts from edit tools
|
|
67
|
+
if not is_failure and self._artifact_capture_hook:
|
|
68
|
+
self._capture_tool_artifact(session_id, tool_name, tool_input)
|
|
69
|
+
|
|
70
|
+
# Simple check for edit tools (case-insensitive)
|
|
71
|
+
is_edit = tool_name.lower() in EDIT_TOOLS
|
|
72
|
+
|
|
73
|
+
# For complex tools (multi_replace, etc), check if they modify files
|
|
74
|
+
# This logic could be expanded, but for now stick to the basic set
|
|
75
|
+
|
|
76
|
+
if not is_failure and is_edit and self._session_storage:
|
|
77
|
+
try:
|
|
78
|
+
# Check if file is internal .gobby file
|
|
79
|
+
file_path = (
|
|
80
|
+
tool_input.get("file_path")
|
|
81
|
+
or tool_input.get("target_file")
|
|
82
|
+
or tool_input.get("path")
|
|
83
|
+
)
|
|
84
|
+
is_internal = file_path and ".gobby/" in str(file_path)
|
|
85
|
+
|
|
86
|
+
if not is_internal:
|
|
87
|
+
# Check if session has any claimed tasks before marking had_edits
|
|
88
|
+
has_claimed_task = False
|
|
89
|
+
if self._task_manager:
|
|
90
|
+
try:
|
|
91
|
+
claimed_tasks = self._task_manager.list_tasks(assignee=session_id)
|
|
92
|
+
has_claimed_task = len(claimed_tasks) > 0
|
|
93
|
+
except Exception as e:
|
|
94
|
+
self.logger.debug(
|
|
95
|
+
f"Failed to check claimed tasks for session {session_id}: {e}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if has_claimed_task:
|
|
99
|
+
self._session_storage.mark_had_edits(session_id)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
# Don't fail the event if tracking fails
|
|
102
|
+
self.logger.warning(f"Failed to process file edit: {e}")
|
|
103
|
+
|
|
104
|
+
else:
|
|
105
|
+
self.logger.debug(f"AFTER_TOOL [{status}]: {tool_name}")
|
|
106
|
+
|
|
107
|
+
# Execute lifecycle workflow triggers
|
|
108
|
+
if self._workflow_handler:
|
|
109
|
+
try:
|
|
110
|
+
wf_response = self._workflow_handler.handle_all_lifecycles(event)
|
|
111
|
+
if wf_response.decision != "allow":
|
|
112
|
+
return wf_response
|
|
113
|
+
if wf_response.context:
|
|
114
|
+
return wf_response
|
|
115
|
+
except Exception as e:
|
|
116
|
+
self.logger.error(f"Failed to execute lifecycle workflows: {e}", exc_info=True)
|
|
117
|
+
|
|
118
|
+
return HookResponse(decision="allow")
|
|
119
|
+
|
|
120
|
+
def handle_before_tool_selection(self, event: HookEvent) -> HookResponse:
|
|
121
|
+
"""Handle BEFORE_TOOL_SELECTION event (Gemini only)."""
|
|
122
|
+
session_id = event.metadata.get("_platform_session_id")
|
|
123
|
+
|
|
124
|
+
if session_id:
|
|
125
|
+
self.logger.debug(f"BEFORE_TOOL_SELECTION: session {session_id}")
|
|
126
|
+
else:
|
|
127
|
+
self.logger.debug("BEFORE_TOOL_SELECTION")
|
|
128
|
+
|
|
129
|
+
return HookResponse(decision="allow")
|
|
130
|
+
|
|
131
|
+
def _capture_tool_artifact(
|
|
132
|
+
self, session_id: str, tool_name: str, tool_input: dict[str, Any]
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Capture artifacts from tool inputs for edit/write tools.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
session_id: Platform session ID
|
|
138
|
+
tool_name: Name of the tool
|
|
139
|
+
tool_input: Tool input dictionary
|
|
140
|
+
"""
|
|
141
|
+
if not self._artifact_capture_hook:
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
# Get content and file path from tool input
|
|
145
|
+
content = tool_input.get("content") or tool_input.get("new_string")
|
|
146
|
+
file_path = (
|
|
147
|
+
tool_input.get("file_path") or tool_input.get("target_file") or tool_input.get("path")
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if not content:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
# Skip internal .gobby files
|
|
154
|
+
if file_path and ".gobby/" in str(file_path):
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
# Detect language from file extension
|
|
158
|
+
language = ""
|
|
159
|
+
if file_path:
|
|
160
|
+
ext_map = {
|
|
161
|
+
".py": "python",
|
|
162
|
+
".js": "javascript",
|
|
163
|
+
".ts": "typescript",
|
|
164
|
+
".tsx": "tsx",
|
|
165
|
+
".jsx": "jsx",
|
|
166
|
+
".rs": "rust",
|
|
167
|
+
".go": "go",
|
|
168
|
+
".java": "java",
|
|
169
|
+
".rb": "ruby",
|
|
170
|
+
".sh": "bash",
|
|
171
|
+
".yaml": "yaml",
|
|
172
|
+
".yml": "yaml",
|
|
173
|
+
".json": "json",
|
|
174
|
+
".md": "markdown",
|
|
175
|
+
".sql": "sql",
|
|
176
|
+
".html": "html",
|
|
177
|
+
".css": "css",
|
|
178
|
+
}
|
|
179
|
+
for ext, lang in ext_map.items():
|
|
180
|
+
if str(file_path).endswith(ext):
|
|
181
|
+
language = lang
|
|
182
|
+
break
|
|
183
|
+
|
|
184
|
+
# Wrap content as markdown code block for process_message
|
|
185
|
+
# This reuses the deduplication and classification logic
|
|
186
|
+
markdown_content = f"```{language}\n{content}\n```"
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
self._artifact_capture_hook.process_message(
|
|
190
|
+
session_id=session_id,
|
|
191
|
+
role="assistant",
|
|
192
|
+
content=markdown_content,
|
|
193
|
+
)
|
|
194
|
+
self.logger.debug(f"Captured artifact from {tool_name}: {file_path or 'unknown'}")
|
|
195
|
+
except Exception as e:
|
|
196
|
+
self.logger.warning(f"Failed to capture artifact from {tool_name}: {e}")
|
gobby/hooks/hook_manager.py
CHANGED
|
@@ -386,6 +386,8 @@ class HookManager:
|
|
|
386
386
|
message_manager=self._message_manager,
|
|
387
387
|
skill_manager=self._skill_manager,
|
|
388
388
|
skills_config=self._config.skills if self._config else None,
|
|
389
|
+
artifact_capture_hook=self._artifact_capture_hook,
|
|
390
|
+
workflow_config=self._config.workflow if self._config else None,
|
|
389
391
|
get_machine_id=self.get_machine_id,
|
|
390
392
|
resolve_project_id=self._resolve_project_id,
|
|
391
393
|
logger=self.logger,
|