@team-agent/installer 0.1.7 → 0.1.9
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/package.json +1 -2
- package/schemas/team.schema.json +1 -1
- package/src/team_agent/runtime.py +510 -15
- package/src/team_agent/spec.py +1 -1
- package/tests/run_tests.py +0 -5651
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
"npm",
|
|
30
30
|
"scripts",
|
|
31
31
|
"src",
|
|
32
|
-
"tests",
|
|
33
32
|
"skills",
|
|
34
33
|
"templates",
|
|
35
34
|
"examples",
|
package/schemas/team.schema.json
CHANGED
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"additionalProperties": false,
|
|
64
64
|
"properties": {
|
|
65
65
|
"backend": { "enum": ["tmux", "pty"] },
|
|
66
|
-
"display_backend": { "enum": ["none", "tmux_attach", "iterm", "ghostty"] },
|
|
66
|
+
"display_backend": { "enum": ["none", "tmux_attach", "iterm", "ghostty", "ghostty_window", "ghostty_workspace"] },
|
|
67
67
|
"session_name": { "type": "string", "minLength": 1 },
|
|
68
68
|
"auto_launch": { "type": "boolean" },
|
|
69
69
|
"require_user_approval_before_launch": { "type": "boolean" },
|
|
@@ -54,6 +54,7 @@ TMUX_PANE_FORMAT = (
|
|
|
54
54
|
"#{pane_current_path}\t#{session_attached}"
|
|
55
55
|
)
|
|
56
56
|
HEALTH_STATUSES = {"RUNNING", "IDLE", "AWAITING_APPROVAL", "BLOCKED", "ERROR", "DONE"}
|
|
57
|
+
GHOSTTY_DISPLAY_BACKENDS = {"ghostty", "ghostty_window", "ghostty_workspace"}
|
|
57
58
|
PEEK_MAX_LINES = 80
|
|
58
59
|
PEEK_SEARCH_SCAN_LINES = 300
|
|
59
60
|
PEEK_MAX_MATCHES = 5
|
|
@@ -456,10 +457,16 @@ def launch(
|
|
|
456
457
|
timeout_s=1.5,
|
|
457
458
|
exclude_session_ids=known_session_ids,
|
|
458
459
|
)
|
|
459
|
-
if state.get("display_backend") in
|
|
460
|
+
if state.get("display_backend") in GHOSTTY_DISPLAY_BACKENDS:
|
|
460
461
|
display_jobs.append((agent["id"], agent))
|
|
461
462
|
started.append({"agent_id": agent["id"], "provider": agent["provider"], "window": agent["id"]})
|
|
462
|
-
for agent_id, display in _open_worker_displays(
|
|
463
|
+
for agent_id, display in _open_worker_displays(
|
|
464
|
+
workspace,
|
|
465
|
+
session_name,
|
|
466
|
+
display_jobs,
|
|
467
|
+
event_log,
|
|
468
|
+
state.get("display_backend", "none"),
|
|
469
|
+
).items():
|
|
463
470
|
if agent_id in state["agents"]:
|
|
464
471
|
state["agents"][agent_id]["display"] = display
|
|
465
472
|
save_runtime_state(workspace, state)
|
|
@@ -1753,6 +1760,7 @@ def shutdown(workspace: Path, keep_logs: bool = True) -> dict[str, Any]:
|
|
|
1753
1760
|
if proc.returncode == 0:
|
|
1754
1761
|
log_path.write_text(proc.stdout, encoding="utf-8")
|
|
1755
1762
|
captured.append(str(log_path))
|
|
1763
|
+
_close_ghostty_workspace(state, event_log)
|
|
1756
1764
|
for agent_id, agent_state in state.get("agents", {}).items():
|
|
1757
1765
|
_close_ghostty_display(agent_id, agent_state, event_log)
|
|
1758
1766
|
closed_displays.add(agent_id)
|
|
@@ -1762,6 +1770,7 @@ def shutdown(workspace: Path, keep_logs: bool = True) -> dict[str, Any]:
|
|
|
1762
1770
|
event_log.write("shutdown.kill_session", session=session_name, keep_logs=keep_logs, captured=captured)
|
|
1763
1771
|
else:
|
|
1764
1772
|
event_log.write("shutdown.idempotent", session=session_name, reason="session missing")
|
|
1773
|
+
_close_ghostty_workspace(state, event_log)
|
|
1765
1774
|
for agent_id, agent_state in state.get("agents", {}).items():
|
|
1766
1775
|
if agent_id not in closed_displays:
|
|
1767
1776
|
_close_ghostty_display(agent_id, agent_state, event_log)
|
|
@@ -1995,7 +2004,7 @@ def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None)
|
|
|
1995
2004
|
timeout_s=1.5,
|
|
1996
2005
|
exclude_session_ids=known_session_ids,
|
|
1997
2006
|
)
|
|
1998
|
-
if display_backend in
|
|
2007
|
+
if display_backend in GHOSTTY_DISPLAY_BACKENDS:
|
|
1999
2008
|
display_jobs.append((agent["id"], agent))
|
|
2000
2009
|
new_agents[agent["id"]] = agent_state
|
|
2001
2010
|
restarted.append(
|
|
@@ -2006,7 +2015,7 @@ def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None)
|
|
|
2006
2015
|
"display_target": None,
|
|
2007
2016
|
}
|
|
2008
2017
|
)
|
|
2009
|
-
display_results = _open_worker_displays(workspace, session_name, display_jobs, event_log)
|
|
2018
|
+
display_results = _open_worker_displays(workspace, session_name, display_jobs, event_log, display_backend)
|
|
2010
2019
|
for agent_id, display in display_results.items():
|
|
2011
2020
|
if agent_id in new_agents:
|
|
2012
2021
|
new_agents[agent_id]["display"] = display
|
|
@@ -2072,6 +2081,10 @@ def _start_agent_unlocked(workspace: Path, agent_id: str, force: bool, open_disp
|
|
|
2072
2081
|
display = agent_state.get("display") or {}
|
|
2073
2082
|
if display.get("status") != "opened":
|
|
2074
2083
|
agent_state["display"] = _open_ghostty_worker_window(workspace, session_name, agent_id, agent, event_log)
|
|
2084
|
+
elif open_display and state.get("display_backend") == "ghostty_workspace":
|
|
2085
|
+
display = agent_state.get("display") or {}
|
|
2086
|
+
if display.get("status") != "opened":
|
|
2087
|
+
agent_state["display"] = _open_ghostty_workspace_agent_display(session_name, agent_id, agent, display, event_log)
|
|
2075
2088
|
state["agents"][agent_id] = agent_state
|
|
2076
2089
|
save_runtime_state(workspace, state)
|
|
2077
2090
|
write_team_state(workspace, spec, state)
|
|
@@ -2248,6 +2261,14 @@ def _start_agent_unlocked(workspace: Path, agent_id: str, force: bool, open_disp
|
|
|
2248
2261
|
_capture_agent_session(workspace, agent_id, agent_state, event_log, timeout_s=1.5, exclude_session_ids=known_session_ids)
|
|
2249
2262
|
if open_display and state.get("display_backend") in {"ghostty", "ghostty_window"}:
|
|
2250
2263
|
agent_state["display"] = _open_ghostty_worker_window(workspace, session_name, agent_id, agent, event_log)
|
|
2264
|
+
elif open_display and state.get("display_backend") == "ghostty_workspace":
|
|
2265
|
+
agent_state["display"] = _open_ghostty_workspace_agent_display(
|
|
2266
|
+
session_name,
|
|
2267
|
+
agent_id,
|
|
2268
|
+
agent,
|
|
2269
|
+
previous.get("display") or {},
|
|
2270
|
+
event_log,
|
|
2271
|
+
)
|
|
2251
2272
|
state["agents"][agent_id] = agent_state
|
|
2252
2273
|
save_runtime_state(workspace, state)
|
|
2253
2274
|
store = MessageStore(workspace)
|
|
@@ -3030,7 +3051,7 @@ def preflight(team_dir: Path) -> dict[str, Any]:
|
|
|
3030
3051
|
ok = ok and bool(tmux_path)
|
|
3031
3052
|
ghostty = _ghostty_command()
|
|
3032
3053
|
ghostty_check = {"name": "ghostty", "ok": bool(ghostty), "path": ghostty, "required": False}
|
|
3033
|
-
if spec and spec.get("runtime", {}).get("display_backend") in
|
|
3054
|
+
if spec and spec.get("runtime", {}).get("display_backend") in GHOSTTY_DISPLAY_BACKENDS:
|
|
3034
3055
|
ghostty_check["required"] = True
|
|
3035
3056
|
ok = ok and bool(ghostty)
|
|
3036
3057
|
checks.append(ghostty_check)
|
|
@@ -4508,14 +4529,34 @@ def _ghostty_command() -> str | None:
|
|
|
4508
4529
|
)
|
|
4509
4530
|
|
|
4510
4531
|
|
|
4532
|
+
def _ghostty_app_exists() -> bool:
|
|
4533
|
+
return Path("/Applications/Ghostty.app").exists()
|
|
4534
|
+
|
|
4535
|
+
|
|
4536
|
+
def _ghostty_pids_by_title(title: str, wait_s: float = 0.0) -> list[int]:
|
|
4537
|
+
deadline = time.monotonic() + max(wait_s, 0.0)
|
|
4538
|
+
while True:
|
|
4539
|
+
pgrep = run_cmd(["pgrep", "-f", f"--title={title}"], timeout=5)
|
|
4540
|
+
if pgrep.returncode == 0:
|
|
4541
|
+
pids = [int(pid) for pid in pgrep.stdout.split() if pid.isdigit()]
|
|
4542
|
+
if pids:
|
|
4543
|
+
return pids
|
|
4544
|
+
if time.monotonic() >= deadline:
|
|
4545
|
+
return []
|
|
4546
|
+
time.sleep(0.2)
|
|
4547
|
+
|
|
4548
|
+
|
|
4511
4549
|
def _open_worker_displays(
|
|
4512
4550
|
workspace: Path,
|
|
4513
4551
|
session_name: str,
|
|
4514
4552
|
jobs: list[tuple[str, dict[str, Any]]],
|
|
4515
4553
|
event_log: EventLog,
|
|
4554
|
+
display_backend: str = "ghostty_window",
|
|
4516
4555
|
) -> dict[str, dict[str, Any]]:
|
|
4517
4556
|
if not jobs:
|
|
4518
4557
|
return {}
|
|
4558
|
+
if display_backend == "ghostty_workspace":
|
|
4559
|
+
return _open_ghostty_workspace(workspace, session_name, jobs, event_log)
|
|
4519
4560
|
if len(jobs) == 1:
|
|
4520
4561
|
agent_id, agent = jobs[0]
|
|
4521
4562
|
return {agent_id: _open_ghostty_worker_window(workspace, session_name, agent_id, agent, event_log)}
|
|
@@ -4550,7 +4591,7 @@ def _open_ghostty_worker_window(
|
|
|
4550
4591
|
agent: dict[str, Any],
|
|
4551
4592
|
event_log: EventLog,
|
|
4552
4593
|
) -> dict[str, Any]:
|
|
4553
|
-
if not
|
|
4594
|
+
if not _ghostty_app_exists():
|
|
4554
4595
|
blocker = {
|
|
4555
4596
|
"backend": "ghostty_window",
|
|
4556
4597
|
"status": "blocked",
|
|
@@ -4592,15 +4633,129 @@ def _open_ghostty_worker_window(
|
|
|
4592
4633
|
if proc.returncode != 0:
|
|
4593
4634
|
display["reason"] = proc.stderr.strip() or proc.stdout.strip() or "open Ghostty.app failed"
|
|
4594
4635
|
else:
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
if pgrep.returncode == 0:
|
|
4598
|
-
display["pids"] = [int(pid) for pid in pgrep.stdout.split() if pid.isdigit()]
|
|
4599
|
-
display["pid"] = display["pids"][0] if display["pids"] else None
|
|
4636
|
+
display["pids"] = _ghostty_pids_by_title(title, wait_s=3.0)
|
|
4637
|
+
display["pid"] = display["pids"][0] if display["pids"] else None
|
|
4600
4638
|
event_log.write("display.ghostty_window", agent_id=agent["id"], **display)
|
|
4601
4639
|
return display
|
|
4602
4640
|
|
|
4603
4641
|
|
|
4642
|
+
def _open_ghostty_workspace(
|
|
4643
|
+
workspace: Path,
|
|
4644
|
+
session_name: str,
|
|
4645
|
+
jobs: list[tuple[str, dict[str, Any]]],
|
|
4646
|
+
event_log: EventLog,
|
|
4647
|
+
) -> dict[str, dict[str, Any]]:
|
|
4648
|
+
if not _ghostty_app_exists():
|
|
4649
|
+
return _ghostty_workspace_blocked(jobs, event_log, "ghostty_app_missing")
|
|
4650
|
+
aggregator_session = _ghostty_workspace_aggregator_name(session_name)
|
|
4651
|
+
linked_results = _prepare_ghostty_workspace_linked_sessions(session_name, jobs)
|
|
4652
|
+
displays: dict[str, dict[str, Any]] = {}
|
|
4653
|
+
linked_jobs: list[tuple[str, dict[str, Any], str]] = []
|
|
4654
|
+
for agent_id, agent in jobs:
|
|
4655
|
+
linked = linked_results.get(agent_id, {})
|
|
4656
|
+
linked_session = linked.get("linked_session") or _ghostty_display_session_name(session_name, agent_id)
|
|
4657
|
+
if linked.get("ok"):
|
|
4658
|
+
linked_jobs.append((agent_id, agent, linked_session))
|
|
4659
|
+
continue
|
|
4660
|
+
displays.update(
|
|
4661
|
+
_ghostty_workspace_blocked(
|
|
4662
|
+
[(agent_id, agent)],
|
|
4663
|
+
event_log,
|
|
4664
|
+
linked.get("reason", "display_session_create_failed"),
|
|
4665
|
+
aggregator_session=aggregator_session,
|
|
4666
|
+
linked_sessions={agent_id: linked_session},
|
|
4667
|
+
error=linked.get("error"),
|
|
4668
|
+
target=f"{session_name}:{agent_id}",
|
|
4669
|
+
)
|
|
4670
|
+
)
|
|
4671
|
+
if not linked_jobs:
|
|
4672
|
+
return displays
|
|
4673
|
+
prepared = _prepare_ghostty_workspace_aggregator(aggregator_session, linked_jobs)
|
|
4674
|
+
if not prepared["ok"]:
|
|
4675
|
+
_kill_ghostty_workspace_linked_sessions([linked_session for _agent_id, _agent, linked_session in linked_jobs])
|
|
4676
|
+
displays.update(
|
|
4677
|
+
_ghostty_workspace_blocked(
|
|
4678
|
+
[(agent_id, agent) for agent_id, agent, _linked_session in linked_jobs],
|
|
4679
|
+
event_log,
|
|
4680
|
+
prepared["reason"],
|
|
4681
|
+
aggregator_session=aggregator_session,
|
|
4682
|
+
linked_sessions={agent_id: linked_session for agent_id, _agent, linked_session in linked_jobs},
|
|
4683
|
+
error=prepared.get("error"),
|
|
4684
|
+
target=prepared.get("target"),
|
|
4685
|
+
)
|
|
4686
|
+
)
|
|
4687
|
+
return displays
|
|
4688
|
+
title = f"team-agent:{session_name}:workspace"
|
|
4689
|
+
launch_args = _ghostty_attach_args(aggregator_session, title)
|
|
4690
|
+
proc = run_cmd(launch_args, timeout=10)
|
|
4691
|
+
if proc.returncode != 0:
|
|
4692
|
+
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4693
|
+
_kill_ghostty_workspace_linked_sessions([linked_session for _agent_id, _agent, linked_session in linked_jobs])
|
|
4694
|
+
displays.update(
|
|
4695
|
+
_ghostty_workspace_blocked(
|
|
4696
|
+
[(agent_id, agent) for agent_id, agent, _linked_session in linked_jobs],
|
|
4697
|
+
event_log,
|
|
4698
|
+
"open Ghostty.app failed",
|
|
4699
|
+
aggregator_session=aggregator_session,
|
|
4700
|
+
linked_sessions={agent_id: linked_session for agent_id, _agent, linked_session in linked_jobs},
|
|
4701
|
+
error=proc.stderr.strip() or proc.stdout.strip(),
|
|
4702
|
+
)
|
|
4703
|
+
)
|
|
4704
|
+
return displays
|
|
4705
|
+
pids = _ghostty_pids_by_title(title, wait_s=3.0)
|
|
4706
|
+
panes = {pane["agent_id"]: pane for pane in prepared["panes"]}
|
|
4707
|
+
for agent_id, agent, linked_session in linked_jobs:
|
|
4708
|
+
pane = panes.get(agent_id, {})
|
|
4709
|
+
display = {
|
|
4710
|
+
"backend": "ghostty_workspace",
|
|
4711
|
+
"status": "opened",
|
|
4712
|
+
"title": title,
|
|
4713
|
+
"pane_title": pane.get("title") or _ghostty_workspace_pane_title(agent),
|
|
4714
|
+
"target": f"{session_name}:{agent_id}",
|
|
4715
|
+
"linked_session": linked_session,
|
|
4716
|
+
"aggregator_session": aggregator_session,
|
|
4717
|
+
"display_session": aggregator_session,
|
|
4718
|
+
"pane_id": pane.get("pane_id"),
|
|
4719
|
+
"launch_args": launch_args,
|
|
4720
|
+
"pid": pids[0] if pids else None,
|
|
4721
|
+
"pids": pids,
|
|
4722
|
+
"tty": None,
|
|
4723
|
+
"fallback": "tmux_headless",
|
|
4724
|
+
"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.",
|
|
4725
|
+
}
|
|
4726
|
+
event_log.write("display.ghostty_workspace", agent_id=agent_id, **display)
|
|
4727
|
+
displays[agent_id] = display
|
|
4728
|
+
return displays
|
|
4729
|
+
|
|
4730
|
+
|
|
4731
|
+
def _ghostty_workspace_blocked(
|
|
4732
|
+
jobs: list[tuple[str, dict[str, Any]]],
|
|
4733
|
+
event_log: EventLog,
|
|
4734
|
+
reason: str,
|
|
4735
|
+
aggregator_session: str | None = None,
|
|
4736
|
+
linked_sessions: dict[str, str] | None = None,
|
|
4737
|
+
error: str | None = None,
|
|
4738
|
+
target: str | None = None,
|
|
4739
|
+
) -> dict[str, dict[str, Any]]:
|
|
4740
|
+
displays: dict[str, dict[str, Any]] = {}
|
|
4741
|
+
for agent_id, _agent in jobs:
|
|
4742
|
+
linked_session = (linked_sessions or {}).get(agent_id)
|
|
4743
|
+
display = {
|
|
4744
|
+
"backend": "ghostty_workspace",
|
|
4745
|
+
"status": "blocked",
|
|
4746
|
+
"reason": reason,
|
|
4747
|
+
"error": error,
|
|
4748
|
+
"target": target or f"{agent_id}",
|
|
4749
|
+
"linked_session": linked_session,
|
|
4750
|
+
"aggregator_session": aggregator_session,
|
|
4751
|
+
"display_session": aggregator_session,
|
|
4752
|
+
"fallback": "tmux_headless",
|
|
4753
|
+
}
|
|
4754
|
+
event_log.write("display.ghostty_workspace_blocked", agent_id=agent_id, **display)
|
|
4755
|
+
displays[agent_id] = display
|
|
4756
|
+
return displays
|
|
4757
|
+
|
|
4758
|
+
|
|
4604
4759
|
def _ghostty_display_session_name(session_name: str, window_name: str) -> str:
|
|
4605
4760
|
raw = f"{session_name}:{window_name}"
|
|
4606
4761
|
digest = hashlib.sha1(raw.encode("utf-8")).hexdigest()[:8]
|
|
@@ -4628,6 +4783,284 @@ def _prepare_ghostty_display_session(session_name: str, window_name: str, displa
|
|
|
4628
4783
|
return {"ok": True, "display_session": display_session}
|
|
4629
4784
|
|
|
4630
4785
|
|
|
4786
|
+
def _ghostty_workspace_aggregator_name(session_name: str) -> str:
|
|
4787
|
+
raw = f"{session_name}:workspace"
|
|
4788
|
+
digest = hashlib.sha1(raw.encode("utf-8")).hexdigest()[:8]
|
|
4789
|
+
safe_session = re.sub(r"[^A-Za-z0-9_.-]", "_", session_name)[:80].strip("._-") or "team"
|
|
4790
|
+
return f"{safe_session}__display__workspace__{digest}"
|
|
4791
|
+
|
|
4792
|
+
|
|
4793
|
+
def _ghostty_workspace_pane_command(linked_session: str) -> str:
|
|
4794
|
+
return f"TMUX= tmux attach-session -t {shlex.quote(linked_session)}"
|
|
4795
|
+
|
|
4796
|
+
|
|
4797
|
+
def _ghostty_workspace_pane_title(agent: dict[str, Any]) -> str:
|
|
4798
|
+
return f"team-agent:{agent['id']}:{agent.get('role', '')}"
|
|
4799
|
+
|
|
4800
|
+
|
|
4801
|
+
def _prepare_ghostty_workspace_linked_sessions(
|
|
4802
|
+
session_name: str,
|
|
4803
|
+
jobs: list[tuple[str, dict[str, Any]]],
|
|
4804
|
+
) -> dict[str, dict[str, Any]]:
|
|
4805
|
+
def prepare(agent_id: str) -> dict[str, Any]:
|
|
4806
|
+
linked_session = _ghostty_display_session_name(session_name, agent_id)
|
|
4807
|
+
result = _prepare_ghostty_display_session(session_name, agent_id, linked_session)
|
|
4808
|
+
result["linked_session"] = linked_session
|
|
4809
|
+
return result
|
|
4810
|
+
|
|
4811
|
+
if len(jobs) == 1:
|
|
4812
|
+
agent_id, _agent = jobs[0]
|
|
4813
|
+
return {agent_id: prepare(agent_id)}
|
|
4814
|
+
results: dict[str, dict[str, Any]] = {}
|
|
4815
|
+
max_workers = min(4, len(jobs))
|
|
4816
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
4817
|
+
futures = {executor.submit(prepare, agent_id): agent_id for agent_id, _agent in jobs}
|
|
4818
|
+
for future in as_completed(futures):
|
|
4819
|
+
agent_id = futures[future]
|
|
4820
|
+
try:
|
|
4821
|
+
results[agent_id] = future.result()
|
|
4822
|
+
except Exception as exc:
|
|
4823
|
+
results[agent_id] = {
|
|
4824
|
+
"ok": False,
|
|
4825
|
+
"reason": "display_session_create_exception",
|
|
4826
|
+
"error": str(exc),
|
|
4827
|
+
"linked_session": _ghostty_display_session_name(session_name, agent_id),
|
|
4828
|
+
}
|
|
4829
|
+
return results
|
|
4830
|
+
|
|
4831
|
+
|
|
4832
|
+
def _prepare_ghostty_workspace_aggregator(
|
|
4833
|
+
aggregator_session: str,
|
|
4834
|
+
linked_jobs: list[tuple[str, dict[str, Any], str]],
|
|
4835
|
+
) -> dict[str, Any]:
|
|
4836
|
+
window_name = "overview"
|
|
4837
|
+
if _tmux_session_exists(aggregator_session):
|
|
4838
|
+
proc = run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4839
|
+
if proc.returncode != 0:
|
|
4840
|
+
return {"ok": False, "reason": "display_session_cleanup_failed", "error": proc.stderr.strip()}
|
|
4841
|
+
|
|
4842
|
+
panes: list[dict[str, Any]] = []
|
|
4843
|
+
first_agent_id, first_agent, first_linked_session = linked_jobs[0]
|
|
4844
|
+
proc = run_cmd(
|
|
4845
|
+
[
|
|
4846
|
+
"tmux",
|
|
4847
|
+
"new-session",
|
|
4848
|
+
"-d",
|
|
4849
|
+
"-P",
|
|
4850
|
+
"-F",
|
|
4851
|
+
"#{pane_id}",
|
|
4852
|
+
"-s",
|
|
4853
|
+
aggregator_session,
|
|
4854
|
+
"-n",
|
|
4855
|
+
window_name,
|
|
4856
|
+
_ghostty_workspace_pane_command(first_linked_session),
|
|
4857
|
+
],
|
|
4858
|
+
timeout=10,
|
|
4859
|
+
)
|
|
4860
|
+
if proc.returncode != 0:
|
|
4861
|
+
return {"ok": False, "reason": "display_session_create_failed", "error": proc.stderr.strip()}
|
|
4862
|
+
first_pane_id = _tmux_stdout_last_line(proc.stdout) or f"{aggregator_session}:{window_name}.0"
|
|
4863
|
+
first_title = _ghostty_workspace_pane_title(first_agent)
|
|
4864
|
+
title_result = _set_ghostty_workspace_pane_title(first_pane_id, first_title)
|
|
4865
|
+
if not title_result["ok"]:
|
|
4866
|
+
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4867
|
+
return title_result
|
|
4868
|
+
panes.append({"agent_id": first_agent_id, "pane_id": first_pane_id, "title": first_title, "linked_session": first_linked_session})
|
|
4869
|
+
|
|
4870
|
+
proc = run_cmd(["tmux", "set-window-option", "-t", f"{aggregator_session}:{window_name}", "remain-on-exit", "on"], timeout=10)
|
|
4871
|
+
if proc.returncode != 0:
|
|
4872
|
+
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4873
|
+
return {"ok": False, "reason": "display_session_remain_on_exit_failed", "error": proc.stderr.strip()}
|
|
4874
|
+
|
|
4875
|
+
for index, (agent_id, agent, linked_session) in enumerate(linked_jobs[1:], start=1):
|
|
4876
|
+
proc = run_cmd(
|
|
4877
|
+
[
|
|
4878
|
+
"tmux",
|
|
4879
|
+
"split-window",
|
|
4880
|
+
"-t",
|
|
4881
|
+
f"{aggregator_session}:{window_name}",
|
|
4882
|
+
"-h",
|
|
4883
|
+
"-P",
|
|
4884
|
+
"-F",
|
|
4885
|
+
"#{pane_id}",
|
|
4886
|
+
_ghostty_workspace_pane_command(linked_session),
|
|
4887
|
+
],
|
|
4888
|
+
timeout=10,
|
|
4889
|
+
)
|
|
4890
|
+
if proc.returncode != 0:
|
|
4891
|
+
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4892
|
+
return {"ok": False, "reason": "display_session_split_failed", "error": proc.stderr.strip(), "target": linked_session}
|
|
4893
|
+
pane_id = _tmux_stdout_last_line(proc.stdout) or f"{aggregator_session}:{window_name}.{index}"
|
|
4894
|
+
title = _ghostty_workspace_pane_title(agent)
|
|
4895
|
+
title_result = _set_ghostty_workspace_pane_title(pane_id, title)
|
|
4896
|
+
if not title_result["ok"]:
|
|
4897
|
+
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4898
|
+
return title_result
|
|
4899
|
+
panes.append({"agent_id": agent_id, "pane_id": pane_id, "title": title, "linked_session": linked_session})
|
|
4900
|
+
|
|
4901
|
+
proc = run_cmd(["tmux", "select-layout", "-t", f"{aggregator_session}:{window_name}", "even-horizontal"], timeout=10)
|
|
4902
|
+
if proc.returncode != 0:
|
|
4903
|
+
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4904
|
+
return {"ok": False, "reason": "display_session_layout_failed", "error": proc.stderr.strip()}
|
|
4905
|
+
return {"ok": True, "aggregator_session": aggregator_session, "panes": panes}
|
|
4906
|
+
|
|
4907
|
+
|
|
4908
|
+
def _set_ghostty_workspace_pane_title(pane_id: str, title: str) -> dict[str, Any]:
|
|
4909
|
+
proc = run_cmd(["tmux", "select-pane", "-t", pane_id, "-T", title], timeout=10)
|
|
4910
|
+
if proc.returncode != 0:
|
|
4911
|
+
return {"ok": False, "reason": "display_session_pane_title_failed", "error": proc.stderr.strip()}
|
|
4912
|
+
return {"ok": True}
|
|
4913
|
+
|
|
4914
|
+
|
|
4915
|
+
def _tmux_stdout_last_line(stdout: str) -> str | None:
|
|
4916
|
+
lines = [line.strip() for line in stdout.splitlines() if line.strip()]
|
|
4917
|
+
return lines[-1] if lines else None
|
|
4918
|
+
|
|
4919
|
+
|
|
4920
|
+
def _open_ghostty_workspace_agent_display(
|
|
4921
|
+
session_name: str,
|
|
4922
|
+
agent_id: str,
|
|
4923
|
+
agent: dict[str, Any],
|
|
4924
|
+
previous_display: dict[str, Any],
|
|
4925
|
+
event_log: EventLog,
|
|
4926
|
+
) -> dict[str, Any]:
|
|
4927
|
+
if not _ghostty_app_exists():
|
|
4928
|
+
return _ghostty_workspace_blocked(
|
|
4929
|
+
[(agent_id, agent)],
|
|
4930
|
+
event_log,
|
|
4931
|
+
"ghostty_app_missing",
|
|
4932
|
+
aggregator_session=_ghostty_workspace_aggregator_name(session_name),
|
|
4933
|
+
linked_sessions={agent_id: _ghostty_display_session_name(session_name, agent_id)},
|
|
4934
|
+
target=f"{session_name}:{agent_id}",
|
|
4935
|
+
)[agent_id]
|
|
4936
|
+
aggregator_session = str(
|
|
4937
|
+
previous_display.get("aggregator_session")
|
|
4938
|
+
or previous_display.get("display_session")
|
|
4939
|
+
or _ghostty_workspace_aggregator_name(session_name)
|
|
4940
|
+
)
|
|
4941
|
+
linked_session = _ghostty_display_session_name(session_name, agent_id)
|
|
4942
|
+
prepared = _prepare_ghostty_display_session(session_name, agent_id, linked_session)
|
|
4943
|
+
if not prepared["ok"]:
|
|
4944
|
+
return _ghostty_workspace_blocked(
|
|
4945
|
+
[(agent_id, agent)],
|
|
4946
|
+
event_log,
|
|
4947
|
+
prepared["reason"],
|
|
4948
|
+
aggregator_session=aggregator_session,
|
|
4949
|
+
linked_sessions={agent_id: linked_session},
|
|
4950
|
+
error=prepared.get("error"),
|
|
4951
|
+
target=f"{session_name}:{agent_id}",
|
|
4952
|
+
)[agent_id]
|
|
4953
|
+
if not _tmux_session_exists(aggregator_session):
|
|
4954
|
+
return _ghostty_workspace_partial_update_display(
|
|
4955
|
+
session_name,
|
|
4956
|
+
agent_id,
|
|
4957
|
+
agent,
|
|
4958
|
+
event_log,
|
|
4959
|
+
reason="aggregator_session_missing",
|
|
4960
|
+
note="pane refresh requires full team restart",
|
|
4961
|
+
)
|
|
4962
|
+
|
|
4963
|
+
pane_title = _ghostty_workspace_pane_title(agent)
|
|
4964
|
+
command = _ghostty_workspace_pane_command(linked_session)
|
|
4965
|
+
pane_id = str(previous_display.get("pane_id") or "")
|
|
4966
|
+
refreshed = False
|
|
4967
|
+
if pane_id:
|
|
4968
|
+
proc = run_cmd(["tmux", "respawn-pane", "-k", "-t", pane_id, command], timeout=10)
|
|
4969
|
+
refreshed = proc.returncode == 0
|
|
4970
|
+
if not refreshed:
|
|
4971
|
+
proc = run_cmd(
|
|
4972
|
+
[
|
|
4973
|
+
"tmux",
|
|
4974
|
+
"split-window",
|
|
4975
|
+
"-t",
|
|
4976
|
+
f"{aggregator_session}:overview",
|
|
4977
|
+
"-h",
|
|
4978
|
+
"-P",
|
|
4979
|
+
"-F",
|
|
4980
|
+
"#{pane_id}",
|
|
4981
|
+
command,
|
|
4982
|
+
],
|
|
4983
|
+
timeout=10,
|
|
4984
|
+
)
|
|
4985
|
+
if proc.returncode != 0:
|
|
4986
|
+
return _ghostty_workspace_partial_update_display(
|
|
4987
|
+
session_name,
|
|
4988
|
+
agent_id,
|
|
4989
|
+
agent,
|
|
4990
|
+
event_log,
|
|
4991
|
+
reason="aggregator_pane_refresh_failed",
|
|
4992
|
+
note=proc.stderr.strip() or "pane refresh requires full team restart",
|
|
4993
|
+
)
|
|
4994
|
+
pane_id = _tmux_stdout_last_line(proc.stdout) or pane_id
|
|
4995
|
+
title_result = _set_ghostty_workspace_pane_title(pane_id, pane_title)
|
|
4996
|
+
if not title_result["ok"]:
|
|
4997
|
+
return _ghostty_workspace_partial_update_display(
|
|
4998
|
+
session_name,
|
|
4999
|
+
agent_id,
|
|
5000
|
+
agent,
|
|
5001
|
+
event_log,
|
|
5002
|
+
reason=title_result["reason"],
|
|
5003
|
+
note=title_result.get("error") or "pane refresh requires full team restart",
|
|
5004
|
+
)
|
|
5005
|
+
run_cmd(["tmux", "select-layout", "-t", f"{aggregator_session}:overview", "even-horizontal"], timeout=10)
|
|
5006
|
+
title = str(previous_display.get("title") or f"team-agent:{session_name}:workspace")
|
|
5007
|
+
pids = [int(pid) for pid in previous_display.get("pids", []) if str(pid).isdigit()]
|
|
5008
|
+
display = {
|
|
5009
|
+
"backend": "ghostty_workspace",
|
|
5010
|
+
"status": "opened",
|
|
5011
|
+
"title": title,
|
|
5012
|
+
"pane_title": pane_title,
|
|
5013
|
+
"target": f"{session_name}:{agent_id}",
|
|
5014
|
+
"linked_session": linked_session,
|
|
5015
|
+
"aggregator_session": aggregator_session,
|
|
5016
|
+
"display_session": aggregator_session,
|
|
5017
|
+
"pane_id": pane_id,
|
|
5018
|
+
"pid": pids[0] if pids else None,
|
|
5019
|
+
"pids": pids,
|
|
5020
|
+
"tty": None,
|
|
5021
|
+
"fallback": "tmux_headless",
|
|
5022
|
+
"note": "Refreshed this worker's Ghostty workspace pane by respawning it against a distinct linked session.",
|
|
5023
|
+
}
|
|
5024
|
+
event_log.write("display.ghostty_workspace", agent_id=agent_id, **display)
|
|
5025
|
+
return display
|
|
5026
|
+
|
|
5027
|
+
|
|
5028
|
+
def _ghostty_workspace_partial_update_display(
|
|
5029
|
+
session_name: str,
|
|
5030
|
+
agent_id: str,
|
|
5031
|
+
agent: dict[str, Any],
|
|
5032
|
+
event_log: EventLog,
|
|
5033
|
+
reason: str = "partial_update_requires_team_restart",
|
|
5034
|
+
note: str = "pane refresh requires full team restart",
|
|
5035
|
+
) -> dict[str, Any]:
|
|
5036
|
+
aggregator_session = _ghostty_workspace_aggregator_name(session_name)
|
|
5037
|
+
display = {
|
|
5038
|
+
"backend": "ghostty_workspace",
|
|
5039
|
+
"status": "blocked",
|
|
5040
|
+
"reason": reason,
|
|
5041
|
+
"target": f"{session_name}:{agent_id}",
|
|
5042
|
+
"linked_session": _ghostty_display_session_name(session_name, agent_id),
|
|
5043
|
+
"aggregator_session": aggregator_session,
|
|
5044
|
+
"display_session": aggregator_session,
|
|
5045
|
+
"pane_title": _ghostty_workspace_pane_title(agent),
|
|
5046
|
+
"fallback": "tmux_headless",
|
|
5047
|
+
"note": note,
|
|
5048
|
+
"action": "restart the team to rebuild the Ghostty workspace layout",
|
|
5049
|
+
}
|
|
5050
|
+
event_log.write("display.ghostty_workspace_partial_update", agent_id=agent_id, **display)
|
|
5051
|
+
return display
|
|
5052
|
+
|
|
5053
|
+
|
|
5054
|
+
def _kill_ghostty_workspace_linked_sessions(linked_sessions: list[str]) -> list[str]:
|
|
5055
|
+
killed: list[str] = []
|
|
5056
|
+
for linked_session in dict.fromkeys(linked_sessions):
|
|
5057
|
+
if _tmux_session_exists(linked_session):
|
|
5058
|
+
proc = run_cmd(["tmux", "kill-session", "-t", linked_session], timeout=10)
|
|
5059
|
+
if proc.returncode == 0:
|
|
5060
|
+
killed.append(linked_session)
|
|
5061
|
+
return killed
|
|
5062
|
+
|
|
5063
|
+
|
|
4631
5064
|
def _ghostty_attach_args(display_session: str, title: str) -> list[str]:
|
|
4632
5065
|
return [
|
|
4633
5066
|
"open",
|
|
@@ -4643,7 +5076,11 @@ def _ghostty_attach_args(display_session: str, title: str) -> list[str]:
|
|
|
4643
5076
|
]
|
|
4644
5077
|
|
|
4645
5078
|
|
|
4646
|
-
def _close_ghostty_display(
|
|
5079
|
+
def _close_ghostty_display(
|
|
5080
|
+
agent_id: str,
|
|
5081
|
+
agent_state: dict[str, Any],
|
|
5082
|
+
event_log: EventLog,
|
|
5083
|
+
) -> None:
|
|
4647
5084
|
display = agent_state.get("display") or {}
|
|
4648
5085
|
if display.get("backend") != "ghostty_window":
|
|
4649
5086
|
return
|
|
@@ -4651,9 +5088,7 @@ def _close_ghostty_display(agent_id: str, agent_state: dict[str, Any], event_log
|
|
|
4651
5088
|
pids = [str(pid) for pid in display.get("pids", []) if str(pid).isdigit()]
|
|
4652
5089
|
title = display.get("title")
|
|
4653
5090
|
if not pids and title:
|
|
4654
|
-
|
|
4655
|
-
if pgrep.returncode == 0:
|
|
4656
|
-
pids = [pid for pid in pgrep.stdout.split() if pid.isdigit()]
|
|
5091
|
+
pids = [str(pid) for pid in _ghostty_pids_by_title(str(title))]
|
|
4657
5092
|
killed: list[str] = []
|
|
4658
5093
|
for pid in pids:
|
|
4659
5094
|
proc = run_cmd(["kill", pid], timeout=5)
|
|
@@ -4674,6 +5109,66 @@ def _close_ghostty_display(agent_id: str, agent_state: dict[str, Any], event_log
|
|
|
4674
5109
|
)
|
|
4675
5110
|
|
|
4676
5111
|
|
|
5112
|
+
def _close_ghostty_workspace(state: dict[str, Any], event_log: EventLog) -> None:
|
|
5113
|
+
displays = [
|
|
5114
|
+
(agent_id, agent_state.get("display") or {})
|
|
5115
|
+
for agent_id, agent_state in state.get("agents", {}).items()
|
|
5116
|
+
if (agent_state.get("display") or {}).get("backend") == "ghostty_workspace"
|
|
5117
|
+
]
|
|
5118
|
+
if not displays:
|
|
5119
|
+
return
|
|
5120
|
+
aggregator_session = next(
|
|
5121
|
+
(
|
|
5122
|
+
str(display.get("aggregator_session") or display.get("display_session"))
|
|
5123
|
+
for _agent_id, display in displays
|
|
5124
|
+
if display.get("aggregator_session") or display.get("display_session")
|
|
5125
|
+
),
|
|
5126
|
+
None,
|
|
5127
|
+
)
|
|
5128
|
+
title = next((str(display.get("title")) for _agent_id, display in displays if display.get("title")), None)
|
|
5129
|
+
pids = {
|
|
5130
|
+
str(pid)
|
|
5131
|
+
for _agent_id, display in displays
|
|
5132
|
+
for pid in display.get("pids", [])
|
|
5133
|
+
if str(pid).isdigit()
|
|
5134
|
+
}
|
|
5135
|
+
if not pids and title:
|
|
5136
|
+
pids = {str(pid) for pid in _ghostty_pids_by_title(str(title))}
|
|
5137
|
+
|
|
5138
|
+
aggregator_closed = False
|
|
5139
|
+
if aggregator_session and _tmux_session_exists(aggregator_session):
|
|
5140
|
+
proc = run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
5141
|
+
if proc.returncode == 0:
|
|
5142
|
+
aggregator_closed = True
|
|
5143
|
+
else:
|
|
5144
|
+
event_log.write(
|
|
5145
|
+
"display.ghostty_workspace_close_failed",
|
|
5146
|
+
aggregator_session=aggregator_session,
|
|
5147
|
+
error=proc.stderr.strip(),
|
|
5148
|
+
)
|
|
5149
|
+
|
|
5150
|
+
linked_sessions = [
|
|
5151
|
+
str(display.get("linked_session"))
|
|
5152
|
+
for _agent_id, display in displays
|
|
5153
|
+
if display.get("linked_session")
|
|
5154
|
+
]
|
|
5155
|
+
linked_closed = _kill_ghostty_workspace_linked_sessions(linked_sessions)
|
|
5156
|
+
|
|
5157
|
+
killed: list[str] = []
|
|
5158
|
+
for pid in sorted(pids):
|
|
5159
|
+
proc = run_cmd(["kill", pid], timeout=5)
|
|
5160
|
+
if proc.returncode == 0:
|
|
5161
|
+
killed.append(pid)
|
|
5162
|
+
event_log.write(
|
|
5163
|
+
"display.ghostty_workspace_closed",
|
|
5164
|
+
pids=killed,
|
|
5165
|
+
title=title,
|
|
5166
|
+
aggregator_session=aggregator_session,
|
|
5167
|
+
linked_sessions=linked_closed,
|
|
5168
|
+
aggregator_closed=aggregator_closed,
|
|
5169
|
+
)
|
|
5170
|
+
|
|
5171
|
+
|
|
4677
5172
|
def get_adapter_or_raise(name: str) -> str:
|
|
4678
5173
|
if name == "tmux" and not shutil_which("tmux"):
|
|
4679
5174
|
raise RuntimeError("tmux is not installed; install tmux 3.3+ before launch")
|
package/src/team_agent/spec.py
CHANGED
|
@@ -196,7 +196,7 @@ def _check_runtime(runtime: Any, errors: list[str]) -> None:
|
|
|
196
196
|
return
|
|
197
197
|
if runtime.get("backend") not in {"tmux", "pty"}:
|
|
198
198
|
errors.append("/runtime/backend: invalid backend")
|
|
199
|
-
if runtime.get("display_backend") not in {"none", "tmux_attach", "iterm", "ghostty", "ghostty_window"}:
|
|
199
|
+
if runtime.get("display_backend") not in {"none", "tmux_attach", "iterm", "ghostty", "ghostty_window", "ghostty_workspace"}:
|
|
200
200
|
errors.append("/runtime/display_backend: invalid display backend")
|
|
201
201
|
if "dangerous_auto_approve" in runtime and not isinstance(runtime["dangerous_auto_approve"], bool):
|
|
202
202
|
errors.append("/runtime/dangerous_auto_approve: must be a boolean")
|