@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,101 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from team_agent.profiles import effective_model, smoke_check_agent_profile, validate_agent_profile
7
+
8
+
9
+ def profile_checks_for_agents(workspace: Path, agents: list[dict[str, Any]]) -> list[dict[str, Any]]:
10
+ return [validate_agent_profile(workspace, agent) for agent in agents if not agent.get("paused")]
11
+
12
+
13
+ def profile_smoke_checks_for_agents(workspace: Path, agents: list[dict[str, Any]]) -> list[dict[str, Any]]:
14
+ return [smoke_check_agent_profile(workspace, agent) for agent in agents if not agent.get("paused")]
15
+
16
+
17
+ def model_checks_for_agents(agents: list[dict[str, Any]], workspace: Path | None = None) -> list[dict[str, Any]]:
18
+ from team_agent.runtime import get_adapter
19
+ checks: list[dict[str, Any]] = []
20
+ for agent in agents:
21
+ if agent.get("paused"):
22
+ continue
23
+ if agent.get("auth_mode") == "compatible_api" and agent.get("provider") == "codex":
24
+ checks.append(
25
+ {
26
+ "ok": True,
27
+ "status": "profile_model_deferred_to_smoke",
28
+ "provider": agent["provider"],
29
+ "model": effective_model(agent, workspace),
30
+ "agent_id": agent["id"],
31
+ }
32
+ )
33
+ continue
34
+ adapter = get_adapter(agent["provider"])
35
+ validator = getattr(adapter, "validate_model", None)
36
+ model = effective_model(agent, workspace)
37
+ if not callable(validator):
38
+ result = {"ok": True, "status": "not_checked", "provider": agent["provider"], "model": model}
39
+ else:
40
+ result = validator(model)
41
+ if not isinstance(result, dict):
42
+ result = {"ok": True, "status": "not_checked", "provider": agent["provider"], "model": model}
43
+ result = dict(result)
44
+ result.setdefault("provider", agent["provider"])
45
+ result.setdefault("model", model)
46
+ result["agent_id"] = agent["id"]
47
+ checks.append(result)
48
+ return checks
49
+
50
+
51
+ def compact_model_checks(checks: list[dict[str, Any]]) -> list[dict[str, Any]]:
52
+ compact: list[dict[str, Any]] = []
53
+ for item in checks:
54
+ compact.append(
55
+ {
56
+ key: item.get(key)
57
+ for key in ["agent_id", "provider", "model", "ok", "status", "reason", "suggested_model", "command"]
58
+ if key in item
59
+ }
60
+ )
61
+ return compact
62
+
63
+
64
+ def format_model_check_failures(failures: list[dict[str, Any]]) -> str:
65
+ lines = ["model validation failed before starting worker windows:"]
66
+ for item in failures:
67
+ message = f"{item.get('agent_id')}: provider={item.get('provider')} model={item.get('model')!r}"
68
+ if item.get("suggested_model"):
69
+ message += f" is not an exact model id; use {item['suggested_model']!r}"
70
+ else:
71
+ message += f" is unsupported ({item.get('reason') or item.get('status')})"
72
+ lines.append(message)
73
+ return "\n".join(lines)
74
+
75
+
76
+ def format_profile_check_failures(failures: list[dict[str, Any]]) -> str:
77
+ lines = ["profile validation failed before starting worker windows:"]
78
+ for item in failures:
79
+ message = f"{item.get('agent_id')}: profile={item.get('profile')!r} auth_mode={item.get('auth_mode')}"
80
+ if item.get("missing_required"):
81
+ message += f" missing {', '.join(item['missing_required'])}"
82
+ else:
83
+ message += f" failed ({item.get('reason') or item.get('status')})"
84
+ if item.get("suggestion"):
85
+ message += f"; {item['suggestion']}"
86
+ lines.append(message)
87
+ return "\n".join(lines)
88
+
89
+
90
+ def format_profile_smoke_failures(failures: list[dict[str, Any]]) -> str:
91
+ lines = ["provider profile smoke check failed before starting worker windows:"]
92
+ for item in failures:
93
+ message = f"{item.get('agent_id')}: provider={item.get('provider')} profile={item.get('profile')!r}"
94
+ message += f" status={item.get('status')} reason={item.get('reason') or 'unknown'}"
95
+ if item.get("http_status"):
96
+ message += f" http_status={item['http_status']}"
97
+ if item.get("error"):
98
+ message += f"; {item['error']}"
99
+ message += "; fix the local profile file or model id, then start again"
100
+ lines.append(message)
101
+ return "\n".join(lines)
@@ -0,0 +1,241 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from team_agent.diagnose.checks import (
7
+ compact_model_checks,
8
+ model_checks_for_agents,
9
+ profile_checks_for_agents,
10
+ )
11
+ from team_agent.events import EventLog
12
+ from team_agent.message_store import MessageStore
13
+ from team_agent.paths import logs_dir, runtime_dir
14
+ from team_agent.profiles import compact_profile_check
15
+ from team_agent.spec import load_spec, workspace_from_spec
16
+ from team_agent.state import load_runtime_state
17
+
18
+
19
+ def diagnose(workspace: Path) -> dict[str, Any]:
20
+ from team_agent.runtime import (
21
+ _capture_has_team_orchestrator_mcp_prompt,
22
+ _leader_receiver_is_direct,
23
+ _tmux_session_exists,
24
+ _tmux_window_exists,
25
+ _validate_leader_receiver,
26
+ get_adapter,
27
+ run_cmd,
28
+ status,
29
+ )
30
+ _ = EventLog # imported for symmetry / future use
31
+ state = load_runtime_state(workspace)
32
+ spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
33
+ spec = load_spec(spec_path) if spec_path.exists() else {}
34
+ store = MessageStore(workspace)
35
+ issues: list[dict[str, Any]] = []
36
+ suggested_repairs: list[dict[str, Any]] = [
37
+ {
38
+ "kind": "mcp_approval_prompt",
39
+ "action": "If a worker pane asks to allow team_orchestrator, select Allow for this session; then run team-agent collect.",
40
+ },
41
+ {
42
+ "kind": "codex_command_approval_prompt",
43
+ "action": "If a worker pane asks to run a shell command, approve only after checking the command; long servers should use pid/log/health-check protocol.",
44
+ },
45
+ {
46
+ "kind": "interrupted_worker",
47
+ "action": "Send: Continue from the current interrupted prompt. Do not redo completed work. Do the next bounded step, then report result_envelope_v1.",
48
+ },
49
+ {
50
+ "kind": "leader_receiver",
51
+ "action": "Worker-to-leader status requires a direct tmux leader receiver. Run team-agent attach-leader --workspace . --provider codex, or pass --pane <pane_id>.",
52
+ },
53
+ {
54
+ "kind": "process_list_unavailable",
55
+ "action": "If pgrep/lsof fail, use pid files, logs, and health-check URLs; record the environment blocker instead of retrying process-list commands.",
56
+ },
57
+ ]
58
+ session_name = state.get("session_name")
59
+ if session_name and not _tmux_session_exists(session_name):
60
+ issues.append(
61
+ {
62
+ "kind": "tmux_session_missing",
63
+ "session": session_name,
64
+ "reason": "tmux has no matching session",
65
+ "suggestion": "Run team-agent launch again or inspect .team/logs/events.jsonl for the shutdown/failure event.",
66
+ }
67
+ )
68
+ leader_receiver = state.get("leader_receiver", {})
69
+ if not _leader_receiver_is_direct(leader_receiver):
70
+ issues.append(
71
+ {
72
+ "kind": "leader_not_attached",
73
+ "mode": leader_receiver.get("mode", "fallback_inbox" if leader_receiver else "none"),
74
+ "suggestion": "Run team-agent attach-leader --workspace . --provider codex, or pass --pane <pane_id> for the existing Codex leader pane.",
75
+ }
76
+ )
77
+ else:
78
+ validation = _validate_leader_receiver(leader_receiver)
79
+ if not validation["ok"]:
80
+ issues.append(
81
+ {
82
+ "kind": validation["reason"],
83
+ "target": leader_receiver.get("pane_id"),
84
+ "provider": leader_receiver.get("provider"),
85
+ "error": validation.get("error"),
86
+ "suggestion": "Run team-agent attach-leader --workspace . --provider codex again with a live Codex pane.",
87
+ }
88
+ )
89
+ elif validation.get("warning"):
90
+ issues.append(
91
+ {
92
+ "kind": "leader_command_unexpected",
93
+ "target": leader_receiver.get("pane_id"),
94
+ "provider": leader_receiver.get("provider"),
95
+ "command": validation.get("pane", {}).get("pane_current_command"),
96
+ "warning": validation["warning"],
97
+ "suggestion": "If this is not the real Codex leader pane, rerun attach-leader with --pane <pane_id>.",
98
+ }
99
+ )
100
+ for agent in spec.get("agents", []):
101
+ adapter = get_adapter(agent["provider"])
102
+ if not adapter.is_installed():
103
+ issues.append(
104
+ {
105
+ "kind": "provider_missing",
106
+ "agent_id": agent["id"],
107
+ "provider": agent["provider"],
108
+ "command": adapter.command_name,
109
+ "suggestion": f"Install {adapter.command_name} and authenticate it before launch.",
110
+ }
111
+ )
112
+ mcp_path = runtime_dir(workspace) / "mcp" / f"{agent['id']}.json"
113
+ if not mcp_path.exists():
114
+ issues.append(
115
+ {
116
+ "kind": "mcp_not_installed",
117
+ "agent_id": agent["id"],
118
+ "provider": agent["provider"],
119
+ "path": str(mcp_path),
120
+ "suggestion": "Run team-agent launch to regenerate provider MCP config.",
121
+ }
122
+ )
123
+ agent_state = state.get("agents", {}).get(agent["id"], {})
124
+ if agent_state.get("status") == "interrupted":
125
+ issues.append(
126
+ {
127
+ "kind": "worker_interrupted",
128
+ "agent_id": agent["id"],
129
+ "suggestion": "Send the standard recovery prompt instead of redispatching the full task.",
130
+ }
131
+ )
132
+ window = agent_state.get("window", agent["id"])
133
+ if session_name and _tmux_window_exists(session_name, window):
134
+ proc = run_cmd(["tmux", "capture-pane", "-p", "-S", "-80", "-t", f"{session_name}:{window}"], timeout=5)
135
+ output = proc.stdout if proc.returncode == 0 else ""
136
+ if _capture_has_team_orchestrator_mcp_prompt(output):
137
+ issues.append(
138
+ {
139
+ "kind": "mcp_approval_prompt",
140
+ "agent_id": agent["id"],
141
+ "suggestion": "Team Agent will auto-approve allowlisted internal MCP prompts; if still blocked, inspect team-agent approvals.",
142
+ }
143
+ )
144
+ if "Would you like to run the following command" in output:
145
+ issues.append(
146
+ {
147
+ "kind": "codex_command_approval_prompt",
148
+ "agent_id": agent["id"],
149
+ "suggestion": "Review and approve or reject the command in the worker pane; do not keep waiting silently.",
150
+ }
151
+ )
152
+ if "Conversation interrupted" in output:
153
+ issues.append(
154
+ {
155
+ "kind": "worker_interrupted",
156
+ "agent_id": agent["id"],
157
+ "suggestion": "Send the standard recovery prompt instead of redispatching the full task.",
158
+ }
159
+ )
160
+ timeout_sec = int(spec.get("communication", {}).get("ack_timeout_sec", 60)) if spec else 60
161
+ failed_messages = store.fail_timeouts(timeout_sec)
162
+ for message_id in failed_messages:
163
+ issues.append(
164
+ {
165
+ "kind": "message_ack_timeout",
166
+ "message_id": message_id,
167
+ "suggestion": "Check target worker status and scrollback; message stayed unacknowledged past timeout.",
168
+ }
169
+ )
170
+ return {
171
+ "ok": not issues,
172
+ "issues": issues,
173
+ "suggested_repairs": suggested_repairs,
174
+ "runtime": status(workspace, as_json=True),
175
+ "event_log": str(logs_dir(workspace) / "events.jsonl"),
176
+ }
177
+
178
+
179
+ def doctor(spec_path: Path | None = None) -> dict[str, Any]:
180
+ from team_agent.runtime import _attach_team_profile_dirs, coordinator_health, get_adapter, shutil_which
181
+ providers = ["codex"]
182
+ spec = None
183
+ workspace = Path.cwd()
184
+ if spec_path:
185
+ spec = load_spec(spec_path)
186
+ workspace = workspace_from_spec(spec, spec_path)
187
+ _attach_team_profile_dirs(spec, spec_path, workspace)
188
+ providers = sorted({a["provider"] for a in spec.get("agents", []) if a["provider"] != "fake"})
189
+ checks: dict[str, Any] = {
190
+ "tmux": {
191
+ "installed": bool(shutil_which("tmux")),
192
+ "path": shutil_which("tmux"),
193
+ },
194
+ "workspace": str(workspace),
195
+ "workspace_is_git_repo": (workspace / ".git").exists(),
196
+ "providers": {},
197
+ "mcp": {
198
+ "server_command": shutil_which("team_orchestrator"),
199
+ "local_module": True,
200
+ },
201
+ "coordinator": coordinator_health(workspace),
202
+ }
203
+ for provider in providers:
204
+ adapter = get_adapter(provider)
205
+ checks["providers"][provider] = {
206
+ "command": adapter.command_name,
207
+ "installed": adapter.is_installed(),
208
+ "version": adapter.version(),
209
+ "auth": adapter.auth_hint(),
210
+ }
211
+ model_checks = model_checks_for_agents(spec.get("agents", []), workspace) if spec else []
212
+ if spec:
213
+ checks["models"] = compact_model_checks(model_checks)
214
+ profile_checks = profile_checks_for_agents(workspace, spec.get("agents", []))
215
+ checks["profiles"] = [compact_profile_check(item) for item in profile_checks]
216
+ missing_required = [
217
+ provider for provider, result in checks["providers"].items() if not result["installed"] and spec_path
218
+ ]
219
+ missing_auth = [
220
+ provider
221
+ for provider, result in checks["providers"].items()
222
+ if spec_path and result.get("auth", {}).get("status") == "missing"
223
+ ]
224
+ invalid_models = [item for item in model_checks if item.get("ok") is False]
225
+ invalid_profiles = [item for item in checks.get("profiles", []) if item.get("ok") is False]
226
+ checks["ok"] = (
227
+ checks["tmux"]["installed"]
228
+ and not missing_required
229
+ and not missing_auth
230
+ and not invalid_models
231
+ and not invalid_profiles
232
+ )
233
+ if missing_required:
234
+ checks["missing_required_providers"] = missing_required
235
+ if missing_auth:
236
+ checks["missing_provider_auth"] = missing_auth
237
+ if invalid_models:
238
+ checks["invalid_models"] = compact_model_checks(invalid_models)
239
+ if invalid_profiles:
240
+ checks["invalid_profiles"] = invalid_profiles
241
+ return checks
@@ -0,0 +1,194 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from team_agent.diagnose.checks import (
9
+ compact_model_checks,
10
+ model_checks_for_agents,
11
+ profile_checks_for_agents,
12
+ profile_smoke_checks_for_agents,
13
+ )
14
+ from team_agent.events import EventLog
15
+ from team_agent.paths import logs_dir, team_workspace
16
+ from team_agent.profiles import compact_profile_check
17
+ from team_agent.rust_core import core_binary
18
+ from team_agent.simple_yaml import dumps
19
+
20
+
21
+ def preflight(team_dir: Path) -> dict[str, Any]:
22
+ from team_agent.compiler import compile_team
23
+ from team_agent.profiles import profile_dir
24
+ from team_agent.runtime import (
25
+ GHOSTTY_DISPLAY_BACKENDS,
26
+ _attach_team_profile_dirs,
27
+ _ghostty_command,
28
+ ensure_workspace_dirs,
29
+ shutil_which,
30
+ )
31
+
32
+ team_dir = team_dir.resolve()
33
+ workspace = team_workspace(team_dir)
34
+ ensure_workspace_dirs(workspace)
35
+ ensure_profiles_for_roles(team_dir)
36
+ event_log = EventLog(workspace)
37
+ checks: list[dict[str, Any]] = []
38
+ ok = True
39
+ spec = None
40
+ try:
41
+ compiled = compile_team(team_dir)
42
+ spec = compiled["spec"]
43
+ _attach_team_profile_dirs(spec, team_dir / "team.spec.yaml", workspace, team_dir)
44
+ checks.append({"name": "compile", "ok": True, "agents": [a["id"] for a in spec.get("agents", [])]})
45
+ except Exception as exc:
46
+ ok = False
47
+ checks.append({"name": "compile", "ok": False, "error": str(exc)})
48
+ tmux_path = shutil_which("tmux")
49
+ checks.append({"name": "tmux", "ok": bool(tmux_path), "path": tmux_path})
50
+ ok = ok and bool(tmux_path)
51
+ ghostty = _ghostty_command()
52
+ ghostty_check = {"name": "ghostty", "ok": bool(ghostty), "path": ghostty, "required": False}
53
+ if spec and spec.get("runtime", {}).get("display_backend") in GHOSTTY_DISPLAY_BACKENDS:
54
+ ghostty_check["required"] = True
55
+ ok = ok and bool(ghostty)
56
+ checks.append(ghostty_check)
57
+ if spec:
58
+ profile_checks = profile_checks_for_agents(workspace, spec.get("agents", []))
59
+ profile_failures = [item for item in profile_checks if item.get("ok") is False]
60
+ checks.append({"name": "profiles", "ok": not profile_failures, "checks": [compact_profile_check(item) for item in profile_checks]})
61
+ ok = ok and not profile_failures
62
+ smoke_checks = profile_smoke_checks_for_agents(workspace, spec.get("agents", []))
63
+ smoke_failures = [item for item in smoke_checks if item.get("ok") is False]
64
+ checks.append({"name": "profile_smoke", "ok": not smoke_failures, "checks": [compact_profile_check(item) for item in smoke_checks]})
65
+ ok = ok and not smoke_failures
66
+ model_checks = model_checks_for_agents(spec.get("agents", []), workspace)
67
+ model_failures = [item for item in model_checks if item.get("ok") is False]
68
+ checks.append({"name": "models", "ok": not model_failures, "checks": compact_model_checks(model_checks)})
69
+ ok = ok and not model_failures
70
+ core = core_binary()
71
+ checks.append(
72
+ {
73
+ "name": "rust_core",
74
+ "ok": True,
75
+ "required": False,
76
+ "available": bool(core),
77
+ "path": str(core) if core else None,
78
+ "status": "available" if core else "python_fallback",
79
+ }
80
+ )
81
+ checks.append({"name": "profile_dir", "ok": profile_dir(workspace).exists() or (team_dir / "profiles").exists()})
82
+ details_log = logs_dir(workspace) / f"preflight-{int(time.time())}.json"
83
+ details = {"team_dir": str(team_dir), "checks": checks}
84
+ details_log.write_text(json.dumps(details, indent=2, ensure_ascii=False), encoding="utf-8")
85
+ event_log.write("preflight.complete", ok=ok, details_log=str(details_log), checks=checks)
86
+ blockers = [] if ok else preflight_blockers(checks)
87
+ return {
88
+ "ok": ok,
89
+ "summary": "preflight passed" if ok else "preflight found blockers: " + "; ".join(blockers[:3]),
90
+ "next_actions": [f"team-agent start --team {team_dir} --yes --json"] if ok else preflight_next_actions(blockers),
91
+ "details_log": str(details_log),
92
+ "checks": checks,
93
+ "blockers": blockers,
94
+ }
95
+
96
+
97
+ def start(team_dir: Path, yes: bool = False) -> dict[str, Any]:
98
+ from team_agent.compiler import compile_team
99
+ from team_agent.runtime import launch
100
+
101
+ team_dir = team_dir.resolve()
102
+ workspace = team_workspace(team_dir)
103
+ spec_path = team_dir / "team.spec.yaml"
104
+ compiled = compile_team(team_dir, spec_path)
105
+ if compiled["spec"].get("context", {}).get("state_file") == "team_state.md":
106
+ state_file = str(team_dir.relative_to(workspace) / "team_state.md") if team_dir.is_relative_to(workspace) else "team_state.md"
107
+ compiled["spec"]["context"]["state_file"] = state_file
108
+ spec_path.write_text(dumps(compiled["spec"]), encoding="utf-8")
109
+ launched = launch(spec_path, auto_approve=yes)
110
+ details_log = logs_dir(workspace) / f"start-{int(time.time())}.json"
111
+ details_log.write_text(json.dumps({"compile": compiled, "launch": launched}, indent=2, ensure_ascii=False), encoding="utf-8")
112
+ return {
113
+ "ok": bool(launched.get("ok")),
114
+ "summary": f"compiled {team_dir} and launched {len(launched.get('agents', []))} agents",
115
+ "next_actions": ["team-agent wait-ready --workspace . --timeout 120 --json"],
116
+ "details_log": str(details_log),
117
+ "spec": str(spec_path),
118
+ "launch": launched,
119
+ }
120
+
121
+
122
+ def preflight_blockers(checks: list[dict[str, Any]]) -> list[str]:
123
+ blockers: list[str] = []
124
+ for check in checks:
125
+ if check.get("ok", True):
126
+ continue
127
+ name = check.get("name") or "check"
128
+ if name == "compile":
129
+ blockers.append(f"compile: {check.get('error')}")
130
+ continue
131
+ for item in check.get("checks", []) or []:
132
+ agent = item.get("agent_id") or item.get("profile") or "-"
133
+ reason = item.get("reason") or item.get("status") or "failed"
134
+ detail = f"{name}: {agent} {reason}"
135
+ if item.get("endpoint"):
136
+ detail += f" endpoint={item['endpoint']}"
137
+ if item.get("proxy_configured"):
138
+ detail += f" proxy={item.get('proxy_url') or item.get('proxy_scheme')}"
139
+ if item.get("proxy_source"):
140
+ detail += f" proxy_source={item['proxy_source']}"
141
+ if item.get("proxy_mode"):
142
+ detail += f" proxy_mode={item['proxy_mode']}"
143
+ if item.get("missing_required"):
144
+ detail += " missing=" + ",".join(item["missing_required"])
145
+ if item.get("effective_model"):
146
+ detail += f" model={item['effective_model']}"
147
+ if item.get("suggestion"):
148
+ detail += f" suggestion={item['suggestion']}"
149
+ blockers.append(detail)
150
+ if not check.get("checks"):
151
+ blockers.append(f"{name}: failed")
152
+ return blockers or ["unknown preflight blocker"]
153
+
154
+
155
+ def preflight_next_actions(blockers: list[str]) -> list[str]:
156
+ actions = ["Fix failed checks, then rerun preflight."]
157
+ if any("proxy_connectivity_failed" in item for item in blockers):
158
+ actions.insert(0, "Allow the profile BASE_URL through the configured proxy, or disable the proxy for Team Agent startup.")
159
+ if any("proxy_source=ambient" in item for item in blockers):
160
+ actions.insert(0, "Current environment proxy is being used for this compatible_api worker; either fix that proxy for BASE_URL, set HTTPS_PROXY/HTTP_PROXY in the profile, or set PROXY_MODE=direct in the profile to bypass proxy for this worker.")
161
+ if any("missing=" in item or "profile_required_values_missing" in item for item in blockers):
162
+ actions.insert(
163
+ 0,
164
+ "Ask the human user to fill the local profile file; agents must inspect only with `team-agent profile show <name> --workspace . --json` or the returned --team variant and must not read .team/*/profiles/*.env.",
165
+ )
166
+ if any("model_mismatch" in item or "does not match profile MODEL" in item for item in blockers):
167
+ actions.insert(0, "Keep the model in the profile MODEL field or make the role model exactly match it.")
168
+ return actions
169
+
170
+
171
+ def ensure_profiles_for_roles(team_dir: Path) -> None:
172
+ from team_agent.compiler import _read_front_matter
173
+ from team_agent.profiles import ensure_profile_secret_boundary, ensure_profile_secret_boundary_dir, init_profile
174
+
175
+ workspace = team_workspace(team_dir)
176
+ profiles_dir = team_dir / "profiles"
177
+ profiles_dir.mkdir(parents=True, exist_ok=True)
178
+ ensure_profile_secret_boundary(workspace)
179
+ ensure_profile_secret_boundary_dir(profiles_dir)
180
+ for role_doc in sorted((team_dir / "agents").glob("*.md")):
181
+ meta, _ = _read_front_matter(role_doc)
182
+ profile = meta.get("profile")
183
+ auth_mode = meta.get("auth_mode") or "subscription"
184
+ if not profile:
185
+ continue
186
+ if not (profiles_dir / f"{profile}.env").exists() and not (profiles_dir / f"{profile}.example.env").exists():
187
+ init_profile(workspace, str(profile), str(auth_mode))
188
+ if auth_mode == "subscription":
189
+ body = f"AUTH_MODE=subscription\nPROFILE_NAME={profile}\n"
190
+ elif auth_mode == "official_api":
191
+ body = f"AUTH_MODE=official_api\nPROFILE_NAME={profile}\nAPI_KEY=\nMODEL=\n"
192
+ else:
193
+ body = f"AUTH_MODE={auth_mode}\nPROFILE_NAME={profile}\nBASE_URL=\nAPI_KEY=\nMODEL=\n"
194
+ (profiles_dir / f"{profile}.example.env").write_text(body, encoding="utf-8")