@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.
Files changed (111) hide show
  1. package/crates/team-agent-core/src/lib.rs +50 -5
  2. package/package.json +1 -1
  3. package/schemas/team.schema.json +1 -0
  4. package/skills/team-agent/SKILL.md +1 -1
  5. package/src/team_agent/approvals/__init__.py +65 -0
  6. package/src/team_agent/approvals/constants.py +6 -0
  7. package/src/team_agent/approvals/parsing.py +176 -0
  8. package/src/team_agent/approvals/runtime_prompts.py +171 -0
  9. package/src/team_agent/approvals/status.py +165 -0
  10. package/src/team_agent/cli/__init__.py +135 -0
  11. package/src/team_agent/cli/commands.py +335 -0
  12. package/src/team_agent/cli/e2e.py +202 -0
  13. package/src/team_agent/cli/helpers.py +137 -0
  14. package/src/team_agent/cli/parser.py +470 -0
  15. package/src/team_agent/compiler.py +98 -33
  16. package/src/team_agent/coordinator/__init__.py +53 -0
  17. package/src/team_agent/{coordinator.py → coordinator/__main__.py} +3 -1
  18. package/src/team_agent/coordinator/lifecycle.py +319 -0
  19. package/src/team_agent/coordinator/metadata.py +61 -0
  20. package/src/team_agent/coordinator/paths.py +17 -0
  21. package/src/team_agent/diagnose/__init__.py +48 -0
  22. package/src/team_agent/diagnose/checks.py +101 -0
  23. package/src/team_agent/diagnose/health.py +241 -0
  24. package/src/team_agent/diagnose/preflight.py +194 -0
  25. package/src/team_agent/diagnose/quick_start.py +233 -0
  26. package/src/team_agent/display/__init__.py +61 -0
  27. package/src/team_agent/display/close.py +147 -0
  28. package/src/team_agent/display/ghostty.py +77 -0
  29. package/src/team_agent/display/worker_window.py +110 -0
  30. package/src/team_agent/display/workspace.py +473 -0
  31. package/src/team_agent/launch/__init__.py +41 -0
  32. package/src/team_agent/launch/bootstrap.py +85 -0
  33. package/src/team_agent/launch/config.py +106 -0
  34. package/src/team_agent/launch/core.py +291 -0
  35. package/src/team_agent/launch/requirements.py +57 -0
  36. package/src/team_agent/leader/__init__.py +320 -0
  37. package/src/team_agent/lifecycle/__init__.py +5 -0
  38. package/src/team_agent/lifecycle/agents.py +226 -0
  39. package/src/team_agent/lifecycle/operations.py +321 -0
  40. package/src/team_agent/lifecycle/start.py +360 -0
  41. package/src/team_agent/mcp_server/__init__.py +42 -0
  42. package/src/team_agent/mcp_server/__main__.py +7 -0
  43. package/src/team_agent/mcp_server/contracts.py +148 -0
  44. package/src/team_agent/mcp_server/normalize.py +257 -0
  45. package/src/team_agent/mcp_server/server.py +150 -0
  46. package/src/team_agent/mcp_server/tools.py +205 -0
  47. package/src/team_agent/message_store/__init__.py +23 -0
  48. package/src/team_agent/message_store/agent_health.py +109 -0
  49. package/src/team_agent/{message_store.py → message_store/core.py} +188 -245
  50. package/src/team_agent/message_store/result_watchers.py +102 -0
  51. package/src/team_agent/message_store/schema.py +266 -0
  52. package/src/team_agent/messaging/__init__.py +1 -0
  53. package/src/team_agent/messaging/activity_detector.py +190 -0
  54. package/src/team_agent/messaging/delivery.py +128 -0
  55. package/src/team_agent/messaging/deps.py +263 -0
  56. package/src/team_agent/messaging/idle_alerts.py +217 -0
  57. package/src/team_agent/messaging/internal_delivery.py +46 -0
  58. package/src/team_agent/messaging/leader.py +317 -0
  59. package/src/team_agent/messaging/leader_panes.py +343 -0
  60. package/src/team_agent/messaging/result_delivery.py +300 -0
  61. package/src/team_agent/messaging/results.py +456 -0
  62. package/src/team_agent/messaging/scheduler.py +418 -0
  63. package/src/team_agent/messaging/send.py +493 -0
  64. package/src/team_agent/messaging/tmux_io.py +337 -0
  65. package/src/team_agent/messaging/tmux_prompt.py +229 -0
  66. package/src/team_agent/orchestrator/__init__.py +376 -0
  67. package/src/team_agent/orchestrator/plan.py +122 -0
  68. package/src/team_agent/orchestrator/state.py +128 -0
  69. package/src/team_agent/profiles/__init__.py +82 -0
  70. package/src/team_agent/profiles/constants.py +19 -0
  71. package/src/team_agent/profiles/core.py +407 -0
  72. package/src/team_agent/profiles/helpers.py +69 -0
  73. package/src/team_agent/profiles/provider_env.py +188 -0
  74. package/src/team_agent/profiles/smoke.py +201 -0
  75. package/src/team_agent/provider_cli/__init__.py +43 -0
  76. package/src/team_agent/provider_cli/adapter.py +167 -0
  77. package/src/team_agent/provider_cli/base.py +48 -0
  78. package/src/team_agent/provider_cli/claude.py +457 -0
  79. package/src/team_agent/provider_cli/codex.py +319 -0
  80. package/src/team_agent/provider_cli/copilot.py +8 -0
  81. package/src/team_agent/provider_cli/fake.py +39 -0
  82. package/src/team_agent/provider_cli/gemini.py +95 -0
  83. package/src/team_agent/provider_cli/opencode.py +8 -0
  84. package/src/team_agent/provider_cli/prompt.py +62 -0
  85. package/src/team_agent/provider_cli/registry.py +18 -0
  86. package/src/team_agent/provider_cli/unsupported.py +32 -0
  87. package/src/team_agent/providers.py +67 -949
  88. package/src/team_agent/quality_gates.py +104 -0
  89. package/src/team_agent/restart/__init__.py +34 -0
  90. package/src/team_agent/restart/orchestration.py +328 -0
  91. package/src/team_agent/restart/selection.py +89 -0
  92. package/src/team_agent/restart/snapshot.py +70 -0
  93. package/src/team_agent/runtime.py +802 -5740
  94. package/src/team_agent/rust_core.py +22 -5
  95. package/src/team_agent/sessions/__init__.py +25 -0
  96. package/src/team_agent/sessions/capture.py +93 -0
  97. package/src/team_agent/sessions/inventory.py +44 -0
  98. package/src/team_agent/sessions/resume.py +135 -0
  99. package/src/team_agent/spec.py +3 -1
  100. package/src/team_agent/state.py +204 -4
  101. package/src/team_agent/status/__init__.py +63 -0
  102. package/src/team_agent/status/approvals.py +52 -0
  103. package/src/team_agent/status/compact.py +158 -0
  104. package/src/team_agent/status/constants.py +18 -0
  105. package/src/team_agent/status/inbox.py +28 -0
  106. package/src/team_agent/status/peek.py +117 -0
  107. package/src/team_agent/status/queries.py +168 -0
  108. package/src/team_agent/terminal.py +57 -0
  109. package/src/team_agent/cli.py +0 -857
  110. package/src/team_agent/mcp_server.py +0 -579
  111. 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