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