@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,473 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
import shlex
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from team_agent.events import EventLog
|
|
10
|
+
from team_agent.display.ghostty import (
|
|
11
|
+
ghostty_app_exists,
|
|
12
|
+
ghostty_attach_args,
|
|
13
|
+
ghostty_display_session_name,
|
|
14
|
+
ghostty_pids_by_title,
|
|
15
|
+
prepare_ghostty_display_session,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
GHOSTTY_WORKSPACE_PANES_PER_WINDOW = 3
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _tmux_stdout_last_line(stdout: str) -> str | None:
|
|
23
|
+
lines = [line.strip() for line in stdout.splitlines() if line.strip()]
|
|
24
|
+
return lines[-1] if lines else None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def open_ghostty_workspace(
|
|
28
|
+
workspace,
|
|
29
|
+
session_name: str,
|
|
30
|
+
jobs: list[tuple[str, dict[str, Any]]],
|
|
31
|
+
event_log: EventLog,
|
|
32
|
+
) -> dict[str, dict[str, Any]]:
|
|
33
|
+
from team_agent.runtime import run_cmd
|
|
34
|
+
_ = workspace
|
|
35
|
+
if not ghostty_app_exists():
|
|
36
|
+
return ghostty_workspace_blocked(jobs, event_log, "ghostty_app_missing")
|
|
37
|
+
aggregator_session = ghostty_workspace_aggregator_name(session_name)
|
|
38
|
+
linked_results = prepare_ghostty_workspace_linked_sessions(session_name, jobs)
|
|
39
|
+
displays: dict[str, dict[str, Any]] = {}
|
|
40
|
+
linked_jobs: list[tuple[str, dict[str, Any], str]] = []
|
|
41
|
+
for agent_id, agent in jobs:
|
|
42
|
+
linked = linked_results.get(agent_id, {})
|
|
43
|
+
linked_session = linked.get("linked_session") or ghostty_display_session_name(session_name, agent_id)
|
|
44
|
+
if linked.get("ok"):
|
|
45
|
+
linked_jobs.append((agent_id, agent, linked_session))
|
|
46
|
+
continue
|
|
47
|
+
displays.update(
|
|
48
|
+
ghostty_workspace_blocked(
|
|
49
|
+
[(agent_id, agent)],
|
|
50
|
+
event_log,
|
|
51
|
+
linked.get("reason", "display_session_create_failed"),
|
|
52
|
+
aggregator_session=aggregator_session,
|
|
53
|
+
linked_sessions={agent_id: linked_session},
|
|
54
|
+
error=linked.get("error"),
|
|
55
|
+
target=f"{session_name}:{agent_id}",
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
if not linked_jobs:
|
|
59
|
+
return displays
|
|
60
|
+
prepared = prepare_ghostty_workspace_aggregator(aggregator_session, linked_jobs)
|
|
61
|
+
if not prepared["ok"]:
|
|
62
|
+
kill_ghostty_workspace_linked_sessions([linked_session for _agent_id, _agent, linked_session in linked_jobs])
|
|
63
|
+
displays.update(
|
|
64
|
+
ghostty_workspace_blocked(
|
|
65
|
+
[(agent_id, agent) for agent_id, agent, _linked_session in linked_jobs],
|
|
66
|
+
event_log,
|
|
67
|
+
prepared["reason"],
|
|
68
|
+
aggregator_session=aggregator_session,
|
|
69
|
+
linked_sessions={agent_id: linked_session for agent_id, _agent, linked_session in linked_jobs},
|
|
70
|
+
error=prepared.get("error"),
|
|
71
|
+
target=prepared.get("target"),
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
return displays
|
|
75
|
+
title = f"team-agent:{session_name}:workspace"
|
|
76
|
+
launch_args = ghostty_attach_args(aggregator_session, title)
|
|
77
|
+
proc = run_cmd(launch_args, timeout=10)
|
|
78
|
+
if proc.returncode != 0:
|
|
79
|
+
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
80
|
+
kill_ghostty_workspace_linked_sessions([linked_session for _agent_id, _agent, linked_session in linked_jobs])
|
|
81
|
+
displays.update(
|
|
82
|
+
ghostty_workspace_blocked(
|
|
83
|
+
[(agent_id, agent) for agent_id, agent, _linked_session in linked_jobs],
|
|
84
|
+
event_log,
|
|
85
|
+
"open Ghostty.app failed",
|
|
86
|
+
aggregator_session=aggregator_session,
|
|
87
|
+
linked_sessions={agent_id: linked_session for agent_id, _agent, linked_session in linked_jobs},
|
|
88
|
+
error=proc.stderr.strip() or proc.stdout.strip(),
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
return displays
|
|
92
|
+
pids = ghostty_pids_by_title(title, wait_s=3.0)
|
|
93
|
+
panes = {pane["agent_id"]: pane for pane in prepared["panes"]}
|
|
94
|
+
for agent_id, agent, linked_session in linked_jobs:
|
|
95
|
+
pane = panes.get(agent_id, {})
|
|
96
|
+
display = {
|
|
97
|
+
"backend": "ghostty_workspace",
|
|
98
|
+
"status": "opened",
|
|
99
|
+
"title": title,
|
|
100
|
+
"pane_title": pane.get("title") or ghostty_workspace_pane_title(agent),
|
|
101
|
+
"target": f"{session_name}:{agent_id}",
|
|
102
|
+
"linked_session": linked_session,
|
|
103
|
+
"aggregator_session": aggregator_session,
|
|
104
|
+
"display_session": aggregator_session,
|
|
105
|
+
"workspace_window": pane.get("window_name"),
|
|
106
|
+
"pane_id": pane.get("pane_id"),
|
|
107
|
+
"launch_args": launch_args,
|
|
108
|
+
"pid": pids[0] if pids else None,
|
|
109
|
+
"pids": pids,
|
|
110
|
+
"tty": None,
|
|
111
|
+
"fallback": "tmux_headless",
|
|
112
|
+
"note": "Ghostty opens one aggregator tmux session; each pane attaches to a distinct linked session pinned to one base worker window, so runtime injection remains session:agent_id addressed.",
|
|
113
|
+
}
|
|
114
|
+
event_log.write("display.ghostty_workspace", agent_id=agent_id, **display)
|
|
115
|
+
displays[agent_id] = display
|
|
116
|
+
return displays
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def ghostty_workspace_blocked(
|
|
120
|
+
jobs: list[tuple[str, dict[str, Any]]],
|
|
121
|
+
event_log: EventLog,
|
|
122
|
+
reason: str,
|
|
123
|
+
aggregator_session: str | None = None,
|
|
124
|
+
linked_sessions: dict[str, str] | None = None,
|
|
125
|
+
error: str | None = None,
|
|
126
|
+
target: str | None = None,
|
|
127
|
+
) -> dict[str, dict[str, Any]]:
|
|
128
|
+
displays: dict[str, dict[str, Any]] = {}
|
|
129
|
+
for agent_id, _agent in jobs:
|
|
130
|
+
linked_session = (linked_sessions or {}).get(agent_id)
|
|
131
|
+
display = {
|
|
132
|
+
"backend": "ghostty_workspace",
|
|
133
|
+
"status": "blocked",
|
|
134
|
+
"reason": reason,
|
|
135
|
+
"error": error,
|
|
136
|
+
"target": target or f"{agent_id}",
|
|
137
|
+
"linked_session": linked_session,
|
|
138
|
+
"aggregator_session": aggregator_session,
|
|
139
|
+
"display_session": aggregator_session,
|
|
140
|
+
"fallback": "tmux_headless",
|
|
141
|
+
}
|
|
142
|
+
event_log.write("display.ghostty_workspace_blocked", agent_id=agent_id, **display)
|
|
143
|
+
displays[agent_id] = display
|
|
144
|
+
return displays
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def ghostty_workspace_aggregator_name(session_name: str) -> str:
|
|
148
|
+
raw = f"{session_name}:workspace"
|
|
149
|
+
digest = hashlib.sha1(raw.encode("utf-8")).hexdigest()[:8]
|
|
150
|
+
safe_session = re.sub(r"[^A-Za-z0-9_.-]", "_", session_name)[:80].strip("._-") or "team"
|
|
151
|
+
return f"{safe_session}__display__workspace__{digest}"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def ghostty_workspace_window_name(index: int) -> str:
|
|
155
|
+
return "overview" if index == 0 else f"overview-{index + 1}"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def ghostty_workspace_pane_command(linked_session: str) -> str:
|
|
159
|
+
return f"TMUX= tmux attach-session -t {shlex.quote(linked_session)}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def ghostty_workspace_pane_title(agent: dict[str, Any]) -> str:
|
|
163
|
+
return f"team-agent:{agent['id']}:{agent.get('role', '')}"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def prepare_ghostty_workspace_linked_sessions(
|
|
167
|
+
session_name: str,
|
|
168
|
+
jobs: list[tuple[str, dict[str, Any]]],
|
|
169
|
+
) -> dict[str, dict[str, Any]]:
|
|
170
|
+
def prepare(agent_id: str) -> dict[str, Any]:
|
|
171
|
+
linked_session = ghostty_display_session_name(session_name, agent_id)
|
|
172
|
+
result = prepare_ghostty_display_session(session_name, agent_id, linked_session)
|
|
173
|
+
result["linked_session"] = linked_session
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
if len(jobs) == 1:
|
|
177
|
+
agent_id, _agent = jobs[0]
|
|
178
|
+
return {agent_id: prepare(agent_id)}
|
|
179
|
+
results: dict[str, dict[str, Any]] = {}
|
|
180
|
+
max_workers = min(4, len(jobs))
|
|
181
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
182
|
+
futures = {executor.submit(prepare, agent_id): agent_id for agent_id, _agent in jobs}
|
|
183
|
+
for future in as_completed(futures):
|
|
184
|
+
agent_id = futures[future]
|
|
185
|
+
try:
|
|
186
|
+
results[agent_id] = future.result()
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
results[agent_id] = {
|
|
189
|
+
"ok": False,
|
|
190
|
+
"reason": "display_session_create_exception",
|
|
191
|
+
"error": str(exc),
|
|
192
|
+
"linked_session": ghostty_display_session_name(session_name, agent_id),
|
|
193
|
+
}
|
|
194
|
+
return results
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def prepare_ghostty_workspace_aggregator(
|
|
198
|
+
aggregator_session: str,
|
|
199
|
+
linked_jobs: list[tuple[str, dict[str, Any], str]],
|
|
200
|
+
) -> dict[str, Any]:
|
|
201
|
+
from team_agent.runtime import _tmux_session_exists, run_cmd
|
|
202
|
+
if _tmux_session_exists(aggregator_session):
|
|
203
|
+
proc = run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
204
|
+
if proc.returncode != 0:
|
|
205
|
+
return {"ok": False, "reason": "display_session_cleanup_failed", "error": proc.stderr.strip()}
|
|
206
|
+
|
|
207
|
+
def fail(reason: str, proc: Any | None = None, target: str | None = None) -> dict[str, Any]:
|
|
208
|
+
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
209
|
+
result = {"ok": False, "reason": reason}
|
|
210
|
+
if proc is not None:
|
|
211
|
+
result["error"] = proc.stderr.strip()
|
|
212
|
+
if target:
|
|
213
|
+
result["target"] = target
|
|
214
|
+
return result
|
|
215
|
+
|
|
216
|
+
panes: list[dict[str, Any]] = []
|
|
217
|
+
for window_index, start in enumerate(range(0, len(linked_jobs), GHOSTTY_WORKSPACE_PANES_PER_WINDOW)):
|
|
218
|
+
window_name = ghostty_workspace_window_name(window_index)
|
|
219
|
+
window_jobs = linked_jobs[start : start + GHOSTTY_WORKSPACE_PANES_PER_WINDOW]
|
|
220
|
+
first_agent_id, first_agent, first_linked_session = window_jobs[0]
|
|
221
|
+
if window_index == 0:
|
|
222
|
+
proc = run_cmd(
|
|
223
|
+
[
|
|
224
|
+
"tmux",
|
|
225
|
+
"new-session",
|
|
226
|
+
"-d",
|
|
227
|
+
"-P",
|
|
228
|
+
"-F",
|
|
229
|
+
"#{pane_id}",
|
|
230
|
+
"-s",
|
|
231
|
+
aggregator_session,
|
|
232
|
+
"-n",
|
|
233
|
+
window_name,
|
|
234
|
+
ghostty_workspace_pane_command(first_linked_session),
|
|
235
|
+
],
|
|
236
|
+
timeout=10,
|
|
237
|
+
)
|
|
238
|
+
if proc.returncode != 0:
|
|
239
|
+
return {"ok": False, "reason": "display_session_create_failed", "error": proc.stderr.strip()}
|
|
240
|
+
else:
|
|
241
|
+
proc = run_cmd(
|
|
242
|
+
[
|
|
243
|
+
"tmux",
|
|
244
|
+
"new-window",
|
|
245
|
+
"-t",
|
|
246
|
+
aggregator_session,
|
|
247
|
+
"-n",
|
|
248
|
+
window_name,
|
|
249
|
+
"-P",
|
|
250
|
+
"-F",
|
|
251
|
+
"#{pane_id}",
|
|
252
|
+
ghostty_workspace_pane_command(first_linked_session),
|
|
253
|
+
],
|
|
254
|
+
timeout=10,
|
|
255
|
+
)
|
|
256
|
+
if proc.returncode != 0:
|
|
257
|
+
return fail("display_session_window_create_failed", proc, first_linked_session)
|
|
258
|
+
first_pane_id = _tmux_stdout_last_line(proc.stdout) or f"{aggregator_session}:{window_name}.0"
|
|
259
|
+
first_title = ghostty_workspace_pane_title(first_agent)
|
|
260
|
+
title_result = set_ghostty_workspace_pane_title(first_pane_id, first_title)
|
|
261
|
+
if not title_result["ok"]:
|
|
262
|
+
return fail(title_result["reason"], target=first_pane_id)
|
|
263
|
+
panes.append(
|
|
264
|
+
{
|
|
265
|
+
"agent_id": first_agent_id,
|
|
266
|
+
"pane_id": first_pane_id,
|
|
267
|
+
"title": first_title,
|
|
268
|
+
"linked_session": first_linked_session,
|
|
269
|
+
"window_name": window_name,
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
proc = run_cmd(["tmux", "set-window-option", "-t", f"{aggregator_session}:{window_name}", "remain-on-exit", "on"], timeout=10)
|
|
274
|
+
if proc.returncode != 0:
|
|
275
|
+
return fail("display_session_remain_on_exit_failed", proc)
|
|
276
|
+
|
|
277
|
+
for index, (agent_id, agent, linked_session) in enumerate(window_jobs[1:], start=1):
|
|
278
|
+
proc = run_cmd(
|
|
279
|
+
[
|
|
280
|
+
"tmux",
|
|
281
|
+
"split-window",
|
|
282
|
+
"-t",
|
|
283
|
+
f"{aggregator_session}:{window_name}",
|
|
284
|
+
"-h",
|
|
285
|
+
"-P",
|
|
286
|
+
"-F",
|
|
287
|
+
"#{pane_id}",
|
|
288
|
+
ghostty_workspace_pane_command(linked_session),
|
|
289
|
+
],
|
|
290
|
+
timeout=10,
|
|
291
|
+
)
|
|
292
|
+
if proc.returncode != 0:
|
|
293
|
+
return fail("display_session_split_failed", proc, linked_session)
|
|
294
|
+
pane_id = _tmux_stdout_last_line(proc.stdout) or f"{aggregator_session}:{window_name}.{index}"
|
|
295
|
+
title = ghostty_workspace_pane_title(agent)
|
|
296
|
+
title_result = set_ghostty_workspace_pane_title(pane_id, title)
|
|
297
|
+
if not title_result["ok"]:
|
|
298
|
+
return fail(title_result["reason"], target=pane_id)
|
|
299
|
+
panes.append(
|
|
300
|
+
{
|
|
301
|
+
"agent_id": agent_id,
|
|
302
|
+
"pane_id": pane_id,
|
|
303
|
+
"title": title,
|
|
304
|
+
"linked_session": linked_session,
|
|
305
|
+
"window_name": window_name,
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
proc = run_cmd(["tmux", "select-layout", "-t", f"{aggregator_session}:{window_name}", "even-horizontal"], timeout=10)
|
|
310
|
+
if proc.returncode != 0:
|
|
311
|
+
return fail("display_session_layout_failed", proc)
|
|
312
|
+
|
|
313
|
+
proc = run_cmd(["tmux", "set-option", "-t", aggregator_session, "mouse", "on"], timeout=10)
|
|
314
|
+
if proc.returncode != 0:
|
|
315
|
+
return fail("display_session_mouse_failed", proc)
|
|
316
|
+
run_cmd(["tmux", "select-window", "-t", f"{aggregator_session}:{ghostty_workspace_window_name(0)}"], timeout=10)
|
|
317
|
+
return {"ok": True, "aggregator_session": aggregator_session, "panes": panes}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def set_ghostty_workspace_pane_title(pane_id: str, title: str) -> dict[str, Any]:
|
|
321
|
+
from team_agent.runtime import run_cmd
|
|
322
|
+
proc = run_cmd(["tmux", "select-pane", "-t", pane_id, "-T", title], timeout=10)
|
|
323
|
+
if proc.returncode != 0:
|
|
324
|
+
return {"ok": False, "reason": "display_session_pane_title_failed", "error": proc.stderr.strip()}
|
|
325
|
+
return {"ok": True}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def open_ghostty_workspace_agent_display(
|
|
329
|
+
session_name: str,
|
|
330
|
+
agent_id: str,
|
|
331
|
+
agent: dict[str, Any],
|
|
332
|
+
previous_display: dict[str, Any],
|
|
333
|
+
event_log: EventLog,
|
|
334
|
+
) -> dict[str, Any]:
|
|
335
|
+
from team_agent.runtime import _tmux_session_exists, run_cmd
|
|
336
|
+
if not ghostty_app_exists():
|
|
337
|
+
return ghostty_workspace_blocked(
|
|
338
|
+
[(agent_id, agent)],
|
|
339
|
+
event_log,
|
|
340
|
+
"ghostty_app_missing",
|
|
341
|
+
aggregator_session=ghostty_workspace_aggregator_name(session_name),
|
|
342
|
+
linked_sessions={agent_id: ghostty_display_session_name(session_name, agent_id)},
|
|
343
|
+
target=f"{session_name}:{agent_id}",
|
|
344
|
+
)[agent_id]
|
|
345
|
+
aggregator_session = str(
|
|
346
|
+
previous_display.get("aggregator_session")
|
|
347
|
+
or previous_display.get("display_session")
|
|
348
|
+
or ghostty_workspace_aggregator_name(session_name)
|
|
349
|
+
)
|
|
350
|
+
linked_session = ghostty_display_session_name(session_name, agent_id)
|
|
351
|
+
prepared = prepare_ghostty_display_session(session_name, agent_id, linked_session)
|
|
352
|
+
if not prepared["ok"]:
|
|
353
|
+
return ghostty_workspace_blocked(
|
|
354
|
+
[(agent_id, agent)],
|
|
355
|
+
event_log,
|
|
356
|
+
prepared["reason"],
|
|
357
|
+
aggregator_session=aggregator_session,
|
|
358
|
+
linked_sessions={agent_id: linked_session},
|
|
359
|
+
error=prepared.get("error"),
|
|
360
|
+
target=f"{session_name}:{agent_id}",
|
|
361
|
+
)[agent_id]
|
|
362
|
+
if not _tmux_session_exists(aggregator_session):
|
|
363
|
+
return ghostty_workspace_partial_update_display(
|
|
364
|
+
session_name,
|
|
365
|
+
agent_id,
|
|
366
|
+
agent,
|
|
367
|
+
event_log,
|
|
368
|
+
reason="aggregator_session_missing",
|
|
369
|
+
note="pane refresh requires full team restart",
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
pane_title = ghostty_workspace_pane_title(agent)
|
|
373
|
+
command = ghostty_workspace_pane_command(linked_session)
|
|
374
|
+
pane_id = str(previous_display.get("pane_id") or "")
|
|
375
|
+
workspace_window = str(previous_display.get("workspace_window") or ghostty_workspace_window_name(0))
|
|
376
|
+
refreshed = False
|
|
377
|
+
if pane_id:
|
|
378
|
+
proc = run_cmd(["tmux", "respawn-pane", "-k", "-t", pane_id, command], timeout=10)
|
|
379
|
+
refreshed = proc.returncode == 0
|
|
380
|
+
if not refreshed:
|
|
381
|
+
proc = run_cmd(
|
|
382
|
+
[
|
|
383
|
+
"tmux",
|
|
384
|
+
"split-window",
|
|
385
|
+
"-t",
|
|
386
|
+
f"{aggregator_session}:{workspace_window}",
|
|
387
|
+
"-h",
|
|
388
|
+
"-P",
|
|
389
|
+
"-F",
|
|
390
|
+
"#{pane_id}",
|
|
391
|
+
command,
|
|
392
|
+
],
|
|
393
|
+
timeout=10,
|
|
394
|
+
)
|
|
395
|
+
if proc.returncode != 0:
|
|
396
|
+
return ghostty_workspace_partial_update_display(
|
|
397
|
+
session_name,
|
|
398
|
+
agent_id,
|
|
399
|
+
agent,
|
|
400
|
+
event_log,
|
|
401
|
+
reason="aggregator_pane_refresh_failed",
|
|
402
|
+
note=proc.stderr.strip() or "pane refresh requires full team restart",
|
|
403
|
+
)
|
|
404
|
+
pane_id = _tmux_stdout_last_line(proc.stdout) or pane_id
|
|
405
|
+
title_result = set_ghostty_workspace_pane_title(pane_id, pane_title)
|
|
406
|
+
if not title_result["ok"]:
|
|
407
|
+
return ghostty_workspace_partial_update_display(
|
|
408
|
+
session_name,
|
|
409
|
+
agent_id,
|
|
410
|
+
agent,
|
|
411
|
+
event_log,
|
|
412
|
+
reason=title_result["reason"],
|
|
413
|
+
note=title_result.get("error") or "pane refresh requires full team restart",
|
|
414
|
+
)
|
|
415
|
+
run_cmd(["tmux", "select-layout", "-t", f"{aggregator_session}:{workspace_window}", "even-horizontal"], timeout=10)
|
|
416
|
+
title = str(previous_display.get("title") or f"team-agent:{session_name}:workspace")
|
|
417
|
+
pids = [int(pid) for pid in previous_display.get("pids", []) if str(pid).isdigit()]
|
|
418
|
+
display = {
|
|
419
|
+
"backend": "ghostty_workspace",
|
|
420
|
+
"status": "opened",
|
|
421
|
+
"title": title,
|
|
422
|
+
"pane_title": pane_title,
|
|
423
|
+
"target": f"{session_name}:{agent_id}",
|
|
424
|
+
"linked_session": linked_session,
|
|
425
|
+
"aggregator_session": aggregator_session,
|
|
426
|
+
"display_session": aggregator_session,
|
|
427
|
+
"workspace_window": workspace_window,
|
|
428
|
+
"pane_id": pane_id,
|
|
429
|
+
"pid": pids[0] if pids else None,
|
|
430
|
+
"pids": pids,
|
|
431
|
+
"tty": None,
|
|
432
|
+
"fallback": "tmux_headless",
|
|
433
|
+
"note": "Refreshed this worker's Ghostty workspace pane by respawning it against a distinct linked session.",
|
|
434
|
+
}
|
|
435
|
+
event_log.write("display.ghostty_workspace", agent_id=agent_id, **display)
|
|
436
|
+
return display
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def ghostty_workspace_partial_update_display(
|
|
440
|
+
session_name: str,
|
|
441
|
+
agent_id: str,
|
|
442
|
+
agent: dict[str, Any],
|
|
443
|
+
event_log: EventLog,
|
|
444
|
+
reason: str = "partial_update_requires_team_restart",
|
|
445
|
+
note: str = "pane refresh requires full team restart",
|
|
446
|
+
) -> dict[str, Any]:
|
|
447
|
+
aggregator_session = ghostty_workspace_aggregator_name(session_name)
|
|
448
|
+
display = {
|
|
449
|
+
"backend": "ghostty_workspace",
|
|
450
|
+
"status": "blocked",
|
|
451
|
+
"reason": reason,
|
|
452
|
+
"target": f"{session_name}:{agent_id}",
|
|
453
|
+
"linked_session": ghostty_display_session_name(session_name, agent_id),
|
|
454
|
+
"aggregator_session": aggregator_session,
|
|
455
|
+
"display_session": aggregator_session,
|
|
456
|
+
"pane_title": ghostty_workspace_pane_title(agent),
|
|
457
|
+
"fallback": "tmux_headless",
|
|
458
|
+
"note": note,
|
|
459
|
+
"action": "restart the team to rebuild the Ghostty workspace layout",
|
|
460
|
+
}
|
|
461
|
+
event_log.write("display.ghostty_workspace_partial_update", agent_id=agent_id, **display)
|
|
462
|
+
return display
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def kill_ghostty_workspace_linked_sessions(linked_sessions: list[str]) -> list[str]:
|
|
466
|
+
from team_agent.runtime import _tmux_session_exists, run_cmd
|
|
467
|
+
killed: list[str] = []
|
|
468
|
+
for linked_session in dict.fromkeys(linked_sessions):
|
|
469
|
+
if _tmux_session_exists(linked_session):
|
|
470
|
+
proc = run_cmd(["tmux", "kill-session", "-t", linked_session], timeout=10)
|
|
471
|
+
if proc.returncode == 0:
|
|
472
|
+
killed.append(linked_session)
|
|
473
|
+
return killed
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from team_agent.launch.bootstrap import (
|
|
4
|
+
attach_team_profile_dirs,
|
|
5
|
+
compile_team_dir_spec,
|
|
6
|
+
init_workspace,
|
|
7
|
+
is_team_doc_dir,
|
|
8
|
+
spec_team_dir,
|
|
9
|
+
tmux_session_conflict_error,
|
|
10
|
+
validate_file,
|
|
11
|
+
)
|
|
12
|
+
from team_agent.launch.config import (
|
|
13
|
+
DANGEROUS_LEADER_FLAGS,
|
|
14
|
+
command_has_flag,
|
|
15
|
+
detect_inherited_dangerous_permissions,
|
|
16
|
+
effective_runtime_config,
|
|
17
|
+
process_ancestry,
|
|
18
|
+
process_info,
|
|
19
|
+
requires_direct_leader_receiver,
|
|
20
|
+
)
|
|
21
|
+
from team_agent.launch.core import launch
|
|
22
|
+
from team_agent.launch.requirements import ensure_agent_start_requirements
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"DANGEROUS_LEADER_FLAGS",
|
|
26
|
+
"attach_team_profile_dirs",
|
|
27
|
+
"command_has_flag",
|
|
28
|
+
"compile_team_dir_spec",
|
|
29
|
+
"detect_inherited_dangerous_permissions",
|
|
30
|
+
"effective_runtime_config",
|
|
31
|
+
"ensure_agent_start_requirements",
|
|
32
|
+
"init_workspace",
|
|
33
|
+
"is_team_doc_dir",
|
|
34
|
+
"launch",
|
|
35
|
+
"process_ancestry",
|
|
36
|
+
"process_info",
|
|
37
|
+
"requires_direct_leader_receiver",
|
|
38
|
+
"spec_team_dir",
|
|
39
|
+
"tmux_session_conflict_error",
|
|
40
|
+
"validate_file",
|
|
41
|
+
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from team_agent.events import EventLog
|
|
7
|
+
from team_agent.simple_yaml import dumps
|
|
8
|
+
from team_agent.spec import load_spec, workspace_from_spec
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def init_workspace(workspace: Path, force: bool = False) -> dict[str, Path]:
|
|
12
|
+
from team_agent.runtime import ensure_workspace_dirs
|
|
13
|
+
from team_agent.paths import example_path, template_path
|
|
14
|
+
|
|
15
|
+
ensure_workspace_dirs(workspace)
|
|
16
|
+
team_dir = workspace / ".team" / "current"
|
|
17
|
+
team_dir.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
spec_path = team_dir / "team.spec.yaml"
|
|
19
|
+
state_path = workspace / "team_state.md"
|
|
20
|
+
if spec_path.exists() and not force:
|
|
21
|
+
from team_agent.runtime import RuntimeError
|
|
22
|
+
raise RuntimeError(f"{spec_path} already exists; pass --force to overwrite")
|
|
23
|
+
spec_path.write_text(example_path("team.spec.yaml").read_text(encoding="utf-8"), encoding="utf-8")
|
|
24
|
+
if not state_path.exists() or force:
|
|
25
|
+
state_path.write_text(template_path("team_state.md").read_text(encoding="utf-8"), encoding="utf-8")
|
|
26
|
+
EventLog(workspace).write("init", spec_path=str(spec_path), state_path=str(state_path))
|
|
27
|
+
return {"spec": spec_path, "state": state_path}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_file(spec_path: Path) -> dict[str, Any]:
|
|
31
|
+
if spec_path.is_dir():
|
|
32
|
+
from team_agent.compiler import compile_team
|
|
33
|
+
|
|
34
|
+
result = compile_team(spec_path)
|
|
35
|
+
spec = result["spec"]
|
|
36
|
+
return {
|
|
37
|
+
"ok": True,
|
|
38
|
+
"type": "team_dir",
|
|
39
|
+
"workspace": str(Path(spec["team"]["workspace"]).resolve()),
|
|
40
|
+
"team": spec["team"]["name"],
|
|
41
|
+
"agents": [agent["id"] for agent in spec.get("agents", [])],
|
|
42
|
+
}
|
|
43
|
+
spec = load_spec(spec_path)
|
|
44
|
+
workspace = workspace_from_spec(spec, spec_path)
|
|
45
|
+
return {"ok": True, "workspace": str(workspace), "team": spec["team"]["name"]}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def tmux_session_conflict_error(session_name: str) -> str:
|
|
49
|
+
return (
|
|
50
|
+
f"tmux session already exists: {session_name}. "
|
|
51
|
+
"Startup will not terminate existing tmux sessions because they may belong to active teams. "
|
|
52
|
+
"Use a different team name or runtime.session_name and start again."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def spec_team_dir(spec_path: Path, workspace: Path) -> Path:
|
|
57
|
+
spec_dir = spec_path.resolve().parent
|
|
58
|
+
if spec_dir.parent.name == ".team":
|
|
59
|
+
return spec_dir
|
|
60
|
+
return workspace.resolve() / ".team" / "current"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_team_doc_dir(team_dir: Path) -> bool:
|
|
64
|
+
return (team_dir / "TEAM.md").exists() and (team_dir / "agents").is_dir()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def compile_team_dir_spec(team_dir: Path, workspace: Path) -> dict[str, Any]:
|
|
68
|
+
from team_agent.compiler import compile_team
|
|
69
|
+
|
|
70
|
+
spec_path = team_dir / "team.spec.yaml"
|
|
71
|
+
compiled = compile_team(team_dir, spec_path)
|
|
72
|
+
if compiled["spec"].get("context", {}).get("state_file") == "team_state.md":
|
|
73
|
+
state_file = str(team_dir.relative_to(workspace) / "team_state.md") if team_dir.is_relative_to(workspace) else "team_state.md"
|
|
74
|
+
compiled["spec"]["context"]["state_file"] = state_file
|
|
75
|
+
spec_path.write_text(dumps(compiled["spec"]), encoding="utf-8")
|
|
76
|
+
return compiled
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def attach_team_profile_dirs(spec: dict[str, Any], spec_path: Path, workspace: Path | None = None, team_dir: Path | None = None) -> None:
|
|
80
|
+
workspace = workspace.resolve() if workspace else workspace_from_spec(spec, spec_path)
|
|
81
|
+
team_dir = team_dir.resolve() if team_dir else spec_team_dir(spec_path, workspace)
|
|
82
|
+
profiles_dir = team_dir / "profiles"
|
|
83
|
+
for agent in spec.get("agents", []):
|
|
84
|
+
if isinstance(agent, dict) and agent.get("profile"):
|
|
85
|
+
agent["_profile_dir"] = str(profiles_dir)
|