gobby 0.2.5__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 +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Hook Dispatcher - Routes Claude Code hooks to HookManager.
|
|
3
|
+
|
|
4
|
+
This is a thin wrapper script that receives hook calls from Claude Code
|
|
5
|
+
and routes them to the appropriate handler via HookManager.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
hook_dispatcher.py --type session-start < input.json > output.json
|
|
9
|
+
hook_dispatcher.py --type pre-tool-use --debug < input.json > output.json
|
|
10
|
+
|
|
11
|
+
Exit Codes:
|
|
12
|
+
0 - Success
|
|
13
|
+
1 - General error (logged, continues)
|
|
14
|
+
2 - Invalid input (argument parsing or JSON)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
# No longer need to import HookManager - we call it via HTTP daemon instead
|
|
24
|
+
|
|
25
|
+
# Default daemon configuration
|
|
26
|
+
DEFAULT_DAEMON_PORT = 8765
|
|
27
|
+
DEFAULT_CONFIG_PATH = "~/.gobby/config.yaml"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_daemon_url() -> str:
|
|
31
|
+
"""Get the daemon HTTP URL from config file.
|
|
32
|
+
|
|
33
|
+
Reads daemon_port from ~/.gobby/config.yaml if it exists,
|
|
34
|
+
otherwise uses the default port 8765.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Full daemon URL like http://localhost:8765
|
|
38
|
+
"""
|
|
39
|
+
config_path = Path(DEFAULT_CONFIG_PATH).expanduser()
|
|
40
|
+
|
|
41
|
+
if config_path.exists():
|
|
42
|
+
try:
|
|
43
|
+
import yaml
|
|
44
|
+
|
|
45
|
+
with open(config_path) as f:
|
|
46
|
+
config = yaml.safe_load(f) or {}
|
|
47
|
+
port = config.get("daemon_port", DEFAULT_DAEMON_PORT)
|
|
48
|
+
except Exception:
|
|
49
|
+
# If config read fails, use default
|
|
50
|
+
port = DEFAULT_DAEMON_PORT
|
|
51
|
+
else:
|
|
52
|
+
port = DEFAULT_DAEMON_PORT
|
|
53
|
+
|
|
54
|
+
return f"http://localhost:{port}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_terminal_context() -> dict[str, str | int | None]:
|
|
58
|
+
"""Capture terminal/process context for session correlation.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dict with terminal identifiers (values may be None if unavailable)
|
|
62
|
+
"""
|
|
63
|
+
context: dict[str, str | int | None] = {}
|
|
64
|
+
|
|
65
|
+
# Parent process ID (shell or Claude process)
|
|
66
|
+
try:
|
|
67
|
+
context["parent_pid"] = os.getppid()
|
|
68
|
+
except Exception:
|
|
69
|
+
context["parent_pid"] = None
|
|
70
|
+
|
|
71
|
+
# TTY device name
|
|
72
|
+
try:
|
|
73
|
+
context["tty"] = os.ttyname(0)
|
|
74
|
+
except Exception:
|
|
75
|
+
context["tty"] = None
|
|
76
|
+
|
|
77
|
+
# macOS Terminal.app session ID
|
|
78
|
+
context["term_session_id"] = os.environ.get("TERM_SESSION_ID")
|
|
79
|
+
|
|
80
|
+
# iTerm2 session ID
|
|
81
|
+
context["iterm_session_id"] = os.environ.get("ITERM_SESSION_ID")
|
|
82
|
+
|
|
83
|
+
# VS Code terminal ID (if running in VS Code integrated terminal)
|
|
84
|
+
context["vscode_terminal_id"] = os.environ.get("VSCODE_GIT_ASKPASS_NODE")
|
|
85
|
+
|
|
86
|
+
# Tmux pane (if running in tmux)
|
|
87
|
+
context["tmux_pane"] = os.environ.get("TMUX_PANE")
|
|
88
|
+
|
|
89
|
+
# Kitty terminal window ID
|
|
90
|
+
context["kitty_window_id"] = os.environ.get("KITTY_WINDOW_ID")
|
|
91
|
+
|
|
92
|
+
# Alacritty IPC socket path (unique per instance)
|
|
93
|
+
context["alacritty_socket"] = os.environ.get("ALACRITTY_SOCKET")
|
|
94
|
+
|
|
95
|
+
# Generic terminal program identifier (set by many terminals)
|
|
96
|
+
context["term_program"] = os.environ.get("TERM_PROGRAM")
|
|
97
|
+
|
|
98
|
+
return context
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def parse_arguments() -> argparse.Namespace:
|
|
102
|
+
"""Parse command line arguments.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Parsed arguments with type and debug flags
|
|
106
|
+
"""
|
|
107
|
+
parser = argparse.ArgumentParser(description="Claude Code Hook Dispatcher")
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"--type",
|
|
110
|
+
required=True,
|
|
111
|
+
help="Hook type (e.g., session-start, pre-tool-use)",
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--debug",
|
|
115
|
+
action="store_true",
|
|
116
|
+
help="Enable debug logging",
|
|
117
|
+
)
|
|
118
|
+
return parser.parse_args()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def check_daemon_running(timeout: float = 0.5) -> bool:
|
|
122
|
+
"""Check if gobby daemon is active and responding.
|
|
123
|
+
|
|
124
|
+
Performs a quick health check to verify the HTTP server is running
|
|
125
|
+
before processing hooks. This prevents hook execution when the daemon
|
|
126
|
+
is stopped, avoiding long timeouts and confusing error messages.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
timeout: Maximum time to wait for response in seconds (default: 0.5)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
True if client is running and responding, False otherwise
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
import httpx
|
|
136
|
+
|
|
137
|
+
daemon_url = get_daemon_url()
|
|
138
|
+
response = httpx.get(
|
|
139
|
+
f"{daemon_url}/admin/status",
|
|
140
|
+
timeout=timeout,
|
|
141
|
+
follow_redirects=False,
|
|
142
|
+
)
|
|
143
|
+
return response.status_code == 200
|
|
144
|
+
except Exception:
|
|
145
|
+
# Any error (connection refused, timeout, etc.) means client is not running
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def main() -> int:
|
|
150
|
+
"""Main dispatcher execution.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Exit code (0=success, 1=error, 2=invalid input)
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
# Parse arguments
|
|
157
|
+
args = parse_arguments()
|
|
158
|
+
except (argparse.ArgumentError, SystemExit):
|
|
159
|
+
# Argument parsing failed - return empty dict and exit 2
|
|
160
|
+
print(json.dumps({}))
|
|
161
|
+
return 2
|
|
162
|
+
|
|
163
|
+
hook_type = args.type
|
|
164
|
+
debug_mode = args.debug
|
|
165
|
+
|
|
166
|
+
# Check if gobby daemon is running before processing hooks
|
|
167
|
+
if not check_daemon_running():
|
|
168
|
+
# Critical hooks that manage session state MUST have daemon running
|
|
169
|
+
# Without daemon, we lose handoff context, session tracking, etc.
|
|
170
|
+
critical_hooks = {"session-start", "session-end", "pre-compact"}
|
|
171
|
+
if hook_type in critical_hooks:
|
|
172
|
+
# Block the hook - forces user to start daemon before critical lifecycle events
|
|
173
|
+
print(
|
|
174
|
+
f"Gobby daemon is not running. Start with 'gobby start' before continuing. "
|
|
175
|
+
f"({hook_type} requires daemon for session state management)",
|
|
176
|
+
file=sys.stderr,
|
|
177
|
+
)
|
|
178
|
+
return 2 # Exit 2 = block operation
|
|
179
|
+
else:
|
|
180
|
+
# Non-critical hooks can proceed without daemon (tool use, notifications, etc.)
|
|
181
|
+
print(
|
|
182
|
+
json.dumps(
|
|
183
|
+
{"status": "daemon_not_running", "message": "gobby daemon is not running"}
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
return 0 # Exit 0 (success) - allow operation to continue
|
|
187
|
+
|
|
188
|
+
# Setup logger for dispatcher (not HookManager)
|
|
189
|
+
# Only log to stderr in debug mode - otherwise logs pollute Claude's stderr reading
|
|
190
|
+
import logging
|
|
191
|
+
|
|
192
|
+
logger = logging.getLogger("gobby.hooks.dispatcher")
|
|
193
|
+
if debug_mode:
|
|
194
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
195
|
+
else:
|
|
196
|
+
# In non-debug mode, suppress all logging to stderr
|
|
197
|
+
logging.basicConfig(level=logging.WARNING, handlers=[])
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
# Read JSON input from stdin
|
|
201
|
+
input_data = json.load(sys.stdin)
|
|
202
|
+
|
|
203
|
+
# Inject terminal context for session-start hooks
|
|
204
|
+
# This captures the terminal/process info for session correlation
|
|
205
|
+
if hook_type == "session-start":
|
|
206
|
+
input_data["terminal_context"] = get_terminal_context()
|
|
207
|
+
|
|
208
|
+
# ALWAYS log what Claude Code sends us (for debugging hook data issues)
|
|
209
|
+
logger.info(f"[{hook_type}] Received input keys: {list(input_data.keys())}")
|
|
210
|
+
|
|
211
|
+
# Log hook-specific critical fields (based on Claude Code SDK documentation)
|
|
212
|
+
if hook_type == "session-start":
|
|
213
|
+
logger.info(
|
|
214
|
+
f"[session-start] session_id={input_data.get('session_id')}, "
|
|
215
|
+
f"source={input_data.get('source')}"
|
|
216
|
+
)
|
|
217
|
+
elif hook_type == "session-end":
|
|
218
|
+
logger.info(
|
|
219
|
+
f"[session-end] session_id={input_data.get('session_id')}, "
|
|
220
|
+
f"reason={input_data.get('reason')}"
|
|
221
|
+
)
|
|
222
|
+
elif hook_type == "user-prompt-submit":
|
|
223
|
+
prompt = input_data.get("prompt", "")
|
|
224
|
+
prompt_preview = prompt[:100] + "..." if len(prompt) > 100 else prompt
|
|
225
|
+
logger.info(
|
|
226
|
+
f"[user-prompt-submit] session_id={input_data.get('session_id')}, "
|
|
227
|
+
f"prompt={prompt_preview}"
|
|
228
|
+
)
|
|
229
|
+
elif hook_type == "pre-tool-use":
|
|
230
|
+
tool_input = input_data.get("tool_input", {})
|
|
231
|
+
# Truncate large values for readability (keep first 200 chars)
|
|
232
|
+
tool_input_preview = {
|
|
233
|
+
k: (v[:200] + "..." if isinstance(v, str) and len(v) > 200 else v)
|
|
234
|
+
for k, v in tool_input.items()
|
|
235
|
+
}
|
|
236
|
+
logger.info(
|
|
237
|
+
f"[pre-tool-use] tool_name={input_data.get('tool_name')}, "
|
|
238
|
+
f"tool_input={tool_input_preview}, "
|
|
239
|
+
f"session_id={input_data.get('session_id')}"
|
|
240
|
+
)
|
|
241
|
+
elif hook_type == "post-tool-use":
|
|
242
|
+
logger.info(
|
|
243
|
+
f"[post-tool-use] tool_name={input_data.get('tool_name')}, "
|
|
244
|
+
f"has_tool_response={bool(input_data.get('tool_response'))}, "
|
|
245
|
+
f"has_tool_input={bool(input_data.get('tool_input'))}, "
|
|
246
|
+
f"session_id={input_data.get('session_id')}"
|
|
247
|
+
)
|
|
248
|
+
elif hook_type == "pre-compact":
|
|
249
|
+
logger.info(
|
|
250
|
+
f"[pre-compact] session_id={input_data.get('session_id')}, "
|
|
251
|
+
f"trigger={input_data.get('trigger')}, "
|
|
252
|
+
f"has_custom_instructions={bool(input_data.get('custom_instructions'))}"
|
|
253
|
+
)
|
|
254
|
+
elif hook_type == "stop":
|
|
255
|
+
logger.info(
|
|
256
|
+
f"[stop] session_id={input_data.get('session_id')}, "
|
|
257
|
+
f"stop_hook_active={input_data.get('stop_hook_active')}"
|
|
258
|
+
)
|
|
259
|
+
elif hook_type == "subagent-start":
|
|
260
|
+
logger.info(
|
|
261
|
+
f"[subagent-start] session_id={input_data.get('session_id')}, "
|
|
262
|
+
f"agent_id={input_data.get('agent_id')}, "
|
|
263
|
+
f"subagent_id={input_data.get('subagent_id')}"
|
|
264
|
+
)
|
|
265
|
+
elif hook_type == "subagent-stop":
|
|
266
|
+
logger.info(
|
|
267
|
+
f"[subagent-stop] session_id={input_data.get('session_id')}, "
|
|
268
|
+
f"agent_id={input_data.get('agent_id')}, "
|
|
269
|
+
f"subagent_id={input_data.get('subagent_id')}"
|
|
270
|
+
)
|
|
271
|
+
elif hook_type == "notification":
|
|
272
|
+
logger.info(
|
|
273
|
+
f"[notification] session_id={input_data.get('session_id')}, "
|
|
274
|
+
f"message={input_data.get('message')}, "
|
|
275
|
+
f"title={input_data.get('title', 'N/A')}"
|
|
276
|
+
)
|
|
277
|
+
elif hook_type == "permission-request":
|
|
278
|
+
tool_input = input_data.get("tool_input", {})
|
|
279
|
+
tool_input_preview = {
|
|
280
|
+
k: (v[:200] + "..." if isinstance(v, str) and len(v) > 200 else v)
|
|
281
|
+
for k, v in tool_input.items()
|
|
282
|
+
}
|
|
283
|
+
logger.info(
|
|
284
|
+
f"[permission-request] tool_name={input_data.get('tool_name')}, "
|
|
285
|
+
f"tool_input={tool_input_preview}, "
|
|
286
|
+
f"session_id={input_data.get('session_id')}"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if debug_mode:
|
|
290
|
+
logger.debug(f"Input data: {input_data}")
|
|
291
|
+
|
|
292
|
+
except json.JSONDecodeError as e:
|
|
293
|
+
# Invalid JSON input - return empty dict and exit 2
|
|
294
|
+
if debug_mode:
|
|
295
|
+
logger.error(f"JSON decode error: {e}")
|
|
296
|
+
print(json.dumps({}))
|
|
297
|
+
return 2
|
|
298
|
+
|
|
299
|
+
# Call daemon HTTP endpoint instead of creating HookManager
|
|
300
|
+
import httpx
|
|
301
|
+
|
|
302
|
+
daemon_url = get_daemon_url()
|
|
303
|
+
try:
|
|
304
|
+
response = httpx.post(
|
|
305
|
+
f"{daemon_url}/hooks/execute",
|
|
306
|
+
json={
|
|
307
|
+
"hook_type": hook_type,
|
|
308
|
+
"input_data": input_data,
|
|
309
|
+
"source": "claude", # Required: identifies CLI source
|
|
310
|
+
},
|
|
311
|
+
timeout=30.0, # Generous timeout for hook processing
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if response.status_code == 200:
|
|
315
|
+
# Success - daemon returns result directly (not wrapped)
|
|
316
|
+
result = response.json()
|
|
317
|
+
|
|
318
|
+
if debug_mode:
|
|
319
|
+
logger.debug(f"Output data: {result}")
|
|
320
|
+
|
|
321
|
+
# Check for block decision - return exit code 2 to signal blocking
|
|
322
|
+
# For blocking, output goes to STDERR (Claude reads stderr on exit 2)
|
|
323
|
+
if result.get("continue") is False or result.get("decision") == "block":
|
|
324
|
+
# Output just the reason, not the full JSON
|
|
325
|
+
reason = result.get("stopReason") or result.get("reason") or "Blocked by hook"
|
|
326
|
+
print(reason, file=sys.stderr)
|
|
327
|
+
return 2
|
|
328
|
+
|
|
329
|
+
# Only print output if there's something meaningful to show
|
|
330
|
+
# Empty dicts cause Claude Code to show "hook success: Success"
|
|
331
|
+
if result and result != {}:
|
|
332
|
+
print(json.dumps(result))
|
|
333
|
+
|
|
334
|
+
return 0
|
|
335
|
+
else:
|
|
336
|
+
# HTTP error from daemon
|
|
337
|
+
error_detail = response.text
|
|
338
|
+
logger.error(
|
|
339
|
+
f"Daemon returned error: status={response.status_code}, detail={error_detail}"
|
|
340
|
+
)
|
|
341
|
+
print(json.dumps({"status": "error", "message": f"Daemon error: {error_detail}"}))
|
|
342
|
+
return 1
|
|
343
|
+
|
|
344
|
+
except httpx.ConnectError:
|
|
345
|
+
# Daemon not reachable - this shouldn't happen since we checked, but handle gracefully
|
|
346
|
+
logger.error("Failed to connect to daemon (unreachable)")
|
|
347
|
+
print(json.dumps({"status": "error", "message": "Daemon unreachable"}))
|
|
348
|
+
return 1
|
|
349
|
+
|
|
350
|
+
except httpx.TimeoutException:
|
|
351
|
+
# Hook processing took too long
|
|
352
|
+
logger.error(f"Hook execution timeout: {hook_type}")
|
|
353
|
+
print(json.dumps({"status": "error", "message": "Hook execution timeout"}))
|
|
354
|
+
return 1
|
|
355
|
+
|
|
356
|
+
except Exception as e:
|
|
357
|
+
# General error - log and return 1
|
|
358
|
+
logger.error(f"Hook execution failed: {e}", exc_info=True)
|
|
359
|
+
print(json.dumps({"status": "error", "message": str(e)}))
|
|
360
|
+
return 1
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
if __name__ == "__main__":
|
|
364
|
+
sys.exit(main())
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate .claude/settings.json configuration.
|
|
3
|
+
|
|
4
|
+
This script validates:
|
|
5
|
+
- JSON syntax correctness
|
|
6
|
+
- Hook structure and dispatcher commands
|
|
7
|
+
- All required hook types are configured
|
|
8
|
+
- Dispatcher script exists and is executable
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> int:
|
|
17
|
+
"""Validate settings.json configuration.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
0 if valid, 1 if invalid
|
|
21
|
+
"""
|
|
22
|
+
# Find settings.json
|
|
23
|
+
claude_dir = Path(__file__).parent.parent
|
|
24
|
+
settings_file = claude_dir / "settings.json"
|
|
25
|
+
|
|
26
|
+
if not settings_file.exists():
|
|
27
|
+
print(f"❌ Settings file not found: {settings_file}")
|
|
28
|
+
return 1
|
|
29
|
+
|
|
30
|
+
# Validate JSON syntax
|
|
31
|
+
try:
|
|
32
|
+
with open(settings_file) as f:
|
|
33
|
+
settings = json.load(f)
|
|
34
|
+
except json.JSONDecodeError as e:
|
|
35
|
+
print(f"❌ Invalid JSON syntax: {e}")
|
|
36
|
+
return 1
|
|
37
|
+
|
|
38
|
+
print("✅ JSON syntax is valid")
|
|
39
|
+
|
|
40
|
+
# Check hooks section exists
|
|
41
|
+
if "hooks" not in settings:
|
|
42
|
+
print("❌ No 'hooks' section found in settings")
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
hooks = settings["hooks"]
|
|
46
|
+
print("✅ Hooks section found")
|
|
47
|
+
|
|
48
|
+
# Required hook types
|
|
49
|
+
required_hooks = [
|
|
50
|
+
"SessionStart",
|
|
51
|
+
"SessionEnd",
|
|
52
|
+
"UserPromptSubmit",
|
|
53
|
+
"PreToolUse",
|
|
54
|
+
"PostToolUse",
|
|
55
|
+
"PreCompact",
|
|
56
|
+
"Notification",
|
|
57
|
+
"Stop",
|
|
58
|
+
"SubagentStart",
|
|
59
|
+
"SubagentStop",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
# Validate each required hook
|
|
63
|
+
for hook_type in required_hooks:
|
|
64
|
+
if hook_type not in hooks:
|
|
65
|
+
print(f"❌ Missing hook type: {hook_type}")
|
|
66
|
+
return 1
|
|
67
|
+
|
|
68
|
+
hook_configs = hooks[hook_type]
|
|
69
|
+
if not isinstance(hook_configs, list) or not hook_configs:
|
|
70
|
+
print(f"❌ Invalid hook configuration for: {hook_type}")
|
|
71
|
+
return 1
|
|
72
|
+
|
|
73
|
+
# Check first configuration
|
|
74
|
+
config = hook_configs[0]
|
|
75
|
+
if "hooks" not in config:
|
|
76
|
+
print(f"❌ No 'hooks' array in {hook_type} configuration")
|
|
77
|
+
return 1
|
|
78
|
+
|
|
79
|
+
# Check command uses dispatcher
|
|
80
|
+
command = config["hooks"][0].get("command", "")
|
|
81
|
+
if "hook_dispatcher.py" not in command:
|
|
82
|
+
print(f"⚠️ Warning: {hook_type} not using dispatcher pattern")
|
|
83
|
+
|
|
84
|
+
print(f"✅ All {len(required_hooks)} required hook types configured")
|
|
85
|
+
|
|
86
|
+
# Validate dispatcher exists
|
|
87
|
+
dispatcher = claude_dir / "hooks" / "hook_dispatcher.py"
|
|
88
|
+
if not dispatcher.exists():
|
|
89
|
+
print(f"❌ Dispatcher not found: {dispatcher}")
|
|
90
|
+
return 1
|
|
91
|
+
|
|
92
|
+
print("✅ Dispatcher script exists")
|
|
93
|
+
|
|
94
|
+
if not dispatcher.stat().st_mode & 0o111:
|
|
95
|
+
print("⚠️ Warning: Dispatcher is not executable")
|
|
96
|
+
|
|
97
|
+
print("\n✅ All validations passed!")
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
sys.exit(main())
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"allowedTools": [],
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=session-start"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"SessionEnd": [
|
|
15
|
+
{
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=session-end"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"UserPromptSubmit": [
|
|
25
|
+
{
|
|
26
|
+
"hooks": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=user-prompt-submit"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"PreToolUse": [
|
|
35
|
+
{
|
|
36
|
+
"matcher": "*",
|
|
37
|
+
"hooks": [
|
|
38
|
+
{
|
|
39
|
+
"type": "command",
|
|
40
|
+
"command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=pre-tool-use"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"PostToolUse": [
|
|
46
|
+
{
|
|
47
|
+
"matcher": "*",
|
|
48
|
+
"hooks": [
|
|
49
|
+
{
|
|
50
|
+
"type": "command",
|
|
51
|
+
"command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=post-tool-use"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"PreCompact": [
|
|
57
|
+
{
|
|
58
|
+
"hooks": [
|
|
59
|
+
{
|
|
60
|
+
"type": "command",
|
|
61
|
+
"command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=pre-compact"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"Notification": [
|
|
67
|
+
{
|
|
68
|
+
"hooks": [
|
|
69
|
+
{
|
|
70
|
+
"type": "command",
|
|
71
|
+
"command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=notification"
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"Stop": [
|
|
77
|
+
{
|
|
78
|
+
"hooks": [
|
|
79
|
+
{
|
|
80
|
+
"type": "command",
|
|
81
|
+
"command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=stop"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"SubagentStart": [
|
|
87
|
+
{
|
|
88
|
+
"hooks": [
|
|
89
|
+
{
|
|
90
|
+
"type": "command",
|
|
91
|
+
"command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=subagent-start"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
"SubagentStop": [
|
|
97
|
+
{
|
|
98
|
+
"hooks": [
|
|
99
|
+
{
|
|
100
|
+
"type": "command",
|
|
101
|
+
"command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=subagent-stop"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"PermissionRequest": [
|
|
107
|
+
{
|
|
108
|
+
"matcher": "*",
|
|
109
|
+
"hooks": [
|
|
110
|
+
{
|
|
111
|
+
"type": "command",
|
|
112
|
+
"command": "uv run python \"$PROJECT_PATH/.claude/hooks/hook_dispatcher.py\" --type=permission-request"
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
}
|