gobby 0.2.5__py3-none-any.whl → 0.2.7__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 +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +13 -4
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +395 -0
- gobby/agents/runner.py +8 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +385 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +15 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +8 -8
- gobby/cli/installers/shared.py +175 -13
- gobby/cli/sessions.py +1 -1
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +12 -5
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +69 -91
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +9 -41
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +188 -2
- gobby/hooks/hook_manager.py +50 -4
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/skill_manager.py +130 -0
- gobby/hooks/webhooks.py +1 -1
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +98 -35
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +56 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -8
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +518 -0
- gobby/mcp_proxy/tools/memory.py +3 -26
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
- gobby/mcp_proxy/tools/skills/__init__.py +616 -0
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -338
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +73 -285
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/runner.py +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +24 -12
- gobby/servers/routes/admin.py +294 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +1 -1
- gobby/servers/routes/mcp/tools.py +48 -1317
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +2 -0
- gobby/sessions/transcripts/claude.py +79 -10
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +286 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +162 -201
- gobby/storage/sessions.py +116 -7
- gobby/storage/skills.py +782 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +57 -7
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +40 -5
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +46 -35
- gobby/tools/summarizer.py +91 -10
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1135
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +93 -1
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +269 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
- gobby/workflows/engine.py +13 -2
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/loader.py +19 -6
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +154 -0
- gobby/workflows/safe_evaluator.py +183 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +111 -1
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1292
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/session_messages.py +0 -1056
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- gobby/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type definitions and data classes for Codex adapter.
|
|
3
|
+
|
|
4
|
+
Extracted from codex.py as part of Phase 3 Strangler Fig decomposition.
|
|
5
|
+
These types are used throughout the Codex adapter implementation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CodexConnectionState(Enum):
|
|
17
|
+
"""Connection state for the Codex app-server."""
|
|
18
|
+
|
|
19
|
+
DISCONNECTED = "disconnected"
|
|
20
|
+
CONNECTING = "connecting"
|
|
21
|
+
CONNECTED = "connected"
|
|
22
|
+
ERROR = "error"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CodexThread:
|
|
27
|
+
"""Represents a Codex conversation thread."""
|
|
28
|
+
|
|
29
|
+
id: str
|
|
30
|
+
preview: str = ""
|
|
31
|
+
model_provider: str = "openai"
|
|
32
|
+
created_at: int = 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class CodexTurn:
|
|
37
|
+
"""Represents a turn in a Codex conversation."""
|
|
38
|
+
|
|
39
|
+
id: str
|
|
40
|
+
thread_id: str
|
|
41
|
+
status: str = "pending"
|
|
42
|
+
items: list[dict[str, Any]] = field(default_factory=list)
|
|
43
|
+
error: str | None = None
|
|
44
|
+
usage: dict[str, int] | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class CodexItem:
|
|
49
|
+
"""Represents an item (message, tool call, etc.) in a turn."""
|
|
50
|
+
|
|
51
|
+
id: str
|
|
52
|
+
type: str # "reasoning", "agent_message", "command_execution", "user_message", etc.
|
|
53
|
+
content: str = ""
|
|
54
|
+
status: str = "pending"
|
|
55
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Type alias for notification handlers
|
|
59
|
+
NotificationHandler = Callable[[str, dict[str, Any]], None]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
__all__ = [
|
|
63
|
+
"CodexConnectionState",
|
|
64
|
+
"CodexThread",
|
|
65
|
+
"CodexTurn",
|
|
66
|
+
"CodexItem",
|
|
67
|
+
"NotificationHandler",
|
|
68
|
+
]
|
gobby/agents/definitions.py
CHANGED
|
@@ -8,11 +8,12 @@ lifecycle behavior, solving recursion loops in delegation.
|
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any, Literal
|
|
12
12
|
|
|
13
13
|
import yaml
|
|
14
14
|
from pydantic import BaseModel, Field
|
|
15
15
|
|
|
16
|
+
from gobby.agents.sandbox import SandboxConfig
|
|
16
17
|
from gobby.utils.project_context import get_project_context
|
|
17
18
|
|
|
18
19
|
logger = logging.getLogger(__name__)
|
|
@@ -29,6 +30,15 @@ class AgentDefinition(BaseModel):
|
|
|
29
30
|
# Execution parameters
|
|
30
31
|
model: str | None = None
|
|
31
32
|
mode: str = "headless" # Default to headless for stability
|
|
33
|
+
provider: str = "claude" # Provider: claude, gemini, codex
|
|
34
|
+
|
|
35
|
+
# Isolation configuration
|
|
36
|
+
isolation: Literal["current", "worktree", "clone"] | None = None
|
|
37
|
+
branch_prefix: str | None = None
|
|
38
|
+
base_branch: str = "main"
|
|
39
|
+
|
|
40
|
+
# Sandbox configuration
|
|
41
|
+
sandbox: SandboxConfig | None = None
|
|
32
42
|
|
|
33
43
|
# Workflow configuration
|
|
34
44
|
workflow: str | None = None
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Isolation Handlers for Unified spawn_agent API.
|
|
3
|
+
|
|
4
|
+
This module provides the abstraction layer for different isolation modes:
|
|
5
|
+
- current: Work in the current directory (no isolation)
|
|
6
|
+
- worktree: Create/reuse a git worktree for isolated work
|
|
7
|
+
- clone: Create a shallow clone for full isolation
|
|
8
|
+
|
|
9
|
+
Each handler implements the IsolationHandler ABC to provide:
|
|
10
|
+
- Environment preparation (worktree/clone creation)
|
|
11
|
+
- Context prompt building (adding isolation warnings)
|
|
12
|
+
- Branch name generation
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import time
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Any, Literal
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class IsolationContext:
|
|
23
|
+
"""Result of environment preparation."""
|
|
24
|
+
|
|
25
|
+
cwd: str
|
|
26
|
+
branch_name: str | None = None
|
|
27
|
+
worktree_id: str | None = None
|
|
28
|
+
clone_id: str | None = None
|
|
29
|
+
isolation_type: Literal["current", "worktree", "clone"] = "current"
|
|
30
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class SpawnConfig:
|
|
35
|
+
"""Configuration passed to isolation handlers."""
|
|
36
|
+
|
|
37
|
+
prompt: str
|
|
38
|
+
task_id: str | None
|
|
39
|
+
task_title: str | None
|
|
40
|
+
task_seq_num: int | None
|
|
41
|
+
branch_name: str | None
|
|
42
|
+
branch_prefix: str | None
|
|
43
|
+
base_branch: str
|
|
44
|
+
project_id: str
|
|
45
|
+
project_path: str
|
|
46
|
+
provider: str
|
|
47
|
+
parent_session_id: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def generate_branch_name(config: SpawnConfig) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Auto-generate branch name from task or fallback to prefix+timestamp.
|
|
53
|
+
|
|
54
|
+
Priority:
|
|
55
|
+
1. Explicit branch_name if provided
|
|
56
|
+
2. task-{seq_num}-{slugified_title} if task info available
|
|
57
|
+
3. {branch_prefix}{timestamp} as fallback (default prefix: "agent/")
|
|
58
|
+
"""
|
|
59
|
+
if config.branch_name:
|
|
60
|
+
return config.branch_name
|
|
61
|
+
|
|
62
|
+
if config.task_seq_num and config.task_title:
|
|
63
|
+
# Generate slug from task title
|
|
64
|
+
slug = config.task_title.lower().replace(" ", "-")
|
|
65
|
+
# Keep only alphanumeric and hyphens
|
|
66
|
+
slug = "".join(c for c in slug if c.isalnum() or c == "-")
|
|
67
|
+
# Truncate to 40 chars
|
|
68
|
+
slug = slug[:40]
|
|
69
|
+
return f"task-{config.task_seq_num}-{slug}"
|
|
70
|
+
|
|
71
|
+
# Fallback to prefix + timestamp
|
|
72
|
+
prefix = config.branch_prefix or "agent/"
|
|
73
|
+
return f"{prefix}{int(time.time())}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class IsolationHandler(ABC):
|
|
77
|
+
"""Abstract base class for isolation handlers."""
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
async def prepare_environment(self, config: SpawnConfig) -> IsolationContext:
|
|
81
|
+
"""
|
|
82
|
+
Prepare isolated environment (worktree/clone creation).
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
config: Spawn configuration with project and task info
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
IsolationContext with cwd and isolation metadata
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def build_context_prompt(self, original_prompt: str, ctx: IsolationContext) -> str:
|
|
93
|
+
"""
|
|
94
|
+
Build prompt with isolation context warnings.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
original_prompt: The original user prompt
|
|
98
|
+
ctx: Isolation context from prepare_environment
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Enhanced prompt with isolation context (or unchanged for current)
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class CurrentIsolationHandler(IsolationHandler):
|
|
106
|
+
"""
|
|
107
|
+
No isolation - work in current directory.
|
|
108
|
+
|
|
109
|
+
This is the simplest handler that just returns the project path
|
|
110
|
+
as the working directory without any git branch changes.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
async def prepare_environment(self, config: SpawnConfig) -> IsolationContext:
|
|
114
|
+
"""Return project path as working directory."""
|
|
115
|
+
return IsolationContext(
|
|
116
|
+
cwd=config.project_path,
|
|
117
|
+
isolation_type="current",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def build_context_prompt(self, original_prompt: str, ctx: IsolationContext) -> str:
|
|
121
|
+
"""Return prompt unchanged - no additional context needed."""
|
|
122
|
+
return original_prompt
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class WorktreeIsolationHandler(IsolationHandler):
|
|
126
|
+
"""
|
|
127
|
+
Worktree isolation - create/reuse a git worktree for isolated work.
|
|
128
|
+
|
|
129
|
+
This handler:
|
|
130
|
+
- Checks for existing worktrees by branch name
|
|
131
|
+
- Creates new worktrees if needed
|
|
132
|
+
- Copies project.json and installs hooks
|
|
133
|
+
- Adds CRITICAL context warning to prompt
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
git_manager: Any, # WorktreeGitManager
|
|
139
|
+
worktree_storage: Any, # LocalWorktreeManager
|
|
140
|
+
) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Initialize WorktreeIsolationHandler with dependencies.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
git_manager: Git manager for worktree operations
|
|
146
|
+
worktree_storage: Storage for worktree records
|
|
147
|
+
"""
|
|
148
|
+
self._git_manager = git_manager
|
|
149
|
+
self._worktree_storage = worktree_storage
|
|
150
|
+
|
|
151
|
+
async def prepare_environment(self, config: SpawnConfig) -> IsolationContext:
|
|
152
|
+
"""
|
|
153
|
+
Prepare worktree environment.
|
|
154
|
+
|
|
155
|
+
- Generate branch name if not provided
|
|
156
|
+
- Check for existing worktree for the branch
|
|
157
|
+
- Create new worktree if needed
|
|
158
|
+
- Return IsolationContext with worktree info
|
|
159
|
+
"""
|
|
160
|
+
branch_name = generate_branch_name(config)
|
|
161
|
+
|
|
162
|
+
# Check if worktree already exists for this branch
|
|
163
|
+
existing = self._worktree_storage.get_by_branch(config.project_id, branch_name)
|
|
164
|
+
if existing:
|
|
165
|
+
# Use existing worktree
|
|
166
|
+
return IsolationContext(
|
|
167
|
+
cwd=existing.worktree_path,
|
|
168
|
+
branch_name=existing.branch_name,
|
|
169
|
+
worktree_id=existing.id,
|
|
170
|
+
isolation_type="worktree",
|
|
171
|
+
extra={"main_repo_path": self._git_manager.repo_path},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Generate worktree path
|
|
175
|
+
from pathlib import Path
|
|
176
|
+
|
|
177
|
+
project_name = Path(self._git_manager.repo_path).name
|
|
178
|
+
worktree_path = self._generate_worktree_path(branch_name, project_name)
|
|
179
|
+
|
|
180
|
+
# Create git worktree
|
|
181
|
+
result = self._git_manager.create_worktree(
|
|
182
|
+
worktree_path=worktree_path,
|
|
183
|
+
branch_name=branch_name,
|
|
184
|
+
base_branch=config.base_branch,
|
|
185
|
+
create_branch=True,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if not result.success:
|
|
189
|
+
raise RuntimeError(f"Failed to create worktree: {result.error}")
|
|
190
|
+
|
|
191
|
+
# Record in storage
|
|
192
|
+
worktree = self._worktree_storage.create(
|
|
193
|
+
project_id=config.project_id,
|
|
194
|
+
branch_name=branch_name,
|
|
195
|
+
worktree_path=worktree_path,
|
|
196
|
+
base_branch=config.base_branch,
|
|
197
|
+
task_id=config.task_id,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return IsolationContext(
|
|
201
|
+
cwd=worktree.worktree_path,
|
|
202
|
+
branch_name=worktree.branch_name,
|
|
203
|
+
worktree_id=worktree.id,
|
|
204
|
+
isolation_type="worktree",
|
|
205
|
+
extra={"main_repo_path": self._git_manager.repo_path},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def build_context_prompt(self, original_prompt: str, ctx: IsolationContext) -> str:
|
|
209
|
+
"""
|
|
210
|
+
Build prompt with CRITICAL worktree context warning.
|
|
211
|
+
|
|
212
|
+
Prepends isolation context to help the agent understand it's
|
|
213
|
+
working in a worktree, not the main repository.
|
|
214
|
+
"""
|
|
215
|
+
warning = f"""CRITICAL: Worktree Context
|
|
216
|
+
You are working in a git worktree, NOT the main repository.
|
|
217
|
+
- Branch: {ctx.branch_name}
|
|
218
|
+
- Worktree path: {ctx.cwd}
|
|
219
|
+
- Main repo: {ctx.extra.get("main_repo_path", "unknown")}
|
|
220
|
+
|
|
221
|
+
Changes in this worktree are isolated from the main repository.
|
|
222
|
+
Commit your changes to the worktree branch when done.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
"""
|
|
227
|
+
return warning + original_prompt
|
|
228
|
+
|
|
229
|
+
def _generate_worktree_path(self, branch_name: str, project_name: str) -> str:
|
|
230
|
+
"""Generate a unique worktree path in temp directory."""
|
|
231
|
+
import tempfile
|
|
232
|
+
|
|
233
|
+
# Sanitize branch name for use in path
|
|
234
|
+
safe_branch = branch_name.replace("/", "-").replace("\\", "-")
|
|
235
|
+
worktree_dir = tempfile.gettempdir()
|
|
236
|
+
return f"{worktree_dir}/gobby-worktrees/{project_name}/{safe_branch}"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class CloneIsolationHandler(IsolationHandler):
|
|
240
|
+
"""
|
|
241
|
+
Clone isolation - create a shallow clone for full isolation.
|
|
242
|
+
|
|
243
|
+
This handler:
|
|
244
|
+
- Checks for existing clones by branch name
|
|
245
|
+
- Creates new shallow clones if needed
|
|
246
|
+
- Adds CRITICAL context warning to prompt
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
def __init__(
|
|
250
|
+
self,
|
|
251
|
+
clone_manager: Any, # CloneGitManager
|
|
252
|
+
clone_storage: Any, # LocalCloneManager
|
|
253
|
+
) -> None:
|
|
254
|
+
"""
|
|
255
|
+
Initialize CloneIsolationHandler with dependencies.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
clone_manager: Git manager for clone operations
|
|
259
|
+
clone_storage: Storage for clone records
|
|
260
|
+
"""
|
|
261
|
+
self._clone_manager = clone_manager
|
|
262
|
+
self._clone_storage = clone_storage
|
|
263
|
+
|
|
264
|
+
async def prepare_environment(self, config: SpawnConfig) -> IsolationContext:
|
|
265
|
+
"""
|
|
266
|
+
Prepare clone environment.
|
|
267
|
+
|
|
268
|
+
- Generate branch name if not provided
|
|
269
|
+
- Check for existing clone for the branch
|
|
270
|
+
- Create new shallow clone if needed
|
|
271
|
+
- Return IsolationContext with clone info
|
|
272
|
+
"""
|
|
273
|
+
branch_name = generate_branch_name(config)
|
|
274
|
+
|
|
275
|
+
# Check if clone already exists for this branch
|
|
276
|
+
existing = self._clone_storage.get_by_branch(config.project_id, branch_name)
|
|
277
|
+
if existing:
|
|
278
|
+
# Use existing clone
|
|
279
|
+
return IsolationContext(
|
|
280
|
+
cwd=existing.clone_path,
|
|
281
|
+
branch_name=existing.branch_name,
|
|
282
|
+
clone_id=existing.id,
|
|
283
|
+
isolation_type="clone",
|
|
284
|
+
extra={"source_repo": config.project_path},
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Generate clone path
|
|
288
|
+
from pathlib import Path
|
|
289
|
+
|
|
290
|
+
project_name = Path(config.project_path).name
|
|
291
|
+
clone_path = self._generate_clone_path(branch_name, project_name)
|
|
292
|
+
|
|
293
|
+
# Create shallow clone
|
|
294
|
+
result = self._clone_manager.create_clone(
|
|
295
|
+
clone_path=clone_path,
|
|
296
|
+
branch_name=branch_name,
|
|
297
|
+
base_branch=config.base_branch,
|
|
298
|
+
shallow=True,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if not result.success:
|
|
302
|
+
raise RuntimeError(f"Failed to create clone: {result.error}")
|
|
303
|
+
|
|
304
|
+
# Record in storage
|
|
305
|
+
clone = self._clone_storage.create(
|
|
306
|
+
project_id=config.project_id,
|
|
307
|
+
branch_name=branch_name,
|
|
308
|
+
clone_path=clone_path,
|
|
309
|
+
base_branch=config.base_branch,
|
|
310
|
+
task_id=config.task_id,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
return IsolationContext(
|
|
314
|
+
cwd=clone.clone_path,
|
|
315
|
+
branch_name=clone.branch_name,
|
|
316
|
+
clone_id=clone.id,
|
|
317
|
+
isolation_type="clone",
|
|
318
|
+
extra={"source_repo": config.project_path},
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def build_context_prompt(self, original_prompt: str, ctx: IsolationContext) -> str:
|
|
322
|
+
"""
|
|
323
|
+
Build prompt with CRITICAL clone context warning.
|
|
324
|
+
|
|
325
|
+
Prepends isolation context to help the agent understand it's
|
|
326
|
+
working in a clone, not the original repository.
|
|
327
|
+
"""
|
|
328
|
+
warning = f"""CRITICAL: Clone Context
|
|
329
|
+
You are working in a shallow clone, NOT the original repository.
|
|
330
|
+
- Branch: {ctx.branch_name}
|
|
331
|
+
- Clone path: {ctx.cwd}
|
|
332
|
+
- Source repo: {ctx.extra.get("source_repo", "unknown")}
|
|
333
|
+
|
|
334
|
+
Changes in this clone are fully isolated from the original repository.
|
|
335
|
+
Push your changes when ready to share with the original.
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
"""
|
|
340
|
+
return warning + original_prompt
|
|
341
|
+
|
|
342
|
+
def _generate_clone_path(self, branch_name: str, project_name: str) -> str:
|
|
343
|
+
"""Generate a unique clone path in temp directory."""
|
|
344
|
+
import tempfile
|
|
345
|
+
|
|
346
|
+
# Sanitize branch name for use in path
|
|
347
|
+
safe_branch = branch_name.replace("/", "-").replace("\\", "-")
|
|
348
|
+
clone_dir = tempfile.gettempdir()
|
|
349
|
+
return f"{clone_dir}/gobby-clones/{project_name}/{safe_branch}"
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def get_isolation_handler(
|
|
353
|
+
mode: Literal["current", "worktree", "clone"],
|
|
354
|
+
*,
|
|
355
|
+
git_manager: Any | None = None,
|
|
356
|
+
worktree_storage: Any | None = None,
|
|
357
|
+
clone_manager: Any | None = None,
|
|
358
|
+
clone_storage: Any | None = None,
|
|
359
|
+
) -> IsolationHandler:
|
|
360
|
+
"""
|
|
361
|
+
Factory function to get the appropriate isolation handler.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
mode: Isolation mode - 'current', 'worktree', or 'clone'
|
|
365
|
+
git_manager: Git manager for worktree operations (required for 'worktree')
|
|
366
|
+
worktree_storage: Storage for worktree records (required for 'worktree')
|
|
367
|
+
clone_manager: Git manager for clone operations (required for 'clone')
|
|
368
|
+
clone_storage: Storage for clone records (required for 'clone')
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
IsolationHandler instance for the specified mode
|
|
372
|
+
|
|
373
|
+
Raises:
|
|
374
|
+
ValueError: If mode is unknown or required dependencies are missing
|
|
375
|
+
"""
|
|
376
|
+
if mode == "current":
|
|
377
|
+
return CurrentIsolationHandler()
|
|
378
|
+
|
|
379
|
+
if mode == "worktree":
|
|
380
|
+
if git_manager is None or worktree_storage is None:
|
|
381
|
+
raise ValueError("git_manager and worktree_storage are required for worktree isolation")
|
|
382
|
+
return WorktreeIsolationHandler(
|
|
383
|
+
git_manager=git_manager,
|
|
384
|
+
worktree_storage=worktree_storage,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if mode == "clone":
|
|
388
|
+
if clone_manager is None or clone_storage is None:
|
|
389
|
+
raise ValueError("clone_manager and clone_storage are required for clone isolation")
|
|
390
|
+
return CloneIsolationHandler(
|
|
391
|
+
clone_manager=clone_manager,
|
|
392
|
+
clone_storage=clone_storage,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
raise ValueError(f"Unknown isolation mode: {mode}")
|
gobby/agents/runner.py
CHANGED
|
@@ -607,6 +607,14 @@ class AgentRunner:
|
|
|
607
607
|
else:
|
|
608
608
|
self._session_storage.update_status(child_session.id, "failed")
|
|
609
609
|
|
|
610
|
+
# Persist cost to session storage for budget tracking
|
|
611
|
+
if result.cost_info and result.cost_info.total_cost > 0:
|
|
612
|
+
self._session_storage.add_cost(child_session.id, result.cost_info.total_cost)
|
|
613
|
+
self.logger.debug(
|
|
614
|
+
f"Persisted cost ${result.cost_info.total_cost:.4f} "
|
|
615
|
+
f"for session {child_session.id}"
|
|
616
|
+
)
|
|
617
|
+
|
|
610
618
|
# Remove from in-memory tracking
|
|
611
619
|
self._untrack_running_agent(agent_run.id)
|
|
612
620
|
|