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,396 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session coordinator module for session lifecycle management.
|
|
3
|
+
|
|
4
|
+
This module is extracted from hook_manager.py using Strangler Fig pattern.
|
|
5
|
+
It provides centralized session registration tracking, message caching,
|
|
6
|
+
and lifecycle coordination.
|
|
7
|
+
|
|
8
|
+
Classes:
|
|
9
|
+
SessionCoordinator: Coordinates session lifecycle operations.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from gobby.storage.agents import LocalAgentRunManager
|
|
21
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
22
|
+
from gobby.storage.worktrees import LocalWorktreeManager
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SessionCoordinator:
|
|
26
|
+
"""
|
|
27
|
+
Coordinates session lifecycle operations.
|
|
28
|
+
|
|
29
|
+
Provides centralized tracking for:
|
|
30
|
+
- Session registration with daemon
|
|
31
|
+
- Title synthesis status
|
|
32
|
+
- Agent message caching between hooks
|
|
33
|
+
- Session lifecycle transitions (completion, cleanup)
|
|
34
|
+
|
|
35
|
+
Thread-safe for concurrent operations.
|
|
36
|
+
|
|
37
|
+
Extracted from HookManager to separate session coordination concerns.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
session_storage: LocalSessionManager | None = None,
|
|
43
|
+
message_processor: Any | None = None,
|
|
44
|
+
agent_run_manager: LocalAgentRunManager | None = None,
|
|
45
|
+
worktree_manager: LocalWorktreeManager | None = None,
|
|
46
|
+
logger: logging.Logger | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Initialize SessionCoordinator.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
session_storage: LocalSessionManager for session queries
|
|
53
|
+
message_processor: SessionMessageProcessor for message registration
|
|
54
|
+
agent_run_manager: LocalAgentRunManager for agent run completion
|
|
55
|
+
worktree_manager: LocalWorktreeManager for worktree release
|
|
56
|
+
logger: Optional logger instance
|
|
57
|
+
"""
|
|
58
|
+
self._session_storage = session_storage
|
|
59
|
+
self._message_processor = message_processor
|
|
60
|
+
self._agent_run_manager = agent_run_manager
|
|
61
|
+
self._worktree_manager = worktree_manager
|
|
62
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
63
|
+
|
|
64
|
+
# Session registration tracking (to avoid noisy logs)
|
|
65
|
+
# Tracks which sessions have been registered with daemon
|
|
66
|
+
self._registered_sessions: set[str] = set()
|
|
67
|
+
self._registered_sessions_lock = threading.Lock()
|
|
68
|
+
|
|
69
|
+
# Session title synthesis tracking
|
|
70
|
+
# Tracks which sessions have had titles synthesized
|
|
71
|
+
self._title_synthesized_sessions: set[str] = set()
|
|
72
|
+
self._title_synthesized_lock = threading.Lock()
|
|
73
|
+
|
|
74
|
+
# Agent message cache (session_id -> (message, timestamp))
|
|
75
|
+
# Used to pass agent responses from stop hook to post-tool-use hook
|
|
76
|
+
self._agent_message_cache: dict[str, tuple[str, float]] = {}
|
|
77
|
+
self._cache_lock = threading.Lock()
|
|
78
|
+
|
|
79
|
+
# Lock for session lookups to prevent race conditions (double firing)
|
|
80
|
+
self._lookup_lock = threading.Lock()
|
|
81
|
+
|
|
82
|
+
# ==================== REGISTRATION TRACKING ====================
|
|
83
|
+
|
|
84
|
+
def register_session(self, session_id: str) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Mark a session as registered with the daemon.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
session_id: The session ID to register
|
|
90
|
+
"""
|
|
91
|
+
with self._registered_sessions_lock:
|
|
92
|
+
self._registered_sessions.add(session_id)
|
|
93
|
+
|
|
94
|
+
def unregister_session(self, session_id: str) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Remove a session from registration tracking.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
session_id: The session ID to unregister
|
|
100
|
+
"""
|
|
101
|
+
with self._registered_sessions_lock:
|
|
102
|
+
self._registered_sessions.discard(session_id)
|
|
103
|
+
|
|
104
|
+
def is_registered(self, session_id: str) -> bool:
|
|
105
|
+
"""
|
|
106
|
+
Check if a session is registered with the daemon.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
session_id: The session ID to check
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
True if registered, False otherwise
|
|
113
|
+
"""
|
|
114
|
+
with self._registered_sessions_lock:
|
|
115
|
+
return session_id in self._registered_sessions
|
|
116
|
+
|
|
117
|
+
def clear_registrations(self) -> None:
|
|
118
|
+
"""Clear all session registrations."""
|
|
119
|
+
with self._registered_sessions_lock:
|
|
120
|
+
self._registered_sessions.clear()
|
|
121
|
+
|
|
122
|
+
# ==================== TITLE SYNTHESIS TRACKING ====================
|
|
123
|
+
|
|
124
|
+
def mark_title_synthesized(self, session_id: str) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Mark a session as having had its title synthesized.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
session_id: The session ID to mark
|
|
130
|
+
"""
|
|
131
|
+
with self._title_synthesized_lock:
|
|
132
|
+
self._title_synthesized_sessions.add(session_id)
|
|
133
|
+
|
|
134
|
+
def is_title_synthesized(self, session_id: str) -> bool:
|
|
135
|
+
"""
|
|
136
|
+
Check if a session has had its title synthesized.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
session_id: The session ID to check
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if title has been synthesized, False otherwise
|
|
143
|
+
"""
|
|
144
|
+
with self._title_synthesized_lock:
|
|
145
|
+
return session_id in self._title_synthesized_sessions
|
|
146
|
+
|
|
147
|
+
# ==================== MESSAGE CACHING ====================
|
|
148
|
+
|
|
149
|
+
def cache_agent_message(self, session_id: str, message: str) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Cache an agent message for later retrieval.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
session_id: The session ID
|
|
155
|
+
message: The message to cache
|
|
156
|
+
"""
|
|
157
|
+
with self._cache_lock:
|
|
158
|
+
self._agent_message_cache[session_id] = (message, time.time())
|
|
159
|
+
|
|
160
|
+
def get_cached_message(
|
|
161
|
+
self, session_id: str, max_age_seconds: float | None = None
|
|
162
|
+
) -> str | None:
|
|
163
|
+
"""
|
|
164
|
+
Get a cached agent message.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
session_id: The session ID
|
|
168
|
+
max_age_seconds: Optional maximum age in seconds. If set, returns None
|
|
169
|
+
for messages older than this.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The cached message, or None if not found or expired
|
|
173
|
+
"""
|
|
174
|
+
with self._cache_lock:
|
|
175
|
+
if session_id not in self._agent_message_cache:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
message, timestamp = self._agent_message_cache[session_id]
|
|
179
|
+
|
|
180
|
+
if max_age_seconds is not None:
|
|
181
|
+
age = time.time() - timestamp
|
|
182
|
+
if age > max_age_seconds:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
return message
|
|
186
|
+
|
|
187
|
+
def clear_cached_message(self, session_id: str) -> None:
|
|
188
|
+
"""
|
|
189
|
+
Clear a cached agent message.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
session_id: The session ID
|
|
193
|
+
"""
|
|
194
|
+
with self._cache_lock:
|
|
195
|
+
self._agent_message_cache.pop(session_id, None)
|
|
196
|
+
|
|
197
|
+
# ==================== LOOKUP LOCK ====================
|
|
198
|
+
|
|
199
|
+
def get_lookup_lock(self) -> threading.Lock:
|
|
200
|
+
"""
|
|
201
|
+
Get the lookup lock for preventing race conditions.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
The lookup lock
|
|
205
|
+
"""
|
|
206
|
+
return self._lookup_lock
|
|
207
|
+
|
|
208
|
+
# ==================== LIFECYCLE OPERATIONS ====================
|
|
209
|
+
|
|
210
|
+
def reregister_active_sessions(self, limit: int = 1000) -> int:
|
|
211
|
+
"""
|
|
212
|
+
Re-register active sessions with the message processor.
|
|
213
|
+
|
|
214
|
+
Called during initialization to restore message processing
|
|
215
|
+
for sessions that were active before a daemon restart.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
limit: Maximum number of sessions to re-register (default 1000).
|
|
219
|
+
Sessions beyond this limit will not be re-registered.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Number of sessions successfully re-registered
|
|
223
|
+
"""
|
|
224
|
+
if not self._message_processor or not self._session_storage:
|
|
225
|
+
return 0
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
# Query active sessions from storage
|
|
229
|
+
active_sessions = self._session_storage.list(status="active", limit=limit)
|
|
230
|
+
registered_count = 0
|
|
231
|
+
|
|
232
|
+
for session in active_sessions:
|
|
233
|
+
jsonl_path = getattr(session, "jsonl_path", None)
|
|
234
|
+
if not jsonl_path:
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
# Determine source from session (default to claude)
|
|
239
|
+
source = getattr(session, "source", "claude") or "claude"
|
|
240
|
+
self._message_processor.register_session(session.id, jsonl_path, source=source)
|
|
241
|
+
registered_count += 1
|
|
242
|
+
except Exception as e:
|
|
243
|
+
self.logger.warning(f"Failed to re-register session {session.id}: {e}")
|
|
244
|
+
|
|
245
|
+
if registered_count > 0:
|
|
246
|
+
self.logger.info(
|
|
247
|
+
f"Re-registered {registered_count} active sessions with message processor"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
return registered_count
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
self.logger.warning(f"Failed to re-register active sessions: {e}")
|
|
254
|
+
return 0
|
|
255
|
+
|
|
256
|
+
def start_agent_run(self, agent_run_id: str) -> bool:
|
|
257
|
+
"""
|
|
258
|
+
Mark an agent run as started when its terminal-mode session begins.
|
|
259
|
+
|
|
260
|
+
Called from handle_session_start when a pre-created session with an
|
|
261
|
+
agent_run_id is detected. This updates the status from 'pending' to
|
|
262
|
+
'running' and sets the started_at timestamp.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
agent_run_id: The agent run ID to start
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
True if the run was started, False otherwise
|
|
269
|
+
"""
|
|
270
|
+
if not self._agent_run_manager:
|
|
271
|
+
self.logger.debug("start_agent_run: No agent_run_manager, skipping")
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
agent_run = self._agent_run_manager.get(agent_run_id)
|
|
276
|
+
if not agent_run:
|
|
277
|
+
self.logger.warning(f"Agent run {agent_run_id} not found")
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
# Only start if currently pending
|
|
281
|
+
if agent_run.status != "pending":
|
|
282
|
+
self.logger.debug(
|
|
283
|
+
f"Agent run {agent_run_id} not pending (status={agent_run.status}), skipping start"
|
|
284
|
+
)
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
self._agent_run_manager.start(agent_run_id)
|
|
288
|
+
self.logger.info(f"Started agent run {agent_run_id}")
|
|
289
|
+
return True
|
|
290
|
+
|
|
291
|
+
except Exception as e:
|
|
292
|
+
self.logger.error(f"Failed to start agent run {agent_run_id}: {e}")
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
def complete_agent_run(self, session: Any) -> None:
|
|
296
|
+
"""
|
|
297
|
+
Complete an agent run when its terminal-mode session ends.
|
|
298
|
+
|
|
299
|
+
Updates the agent run status based on session outcome, removes the
|
|
300
|
+
agent from the in-memory running registry, and releases any worktrees
|
|
301
|
+
associated with the session.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
session: Session object with agent_run_id
|
|
305
|
+
"""
|
|
306
|
+
# Check for agent_run_id
|
|
307
|
+
agent_run_id = getattr(session, "agent_run_id", None)
|
|
308
|
+
if not agent_run_id:
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
self.logger.debug(f"Completing agent run {agent_run_id} for session {session.id}")
|
|
312
|
+
|
|
313
|
+
# Remove from in-memory running agents registry
|
|
314
|
+
try:
|
|
315
|
+
from gobby.agents.registry import get_running_agent_registry
|
|
316
|
+
|
|
317
|
+
running_registry = get_running_agent_registry()
|
|
318
|
+
removed = running_registry.remove(agent_run_id)
|
|
319
|
+
if removed:
|
|
320
|
+
self.logger.debug(f"Unregistered running agent {agent_run_id} from registry")
|
|
321
|
+
except Exception as e:
|
|
322
|
+
self.logger.warning(f"Failed to unregister agent from running registry: {e}")
|
|
323
|
+
|
|
324
|
+
if not self._agent_run_manager:
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
agent_run = self._agent_run_manager.get(agent_run_id)
|
|
329
|
+
if not agent_run:
|
|
330
|
+
self.logger.warning(f"Agent run {agent_run_id} not found")
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
# Skip if already completed
|
|
334
|
+
if agent_run.status in ("success", "error", "timeout", "cancelled"):
|
|
335
|
+
self.logger.debug(
|
|
336
|
+
f"Agent run {agent_run_id} already in terminal state: {agent_run.status}"
|
|
337
|
+
)
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
# Use summary as result if available
|
|
341
|
+
result = (
|
|
342
|
+
getattr(session, "summary_markdown", None)
|
|
343
|
+
or getattr(session, "compact_markdown", None)
|
|
344
|
+
or ""
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Mark as success
|
|
348
|
+
self._agent_run_manager.complete(
|
|
349
|
+
run_id=agent_run_id,
|
|
350
|
+
result=result,
|
|
351
|
+
tool_calls_count=0,
|
|
352
|
+
turns_used=0,
|
|
353
|
+
)
|
|
354
|
+
self.logger.info(f"Completed agent run {agent_run_id}")
|
|
355
|
+
|
|
356
|
+
except Exception as e:
|
|
357
|
+
self.logger.error(f"Failed to complete agent run {agent_run_id}: {e}")
|
|
358
|
+
|
|
359
|
+
# Release any worktrees associated with this session
|
|
360
|
+
try:
|
|
361
|
+
self.release_session_worktrees(session.id)
|
|
362
|
+
except Exception as e:
|
|
363
|
+
self.logger.warning(f"Failed to release worktrees for session {session.id}: {e}")
|
|
364
|
+
|
|
365
|
+
def release_session_worktrees(self, session_id: str) -> None:
|
|
366
|
+
"""
|
|
367
|
+
Release all worktrees claimed by a session.
|
|
368
|
+
|
|
369
|
+
When a session ends, any worktrees it claimed should be released
|
|
370
|
+
so they can be reused by other sessions.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
session_id: The session ID whose worktrees to release
|
|
374
|
+
"""
|
|
375
|
+
if not self._worktree_manager:
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
# Find worktrees owned by this session
|
|
380
|
+
worktrees = self._worktree_manager.list_worktrees(agent_session_id=session_id)
|
|
381
|
+
|
|
382
|
+
for worktree in worktrees:
|
|
383
|
+
try:
|
|
384
|
+
# Release the worktree (sets agent_session_id to NULL)
|
|
385
|
+
self._worktree_manager.release(worktree.id)
|
|
386
|
+
self.logger.debug(f"Released worktree {worktree.id} from session {session_id}")
|
|
387
|
+
except Exception as e:
|
|
388
|
+
self.logger.warning(f"Failed to release worktree {worktree.id}: {e}")
|
|
389
|
+
|
|
390
|
+
if worktrees:
|
|
391
|
+
self.logger.info(f"Released {len(worktrees)} worktree(s) from session {session_id}")
|
|
392
|
+
except Exception as e:
|
|
393
|
+
self.logger.warning(f"Failed to list worktrees for session {session_id}: {e}")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
__all__ = ["SessionCoordinator"]
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Verification runner for git hooks.
|
|
2
|
+
|
|
3
|
+
Executes configured verification commands (lint, typecheck, tests, etc.) for git hook stages.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import subprocess # nosec B404 - subprocess needed for verification commands
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from gobby.config.features import HooksConfig, HookStageConfig, ProjectVerificationConfig
|
|
13
|
+
from gobby.utils.project_context import get_hooks_config, get_verification_config
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# Default timeout for verification commands (5 minutes)
|
|
18
|
+
DEFAULT_TIMEOUT = 300
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class VerificationResult:
|
|
23
|
+
"""Result of running a single verification command."""
|
|
24
|
+
|
|
25
|
+
name: str
|
|
26
|
+
command: str
|
|
27
|
+
success: bool
|
|
28
|
+
exit_code: int | None = None
|
|
29
|
+
stdout: str = ""
|
|
30
|
+
stderr: str = ""
|
|
31
|
+
duration_ms: int = 0
|
|
32
|
+
skipped: bool = False
|
|
33
|
+
skip_reason: str | None = None
|
|
34
|
+
error: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class StageResult:
|
|
39
|
+
"""Result of running all verification commands for a hook stage."""
|
|
40
|
+
|
|
41
|
+
stage: str
|
|
42
|
+
success: bool
|
|
43
|
+
results: list[VerificationResult] = field(default_factory=list)
|
|
44
|
+
skipped: bool = False
|
|
45
|
+
skip_reason: str | None = None
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def failed_count(self) -> int:
|
|
49
|
+
"""Number of failed verifications."""
|
|
50
|
+
return sum(1 for r in self.results if not r.success and not r.skipped)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def passed_count(self) -> int:
|
|
54
|
+
"""Number of passed verifications."""
|
|
55
|
+
return sum(1 for r in self.results if r.success)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def skipped_count(self) -> int:
|
|
59
|
+
"""Number of skipped verifications."""
|
|
60
|
+
return sum(1 for r in self.results if r.skipped)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run_command(
|
|
64
|
+
name: str,
|
|
65
|
+
command: str,
|
|
66
|
+
cwd: Path,
|
|
67
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
68
|
+
) -> VerificationResult:
|
|
69
|
+
"""Execute a verification command and return the result.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name: Name of the verification (e.g., 'lint', 'unit_tests').
|
|
73
|
+
command: The command to execute.
|
|
74
|
+
cwd: Working directory for the command.
|
|
75
|
+
timeout: Maximum execution time in seconds.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
VerificationResult with command output and status.
|
|
79
|
+
"""
|
|
80
|
+
start_time = time.time()
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
result = subprocess.run(
|
|
84
|
+
command,
|
|
85
|
+
shell=True, # nosec B602 - user-configured verification commands require shell features
|
|
86
|
+
capture_output=True,
|
|
87
|
+
text=True,
|
|
88
|
+
timeout=timeout,
|
|
89
|
+
cwd=cwd,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
93
|
+
|
|
94
|
+
return VerificationResult(
|
|
95
|
+
name=name,
|
|
96
|
+
command=command,
|
|
97
|
+
success=result.returncode == 0,
|
|
98
|
+
exit_code=result.returncode,
|
|
99
|
+
stdout=result.stdout,
|
|
100
|
+
stderr=result.stderr,
|
|
101
|
+
duration_ms=duration_ms,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
except subprocess.TimeoutExpired:
|
|
105
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
106
|
+
return VerificationResult(
|
|
107
|
+
name=name,
|
|
108
|
+
command=command,
|
|
109
|
+
success=False,
|
|
110
|
+
duration_ms=duration_ms,
|
|
111
|
+
error=f"Command timed out after {timeout} seconds",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
116
|
+
return VerificationResult(
|
|
117
|
+
name=name,
|
|
118
|
+
command=command,
|
|
119
|
+
success=False,
|
|
120
|
+
duration_ms=duration_ms,
|
|
121
|
+
error=str(e),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class VerificationRunner:
|
|
126
|
+
"""Runs verification commands for git hooks.
|
|
127
|
+
|
|
128
|
+
Reads configuration from project.json and executes the appropriate
|
|
129
|
+
commands for each hook stage (pre-commit, pre-push, pre-merge).
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def __init__(
|
|
133
|
+
self,
|
|
134
|
+
verification_config: ProjectVerificationConfig | None = None,
|
|
135
|
+
hooks_config: HooksConfig | None = None,
|
|
136
|
+
cwd: Path | None = None,
|
|
137
|
+
):
|
|
138
|
+
"""Initialize VerificationRunner.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
verification_config: Verification commands configuration.
|
|
142
|
+
hooks_config: Git hooks configuration.
|
|
143
|
+
cwd: Working directory (auto-detected if None).
|
|
144
|
+
"""
|
|
145
|
+
self.cwd = cwd or Path.cwd()
|
|
146
|
+
self.verification_config = verification_config
|
|
147
|
+
self.hooks_config = hooks_config
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def from_project(cls, cwd: Path | None = None) -> "VerificationRunner":
|
|
151
|
+
"""Create a VerificationRunner from project configuration.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
cwd: Working directory to search for project config.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
VerificationRunner instance with loaded configuration.
|
|
158
|
+
"""
|
|
159
|
+
cwd = cwd or Path.cwd()
|
|
160
|
+
verification_config = get_verification_config(cwd)
|
|
161
|
+
hooks_config = get_hooks_config(cwd)
|
|
162
|
+
return cls(
|
|
163
|
+
verification_config=verification_config,
|
|
164
|
+
hooks_config=hooks_config,
|
|
165
|
+
cwd=cwd,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def run_stage(self, stage: str) -> StageResult:
|
|
169
|
+
"""Run all verification commands for a hook stage.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
stage: Hook stage name (e.g., 'pre-commit', 'pre-push', 'pre-merge').
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
StageResult with all verification results.
|
|
176
|
+
"""
|
|
177
|
+
# Check if hooks are configured
|
|
178
|
+
if not self.hooks_config:
|
|
179
|
+
return StageResult(
|
|
180
|
+
stage=stage,
|
|
181
|
+
success=True,
|
|
182
|
+
skipped=True,
|
|
183
|
+
skip_reason="No hooks configured in project.json",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Get stage configuration
|
|
187
|
+
stage_config = self.hooks_config.get_stage(stage)
|
|
188
|
+
|
|
189
|
+
# Check if stage is enabled
|
|
190
|
+
if not stage_config.enabled:
|
|
191
|
+
return StageResult(
|
|
192
|
+
stage=stage,
|
|
193
|
+
success=True,
|
|
194
|
+
skipped=True,
|
|
195
|
+
skip_reason=f"Hook stage '{stage}' is disabled",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Check if any commands are configured for this stage
|
|
199
|
+
if not stage_config.run:
|
|
200
|
+
return StageResult(
|
|
201
|
+
stage=stage,
|
|
202
|
+
success=True,
|
|
203
|
+
skipped=True,
|
|
204
|
+
skip_reason=f"No commands configured for '{stage}'",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Check if verification config exists
|
|
208
|
+
if not self.verification_config:
|
|
209
|
+
return StageResult(
|
|
210
|
+
stage=stage,
|
|
211
|
+
success=True,
|
|
212
|
+
skipped=True,
|
|
213
|
+
skip_reason="No verification commands defined in project.json",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Run each command
|
|
217
|
+
results: list[VerificationResult] = []
|
|
218
|
+
overall_success = True
|
|
219
|
+
|
|
220
|
+
for cmd_name in stage_config.run:
|
|
221
|
+
command = self.verification_config.get_command(cmd_name)
|
|
222
|
+
|
|
223
|
+
if not command:
|
|
224
|
+
# Command not defined - skip with warning
|
|
225
|
+
results.append(
|
|
226
|
+
VerificationResult(
|
|
227
|
+
name=cmd_name,
|
|
228
|
+
command="",
|
|
229
|
+
success=True,
|
|
230
|
+
skipped=True,
|
|
231
|
+
skip_reason=f"Command '{cmd_name}' not defined in verification config",
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
# Run the command
|
|
237
|
+
result = run_command(
|
|
238
|
+
name=cmd_name,
|
|
239
|
+
command=command,
|
|
240
|
+
cwd=self.cwd,
|
|
241
|
+
timeout=stage_config.timeout,
|
|
242
|
+
)
|
|
243
|
+
results.append(result)
|
|
244
|
+
|
|
245
|
+
if not result.success:
|
|
246
|
+
overall_success = False
|
|
247
|
+
if stage_config.fail_fast:
|
|
248
|
+
# Stop on first failure
|
|
249
|
+
break
|
|
250
|
+
|
|
251
|
+
return StageResult(
|
|
252
|
+
stage=stage,
|
|
253
|
+
success=overall_success,
|
|
254
|
+
results=results,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
def get_stage_config(self, stage: str) -> HookStageConfig | None:
|
|
258
|
+
"""Get configuration for a hook stage.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
stage: Hook stage name.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
HookStageConfig if configured, None otherwise.
|
|
265
|
+
"""
|
|
266
|
+
if not self.hooks_config:
|
|
267
|
+
return None
|
|
268
|
+
return self.hooks_config.get_stage(stage)
|