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,23 @@
|
|
|
1
|
+
"""Orchestration tool modules.
|
|
2
|
+
|
|
3
|
+
Contains decomposed orchestration functionality:
|
|
4
|
+
- orchestrate: Core orchestration tools (orchestrate_ready_tasks)
|
|
5
|
+
- monitor: Status monitoring tools (get_orchestration_status, poll_agent_status)
|
|
6
|
+
- review: Review workflow tools (spawn_review_agent, process_completed_agents)
|
|
7
|
+
- cleanup: Cleanup tools (cleanup_reviewed_worktrees, cleanup_stale_worktrees)
|
|
8
|
+
- utils: Shared utilities
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from gobby.mcp_proxy.tools.orchestration.cleanup import register_cleanup
|
|
12
|
+
from gobby.mcp_proxy.tools.orchestration.monitor import register_monitor
|
|
13
|
+
from gobby.mcp_proxy.tools.orchestration.orchestrate import register_orchestrator
|
|
14
|
+
from gobby.mcp_proxy.tools.orchestration.review import register_reviewer
|
|
15
|
+
from gobby.mcp_proxy.tools.orchestration.utils import get_current_project_id
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"register_cleanup",
|
|
19
|
+
"register_monitor",
|
|
20
|
+
"register_orchestrator",
|
|
21
|
+
"register_reviewer",
|
|
22
|
+
"get_current_project_id",
|
|
23
|
+
]
|
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
"""Task orchestration tools: cleanup (cleanup_reviewed_worktrees, cleanup_stale_worktrees)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import UTC, datetime, timedelta
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
10
|
+
|
|
11
|
+
from .utils import get_current_project_id
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from gobby.storage.tasks import LocalTaskManager
|
|
15
|
+
from gobby.storage.worktrees import LocalWorktreeManager
|
|
16
|
+
from gobby.worktrees.git import WorktreeGitManager
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def register_cleanup(
|
|
22
|
+
registry: InternalToolRegistry,
|
|
23
|
+
task_manager: LocalTaskManager,
|
|
24
|
+
worktree_storage: LocalWorktreeManager,
|
|
25
|
+
git_manager: WorktreeGitManager | None = None,
|
|
26
|
+
default_project_id: str | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Register cleanup tools."""
|
|
29
|
+
|
|
30
|
+
async def cleanup_reviewed_worktrees(
|
|
31
|
+
parent_session_id: str,
|
|
32
|
+
merge_to_base: bool = True,
|
|
33
|
+
delete_worktrees: bool = True,
|
|
34
|
+
delete_branches: bool = False,
|
|
35
|
+
force: bool = False,
|
|
36
|
+
project_path: str | None = None,
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
|
+
"""
|
|
39
|
+
Clean up worktrees for reviewed agents.
|
|
40
|
+
|
|
41
|
+
After successful review, this tool:
|
|
42
|
+
1. Merges worktree branch to its base branch (if merge_to_base=True)
|
|
43
|
+
2. Marks worktree as merged in database
|
|
44
|
+
3. Deletes the git worktree (if delete_worktrees=True)
|
|
45
|
+
4. Optionally deletes the branch (if delete_branches=True)
|
|
46
|
+
5. Updates workflow state by clearing reviewed_agents
|
|
47
|
+
|
|
48
|
+
The base branch is whatever branch the worktree was created from
|
|
49
|
+
(stored in worktree.base_branch), allowing the orchestrator to work
|
|
50
|
+
on any branch (dev, main, feature branches, etc.).
|
|
51
|
+
|
|
52
|
+
Used by the auto-orchestrator workflow's cleanup step.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
parent_session_id: Parent session ID (orchestrator session)
|
|
56
|
+
merge_to_base: Whether to merge branch to base before cleanup
|
|
57
|
+
delete_worktrees: Whether to delete git worktrees
|
|
58
|
+
delete_branches: Whether to delete branches after cleanup
|
|
59
|
+
force: Force deletion even if worktree is dirty
|
|
60
|
+
project_path: Path to project directory
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Dict with:
|
|
64
|
+
- merged: List of successfully merged branches
|
|
65
|
+
- deleted: List of deleted worktrees
|
|
66
|
+
- failed: List of failed operations with reasons
|
|
67
|
+
- summary: Counts
|
|
68
|
+
"""
|
|
69
|
+
if git_manager is None:
|
|
70
|
+
return {
|
|
71
|
+
"success": False,
|
|
72
|
+
"error": "Git manager not configured. Cannot cleanup worktrees.",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Get workflow state
|
|
76
|
+
from gobby.workflows.state_manager import WorkflowStateManager
|
|
77
|
+
|
|
78
|
+
state_manager = WorkflowStateManager(task_manager.db)
|
|
79
|
+
state = state_manager.get_state(parent_session_id)
|
|
80
|
+
if not state:
|
|
81
|
+
return {
|
|
82
|
+
"success": True,
|
|
83
|
+
"merged": [],
|
|
84
|
+
"deleted": [],
|
|
85
|
+
"failed": [],
|
|
86
|
+
"summary": {"merged": 0, "deleted": 0, "failed": 0},
|
|
87
|
+
"message": "No workflow state found",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
workflow_vars = state.variables
|
|
91
|
+
reviewed_agents = workflow_vars.get("reviewed_agents", [])
|
|
92
|
+
|
|
93
|
+
if not reviewed_agents:
|
|
94
|
+
return {
|
|
95
|
+
"success": True,
|
|
96
|
+
"merged": [],
|
|
97
|
+
"deleted": [],
|
|
98
|
+
"failed": [],
|
|
99
|
+
"summary": {"merged": 0, "deleted": 0, "failed": 0},
|
|
100
|
+
"message": "No reviewed agents to cleanup",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
merged: list[dict[str, Any]] = []
|
|
104
|
+
deleted: list[dict[str, Any]] = []
|
|
105
|
+
failed: list[dict[str, Any]] = []
|
|
106
|
+
cleaned_agents: list[dict[str, Any]] = []
|
|
107
|
+
|
|
108
|
+
for agent_info in reviewed_agents:
|
|
109
|
+
worktree_id = agent_info.get("worktree_id")
|
|
110
|
+
task_id = agent_info.get("task_id")
|
|
111
|
+
branch_name = agent_info.get("branch_name")
|
|
112
|
+
|
|
113
|
+
if not worktree_id:
|
|
114
|
+
failed.append(
|
|
115
|
+
{
|
|
116
|
+
**agent_info,
|
|
117
|
+
"failure_reason": "Missing worktree_id",
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
# Get worktree from storage
|
|
123
|
+
worktree = worktree_storage.get(worktree_id)
|
|
124
|
+
if not worktree:
|
|
125
|
+
# Worktree already deleted, consider it cleaned
|
|
126
|
+
cleaned_agents.append(agent_info)
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
branch = branch_name or worktree.branch_name
|
|
130
|
+
worktree_path = worktree.worktree_path
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
# Track if at least one successful operation occurred
|
|
134
|
+
operation_succeeded = False
|
|
135
|
+
|
|
136
|
+
# Step 1: Merge branch to base (if enabled)
|
|
137
|
+
merge_succeeded = False
|
|
138
|
+
if merge_to_base:
|
|
139
|
+
# Validate required fields for merge
|
|
140
|
+
if not branch:
|
|
141
|
+
failed.append(
|
|
142
|
+
{
|
|
143
|
+
**agent_info,
|
|
144
|
+
"failure_reason": "Missing branch name for merge operation",
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
continue
|
|
148
|
+
if not worktree.base_branch:
|
|
149
|
+
failed.append(
|
|
150
|
+
{
|
|
151
|
+
**agent_info,
|
|
152
|
+
"failure_reason": "Missing base_branch for merge operation",
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
merge_result = _merge_branch_to_base(
|
|
158
|
+
git_manager=git_manager,
|
|
159
|
+
branch_name=branch,
|
|
160
|
+
base_branch=worktree.base_branch,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if merge_result["success"]:
|
|
164
|
+
merge_succeeded = True
|
|
165
|
+
operation_succeeded = True
|
|
166
|
+
merged.append(
|
|
167
|
+
{
|
|
168
|
+
"worktree_id": worktree_id,
|
|
169
|
+
"task_id": task_id,
|
|
170
|
+
"branch_name": branch,
|
|
171
|
+
"merge_commit": merge_result.get("merge_commit"),
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
# Merge failed - cannot proceed with cleanup
|
|
176
|
+
failed.append(
|
|
177
|
+
{
|
|
178
|
+
**agent_info,
|
|
179
|
+
"failure_reason": f"Merge failed: {merge_result.get('error')}",
|
|
180
|
+
"merge_error": merge_result.get("error"),
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
# Step 2: Mark worktree as merged (only if merge actually occurred)
|
|
186
|
+
if merge_succeeded:
|
|
187
|
+
worktree_storage.mark_merged(worktree_id)
|
|
188
|
+
|
|
189
|
+
# Step 3: Delete git worktree (if enabled)
|
|
190
|
+
if delete_worktrees:
|
|
191
|
+
# Validate required fields for deletion
|
|
192
|
+
if not worktree_path:
|
|
193
|
+
failed.append(
|
|
194
|
+
{
|
|
195
|
+
**agent_info,
|
|
196
|
+
"failure_reason": "Missing worktree_path for delete operation",
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
delete_result = git_manager.delete_worktree(
|
|
202
|
+
worktree_path=worktree_path,
|
|
203
|
+
force=force,
|
|
204
|
+
delete_branch=delete_branches,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if delete_result.success:
|
|
208
|
+
operation_succeeded = True
|
|
209
|
+
deleted.append(
|
|
210
|
+
{
|
|
211
|
+
"worktree_id": worktree_id,
|
|
212
|
+
"task_id": task_id,
|
|
213
|
+
"worktree_path": worktree_path,
|
|
214
|
+
"branch_deleted": delete_branches,
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Also delete the database record
|
|
219
|
+
worktree_storage.delete(worktree_id)
|
|
220
|
+
else:
|
|
221
|
+
# Worktree deletion failed - report actual merge status
|
|
222
|
+
failed.append(
|
|
223
|
+
{
|
|
224
|
+
**agent_info,
|
|
225
|
+
"failure_reason": f"Worktree deletion failed: {delete_result.message}",
|
|
226
|
+
"worktree_status": "merged"
|
|
227
|
+
if merge_succeeded
|
|
228
|
+
else agent_info.get("worktree_status", "unmerged"),
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
# Only mark as cleaned if at least one operation succeeded
|
|
234
|
+
if operation_succeeded:
|
|
235
|
+
cleaned_agents.append(agent_info)
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.exception(f"Error cleaning up worktree {worktree_id}")
|
|
239
|
+
failed.append(
|
|
240
|
+
{
|
|
241
|
+
**agent_info,
|
|
242
|
+
"failure_reason": str(e),
|
|
243
|
+
}
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Update workflow state
|
|
247
|
+
try:
|
|
248
|
+
state = state_manager.get_state(parent_session_id)
|
|
249
|
+
if state:
|
|
250
|
+
# Remove successfully cleaned agents from reviewed_agents
|
|
251
|
+
# Compare by worktree_id to avoid dict identity issues
|
|
252
|
+
cleaned_worktree_ids = {a.get("worktree_id") for a in cleaned_agents}
|
|
253
|
+
remaining_reviewed = [
|
|
254
|
+
a for a in reviewed_agents if a.get("worktree_id") not in cleaned_worktree_ids
|
|
255
|
+
]
|
|
256
|
+
state.variables["reviewed_agents"] = remaining_reviewed
|
|
257
|
+
|
|
258
|
+
# Track cleanup history
|
|
259
|
+
cleanup_history = state.variables.get("cleanup_history", [])
|
|
260
|
+
cleanup_history.append(
|
|
261
|
+
{
|
|
262
|
+
"merged_count": len(merged),
|
|
263
|
+
"deleted_count": len(deleted),
|
|
264
|
+
"failed_count": len(failed),
|
|
265
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
state.variables["cleanup_history"] = cleanup_history
|
|
269
|
+
|
|
270
|
+
state_manager.save_state(state)
|
|
271
|
+
except Exception as e:
|
|
272
|
+
logger.warning(f"Failed to update workflow state after cleanup: {e}")
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
"success": True,
|
|
276
|
+
"merged": merged,
|
|
277
|
+
"deleted": deleted,
|
|
278
|
+
"failed": failed,
|
|
279
|
+
"summary": {
|
|
280
|
+
"merged": len(merged),
|
|
281
|
+
"deleted": len(deleted),
|
|
282
|
+
"failed": len(failed),
|
|
283
|
+
},
|
|
284
|
+
"remaining_reviewed": len(reviewed_agents) - len(cleaned_agents),
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async def cleanup_stale_worktrees(
|
|
288
|
+
project_path: str | None = None,
|
|
289
|
+
older_than_hours: int = 24,
|
|
290
|
+
force: bool = False,
|
|
291
|
+
) -> dict[str, Any]:
|
|
292
|
+
"""
|
|
293
|
+
Clean up stale worktrees that have been inactive.
|
|
294
|
+
|
|
295
|
+
Finds worktrees marked as stale or with no active agent session
|
|
296
|
+
that are older than the specified threshold, and cleans them up.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
project_path: Path to project directory
|
|
300
|
+
older_than_hours: Only cleanup worktrees older than this (hours)
|
|
301
|
+
force: Force deletion even if dirty
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Dict with cleanup results
|
|
305
|
+
"""
|
|
306
|
+
if git_manager is None:
|
|
307
|
+
return {
|
|
308
|
+
"success": False,
|
|
309
|
+
"error": "Git manager not configured",
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# Validate older_than_hours
|
|
313
|
+
try:
|
|
314
|
+
older_than_hours = int(older_than_hours)
|
|
315
|
+
except (TypeError, ValueError):
|
|
316
|
+
return {
|
|
317
|
+
"success": False,
|
|
318
|
+
"error": "older_than_hours must be an integer",
|
|
319
|
+
}
|
|
320
|
+
if older_than_hours < 0:
|
|
321
|
+
return {
|
|
322
|
+
"success": False,
|
|
323
|
+
"error": "older_than_hours must be non-negative",
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# Resolve project ID
|
|
327
|
+
resolved_project_id = default_project_id
|
|
328
|
+
if project_path:
|
|
329
|
+
from pathlib import Path
|
|
330
|
+
|
|
331
|
+
from gobby.utils.project_context import get_project_context
|
|
332
|
+
|
|
333
|
+
ctx = get_project_context(Path(project_path))
|
|
334
|
+
if ctx:
|
|
335
|
+
resolved_project_id = ctx.get("id")
|
|
336
|
+
|
|
337
|
+
if not resolved_project_id:
|
|
338
|
+
resolved_project_id = get_current_project_id()
|
|
339
|
+
|
|
340
|
+
if not resolved_project_id:
|
|
341
|
+
return {
|
|
342
|
+
"success": False,
|
|
343
|
+
"error": "Could not resolve project ID",
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
from gobby.storage.worktrees import WorktreeStatus as WTStatus
|
|
347
|
+
|
|
348
|
+
# Get all worktrees to check for stale or abandoned candidates
|
|
349
|
+
all_worktrees = worktree_storage.list_worktrees(
|
|
350
|
+
project_id=resolved_project_id,
|
|
351
|
+
limit=100,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
cutoff = datetime.now(UTC) - timedelta(hours=older_than_hours)
|
|
355
|
+
candidates = []
|
|
356
|
+
|
|
357
|
+
for wt in all_worktrees:
|
|
358
|
+
# Skip merged or abandoned
|
|
359
|
+
if wt.status in [WTStatus.MERGED.value, WTStatus.ABANDONED.value]:
|
|
360
|
+
continue
|
|
361
|
+
|
|
362
|
+
# Add stale worktrees
|
|
363
|
+
if wt.status == WTStatus.STALE.value:
|
|
364
|
+
candidates.append(wt)
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
# Add active worktrees with no active session that are old enough
|
|
368
|
+
if wt.agent_session_id is None:
|
|
369
|
+
try:
|
|
370
|
+
created = datetime.fromisoformat(wt.created_at.replace("Z", "+00:00"))
|
|
371
|
+
if created < cutoff:
|
|
372
|
+
candidates.append(wt)
|
|
373
|
+
except (ValueError, AttributeError):
|
|
374
|
+
pass
|
|
375
|
+
|
|
376
|
+
deleted: list[dict[str, Any]] = []
|
|
377
|
+
failed: list[dict[str, Any]] = []
|
|
378
|
+
|
|
379
|
+
for wt in candidates:
|
|
380
|
+
try:
|
|
381
|
+
# Mark as stale first if not already
|
|
382
|
+
if wt.status != WTStatus.STALE.value:
|
|
383
|
+
worktree_storage.mark_stale(wt.id)
|
|
384
|
+
|
|
385
|
+
# Delete the git worktree
|
|
386
|
+
delete_result = git_manager.delete_worktree(
|
|
387
|
+
worktree_path=wt.worktree_path,
|
|
388
|
+
force=force,
|
|
389
|
+
delete_branch=False, # Keep branches for stale cleanup
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
if delete_result.success:
|
|
393
|
+
# Mark as abandoned and delete record
|
|
394
|
+
worktree_storage.mark_abandoned(wt.id)
|
|
395
|
+
worktree_storage.delete(wt.id)
|
|
396
|
+
deleted.append(
|
|
397
|
+
{
|
|
398
|
+
"worktree_id": wt.id,
|
|
399
|
+
"branch_name": wt.branch_name,
|
|
400
|
+
"worktree_path": wt.worktree_path,
|
|
401
|
+
}
|
|
402
|
+
)
|
|
403
|
+
else:
|
|
404
|
+
failed.append(
|
|
405
|
+
{
|
|
406
|
+
"worktree_id": wt.id,
|
|
407
|
+
"branch_name": wt.branch_name,
|
|
408
|
+
"failure_reason": delete_result.message,
|
|
409
|
+
}
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
except Exception as e:
|
|
413
|
+
logger.exception(f"Error cleaning up stale worktree {wt.id}")
|
|
414
|
+
failed.append(
|
|
415
|
+
{
|
|
416
|
+
"worktree_id": wt.id,
|
|
417
|
+
"branch_name": wt.branch_name,
|
|
418
|
+
"failure_reason": str(e),
|
|
419
|
+
}
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
"success": True,
|
|
424
|
+
"deleted": deleted,
|
|
425
|
+
"failed": failed,
|
|
426
|
+
"summary": {
|
|
427
|
+
"candidates": len(candidates),
|
|
428
|
+
"deleted": len(deleted),
|
|
429
|
+
"failed": len(failed),
|
|
430
|
+
},
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
registry.register(
|
|
434
|
+
name="cleanup_reviewed_worktrees",
|
|
435
|
+
description=(
|
|
436
|
+
"Clean up worktrees for reviewed agents. "
|
|
437
|
+
"Merges branches to base branch (from worktree.base_branch), marks as merged, deletes worktrees. "
|
|
438
|
+
"Used by auto-orchestrator cleanup step."
|
|
439
|
+
),
|
|
440
|
+
input_schema={
|
|
441
|
+
"type": "object",
|
|
442
|
+
"properties": {
|
|
443
|
+
"parent_session_id": {
|
|
444
|
+
"type": "string",
|
|
445
|
+
"description": "Parent session ID (orchestrator session)",
|
|
446
|
+
},
|
|
447
|
+
"merge_to_base": {
|
|
448
|
+
"type": "boolean",
|
|
449
|
+
"description": "Whether to merge branch to base before cleanup",
|
|
450
|
+
"default": True,
|
|
451
|
+
},
|
|
452
|
+
"delete_worktrees": {
|
|
453
|
+
"type": "boolean",
|
|
454
|
+
"description": "Whether to delete git worktrees",
|
|
455
|
+
"default": True,
|
|
456
|
+
},
|
|
457
|
+
"delete_branches": {
|
|
458
|
+
"type": "boolean",
|
|
459
|
+
"description": "Whether to delete branches after cleanup",
|
|
460
|
+
"default": False,
|
|
461
|
+
},
|
|
462
|
+
"force": {
|
|
463
|
+
"type": "boolean",
|
|
464
|
+
"description": "Force deletion even if worktree is dirty",
|
|
465
|
+
"default": False,
|
|
466
|
+
},
|
|
467
|
+
"project_path": {
|
|
468
|
+
"type": "string",
|
|
469
|
+
"description": "Path to project directory (optional)",
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
"required": ["parent_session_id"],
|
|
473
|
+
},
|
|
474
|
+
func=cleanup_reviewed_worktrees,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
registry.register(
|
|
478
|
+
name="cleanup_stale_worktrees",
|
|
479
|
+
description=(
|
|
480
|
+
"Clean up stale worktrees that have been inactive. "
|
|
481
|
+
"Deletes worktrees with no active agent older than threshold."
|
|
482
|
+
),
|
|
483
|
+
input_schema={
|
|
484
|
+
"type": "object",
|
|
485
|
+
"properties": {
|
|
486
|
+
"project_path": {
|
|
487
|
+
"type": "string",
|
|
488
|
+
"description": "Path to project directory (optional)",
|
|
489
|
+
},
|
|
490
|
+
"older_than_hours": {
|
|
491
|
+
"type": "integer",
|
|
492
|
+
"description": "Only cleanup worktrees older than this (hours)",
|
|
493
|
+
"default": 24,
|
|
494
|
+
},
|
|
495
|
+
"force": {
|
|
496
|
+
"type": "boolean",
|
|
497
|
+
"description": "Force deletion even if dirty",
|
|
498
|
+
"default": False,
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
"required": [],
|
|
502
|
+
},
|
|
503
|
+
func=cleanup_stale_worktrees,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _merge_branch_to_base(
|
|
508
|
+
git_manager: WorktreeGitManager,
|
|
509
|
+
branch_name: str,
|
|
510
|
+
base_branch: str = "main",
|
|
511
|
+
) -> dict[str, Any]:
|
|
512
|
+
"""
|
|
513
|
+
Merge a branch back to its base branch.
|
|
514
|
+
|
|
515
|
+
The base_branch is typically the branch the worktree was created from
|
|
516
|
+
(e.g., dev, main, or a feature branch). This allows the orchestrator
|
|
517
|
+
to run on any branch and merge completed work back.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
git_manager: Git manager instance
|
|
521
|
+
branch_name: Branch to merge (the worktree branch)
|
|
522
|
+
base_branch: Target branch to merge into (from worktree.base_branch)
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
Dict with success status, merge_commit, and error details
|
|
526
|
+
"""
|
|
527
|
+
try:
|
|
528
|
+
# Fetch latest from remote
|
|
529
|
+
fetch_result = git_manager._run_git(
|
|
530
|
+
["fetch", "origin", base_branch],
|
|
531
|
+
timeout=60,
|
|
532
|
+
)
|
|
533
|
+
if fetch_result.returncode != 0:
|
|
534
|
+
return {
|
|
535
|
+
"success": False,
|
|
536
|
+
"error": f"Failed to fetch: {fetch_result.stderr}",
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
# Checkout the base branch
|
|
540
|
+
checkout_result = git_manager._run_git(
|
|
541
|
+
["checkout", base_branch],
|
|
542
|
+
timeout=30,
|
|
543
|
+
)
|
|
544
|
+
if checkout_result.returncode != 0:
|
|
545
|
+
return {
|
|
546
|
+
"success": False,
|
|
547
|
+
"error": f"Failed to checkout {base_branch}: {checkout_result.stderr}",
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
# Pull latest
|
|
551
|
+
pull_result = git_manager._run_git(
|
|
552
|
+
["pull", "origin", base_branch],
|
|
553
|
+
timeout=60,
|
|
554
|
+
)
|
|
555
|
+
if pull_result.returncode != 0:
|
|
556
|
+
return {
|
|
557
|
+
"success": False,
|
|
558
|
+
"error": f"Failed to pull: {pull_result.stderr}",
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
# Merge the branch
|
|
562
|
+
merge_result = git_manager._run_git(
|
|
563
|
+
["merge", branch_name, "--no-ff", "-m", f"Merge branch '{branch_name}'"],
|
|
564
|
+
timeout=120,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
if merge_result.returncode != 0:
|
|
568
|
+
# Check for conflicts
|
|
569
|
+
has_conflicts = "CONFLICT" in merge_result.stdout or "CONFLICT" in merge_result.stderr
|
|
570
|
+
|
|
571
|
+
# Always try to abort/reset the repo to ensure clean state
|
|
572
|
+
abort_result = git_manager._run_git(["merge", "--abort"], timeout=10)
|
|
573
|
+
if abort_result.returncode != 0:
|
|
574
|
+
# Abort failed, force reset to clean state
|
|
575
|
+
git_manager._run_git(["reset", "--hard", "HEAD"], timeout=10)
|
|
576
|
+
git_manager._run_git(["clean", "-fd"], timeout=10)
|
|
577
|
+
|
|
578
|
+
if has_conflicts:
|
|
579
|
+
return {
|
|
580
|
+
"success": False,
|
|
581
|
+
"error": "Merge conflict detected",
|
|
582
|
+
"conflicts": True,
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
"success": False,
|
|
586
|
+
"error": merge_result.stderr or merge_result.stdout,
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
# Get the merge commit SHA
|
|
590
|
+
log_result = git_manager._run_git(
|
|
591
|
+
["rev-parse", "HEAD"],
|
|
592
|
+
timeout=10,
|
|
593
|
+
)
|
|
594
|
+
merge_commit = log_result.stdout.strip() if log_result.returncode == 0 else None
|
|
595
|
+
|
|
596
|
+
# Push the merge to remote
|
|
597
|
+
push_result = git_manager._run_git(
|
|
598
|
+
["push", "origin", base_branch],
|
|
599
|
+
timeout=60,
|
|
600
|
+
)
|
|
601
|
+
if push_result.returncode != 0:
|
|
602
|
+
return {
|
|
603
|
+
"success": False,
|
|
604
|
+
"error": f"Merge succeeded but push failed: {push_result.stderr}",
|
|
605
|
+
"merge_commit": merge_commit,
|
|
606
|
+
"push_failed": True,
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return {
|
|
610
|
+
"success": True,
|
|
611
|
+
"merge_commit": merge_commit,
|
|
612
|
+
"message": f"Successfully merged {branch_name} to {base_branch}",
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
except Exception as e:
|
|
616
|
+
return {
|
|
617
|
+
"success": False,
|
|
618
|
+
"error": str(e),
|
|
619
|
+
}
|