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.
Files changed (63) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/claude_code.py +3 -26
  3. gobby/app_context.py +59 -0
  4. gobby/cli/utils.py +5 -17
  5. gobby/config/features.py +0 -20
  6. gobby/config/tasks.py +4 -0
  7. gobby/hooks/event_handlers/__init__.py +155 -0
  8. gobby/hooks/event_handlers/_agent.py +175 -0
  9. gobby/hooks/event_handlers/_base.py +87 -0
  10. gobby/hooks/event_handlers/_misc.py +66 -0
  11. gobby/hooks/event_handlers/_session.py +573 -0
  12. gobby/hooks/event_handlers/_tool.py +196 -0
  13. gobby/hooks/hook_manager.py +2 -0
  14. gobby/llm/claude.py +377 -42
  15. gobby/mcp_proxy/importer.py +4 -41
  16. gobby/mcp_proxy/manager.py +13 -3
  17. gobby/mcp_proxy/registries.py +14 -0
  18. gobby/mcp_proxy/services/recommendation.py +2 -28
  19. gobby/mcp_proxy/tools/artifacts.py +3 -3
  20. gobby/mcp_proxy/tools/task_readiness.py +27 -4
  21. gobby/mcp_proxy/tools/workflows/__init__.py +266 -0
  22. gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
  23. gobby/mcp_proxy/tools/workflows/_import.py +112 -0
  24. gobby/mcp_proxy/tools/workflows/_lifecycle.py +321 -0
  25. gobby/mcp_proxy/tools/workflows/_query.py +207 -0
  26. gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
  27. gobby/mcp_proxy/tools/workflows/_terminal.py +139 -0
  28. gobby/memory/components/__init__.py +0 -0
  29. gobby/memory/components/ingestion.py +98 -0
  30. gobby/memory/components/search.py +108 -0
  31. gobby/memory/manager.py +16 -25
  32. gobby/paths.py +51 -0
  33. gobby/prompts/loader.py +1 -35
  34. gobby/runner.py +23 -10
  35. gobby/servers/http.py +186 -149
  36. gobby/servers/routes/admin.py +12 -0
  37. gobby/servers/routes/mcp/endpoints/execution.py +15 -7
  38. gobby/servers/routes/mcp/endpoints/registry.py +8 -8
  39. gobby/sessions/analyzer.py +2 -2
  40. gobby/skills/parser.py +23 -0
  41. gobby/skills/sync.py +5 -4
  42. gobby/storage/artifacts.py +19 -0
  43. gobby/storage/migrations.py +25 -2
  44. gobby/storage/skills.py +47 -7
  45. gobby/tasks/external_validator.py +4 -17
  46. gobby/tasks/validation.py +13 -87
  47. gobby/tools/summarizer.py +18 -51
  48. gobby/utils/status.py +13 -0
  49. gobby/workflows/actions.py +5 -0
  50. gobby/workflows/context_actions.py +21 -24
  51. gobby/workflows/enforcement/__init__.py +11 -1
  52. gobby/workflows/enforcement/blocking.py +96 -0
  53. gobby/workflows/enforcement/handlers.py +35 -1
  54. gobby/workflows/engine.py +6 -3
  55. gobby/workflows/lifecycle_evaluator.py +2 -1
  56. {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/METADATA +1 -1
  57. {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/RECORD +61 -45
  58. gobby/hooks/event_handlers.py +0 -1008
  59. gobby/mcp_proxy/tools/workflows.py +0 -1023
  60. {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/WHEEL +0 -0
  61. {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/entry_points.txt +0 -0
  62. {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/licenses/LICENSE.md +0 -0
  63. {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}")
@@ -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,