gobby 0.2.5__py3-none-any.whl → 0.2.7__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 +13 -4
- 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/agents/definitions.py +11 -1
- gobby/agents/isolation.py +395 -0
- gobby/agents/runner.py +8 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +385 -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/prompt_manager.py +125 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +15 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +8 -8
- gobby/cli/installers/shared.py +175 -13
- gobby/cli/sessions.py +1 -1
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +12 -5
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +69 -91
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +9 -41
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +188 -2
- gobby/hooks/hook_manager.py +50 -4
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/skill_manager.py +130 -0
- gobby/hooks/webhooks.py +1 -1
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +98 -35
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +56 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -8
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +518 -0
- gobby/mcp_proxy/tools/memory.py +3 -26
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +232 -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 +499 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
- gobby/mcp_proxy/tools/skills/__init__.py +616 -0
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -338
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +73 -285
- 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 +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +24 -12
- gobby/servers/routes/admin.py +294 -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 +1 -1
- gobby/servers/routes/mcp/tools.py +48 -1317
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +2 -0
- gobby/sessions/transcripts/claude.py +79 -10
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +286 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +162 -201
- gobby/storage/sessions.py +116 -7
- gobby/storage/skills.py +782 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +57 -7
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +40 -5
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +46 -35
- gobby/tools/summarizer.py +91 -10
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1135
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +93 -1
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +269 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
- gobby/workflows/engine.py +13 -2
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/loader.py +19 -6
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +154 -0
- gobby/workflows/safe_evaluator.py +183 -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 +111 -1
- 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.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1292
- 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/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/session_messages.py +0 -1056
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- 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/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/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
gobby/hooks/event_handlers.py
CHANGED
|
@@ -17,7 +17,9 @@ from typing import TYPE_CHECKING, Any
|
|
|
17
17
|
from gobby.hooks.events import HookEvent, HookEventType, HookResponse
|
|
18
18
|
|
|
19
19
|
if TYPE_CHECKING:
|
|
20
|
+
from gobby.config.skills import SkillsConfig
|
|
20
21
|
from gobby.hooks.session_coordinator import SessionCoordinator
|
|
22
|
+
from gobby.hooks.skill_manager import HookSkillManager
|
|
21
23
|
from gobby.sessions.manager import SessionManager
|
|
22
24
|
from gobby.sessions.summary import SummaryFileGenerator
|
|
23
25
|
from gobby.storage.session_messages import LocalSessionMessageManager
|
|
@@ -27,6 +29,16 @@ if TYPE_CHECKING:
|
|
|
27
29
|
from gobby.workflows.hooks import WorkflowHookHandler
|
|
28
30
|
|
|
29
31
|
|
|
32
|
+
EDIT_TOOLS = {
|
|
33
|
+
"write_file",
|
|
34
|
+
"replace",
|
|
35
|
+
"edit_file",
|
|
36
|
+
"notebook_edit",
|
|
37
|
+
"edit",
|
|
38
|
+
"write",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
30
42
|
class EventHandlers:
|
|
31
43
|
"""
|
|
32
44
|
Manages event handler registration and dispatch.
|
|
@@ -48,6 +60,8 @@ class EventHandlers:
|
|
|
48
60
|
task_manager: LocalTaskManager | None = None,
|
|
49
61
|
session_coordinator: SessionCoordinator | None = None,
|
|
50
62
|
message_manager: LocalSessionMessageManager | None = None,
|
|
63
|
+
skill_manager: HookSkillManager | None = None,
|
|
64
|
+
skills_config: SkillsConfig | None = None,
|
|
51
65
|
get_machine_id: Callable[[], str] | None = None,
|
|
52
66
|
resolve_project_id: Callable[[str | None, str | None], str] | None = None,
|
|
53
67
|
logger: logging.Logger | None = None,
|
|
@@ -65,6 +79,8 @@ class EventHandlers:
|
|
|
65
79
|
task_manager: LocalTaskManager for task operations
|
|
66
80
|
session_coordinator: SessionCoordinator for session tracking
|
|
67
81
|
message_manager: LocalSessionMessageManager for messages
|
|
82
|
+
skill_manager: HookSkillManager for skill discovery
|
|
83
|
+
skills_config: SkillsConfig for skill injection settings
|
|
68
84
|
get_machine_id: Function to get machine ID
|
|
69
85
|
resolve_project_id: Function to resolve project ID from cwd
|
|
70
86
|
logger: Optional logger instance
|
|
@@ -78,6 +94,8 @@ class EventHandlers:
|
|
|
78
94
|
self._task_manager = task_manager
|
|
79
95
|
self._session_coordinator = session_coordinator
|
|
80
96
|
self._message_manager = message_manager
|
|
97
|
+
self._skill_manager = skill_manager
|
|
98
|
+
self._skills_config = skills_config
|
|
81
99
|
self._get_machine_id = get_machine_id or (lambda: "unknown-machine")
|
|
82
100
|
self._resolve_project_id = resolve_project_id or (lambda p, c: p or "")
|
|
83
101
|
self.logger = logger or logging.getLogger(__name__)
|
|
@@ -213,9 +231,27 @@ class EventHandlers:
|
|
|
213
231
|
self.logger.warning(f"Workflow error: {e}")
|
|
214
232
|
|
|
215
233
|
# Build system message (terminal display only)
|
|
216
|
-
|
|
234
|
+
# Display #N format if seq_num available, fallback to UUID
|
|
235
|
+
session_ref = (
|
|
236
|
+
f"#{existing_session.seq_num}" if existing_session.seq_num else session_id
|
|
237
|
+
)
|
|
238
|
+
system_message = f"\nGobby Session Ref: {session_ref}"
|
|
239
|
+
system_message += f"\nGobby Session ID: {session_id}"
|
|
240
|
+
system_message += f"\nExternal ID: {external_id}"
|
|
217
241
|
if parent_session_id:
|
|
218
242
|
context_parts.append(f"Parent session: {parent_session_id}")
|
|
243
|
+
|
|
244
|
+
# Add active lifecycle workflows
|
|
245
|
+
if wf_response.metadata and "discovered_workflows" in wf_response.metadata:
|
|
246
|
+
wf_list = wf_response.metadata["discovered_workflows"]
|
|
247
|
+
if wf_list:
|
|
248
|
+
system_message += "\nActive workflows:"
|
|
249
|
+
for w in wf_list:
|
|
250
|
+
source = "project" if w["is_project"] else "global"
|
|
251
|
+
system_message += (
|
|
252
|
+
f"\n - {w['name']} ({source}, priority={w['priority']})"
|
|
253
|
+
)
|
|
254
|
+
|
|
219
255
|
if wf_response.system_message:
|
|
220
256
|
system_message += f"\n\n{wf_response.system_message}"
|
|
221
257
|
|
|
@@ -312,7 +348,25 @@ class EventHandlers:
|
|
|
312
348
|
context_parts.append(f"Parent session: {parent_session_id}")
|
|
313
349
|
|
|
314
350
|
# Build system message (terminal display only)
|
|
315
|
-
|
|
351
|
+
# Fetch session to get seq_num for #N display
|
|
352
|
+
session_ref = session_id # fallback
|
|
353
|
+
if session_id and self._session_storage:
|
|
354
|
+
session_obj = self._session_storage.get(session_id)
|
|
355
|
+
if session_obj and session_obj.seq_num:
|
|
356
|
+
session_ref = f"#{session_obj.seq_num}"
|
|
357
|
+
system_message = f"\nGobby Session Ref: {session_ref}"
|
|
358
|
+
system_message += f"\nGobby Session ID: {session_id}"
|
|
359
|
+
system_message += f"\nExternal ID: {external_id}"
|
|
360
|
+
|
|
361
|
+
# Add active lifecycle workflows
|
|
362
|
+
if wf_response.metadata and "discovered_workflows" in wf_response.metadata:
|
|
363
|
+
wf_list = wf_response.metadata["discovered_workflows"]
|
|
364
|
+
if wf_list:
|
|
365
|
+
system_message += "\nActive workflows:"
|
|
366
|
+
for w in wf_list:
|
|
367
|
+
source = "project" if w["is_project"] else "global"
|
|
368
|
+
system_message += f"\n - {w['name']} ({source}, priority={w['priority']})"
|
|
369
|
+
|
|
316
370
|
if wf_response.system_message:
|
|
317
371
|
system_message += f"\n\n{wf_response.system_message}"
|
|
318
372
|
|
|
@@ -322,6 +376,11 @@ class EventHandlers:
|
|
|
322
376
|
context_parts.append("\n## Active Task Context\n")
|
|
323
377
|
context_parts.append(f"You are working on task: {task_title} ({event.task_id})")
|
|
324
378
|
|
|
379
|
+
# Inject core skills if enabled (restoring from parent session if available)
|
|
380
|
+
skill_context = self._build_skill_injection_context(parent_session_id)
|
|
381
|
+
if skill_context:
|
|
382
|
+
context_parts.append(skill_context)
|
|
383
|
+
|
|
325
384
|
# Build metadata with terminal context (filter out nulls)
|
|
326
385
|
metadata: dict[str, Any] = {
|
|
327
386
|
"session_id": session_id,
|
|
@@ -434,6 +493,112 @@ class EventHandlers:
|
|
|
434
493
|
|
|
435
494
|
return HookResponse(decision="allow")
|
|
436
495
|
|
|
496
|
+
def _build_skill_injection_context(self, parent_session_id: str | None = None) -> str | None:
|
|
497
|
+
"""Build skill injection context for session-start.
|
|
498
|
+
|
|
499
|
+
Combines alwaysApply skills with skills restored from parent session.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
parent_session_id: Optional parent session ID to restore skills from
|
|
503
|
+
|
|
504
|
+
Returns context string with available skills if injection is enabled,
|
|
505
|
+
or None if disabled.
|
|
506
|
+
"""
|
|
507
|
+
# Skip if no skill manager or config
|
|
508
|
+
if not self._skill_manager or not self._skills_config:
|
|
509
|
+
return None
|
|
510
|
+
|
|
511
|
+
# Check if injection is enabled
|
|
512
|
+
if not self._skills_config.inject_core_skills:
|
|
513
|
+
return None
|
|
514
|
+
|
|
515
|
+
# Check injection format
|
|
516
|
+
if self._skills_config.injection_format == "none":
|
|
517
|
+
return None
|
|
518
|
+
|
|
519
|
+
# Get alwaysApply skills
|
|
520
|
+
try:
|
|
521
|
+
core_skills = self._skill_manager.discover_core_skills()
|
|
522
|
+
always_apply_skills = [s for s in core_skills if s.is_always_apply()]
|
|
523
|
+
|
|
524
|
+
# Get restored skills from parent session
|
|
525
|
+
restored_skills = self._restore_skills_from_parent(parent_session_id)
|
|
526
|
+
|
|
527
|
+
# Combine: alwaysApply skills + any additional restored skills
|
|
528
|
+
skill_names = [s.name for s in always_apply_skills]
|
|
529
|
+
for skill_name in restored_skills:
|
|
530
|
+
if skill_name not in skill_names:
|
|
531
|
+
skill_names.append(skill_name)
|
|
532
|
+
|
|
533
|
+
if not skill_names:
|
|
534
|
+
return None
|
|
535
|
+
|
|
536
|
+
# Build context based on format
|
|
537
|
+
if self._skills_config.injection_format == "summary":
|
|
538
|
+
return (
|
|
539
|
+
"\n## Available Skills\n"
|
|
540
|
+
f"The following skills are always available: {', '.join(skill_names)}\n"
|
|
541
|
+
"Use the /skill-name syntax to invoke them."
|
|
542
|
+
)
|
|
543
|
+
elif self._skills_config.injection_format == "full":
|
|
544
|
+
parts = ["\n## Available Skills\n"]
|
|
545
|
+
# Build a map of always_apply skills for quick lookup
|
|
546
|
+
always_apply_map = {s.name: s for s in always_apply_skills}
|
|
547
|
+
# Iterate over combined skill_names list (always_apply + restored)
|
|
548
|
+
for skill_name in skill_names:
|
|
549
|
+
parts.append(f"### {skill_name}")
|
|
550
|
+
# Get description from always_apply skill if available
|
|
551
|
+
if skill_name in always_apply_map:
|
|
552
|
+
skill = always_apply_map[skill_name]
|
|
553
|
+
if skill.description:
|
|
554
|
+
parts.append(skill.description)
|
|
555
|
+
parts.append("")
|
|
556
|
+
return "\n".join(parts)
|
|
557
|
+
else:
|
|
558
|
+
return None
|
|
559
|
+
|
|
560
|
+
except Exception as e:
|
|
561
|
+
self.logger.warning(f"Failed to build skill injection context: {e}")
|
|
562
|
+
return None
|
|
563
|
+
|
|
564
|
+
def _restore_skills_from_parent(self, parent_session_id: str | None) -> list[str]:
|
|
565
|
+
"""Restore active skills from parent session's handoff context.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
parent_session_id: Parent session ID to restore from
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
List of skill names from the parent session
|
|
572
|
+
"""
|
|
573
|
+
if not parent_session_id or not self._session_storage:
|
|
574
|
+
return []
|
|
575
|
+
|
|
576
|
+
try:
|
|
577
|
+
parent = self._session_storage.get(parent_session_id)
|
|
578
|
+
if not parent:
|
|
579
|
+
return []
|
|
580
|
+
|
|
581
|
+
compact_md = getattr(parent, "compact_markdown", None)
|
|
582
|
+
if not compact_md:
|
|
583
|
+
return []
|
|
584
|
+
|
|
585
|
+
# Parse active skills from markdown
|
|
586
|
+
# Format: "### Active Skills\nSkills available: skill1, skill2, skill3"
|
|
587
|
+
import re
|
|
588
|
+
|
|
589
|
+
match = re.search(r"### Active Skills\s*\nSkills available:\s*([^\n]+)", compact_md)
|
|
590
|
+
if match:
|
|
591
|
+
skills_str = match.group(1).strip()
|
|
592
|
+
skills = [s.strip() for s in skills_str.split(",") if s.strip()]
|
|
593
|
+
self.logger.debug(f"Restored {len(skills)} skills from parent session")
|
|
594
|
+
return skills
|
|
595
|
+
|
|
596
|
+
return []
|
|
597
|
+
|
|
598
|
+
except Exception as e:
|
|
599
|
+
self.logger.warning(f"Failed to restore skills from parent: {e}")
|
|
600
|
+
return []
|
|
601
|
+
|
|
437
602
|
# ==================== AGENT HANDLERS ====================
|
|
438
603
|
|
|
439
604
|
def handle_before_agent(self, event: HookEvent) -> HookResponse:
|
|
@@ -548,6 +713,27 @@ class EventHandlers:
|
|
|
548
713
|
status = "FAIL" if is_failure else "OK"
|
|
549
714
|
if session_id:
|
|
550
715
|
self.logger.debug(f"AFTER_TOOL [{status}]: {tool_name}, session {session_id}")
|
|
716
|
+
|
|
717
|
+
# Track edits for session high-water mark
|
|
718
|
+
# Only if tool succeeded, matches edit tools, and session has claimed a task
|
|
719
|
+
if (
|
|
720
|
+
not is_failure
|
|
721
|
+
and tool_name
|
|
722
|
+
and tool_name.lower() in EDIT_TOOLS
|
|
723
|
+
and self._session_storage
|
|
724
|
+
and self._task_manager
|
|
725
|
+
):
|
|
726
|
+
try:
|
|
727
|
+
# Check if session has any claimed tasks in progress
|
|
728
|
+
claimed_tasks = self._task_manager.list_tasks(
|
|
729
|
+
assignee=session_id, status="in_progress", limit=1
|
|
730
|
+
)
|
|
731
|
+
if claimed_tasks:
|
|
732
|
+
self._session_storage.mark_had_edits(session_id)
|
|
733
|
+
self.logger.debug(f"Marked session {session_id} as had_edits")
|
|
734
|
+
except Exception as e:
|
|
735
|
+
self.logger.warning(f"Failed to track edit history: {e}")
|
|
736
|
+
|
|
551
737
|
else:
|
|
552
738
|
self.logger.debug(f"AFTER_TOOL [{status}]: {tool_name}")
|
|
553
739
|
|
gobby/hooks/hook_manager.py
CHANGED
|
@@ -17,7 +17,7 @@ Example:
|
|
|
17
17
|
|
|
18
18
|
manager = HookManager(
|
|
19
19
|
daemon_host="localhost",
|
|
20
|
-
daemon_port=
|
|
20
|
+
daemon_port=60887
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
result = manager.execute(
|
|
@@ -42,6 +42,7 @@ from gobby.hooks.events import HookEvent, HookEventType, HookResponse
|
|
|
42
42
|
from gobby.hooks.health_monitor import HealthMonitor
|
|
43
43
|
from gobby.hooks.plugins import PluginLoader, run_plugin_handlers
|
|
44
44
|
from gobby.hooks.session_coordinator import SessionCoordinator
|
|
45
|
+
from gobby.hooks.skill_manager import HookSkillManager
|
|
45
46
|
from gobby.hooks.webhooks import WebhookDispatcher
|
|
46
47
|
from gobby.memory.manager import MemoryManager
|
|
47
48
|
from gobby.sessions.manager import SessionManager
|
|
@@ -79,6 +80,21 @@ class HookManager:
|
|
|
79
80
|
- TranscriptProcessor: JSONL parsing and analysis
|
|
80
81
|
- WorkflowEngine: Handles session handoff and LLM-powered summaries
|
|
81
82
|
|
|
83
|
+
Session ID Mapping:
|
|
84
|
+
There are two types of session IDs used throughout the system:
|
|
85
|
+
|
|
86
|
+
| Name | Description | Example |
|
|
87
|
+
|--------------------------|------------------------------------------------|----------------------------------------|
|
|
88
|
+
| external_id / session_id | CLI's internal session UUID (Claude Code, etc) | 683bc13e-091e-4911-9e59-e7546e385cd6 |
|
|
89
|
+
| _platform_session_id | Gobby's internal session.id (database PK) | 0ebb2c00-0f58-4c39-9370-eba1833dec33 |
|
|
90
|
+
|
|
91
|
+
The _platform_session_id is derived from session_manager.get_session_id(external_id, source)
|
|
92
|
+
which looks up Gobby's session by the CLI's external_id.
|
|
93
|
+
|
|
94
|
+
When injecting into agent context:
|
|
95
|
+
- "session_id" in response.metadata = Gobby's _platform_session_id (for MCP tool calls)
|
|
96
|
+
- "external_id" in response.metadata = CLI's session UUID (for transcript lookups)
|
|
97
|
+
|
|
82
98
|
Attributes:
|
|
83
99
|
daemon_host: Host for daemon communication
|
|
84
100
|
daemon_port: Port for daemon communication
|
|
@@ -89,7 +105,7 @@ class HookManager:
|
|
|
89
105
|
def __init__(
|
|
90
106
|
self,
|
|
91
107
|
daemon_host: str = "localhost",
|
|
92
|
-
daemon_port: int =
|
|
108
|
+
daemon_port: int = 60887,
|
|
93
109
|
llm_service: "LLMService | None" = None,
|
|
94
110
|
config: Any | None = None,
|
|
95
111
|
log_file: str | None = None,
|
|
@@ -212,7 +228,7 @@ class HookManager:
|
|
|
212
228
|
)
|
|
213
229
|
|
|
214
230
|
if not memory_config:
|
|
215
|
-
from gobby.config.
|
|
231
|
+
from gobby.config.persistence import MemoryConfig
|
|
216
232
|
|
|
217
233
|
memory_config = MemoryConfig()
|
|
218
234
|
|
|
@@ -300,7 +316,7 @@ class HookManager:
|
|
|
300
316
|
if self._config and hasattr(self._config, "hook_extensions"):
|
|
301
317
|
webhooks_config = self._config.hook_extensions.webhooks
|
|
302
318
|
if not webhooks_config:
|
|
303
|
-
from gobby.config.
|
|
319
|
+
from gobby.config.extensions import WebhooksConfig
|
|
304
320
|
|
|
305
321
|
webhooks_config = WebhooksConfig()
|
|
306
322
|
self._webhook_dispatcher = WebhookDispatcher(webhooks_config)
|
|
@@ -350,6 +366,9 @@ class HookManager:
|
|
|
350
366
|
logger=self.logger,
|
|
351
367
|
)
|
|
352
368
|
|
|
369
|
+
# Skill manager for core skill injection
|
|
370
|
+
self._skill_manager = HookSkillManager()
|
|
371
|
+
|
|
353
372
|
# Event handlers (delegated to EventHandlers module)
|
|
354
373
|
self._event_handlers = EventHandlers(
|
|
355
374
|
session_manager=self._session_manager,
|
|
@@ -361,6 +380,8 @@ class HookManager:
|
|
|
361
380
|
task_manager=self._task_manager,
|
|
362
381
|
session_coordinator=self._session_coordinator,
|
|
363
382
|
message_manager=self._message_manager,
|
|
383
|
+
skill_manager=self._skill_manager,
|
|
384
|
+
skills_config=self._config.skills if self._config else None,
|
|
364
385
|
get_machine_id=self.get_machine_id,
|
|
365
386
|
resolve_project_id=self._resolve_project_id,
|
|
366
387
|
logger=self.logger,
|
|
@@ -620,6 +641,31 @@ class HookManager:
|
|
|
620
641
|
try:
|
|
621
642
|
response = handler(event)
|
|
622
643
|
|
|
644
|
+
# Copy session metadata from event to response for adapter injection
|
|
645
|
+
# The adapter reads response.metadata to inject session info into agent context
|
|
646
|
+
if event.metadata.get("_platform_session_id"):
|
|
647
|
+
response.metadata["session_id"] = event.metadata["_platform_session_id"]
|
|
648
|
+
if event.session_id: # external_id (e.g., Claude Code's session UUID)
|
|
649
|
+
response.metadata["external_id"] = event.session_id
|
|
650
|
+
if event.machine_id:
|
|
651
|
+
response.metadata["machine_id"] = event.machine_id
|
|
652
|
+
if event.project_id:
|
|
653
|
+
response.metadata["project_id"] = event.project_id
|
|
654
|
+
# Copy terminal context if present
|
|
655
|
+
for key in [
|
|
656
|
+
"terminal_term_program",
|
|
657
|
+
"terminal_tty",
|
|
658
|
+
"terminal_parent_pid",
|
|
659
|
+
"terminal_iterm_session_id",
|
|
660
|
+
"terminal_term_session_id",
|
|
661
|
+
"terminal_kitty_window_id",
|
|
662
|
+
"terminal_tmux_pane",
|
|
663
|
+
"terminal_vscode_terminal_id",
|
|
664
|
+
"terminal_alacritty_socket",
|
|
665
|
+
]:
|
|
666
|
+
if event.metadata.get(key):
|
|
667
|
+
response.metadata[key] = event.metadata[key]
|
|
668
|
+
|
|
623
669
|
# Merge workflow context if present
|
|
624
670
|
if workflow_context:
|
|
625
671
|
if response.context:
|
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
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""HookSkillManager - Skill management for the hook system.
|
|
2
|
+
|
|
3
|
+
This module provides skill discovery and management for the hook system,
|
|
4
|
+
allowing hooks to access and use skills (Agent Skills specification).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from gobby.skills.loader import SkillLoader
|
|
13
|
+
from gobby.skills.parser import ParsedSkill
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HookSkillManager:
|
|
19
|
+
"""Manage skills for the hook system.
|
|
20
|
+
|
|
21
|
+
Provides discovery and access to core skills bundled with Gobby,
|
|
22
|
+
as well as project-specific skills.
|
|
23
|
+
|
|
24
|
+
Example usage:
|
|
25
|
+
```python
|
|
26
|
+
from gobby.hooks.skill_manager import HookSkillManager
|
|
27
|
+
|
|
28
|
+
manager = HookSkillManager()
|
|
29
|
+
skills = manager.discover_core_skills()
|
|
30
|
+
|
|
31
|
+
# Get a specific skill
|
|
32
|
+
tasks_skill = manager.get_skill_by_name("gobby-tasks")
|
|
33
|
+
```
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
"""Initialize the skill manager."""
|
|
38
|
+
# Path to built-in skills: src/gobby/hooks/ -> src/gobby/install/shared/skills/
|
|
39
|
+
self._base_dir = Path(__file__).parent.parent
|
|
40
|
+
self._core_skills_path = self._base_dir / "install" / "shared" / "skills"
|
|
41
|
+
|
|
42
|
+
# Loader for parsing skills (use "filesystem" for bundled core skills)
|
|
43
|
+
self._loader = SkillLoader(default_source_type="filesystem")
|
|
44
|
+
|
|
45
|
+
# Cache of discovered skills
|
|
46
|
+
self._core_skills: list[ParsedSkill] | None = None
|
|
47
|
+
|
|
48
|
+
def discover_core_skills(self) -> list[ParsedSkill]:
|
|
49
|
+
"""Discover built-in skills from install/shared/skills/.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of ParsedSkill objects for all valid core skills.
|
|
53
|
+
Invalid skills are logged as warnings and skipped.
|
|
54
|
+
"""
|
|
55
|
+
if self._core_skills is not None:
|
|
56
|
+
return self._core_skills
|
|
57
|
+
|
|
58
|
+
if not self._core_skills_path.exists():
|
|
59
|
+
logger.warning(f"Core skills path not found: {self._core_skills_path}")
|
|
60
|
+
self._core_skills = []
|
|
61
|
+
return self._core_skills
|
|
62
|
+
|
|
63
|
+
# Load all skills from the core directory
|
|
64
|
+
self._core_skills = self._loader.load_directory(
|
|
65
|
+
self._core_skills_path,
|
|
66
|
+
validate=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
logger.debug(f"Discovered {len(self._core_skills)} core skills")
|
|
70
|
+
return self._core_skills
|
|
71
|
+
|
|
72
|
+
def get_skill_by_name(self, name: str) -> ParsedSkill | None:
|
|
73
|
+
"""Get a skill by name.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
name: The skill name to look up.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
ParsedSkill if found, None otherwise.
|
|
80
|
+
"""
|
|
81
|
+
# Ensure skills are discovered
|
|
82
|
+
skills = self.discover_core_skills()
|
|
83
|
+
|
|
84
|
+
for skill in skills:
|
|
85
|
+
if skill.name == name:
|
|
86
|
+
return skill
|
|
87
|
+
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
def refresh(self) -> None:
|
|
91
|
+
"""Clear the cache and rediscover skills."""
|
|
92
|
+
self._core_skills = None
|
|
93
|
+
|
|
94
|
+
def recommend_skills(self, category: str | None = None) -> list[str]:
|
|
95
|
+
"""Recommend relevant skills based on task category.
|
|
96
|
+
|
|
97
|
+
Maps task categories to relevant core skills that would be helpful
|
|
98
|
+
for that type of work.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
category: Task category (e.g., 'code', 'docs', 'test', 'config')
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of skill names that are relevant for the category
|
|
105
|
+
"""
|
|
106
|
+
# Category to skill mappings
|
|
107
|
+
category_skills: dict[str, list[str]] = {
|
|
108
|
+
"code": ["gobby-tasks", "gobby-expand", "gobby-worktrees"],
|
|
109
|
+
"test": ["gobby-tasks", "gobby-expand"],
|
|
110
|
+
"docs": ["gobby-tasks", "gobby-plan"],
|
|
111
|
+
"config": ["gobby-tasks", "gobby-mcp"],
|
|
112
|
+
"refactor": ["gobby-tasks", "gobby-expand", "gobby-worktrees"],
|
|
113
|
+
"planning": ["gobby-tasks", "gobby-plan", "gobby-expand"],
|
|
114
|
+
"research": ["gobby-tasks", "gobby-memory"],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Get skills for the category (or empty list if no match)
|
|
118
|
+
recommended = category_skills.get(category or "", [])
|
|
119
|
+
|
|
120
|
+
# Always include alwaysApply skills
|
|
121
|
+
skills = self.discover_core_skills()
|
|
122
|
+
always_apply = [s.name for s in skills if s.is_always_apply()]
|
|
123
|
+
|
|
124
|
+
# Combine and dedupe while preserving order
|
|
125
|
+
result = list(always_apply)
|
|
126
|
+
for skill_name in recommended:
|
|
127
|
+
if skill_name not in result:
|
|
128
|
+
result.append(skill_name)
|
|
129
|
+
|
|
130
|
+
return result
|
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__)
|
|
@@ -23,7 +23,7 @@ from pathlib import Path
|
|
|
23
23
|
# No longer need to import HookManager - we call it via HTTP daemon instead
|
|
24
24
|
|
|
25
25
|
# Default daemon configuration
|
|
26
|
-
DEFAULT_DAEMON_PORT =
|
|
26
|
+
DEFAULT_DAEMON_PORT = 60887
|
|
27
27
|
DEFAULT_CONFIG_PATH = "~/.gobby/config.yaml"
|
|
28
28
|
|
|
29
29
|
|
|
@@ -31,10 +31,10 @@ def get_daemon_url() -> str:
|
|
|
31
31
|
"""Get the daemon HTTP URL from config file.
|
|
32
32
|
|
|
33
33
|
Reads daemon_port from ~/.gobby/config.yaml if it exists,
|
|
34
|
-
otherwise uses the default port
|
|
34
|
+
otherwise uses the default port 60887.
|
|
35
35
|
|
|
36
36
|
Returns:
|
|
37
|
-
Full daemon URL like http://localhost:
|
|
37
|
+
Full daemon URL like http://localhost:60887
|
|
38
38
|
"""
|
|
39
39
|
config_path = Path(DEFAULT_CONFIG_PATH).expanduser()
|
|
40
40
|
|
|
@@ -308,7 +308,7 @@ def main() -> int:
|
|
|
308
308
|
"input_data": input_data,
|
|
309
309
|
"source": "claude", # Required: identifies CLI source
|
|
310
310
|
},
|
|
311
|
-
timeout=
|
|
311
|
+
timeout=90.0, # LLM-powered hooks (pre-compact summary) need more time
|
|
312
312
|
)
|
|
313
313
|
|
|
314
314
|
if response.status_code == 200:
|