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,547 @@
|
|
|
1
|
+
"""Local worktree storage manager."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from gobby.storage.database import DatabaseProtocol
|
|
12
|
+
from gobby.utils.id import generate_prefixed_id
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WorktreeStatus(str, Enum):
|
|
18
|
+
"""Worktree status values."""
|
|
19
|
+
|
|
20
|
+
ACTIVE = "active"
|
|
21
|
+
STALE = "stale"
|
|
22
|
+
MERGED = "merged"
|
|
23
|
+
ABANDONED = "abandoned"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Worktree:
|
|
28
|
+
"""Worktree data model."""
|
|
29
|
+
|
|
30
|
+
id: str
|
|
31
|
+
project_id: str
|
|
32
|
+
task_id: str | None
|
|
33
|
+
branch_name: str
|
|
34
|
+
worktree_path: str
|
|
35
|
+
base_branch: str
|
|
36
|
+
agent_session_id: str | None
|
|
37
|
+
status: str
|
|
38
|
+
created_at: str
|
|
39
|
+
updated_at: str
|
|
40
|
+
merged_at: str | None
|
|
41
|
+
merge_state: str | None = None # "pending", "resolved", or None
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_row(cls, row: Any) -> Worktree:
|
|
45
|
+
"""Create Worktree from database row."""
|
|
46
|
+
# Handle merge_state which may not exist in older schemas
|
|
47
|
+
merge_state = row.get("merge_state") if hasattr(row, "get") else None
|
|
48
|
+
if merge_state is None:
|
|
49
|
+
try:
|
|
50
|
+
merge_state = row["merge_state"]
|
|
51
|
+
except (KeyError, IndexError):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
return cls(
|
|
55
|
+
id=row["id"],
|
|
56
|
+
project_id=row["project_id"],
|
|
57
|
+
task_id=row["task_id"],
|
|
58
|
+
branch_name=row["branch_name"],
|
|
59
|
+
worktree_path=row["worktree_path"],
|
|
60
|
+
base_branch=row["base_branch"],
|
|
61
|
+
agent_session_id=row["agent_session_id"],
|
|
62
|
+
status=row["status"],
|
|
63
|
+
created_at=row["created_at"],
|
|
64
|
+
updated_at=row["updated_at"],
|
|
65
|
+
merged_at=row["merged_at"],
|
|
66
|
+
merge_state=merge_state,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict[str, Any]:
|
|
70
|
+
"""Convert to dictionary."""
|
|
71
|
+
return {
|
|
72
|
+
"id": self.id,
|
|
73
|
+
"project_id": self.project_id,
|
|
74
|
+
"task_id": self.task_id,
|
|
75
|
+
"branch_name": self.branch_name,
|
|
76
|
+
"worktree_path": self.worktree_path,
|
|
77
|
+
"base_branch": self.base_branch,
|
|
78
|
+
"agent_session_id": self.agent_session_id,
|
|
79
|
+
"status": self.status,
|
|
80
|
+
"created_at": self.created_at,
|
|
81
|
+
"updated_at": self.updated_at,
|
|
82
|
+
"merged_at": self.merged_at,
|
|
83
|
+
"merge_state": self.merge_state,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class LocalWorktreeManager:
|
|
88
|
+
"""Manager for local worktree storage."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, db: DatabaseProtocol):
|
|
91
|
+
"""Initialize with database connection."""
|
|
92
|
+
self.db = db
|
|
93
|
+
|
|
94
|
+
def create(
|
|
95
|
+
self,
|
|
96
|
+
project_id: str,
|
|
97
|
+
branch_name: str,
|
|
98
|
+
worktree_path: str,
|
|
99
|
+
base_branch: str = "main",
|
|
100
|
+
task_id: str | None = None,
|
|
101
|
+
agent_session_id: str | None = None,
|
|
102
|
+
) -> Worktree:
|
|
103
|
+
"""
|
|
104
|
+
Create a new worktree record.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
project_id: Project ID
|
|
108
|
+
branch_name: Git branch name
|
|
109
|
+
worktree_path: Absolute path to worktree directory
|
|
110
|
+
base_branch: Base branch for the worktree
|
|
111
|
+
task_id: Optional task ID to link
|
|
112
|
+
agent_session_id: Optional session ID that owns this worktree
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Created Worktree instance
|
|
116
|
+
"""
|
|
117
|
+
worktree_id = generate_prefixed_id("wt", length=6)
|
|
118
|
+
now = datetime.now(UTC).isoformat()
|
|
119
|
+
|
|
120
|
+
self.db.execute(
|
|
121
|
+
"""
|
|
122
|
+
INSERT INTO worktrees (
|
|
123
|
+
id, project_id, task_id, branch_name, worktree_path,
|
|
124
|
+
base_branch, agent_session_id, status, created_at, updated_at
|
|
125
|
+
)
|
|
126
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
127
|
+
""",
|
|
128
|
+
(
|
|
129
|
+
worktree_id,
|
|
130
|
+
project_id,
|
|
131
|
+
task_id,
|
|
132
|
+
branch_name,
|
|
133
|
+
worktree_path,
|
|
134
|
+
base_branch,
|
|
135
|
+
agent_session_id,
|
|
136
|
+
WorktreeStatus.ACTIVE.value,
|
|
137
|
+
now,
|
|
138
|
+
now,
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return Worktree(
|
|
143
|
+
id=worktree_id,
|
|
144
|
+
project_id=project_id,
|
|
145
|
+
task_id=task_id,
|
|
146
|
+
branch_name=branch_name,
|
|
147
|
+
worktree_path=worktree_path,
|
|
148
|
+
base_branch=base_branch,
|
|
149
|
+
agent_session_id=agent_session_id,
|
|
150
|
+
status=WorktreeStatus.ACTIVE.value,
|
|
151
|
+
created_at=now,
|
|
152
|
+
updated_at=now,
|
|
153
|
+
merged_at=None,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def get(self, worktree_id: str) -> Worktree | None:
|
|
157
|
+
"""Get worktree by ID."""
|
|
158
|
+
row = self.db.fetchone("SELECT * FROM worktrees WHERE id = ?", (worktree_id,))
|
|
159
|
+
return Worktree.from_row(row) if row else None
|
|
160
|
+
|
|
161
|
+
def get_by_path(self, worktree_path: str) -> Worktree | None:
|
|
162
|
+
"""Get worktree by path."""
|
|
163
|
+
row = self.db.fetchone("SELECT * FROM worktrees WHERE worktree_path = ?", (worktree_path,))
|
|
164
|
+
return Worktree.from_row(row) if row else None
|
|
165
|
+
|
|
166
|
+
def get_by_branch(self, project_id: str, branch_name: str) -> Worktree | None:
|
|
167
|
+
"""Get worktree by project and branch name."""
|
|
168
|
+
row = self.db.fetchone(
|
|
169
|
+
"SELECT * FROM worktrees WHERE project_id = ? AND branch_name = ?",
|
|
170
|
+
(project_id, branch_name),
|
|
171
|
+
)
|
|
172
|
+
return Worktree.from_row(row) if row else None
|
|
173
|
+
|
|
174
|
+
def get_by_task(self, task_id: str) -> Worktree | None:
|
|
175
|
+
"""Get worktree linked to a task."""
|
|
176
|
+
row = self.db.fetchone("SELECT * FROM worktrees WHERE task_id = ?", (task_id,))
|
|
177
|
+
return Worktree.from_row(row) if row else None
|
|
178
|
+
|
|
179
|
+
def list_worktrees(
|
|
180
|
+
self,
|
|
181
|
+
project_id: str | None = None,
|
|
182
|
+
status: str | None = None,
|
|
183
|
+
agent_session_id: str | None = None,
|
|
184
|
+
limit: int = 50,
|
|
185
|
+
) -> list[Worktree]:
|
|
186
|
+
"""
|
|
187
|
+
List worktrees with optional filters.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
project_id: Filter by project
|
|
191
|
+
status: Filter by status
|
|
192
|
+
agent_session_id: Filter by owning session
|
|
193
|
+
limit: Maximum number of results
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
List of Worktree instances
|
|
197
|
+
"""
|
|
198
|
+
conditions = []
|
|
199
|
+
params: list[Any] = []
|
|
200
|
+
|
|
201
|
+
if project_id:
|
|
202
|
+
conditions.append("project_id = ?")
|
|
203
|
+
params.append(project_id)
|
|
204
|
+
if status:
|
|
205
|
+
conditions.append("status = ?")
|
|
206
|
+
params.append(status)
|
|
207
|
+
if agent_session_id:
|
|
208
|
+
conditions.append("agent_session_id = ?")
|
|
209
|
+
params.append(agent_session_id)
|
|
210
|
+
|
|
211
|
+
where_clause = " AND ".join(conditions) if conditions else "1=1"
|
|
212
|
+
params.append(limit)
|
|
213
|
+
|
|
214
|
+
# nosec B608: where_clause built from hardcoded condition strings, values parameterized
|
|
215
|
+
rows = self.db.fetchall(
|
|
216
|
+
f"""
|
|
217
|
+
SELECT * FROM worktrees
|
|
218
|
+
WHERE {where_clause}
|
|
219
|
+
ORDER BY created_at DESC
|
|
220
|
+
LIMIT ?
|
|
221
|
+
""", # nosec B608
|
|
222
|
+
tuple(params),
|
|
223
|
+
)
|
|
224
|
+
return [Worktree.from_row(row) for row in rows]
|
|
225
|
+
|
|
226
|
+
# Allowlist of valid worktree column names to prevent SQL injection
|
|
227
|
+
_VALID_UPDATE_FIELDS = frozenset(
|
|
228
|
+
{
|
|
229
|
+
"branch_name",
|
|
230
|
+
"base_branch",
|
|
231
|
+
"worktree_path",
|
|
232
|
+
"status",
|
|
233
|
+
"agent_session_id",
|
|
234
|
+
"task_id",
|
|
235
|
+
"last_activity_at",
|
|
236
|
+
"updated_at",
|
|
237
|
+
"merged_at",
|
|
238
|
+
"merge_state",
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def update(self, worktree_id: str, **fields: Any) -> Worktree | None:
|
|
243
|
+
"""
|
|
244
|
+
Update worktree fields.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
worktree_id: Worktree ID to update
|
|
248
|
+
**fields: Fields to update (must be valid column names)
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Updated Worktree or None if not found
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
ValueError: If any field name is not in the allowlist
|
|
255
|
+
"""
|
|
256
|
+
if not fields:
|
|
257
|
+
return self.get(worktree_id)
|
|
258
|
+
|
|
259
|
+
# Validate field names against allowlist to prevent SQL injection
|
|
260
|
+
invalid_fields = set(fields.keys()) - self._VALID_UPDATE_FIELDS
|
|
261
|
+
if invalid_fields:
|
|
262
|
+
raise ValueError(f"Invalid field names: {invalid_fields}")
|
|
263
|
+
|
|
264
|
+
# Add updated_at timestamp
|
|
265
|
+
fields["updated_at"] = datetime.now(UTC).isoformat()
|
|
266
|
+
|
|
267
|
+
# nosec B608: Fields validated against _VALID_UPDATE_FIELDS allowlist above
|
|
268
|
+
set_clause = ", ".join(f"{key} = ?" for key in fields.keys())
|
|
269
|
+
values = list(fields.values()) + [worktree_id]
|
|
270
|
+
|
|
271
|
+
self.db.execute(
|
|
272
|
+
f"UPDATE worktrees SET {set_clause} WHERE id = ?", # nosec B608
|
|
273
|
+
tuple(values),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return self.get(worktree_id)
|
|
277
|
+
|
|
278
|
+
def delete(self, worktree_id: str) -> bool:
|
|
279
|
+
"""
|
|
280
|
+
Delete worktree record.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
worktree_id: Worktree ID to delete
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
True if deleted, False if not found
|
|
287
|
+
"""
|
|
288
|
+
cursor = self.db.execute("DELETE FROM worktrees WHERE id = ?", (worktree_id,))
|
|
289
|
+
return cursor.rowcount > 0
|
|
290
|
+
|
|
291
|
+
# Status transition methods
|
|
292
|
+
|
|
293
|
+
def claim(self, worktree_id: str, session_id: str) -> Worktree | None:
|
|
294
|
+
"""
|
|
295
|
+
Claim ownership of a worktree for a session.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
worktree_id: Worktree ID
|
|
299
|
+
session_id: Session ID claiming ownership
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Updated Worktree or None if not found
|
|
303
|
+
"""
|
|
304
|
+
return self.update(worktree_id, agent_session_id=session_id)
|
|
305
|
+
|
|
306
|
+
def release(self, worktree_id: str) -> Worktree | None:
|
|
307
|
+
"""
|
|
308
|
+
Release ownership of a worktree.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
worktree_id: Worktree ID
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Updated Worktree or None if not found
|
|
315
|
+
"""
|
|
316
|
+
return self.update(worktree_id, agent_session_id=None)
|
|
317
|
+
|
|
318
|
+
def mark_stale(self, worktree_id: str) -> Worktree | None:
|
|
319
|
+
"""
|
|
320
|
+
Mark worktree as stale (inactive).
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
worktree_id: Worktree ID
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Updated Worktree or None if not found
|
|
327
|
+
"""
|
|
328
|
+
return self.update(worktree_id, status=WorktreeStatus.STALE.value)
|
|
329
|
+
|
|
330
|
+
def mark_merged(self, worktree_id: str) -> Worktree | None:
|
|
331
|
+
"""
|
|
332
|
+
Mark worktree as merged.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
worktree_id: Worktree ID
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Updated Worktree or None if not found
|
|
339
|
+
"""
|
|
340
|
+
now = datetime.now(UTC).isoformat()
|
|
341
|
+
return self.update(
|
|
342
|
+
worktree_id,
|
|
343
|
+
status=WorktreeStatus.MERGED.value,
|
|
344
|
+
merged_at=now,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def mark_abandoned(self, worktree_id: str) -> Worktree | None:
|
|
348
|
+
"""
|
|
349
|
+
Mark worktree as abandoned.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
worktree_id: Worktree ID
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Updated Worktree or None if not found
|
|
356
|
+
"""
|
|
357
|
+
return self.update(worktree_id, status=WorktreeStatus.ABANDONED.value)
|
|
358
|
+
|
|
359
|
+
def find_stale(
|
|
360
|
+
self,
|
|
361
|
+
project_id: str,
|
|
362
|
+
hours: int = 24,
|
|
363
|
+
limit: int = 50,
|
|
364
|
+
) -> list[Worktree]:
|
|
365
|
+
"""
|
|
366
|
+
Find worktrees that are stale (no activity for N hours).
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
project_id: Project ID
|
|
370
|
+
hours: Hours of inactivity threshold
|
|
371
|
+
limit: Maximum number of results
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
List of stale Worktree instances
|
|
375
|
+
"""
|
|
376
|
+
# Calculate cutoff time
|
|
377
|
+
from datetime import timedelta
|
|
378
|
+
|
|
379
|
+
cutoff = (datetime.now(UTC) - timedelta(hours=hours)).isoformat()
|
|
380
|
+
|
|
381
|
+
rows = self.db.fetchall(
|
|
382
|
+
"""
|
|
383
|
+
SELECT * FROM worktrees
|
|
384
|
+
WHERE project_id = ?
|
|
385
|
+
AND status = ?
|
|
386
|
+
AND updated_at < ?
|
|
387
|
+
ORDER BY updated_at ASC
|
|
388
|
+
LIMIT ?
|
|
389
|
+
""",
|
|
390
|
+
(project_id, WorktreeStatus.ACTIVE.value, cutoff, limit),
|
|
391
|
+
)
|
|
392
|
+
return [Worktree.from_row(row) for row in rows]
|
|
393
|
+
|
|
394
|
+
def cleanup_stale(
|
|
395
|
+
self,
|
|
396
|
+
project_id: str,
|
|
397
|
+
hours: int = 24,
|
|
398
|
+
dry_run: bool = True,
|
|
399
|
+
) -> list[Worktree]:
|
|
400
|
+
"""
|
|
401
|
+
Mark stale worktrees as abandoned.
|
|
402
|
+
|
|
403
|
+
This only updates the database status. The actual git worktree
|
|
404
|
+
cleanup should be done by the WorktreeManager after calling this.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
project_id: Project ID
|
|
408
|
+
hours: Hours of inactivity threshold
|
|
409
|
+
dry_run: If True, just return candidates without updating
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
List of worktrees marked/to be marked as abandoned.
|
|
413
|
+
When dry_run is False, returns refreshed worktrees with updated status.
|
|
414
|
+
"""
|
|
415
|
+
stale = self.find_stale(project_id, hours)
|
|
416
|
+
|
|
417
|
+
if not dry_run:
|
|
418
|
+
updated: list[Worktree] = []
|
|
419
|
+
for worktree in stale:
|
|
420
|
+
# mark_abandoned returns the updated Worktree
|
|
421
|
+
result = self.mark_abandoned(worktree.id)
|
|
422
|
+
if result is not None:
|
|
423
|
+
updated.append(result)
|
|
424
|
+
return updated
|
|
425
|
+
|
|
426
|
+
return stale
|
|
427
|
+
|
|
428
|
+
def count_by_status(self, project_id: str) -> dict[str, int]:
|
|
429
|
+
"""
|
|
430
|
+
Get count of worktrees by status for a project.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
project_id: Project ID
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Dict mapping status to count
|
|
437
|
+
"""
|
|
438
|
+
rows = self.db.fetchall(
|
|
439
|
+
"""
|
|
440
|
+
SELECT status, COUNT(*) as count
|
|
441
|
+
FROM worktrees
|
|
442
|
+
WHERE project_id = ?
|
|
443
|
+
GROUP BY status
|
|
444
|
+
""",
|
|
445
|
+
(project_id,),
|
|
446
|
+
)
|
|
447
|
+
return {row["status"]: row["count"] for row in rows}
|
|
448
|
+
|
|
449
|
+
# Merge state methods
|
|
450
|
+
|
|
451
|
+
def set_merge_state(self, worktree_id: str, merge_state: str | None) -> Worktree | None:
|
|
452
|
+
"""
|
|
453
|
+
Set the merge state for a worktree.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
worktree_id: Worktree ID
|
|
457
|
+
merge_state: Merge state ("pending", "resolved", or None)
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Updated Worktree or None if not found
|
|
461
|
+
"""
|
|
462
|
+
return self.update(worktree_id, merge_state=merge_state)
|
|
463
|
+
|
|
464
|
+
def get_by_merge_state(
|
|
465
|
+
self,
|
|
466
|
+
merge_state: str,
|
|
467
|
+
project_id: str | None = None,
|
|
468
|
+
limit: int = 50,
|
|
469
|
+
) -> list[Worktree]:
|
|
470
|
+
"""
|
|
471
|
+
Get worktrees by merge state.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
merge_state: Merge state to filter by
|
|
475
|
+
project_id: Optional project ID filter
|
|
476
|
+
limit: Maximum number of results
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
List of Worktree instances with the given merge state
|
|
480
|
+
"""
|
|
481
|
+
if project_id:
|
|
482
|
+
rows = self.db.fetchall(
|
|
483
|
+
"""
|
|
484
|
+
SELECT * FROM worktrees
|
|
485
|
+
WHERE merge_state = ? AND project_id = ?
|
|
486
|
+
ORDER BY updated_at DESC
|
|
487
|
+
LIMIT ?
|
|
488
|
+
""",
|
|
489
|
+
(merge_state, project_id, limit),
|
|
490
|
+
)
|
|
491
|
+
else:
|
|
492
|
+
rows = self.db.fetchall(
|
|
493
|
+
"""
|
|
494
|
+
SELECT * FROM worktrees
|
|
495
|
+
WHERE merge_state = ?
|
|
496
|
+
ORDER BY updated_at DESC
|
|
497
|
+
LIMIT ?
|
|
498
|
+
""",
|
|
499
|
+
(merge_state, limit),
|
|
500
|
+
)
|
|
501
|
+
return [Worktree.from_row(row) for row in rows]
|
|
502
|
+
|
|
503
|
+
def sync_with_merge_resolution(
|
|
504
|
+
self,
|
|
505
|
+
worktree_id: str,
|
|
506
|
+
merge_manager: Any | None = None,
|
|
507
|
+
strategy: str = "auto",
|
|
508
|
+
) -> dict[str, Any]:
|
|
509
|
+
"""
|
|
510
|
+
Sync worktree with merge resolution support.
|
|
511
|
+
|
|
512
|
+
When conflicts are detected during sync, a merge resolution
|
|
513
|
+
is initiated with the specified strategy.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
worktree_id: Worktree ID
|
|
517
|
+
merge_manager: MergeResolutionManager for creating resolutions
|
|
518
|
+
strategy: Resolution strategy ("auto", "ai-only", "human")
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
Dict with sync result and optional merge info
|
|
522
|
+
"""
|
|
523
|
+
worktree = self.get(worktree_id)
|
|
524
|
+
if not worktree:
|
|
525
|
+
return {"success": False, "error": "Worktree not found"}
|
|
526
|
+
|
|
527
|
+
# Placeholder: actual sync would involve git operations
|
|
528
|
+
# and detection of merge conflicts
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
"success": True,
|
|
532
|
+
"worktree_id": worktree_id,
|
|
533
|
+
"merge_initiated": False,
|
|
534
|
+
"message": "Sync completed without conflicts",
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
def sync(self, worktree_id: str) -> dict[str, Any]:
|
|
538
|
+
"""
|
|
539
|
+
Basic sync without merge resolution.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
worktree_id: Worktree ID
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
Dict with sync result
|
|
546
|
+
"""
|
|
547
|
+
return self.sync_with_merge_resolution(worktree_id)
|
gobby/sync/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Sync services for external integrations.
|
|
2
|
+
|
|
3
|
+
This module provides sync services that orchestrate between gobby tasks
|
|
4
|
+
and external services like GitHub and Linear.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from gobby.sync.github import (
|
|
8
|
+
GitHubNotFoundError,
|
|
9
|
+
GitHubRateLimitError,
|
|
10
|
+
GitHubSyncError,
|
|
11
|
+
GitHubSyncService,
|
|
12
|
+
)
|
|
13
|
+
from gobby.sync.linear import (
|
|
14
|
+
LinearNotFoundError,
|
|
15
|
+
LinearRateLimitError,
|
|
16
|
+
LinearSyncError,
|
|
17
|
+
LinearSyncService,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"GitHubSyncService",
|
|
22
|
+
"GitHubSyncError",
|
|
23
|
+
"GitHubRateLimitError",
|
|
24
|
+
"GitHubNotFoundError",
|
|
25
|
+
"LinearSyncService",
|
|
26
|
+
"LinearSyncError",
|
|
27
|
+
"LinearRateLimitError",
|
|
28
|
+
"LinearNotFoundError",
|
|
29
|
+
]
|