@team-agent/installer 0.1.10 → 0.2.0
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.
- package/crates/team-agent-core/src/lib.rs +50 -5
- package/package.json +1 -1
- package/schemas/team.schema.json +1 -0
- package/skills/team-agent/SKILL.md +1 -1
- package/src/team_agent/approvals/__init__.py +65 -0
- package/src/team_agent/approvals/constants.py +6 -0
- package/src/team_agent/approvals/parsing.py +176 -0
- package/src/team_agent/approvals/runtime_prompts.py +171 -0
- package/src/team_agent/approvals/status.py +165 -0
- package/src/team_agent/cli/__init__.py +135 -0
- package/src/team_agent/cli/commands.py +335 -0
- package/src/team_agent/cli/e2e.py +202 -0
- package/src/team_agent/cli/helpers.py +137 -0
- package/src/team_agent/cli/parser.py +470 -0
- package/src/team_agent/compiler.py +98 -33
- package/src/team_agent/coordinator/__init__.py +53 -0
- package/src/team_agent/{coordinator.py → coordinator/__main__.py} +3 -1
- package/src/team_agent/coordinator/lifecycle.py +319 -0
- package/src/team_agent/coordinator/metadata.py +61 -0
- package/src/team_agent/coordinator/paths.py +17 -0
- package/src/team_agent/diagnose/__init__.py +48 -0
- package/src/team_agent/diagnose/checks.py +101 -0
- package/src/team_agent/diagnose/health.py +241 -0
- package/src/team_agent/diagnose/preflight.py +194 -0
- package/src/team_agent/diagnose/quick_start.py +233 -0
- package/src/team_agent/display/__init__.py +61 -0
- package/src/team_agent/display/close.py +147 -0
- package/src/team_agent/display/ghostty.py +77 -0
- package/src/team_agent/display/worker_window.py +110 -0
- package/src/team_agent/display/workspace.py +473 -0
- package/src/team_agent/launch/__init__.py +41 -0
- package/src/team_agent/launch/bootstrap.py +85 -0
- package/src/team_agent/launch/config.py +106 -0
- package/src/team_agent/launch/core.py +291 -0
- package/src/team_agent/launch/requirements.py +57 -0
- package/src/team_agent/leader/__init__.py +320 -0
- package/src/team_agent/lifecycle/__init__.py +5 -0
- package/src/team_agent/lifecycle/agents.py +226 -0
- package/src/team_agent/lifecycle/operations.py +321 -0
- package/src/team_agent/lifecycle/start.py +360 -0
- package/src/team_agent/mcp_server/__init__.py +42 -0
- package/src/team_agent/mcp_server/__main__.py +7 -0
- package/src/team_agent/mcp_server/contracts.py +148 -0
- package/src/team_agent/mcp_server/normalize.py +257 -0
- package/src/team_agent/mcp_server/server.py +150 -0
- package/src/team_agent/mcp_server/tools.py +205 -0
- package/src/team_agent/message_store/__init__.py +23 -0
- package/src/team_agent/message_store/agent_health.py +109 -0
- package/src/team_agent/{message_store.py → message_store/core.py} +188 -245
- package/src/team_agent/message_store/result_watchers.py +102 -0
- package/src/team_agent/message_store/schema.py +266 -0
- package/src/team_agent/messaging/__init__.py +1 -0
- package/src/team_agent/messaging/activity_detector.py +190 -0
- package/src/team_agent/messaging/delivery.py +128 -0
- package/src/team_agent/messaging/deps.py +263 -0
- package/src/team_agent/messaging/idle_alerts.py +217 -0
- package/src/team_agent/messaging/internal_delivery.py +46 -0
- package/src/team_agent/messaging/leader.py +317 -0
- package/src/team_agent/messaging/leader_panes.py +343 -0
- package/src/team_agent/messaging/result_delivery.py +300 -0
- package/src/team_agent/messaging/results.py +456 -0
- package/src/team_agent/messaging/scheduler.py +418 -0
- package/src/team_agent/messaging/send.py +493 -0
- package/src/team_agent/messaging/tmux_io.py +337 -0
- package/src/team_agent/messaging/tmux_prompt.py +229 -0
- package/src/team_agent/orchestrator/__init__.py +376 -0
- package/src/team_agent/orchestrator/plan.py +122 -0
- package/src/team_agent/orchestrator/state.py +128 -0
- package/src/team_agent/profiles/__init__.py +82 -0
- package/src/team_agent/profiles/constants.py +19 -0
- package/src/team_agent/profiles/core.py +407 -0
- package/src/team_agent/profiles/helpers.py +69 -0
- package/src/team_agent/profiles/provider_env.py +188 -0
- package/src/team_agent/profiles/smoke.py +201 -0
- package/src/team_agent/provider_cli/__init__.py +43 -0
- package/src/team_agent/provider_cli/adapter.py +167 -0
- package/src/team_agent/provider_cli/base.py +48 -0
- package/src/team_agent/provider_cli/claude.py +457 -0
- package/src/team_agent/provider_cli/codex.py +319 -0
- package/src/team_agent/provider_cli/copilot.py +8 -0
- package/src/team_agent/provider_cli/fake.py +39 -0
- package/src/team_agent/provider_cli/gemini.py +95 -0
- package/src/team_agent/provider_cli/opencode.py +8 -0
- package/src/team_agent/provider_cli/prompt.py +62 -0
- package/src/team_agent/provider_cli/registry.py +18 -0
- package/src/team_agent/provider_cli/unsupported.py +32 -0
- package/src/team_agent/providers.py +67 -949
- package/src/team_agent/quality_gates.py +104 -0
- package/src/team_agent/restart/__init__.py +34 -0
- package/src/team_agent/restart/orchestration.py +328 -0
- package/src/team_agent/restart/selection.py +89 -0
- package/src/team_agent/restart/snapshot.py +70 -0
- package/src/team_agent/runtime.py +802 -5740
- package/src/team_agent/rust_core.py +22 -5
- package/src/team_agent/sessions/__init__.py +25 -0
- package/src/team_agent/sessions/capture.py +93 -0
- package/src/team_agent/sessions/inventory.py +44 -0
- package/src/team_agent/sessions/resume.py +135 -0
- package/src/team_agent/spec.py +3 -1
- package/src/team_agent/state.py +204 -4
- package/src/team_agent/status/__init__.py +63 -0
- package/src/team_agent/status/approvals.py +52 -0
- package/src/team_agent/status/compact.py +158 -0
- package/src/team_agent/status/constants.py +18 -0
- package/src/team_agent/status/inbox.py +28 -0
- package/src/team_agent/status/peek.py +117 -0
- package/src/team_agent/status/queries.py +168 -0
- package/src/team_agent/terminal.py +57 -0
- package/src/team_agent/cli.py +0 -857
- package/src/team_agent/mcp_server.py +0 -579
- package/src/team_agent/profiles.py +0 -882
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from team_agent.display.close import (
|
|
4
|
+
close_ghostty_display,
|
|
5
|
+
close_ghostty_workspace,
|
|
6
|
+
close_ghostty_workspace_slot,
|
|
7
|
+
)
|
|
8
|
+
from team_agent.display.ghostty import (
|
|
9
|
+
ghostty_app_exists,
|
|
10
|
+
ghostty_attach_args,
|
|
11
|
+
ghostty_command,
|
|
12
|
+
ghostty_display_session_name,
|
|
13
|
+
ghostty_pids_by_title,
|
|
14
|
+
prepare_ghostty_display_session,
|
|
15
|
+
)
|
|
16
|
+
from team_agent.display.worker_window import (
|
|
17
|
+
open_ghostty_worker_window,
|
|
18
|
+
open_worker_displays,
|
|
19
|
+
)
|
|
20
|
+
from team_agent.display.workspace import (
|
|
21
|
+
GHOSTTY_WORKSPACE_PANES_PER_WINDOW,
|
|
22
|
+
ghostty_workspace_aggregator_name,
|
|
23
|
+
ghostty_workspace_blocked,
|
|
24
|
+
ghostty_workspace_pane_command,
|
|
25
|
+
ghostty_workspace_pane_title,
|
|
26
|
+
ghostty_workspace_partial_update_display,
|
|
27
|
+
ghostty_workspace_window_name,
|
|
28
|
+
kill_ghostty_workspace_linked_sessions,
|
|
29
|
+
open_ghostty_workspace,
|
|
30
|
+
open_ghostty_workspace_agent_display,
|
|
31
|
+
prepare_ghostty_workspace_aggregator,
|
|
32
|
+
prepare_ghostty_workspace_linked_sessions,
|
|
33
|
+
set_ghostty_workspace_pane_title,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"GHOSTTY_WORKSPACE_PANES_PER_WINDOW",
|
|
38
|
+
"close_ghostty_display",
|
|
39
|
+
"close_ghostty_workspace",
|
|
40
|
+
"close_ghostty_workspace_slot",
|
|
41
|
+
"ghostty_app_exists",
|
|
42
|
+
"ghostty_attach_args",
|
|
43
|
+
"ghostty_command",
|
|
44
|
+
"ghostty_display_session_name",
|
|
45
|
+
"ghostty_pids_by_title",
|
|
46
|
+
"ghostty_workspace_aggregator_name",
|
|
47
|
+
"ghostty_workspace_blocked",
|
|
48
|
+
"ghostty_workspace_pane_command",
|
|
49
|
+
"ghostty_workspace_pane_title",
|
|
50
|
+
"ghostty_workspace_partial_update_display",
|
|
51
|
+
"ghostty_workspace_window_name",
|
|
52
|
+
"kill_ghostty_workspace_linked_sessions",
|
|
53
|
+
"open_ghostty_worker_window",
|
|
54
|
+
"open_ghostty_workspace",
|
|
55
|
+
"open_ghostty_workspace_agent_display",
|
|
56
|
+
"open_worker_displays",
|
|
57
|
+
"prepare_ghostty_display_session",
|
|
58
|
+
"prepare_ghostty_workspace_aggregator",
|
|
59
|
+
"prepare_ghostty_workspace_linked_sessions",
|
|
60
|
+
"set_ghostty_workspace_pane_title",
|
|
61
|
+
]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from team_agent.events import EventLog
|
|
6
|
+
from team_agent.display.ghostty import ghostty_pids_by_title
|
|
7
|
+
from team_agent.display.workspace import kill_ghostty_workspace_linked_sessions
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def close_ghostty_display(
|
|
11
|
+
agent_id: str,
|
|
12
|
+
agent_state: dict[str, Any],
|
|
13
|
+
event_log: EventLog,
|
|
14
|
+
) -> None:
|
|
15
|
+
from team_agent.runtime import _tmux_session_exists, run_cmd
|
|
16
|
+
display = agent_state.get("display") or {}
|
|
17
|
+
if display.get("backend") != "ghostty_window":
|
|
18
|
+
return
|
|
19
|
+
display_session = display.get("display_session")
|
|
20
|
+
pids = [str(pid) for pid in display.get("pids", []) if str(pid).isdigit()]
|
|
21
|
+
title = display.get("title")
|
|
22
|
+
if not pids and title:
|
|
23
|
+
pids = [str(pid) for pid in ghostty_pids_by_title(str(title))]
|
|
24
|
+
killed: list[str] = []
|
|
25
|
+
for pid in pids:
|
|
26
|
+
proc = run_cmd(["kill", pid], timeout=5)
|
|
27
|
+
if proc.returncode == 0:
|
|
28
|
+
killed.append(pid)
|
|
29
|
+
if killed:
|
|
30
|
+
event_log.write("display.ghostty_closed", agent_id=agent_id, pids=killed, title=title)
|
|
31
|
+
if display_session and _tmux_session_exists(str(display_session)):
|
|
32
|
+
proc = run_cmd(["tmux", "kill-session", "-t", str(display_session)], timeout=10)
|
|
33
|
+
if proc.returncode == 0:
|
|
34
|
+
event_log.write("display.ghostty_display_session_closed", agent_id=agent_id, display_session=display_session)
|
|
35
|
+
else:
|
|
36
|
+
event_log.write(
|
|
37
|
+
"display.ghostty_display_session_close_failed",
|
|
38
|
+
agent_id=agent_id,
|
|
39
|
+
display_session=display_session,
|
|
40
|
+
error=proc.stderr.strip(),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def close_ghostty_workspace_slot(
|
|
45
|
+
agent_id: str,
|
|
46
|
+
display: dict[str, Any],
|
|
47
|
+
event_log: EventLog,
|
|
48
|
+
) -> None:
|
|
49
|
+
from team_agent.runtime import _tmux_session_exists, run_cmd
|
|
50
|
+
pane_id = display.get("pane_id")
|
|
51
|
+
linked_session = display.get("linked_session")
|
|
52
|
+
stopped_title = f"stopped: {agent_id}"
|
|
53
|
+
relabeled = False
|
|
54
|
+
if pane_id:
|
|
55
|
+
proc = run_cmd(["tmux", "select-pane", "-t", str(pane_id), "-T", stopped_title], timeout=10)
|
|
56
|
+
if proc.returncode == 0:
|
|
57
|
+
relabeled = True
|
|
58
|
+
else:
|
|
59
|
+
event_log.write(
|
|
60
|
+
"display.ghostty_workspace_slot_relabel_failed",
|
|
61
|
+
agent_id=agent_id,
|
|
62
|
+
pane_id=pane_id,
|
|
63
|
+
error=proc.stderr.strip(),
|
|
64
|
+
)
|
|
65
|
+
linked_session_closed = False
|
|
66
|
+
if linked_session and _tmux_session_exists(str(linked_session)):
|
|
67
|
+
proc = run_cmd(["tmux", "kill-session", "-t", str(linked_session)], timeout=10)
|
|
68
|
+
if proc.returncode == 0:
|
|
69
|
+
linked_session_closed = True
|
|
70
|
+
else:
|
|
71
|
+
event_log.write(
|
|
72
|
+
"display.ghostty_workspace_slot_linked_session_close_failed",
|
|
73
|
+
agent_id=agent_id,
|
|
74
|
+
linked_session=linked_session,
|
|
75
|
+
error=proc.stderr.strip(),
|
|
76
|
+
)
|
|
77
|
+
display["status"] = "stopped"
|
|
78
|
+
display["pane_title"] = stopped_title
|
|
79
|
+
event_log.write(
|
|
80
|
+
"display.ghostty_workspace_slot_closed",
|
|
81
|
+
agent_id=agent_id,
|
|
82
|
+
pane_id=pane_id,
|
|
83
|
+
linked_session=linked_session,
|
|
84
|
+
relabeled=relabeled,
|
|
85
|
+
linked_session_closed=linked_session_closed,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def close_ghostty_workspace(state: dict[str, Any], event_log: EventLog) -> None:
|
|
90
|
+
from team_agent.runtime import _tmux_session_exists, run_cmd
|
|
91
|
+
displays = [
|
|
92
|
+
(agent_id, agent_state.get("display") or {})
|
|
93
|
+
for agent_id, agent_state in state.get("agents", {}).items()
|
|
94
|
+
if (agent_state.get("display") or {}).get("backend") == "ghostty_workspace"
|
|
95
|
+
]
|
|
96
|
+
if not displays:
|
|
97
|
+
return
|
|
98
|
+
aggregator_session = next(
|
|
99
|
+
(
|
|
100
|
+
str(display.get("aggregator_session") or display.get("display_session"))
|
|
101
|
+
for _agent_id, display in displays
|
|
102
|
+
if display.get("aggregator_session") or display.get("display_session")
|
|
103
|
+
),
|
|
104
|
+
None,
|
|
105
|
+
)
|
|
106
|
+
title = next((str(display.get("title")) for _agent_id, display in displays if display.get("title")), None)
|
|
107
|
+
pids = {
|
|
108
|
+
str(pid)
|
|
109
|
+
for _agent_id, display in displays
|
|
110
|
+
for pid in display.get("pids", [])
|
|
111
|
+
if str(pid).isdigit()
|
|
112
|
+
}
|
|
113
|
+
if not pids and title:
|
|
114
|
+
pids = {str(pid) for pid in ghostty_pids_by_title(str(title))}
|
|
115
|
+
|
|
116
|
+
aggregator_closed = False
|
|
117
|
+
if aggregator_session and _tmux_session_exists(aggregator_session):
|
|
118
|
+
proc = run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
119
|
+
if proc.returncode == 0:
|
|
120
|
+
aggregator_closed = True
|
|
121
|
+
else:
|
|
122
|
+
event_log.write(
|
|
123
|
+
"display.ghostty_workspace_close_failed",
|
|
124
|
+
aggregator_session=aggregator_session,
|
|
125
|
+
error=proc.stderr.strip(),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
linked_sessions = [
|
|
129
|
+
str(display.get("linked_session"))
|
|
130
|
+
for _agent_id, display in displays
|
|
131
|
+
if display.get("linked_session")
|
|
132
|
+
]
|
|
133
|
+
linked_closed = kill_ghostty_workspace_linked_sessions(linked_sessions)
|
|
134
|
+
|
|
135
|
+
killed: list[str] = []
|
|
136
|
+
for pid in sorted(pids):
|
|
137
|
+
proc = run_cmd(["kill", pid], timeout=5)
|
|
138
|
+
if proc.returncode == 0:
|
|
139
|
+
killed.append(pid)
|
|
140
|
+
event_log.write(
|
|
141
|
+
"display.ghostty_workspace_closed",
|
|
142
|
+
pids=killed,
|
|
143
|
+
title=title,
|
|
144
|
+
aggregator_session=aggregator_session,
|
|
145
|
+
linked_sessions=linked_closed,
|
|
146
|
+
aggregator_closed=aggregator_closed,
|
|
147
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ghostty_command() -> str | None:
|
|
11
|
+
from team_agent.runtime import shutil_which
|
|
12
|
+
return shutil_which("ghostty") or (
|
|
13
|
+
"/Applications/Ghostty.app/Contents/MacOS/ghostty"
|
|
14
|
+
if Path("/Applications/Ghostty.app/Contents/MacOS/ghostty").exists()
|
|
15
|
+
else None
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def ghostty_app_exists() -> bool:
|
|
20
|
+
return Path("/Applications/Ghostty.app").exists()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def ghostty_pids_by_title(title: str, wait_s: float = 0.0) -> list[int]:
|
|
24
|
+
from team_agent.runtime import run_cmd
|
|
25
|
+
deadline = time.monotonic() + max(wait_s, 0.0)
|
|
26
|
+
while True:
|
|
27
|
+
pgrep = run_cmd(["pgrep", "-f", f"--title={title}"], timeout=5)
|
|
28
|
+
if pgrep.returncode == 0:
|
|
29
|
+
pids = [int(pid) for pid in pgrep.stdout.split() if pid.isdigit()]
|
|
30
|
+
if pids:
|
|
31
|
+
return pids
|
|
32
|
+
if time.monotonic() >= deadline:
|
|
33
|
+
return []
|
|
34
|
+
time.sleep(0.2)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def ghostty_attach_args(display_session: str, title: str) -> list[str]:
|
|
38
|
+
return [
|
|
39
|
+
"open",
|
|
40
|
+
"-na",
|
|
41
|
+
"Ghostty.app",
|
|
42
|
+
"--args",
|
|
43
|
+
f"--title={title}",
|
|
44
|
+
"-e",
|
|
45
|
+
"tmux",
|
|
46
|
+
"attach-session",
|
|
47
|
+
"-t",
|
|
48
|
+
display_session,
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def ghostty_display_session_name(session_name: str, window_name: str) -> str:
|
|
53
|
+
raw = f"{session_name}:{window_name}"
|
|
54
|
+
digest = hashlib.sha1(raw.encode("utf-8")).hexdigest()[:8]
|
|
55
|
+
safe_session = re.sub(r"[^A-Za-z0-9_.-]", "_", session_name)[:80].strip("._-") or "team"
|
|
56
|
+
safe_window = re.sub(r"[^A-Za-z0-9_.-]", "_", window_name)[:40].strip("._-") or "agent"
|
|
57
|
+
return f"{safe_session}__display__{safe_window}__{digest}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def prepare_ghostty_display_session(session_name: str, window_name: str, display_session: str) -> dict[str, Any]:
|
|
61
|
+
from team_agent.runtime import _tmux_session_exists, _tmux_window_exists, run_cmd
|
|
62
|
+
if not _tmux_window_exists(session_name, window_name):
|
|
63
|
+
return {"ok": False, "reason": "tmux_target_missing"}
|
|
64
|
+
if display_session == session_name:
|
|
65
|
+
return {"ok": False, "reason": "display_session_conflicts_with_base_session"}
|
|
66
|
+
if _tmux_session_exists(display_session):
|
|
67
|
+
proc = run_cmd(["tmux", "kill-session", "-t", display_session], timeout=10)
|
|
68
|
+
if proc.returncode != 0:
|
|
69
|
+
return {"ok": False, "reason": "display_session_cleanup_failed", "error": proc.stderr.strip()}
|
|
70
|
+
proc = run_cmd(["tmux", "new-session", "-d", "-t", session_name, "-s", display_session], timeout=10)
|
|
71
|
+
if proc.returncode != 0:
|
|
72
|
+
return {"ok": False, "reason": "display_session_create_failed", "error": proc.stderr.strip()}
|
|
73
|
+
proc = run_cmd(["tmux", "select-window", "-t", f"{display_session}:{window_name}"], timeout=10)
|
|
74
|
+
if proc.returncode != 0:
|
|
75
|
+
run_cmd(["tmux", "kill-session", "-t", display_session], timeout=10)
|
|
76
|
+
return {"ok": False, "reason": "display_session_select_window_failed", "error": proc.stderr.strip()}
|
|
77
|
+
return {"ok": True, "display_session": display_session}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from team_agent.events import EventLog
|
|
8
|
+
from team_agent.display.ghostty import (
|
|
9
|
+
ghostty_app_exists,
|
|
10
|
+
ghostty_attach_args,
|
|
11
|
+
ghostty_display_session_name,
|
|
12
|
+
ghostty_pids_by_title,
|
|
13
|
+
prepare_ghostty_display_session,
|
|
14
|
+
)
|
|
15
|
+
from team_agent.display.workspace import open_ghostty_workspace
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def open_worker_displays(
|
|
19
|
+
workspace: Path,
|
|
20
|
+
session_name: str,
|
|
21
|
+
jobs: list[tuple[str, dict[str, Any]]],
|
|
22
|
+
event_log: EventLog,
|
|
23
|
+
display_backend: str = "ghostty_window",
|
|
24
|
+
) -> dict[str, dict[str, Any]]:
|
|
25
|
+
if not jobs:
|
|
26
|
+
return {}
|
|
27
|
+
if display_backend == "ghostty_workspace":
|
|
28
|
+
return open_ghostty_workspace(workspace, session_name, jobs, event_log)
|
|
29
|
+
if len(jobs) == 1:
|
|
30
|
+
agent_id, agent = jobs[0]
|
|
31
|
+
return {agent_id: open_ghostty_worker_window(workspace, session_name, agent_id, agent, event_log)}
|
|
32
|
+
results: dict[str, dict[str, Any]] = {}
|
|
33
|
+
max_workers = min(4, len(jobs))
|
|
34
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
35
|
+
futures = {
|
|
36
|
+
executor.submit(open_ghostty_worker_window, workspace, session_name, agent_id, agent, event_log): agent_id
|
|
37
|
+
for agent_id, agent in jobs
|
|
38
|
+
}
|
|
39
|
+
for future in as_completed(futures):
|
|
40
|
+
agent_id = futures[future]
|
|
41
|
+
try:
|
|
42
|
+
results[agent_id] = future.result()
|
|
43
|
+
except Exception as exc:
|
|
44
|
+
display = {
|
|
45
|
+
"backend": "ghostty_window",
|
|
46
|
+
"status": "blocked",
|
|
47
|
+
"reason": "display_open_exception",
|
|
48
|
+
"error": str(exc),
|
|
49
|
+
"fallback": "tmux_headless",
|
|
50
|
+
}
|
|
51
|
+
event_log.write("display.ghostty_blocked", agent_id=agent_id, **display)
|
|
52
|
+
results[agent_id] = display
|
|
53
|
+
return results
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def open_ghostty_worker_window(
|
|
57
|
+
workspace: Path,
|
|
58
|
+
session_name: str,
|
|
59
|
+
window_name: str,
|
|
60
|
+
agent: dict[str, Any],
|
|
61
|
+
event_log: EventLog,
|
|
62
|
+
) -> dict[str, Any]:
|
|
63
|
+
from team_agent.runtime import run_cmd
|
|
64
|
+
_ = workspace
|
|
65
|
+
if not ghostty_app_exists():
|
|
66
|
+
blocker = {
|
|
67
|
+
"backend": "ghostty_window",
|
|
68
|
+
"status": "blocked",
|
|
69
|
+
"reason": "ghostty_app_missing",
|
|
70
|
+
"fallback": "tmux_headless",
|
|
71
|
+
}
|
|
72
|
+
event_log.write("display.ghostty_blocked", agent_id=agent["id"], **blocker)
|
|
73
|
+
return blocker
|
|
74
|
+
title = f"team-agent:{agent['id']}:{agent.get('role', '')}"
|
|
75
|
+
display_session = ghostty_display_session_name(session_name, window_name)
|
|
76
|
+
prepared = prepare_ghostty_display_session(session_name, window_name, display_session)
|
|
77
|
+
if not prepared["ok"]:
|
|
78
|
+
blocker = {
|
|
79
|
+
"backend": "ghostty_window",
|
|
80
|
+
"status": "blocked",
|
|
81
|
+
"reason": prepared["reason"],
|
|
82
|
+
"error": prepared.get("error"),
|
|
83
|
+
"target": f"{session_name}:{window_name}",
|
|
84
|
+
"display_session": display_session,
|
|
85
|
+
"fallback": "tmux_headless",
|
|
86
|
+
}
|
|
87
|
+
event_log.write("display.ghostty_blocked", agent_id=agent["id"], **blocker)
|
|
88
|
+
return blocker
|
|
89
|
+
launch_args = ghostty_attach_args(display_session, title)
|
|
90
|
+
proc = run_cmd(launch_args, timeout=10)
|
|
91
|
+
display = {
|
|
92
|
+
"backend": "ghostty_window",
|
|
93
|
+
"status": "opened" if proc.returncode == 0 else "blocked",
|
|
94
|
+
"title": title,
|
|
95
|
+
"target": f"{session_name}:{window_name}",
|
|
96
|
+
"display_session": display_session,
|
|
97
|
+
"launch_args": launch_args,
|
|
98
|
+
"pid": None,
|
|
99
|
+
"pids": [],
|
|
100
|
+
"tty": None,
|
|
101
|
+
"fallback": "tmux_headless",
|
|
102
|
+
"note": "Ghostty opens a dedicated linked tmux session per worker so each display has an independent active window; runtime injection remains tmux-backed.",
|
|
103
|
+
}
|
|
104
|
+
if proc.returncode != 0:
|
|
105
|
+
display["reason"] = proc.stderr.strip() or proc.stdout.strip() or "open Ghostty.app failed"
|
|
106
|
+
else:
|
|
107
|
+
display["pids"] = ghostty_pids_by_title(title, wait_s=3.0)
|
|
108
|
+
display["pid"] = display["pids"][0] if display["pids"] else None
|
|
109
|
+
event_log.write("display.ghostty_window", agent_id=agent["id"], **display)
|
|
110
|
+
return display
|