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,550 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Merge resolution storage module.
|
|
3
|
+
|
|
4
|
+
Stores merge resolutions and conflicts for worktree merge operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import sqlite3
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from gobby.storage.database import DatabaseProtocol
|
|
16
|
+
from gobby.utils.id import generate_prefixed_id
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConflictStatus(Enum):
|
|
22
|
+
"""Status of a merge conflict."""
|
|
23
|
+
|
|
24
|
+
PENDING = "pending"
|
|
25
|
+
RESOLVED = "resolved"
|
|
26
|
+
FAILED = "failed"
|
|
27
|
+
HUMAN_REVIEW = "human_review"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class MergeResolution:
|
|
32
|
+
"""A merge resolution record tracking a merge operation."""
|
|
33
|
+
|
|
34
|
+
id: str
|
|
35
|
+
worktree_id: str
|
|
36
|
+
source_branch: str
|
|
37
|
+
target_branch: str
|
|
38
|
+
status: str
|
|
39
|
+
tier_used: str | None
|
|
40
|
+
created_at: str
|
|
41
|
+
updated_at: str
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_row(cls, row: sqlite3.Row) -> "MergeResolution":
|
|
45
|
+
"""Create a MergeResolution from a database row."""
|
|
46
|
+
return cls(
|
|
47
|
+
id=row["id"],
|
|
48
|
+
worktree_id=row["worktree_id"],
|
|
49
|
+
source_branch=row["source_branch"],
|
|
50
|
+
target_branch=row["target_branch"],
|
|
51
|
+
status=row["status"],
|
|
52
|
+
tier_used=row["tier_used"],
|
|
53
|
+
created_at=row["created_at"],
|
|
54
|
+
updated_at=row["updated_at"],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict[str, Any]:
|
|
58
|
+
"""Convert resolution to dictionary for serialization."""
|
|
59
|
+
return {
|
|
60
|
+
"id": self.id,
|
|
61
|
+
"worktree_id": self.worktree_id,
|
|
62
|
+
"source_branch": self.source_branch,
|
|
63
|
+
"target_branch": self.target_branch,
|
|
64
|
+
"status": self.status,
|
|
65
|
+
"tier_used": self.tier_used,
|
|
66
|
+
"created_at": self.created_at,
|
|
67
|
+
"updated_at": self.updated_at,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class MergeConflict:
|
|
73
|
+
"""A merge conflict record for a specific file."""
|
|
74
|
+
|
|
75
|
+
id: str
|
|
76
|
+
resolution_id: str
|
|
77
|
+
file_path: str
|
|
78
|
+
status: str
|
|
79
|
+
ours_content: str | None
|
|
80
|
+
theirs_content: str | None
|
|
81
|
+
resolved_content: str | None
|
|
82
|
+
created_at: str
|
|
83
|
+
updated_at: str
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_row(cls, row: sqlite3.Row) -> "MergeConflict":
|
|
87
|
+
"""Create a MergeConflict from a database row."""
|
|
88
|
+
return cls(
|
|
89
|
+
id=row["id"],
|
|
90
|
+
resolution_id=row["resolution_id"],
|
|
91
|
+
file_path=row["file_path"],
|
|
92
|
+
status=row["status"],
|
|
93
|
+
ours_content=row["ours_content"],
|
|
94
|
+
theirs_content=row["theirs_content"],
|
|
95
|
+
resolved_content=row["resolved_content"],
|
|
96
|
+
created_at=row["created_at"],
|
|
97
|
+
updated_at=row["updated_at"],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> dict[str, Any]:
|
|
101
|
+
"""Convert conflict to dictionary for serialization."""
|
|
102
|
+
return {
|
|
103
|
+
"id": self.id,
|
|
104
|
+
"resolution_id": self.resolution_id,
|
|
105
|
+
"file_path": self.file_path,
|
|
106
|
+
"status": self.status,
|
|
107
|
+
"ours_content": self.ours_content,
|
|
108
|
+
"theirs_content": self.theirs_content,
|
|
109
|
+
"resolved_content": self.resolved_content,
|
|
110
|
+
"created_at": self.created_at,
|
|
111
|
+
"updated_at": self.updated_at,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class MergeResolutionManager:
|
|
116
|
+
"""Manages merge resolutions and conflicts in local SQLite database."""
|
|
117
|
+
|
|
118
|
+
def __init__(self, db: DatabaseProtocol):
|
|
119
|
+
self.db = db
|
|
120
|
+
self._change_listeners: list[Callable[[], Any]] = []
|
|
121
|
+
|
|
122
|
+
def add_change_listener(self, listener: Callable[[], Any]) -> None:
|
|
123
|
+
"""Add a change listener that will be called on create/update/delete."""
|
|
124
|
+
self._change_listeners.append(listener)
|
|
125
|
+
|
|
126
|
+
def _notify_listeners(self) -> None:
|
|
127
|
+
"""Notify all change listeners."""
|
|
128
|
+
for listener in self._change_listeners:
|
|
129
|
+
try:
|
|
130
|
+
listener()
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.error(f"Error in merge resolution change listener: {e}")
|
|
133
|
+
|
|
134
|
+
# =========================================================================
|
|
135
|
+
# Resolution CRUD
|
|
136
|
+
# =========================================================================
|
|
137
|
+
|
|
138
|
+
def create_resolution(
|
|
139
|
+
self,
|
|
140
|
+
worktree_id: str,
|
|
141
|
+
source_branch: str,
|
|
142
|
+
target_branch: str,
|
|
143
|
+
status: str = "pending",
|
|
144
|
+
tier_used: str | None = None,
|
|
145
|
+
) -> MergeResolution:
|
|
146
|
+
"""Create a new merge resolution.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
worktree_id: ID of the worktree
|
|
150
|
+
source_branch: Branch being merged in
|
|
151
|
+
target_branch: Target branch (e.g., main)
|
|
152
|
+
status: Resolution status (default: pending)
|
|
153
|
+
tier_used: Resolution tier used (if resolved)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
The created MergeResolution
|
|
157
|
+
"""
|
|
158
|
+
now = datetime.now(UTC).isoformat()
|
|
159
|
+
resolution_id = generate_prefixed_id("mr", worktree_id + source_branch)
|
|
160
|
+
|
|
161
|
+
with self.db.transaction() as conn:
|
|
162
|
+
conn.execute(
|
|
163
|
+
"""
|
|
164
|
+
INSERT INTO merge_resolutions (
|
|
165
|
+
id, worktree_id, source_branch, target_branch,
|
|
166
|
+
status, tier_used, created_at, updated_at
|
|
167
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
168
|
+
""",
|
|
169
|
+
(
|
|
170
|
+
resolution_id,
|
|
171
|
+
worktree_id,
|
|
172
|
+
source_branch,
|
|
173
|
+
target_branch,
|
|
174
|
+
status,
|
|
175
|
+
tier_used,
|
|
176
|
+
now,
|
|
177
|
+
now,
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
self._notify_listeners()
|
|
182
|
+
result = self.get_resolution(resolution_id)
|
|
183
|
+
if result is None:
|
|
184
|
+
raise RuntimeError(
|
|
185
|
+
f"Failed to retrieve resolution '{resolution_id}' after successful insert"
|
|
186
|
+
)
|
|
187
|
+
return result
|
|
188
|
+
|
|
189
|
+
def get_resolution(self, resolution_id: str) -> MergeResolution | None:
|
|
190
|
+
"""Get a resolution by ID.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
resolution_id: The resolution ID
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
The MergeResolution if found, None otherwise
|
|
197
|
+
"""
|
|
198
|
+
row = self.db.fetchone("SELECT * FROM merge_resolutions WHERE id = ?", (resolution_id,))
|
|
199
|
+
if not row:
|
|
200
|
+
return None
|
|
201
|
+
return MergeResolution.from_row(row)
|
|
202
|
+
|
|
203
|
+
def update_resolution(
|
|
204
|
+
self,
|
|
205
|
+
resolution_id: str,
|
|
206
|
+
status: str | None = None,
|
|
207
|
+
tier_used: str | None = None,
|
|
208
|
+
) -> MergeResolution | None:
|
|
209
|
+
"""Update a resolution.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
resolution_id: The resolution ID
|
|
213
|
+
status: New status (optional)
|
|
214
|
+
tier_used: New tier used (optional)
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
The updated MergeResolution if found, None otherwise
|
|
218
|
+
"""
|
|
219
|
+
resolution = self.get_resolution(resolution_id)
|
|
220
|
+
if not resolution:
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
now = datetime.now(UTC).isoformat()
|
|
224
|
+
new_status = status if status is not None else resolution.status
|
|
225
|
+
new_tier = tier_used if tier_used is not None else resolution.tier_used
|
|
226
|
+
|
|
227
|
+
with self.db.transaction() as conn:
|
|
228
|
+
conn.execute(
|
|
229
|
+
"""
|
|
230
|
+
UPDATE merge_resolutions
|
|
231
|
+
SET status = ?, tier_used = ?, updated_at = ?
|
|
232
|
+
WHERE id = ?
|
|
233
|
+
""",
|
|
234
|
+
(new_status, new_tier, now, resolution_id),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
self._notify_listeners()
|
|
238
|
+
return self.get_resolution(resolution_id)
|
|
239
|
+
|
|
240
|
+
def delete_resolution(self, resolution_id: str) -> bool:
|
|
241
|
+
"""Delete a resolution by ID.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
resolution_id: The resolution ID to delete
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
True if deleted, False if not found
|
|
248
|
+
"""
|
|
249
|
+
with self.db.transaction() as conn:
|
|
250
|
+
cursor = conn.execute("DELETE FROM merge_resolutions WHERE id = ?", (resolution_id,))
|
|
251
|
+
if cursor.rowcount == 0:
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
self._notify_listeners()
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
def list_resolutions(
|
|
258
|
+
self,
|
|
259
|
+
worktree_id: str | None = None,
|
|
260
|
+
source_branch: str | None = None,
|
|
261
|
+
target_branch: str | None = None,
|
|
262
|
+
status: str | None = None,
|
|
263
|
+
limit: int = 100,
|
|
264
|
+
offset: int = 0,
|
|
265
|
+
) -> list[MergeResolution]:
|
|
266
|
+
"""List resolutions with optional filters.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
worktree_id: Filter by worktree ID
|
|
270
|
+
source_branch: Filter by source branch
|
|
271
|
+
target_branch: Filter by target branch
|
|
272
|
+
status: Filter by status
|
|
273
|
+
limit: Maximum number of results
|
|
274
|
+
offset: Offset for pagination
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
List of matching MergeResolutions
|
|
278
|
+
"""
|
|
279
|
+
query = "SELECT * FROM merge_resolutions WHERE 1=1"
|
|
280
|
+
params: list[Any] = []
|
|
281
|
+
|
|
282
|
+
if worktree_id:
|
|
283
|
+
query += " AND worktree_id = ?"
|
|
284
|
+
params.append(worktree_id)
|
|
285
|
+
|
|
286
|
+
if source_branch:
|
|
287
|
+
query += " AND source_branch = ?"
|
|
288
|
+
params.append(source_branch)
|
|
289
|
+
|
|
290
|
+
if target_branch:
|
|
291
|
+
query += " AND target_branch = ?"
|
|
292
|
+
params.append(target_branch)
|
|
293
|
+
|
|
294
|
+
if status:
|
|
295
|
+
query += " AND status = ?"
|
|
296
|
+
params.append(status)
|
|
297
|
+
|
|
298
|
+
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
|
299
|
+
params.extend([limit, offset])
|
|
300
|
+
|
|
301
|
+
rows = self.db.fetchall(query, tuple(params))
|
|
302
|
+
return [MergeResolution.from_row(row) for row in rows]
|
|
303
|
+
|
|
304
|
+
# =========================================================================
|
|
305
|
+
# Conflict CRUD
|
|
306
|
+
# =========================================================================
|
|
307
|
+
|
|
308
|
+
def create_conflict(
|
|
309
|
+
self,
|
|
310
|
+
resolution_id: str,
|
|
311
|
+
file_path: str,
|
|
312
|
+
ours_content: str | None = None,
|
|
313
|
+
theirs_content: str | None = None,
|
|
314
|
+
status: str = "pending",
|
|
315
|
+
) -> MergeConflict:
|
|
316
|
+
"""Create a new merge conflict.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
resolution_id: ID of the parent resolution
|
|
320
|
+
file_path: Path to the conflicting file
|
|
321
|
+
ours_content: Content from our side
|
|
322
|
+
theirs_content: Content from their side
|
|
323
|
+
status: Conflict status (default: pending)
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
The created MergeConflict
|
|
327
|
+
"""
|
|
328
|
+
now = datetime.now(UTC).isoformat()
|
|
329
|
+
conflict_id = generate_prefixed_id("mc", resolution_id + file_path)
|
|
330
|
+
|
|
331
|
+
with self.db.transaction() as conn:
|
|
332
|
+
conn.execute(
|
|
333
|
+
"""
|
|
334
|
+
INSERT INTO merge_conflicts (
|
|
335
|
+
id, resolution_id, file_path, status,
|
|
336
|
+
ours_content, theirs_content, created_at, updated_at
|
|
337
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
338
|
+
""",
|
|
339
|
+
(
|
|
340
|
+
conflict_id,
|
|
341
|
+
resolution_id,
|
|
342
|
+
file_path,
|
|
343
|
+
status,
|
|
344
|
+
ours_content,
|
|
345
|
+
theirs_content,
|
|
346
|
+
now,
|
|
347
|
+
now,
|
|
348
|
+
),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
self._notify_listeners()
|
|
352
|
+
result = self.get_conflict(conflict_id)
|
|
353
|
+
if result is None:
|
|
354
|
+
raise RuntimeError(
|
|
355
|
+
f"Failed to retrieve conflict '{conflict_id}' after successful insert"
|
|
356
|
+
)
|
|
357
|
+
return result
|
|
358
|
+
|
|
359
|
+
def get_conflict(self, conflict_id: str) -> MergeConflict | None:
|
|
360
|
+
"""Get a conflict by ID.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
conflict_id: The conflict ID
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
The MergeConflict if found, None otherwise
|
|
367
|
+
"""
|
|
368
|
+
row = self.db.fetchone("SELECT * FROM merge_conflicts WHERE id = ?", (conflict_id,))
|
|
369
|
+
if not row:
|
|
370
|
+
return None
|
|
371
|
+
return MergeConflict.from_row(row)
|
|
372
|
+
|
|
373
|
+
def update_conflict(
|
|
374
|
+
self,
|
|
375
|
+
conflict_id: str,
|
|
376
|
+
status: str | None = None,
|
|
377
|
+
resolved_content: str | None = None,
|
|
378
|
+
) -> MergeConflict | None:
|
|
379
|
+
"""Update a conflict.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
conflict_id: The conflict ID
|
|
383
|
+
status: New status (optional)
|
|
384
|
+
resolved_content: Resolved content (optional)
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
The updated MergeConflict if found, None otherwise
|
|
388
|
+
"""
|
|
389
|
+
conflict = self.get_conflict(conflict_id)
|
|
390
|
+
if not conflict:
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
now = datetime.now(UTC).isoformat()
|
|
394
|
+
new_status = status if status is not None else conflict.status
|
|
395
|
+
new_resolved = (
|
|
396
|
+
resolved_content if resolved_content is not None else conflict.resolved_content
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
with self.db.transaction() as conn:
|
|
400
|
+
conn.execute(
|
|
401
|
+
"""
|
|
402
|
+
UPDATE merge_conflicts
|
|
403
|
+
SET status = ?, resolved_content = ?, updated_at = ?
|
|
404
|
+
WHERE id = ?
|
|
405
|
+
""",
|
|
406
|
+
(new_status, new_resolved, now, conflict_id),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
self._notify_listeners()
|
|
410
|
+
return self.get_conflict(conflict_id)
|
|
411
|
+
|
|
412
|
+
def delete_conflict(self, conflict_id: str) -> bool:
|
|
413
|
+
"""Delete a conflict by ID.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
conflict_id: The conflict ID to delete
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
True if deleted, False if not found
|
|
420
|
+
"""
|
|
421
|
+
with self.db.transaction() as conn:
|
|
422
|
+
cursor = conn.execute("DELETE FROM merge_conflicts WHERE id = ?", (conflict_id,))
|
|
423
|
+
if cursor.rowcount == 0:
|
|
424
|
+
return False
|
|
425
|
+
|
|
426
|
+
self._notify_listeners()
|
|
427
|
+
return True
|
|
428
|
+
|
|
429
|
+
def list_conflicts(
|
|
430
|
+
self,
|
|
431
|
+
resolution_id: str | None = None,
|
|
432
|
+
file_path: str | None = None,
|
|
433
|
+
status: str | None = None,
|
|
434
|
+
limit: int = 100,
|
|
435
|
+
offset: int = 0,
|
|
436
|
+
) -> list[MergeConflict]:
|
|
437
|
+
"""List conflicts with optional filters.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
resolution_id: Filter by resolution ID
|
|
441
|
+
file_path: Filter by file path
|
|
442
|
+
status: Filter by status
|
|
443
|
+
limit: Maximum number of results
|
|
444
|
+
offset: Offset for pagination
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
List of matching MergeConflicts
|
|
448
|
+
"""
|
|
449
|
+
query = "SELECT * FROM merge_conflicts WHERE 1=1"
|
|
450
|
+
params: list[Any] = []
|
|
451
|
+
|
|
452
|
+
if resolution_id:
|
|
453
|
+
query += " AND resolution_id = ?"
|
|
454
|
+
params.append(resolution_id)
|
|
455
|
+
|
|
456
|
+
if file_path:
|
|
457
|
+
query += " AND file_path = ?"
|
|
458
|
+
params.append(file_path)
|
|
459
|
+
|
|
460
|
+
if status:
|
|
461
|
+
query += " AND status = ?"
|
|
462
|
+
params.append(status)
|
|
463
|
+
|
|
464
|
+
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
|
465
|
+
params.extend([limit, offset])
|
|
466
|
+
|
|
467
|
+
rows = self.db.fetchall(query, tuple(params))
|
|
468
|
+
return [MergeConflict.from_row(row) for row in rows]
|
|
469
|
+
|
|
470
|
+
# =========================================================================
|
|
471
|
+
# Helper Methods for CLI
|
|
472
|
+
# =========================================================================
|
|
473
|
+
|
|
474
|
+
def get_active_resolution(self, worktree_id: str | None = None) -> MergeResolution | None:
|
|
475
|
+
"""
|
|
476
|
+
Get the current active (pending) merge resolution.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
worktree_id: Optional worktree ID to filter by
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
The most recent pending MergeResolution, or None
|
|
483
|
+
"""
|
|
484
|
+
if worktree_id:
|
|
485
|
+
row = self.db.fetchone(
|
|
486
|
+
"""
|
|
487
|
+
SELECT * FROM merge_resolutions
|
|
488
|
+
WHERE worktree_id = ? AND status = 'pending'
|
|
489
|
+
ORDER BY created_at DESC
|
|
490
|
+
LIMIT 1
|
|
491
|
+
""",
|
|
492
|
+
(worktree_id,),
|
|
493
|
+
)
|
|
494
|
+
else:
|
|
495
|
+
row = self.db.fetchone(
|
|
496
|
+
"""
|
|
497
|
+
SELECT * FROM merge_resolutions
|
|
498
|
+
WHERE status = 'pending'
|
|
499
|
+
ORDER BY created_at DESC
|
|
500
|
+
LIMIT 1
|
|
501
|
+
"""
|
|
502
|
+
)
|
|
503
|
+
return MergeResolution.from_row(row) if row else None
|
|
504
|
+
|
|
505
|
+
def get_conflict_by_path(
|
|
506
|
+
self, file_path: str, resolution_id: str | None = None
|
|
507
|
+
) -> MergeConflict | None:
|
|
508
|
+
"""
|
|
509
|
+
Get a conflict by file path.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
file_path: Path to the conflicting file
|
|
513
|
+
resolution_id: Optional resolution ID to filter by
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
The MergeConflict if found, None otherwise
|
|
517
|
+
"""
|
|
518
|
+
if resolution_id:
|
|
519
|
+
row = self.db.fetchone(
|
|
520
|
+
"""
|
|
521
|
+
SELECT * FROM merge_conflicts
|
|
522
|
+
WHERE file_path = ? AND resolution_id = ?
|
|
523
|
+
""",
|
|
524
|
+
(file_path, resolution_id),
|
|
525
|
+
)
|
|
526
|
+
else:
|
|
527
|
+
# Find any pending conflict with this path
|
|
528
|
+
row = self.db.fetchone(
|
|
529
|
+
"""
|
|
530
|
+
SELECT c.* FROM merge_conflicts c
|
|
531
|
+
JOIN merge_resolutions r ON c.resolution_id = r.id
|
|
532
|
+
WHERE c.file_path = ? AND r.status = 'pending'
|
|
533
|
+
ORDER BY c.created_at DESC
|
|
534
|
+
LIMIT 1
|
|
535
|
+
""",
|
|
536
|
+
(file_path,),
|
|
537
|
+
)
|
|
538
|
+
return MergeConflict.from_row(row) if row else None
|
|
539
|
+
|
|
540
|
+
def has_active_resolution_for_worktree(self, worktree_id: str) -> bool:
|
|
541
|
+
"""
|
|
542
|
+
Check if a worktree has an active (pending) merge resolution.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
worktree_id: Worktree ID to check
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
True if an active resolution exists
|
|
549
|
+
"""
|
|
550
|
+
return self.get_active_resolution(worktree_id) is not None
|