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/agents/spawn.py
ADDED
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
"""Terminal spawning for agent execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import tempfile
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from gobby.agents.constants import get_terminal_env_vars
|
|
13
|
+
from gobby.agents.session import ChildSessionConfig, ChildSessionManager
|
|
14
|
+
from gobby.agents.spawners import (
|
|
15
|
+
AlacrittySpawner,
|
|
16
|
+
CmdSpawner,
|
|
17
|
+
EmbeddedSpawner,
|
|
18
|
+
GhosttySpawner,
|
|
19
|
+
GnomeTerminalSpawner,
|
|
20
|
+
HeadlessSpawner,
|
|
21
|
+
ITermSpawner,
|
|
22
|
+
KittySpawner,
|
|
23
|
+
KonsoleSpawner,
|
|
24
|
+
PowerShellSpawner,
|
|
25
|
+
SpawnMode,
|
|
26
|
+
SpawnResult,
|
|
27
|
+
TerminalAppSpawner,
|
|
28
|
+
TerminalSpawnerBase,
|
|
29
|
+
TerminalType,
|
|
30
|
+
TmuxSpawner,
|
|
31
|
+
WindowsTerminalSpawner,
|
|
32
|
+
WSLSpawner,
|
|
33
|
+
)
|
|
34
|
+
from gobby.agents.spawners.base import EmbeddedPTYResult, HeadlessResult
|
|
35
|
+
from gobby.agents.tty_config import get_tty_config
|
|
36
|
+
|
|
37
|
+
# Re-export for backward compatibility - these types moved to spawners/ package
|
|
38
|
+
__all__ = [
|
|
39
|
+
# Enums
|
|
40
|
+
"SpawnMode",
|
|
41
|
+
"TerminalType",
|
|
42
|
+
# Result dataclasses
|
|
43
|
+
"SpawnResult",
|
|
44
|
+
"EmbeddedPTYResult",
|
|
45
|
+
"HeadlessResult",
|
|
46
|
+
# Base class
|
|
47
|
+
"TerminalSpawnerBase",
|
|
48
|
+
# Orchestrator
|
|
49
|
+
"TerminalSpawner",
|
|
50
|
+
# Spawner implementations
|
|
51
|
+
"GhosttySpawner",
|
|
52
|
+
"ITermSpawner",
|
|
53
|
+
"TerminalAppSpawner",
|
|
54
|
+
"KittySpawner",
|
|
55
|
+
"AlacrittySpawner",
|
|
56
|
+
"GnomeTerminalSpawner",
|
|
57
|
+
"KonsoleSpawner",
|
|
58
|
+
"WindowsTerminalSpawner",
|
|
59
|
+
"CmdSpawner",
|
|
60
|
+
"PowerShellSpawner",
|
|
61
|
+
"WSLSpawner",
|
|
62
|
+
"TmuxSpawner",
|
|
63
|
+
"EmbeddedSpawner",
|
|
64
|
+
"HeadlessSpawner",
|
|
65
|
+
# Helpers
|
|
66
|
+
"PreparedSpawn",
|
|
67
|
+
"prepare_terminal_spawn",
|
|
68
|
+
"prepare_gemini_spawn_with_preflight",
|
|
69
|
+
"prepare_codex_spawn_with_preflight",
|
|
70
|
+
"read_prompt_from_env",
|
|
71
|
+
"build_cli_command",
|
|
72
|
+
"build_gemini_command_with_resume",
|
|
73
|
+
"build_codex_command_with_resume",
|
|
74
|
+
"MAX_ENV_PROMPT_LENGTH",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
# Maximum prompt length to pass via environment variable
|
|
78
|
+
# Longer prompts will be written to a temp file
|
|
79
|
+
MAX_ENV_PROMPT_LENGTH = 4096
|
|
80
|
+
|
|
81
|
+
logger = logging.getLogger(__name__)
|
|
82
|
+
|
|
83
|
+
# Module-level set for tracking prompt files to clean up on exit
|
|
84
|
+
# This avoids registering a new atexit handler for each prompt file
|
|
85
|
+
_prompt_files_to_cleanup: set[Path] = set()
|
|
86
|
+
_atexit_registered = False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _cleanup_all_prompt_files() -> None:
|
|
90
|
+
"""Clean up all tracked prompt files on process exit."""
|
|
91
|
+
for prompt_path in list(_prompt_files_to_cleanup):
|
|
92
|
+
try:
|
|
93
|
+
if prompt_path.exists():
|
|
94
|
+
prompt_path.unlink()
|
|
95
|
+
except OSError:
|
|
96
|
+
pass
|
|
97
|
+
_prompt_files_to_cleanup.clear()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _create_prompt_file(prompt: str, session_id: str) -> str:
|
|
101
|
+
"""
|
|
102
|
+
Create a prompt file with secure permissions.
|
|
103
|
+
|
|
104
|
+
The file is created in the system temp directory with restrictive
|
|
105
|
+
permissions (owner read/write only) and tracked for cleanup on exit.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
prompt: The prompt content to write
|
|
109
|
+
session_id: Session ID for naming the file
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Path to the created temp file
|
|
113
|
+
"""
|
|
114
|
+
global _atexit_registered
|
|
115
|
+
|
|
116
|
+
# Create temp directory with restrictive permissions
|
|
117
|
+
temp_dir = Path(tempfile.gettempdir()) / "gobby-prompts"
|
|
118
|
+
temp_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
119
|
+
|
|
120
|
+
# Create the prompt file path
|
|
121
|
+
prompt_path = temp_dir / f"prompt-{session_id}.txt"
|
|
122
|
+
|
|
123
|
+
# Write with secure permissions atomically - create with mode 0o600 from the start
|
|
124
|
+
# This avoids the TOCTOU window between write_text and chmod
|
|
125
|
+
fd = os.open(str(prompt_path), os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600)
|
|
126
|
+
try:
|
|
127
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
128
|
+
f.write(prompt)
|
|
129
|
+
f.flush()
|
|
130
|
+
os.fsync(f.fileno())
|
|
131
|
+
except Exception:
|
|
132
|
+
# fd is closed by fdopen, but if fdopen fails we need to close it
|
|
133
|
+
try:
|
|
134
|
+
os.close(fd)
|
|
135
|
+
except OSError:
|
|
136
|
+
pass
|
|
137
|
+
raise
|
|
138
|
+
|
|
139
|
+
# Track for cleanup
|
|
140
|
+
_prompt_files_to_cleanup.add(prompt_path)
|
|
141
|
+
|
|
142
|
+
# Register cleanup handler once
|
|
143
|
+
if not _atexit_registered:
|
|
144
|
+
atexit.register(_cleanup_all_prompt_files)
|
|
145
|
+
_atexit_registered = True
|
|
146
|
+
|
|
147
|
+
logger.debug(f"Created secure prompt file: {prompt_path}")
|
|
148
|
+
return str(prompt_path)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def build_cli_command(
|
|
152
|
+
cli: str,
|
|
153
|
+
prompt: str | None = None,
|
|
154
|
+
session_id: str | None = None,
|
|
155
|
+
auto_approve: bool = False,
|
|
156
|
+
working_directory: str | None = None,
|
|
157
|
+
mode: str = "terminal",
|
|
158
|
+
) -> list[str]:
|
|
159
|
+
"""
|
|
160
|
+
Build the CLI command with proper prompt passing and permission flags.
|
|
161
|
+
|
|
162
|
+
Each CLI has different syntax for passing prompts and handling permissions:
|
|
163
|
+
|
|
164
|
+
Claude Code:
|
|
165
|
+
- claude --session-id <uuid> --dangerously-skip-permissions [prompt]
|
|
166
|
+
- Use --dangerously-skip-permissions for autonomous subagent operation
|
|
167
|
+
|
|
168
|
+
Gemini CLI:
|
|
169
|
+
- gemini -i "prompt" (interactive mode with initial prompt)
|
|
170
|
+
- gemini --approval-mode yolo -i "prompt" (YOLO + interactive)
|
|
171
|
+
- gemini "prompt" (one-shot non-interactive for headless)
|
|
172
|
+
|
|
173
|
+
Codex CLI:
|
|
174
|
+
- codex --full-auto -C <dir> [PROMPT]
|
|
175
|
+
- Or: codex -c 'sandbox_permissions=["disk-full-read-access"]' -a never [PROMPT]
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
cli: CLI name (claude, gemini, codex)
|
|
179
|
+
prompt: Optional prompt to pass
|
|
180
|
+
session_id: Optional session ID (used by Claude CLI)
|
|
181
|
+
auto_approve: If True, add flags to auto-approve actions/permissions
|
|
182
|
+
working_directory: Optional working directory (used by Codex -C flag)
|
|
183
|
+
mode: Execution mode - "terminal" (interactive) or "headless" (non-interactive)
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Command list for subprocess execution
|
|
187
|
+
"""
|
|
188
|
+
command = [cli]
|
|
189
|
+
|
|
190
|
+
if cli == "claude":
|
|
191
|
+
# Claude CLI flags
|
|
192
|
+
if session_id:
|
|
193
|
+
command.extend(["--session-id", session_id])
|
|
194
|
+
if auto_approve:
|
|
195
|
+
# Skip all permission prompts for autonomous subagent operation
|
|
196
|
+
command.append("--dangerously-skip-permissions")
|
|
197
|
+
if prompt:
|
|
198
|
+
# Use -p (print mode) for non-interactive execution.
|
|
199
|
+
# NOTE: Print mode bypasses hooks - headless spawner manually tracks status.
|
|
200
|
+
command.append("-p")
|
|
201
|
+
|
|
202
|
+
elif cli == "gemini":
|
|
203
|
+
# Gemini CLI flags
|
|
204
|
+
if auto_approve:
|
|
205
|
+
command.extend(["--approval-mode", "yolo"])
|
|
206
|
+
# For terminal mode, use -i (prompt-interactive) to execute prompt and stay interactive
|
|
207
|
+
# For headless mode, use positional prompt for one-shot execution
|
|
208
|
+
if prompt:
|
|
209
|
+
if mode == "terminal":
|
|
210
|
+
command.extend(["-i", prompt])
|
|
211
|
+
return command # Don't add prompt again as positional
|
|
212
|
+
# else: fall through to add as positional for headless
|
|
213
|
+
|
|
214
|
+
elif cli == "codex":
|
|
215
|
+
# Codex CLI flags
|
|
216
|
+
if auto_approve:
|
|
217
|
+
# --full-auto: low-friction sandboxed automatic execution
|
|
218
|
+
command.append("--full-auto")
|
|
219
|
+
if working_directory:
|
|
220
|
+
command.extend(["-C", working_directory])
|
|
221
|
+
|
|
222
|
+
# All three CLIs accept prompt as positional argument (must come last)
|
|
223
|
+
# For Gemini terminal mode, this is skipped (handled above with -i flag)
|
|
224
|
+
if prompt:
|
|
225
|
+
command.append(prompt)
|
|
226
|
+
|
|
227
|
+
return command
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class TerminalSpawner:
|
|
231
|
+
"""
|
|
232
|
+
Main terminal spawner that auto-detects and uses available terminals.
|
|
233
|
+
|
|
234
|
+
Provides a unified interface for spawning terminal processes across
|
|
235
|
+
different platforms and terminal emulators. Terminal preferences and
|
|
236
|
+
configurations are loaded from ~/.gobby/tty_config.yaml.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
# Map terminal names to spawner classes
|
|
240
|
+
SPAWNER_CLASSES: dict[str, type[TerminalSpawnerBase]] = {
|
|
241
|
+
"ghostty": GhosttySpawner,
|
|
242
|
+
"iterm": ITermSpawner,
|
|
243
|
+
"terminal.app": TerminalAppSpawner,
|
|
244
|
+
"kitty": KittySpawner,
|
|
245
|
+
"alacritty": AlacrittySpawner,
|
|
246
|
+
"gnome-terminal": GnomeTerminalSpawner,
|
|
247
|
+
"konsole": KonsoleSpawner,
|
|
248
|
+
"windows-terminal": WindowsTerminalSpawner,
|
|
249
|
+
"cmd": CmdSpawner,
|
|
250
|
+
"powershell": PowerShellSpawner,
|
|
251
|
+
"wsl": WSLSpawner,
|
|
252
|
+
"tmux": TmuxSpawner,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
def __init__(self) -> None:
|
|
256
|
+
"""Initialize with platform-specific terminal preferences."""
|
|
257
|
+
self._spawners: dict[TerminalType, TerminalSpawnerBase] = {}
|
|
258
|
+
self._register_spawners()
|
|
259
|
+
|
|
260
|
+
def _register_spawners(self) -> None:
|
|
261
|
+
"""Register all available spawners."""
|
|
262
|
+
all_spawners = [
|
|
263
|
+
GhosttySpawner(),
|
|
264
|
+
ITermSpawner(),
|
|
265
|
+
TerminalAppSpawner(),
|
|
266
|
+
KittySpawner(),
|
|
267
|
+
AlacrittySpawner(),
|
|
268
|
+
GnomeTerminalSpawner(),
|
|
269
|
+
KonsoleSpawner(),
|
|
270
|
+
WindowsTerminalSpawner(),
|
|
271
|
+
CmdSpawner(),
|
|
272
|
+
PowerShellSpawner(),
|
|
273
|
+
WSLSpawner(),
|
|
274
|
+
TmuxSpawner(),
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
for spawner in all_spawners:
|
|
278
|
+
self._spawners[spawner.terminal_type] = spawner
|
|
279
|
+
|
|
280
|
+
def get_available_terminals(self) -> list[TerminalType]:
|
|
281
|
+
"""Get list of available terminals on this system."""
|
|
282
|
+
return [
|
|
283
|
+
term_type for term_type, spawner in self._spawners.items() if spawner.is_available()
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
def get_preferred_terminal(self) -> TerminalType | None:
|
|
287
|
+
"""Get the preferred available terminal for this platform based on config."""
|
|
288
|
+
config = get_tty_config()
|
|
289
|
+
preferences = config.get_preferences()
|
|
290
|
+
|
|
291
|
+
for terminal_name in preferences:
|
|
292
|
+
spawner_cls = self.SPAWNER_CLASSES.get(terminal_name)
|
|
293
|
+
if spawner_cls is None:
|
|
294
|
+
continue
|
|
295
|
+
spawner = spawner_cls()
|
|
296
|
+
if spawner.is_available():
|
|
297
|
+
return spawner.terminal_type
|
|
298
|
+
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
def spawn(
|
|
302
|
+
self,
|
|
303
|
+
command: list[str],
|
|
304
|
+
cwd: str | Path,
|
|
305
|
+
terminal: TerminalType | str = TerminalType.AUTO,
|
|
306
|
+
env: dict[str, str] | None = None,
|
|
307
|
+
title: str | None = None,
|
|
308
|
+
) -> SpawnResult:
|
|
309
|
+
"""
|
|
310
|
+
Spawn a command in a new terminal window.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
command: Command to run
|
|
314
|
+
cwd: Working directory
|
|
315
|
+
terminal: Terminal type or "auto" for auto-detection
|
|
316
|
+
env: Environment variables to set
|
|
317
|
+
title: Optional window title
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
SpawnResult with success status
|
|
321
|
+
"""
|
|
322
|
+
# Convert string to enum if needed
|
|
323
|
+
if isinstance(terminal, str):
|
|
324
|
+
try:
|
|
325
|
+
terminal = TerminalType(terminal)
|
|
326
|
+
except ValueError:
|
|
327
|
+
return SpawnResult(
|
|
328
|
+
success=False,
|
|
329
|
+
message=f"Unknown terminal type: {terminal}",
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Auto-detect if requested
|
|
333
|
+
if terminal == TerminalType.AUTO:
|
|
334
|
+
preferred = self.get_preferred_terminal()
|
|
335
|
+
if preferred is None:
|
|
336
|
+
return SpawnResult(
|
|
337
|
+
success=False,
|
|
338
|
+
message="No supported terminal found on this system",
|
|
339
|
+
)
|
|
340
|
+
terminal = preferred
|
|
341
|
+
|
|
342
|
+
# Get spawner
|
|
343
|
+
spawner = self._spawners.get(terminal)
|
|
344
|
+
if spawner is None:
|
|
345
|
+
return SpawnResult(
|
|
346
|
+
success=False,
|
|
347
|
+
message=f"No spawner registered for terminal: {terminal}",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
if not spawner.is_available():
|
|
351
|
+
return SpawnResult(
|
|
352
|
+
success=False,
|
|
353
|
+
message=f"Terminal {terminal.value} is not available on this system",
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Spawn the terminal
|
|
357
|
+
return spawner.spawn(command, cwd, env, title)
|
|
358
|
+
|
|
359
|
+
def spawn_agent(
|
|
360
|
+
self,
|
|
361
|
+
cli: str,
|
|
362
|
+
cwd: str | Path,
|
|
363
|
+
session_id: str,
|
|
364
|
+
parent_session_id: str,
|
|
365
|
+
agent_run_id: str,
|
|
366
|
+
project_id: str,
|
|
367
|
+
workflow_name: str | None = None,
|
|
368
|
+
agent_depth: int = 1,
|
|
369
|
+
max_agent_depth: int = 3,
|
|
370
|
+
terminal: TerminalType | str = TerminalType.AUTO,
|
|
371
|
+
prompt: str | None = None,
|
|
372
|
+
) -> SpawnResult:
|
|
373
|
+
"""
|
|
374
|
+
Spawn a CLI agent in a new terminal with Gobby environment variables.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
cli: CLI to run (e.g., "claude", "gemini", "codex")
|
|
378
|
+
cwd: Working directory (usually project root or worktree)
|
|
379
|
+
session_id: Pre-created child session ID
|
|
380
|
+
parent_session_id: Parent session for context resolution
|
|
381
|
+
agent_run_id: Agent run record ID
|
|
382
|
+
project_id: Project ID
|
|
383
|
+
workflow_name: Optional workflow to activate
|
|
384
|
+
agent_depth: Current nesting depth
|
|
385
|
+
max_agent_depth: Maximum allowed depth
|
|
386
|
+
terminal: Terminal type or "auto"
|
|
387
|
+
prompt: Optional initial prompt
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
SpawnResult with success status
|
|
391
|
+
"""
|
|
392
|
+
# Build command with prompt as CLI argument and auto-approve for autonomous work
|
|
393
|
+
command = build_cli_command(
|
|
394
|
+
cli,
|
|
395
|
+
prompt=prompt,
|
|
396
|
+
session_id=session_id,
|
|
397
|
+
auto_approve=True, # Subagents need to work autonomously
|
|
398
|
+
working_directory=str(cwd) if cli == "codex" else None,
|
|
399
|
+
mode="terminal", # Interactive terminal mode
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Handle prompt for environment variables (backup for hooks/context)
|
|
403
|
+
prompt_env: str | None = None
|
|
404
|
+
prompt_file: str | None = None
|
|
405
|
+
|
|
406
|
+
if prompt:
|
|
407
|
+
if len(prompt) <= MAX_ENV_PROMPT_LENGTH:
|
|
408
|
+
prompt_env = prompt
|
|
409
|
+
else:
|
|
410
|
+
prompt_file = self._write_prompt_file(prompt, session_id)
|
|
411
|
+
|
|
412
|
+
# Build environment
|
|
413
|
+
env = get_terminal_env_vars(
|
|
414
|
+
session_id=session_id,
|
|
415
|
+
parent_session_id=parent_session_id,
|
|
416
|
+
agent_run_id=agent_run_id,
|
|
417
|
+
project_id=project_id,
|
|
418
|
+
workflow_name=workflow_name,
|
|
419
|
+
agent_depth=agent_depth,
|
|
420
|
+
max_agent_depth=max_agent_depth,
|
|
421
|
+
prompt=prompt_env,
|
|
422
|
+
prompt_file=prompt_file,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Set title (avoid colons/parentheses which Ghostty interprets as config syntax)
|
|
426
|
+
title = f"gobby-{cli}-d{agent_depth}"
|
|
427
|
+
|
|
428
|
+
return self.spawn(
|
|
429
|
+
command=command,
|
|
430
|
+
cwd=cwd,
|
|
431
|
+
terminal=terminal,
|
|
432
|
+
env=env,
|
|
433
|
+
title=title,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
def _write_prompt_file(self, prompt: str, session_id: str) -> str:
|
|
437
|
+
"""
|
|
438
|
+
Write prompt to a temp file for passing to spawned agent.
|
|
439
|
+
|
|
440
|
+
Delegates to the module-level _create_prompt_file helper which
|
|
441
|
+
handles secure permissions and cleanup tracking.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
prompt: The prompt content
|
|
445
|
+
session_id: Session ID for naming the file
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
Path to the created temp file
|
|
449
|
+
"""
|
|
450
|
+
return _create_prompt_file(prompt, session_id)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@dataclass
|
|
454
|
+
class PreparedSpawn:
|
|
455
|
+
"""Configuration for a prepared terminal spawn."""
|
|
456
|
+
|
|
457
|
+
session_id: str
|
|
458
|
+
"""The pre-created child session ID."""
|
|
459
|
+
|
|
460
|
+
agent_run_id: str
|
|
461
|
+
"""The agent run record ID."""
|
|
462
|
+
|
|
463
|
+
parent_session_id: str
|
|
464
|
+
"""The parent session ID."""
|
|
465
|
+
|
|
466
|
+
project_id: str
|
|
467
|
+
"""The project ID."""
|
|
468
|
+
|
|
469
|
+
workflow_name: str | None
|
|
470
|
+
"""Workflow to activate (if any)."""
|
|
471
|
+
|
|
472
|
+
agent_depth: int
|
|
473
|
+
"""Current agent depth."""
|
|
474
|
+
|
|
475
|
+
env_vars: dict[str, str]
|
|
476
|
+
"""Environment variables to set."""
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def prepare_terminal_spawn(
|
|
480
|
+
session_manager: ChildSessionManager,
|
|
481
|
+
parent_session_id: str,
|
|
482
|
+
project_id: str,
|
|
483
|
+
machine_id: str,
|
|
484
|
+
source: str = "claude",
|
|
485
|
+
agent_id: str | None = None,
|
|
486
|
+
workflow_name: str | None = None,
|
|
487
|
+
title: str | None = None,
|
|
488
|
+
git_branch: str | None = None,
|
|
489
|
+
prompt: str | None = None,
|
|
490
|
+
max_agent_depth: int = 3,
|
|
491
|
+
) -> PreparedSpawn:
|
|
492
|
+
"""
|
|
493
|
+
Prepare a terminal spawn by creating the child session.
|
|
494
|
+
|
|
495
|
+
This should be called before spawning a terminal to:
|
|
496
|
+
1. Create the child session in the database
|
|
497
|
+
2. Generate the agent run ID
|
|
498
|
+
3. Build the environment variables
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
session_manager: ChildSessionManager for session creation
|
|
502
|
+
parent_session_id: Parent session ID
|
|
503
|
+
project_id: Project ID
|
|
504
|
+
machine_id: Machine ID
|
|
505
|
+
source: CLI source (claude, gemini, codex)
|
|
506
|
+
agent_id: Optional agent ID
|
|
507
|
+
workflow_name: Optional workflow to activate
|
|
508
|
+
title: Optional session title
|
|
509
|
+
git_branch: Optional git branch
|
|
510
|
+
prompt: Optional initial prompt
|
|
511
|
+
max_agent_depth: Maximum agent depth
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
PreparedSpawn with all necessary spawn configuration
|
|
515
|
+
|
|
516
|
+
Raises:
|
|
517
|
+
ValueError: If max agent depth exceeded
|
|
518
|
+
"""
|
|
519
|
+
import uuid
|
|
520
|
+
|
|
521
|
+
# Create child session config
|
|
522
|
+
config = ChildSessionConfig(
|
|
523
|
+
parent_session_id=parent_session_id,
|
|
524
|
+
project_id=project_id,
|
|
525
|
+
machine_id=machine_id,
|
|
526
|
+
source=source,
|
|
527
|
+
agent_id=agent_id,
|
|
528
|
+
workflow_name=workflow_name,
|
|
529
|
+
title=title,
|
|
530
|
+
git_branch=git_branch,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# Create the child session
|
|
534
|
+
child_session = session_manager.create_child_session(config)
|
|
535
|
+
|
|
536
|
+
# Generate agent run ID
|
|
537
|
+
agent_run_id = f"run-{uuid.uuid4().hex[:12]}"
|
|
538
|
+
|
|
539
|
+
# Handle prompt - decide env var vs file
|
|
540
|
+
prompt_env: str | None = None
|
|
541
|
+
prompt_file: str | None = None
|
|
542
|
+
|
|
543
|
+
if prompt:
|
|
544
|
+
if len(prompt) <= MAX_ENV_PROMPT_LENGTH:
|
|
545
|
+
prompt_env = prompt
|
|
546
|
+
else:
|
|
547
|
+
# Write to temp file with secure permissions
|
|
548
|
+
prompt_file = _create_prompt_file(prompt, child_session.id)
|
|
549
|
+
|
|
550
|
+
# Build environment variables
|
|
551
|
+
env_vars = get_terminal_env_vars(
|
|
552
|
+
session_id=child_session.id,
|
|
553
|
+
parent_session_id=parent_session_id,
|
|
554
|
+
agent_run_id=agent_run_id,
|
|
555
|
+
project_id=project_id,
|
|
556
|
+
workflow_name=workflow_name,
|
|
557
|
+
agent_depth=child_session.agent_depth,
|
|
558
|
+
max_agent_depth=max_agent_depth,
|
|
559
|
+
prompt=prompt_env,
|
|
560
|
+
prompt_file=prompt_file,
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
return PreparedSpawn(
|
|
564
|
+
session_id=child_session.id,
|
|
565
|
+
agent_run_id=agent_run_id,
|
|
566
|
+
parent_session_id=parent_session_id,
|
|
567
|
+
project_id=project_id,
|
|
568
|
+
workflow_name=workflow_name,
|
|
569
|
+
agent_depth=child_session.agent_depth,
|
|
570
|
+
env_vars=env_vars,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def read_prompt_from_env() -> str | None:
|
|
575
|
+
"""
|
|
576
|
+
Read initial prompt from environment variables.
|
|
577
|
+
|
|
578
|
+
Checks GOBBY_PROMPT_FILE first (for long prompts),
|
|
579
|
+
then falls back to GOBBY_PROMPT (for short prompts).
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
Prompt string or None if not set
|
|
583
|
+
"""
|
|
584
|
+
from gobby.agents.constants import GOBBY_PROMPT, GOBBY_PROMPT_FILE
|
|
585
|
+
|
|
586
|
+
# Check for prompt file first
|
|
587
|
+
prompt_file = os.environ.get(GOBBY_PROMPT_FILE)
|
|
588
|
+
if prompt_file:
|
|
589
|
+
try:
|
|
590
|
+
prompt_path = Path(prompt_file)
|
|
591
|
+
if prompt_path.exists():
|
|
592
|
+
return prompt_path.read_text(encoding="utf-8")
|
|
593
|
+
else:
|
|
594
|
+
logger.warning(f"Prompt file not found: {prompt_file}")
|
|
595
|
+
except Exception as e:
|
|
596
|
+
logger.error(f"Error reading prompt file: {e}")
|
|
597
|
+
|
|
598
|
+
# Fall back to inline prompt
|
|
599
|
+
return os.environ.get(GOBBY_PROMPT)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
async def prepare_gemini_spawn_with_preflight(
|
|
603
|
+
session_manager: ChildSessionManager,
|
|
604
|
+
parent_session_id: str,
|
|
605
|
+
project_id: str,
|
|
606
|
+
machine_id: str,
|
|
607
|
+
agent_id: str | None = None,
|
|
608
|
+
workflow_name: str | None = None,
|
|
609
|
+
title: str | None = None,
|
|
610
|
+
git_branch: str | None = None,
|
|
611
|
+
prompt: str | None = None,
|
|
612
|
+
max_agent_depth: int = 3,
|
|
613
|
+
preflight_timeout: float = 10.0,
|
|
614
|
+
) -> PreparedSpawn:
|
|
615
|
+
"""
|
|
616
|
+
Prepare a Gemini terminal spawn with preflight session ID capture.
|
|
617
|
+
|
|
618
|
+
This is necessary because Gemini CLI in interactive mode cannot introspect
|
|
619
|
+
its own session_id. We use preflight capture to:
|
|
620
|
+
1. Launch Gemini with stream-json to capture its session_id
|
|
621
|
+
2. Create the Gobby session with that external_id
|
|
622
|
+
3. Resume the Gemini session with -r flag
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
session_manager: ChildSessionManager for session creation
|
|
626
|
+
parent_session_id: Parent session ID
|
|
627
|
+
project_id: Project ID
|
|
628
|
+
machine_id: Machine ID
|
|
629
|
+
agent_id: Optional agent ID
|
|
630
|
+
workflow_name: Optional workflow to activate
|
|
631
|
+
title: Optional session title
|
|
632
|
+
git_branch: Optional git branch
|
|
633
|
+
prompt: Optional initial prompt
|
|
634
|
+
max_agent_depth: Maximum agent depth
|
|
635
|
+
preflight_timeout: Timeout for preflight capture (default 10s)
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
PreparedSpawn with gemini_external_id set in env_vars
|
|
639
|
+
|
|
640
|
+
Raises:
|
|
641
|
+
ValueError: If max agent depth exceeded
|
|
642
|
+
asyncio.TimeoutError: If preflight capture times out
|
|
643
|
+
"""
|
|
644
|
+
import uuid
|
|
645
|
+
|
|
646
|
+
from gobby.agents.gemini_session import capture_gemini_session_id
|
|
647
|
+
|
|
648
|
+
# 1. Preflight: capture Gemini's session_id
|
|
649
|
+
logger.info("Starting Gemini preflight capture...")
|
|
650
|
+
gemini_info = await capture_gemini_session_id(timeout=preflight_timeout)
|
|
651
|
+
logger.info(f"Captured Gemini session_id: {gemini_info.session_id}")
|
|
652
|
+
|
|
653
|
+
# 2. Create child session config with Gemini's session_id as external_id
|
|
654
|
+
config = ChildSessionConfig(
|
|
655
|
+
parent_session_id=parent_session_id,
|
|
656
|
+
project_id=project_id,
|
|
657
|
+
machine_id=machine_id,
|
|
658
|
+
source="gemini",
|
|
659
|
+
agent_id=agent_id,
|
|
660
|
+
workflow_name=workflow_name,
|
|
661
|
+
title=title,
|
|
662
|
+
git_branch=git_branch,
|
|
663
|
+
external_id=gemini_info.session_id, # Link to Gemini's session
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
# Create the child session
|
|
667
|
+
child_session = session_manager.create_child_session(config)
|
|
668
|
+
|
|
669
|
+
# Generate agent run ID
|
|
670
|
+
agent_run_id = f"run-{uuid.uuid4().hex[:12]}"
|
|
671
|
+
|
|
672
|
+
# Handle prompt - decide env var vs file
|
|
673
|
+
prompt_env: str | None = None
|
|
674
|
+
prompt_file: str | None = None
|
|
675
|
+
|
|
676
|
+
if prompt:
|
|
677
|
+
if len(prompt) <= MAX_ENV_PROMPT_LENGTH:
|
|
678
|
+
prompt_env = prompt
|
|
679
|
+
else:
|
|
680
|
+
prompt_file = _create_prompt_file(prompt, child_session.id)
|
|
681
|
+
|
|
682
|
+
# Build environment variables
|
|
683
|
+
env_vars = get_terminal_env_vars(
|
|
684
|
+
session_id=child_session.id,
|
|
685
|
+
parent_session_id=parent_session_id,
|
|
686
|
+
agent_run_id=agent_run_id,
|
|
687
|
+
project_id=project_id,
|
|
688
|
+
workflow_name=workflow_name,
|
|
689
|
+
agent_depth=child_session.agent_depth,
|
|
690
|
+
max_agent_depth=max_agent_depth,
|
|
691
|
+
prompt=prompt_env,
|
|
692
|
+
prompt_file=prompt_file,
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
# Add Gemini-specific env vars for session linking
|
|
696
|
+
env_vars["GOBBY_GEMINI_EXTERNAL_ID"] = gemini_info.session_id
|
|
697
|
+
if gemini_info.model:
|
|
698
|
+
env_vars["GOBBY_GEMINI_MODEL"] = gemini_info.model
|
|
699
|
+
|
|
700
|
+
return PreparedSpawn(
|
|
701
|
+
session_id=child_session.id,
|
|
702
|
+
agent_run_id=agent_run_id,
|
|
703
|
+
parent_session_id=parent_session_id,
|
|
704
|
+
project_id=project_id,
|
|
705
|
+
workflow_name=workflow_name,
|
|
706
|
+
agent_depth=child_session.agent_depth,
|
|
707
|
+
env_vars=env_vars,
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def build_gemini_command_with_resume(
|
|
712
|
+
gemini_external_id: str,
|
|
713
|
+
prompt: str | None = None,
|
|
714
|
+
auto_approve: bool = False,
|
|
715
|
+
gobby_session_id: str | None = None,
|
|
716
|
+
) -> list[str]:
|
|
717
|
+
"""
|
|
718
|
+
Build Gemini CLI command with session resume.
|
|
719
|
+
|
|
720
|
+
Uses -r flag to resume a preflight-captured session, with session context
|
|
721
|
+
injected into the initial prompt.
|
|
722
|
+
|
|
723
|
+
Args:
|
|
724
|
+
gemini_external_id: Gemini's session_id from preflight capture
|
|
725
|
+
prompt: Optional user prompt
|
|
726
|
+
auto_approve: If True, add --approval-mode yolo
|
|
727
|
+
gobby_session_id: Gobby session ID to inject into context
|
|
728
|
+
|
|
729
|
+
Returns:
|
|
730
|
+
Command list for subprocess execution
|
|
731
|
+
"""
|
|
732
|
+
command = ["gemini"]
|
|
733
|
+
|
|
734
|
+
# Resume the preflight session
|
|
735
|
+
command.extend(["-r", gemini_external_id])
|
|
736
|
+
|
|
737
|
+
if auto_approve:
|
|
738
|
+
command.extend(["--approval-mode", "yolo"])
|
|
739
|
+
|
|
740
|
+
# Build prompt with session context
|
|
741
|
+
if gobby_session_id:
|
|
742
|
+
context_prefix = (
|
|
743
|
+
f"Your Gobby session_id is: {gobby_session_id}\n"
|
|
744
|
+
f"Use this when calling Gobby MCP tools.\n\n"
|
|
745
|
+
)
|
|
746
|
+
full_prompt = context_prefix + (prompt or "")
|
|
747
|
+
else:
|
|
748
|
+
full_prompt = prompt or ""
|
|
749
|
+
|
|
750
|
+
# Use -i for interactive mode with initial prompt
|
|
751
|
+
if full_prompt:
|
|
752
|
+
command.extend(["-i", full_prompt])
|
|
753
|
+
|
|
754
|
+
return command
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
# =============================================================================
|
|
758
|
+
# Codex Preflight Capture
|
|
759
|
+
# =============================================================================
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
async def prepare_codex_spawn_with_preflight(
|
|
763
|
+
session_manager: ChildSessionManager,
|
|
764
|
+
parent_session_id: str,
|
|
765
|
+
project_id: str,
|
|
766
|
+
machine_id: str,
|
|
767
|
+
agent_id: str | None = None,
|
|
768
|
+
workflow_name: str | None = None,
|
|
769
|
+
title: str | None = None,
|
|
770
|
+
git_branch: str | None = None,
|
|
771
|
+
prompt: str | None = None,
|
|
772
|
+
max_agent_depth: int = 3,
|
|
773
|
+
preflight_timeout: float = 30.0,
|
|
774
|
+
) -> PreparedSpawn:
|
|
775
|
+
"""
|
|
776
|
+
Prepare a Codex terminal spawn with preflight session ID capture.
|
|
777
|
+
|
|
778
|
+
This is necessary because we need Codex's session_id before launching
|
|
779
|
+
interactive mode to properly link sessions. We use preflight capture to:
|
|
780
|
+
1. Launch Codex with `exec "exit"` to capture its session_id
|
|
781
|
+
2. Create the Gobby session with that external_id
|
|
782
|
+
3. Resume the Codex session with `codex resume {session_id}`
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
session_manager: ChildSessionManager for session creation
|
|
786
|
+
parent_session_id: Parent session ID
|
|
787
|
+
project_id: Project ID
|
|
788
|
+
machine_id: Machine ID
|
|
789
|
+
agent_id: Optional agent ID
|
|
790
|
+
workflow_name: Optional workflow to activate
|
|
791
|
+
title: Optional session title
|
|
792
|
+
git_branch: Optional git branch
|
|
793
|
+
prompt: Optional initial prompt
|
|
794
|
+
max_agent_depth: Maximum agent depth
|
|
795
|
+
preflight_timeout: Timeout for preflight capture (default 30s)
|
|
796
|
+
|
|
797
|
+
Returns:
|
|
798
|
+
PreparedSpawn with codex_external_id set in env_vars
|
|
799
|
+
|
|
800
|
+
Raises:
|
|
801
|
+
ValueError: If max agent depth exceeded
|
|
802
|
+
asyncio.TimeoutError: If preflight capture times out
|
|
803
|
+
"""
|
|
804
|
+
import uuid
|
|
805
|
+
|
|
806
|
+
from gobby.agents.codex_session import capture_codex_session_id
|
|
807
|
+
|
|
808
|
+
# 1. Preflight: capture Codex's session_id
|
|
809
|
+
logger.info("Starting Codex preflight capture...")
|
|
810
|
+
codex_info = await capture_codex_session_id(timeout=preflight_timeout)
|
|
811
|
+
logger.info(f"Captured Codex session_id: {codex_info.session_id}")
|
|
812
|
+
|
|
813
|
+
# 2. Create child session config with Codex's session_id as external_id
|
|
814
|
+
config = ChildSessionConfig(
|
|
815
|
+
parent_session_id=parent_session_id,
|
|
816
|
+
project_id=project_id,
|
|
817
|
+
machine_id=machine_id,
|
|
818
|
+
source="codex",
|
|
819
|
+
agent_id=agent_id,
|
|
820
|
+
workflow_name=workflow_name,
|
|
821
|
+
title=title,
|
|
822
|
+
git_branch=git_branch,
|
|
823
|
+
external_id=codex_info.session_id, # Link to Codex's session
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
# Create the child session
|
|
827
|
+
child_session = session_manager.create_child_session(config)
|
|
828
|
+
|
|
829
|
+
# Generate agent run ID
|
|
830
|
+
agent_run_id = f"run-{uuid.uuid4().hex[:12]}"
|
|
831
|
+
|
|
832
|
+
# Handle prompt - decide env var vs file
|
|
833
|
+
prompt_env: str | None = None
|
|
834
|
+
prompt_file: str | None = None
|
|
835
|
+
|
|
836
|
+
if prompt:
|
|
837
|
+
if len(prompt) <= MAX_ENV_PROMPT_LENGTH:
|
|
838
|
+
prompt_env = prompt
|
|
839
|
+
else:
|
|
840
|
+
prompt_file = _create_prompt_file(prompt, child_session.id)
|
|
841
|
+
|
|
842
|
+
# Build environment variables
|
|
843
|
+
env_vars = get_terminal_env_vars(
|
|
844
|
+
session_id=child_session.id,
|
|
845
|
+
parent_session_id=parent_session_id,
|
|
846
|
+
agent_run_id=agent_run_id,
|
|
847
|
+
project_id=project_id,
|
|
848
|
+
workflow_name=workflow_name,
|
|
849
|
+
agent_depth=child_session.agent_depth,
|
|
850
|
+
max_agent_depth=max_agent_depth,
|
|
851
|
+
prompt=prompt_env,
|
|
852
|
+
prompt_file=prompt_file,
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
# Add Codex-specific env vars for session linking
|
|
856
|
+
env_vars["GOBBY_CODEX_EXTERNAL_ID"] = codex_info.session_id
|
|
857
|
+
if codex_info.model:
|
|
858
|
+
env_vars["GOBBY_CODEX_MODEL"] = codex_info.model
|
|
859
|
+
|
|
860
|
+
return PreparedSpawn(
|
|
861
|
+
session_id=child_session.id,
|
|
862
|
+
agent_run_id=agent_run_id,
|
|
863
|
+
parent_session_id=parent_session_id,
|
|
864
|
+
project_id=project_id,
|
|
865
|
+
workflow_name=workflow_name,
|
|
866
|
+
agent_depth=child_session.agent_depth,
|
|
867
|
+
env_vars=env_vars,
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def build_codex_command_with_resume(
|
|
872
|
+
codex_external_id: str,
|
|
873
|
+
prompt: str | None = None,
|
|
874
|
+
auto_approve: bool = False,
|
|
875
|
+
gobby_session_id: str | None = None,
|
|
876
|
+
working_directory: str | None = None,
|
|
877
|
+
) -> list[str]:
|
|
878
|
+
"""
|
|
879
|
+
Build Codex CLI command with session resume.
|
|
880
|
+
|
|
881
|
+
Uses `codex resume {session_id}` to resume a preflight-captured session,
|
|
882
|
+
with session context injected into the prompt.
|
|
883
|
+
|
|
884
|
+
Args:
|
|
885
|
+
codex_external_id: Codex's session_id from preflight capture
|
|
886
|
+
prompt: Optional user prompt
|
|
887
|
+
auto_approve: If True, add --full-auto flag
|
|
888
|
+
gobby_session_id: Gobby session ID to inject into context
|
|
889
|
+
working_directory: Optional working directory override
|
|
890
|
+
|
|
891
|
+
Returns:
|
|
892
|
+
Command list for subprocess execution
|
|
893
|
+
"""
|
|
894
|
+
command = ["codex", "resume", codex_external_id]
|
|
895
|
+
|
|
896
|
+
if auto_approve:
|
|
897
|
+
command.append("--full-auto")
|
|
898
|
+
|
|
899
|
+
if working_directory:
|
|
900
|
+
command.extend(["-C", working_directory])
|
|
901
|
+
|
|
902
|
+
# Build prompt with session context
|
|
903
|
+
if gobby_session_id:
|
|
904
|
+
context_prefix = (
|
|
905
|
+
f"Your Gobby session_id is: {gobby_session_id}\n"
|
|
906
|
+
f"Use this when calling Gobby MCP tools.\n\n"
|
|
907
|
+
)
|
|
908
|
+
full_prompt = context_prefix + (prompt or "")
|
|
909
|
+
else:
|
|
910
|
+
full_prompt = prompt or ""
|
|
911
|
+
|
|
912
|
+
# Prompt is a positional argument after session_id
|
|
913
|
+
if full_prompt:
|
|
914
|
+
command.append(full_prompt)
|
|
915
|
+
|
|
916
|
+
return command
|