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
gobby/cli/memory.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from gobby.cli.utils import resolve_project_ref
|
|
6
|
+
from gobby.config.app import DaemonConfig
|
|
7
|
+
from gobby.memory.manager import MemoryManager
|
|
8
|
+
from gobby.storage.database import LocalDatabase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_memory_manager(ctx: click.Context) -> MemoryManager:
|
|
12
|
+
"""Get memory manager."""
|
|
13
|
+
config: DaemonConfig = ctx.obj["config"]
|
|
14
|
+
db = LocalDatabase()
|
|
15
|
+
|
|
16
|
+
return MemoryManager(db, config.memory)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.group()
|
|
20
|
+
def memory() -> None:
|
|
21
|
+
"""Manage Gobby memories."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@memory.command()
|
|
26
|
+
@click.argument("content")
|
|
27
|
+
@click.option(
|
|
28
|
+
"--type", "-t", "memory_type", default="fact", help="Type of memory (fact, preference, etc.)"
|
|
29
|
+
)
|
|
30
|
+
@click.option("--importance", "-i", type=float, default=0.5, help="Importance (0.0 - 1.0)")
|
|
31
|
+
@click.option("--project", "-p", "project_ref", help="Project (name or UUID)")
|
|
32
|
+
@click.pass_context
|
|
33
|
+
def create(
|
|
34
|
+
ctx: click.Context, content: str, memory_type: str, importance: float, project_ref: str | None
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Create a new memory."""
|
|
37
|
+
project_id = resolve_project_ref(project_ref) if project_ref else None
|
|
38
|
+
manager = get_memory_manager(ctx)
|
|
39
|
+
memory = asyncio.run(
|
|
40
|
+
manager.remember(
|
|
41
|
+
content=content,
|
|
42
|
+
memory_type=memory_type,
|
|
43
|
+
importance=importance,
|
|
44
|
+
project_id=project_id,
|
|
45
|
+
source_type="cli",
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
click.echo(f"Created memory: {memory.id} - {memory.content}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@memory.command()
|
|
52
|
+
@click.argument("query", required=False)
|
|
53
|
+
@click.option("--project", "-p", "project_ref", help="Project (name or UUID)")
|
|
54
|
+
@click.option("--limit", "-n", default=10, help="Max results")
|
|
55
|
+
@click.option("--tags-all", "tags_all", help="Require ALL tags (comma-separated)")
|
|
56
|
+
@click.option("--tags-any", "tags_any", help="Require ANY tag (comma-separated)")
|
|
57
|
+
@click.option("--tags-none", "tags_none", help="Exclude memories with these tags (comma-separated)")
|
|
58
|
+
@click.pass_context
|
|
59
|
+
def recall(
|
|
60
|
+
ctx: click.Context,
|
|
61
|
+
query: str | None,
|
|
62
|
+
project_ref: str | None,
|
|
63
|
+
limit: int,
|
|
64
|
+
tags_all: str | None,
|
|
65
|
+
tags_any: str | None,
|
|
66
|
+
tags_none: str | None,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Retrieve memories with optional tag filtering."""
|
|
69
|
+
project_id = resolve_project_ref(project_ref) if project_ref else None
|
|
70
|
+
manager = get_memory_manager(ctx)
|
|
71
|
+
|
|
72
|
+
# Parse comma-separated tags
|
|
73
|
+
tags_all_list = [t.strip() for t in tags_all.split(",") if t.strip()] if tags_all else None
|
|
74
|
+
tags_any_list = [t.strip() for t in tags_any.split(",") if t.strip()] if tags_any else None
|
|
75
|
+
tags_none_list = [t.strip() for t in tags_none.split(",") if t.strip()] if tags_none else None
|
|
76
|
+
|
|
77
|
+
memories = manager.recall(
|
|
78
|
+
query=query,
|
|
79
|
+
project_id=project_id,
|
|
80
|
+
limit=limit,
|
|
81
|
+
tags_all=tags_all_list,
|
|
82
|
+
tags_any=tags_any_list,
|
|
83
|
+
tags_none=tags_none_list,
|
|
84
|
+
)
|
|
85
|
+
if not memories:
|
|
86
|
+
click.echo("No memories found.")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
for mem in memories:
|
|
90
|
+
tags_str = f" [{', '.join(mem.tags)}]" if mem.tags else ""
|
|
91
|
+
click.echo(f"[{mem.id[:8]}] ({mem.memory_type}, {mem.importance}){tags_str} {mem.content}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@memory.command()
|
|
95
|
+
@click.argument("memory_ref")
|
|
96
|
+
@click.pass_context
|
|
97
|
+
def delete(ctx: click.Context, memory_ref: str) -> None:
|
|
98
|
+
"""Delete a memory by ID (UUID or prefix)."""
|
|
99
|
+
manager = get_memory_manager(ctx)
|
|
100
|
+
memory_id = resolve_memory_id(manager, memory_ref)
|
|
101
|
+
success = manager.forget(memory_id)
|
|
102
|
+
if success:
|
|
103
|
+
click.echo(f"Deleted memory: {memory_id}")
|
|
104
|
+
else:
|
|
105
|
+
click.echo(f"Memory not found: {memory_id}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@memory.command("list")
|
|
109
|
+
@click.option("--type", "-t", "memory_type", help="Filter by memory type")
|
|
110
|
+
@click.option("--min-importance", "-i", type=float, help="Minimum importance threshold")
|
|
111
|
+
@click.option("--limit", "-n", default=50, help="Max results")
|
|
112
|
+
@click.option("--project", "-p", "project_ref", help="Project (name or UUID)")
|
|
113
|
+
@click.option("--tags-all", "tags_all", help="Require ALL tags (comma-separated)")
|
|
114
|
+
@click.option("--tags-any", "tags_any", help="Require ANY tag (comma-separated)")
|
|
115
|
+
@click.option("--tags-none", "tags_none", help="Exclude memories with these tags (comma-separated)")
|
|
116
|
+
@click.pass_context
|
|
117
|
+
def list_memories(
|
|
118
|
+
ctx: click.Context,
|
|
119
|
+
memory_type: str | None,
|
|
120
|
+
min_importance: float | None,
|
|
121
|
+
project_ref: str | None,
|
|
122
|
+
limit: int,
|
|
123
|
+
tags_all: str | None,
|
|
124
|
+
tags_any: str | None,
|
|
125
|
+
tags_none: str | None,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""List all memories with optional filtering."""
|
|
128
|
+
project_id = resolve_project_ref(project_ref) if project_ref else None
|
|
129
|
+
manager = get_memory_manager(ctx)
|
|
130
|
+
|
|
131
|
+
# Parse comma-separated tags
|
|
132
|
+
tags_all_list = [t.strip() for t in tags_all.split(",") if t.strip()] if tags_all else None
|
|
133
|
+
tags_any_list = [t.strip() for t in tags_any.split(",") if t.strip()] if tags_any else None
|
|
134
|
+
tags_none_list = [t.strip() for t in tags_none.split(",") if t.strip()] if tags_none else None
|
|
135
|
+
|
|
136
|
+
memories = manager.list_memories(
|
|
137
|
+
project_id=project_id,
|
|
138
|
+
memory_type=memory_type,
|
|
139
|
+
min_importance=min_importance,
|
|
140
|
+
limit=limit,
|
|
141
|
+
tags_all=tags_all_list,
|
|
142
|
+
tags_any=tags_any_list,
|
|
143
|
+
tags_none=tags_none_list,
|
|
144
|
+
)
|
|
145
|
+
if not memories:
|
|
146
|
+
click.echo("No memories found.")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
for mem in memories:
|
|
150
|
+
tags_str = f" [{', '.join(mem.tags)}]" if mem.tags else ""
|
|
151
|
+
click.echo(f"[{mem.id[:8]}] ({mem.memory_type}, {mem.importance:.2f}){tags_str}")
|
|
152
|
+
click.echo(f" {mem.content[:100]}{'...' if len(mem.content) > 100 else ''}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@memory.command("show")
|
|
156
|
+
@click.argument("memory_ref")
|
|
157
|
+
@click.pass_context
|
|
158
|
+
def show_memory(ctx: click.Context, memory_ref: str) -> None:
|
|
159
|
+
"""Show details of a specific memory (UUID or prefix)."""
|
|
160
|
+
manager = get_memory_manager(ctx)
|
|
161
|
+
memory_id = resolve_memory_id(manager, memory_ref)
|
|
162
|
+
memory = manager.get_memory(memory_id)
|
|
163
|
+
if not memory:
|
|
164
|
+
click.echo(f"Memory not found: {memory_id}")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
click.echo(f"ID: {memory.id}")
|
|
168
|
+
click.echo(f"Type: {memory.memory_type}")
|
|
169
|
+
click.echo(f"Importance: {memory.importance}")
|
|
170
|
+
click.echo(f"Created: {memory.created_at}")
|
|
171
|
+
click.echo(f"Updated: {memory.updated_at}")
|
|
172
|
+
click.echo(f"Source: {memory.source_type}")
|
|
173
|
+
click.echo(f"Access Count: {memory.access_count}")
|
|
174
|
+
if memory.tags:
|
|
175
|
+
click.echo(f"Tags: {', '.join(memory.tags)}")
|
|
176
|
+
click.echo(f"Content:\n{memory.content}")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@memory.command("update")
|
|
180
|
+
@click.argument("memory_ref")
|
|
181
|
+
@click.option("--content", "-c", help="New content")
|
|
182
|
+
@click.option("--importance", "-i", type=float, help="New importance (0.0-1.0)")
|
|
183
|
+
@click.option("--tags", "-t", help="New tags (comma-separated)")
|
|
184
|
+
@click.pass_context
|
|
185
|
+
def update_memory(
|
|
186
|
+
ctx: click.Context,
|
|
187
|
+
memory_ref: str,
|
|
188
|
+
content: str | None,
|
|
189
|
+
importance: float | None,
|
|
190
|
+
tags: str | None,
|
|
191
|
+
) -> None:
|
|
192
|
+
"""Update an existing memory (UUID or prefix)."""
|
|
193
|
+
manager = get_memory_manager(ctx)
|
|
194
|
+
memory_id = resolve_memory_id(manager, memory_ref)
|
|
195
|
+
|
|
196
|
+
# Parse tags if provided
|
|
197
|
+
tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else None
|
|
198
|
+
if tag_list is not None and len(tag_list) == 0:
|
|
199
|
+
tag_list = None
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
memory = manager.update_memory(
|
|
203
|
+
memory_id=memory_id,
|
|
204
|
+
content=content,
|
|
205
|
+
importance=importance,
|
|
206
|
+
tags=tag_list,
|
|
207
|
+
)
|
|
208
|
+
click.echo(f"Updated memory: {memory.id}")
|
|
209
|
+
click.echo(f" Content: {memory.content[:80]}{'...' if len(memory.content) > 80 else ''}")
|
|
210
|
+
click.echo(f" Importance: {memory.importance}")
|
|
211
|
+
except ValueError as e:
|
|
212
|
+
click.echo(f"Error: {e}")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@memory.command("stats")
|
|
216
|
+
@click.option("--project", "-p", "project_ref", help="Project (name or UUID)")
|
|
217
|
+
@click.pass_context
|
|
218
|
+
def memory_stats(ctx: click.Context, project_ref: str | None) -> None:
|
|
219
|
+
"""Show memory system statistics."""
|
|
220
|
+
project_id = resolve_project_ref(project_ref) if project_ref else None
|
|
221
|
+
manager = get_memory_manager(ctx)
|
|
222
|
+
stats = manager.get_stats(project_id=project_id)
|
|
223
|
+
|
|
224
|
+
click.echo("Memory Statistics:")
|
|
225
|
+
click.echo(f" Total Memories: {stats['total_count']}")
|
|
226
|
+
click.echo(f" Average Importance: {stats['avg_importance']:.3f}")
|
|
227
|
+
if stats["by_type"]:
|
|
228
|
+
click.echo(" By Type:")
|
|
229
|
+
for mem_type, count in stats["by_type"].items():
|
|
230
|
+
click.echo(f" {mem_type}: {count}")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@memory.command("export")
|
|
234
|
+
@click.option("--project", "-p", "project_ref", help="Project (name or UUID)")
|
|
235
|
+
@click.option(
|
|
236
|
+
"--output", "-o", "output_file", type=click.Path(), help="Output file (stdout if not specified)"
|
|
237
|
+
)
|
|
238
|
+
@click.option("--no-metadata", is_flag=True, help="Exclude memory metadata")
|
|
239
|
+
@click.option("--no-stats", is_flag=True, help="Exclude summary statistics")
|
|
240
|
+
@click.pass_context
|
|
241
|
+
def export_memories(
|
|
242
|
+
ctx: click.Context,
|
|
243
|
+
project_ref: str | None,
|
|
244
|
+
output_file: str | None,
|
|
245
|
+
no_metadata: bool,
|
|
246
|
+
no_stats: bool,
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Export memories as markdown.
|
|
249
|
+
|
|
250
|
+
Exports all memories (or filtered by project) to a formatted markdown document.
|
|
251
|
+
Output goes to stdout by default, or to a file with --output.
|
|
252
|
+
|
|
253
|
+
Examples:
|
|
254
|
+
|
|
255
|
+
gobby memory export # Export all to stdout
|
|
256
|
+
|
|
257
|
+
gobby memory export -o memories.md # Export to file
|
|
258
|
+
|
|
259
|
+
gobby memory export -p myproject # Export specific project
|
|
260
|
+
|
|
261
|
+
gobby memory export --no-metadata # Content only, no metadata
|
|
262
|
+
"""
|
|
263
|
+
project_id = resolve_project_ref(project_ref) if project_ref else None
|
|
264
|
+
manager = get_memory_manager(ctx)
|
|
265
|
+
|
|
266
|
+
markdown = manager.export_markdown(
|
|
267
|
+
project_id=project_id,
|
|
268
|
+
include_metadata=not no_metadata,
|
|
269
|
+
include_stats=not no_stats,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if output_file:
|
|
273
|
+
from pathlib import Path
|
|
274
|
+
|
|
275
|
+
path = Path(output_file)
|
|
276
|
+
try:
|
|
277
|
+
path.write_text(markdown, encoding="utf-8")
|
|
278
|
+
click.echo(f"Exported memories to {output_file}")
|
|
279
|
+
except OSError as e:
|
|
280
|
+
raise click.ClickException(f"Failed to write to {output_file}: {e}") from e
|
|
281
|
+
else:
|
|
282
|
+
click.echo(markdown)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def resolve_memory_id(manager: MemoryManager, memory_ref: str) -> str:
|
|
286
|
+
"""Resolve memory reference (UUID or prefix) to full ID."""
|
|
287
|
+
# Try exact match first
|
|
288
|
+
# Optimization: check 36 chars?
|
|
289
|
+
if len(memory_ref) == 36 and manager.get_memory(memory_ref):
|
|
290
|
+
return memory_ref
|
|
291
|
+
|
|
292
|
+
# Try prefix match using MemoryManager method
|
|
293
|
+
memories = manager.find_by_prefix(memory_ref, limit=5)
|
|
294
|
+
|
|
295
|
+
if not memories:
|
|
296
|
+
raise click.ClickException(f"Memory not found: {memory_ref}")
|
|
297
|
+
|
|
298
|
+
if len(memories) > 1:
|
|
299
|
+
click.echo(f"Ambiguous memory reference '{memory_ref}' matches:", err=True)
|
|
300
|
+
for mem in memories:
|
|
301
|
+
click.echo(f" {mem.id}", err=True)
|
|
302
|
+
raise click.ClickException(f"Ambiguous memory reference: {memory_ref}")
|
|
303
|
+
|
|
304
|
+
return memories[0].id
|
gobby/cli/merge.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Merge conflict resolution CLI commands.
|
|
3
|
+
|
|
4
|
+
Commands for managing merge operations:
|
|
5
|
+
- start: Start a merge with AI-powered resolution
|
|
6
|
+
- status: Show merge resolution status
|
|
7
|
+
- resolve: Resolve a specific file conflict
|
|
8
|
+
- apply: Apply resolved changes and complete merge
|
|
9
|
+
- abort: Abort the merge operation
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
|
|
17
|
+
from gobby.storage.database import LocalDatabase
|
|
18
|
+
from gobby.storage.merge_resolutions import MergeResolutionManager
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_merge_manager() -> MergeResolutionManager:
|
|
22
|
+
"""Get initialized merge resolution manager."""
|
|
23
|
+
db = LocalDatabase()
|
|
24
|
+
return MergeResolutionManager(db)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_merge_resolver() -> Any:
|
|
28
|
+
"""Get merge resolver for AI-powered resolution."""
|
|
29
|
+
from gobby.worktrees.merge import MergeResolver
|
|
30
|
+
|
|
31
|
+
return MergeResolver()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_project_context() -> dict[str, Any] | None:
|
|
35
|
+
"""Get current project context."""
|
|
36
|
+
import os
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
# Look for .gobby/project.json in current directory or parents
|
|
40
|
+
cwd = Path(os.getcwd())
|
|
41
|
+
for parent in [cwd, *cwd.parents]:
|
|
42
|
+
project_file = parent / ".gobby" / "project.json"
|
|
43
|
+
if project_file.exists():
|
|
44
|
+
import json as json_module
|
|
45
|
+
|
|
46
|
+
result: dict[str, Any] = json_module.loads(project_file.read_text())
|
|
47
|
+
return result
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_worktree_context() -> dict[str, Any] | None:
|
|
52
|
+
"""Get current worktree context if in a worktree."""
|
|
53
|
+
import os
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
|
|
56
|
+
from gobby.storage.worktrees import LocalWorktreeManager
|
|
57
|
+
|
|
58
|
+
db = LocalDatabase()
|
|
59
|
+
manager = LocalWorktreeManager(db)
|
|
60
|
+
|
|
61
|
+
# Check if current directory is a worktree
|
|
62
|
+
cwd = Path(os.getcwd()).resolve()
|
|
63
|
+
worktrees = manager.list_worktrees()
|
|
64
|
+
for wt in worktrees:
|
|
65
|
+
if wt.worktree_path:
|
|
66
|
+
worktree_path = Path(wt.worktree_path).resolve()
|
|
67
|
+
# Use is_relative_to for proper path containment check
|
|
68
|
+
try:
|
|
69
|
+
cwd.relative_to(worktree_path)
|
|
70
|
+
# If we get here, cwd is inside worktree_path
|
|
71
|
+
return {
|
|
72
|
+
"id": wt.id,
|
|
73
|
+
"branch_name": wt.branch_name,
|
|
74
|
+
"worktree_path": wt.worktree_path,
|
|
75
|
+
"base_branch": wt.base_branch,
|
|
76
|
+
}
|
|
77
|
+
except ValueError:
|
|
78
|
+
# cwd is not relative to worktree_path
|
|
79
|
+
continue
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@click.group()
|
|
84
|
+
def merge() -> None:
|
|
85
|
+
"""Manage merge operations with AI-powered conflict resolution."""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@merge.command("start")
|
|
90
|
+
@click.argument("source_branch")
|
|
91
|
+
@click.option(
|
|
92
|
+
"--target",
|
|
93
|
+
"-t",
|
|
94
|
+
"target_branch",
|
|
95
|
+
default="main",
|
|
96
|
+
help="Target branch to merge into (default: main)",
|
|
97
|
+
)
|
|
98
|
+
@click.option(
|
|
99
|
+
"--strategy",
|
|
100
|
+
"-s",
|
|
101
|
+
type=click.Choice(["auto", "ai-only", "human"]),
|
|
102
|
+
default="auto",
|
|
103
|
+
help="Resolution strategy (default: auto)",
|
|
104
|
+
)
|
|
105
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
106
|
+
def merge_start(
|
|
107
|
+
source_branch: str,
|
|
108
|
+
target_branch: str,
|
|
109
|
+
strategy: str,
|
|
110
|
+
json_format: bool,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Start a merge operation with AI-powered conflict resolution.
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
|
|
116
|
+
gobby merge start feature/my-feature
|
|
117
|
+
|
|
118
|
+
gobby merge start feature/auth --target develop --strategy ai-only
|
|
119
|
+
"""
|
|
120
|
+
project = get_project_context()
|
|
121
|
+
if not project:
|
|
122
|
+
click.echo("Error: Not in a Gobby project. Run 'gobby init' first.", err=True)
|
|
123
|
+
raise SystemExit(1)
|
|
124
|
+
|
|
125
|
+
# Get worktree context if available
|
|
126
|
+
worktree = get_worktree_context()
|
|
127
|
+
worktree_id = worktree["id"] if worktree else project.get("id", "default")
|
|
128
|
+
|
|
129
|
+
manager = get_merge_manager()
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
# Create resolution record with strategy
|
|
133
|
+
resolution = manager.create_resolution(
|
|
134
|
+
worktree_id=worktree_id,
|
|
135
|
+
source_branch=source_branch,
|
|
136
|
+
target_branch=target_branch,
|
|
137
|
+
status="pending",
|
|
138
|
+
tier_used=strategy,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if json_format:
|
|
142
|
+
click.echo(json.dumps(resolution.to_dict(), indent=2, default=str))
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
click.echo(f"Started merge: {resolution.id}")
|
|
146
|
+
click.echo(f" Source: {source_branch}")
|
|
147
|
+
click.echo(f" Target: {target_branch}")
|
|
148
|
+
click.echo(f" Strategy: {strategy}")
|
|
149
|
+
click.echo(f" Status: {resolution.status}")
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
click.echo(f"Error starting merge: {e}", err=True)
|
|
153
|
+
raise SystemExit(1) from None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@merge.command("status")
|
|
157
|
+
@click.option("--verbose", "-v", is_flag=True, help="Show detailed conflict information")
|
|
158
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
159
|
+
def merge_status(verbose: bool, json_format: bool) -> None:
|
|
160
|
+
"""Show the status of current merge operation.
|
|
161
|
+
|
|
162
|
+
Examples:
|
|
163
|
+
|
|
164
|
+
gobby merge status
|
|
165
|
+
|
|
166
|
+
gobby merge status --verbose
|
|
167
|
+
"""
|
|
168
|
+
project = get_project_context()
|
|
169
|
+
if not project:
|
|
170
|
+
click.echo("Error: Not in a Gobby project. Run 'gobby init' first.", err=True)
|
|
171
|
+
raise SystemExit(1)
|
|
172
|
+
|
|
173
|
+
manager = get_merge_manager()
|
|
174
|
+
|
|
175
|
+
# Get worktree context for filtering
|
|
176
|
+
worktree = get_worktree_context()
|
|
177
|
+
worktree_id = worktree["id"] if worktree else None
|
|
178
|
+
|
|
179
|
+
# List active resolutions
|
|
180
|
+
resolutions = manager.list_resolutions(
|
|
181
|
+
worktree_id=worktree_id,
|
|
182
|
+
status="pending",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if json_format:
|
|
186
|
+
output = []
|
|
187
|
+
for res in resolutions:
|
|
188
|
+
res_dict = res.to_dict()
|
|
189
|
+
res_dict["conflicts"] = [
|
|
190
|
+
c.to_dict() for c in manager.list_conflicts(resolution_id=res.id)
|
|
191
|
+
]
|
|
192
|
+
output.append(res_dict)
|
|
193
|
+
click.echo(json.dumps(output, indent=2, default=str))
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
if not resolutions:
|
|
197
|
+
click.echo("No active merge operations found.")
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
for res in resolutions:
|
|
201
|
+
conflicts = manager.list_conflicts(resolution_id=res.id)
|
|
202
|
+
pending_count = sum(1 for c in conflicts if c.status == "pending")
|
|
203
|
+
resolved_count = sum(1 for c in conflicts if c.status == "resolved")
|
|
204
|
+
|
|
205
|
+
click.echo(f"Merge: {res.id}")
|
|
206
|
+
click.echo(f" Source: {res.source_branch} -> {res.target_branch}")
|
|
207
|
+
click.echo(f" Status: {res.status}")
|
|
208
|
+
click.echo(f" Conflicts: {pending_count} pending, {resolved_count} resolved")
|
|
209
|
+
|
|
210
|
+
if verbose and conflicts:
|
|
211
|
+
click.echo(" Files:")
|
|
212
|
+
for conflict in conflicts:
|
|
213
|
+
status_icon = "✓" if conflict.status == "resolved" else "○"
|
|
214
|
+
click.echo(f" {status_icon} {conflict.file_path} ({conflict.status})")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@merge.command("resolve")
|
|
218
|
+
@click.argument("file_path")
|
|
219
|
+
@click.option(
|
|
220
|
+
"--strategy",
|
|
221
|
+
"-s",
|
|
222
|
+
type=click.Choice(["ai", "human"]),
|
|
223
|
+
default="ai",
|
|
224
|
+
help="Resolution strategy (default: ai)",
|
|
225
|
+
)
|
|
226
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
227
|
+
def merge_resolve(file_path: str, strategy: str, json_format: bool) -> None:
|
|
228
|
+
"""Resolve a specific file conflict.
|
|
229
|
+
|
|
230
|
+
Examples:
|
|
231
|
+
|
|
232
|
+
gobby merge resolve src/main.py
|
|
233
|
+
|
|
234
|
+
gobby merge resolve src/config.py --strategy human
|
|
235
|
+
"""
|
|
236
|
+
project = get_project_context()
|
|
237
|
+
if not project:
|
|
238
|
+
click.echo("Error: Not in a Gobby project. Run 'gobby init' first.", err=True)
|
|
239
|
+
raise SystemExit(1)
|
|
240
|
+
|
|
241
|
+
manager = get_merge_manager()
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
# Find conflict by file path
|
|
245
|
+
conflict = manager.get_conflict_by_path(file_path)
|
|
246
|
+
if not conflict:
|
|
247
|
+
click.echo(f"Error: No conflict found for file '{file_path}'", err=True)
|
|
248
|
+
raise SystemExit(1)
|
|
249
|
+
|
|
250
|
+
if strategy == "ai":
|
|
251
|
+
# AI resolution
|
|
252
|
+
get_merge_resolver() # Validates resolver is available
|
|
253
|
+
# Would call AI resolver here
|
|
254
|
+
click.echo(f"Resolving {file_path} with AI...")
|
|
255
|
+
manager.update_conflict(conflict.id, status="resolved")
|
|
256
|
+
else:
|
|
257
|
+
# Human resolution - just mark as pending human review
|
|
258
|
+
click.echo(f"Marked {file_path} for human resolution")
|
|
259
|
+
|
|
260
|
+
if json_format:
|
|
261
|
+
updated = manager.get_conflict(conflict.id)
|
|
262
|
+
if updated:
|
|
263
|
+
click.echo(json.dumps(updated.to_dict(), indent=2, default=str))
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
click.echo(f"Resolved: {file_path}")
|
|
267
|
+
|
|
268
|
+
except AttributeError:
|
|
269
|
+
# get_conflict_by_path may not exist
|
|
270
|
+
click.echo(f"Error: Conflict not found for '{file_path}'", err=True)
|
|
271
|
+
raise SystemExit(1) from None
|
|
272
|
+
except Exception as e:
|
|
273
|
+
click.echo(f"Error resolving conflict: {e}", err=True)
|
|
274
|
+
raise SystemExit(1) from None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@merge.command("apply")
|
|
278
|
+
@click.option("--force", "-f", is_flag=True, help="Force apply even with pending conflicts")
|
|
279
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
280
|
+
def merge_apply(force: bool, json_format: bool) -> None:
|
|
281
|
+
"""Apply resolved changes and complete the merge.
|
|
282
|
+
|
|
283
|
+
Examples:
|
|
284
|
+
|
|
285
|
+
gobby merge apply
|
|
286
|
+
|
|
287
|
+
gobby merge apply --force
|
|
288
|
+
"""
|
|
289
|
+
project = get_project_context()
|
|
290
|
+
if not project:
|
|
291
|
+
click.echo("Error: Not in a Gobby project. Run 'gobby init' first.", err=True)
|
|
292
|
+
raise SystemExit(1)
|
|
293
|
+
|
|
294
|
+
manager = get_merge_manager()
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
# Get active resolution
|
|
298
|
+
resolution = manager.get_active_resolution()
|
|
299
|
+
if not resolution:
|
|
300
|
+
click.echo("Error: No active merge operation found.", err=True)
|
|
301
|
+
raise SystemExit(1)
|
|
302
|
+
|
|
303
|
+
# Check for pending conflicts
|
|
304
|
+
conflicts = manager.list_conflicts(resolution_id=resolution.id)
|
|
305
|
+
pending = [c for c in conflicts if c.status == "pending"]
|
|
306
|
+
|
|
307
|
+
if pending and not force:
|
|
308
|
+
click.echo(
|
|
309
|
+
f"Error: {len(pending)} pending conflict(s). "
|
|
310
|
+
"Resolve them or use --force to apply anyway.",
|
|
311
|
+
err=True,
|
|
312
|
+
)
|
|
313
|
+
raise SystemExit(1)
|
|
314
|
+
|
|
315
|
+
# Apply merge
|
|
316
|
+
manager.update_resolution(resolution.id, status="resolved")
|
|
317
|
+
|
|
318
|
+
if json_format:
|
|
319
|
+
updated = manager.get_resolution(resolution.id)
|
|
320
|
+
if updated:
|
|
321
|
+
click.echo(json.dumps(updated.to_dict(), indent=2, default=str))
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
click.echo(f"Applied merge: {resolution.id}")
|
|
325
|
+
click.echo(f" {len(conflicts)} file(s) merged")
|
|
326
|
+
|
|
327
|
+
except AttributeError:
|
|
328
|
+
# get_active_resolution may not exist
|
|
329
|
+
click.echo("Error: No active merge operation found.", err=True)
|
|
330
|
+
raise SystemExit(1) from None
|
|
331
|
+
except Exception as e:
|
|
332
|
+
click.echo(f"Error applying merge: {e}", err=True)
|
|
333
|
+
raise SystemExit(1) from None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@merge.command("abort")
|
|
337
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
338
|
+
def merge_abort(json_format: bool) -> None:
|
|
339
|
+
"""Abort the current merge operation.
|
|
340
|
+
|
|
341
|
+
Examples:
|
|
342
|
+
|
|
343
|
+
gobby merge abort
|
|
344
|
+
"""
|
|
345
|
+
project = get_project_context()
|
|
346
|
+
if not project:
|
|
347
|
+
click.echo("Error: Not in a Gobby project. Run 'gobby init' first.", err=True)
|
|
348
|
+
raise SystemExit(1)
|
|
349
|
+
|
|
350
|
+
manager = get_merge_manager()
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
# Get active resolution
|
|
354
|
+
resolution = manager.get_active_resolution()
|
|
355
|
+
if not resolution:
|
|
356
|
+
click.echo("Error: No active merge operation to abort.", err=True)
|
|
357
|
+
raise SystemExit(1)
|
|
358
|
+
|
|
359
|
+
# Check if already resolved
|
|
360
|
+
if resolution.status == "resolved":
|
|
361
|
+
click.echo("Error: Cannot abort an already resolved merge.", err=True)
|
|
362
|
+
raise SystemExit(1)
|
|
363
|
+
|
|
364
|
+
# Delete resolution (cascades to conflicts)
|
|
365
|
+
resolution_id = resolution.id
|
|
366
|
+
deleted = manager.delete_resolution(resolution_id)
|
|
367
|
+
|
|
368
|
+
if json_format:
|
|
369
|
+
click.echo(json.dumps({"aborted": deleted, "resolution_id": resolution_id}))
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
if deleted:
|
|
373
|
+
click.echo(f"Aborted merge: {resolution_id}")
|
|
374
|
+
else:
|
|
375
|
+
click.echo("Failed to abort merge.", err=True)
|
|
376
|
+
raise SystemExit(1)
|
|
377
|
+
|
|
378
|
+
except AttributeError:
|
|
379
|
+
# get_active_resolution may not exist
|
|
380
|
+
click.echo("Error: No active merge operation to abort.", err=True)
|
|
381
|
+
raise SystemExit(1) from None
|
|
382
|
+
except Exception as e:
|
|
383
|
+
click.echo(f"Error aborting merge: {e}", err=True)
|
|
384
|
+
raise SystemExit(1) from None
|