gobby 0.2.6__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/__init__.py +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +96 -35
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/adapters/gemini.py +140 -38
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +525 -0
- gobby/agents/registry.py +11 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/session.py +1 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +415 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/macos.py +26 -1
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +0 -2
- gobby/cli/install.py +4 -4
- gobby/cli/installers/claude.py +6 -0
- gobby/cli/installers/gemini.py +6 -0
- gobby/cli/installers/shared.py +103 -4
- gobby/cli/memory.py +185 -0
- gobby/cli/sessions.py +1 -1
- gobby/cli/utils.py +9 -2
- gobby/clones/git.py +177 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +10 -94
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/skills.py +31 -0
- gobby/config/tasks.py +4 -28
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +150 -8
- gobby/hooks/hook_manager.py +21 -3
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/webhooks.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
- gobby/llm/resolver.py +3 -2
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +4 -2
- gobby/mcp_proxy/registries.py +22 -8
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/tools/agent_messaging.py +93 -44
- gobby/mcp_proxy/tools/agents.py +76 -740
- gobby/mcp_proxy/tools/artifacts.py +43 -9
- gobby/mcp_proxy/tools/clones.py +0 -385
- gobby/mcp_proxy/tools/memory.py +2 -2
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +239 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +503 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +166 -0
- gobby/mcp_proxy/tools/skills/__init__.py +14 -29
- gobby/mcp_proxy/tools/spawn_agent.py +455 -0
- 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 +79 -30
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
- gobby/mcp_proxy/tools/tasks/_session.py +22 -7
- gobby/mcp_proxy/tools/workflows.py +84 -34
- gobby/mcp_proxy/tools/worktrees.py +32 -350
- gobby/memory/extractor.py +15 -1
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +62 -283
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/runner.py +13 -0
- gobby/servers/http.py +1 -4
- gobby/servers/routes/admin.py +14 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +51 -4
- gobby/servers/routes/mcp/tools.py +48 -1506
- gobby/servers/websocket.py +57 -1
- gobby/sessions/analyzer.py +2 -2
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/manager.py +9 -0
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +15 -5
- gobby/sessions/transcripts/gemini.py +100 -34
- gobby/skills/parser.py +30 -2
- gobby/storage/database.py +9 -2
- gobby/storage/memories.py +32 -21
- gobby/storage/migrations.py +174 -368
- gobby/storage/sessions.py +45 -7
- gobby/storage/skills.py +80 -7
- gobby/storage/tasks/_lifecycle.py +18 -3
- gobby/sync/memories.py +1 -1
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +22 -20
- gobby/tools/summarizer.py +91 -10
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1217
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +50 -1
- gobby/workflows/detection_helpers.py +38 -24
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +281 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/enforcement/task_policy.py +542 -0
- gobby/workflows/engine.py +93 -0
- gobby/workflows/evaluator.py +110 -0
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/hooks.py +41 -0
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +91 -0
- gobby/workflows/safe_evaluator.py +191 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +217 -51
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/METADATA +6 -1
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/RECORD +139 -163
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1332
- gobby/cli/tui.py +0 -34
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/mcp_proxy/tools/session_messages.py +0 -1055
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/handoff/compact.md +0 -63
- gobby/prompts/defaults/handoff/session_end.md +0 -57
- gobby/prompts/defaults/memory/extract.md +0 -61
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- 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/workflows/task_enforcement_actions.py +0 -1343
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
gobby/config/extensions.py
CHANGED
|
@@ -137,8 +137,8 @@ class PluginsConfig(BaseModel):
|
|
|
137
137
|
description="Enable plugin system (disabled by default for security)",
|
|
138
138
|
)
|
|
139
139
|
plugin_dirs: list[str] = Field(
|
|
140
|
-
default_factory=lambda: ["
|
|
141
|
-
description="Directories to scan for plugins (
|
|
140
|
+
default_factory=lambda: [".gobby/plugins"],
|
|
141
|
+
description="Directories to scan for plugins (project-scoped)",
|
|
142
142
|
)
|
|
143
143
|
auto_discover: bool = Field(
|
|
144
144
|
default=True,
|
gobby/config/features.py
CHANGED
|
@@ -42,45 +42,22 @@ class ToolSummarizerConfig(BaseModel):
|
|
|
42
42
|
default="claude-haiku-4-5",
|
|
43
43
|
description="Model to use for summarization (fast/cheap recommended)",
|
|
44
44
|
)
|
|
45
|
-
prompt: str = Field(
|
|
46
|
-
default="""Summarize this MCP tool description in 180 characters or less.
|
|
47
|
-
Keep it to three sentences or less. Be concise and preserve the key functionality.
|
|
48
|
-
Do not add quotes, extra formatting, or code examples.
|
|
49
45
|
|
|
50
|
-
Description: {description}
|
|
51
|
-
|
|
52
|
-
Summary:""",
|
|
53
|
-
description="DEPRECATED: Use prompt_path instead. Prompt template for tool description summarization",
|
|
54
|
-
)
|
|
55
46
|
prompt_path: str | None = Field(
|
|
56
47
|
default=None,
|
|
57
48
|
description="Path to custom tool summary prompt template (e.g., 'features/tool_summary')",
|
|
58
49
|
)
|
|
59
|
-
|
|
60
|
-
default="You are a technical summarizer. Create concise tool descriptions.",
|
|
61
|
-
description="DEPRECATED: Use system_prompt_path instead. System prompt for tool description summarization",
|
|
62
|
-
)
|
|
50
|
+
|
|
63
51
|
system_prompt_path: str | None = Field(
|
|
64
52
|
default=None,
|
|
65
53
|
description="Path to custom tool summary system prompt (e.g., 'features/tool_summary_system')",
|
|
66
54
|
)
|
|
67
|
-
server_description_prompt: str = Field(
|
|
68
|
-
default="""Write a single concise sentence describing what the '{server_name}' MCP server does based on its tools.
|
|
69
55
|
|
|
70
|
-
Tools:
|
|
71
|
-
{tools_list}
|
|
72
|
-
|
|
73
|
-
Description (1 sentence, try to keep under 100 characters):""",
|
|
74
|
-
description="DEPRECATED: Use server_description_prompt_path instead. Prompt template for server description generation",
|
|
75
|
-
)
|
|
76
56
|
server_description_prompt_path: str | None = Field(
|
|
77
57
|
default=None,
|
|
78
58
|
description="Path to custom server description prompt (e.g., 'features/server_description')",
|
|
79
59
|
)
|
|
80
|
-
|
|
81
|
-
default="You write concise technical descriptions.",
|
|
82
|
-
description="DEPRECATED: Use server_description_system_prompt_path instead. System prompt for server description generation",
|
|
83
|
-
)
|
|
60
|
+
|
|
84
61
|
server_description_system_prompt_path: str | None = Field(
|
|
85
62
|
default=None,
|
|
86
63
|
description="Path to custom server description system prompt (e.g., 'features/server_description_system')",
|
|
@@ -110,26 +87,12 @@ class TaskDescriptionConfig(BaseModel):
|
|
|
110
87
|
default=50,
|
|
111
88
|
description="Minimum length of structured extraction before LLM fallback triggers",
|
|
112
89
|
)
|
|
113
|
-
prompt: str = Field(
|
|
114
|
-
default="""Generate a concise task description for this task from a spec document.
|
|
115
|
-
|
|
116
|
-
Task title: {task_title}
|
|
117
|
-
Section: {section_title}
|
|
118
|
-
Section content: {section_content}
|
|
119
|
-
Existing context: {existing_context}
|
|
120
90
|
|
|
121
|
-
Write a 1-2 sentence description focusing on the goal and deliverable.
|
|
122
|
-
Do not add quotes, extra formatting, or implementation details.""",
|
|
123
|
-
description="DEPRECATED: Use prompt_path instead. Prompt template for task description generation",
|
|
124
|
-
)
|
|
125
91
|
prompt_path: str | None = Field(
|
|
126
92
|
default=None,
|
|
127
93
|
description="Path to custom task description prompt (e.g., 'features/task_description')",
|
|
128
94
|
)
|
|
129
|
-
|
|
130
|
-
default="You are a technical writer creating concise task descriptions for developers.",
|
|
131
|
-
description="DEPRECATED: Use system_prompt_path instead. System prompt for task description generation",
|
|
132
|
-
)
|
|
95
|
+
|
|
133
96
|
system_prompt_path: str | None = Field(
|
|
134
97
|
default=None,
|
|
135
98
|
description="Path to custom task description system prompt (e.g., 'features/task_description_system')",
|
|
@@ -159,83 +122,17 @@ class RecommendToolsConfig(BaseModel):
|
|
|
159
122
|
default="claude-sonnet-4-5",
|
|
160
123
|
description="Model to use for tool recommendations",
|
|
161
124
|
)
|
|
162
|
-
|
|
163
|
-
default="""You are a tool recommendation assistant for Claude Code with access to MCP servers.
|
|
164
|
-
|
|
165
|
-
CRITICAL PRIORITIZATION RULES:
|
|
166
|
-
1. Analyze the task type (code navigation, docs lookup, database query, planning, data processing, etc.)
|
|
167
|
-
2. Check available MCP server DESCRIPTIONS for capability matches
|
|
168
|
-
3. If ANY MCP server's description matches the task type -> recommend those tools FIRST
|
|
169
|
-
4. Only recommend built-in Claude Code tools (Grep, Read, Bash, WebSearch) if NO suitable MCP server exists
|
|
170
|
-
|
|
171
|
-
TASK TYPE MATCHING GUIDELINES:
|
|
172
|
-
- Task needs library/framework documentation -> Look for MCP servers describing "documentation", "library docs", "API reference"
|
|
173
|
-
- Task needs code navigation/architecture understanding -> Look for MCP servers describing "code analysis", "symbols", "semantic search"
|
|
174
|
-
- Task needs database operations -> Look for MCP servers describing "database", "PostgreSQL", "SQL"
|
|
175
|
-
- Task needs complex reasoning/planning -> Look for MCP servers describing "problem-solving", "thinking", "reasoning"
|
|
176
|
-
- Task needs data processing/large datasets -> Look for MCP servers describing "code execution", "data processing", "token optimization"
|
|
177
|
-
|
|
178
|
-
ANTI-PATTERNS (What NOT to recommend):
|
|
179
|
-
- Don't recommend WebSearch when an MCP server provides library/framework documentation
|
|
180
|
-
- Don't recommend Grep/Read for code architecture questions when an MCP server does semantic code analysis
|
|
181
|
-
- Don't recommend Bash for database queries when an MCP server provides database tools
|
|
182
|
-
- Don't recommend direct implementation when an MCP server provides structured reasoning
|
|
183
|
-
|
|
184
|
-
OUTPUT FORMAT:
|
|
185
|
-
Be concise and specific. Recommend 1-3 tools maximum with:
|
|
186
|
-
1. Which MCP server and tools to use (if applicable)
|
|
187
|
-
2. Brief rationale based on server description matching task type
|
|
188
|
-
3. Suggested workflow (e.g., "First call X, then use result with Y")
|
|
189
|
-
4. Only mention built-in tools if no MCP server is suitable""",
|
|
190
|
-
description="DEPRECATED: Use prompt_path instead. System prompt for recommend_tools() MCP tool.",
|
|
191
|
-
)
|
|
125
|
+
|
|
192
126
|
prompt_path: str | None = Field(
|
|
193
127
|
default=None,
|
|
194
128
|
description="Path to custom recommend tools system prompt (e.g., 'features/recommend_tools')",
|
|
195
129
|
)
|
|
196
|
-
|
|
197
|
-
default="""You are an expert at selecting tools for tasks.
|
|
198
|
-
Task: {task_description}
|
|
199
|
-
|
|
200
|
-
Candidate tools (ranked by semantic similarity):
|
|
201
|
-
{candidate_list}
|
|
202
|
-
|
|
203
|
-
Re-rank these tools by relevance to the task and provide reasoning.
|
|
204
|
-
Return the top {top_k} most relevant as JSON:
|
|
205
|
-
{{
|
|
206
|
-
"recommendations": [
|
|
207
|
-
{{
|
|
208
|
-
"server": "server_name",
|
|
209
|
-
"tool": "tool_name",
|
|
210
|
-
"reason": "Why this tool is the best choice"
|
|
211
|
-
}}
|
|
212
|
-
]
|
|
213
|
-
}}""",
|
|
214
|
-
description="DEPRECATED: Use hybrid_rerank_prompt_path instead. Prompt template for hybrid mode re-ranking",
|
|
215
|
-
)
|
|
130
|
+
|
|
216
131
|
hybrid_rerank_prompt_path: str | None = Field(
|
|
217
132
|
default=None,
|
|
218
133
|
description="Path to custom hybrid re-rank prompt (e.g., 'features/recommend_tools_hybrid')",
|
|
219
134
|
)
|
|
220
|
-
|
|
221
|
-
default="""You are an expert at selecting the right tools for a given task.
|
|
222
|
-
Task: {task_description}
|
|
223
|
-
|
|
224
|
-
Available Servers: {available_servers}
|
|
225
|
-
|
|
226
|
-
Please recommend which tools from these servers would be most useful for this task.
|
|
227
|
-
Return a JSON object with this structure:
|
|
228
|
-
{{
|
|
229
|
-
"recommendations": [
|
|
230
|
-
{{
|
|
231
|
-
"server": "server_name",
|
|
232
|
-
"tool": "tool_name",
|
|
233
|
-
"reason": "Why this tool is useful"
|
|
234
|
-
}}
|
|
235
|
-
]
|
|
236
|
-
}}""",
|
|
237
|
-
description="DEPRECATED: Use llm_prompt_path instead. Prompt template for LLM mode recommendations",
|
|
238
|
-
)
|
|
135
|
+
|
|
239
136
|
llm_prompt_path: str | None = Field(
|
|
240
137
|
default=None,
|
|
241
138
|
description="Path to custom LLM recommendation prompt (e.g., 'features/recommend_tools_llm')",
|
|
@@ -276,37 +173,17 @@ class ImportMCPServerConfig(BaseModel):
|
|
|
276
173
|
default="claude-haiku-4-5",
|
|
277
174
|
description="Model to use for config extraction",
|
|
278
175
|
)
|
|
279
|
-
|
|
280
|
-
default=DEFAULT_IMPORT_MCP_SERVER_PROMPT,
|
|
281
|
-
description="DEPRECATED: Use prompt_path instead. System prompt for MCP server config extraction",
|
|
282
|
-
)
|
|
176
|
+
|
|
283
177
|
prompt_path: str | None = Field(
|
|
284
178
|
default=None,
|
|
285
179
|
description="Path to custom import MCP system prompt (e.g., 'features/import_mcp')",
|
|
286
180
|
)
|
|
287
|
-
github_fetch_prompt: str = Field(
|
|
288
|
-
default="""Fetch the README from this GitHub repository and extract MCP server configuration:
|
|
289
|
-
|
|
290
|
-
{github_url}
|
|
291
|
-
|
|
292
|
-
If the URL doesn't point directly to a README, try to find and fetch the README.md file.
|
|
293
181
|
|
|
294
|
-
After reading the documentation, extract the MCP server configuration as a JSON object.""",
|
|
295
|
-
description="DEPRECATED: Use github_fetch_prompt_path instead. User prompt template for GitHub import",
|
|
296
|
-
)
|
|
297
182
|
github_fetch_prompt_path: str | None = Field(
|
|
298
183
|
default=None,
|
|
299
184
|
description="Path to custom GitHub fetch prompt (e.g., 'features/import_mcp_github')",
|
|
300
185
|
)
|
|
301
|
-
search_fetch_prompt: str = Field(
|
|
302
|
-
default="""Search for MCP server: {search_query}
|
|
303
|
-
|
|
304
|
-
Find the official documentation or GitHub repository for this MCP server.
|
|
305
|
-
Then fetch and read the README or installation docs.
|
|
306
186
|
|
|
307
|
-
After reading the documentation, extract the MCP server configuration as a JSON object.""",
|
|
308
|
-
description="DEPRECATED: Use search_fetch_prompt_path instead. User prompt template for search-based import",
|
|
309
|
-
)
|
|
310
187
|
search_fetch_prompt_path: str | None = Field(
|
|
311
188
|
default=None,
|
|
312
189
|
description="Path to custom search fetch prompt (e.g., 'features/import_mcp_search')",
|
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
|
@@ -38,14 +38,6 @@ class CompactHandoffConfig(BaseModel):
|
|
|
38
38
|
default=True,
|
|
39
39
|
description="Enable compact handoff context extraction and injection",
|
|
40
40
|
)
|
|
41
|
-
# DEPRECATED: prompt field is no longer used.
|
|
42
|
-
# Template is now defined in session-handoff.yaml workflow file.
|
|
43
|
-
# Kept for backwards compatibility but will be removed in a future version.
|
|
44
|
-
prompt: str | None = Field(
|
|
45
|
-
default=None,
|
|
46
|
-
description="DEPRECATED: Template moved to session-handoff.yaml workflow. "
|
|
47
|
-
"This field is ignored.",
|
|
48
|
-
)
|
|
49
41
|
|
|
50
42
|
|
|
51
43
|
class PatternCriteriaConfig(BaseModel):
|
|
@@ -147,10 +139,7 @@ class TaskExpansionConfig(BaseModel):
|
|
|
147
139
|
default="claude-opus-4-5",
|
|
148
140
|
description="Model to use for expansion",
|
|
149
141
|
)
|
|
150
|
-
|
|
151
|
-
default=None,
|
|
152
|
-
description="DEPRECATED: Use prompt_path instead. Custom prompt template for task expansion",
|
|
153
|
-
)
|
|
142
|
+
|
|
154
143
|
prompt_path: str | None = Field(
|
|
155
144
|
default=None,
|
|
156
145
|
description="Path to custom user prompt template (e.g., 'expansion/user')",
|
|
@@ -175,14 +164,7 @@ class TaskExpansionConfig(BaseModel):
|
|
|
175
164
|
default="You are a senior developer researching a codebase. Use tools to find relevant code.",
|
|
176
165
|
description="System prompt for the research agent",
|
|
177
166
|
)
|
|
178
|
-
|
|
179
|
-
default=None,
|
|
180
|
-
description="DEPRECATED: Use system_prompt_path instead. Custom system prompt for task expansion",
|
|
181
|
-
)
|
|
182
|
-
tdd_prompt: str | None = Field(
|
|
183
|
-
default=None,
|
|
184
|
-
description="DEPRECATED: TDD instructions are now embedded in task descriptions for code/config categories",
|
|
185
|
-
)
|
|
167
|
+
|
|
186
168
|
web_research_enabled: bool = Field(
|
|
187
169
|
default=True,
|
|
188
170
|
description="Enable web research for task expansion using MCP tools",
|
|
@@ -228,10 +210,7 @@ class TaskValidationConfig(BaseModel):
|
|
|
228
210
|
default="You are a QA validator. Output ONLY valid JSON. No markdown, no explanation, no code blocks. Just the raw JSON object.",
|
|
229
211
|
description="System prompt for task validation",
|
|
230
212
|
)
|
|
231
|
-
|
|
232
|
-
default=None,
|
|
233
|
-
description="DEPRECATED: Use prompt_path instead. Custom prompt template for task validation",
|
|
234
|
-
)
|
|
213
|
+
|
|
235
214
|
prompt_path: str | None = Field(
|
|
236
215
|
default=None,
|
|
237
216
|
description="Path to custom validation prompt template (e.g., 'validation/validate')",
|
|
@@ -260,10 +239,7 @@ class TaskValidationConfig(BaseModel):
|
|
|
260
239
|
default="You are a QA engineer writing acceptance criteria. CRITICAL: Only include requirements explicitly stated in the task. Do NOT invent specific values, thresholds, timeouts, or edge cases that aren't mentioned. Vague tasks get vague criteria. Use markdown checkboxes.",
|
|
261
240
|
description="System prompt for generating validation criteria",
|
|
262
241
|
)
|
|
263
|
-
|
|
264
|
-
default=None,
|
|
265
|
-
description="DEPRECATED: Use criteria_prompt_path instead. Custom prompt template for generating validation criteria",
|
|
266
|
-
)
|
|
242
|
+
|
|
267
243
|
# Validation loop control
|
|
268
244
|
max_iterations: int = Field(
|
|
269
245
|
default=10,
|
gobby/hooks/__init__.py
CHANGED
|
@@ -65,14 +65,6 @@ from gobby.hooks.plugins import (
|
|
|
65
65
|
from gobby.hooks.session_coordinator import SessionCoordinator
|
|
66
66
|
from gobby.hooks.webhooks import WebhookDispatcher
|
|
67
67
|
|
|
68
|
-
# Legacy imports for backward compatibility
|
|
69
|
-
from gobby.sessions.manager import SessionManager
|
|
70
|
-
from gobby.sessions.summary import SummaryFileGenerator
|
|
71
|
-
from gobby.sessions.transcripts.claude import ClaudeTranscriptParser
|
|
72
|
-
|
|
73
|
-
# Backward-compatible alias
|
|
74
|
-
TranscriptProcessor = ClaudeTranscriptParser
|
|
75
|
-
|
|
76
68
|
__all__ = [
|
|
77
69
|
# Core coordinator
|
|
78
70
|
"HookManager",
|
|
@@ -96,9 +88,4 @@ __all__ = [
|
|
|
96
88
|
"RegisteredHandler",
|
|
97
89
|
"hook_handler",
|
|
98
90
|
"run_plugin_handlers",
|
|
99
|
-
# Legacy exports (backward compatibility)
|
|
100
|
-
"SessionManager",
|
|
101
|
-
"SummaryFileGenerator",
|
|
102
|
-
"TranscriptProcessor",
|
|
103
|
-
"ClaudeTranscriptParser",
|
|
104
91
|
]
|
gobby/hooks/event_handlers.py
CHANGED
|
@@ -29,6 +29,16 @@ if TYPE_CHECKING:
|
|
|
29
29
|
from gobby.workflows.hooks import WorkflowHookHandler
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
EDIT_TOOLS = {
|
|
33
|
+
"write_file",
|
|
34
|
+
"replace",
|
|
35
|
+
"edit_file",
|
|
36
|
+
"notebook_edit",
|
|
37
|
+
"edit",
|
|
38
|
+
"write",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
32
42
|
class EventHandlers:
|
|
33
43
|
"""
|
|
34
44
|
Manages event handler registration and dispatch.
|
|
@@ -137,6 +147,56 @@ class EventHandlers:
|
|
|
137
147
|
"""
|
|
138
148
|
return dict(self._handler_map)
|
|
139
149
|
|
|
150
|
+
def _auto_activate_workflow(
|
|
151
|
+
self, workflow_name: str, session_id: str, project_path: str | None
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Auto-activate a workflow for a session.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
workflow_name: Name of the workflow to activate
|
|
157
|
+
session_id: Session ID to activate workflow for
|
|
158
|
+
project_path: Project path for workflow context
|
|
159
|
+
"""
|
|
160
|
+
if not self._workflow_handler:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
result = self._workflow_handler.activate_workflow(
|
|
165
|
+
workflow_name=workflow_name,
|
|
166
|
+
session_id=session_id,
|
|
167
|
+
project_path=project_path,
|
|
168
|
+
)
|
|
169
|
+
if result.get("success"):
|
|
170
|
+
self.logger.info(
|
|
171
|
+
"Auto-activated workflow for session",
|
|
172
|
+
extra={
|
|
173
|
+
"workflow_name": workflow_name,
|
|
174
|
+
"session_id": session_id,
|
|
175
|
+
"project_path": project_path,
|
|
176
|
+
},
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
self.logger.warning(
|
|
180
|
+
"Failed to auto-activate workflow",
|
|
181
|
+
extra={
|
|
182
|
+
"workflow_name": workflow_name,
|
|
183
|
+
"session_id": session_id,
|
|
184
|
+
"project_path": project_path,
|
|
185
|
+
"error": result.get("error"),
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
self.logger.warning(
|
|
190
|
+
"Failed to auto-activate workflow",
|
|
191
|
+
extra={
|
|
192
|
+
"workflow_name": workflow_name,
|
|
193
|
+
"session_id": session_id,
|
|
194
|
+
"project_path": project_path,
|
|
195
|
+
"error": str(e),
|
|
196
|
+
},
|
|
197
|
+
exc_info=True,
|
|
198
|
+
)
|
|
199
|
+
|
|
140
200
|
# ==================== SESSION HANDLERS ====================
|
|
141
201
|
|
|
142
202
|
def handle_session_start(self, event: HookEvent) -> HookResponse:
|
|
@@ -197,6 +257,12 @@ class EventHandlers:
|
|
|
197
257
|
except Exception as e:
|
|
198
258
|
self.logger.warning(f"Failed to start agent run: {e}")
|
|
199
259
|
|
|
260
|
+
# Auto-activate workflow if specified for this session
|
|
261
|
+
if existing_session.workflow_name and session_id:
|
|
262
|
+
self._auto_activate_workflow(
|
|
263
|
+
existing_session.workflow_name, session_id, cwd
|
|
264
|
+
)
|
|
265
|
+
|
|
200
266
|
# Update event metadata
|
|
201
267
|
event.metadata["_platform_session_id"] = session_id
|
|
202
268
|
|
|
@@ -221,8 +287,13 @@ class EventHandlers:
|
|
|
221
287
|
self.logger.warning(f"Workflow error: {e}")
|
|
222
288
|
|
|
223
289
|
# Build system message (terminal display only)
|
|
224
|
-
|
|
225
|
-
|
|
290
|
+
# Display #N format if seq_num available, fallback to UUID
|
|
291
|
+
session_ref = (
|
|
292
|
+
f"#{existing_session.seq_num}" if existing_session.seq_num else session_id
|
|
293
|
+
)
|
|
294
|
+
system_message = f"\nGobby Session ID: {session_ref}"
|
|
295
|
+
system_message += " <- Use this for MCP tool calls (session_id parameter)"
|
|
296
|
+
system_message += f"\nExternal ID: {external_id} (CLI-native, rarely needed)"
|
|
226
297
|
if parent_session_id:
|
|
227
298
|
context_parts.append(f"Parent session: {parent_session_id}")
|
|
228
299
|
|
|
@@ -246,6 +317,7 @@ class EventHandlers:
|
|
|
246
317
|
system_message=system_message,
|
|
247
318
|
metadata={
|
|
248
319
|
"session_id": session_id,
|
|
320
|
+
"session_ref": session_ref,
|
|
249
321
|
"parent_session_id": parent_session_id,
|
|
250
322
|
"machine_id": machine_id,
|
|
251
323
|
"project_id": existing_session.project_id,
|
|
@@ -257,9 +329,13 @@ class EventHandlers:
|
|
|
257
329
|
except Exception as e:
|
|
258
330
|
self.logger.debug(f"No pre-created session found: {e}")
|
|
259
331
|
|
|
260
|
-
# Step 1: Find parent session
|
|
261
|
-
|
|
262
|
-
|
|
332
|
+
# Step 1: Find parent session
|
|
333
|
+
# Check env vars first (spawned agent case), then handoff (source='clear')
|
|
334
|
+
parent_session_id = input_data.get("parent_session_id")
|
|
335
|
+
workflow_name = input_data.get("workflow_name")
|
|
336
|
+
agent_depth = input_data.get("agent_depth")
|
|
337
|
+
|
|
338
|
+
if not parent_session_id and session_source == "clear" and self._session_storage:
|
|
263
339
|
try:
|
|
264
340
|
parent = self._session_storage.find_parent(
|
|
265
341
|
machine_id=machine_id,
|
|
@@ -276,6 +352,14 @@ class EventHandlers:
|
|
|
276
352
|
# Step 2: Register new session with parent if found
|
|
277
353
|
# Extract terminal context (injected by hook_dispatcher for terminal correlation)
|
|
278
354
|
terminal_context = input_data.get("terminal_context")
|
|
355
|
+
# Parse agent_depth as int if provided
|
|
356
|
+
agent_depth_val = 0
|
|
357
|
+
if agent_depth:
|
|
358
|
+
try:
|
|
359
|
+
agent_depth_val = int(agent_depth)
|
|
360
|
+
except (ValueError, TypeError):
|
|
361
|
+
pass
|
|
362
|
+
|
|
279
363
|
session_id = None
|
|
280
364
|
if self._session_manager:
|
|
281
365
|
session_id = self._session_manager.register_session(
|
|
@@ -287,6 +371,8 @@ class EventHandlers:
|
|
|
287
371
|
source=cli_source,
|
|
288
372
|
project_path=cwd,
|
|
289
373
|
terminal_context=terminal_context,
|
|
374
|
+
workflow_name=workflow_name,
|
|
375
|
+
agent_depth=agent_depth_val,
|
|
290
376
|
)
|
|
291
377
|
|
|
292
378
|
# Step 2b: Mark parent session as expired after successful handoff
|
|
@@ -297,6 +383,10 @@ class EventHandlers:
|
|
|
297
383
|
except Exception as e:
|
|
298
384
|
self.logger.warning(f"Failed to mark parent session as expired: {e}")
|
|
299
385
|
|
|
386
|
+
# Step 2c: Auto-activate workflow if specified (for spawned agents)
|
|
387
|
+
if workflow_name and session_id:
|
|
388
|
+
self._auto_activate_workflow(workflow_name, session_id, cwd)
|
|
389
|
+
|
|
300
390
|
# Step 3: Track registered session
|
|
301
391
|
if transcript_path and self._session_coordinator:
|
|
302
392
|
try:
|
|
@@ -333,8 +423,19 @@ class EventHandlers:
|
|
|
333
423
|
context_parts.append(f"Parent session: {parent_session_id}")
|
|
334
424
|
|
|
335
425
|
# Build system message (terminal display only)
|
|
336
|
-
|
|
337
|
-
|
|
426
|
+
# Fetch session to get seq_num for #N display
|
|
427
|
+
session_ref = session_id # fallback
|
|
428
|
+
if session_id and self._session_storage:
|
|
429
|
+
session_obj = self._session_storage.get(session_id)
|
|
430
|
+
if session_obj and session_obj.seq_num:
|
|
431
|
+
session_ref = f"#{session_obj.seq_num}"
|
|
432
|
+
# Format: "Gobby Session ID: #N" with usage hint
|
|
433
|
+
if session_ref and session_ref != session_id:
|
|
434
|
+
system_message = f"\nGobby Session ID: {session_ref}"
|
|
435
|
+
else:
|
|
436
|
+
system_message = f"\nGobby Session ID: {session_id}"
|
|
437
|
+
system_message += " <- Use this for MCP tool calls (session_id parameter)"
|
|
438
|
+
system_message += f"\nExternal ID: {external_id} (CLI-native, rarely needed)"
|
|
338
439
|
|
|
339
440
|
# Add active lifecycle workflows
|
|
340
441
|
if wf_response.metadata and "discovered_workflows" in wf_response.metadata:
|
|
@@ -362,6 +463,7 @@ class EventHandlers:
|
|
|
362
463
|
# Build metadata with terminal context (filter out nulls)
|
|
363
464
|
metadata: dict[str, Any] = {
|
|
364
465
|
"session_id": session_id,
|
|
466
|
+
"session_ref": session_ref,
|
|
365
467
|
"parent_session_id": parent_session_id,
|
|
366
468
|
"machine_id": machine_id,
|
|
367
469
|
"project_id": project_id,
|
|
@@ -691,6 +793,33 @@ class EventHandlers:
|
|
|
691
793
|
status = "FAIL" if is_failure else "OK"
|
|
692
794
|
if session_id:
|
|
693
795
|
self.logger.debug(f"AFTER_TOOL [{status}]: {tool_name}, session {session_id}")
|
|
796
|
+
|
|
797
|
+
# Track edits for session high-water mark
|
|
798
|
+
# Only if tool succeeded, matches edit tools, and session has claimed a task
|
|
799
|
+
# Skip .gobby/ internal files (tasks.jsonl, memories.jsonl, etc.)
|
|
800
|
+
tool_input = input_data.get("tool_input", {})
|
|
801
|
+
file_path = tool_input.get("file_path", "")
|
|
802
|
+
is_gobby_internal = "/.gobby/" in file_path or file_path.startswith(".gobby/")
|
|
803
|
+
|
|
804
|
+
if (
|
|
805
|
+
not is_failure
|
|
806
|
+
and tool_name
|
|
807
|
+
and tool_name.lower() in EDIT_TOOLS
|
|
808
|
+
and not is_gobby_internal
|
|
809
|
+
and self._session_storage
|
|
810
|
+
and self._task_manager
|
|
811
|
+
):
|
|
812
|
+
try:
|
|
813
|
+
# Check if session has any claimed tasks in progress
|
|
814
|
+
claimed_tasks = self._task_manager.list_tasks(
|
|
815
|
+
assignee=session_id, status="in_progress", limit=1
|
|
816
|
+
)
|
|
817
|
+
if claimed_tasks:
|
|
818
|
+
self._session_storage.mark_had_edits(session_id)
|
|
819
|
+
self.logger.debug(f"Marked session {session_id} as had_edits")
|
|
820
|
+
except Exception as e:
|
|
821
|
+
self.logger.warning(f"Failed to track edit history: {e}")
|
|
822
|
+
|
|
694
823
|
else:
|
|
695
824
|
self.logger.debug(f"AFTER_TOOL [{status}]: {tool_name}")
|
|
696
825
|
|
|
@@ -737,10 +866,23 @@ class EventHandlers:
|
|
|
737
866
|
# ==================== COMPACT HANDLER ====================
|
|
738
867
|
|
|
739
868
|
def handle_pre_compact(self, event: HookEvent) -> HookResponse:
|
|
740
|
-
"""Handle PRE_COMPACT event.
|
|
869
|
+
"""Handle PRE_COMPACT event.
|
|
870
|
+
|
|
871
|
+
Note: Gemini fires PreCompress constantly during normal operation,
|
|
872
|
+
unlike Claude which fires it only when approaching context limits.
|
|
873
|
+
We skip handoff logic and workflow execution for Gemini to avoid
|
|
874
|
+
excessive state changes and workflow interruptions.
|
|
875
|
+
"""
|
|
876
|
+
from gobby.hooks.events import SessionSource
|
|
877
|
+
|
|
741
878
|
trigger = event.data.get("trigger", "auto")
|
|
742
879
|
session_id = event.metadata.get("_platform_session_id")
|
|
743
880
|
|
|
881
|
+
# Skip handoff logic for Gemini - it fires PreCompress too frequently
|
|
882
|
+
if event.source == SessionSource.GEMINI:
|
|
883
|
+
self.logger.debug(f"PRE_COMPACT ({trigger}): session {session_id} [Gemini - skipped]")
|
|
884
|
+
return HookResponse(decision="allow")
|
|
885
|
+
|
|
744
886
|
if session_id:
|
|
745
887
|
self.logger.debug(f"PRE_COMPACT ({trigger}): session {session_id}")
|
|
746
888
|
# Mark session as handoff_ready so it can be found as parent after compact
|
gobby/hooks/hook_manager.py
CHANGED
|
@@ -228,7 +228,7 @@ class HookManager:
|
|
|
228
228
|
)
|
|
229
229
|
|
|
230
230
|
if not memory_config:
|
|
231
|
-
from gobby.config.
|
|
231
|
+
from gobby.config.persistence import MemoryConfig
|
|
232
232
|
|
|
233
233
|
memory_config = MemoryConfig()
|
|
234
234
|
|
|
@@ -316,7 +316,7 @@ class HookManager:
|
|
|
316
316
|
if self._config and hasattr(self._config, "hook_extensions"):
|
|
317
317
|
webhooks_config = self._config.hook_extensions.webhooks
|
|
318
318
|
if not webhooks_config:
|
|
319
|
-
from gobby.config.
|
|
319
|
+
from gobby.config.extensions import WebhooksConfig
|
|
320
320
|
|
|
321
321
|
webhooks_config = WebhooksConfig()
|
|
322
322
|
self._webhook_dispatcher = WebhookDispatcher(webhooks_config)
|
|
@@ -369,6 +369,10 @@ class HookManager:
|
|
|
369
369
|
# Skill manager for core skill injection
|
|
370
370
|
self._skill_manager = HookSkillManager()
|
|
371
371
|
|
|
372
|
+
# Track sessions that have received full metadata injection
|
|
373
|
+
# Key: "{platform_session_id}:{source}" - cleared on daemon restart
|
|
374
|
+
self._injected_sessions: set[str] = set()
|
|
375
|
+
|
|
372
376
|
# Event handlers (delegated to EventHandlers module)
|
|
373
377
|
self._event_handlers = EventHandlers(
|
|
374
378
|
session_manager=self._session_manager,
|
|
@@ -644,7 +648,21 @@ class HookManager:
|
|
|
644
648
|
# Copy session metadata from event to response for adapter injection
|
|
645
649
|
# The adapter reads response.metadata to inject session info into agent context
|
|
646
650
|
if event.metadata.get("_platform_session_id"):
|
|
647
|
-
|
|
651
|
+
platform_session_id = event.metadata["_platform_session_id"]
|
|
652
|
+
response.metadata["session_id"] = platform_session_id
|
|
653
|
+
# Look up seq_num for session_ref (#N format)
|
|
654
|
+
if self._session_storage:
|
|
655
|
+
session_obj = self._session_storage.get(platform_session_id)
|
|
656
|
+
if session_obj and session_obj.seq_num:
|
|
657
|
+
response.metadata["session_ref"] = f"#{session_obj.seq_num}"
|
|
658
|
+
|
|
659
|
+
# Track first hook per session for token optimization
|
|
660
|
+
# Adapters use this flag to inject full metadata only on first hook
|
|
661
|
+
session_key = f"{platform_session_id}:{event.source.value}"
|
|
662
|
+
is_first = session_key not in self._injected_sessions
|
|
663
|
+
if is_first:
|
|
664
|
+
self._injected_sessions.add(session_key)
|
|
665
|
+
response.metadata["_first_hook_for_session"] = is_first
|
|
648
666
|
if event.session_id: # external_id (e.g., Claude Code's session UUID)
|
|
649
667
|
response.metadata["external_id"] = event.session_id
|
|
650
668
|
if event.machine_id:
|
gobby/hooks/plugins.py
CHANGED
|
@@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
23
23
|
from gobby.hooks.events import HookEvent, HookEventType, HookResponse
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
|
-
from gobby.config.
|
|
26
|
+
from gobby.config.extensions import PluginsConfig
|
|
27
27
|
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
29
29
|
|
gobby/hooks/webhooks.py
CHANGED
|
@@ -20,7 +20,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
20
20
|
import httpx
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
|
-
from gobby.config.
|
|
23
|
+
from gobby.config.extensions import WebhookEndpointConfig, WebhooksConfig
|
|
24
24
|
from gobby.hooks.events import HookEvent
|
|
25
25
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|