gobby 0.2.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Terminal spawner implementations for agent execution.
|
|
2
|
+
|
|
3
|
+
This package provides Strategy pattern implementations for spawning
|
|
4
|
+
agents in various terminal types across different platforms.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from gobby.agents.spawners import (
|
|
8
|
+
TerminalSpawnerBase,
|
|
9
|
+
SpawnResult,
|
|
10
|
+
TerminalType,
|
|
11
|
+
SpawnMode,
|
|
12
|
+
# Platform-specific spawners
|
|
13
|
+
GhosttySpawner,
|
|
14
|
+
ITermSpawner,
|
|
15
|
+
# etc.
|
|
16
|
+
)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from gobby.agents.spawners.base import (
|
|
20
|
+
EmbeddedPTYResult,
|
|
21
|
+
HeadlessResult,
|
|
22
|
+
SpawnMode,
|
|
23
|
+
SpawnResult,
|
|
24
|
+
TerminalSpawnerBase,
|
|
25
|
+
TerminalType,
|
|
26
|
+
)
|
|
27
|
+
from gobby.agents.spawners.cross_platform import (
|
|
28
|
+
AlacrittySpawner,
|
|
29
|
+
KittySpawner,
|
|
30
|
+
TmuxSpawner,
|
|
31
|
+
)
|
|
32
|
+
from gobby.agents.spawners.embedded import EmbeddedSpawner
|
|
33
|
+
from gobby.agents.spawners.headless import HeadlessSpawner
|
|
34
|
+
from gobby.agents.spawners.linux import (
|
|
35
|
+
GnomeTerminalSpawner,
|
|
36
|
+
KonsoleSpawner,
|
|
37
|
+
)
|
|
38
|
+
from gobby.agents.spawners.macos import (
|
|
39
|
+
GhosttySpawner,
|
|
40
|
+
ITermSpawner,
|
|
41
|
+
TerminalAppSpawner,
|
|
42
|
+
)
|
|
43
|
+
from gobby.agents.spawners.windows import (
|
|
44
|
+
CmdSpawner,
|
|
45
|
+
PowerShellSpawner,
|
|
46
|
+
WindowsTerminalSpawner,
|
|
47
|
+
WSLSpawner,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
# Base types
|
|
52
|
+
"SpawnMode",
|
|
53
|
+
"TerminalType",
|
|
54
|
+
"SpawnResult",
|
|
55
|
+
"EmbeddedPTYResult",
|
|
56
|
+
"HeadlessResult",
|
|
57
|
+
"TerminalSpawnerBase",
|
|
58
|
+
# macOS spawners
|
|
59
|
+
"GhosttySpawner",
|
|
60
|
+
"ITermSpawner",
|
|
61
|
+
"TerminalAppSpawner",
|
|
62
|
+
# Linux spawners
|
|
63
|
+
"GnomeTerminalSpawner",
|
|
64
|
+
"KonsoleSpawner",
|
|
65
|
+
# Windows spawners
|
|
66
|
+
"WindowsTerminalSpawner",
|
|
67
|
+
"CmdSpawner",
|
|
68
|
+
"PowerShellSpawner",
|
|
69
|
+
"WSLSpawner",
|
|
70
|
+
# Cross-platform spawners
|
|
71
|
+
"KittySpawner",
|
|
72
|
+
"AlacrittySpawner",
|
|
73
|
+
"TmuxSpawner",
|
|
74
|
+
# Embedded/Headless spawners
|
|
75
|
+
"EmbeddedSpawner",
|
|
76
|
+
"HeadlessSpawner",
|
|
77
|
+
]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Base classes and types for terminal spawners."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess # nosec B404 - subprocess needed for terminal spawning
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SpawnMode(str, Enum):
|
|
15
|
+
"""Agent execution mode."""
|
|
16
|
+
|
|
17
|
+
TERMINAL = "terminal" # Spawn in external terminal window
|
|
18
|
+
EMBEDDED = "embedded" # Return PTY handle for UI attachment
|
|
19
|
+
HEADLESS = "headless" # Daemon captures output, no terminal visible
|
|
20
|
+
IN_PROCESS = "in_process" # Run via SDK in daemon process
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TerminalType(str, Enum):
|
|
24
|
+
"""Supported terminal types."""
|
|
25
|
+
|
|
26
|
+
# macOS
|
|
27
|
+
GHOSTTY = "ghostty"
|
|
28
|
+
ITERM = "iterm"
|
|
29
|
+
TERMINAL_APP = "terminal.app"
|
|
30
|
+
KITTY = "kitty"
|
|
31
|
+
ALACRITTY = "alacritty"
|
|
32
|
+
|
|
33
|
+
# Linux
|
|
34
|
+
GNOME_TERMINAL = "gnome-terminal"
|
|
35
|
+
KONSOLE = "konsole"
|
|
36
|
+
|
|
37
|
+
# Windows
|
|
38
|
+
WINDOWS_TERMINAL = "windows-terminal"
|
|
39
|
+
CMD = "cmd"
|
|
40
|
+
POWERSHELL = "powershell"
|
|
41
|
+
WSL = "wsl"
|
|
42
|
+
|
|
43
|
+
# Cross-platform multiplexer
|
|
44
|
+
TMUX = "tmux"
|
|
45
|
+
|
|
46
|
+
# Auto-detect
|
|
47
|
+
AUTO = "auto"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class SpawnResult:
|
|
52
|
+
"""Result of spawning a terminal process."""
|
|
53
|
+
|
|
54
|
+
success: bool
|
|
55
|
+
message: str
|
|
56
|
+
pid: int | None = None
|
|
57
|
+
terminal_type: str | None = None
|
|
58
|
+
error: str | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class EmbeddedPTYResult:
|
|
63
|
+
"""Result of spawning an embedded PTY process."""
|
|
64
|
+
|
|
65
|
+
success: bool
|
|
66
|
+
message: str
|
|
67
|
+
master_fd: int | None = None
|
|
68
|
+
"""Master file descriptor for reading/writing to PTY."""
|
|
69
|
+
slave_fd: int | None = None
|
|
70
|
+
"""Slave file descriptor (used by child process)."""
|
|
71
|
+
pid: int | None = None
|
|
72
|
+
"""Child process PID."""
|
|
73
|
+
error: str | None = None
|
|
74
|
+
|
|
75
|
+
def close(self) -> None:
|
|
76
|
+
"""Close the PTY file descriptors."""
|
|
77
|
+
if self.master_fd is not None:
|
|
78
|
+
try:
|
|
79
|
+
os.close(self.master_fd)
|
|
80
|
+
except OSError:
|
|
81
|
+
pass
|
|
82
|
+
if self.slave_fd is not None:
|
|
83
|
+
try:
|
|
84
|
+
os.close(self.slave_fd)
|
|
85
|
+
except OSError:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class HeadlessResult:
|
|
91
|
+
"""Result of spawning a headless process."""
|
|
92
|
+
|
|
93
|
+
success: bool
|
|
94
|
+
message: str
|
|
95
|
+
pid: int | None = None
|
|
96
|
+
"""Child process PID."""
|
|
97
|
+
process: subprocess.Popen[Any] | None = None
|
|
98
|
+
"""Subprocess handle for output capture."""
|
|
99
|
+
output_buffer: list[str] = field(default_factory=list)
|
|
100
|
+
"""Captured output lines."""
|
|
101
|
+
error: str | None = None
|
|
102
|
+
|
|
103
|
+
def get_output(self) -> str:
|
|
104
|
+
"""Get all captured output as a string."""
|
|
105
|
+
return "\n".join(self.output_buffer)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TerminalSpawnerBase(ABC):
|
|
109
|
+
"""Base class for terminal spawners."""
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
@abstractmethod
|
|
113
|
+
def terminal_type(self) -> TerminalType:
|
|
114
|
+
"""The terminal type this spawner handles."""
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
@abstractmethod
|
|
118
|
+
def is_available(self) -> bool:
|
|
119
|
+
"""Check if this terminal is available on the system."""
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
def spawn(
|
|
124
|
+
self,
|
|
125
|
+
command: list[str],
|
|
126
|
+
cwd: str | Path,
|
|
127
|
+
env: dict[str, str] | None = None,
|
|
128
|
+
title: str | None = None,
|
|
129
|
+
) -> SpawnResult:
|
|
130
|
+
"""
|
|
131
|
+
Spawn a new terminal window with the given command.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
command: Command to run in the terminal
|
|
135
|
+
cwd: Working directory
|
|
136
|
+
env: Environment variables to set
|
|
137
|
+
title: Optional window title
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
SpawnResult with success status and process info
|
|
141
|
+
"""
|
|
142
|
+
pass
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Cross-platform terminal spawners: Kitty, Alacritty, and tmux."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import shlex
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess # nosec B404 - subprocess needed for terminal spawning
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from gobby.agents.spawners.base import SpawnResult, TerminalSpawnerBase, TerminalType
|
|
14
|
+
from gobby.agents.tty_config import get_tty_config
|
|
15
|
+
|
|
16
|
+
__all__ = ["KittySpawner", "AlacrittySpawner", "TmuxSpawner"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class KittySpawner(TerminalSpawnerBase):
|
|
20
|
+
"""Spawner for Kitty terminal."""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def terminal_type(self) -> TerminalType:
|
|
24
|
+
return TerminalType.KITTY
|
|
25
|
+
|
|
26
|
+
def is_available(self) -> bool:
|
|
27
|
+
config = get_tty_config().get_terminal_config("kitty")
|
|
28
|
+
if not config.enabled:
|
|
29
|
+
return False
|
|
30
|
+
# On macOS, check app bundle; on other platforms check CLI
|
|
31
|
+
if platform.system() == "Darwin":
|
|
32
|
+
app_path = config.app_path or "/Applications/kitty.app"
|
|
33
|
+
return Path(app_path).exists()
|
|
34
|
+
command = config.command or "kitty"
|
|
35
|
+
return shutil.which(command) is not None
|
|
36
|
+
|
|
37
|
+
def spawn(
|
|
38
|
+
self,
|
|
39
|
+
command: list[str],
|
|
40
|
+
cwd: str | Path,
|
|
41
|
+
env: dict[str, str] | None = None,
|
|
42
|
+
title: str | None = None,
|
|
43
|
+
) -> SpawnResult:
|
|
44
|
+
try:
|
|
45
|
+
tty_config = get_tty_config().get_terminal_config("kitty")
|
|
46
|
+
if platform.system() == "Darwin":
|
|
47
|
+
# On macOS, --detach doesn't work properly - command doesn't execute
|
|
48
|
+
# Use direct path without --detach, subprocess handles backgrounding
|
|
49
|
+
app_path = tty_config.app_path or "/Applications/kitty.app"
|
|
50
|
+
kitty_path = f"{app_path}/Contents/MacOS/kitty"
|
|
51
|
+
args = [kitty_path, "--directory", str(cwd)]
|
|
52
|
+
else:
|
|
53
|
+
# On Linux, --detach works correctly
|
|
54
|
+
cli_command = tty_config.command or "kitty"
|
|
55
|
+
args = [cli_command, "--detach", "--directory", str(cwd)]
|
|
56
|
+
|
|
57
|
+
# Add extra options from config (includes confirm_os_window_close=0 by default)
|
|
58
|
+
args.extend(tty_config.options)
|
|
59
|
+
|
|
60
|
+
if title:
|
|
61
|
+
args.extend(["--title", title])
|
|
62
|
+
# Add end-of-options separator before the user command
|
|
63
|
+
# This ensures command arguments starting with '-' are not interpreted as Kitty options
|
|
64
|
+
args.append("--")
|
|
65
|
+
args.extend(command)
|
|
66
|
+
|
|
67
|
+
spawn_env = os.environ.copy()
|
|
68
|
+
if env:
|
|
69
|
+
spawn_env.update(env)
|
|
70
|
+
|
|
71
|
+
process = subprocess.Popen( # nosec B603 - args built from config
|
|
72
|
+
args,
|
|
73
|
+
env=spawn_env,
|
|
74
|
+
start_new_session=True,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return SpawnResult(
|
|
78
|
+
success=True,
|
|
79
|
+
message=f"Spawned Kitty with PID {process.pid}",
|
|
80
|
+
pid=process.pid,
|
|
81
|
+
terminal_type=self.terminal_type.value,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
return SpawnResult(
|
|
86
|
+
success=False,
|
|
87
|
+
message=f"Failed to spawn Kitty: {e}",
|
|
88
|
+
error=str(e),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class AlacrittySpawner(TerminalSpawnerBase):
|
|
93
|
+
"""Spawner for Alacritty terminal."""
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def terminal_type(self) -> TerminalType:
|
|
97
|
+
return TerminalType.ALACRITTY
|
|
98
|
+
|
|
99
|
+
def is_available(self) -> bool:
|
|
100
|
+
config = get_tty_config().get_terminal_config("alacritty")
|
|
101
|
+
if not config.enabled:
|
|
102
|
+
return False
|
|
103
|
+
command = config.command or "alacritty"
|
|
104
|
+
return shutil.which(command) is not None
|
|
105
|
+
|
|
106
|
+
def spawn(
|
|
107
|
+
self,
|
|
108
|
+
command: list[str],
|
|
109
|
+
cwd: str | Path,
|
|
110
|
+
env: dict[str, str] | None = None,
|
|
111
|
+
title: str | None = None,
|
|
112
|
+
) -> SpawnResult:
|
|
113
|
+
try:
|
|
114
|
+
tty_config = get_tty_config().get_terminal_config("alacritty")
|
|
115
|
+
cli_command = tty_config.command or "alacritty"
|
|
116
|
+
args = [cli_command, "--working-directory", str(cwd)]
|
|
117
|
+
# Add extra options from config
|
|
118
|
+
args.extend(tty_config.options)
|
|
119
|
+
if title:
|
|
120
|
+
args.extend(["--title", title])
|
|
121
|
+
args.extend(["-e"] + command)
|
|
122
|
+
|
|
123
|
+
spawn_env = os.environ.copy()
|
|
124
|
+
if env:
|
|
125
|
+
spawn_env.update(env)
|
|
126
|
+
|
|
127
|
+
process = subprocess.Popen( # nosec B603 - args built from config
|
|
128
|
+
args,
|
|
129
|
+
env=spawn_env,
|
|
130
|
+
start_new_session=True,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return SpawnResult(
|
|
134
|
+
success=True,
|
|
135
|
+
message=f"Spawned Alacritty with PID {process.pid}",
|
|
136
|
+
pid=process.pid,
|
|
137
|
+
terminal_type=self.terminal_type.value,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
return SpawnResult(
|
|
142
|
+
success=False,
|
|
143
|
+
message=f"Failed to spawn Alacritty: {e}",
|
|
144
|
+
error=str(e),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class TmuxSpawner(TerminalSpawnerBase):
|
|
149
|
+
"""
|
|
150
|
+
Spawner for tmux terminal multiplexer.
|
|
151
|
+
|
|
152
|
+
Creates a new detached tmux session that runs the command.
|
|
153
|
+
The session can be attached to later with `tmux attach -t <session>`.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def terminal_type(self) -> TerminalType:
|
|
158
|
+
return TerminalType.TMUX
|
|
159
|
+
|
|
160
|
+
def is_available(self) -> bool:
|
|
161
|
+
# tmux is available on macOS and Linux (not Windows natively)
|
|
162
|
+
if platform.system() == "Windows":
|
|
163
|
+
return False
|
|
164
|
+
config = get_tty_config().get_terminal_config("tmux")
|
|
165
|
+
if not config.enabled:
|
|
166
|
+
return False
|
|
167
|
+
command = config.command or "tmux"
|
|
168
|
+
return shutil.which(command) is not None
|
|
169
|
+
|
|
170
|
+
def spawn(
|
|
171
|
+
self,
|
|
172
|
+
command: list[str],
|
|
173
|
+
cwd: str | Path,
|
|
174
|
+
env: dict[str, str] | None = None,
|
|
175
|
+
title: str | None = None,
|
|
176
|
+
) -> SpawnResult:
|
|
177
|
+
try:
|
|
178
|
+
tty_config = get_tty_config().get_terminal_config("tmux")
|
|
179
|
+
cli_command = tty_config.command or "tmux"
|
|
180
|
+
|
|
181
|
+
# Generate a unique session name based on title or timestamp
|
|
182
|
+
session_name = title or f"gobby-{int(time.time())}"
|
|
183
|
+
# Sanitize session name (tmux doesn't like dots or colons)
|
|
184
|
+
session_name = session_name.replace(".", "-").replace(":", "-")
|
|
185
|
+
|
|
186
|
+
# Build tmux command:
|
|
187
|
+
# tmux new-session -d -s <name> -n <name> -c <cwd> <command> \
|
|
188
|
+
# \; set-option destroy-unattached off \
|
|
189
|
+
# \; set-environment VAR value ...
|
|
190
|
+
# -d: detached (runs in background)
|
|
191
|
+
# -s: session name
|
|
192
|
+
# -n: window name (title)
|
|
193
|
+
# -c: starting directory
|
|
194
|
+
# The chained set-option prevents session destruction when user has
|
|
195
|
+
# global destroy-unattached on (must be atomic with session creation)
|
|
196
|
+
args = [
|
|
197
|
+
cli_command,
|
|
198
|
+
"new-session",
|
|
199
|
+
"-d", # Detached
|
|
200
|
+
"-s",
|
|
201
|
+
session_name,
|
|
202
|
+
"-n",
|
|
203
|
+
session_name, # Window title
|
|
204
|
+
"-c",
|
|
205
|
+
str(cwd),
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
# Add extra options from config
|
|
209
|
+
args.extend(tty_config.options)
|
|
210
|
+
|
|
211
|
+
# Build the command to run, injecting env vars if provided
|
|
212
|
+
# We export env vars in the shell command so they're available to the process
|
|
213
|
+
# (tmux set-environment only affects new processes, not the initial command)
|
|
214
|
+
if env:
|
|
215
|
+
# Build export statements for each env var
|
|
216
|
+
exports = " ".join(
|
|
217
|
+
f"export {shlex.quote(k)}={shlex.quote(v)};" for k, v in env.items()
|
|
218
|
+
)
|
|
219
|
+
# Wrap command with exports
|
|
220
|
+
shell_cmd = f"{exports} exec {shlex.join(command)}"
|
|
221
|
+
args.extend(["sh", "-c", shell_cmd])
|
|
222
|
+
elif len(command) == 1:
|
|
223
|
+
args.append(command[0])
|
|
224
|
+
else:
|
|
225
|
+
# Use shell to handle complex commands with arguments
|
|
226
|
+
args.extend(["sh", "-c", shlex.join(command)])
|
|
227
|
+
|
|
228
|
+
# Chain set-option to disable destroy-unattached atomically
|
|
229
|
+
# This prevents the session from being destroyed before we can configure it
|
|
230
|
+
args.extend([";", "set-option", "-t", session_name, "destroy-unattached", "off"])
|
|
231
|
+
|
|
232
|
+
spawn_env = os.environ.copy()
|
|
233
|
+
if env:
|
|
234
|
+
spawn_env.update(env)
|
|
235
|
+
|
|
236
|
+
process = subprocess.Popen( # nosec B603 - args built from config
|
|
237
|
+
args,
|
|
238
|
+
cwd=cwd,
|
|
239
|
+
env=spawn_env,
|
|
240
|
+
start_new_session=True,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Wait for tmux to start (it exits quickly after creating the session)
|
|
244
|
+
process.wait()
|
|
245
|
+
|
|
246
|
+
if process.returncode != 0:
|
|
247
|
+
return SpawnResult(
|
|
248
|
+
success=False,
|
|
249
|
+
message=f"tmux exited with code {process.returncode}",
|
|
250
|
+
error=f"tmux failed to create session '{session_name}'",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# pid=None since tmux process has exited; session_name is the identifier
|
|
254
|
+
return SpawnResult(
|
|
255
|
+
success=True,
|
|
256
|
+
message=f"Spawned tmux session '{session_name}' (attach with: tmux attach -t {session_name})",
|
|
257
|
+
pid=None,
|
|
258
|
+
terminal_type=self.terminal_type.value,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
return SpawnResult(
|
|
263
|
+
success=False,
|
|
264
|
+
message=f"Failed to spawn tmux: {e}",
|
|
265
|
+
error=str(e),
|
|
266
|
+
)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Embedded PTY spawner for agent execution with UI attachment."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from gobby.agents.constants import get_terminal_env_vars
|
|
11
|
+
from gobby.agents.spawners.base import EmbeddedPTYResult
|
|
12
|
+
|
|
13
|
+
# pty is only available on Unix-like systems
|
|
14
|
+
try:
|
|
15
|
+
import pty
|
|
16
|
+
except ImportError:
|
|
17
|
+
pty = None # type: ignore[assignment]
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
|
|
22
|
+
__all__ = ["EmbeddedSpawner"]
|
|
23
|
+
|
|
24
|
+
# Maximum prompt length to pass via environment variable
|
|
25
|
+
# Longer prompts will be written to a temp file
|
|
26
|
+
MAX_ENV_PROMPT_LENGTH = 4096
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Import these from spawn.py to avoid duplication
|
|
30
|
+
def _get_spawn_utils() -> tuple[
|
|
31
|
+
Callable[..., list[str]],
|
|
32
|
+
Callable[[str, str], str],
|
|
33
|
+
int,
|
|
34
|
+
]:
|
|
35
|
+
"""Lazy import to avoid circular dependencies."""
|
|
36
|
+
from gobby.agents.spawn import (
|
|
37
|
+
MAX_ENV_PROMPT_LENGTH as _MAX_ENV_PROMPT_LENGTH,
|
|
38
|
+
)
|
|
39
|
+
from gobby.agents.spawn import (
|
|
40
|
+
_create_prompt_file,
|
|
41
|
+
build_cli_command,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return build_cli_command, _create_prompt_file, _MAX_ENV_PROMPT_LENGTH
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class EmbeddedSpawner:
|
|
48
|
+
"""
|
|
49
|
+
Spawner for embedded mode with PTY.
|
|
50
|
+
|
|
51
|
+
Creates a pseudo-terminal that can be attached to a UI component.
|
|
52
|
+
The master file descriptor can be used to read/write to the process.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def spawn(
|
|
56
|
+
self,
|
|
57
|
+
command: list[str],
|
|
58
|
+
cwd: str | Path,
|
|
59
|
+
env: dict[str, str] | None = None,
|
|
60
|
+
) -> EmbeddedPTYResult:
|
|
61
|
+
"""
|
|
62
|
+
Spawn a process with a PTY for embedded mode.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
command: Command to run
|
|
66
|
+
cwd: Working directory
|
|
67
|
+
env: Environment variables to set
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
EmbeddedPTYResult with PTY file descriptors and process info
|
|
71
|
+
"""
|
|
72
|
+
if not command or len(command) == 0:
|
|
73
|
+
return EmbeddedPTYResult(
|
|
74
|
+
success=False,
|
|
75
|
+
message="Cannot spawn process with empty command",
|
|
76
|
+
error="Empty command list provided",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if platform.system() == "Windows" or pty is None:
|
|
80
|
+
return EmbeddedPTYResult(
|
|
81
|
+
success=False,
|
|
82
|
+
message="Embedded PTY mode not supported on Windows",
|
|
83
|
+
error="Windows does not support Unix PTY",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
master_fd: int | None = None
|
|
87
|
+
slave_fd: int | None = None
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# Create pseudo-terminal
|
|
91
|
+
master_fd, slave_fd = pty.openpty()
|
|
92
|
+
|
|
93
|
+
# Merge environment
|
|
94
|
+
spawn_env = os.environ.copy()
|
|
95
|
+
if env:
|
|
96
|
+
spawn_env.update(env)
|
|
97
|
+
|
|
98
|
+
# Fork and exec
|
|
99
|
+
pid = os.fork()
|
|
100
|
+
|
|
101
|
+
if pid == 0:
|
|
102
|
+
# Child process
|
|
103
|
+
try:
|
|
104
|
+
# Close master fd in child - not needed
|
|
105
|
+
os.close(master_fd)
|
|
106
|
+
|
|
107
|
+
# Create new session
|
|
108
|
+
os.setsid()
|
|
109
|
+
|
|
110
|
+
# Set slave as controlling terminal
|
|
111
|
+
os.dup2(slave_fd, 0) # stdin
|
|
112
|
+
os.dup2(slave_fd, 1) # stdout
|
|
113
|
+
os.dup2(slave_fd, 2) # stderr
|
|
114
|
+
|
|
115
|
+
# Close original slave fd after duplication
|
|
116
|
+
os.close(slave_fd)
|
|
117
|
+
|
|
118
|
+
# Change to working directory
|
|
119
|
+
os.chdir(cwd)
|
|
120
|
+
|
|
121
|
+
# Execute command
|
|
122
|
+
os.execvpe(command[0], command, spawn_env) # nosec B606 - config
|
|
123
|
+
except Exception:
|
|
124
|
+
# Ensure we exit on any failure
|
|
125
|
+
os._exit(1)
|
|
126
|
+
|
|
127
|
+
# Should never reach here, but just in case
|
|
128
|
+
os._exit(1)
|
|
129
|
+
else:
|
|
130
|
+
# Parent process - close slave fd (child has its own copy)
|
|
131
|
+
os.close(slave_fd)
|
|
132
|
+
slave_fd = None # Mark as closed
|
|
133
|
+
|
|
134
|
+
return EmbeddedPTYResult(
|
|
135
|
+
success=True,
|
|
136
|
+
message=f"Spawned embedded PTY with PID {pid}",
|
|
137
|
+
master_fd=master_fd,
|
|
138
|
+
slave_fd=None, # Closed in parent
|
|
139
|
+
pid=pid,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
# Clean up file descriptors on any error
|
|
144
|
+
if master_fd is not None:
|
|
145
|
+
try:
|
|
146
|
+
os.close(master_fd)
|
|
147
|
+
except OSError:
|
|
148
|
+
pass
|
|
149
|
+
if slave_fd is not None:
|
|
150
|
+
try:
|
|
151
|
+
os.close(slave_fd)
|
|
152
|
+
except OSError:
|
|
153
|
+
pass
|
|
154
|
+
return EmbeddedPTYResult(
|
|
155
|
+
success=False,
|
|
156
|
+
message=f"Failed to spawn embedded PTY: {e}",
|
|
157
|
+
error=str(e),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def spawn_agent(
|
|
161
|
+
self,
|
|
162
|
+
cli: str,
|
|
163
|
+
cwd: str | Path,
|
|
164
|
+
session_id: str,
|
|
165
|
+
parent_session_id: str,
|
|
166
|
+
agent_run_id: str,
|
|
167
|
+
project_id: str,
|
|
168
|
+
workflow_name: str | None = None,
|
|
169
|
+
agent_depth: int = 1,
|
|
170
|
+
max_agent_depth: int = 3,
|
|
171
|
+
prompt: str | None = None,
|
|
172
|
+
) -> EmbeddedPTYResult:
|
|
173
|
+
"""
|
|
174
|
+
Spawn a CLI agent with embedded PTY.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
cli: CLI to run
|
|
178
|
+
cwd: Working directory
|
|
179
|
+
session_id: Pre-created child session ID
|
|
180
|
+
parent_session_id: Parent session ID
|
|
181
|
+
agent_run_id: Agent run record ID
|
|
182
|
+
project_id: Project ID
|
|
183
|
+
workflow_name: Optional workflow to activate
|
|
184
|
+
agent_depth: Current nesting depth
|
|
185
|
+
max_agent_depth: Maximum allowed depth
|
|
186
|
+
prompt: Optional initial prompt
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
EmbeddedPTYResult with PTY info
|
|
190
|
+
"""
|
|
191
|
+
build_cli_command, _create_prompt_file, max_env_prompt_length = _get_spawn_utils()
|
|
192
|
+
|
|
193
|
+
# Build command with prompt as CLI argument and auto-approve for autonomous work
|
|
194
|
+
command = build_cli_command(
|
|
195
|
+
cli,
|
|
196
|
+
prompt=prompt,
|
|
197
|
+
session_id=session_id,
|
|
198
|
+
auto_approve=True, # Subagents need to work autonomously
|
|
199
|
+
working_directory=str(cwd) if cli == "codex" else None,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Handle prompt for environment variables (backup for hooks/context)
|
|
203
|
+
prompt_env: str | None = None
|
|
204
|
+
prompt_file: str | None = None
|
|
205
|
+
|
|
206
|
+
if prompt:
|
|
207
|
+
if len(prompt) <= max_env_prompt_length:
|
|
208
|
+
prompt_env = prompt
|
|
209
|
+
else:
|
|
210
|
+
# Write to temp file with secure permissions
|
|
211
|
+
prompt_file = _create_prompt_file(prompt, session_id)
|
|
212
|
+
|
|
213
|
+
env = get_terminal_env_vars(
|
|
214
|
+
session_id=session_id,
|
|
215
|
+
parent_session_id=parent_session_id,
|
|
216
|
+
agent_run_id=agent_run_id,
|
|
217
|
+
project_id=project_id,
|
|
218
|
+
workflow_name=workflow_name,
|
|
219
|
+
agent_depth=agent_depth,
|
|
220
|
+
max_agent_depth=max_agent_depth,
|
|
221
|
+
prompt=prompt_env,
|
|
222
|
+
prompt_file=prompt_file,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return self.spawn(command, cwd, env)
|