@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,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")
@@ -0,0 +1,233 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ import time
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from team_agent.diagnose.preflight import ensure_profiles_for_roles, preflight
10
+ from team_agent.events import EventLog
11
+ from team_agent.message_store import MessageStore
12
+ from team_agent.paths import logs_dir, team_workspace
13
+ from team_agent.spec import load_spec
14
+ from team_agent.state import load_runtime_state, save_runtime_state, write_team_state
15
+ from team_agent.task_graph import TASK_STATUSES
16
+
17
+
18
+ def quick_start(
19
+ agents_dir: Path,
20
+ name: str | None = None,
21
+ yes: bool = False,
22
+ fresh: bool = False,
23
+ team_id: str | None = None,
24
+ ) -> dict[str, Any]:
25
+ from team_agent.runtime import (
26
+ RuntimeError,
27
+ _compile_team_dir_spec,
28
+ _quick_start_existing_context,
29
+ ensure_workspace_dirs,
30
+ launch,
31
+ start_coordinator,
32
+ )
33
+
34
+ team_dir = prepare_quick_start_team(agents_dir.resolve(), Path.cwd().resolve(), name, team_id=team_id)
35
+ workspace = team_workspace(team_dir)
36
+ ensure_workspace_dirs(workspace)
37
+ ensure_profiles_for_roles(team_dir)
38
+ compiled = _compile_team_dir_spec(team_dir, workspace)
39
+ spec_path = team_dir / "team.spec.yaml"
40
+ existing = _quick_start_existing_context(workspace, compiled["spec"]["runtime"]["session_name"])
41
+ if existing and not fresh:
42
+ return {
43
+ "ok": False,
44
+ "step": "existing_runtime_state",
45
+ "summary": (
46
+ "quick-start would start fresh workers from role docs for an existing team. "
47
+ "Use restart to continue the previous worker context, or pass --fresh to intentionally start new workers."
48
+ ),
49
+ "team": existing.get("team_name"),
50
+ "session_name": existing.get("session_name"),
51
+ "state_path": existing.get("state_path"),
52
+ "next_actions": [
53
+ f"team-agent restart {workspace} --team {existing.get('session_name')}",
54
+ f"team-agent quick-start {team_dir} --fresh",
55
+ ],
56
+ }
57
+ preflight_result = preflight(team_dir)
58
+ if not preflight_result.get("ok"):
59
+ return {
60
+ "ok": False,
61
+ "step": "preflight",
62
+ "summary": preflight_result.get("summary"),
63
+ "details_log": preflight_result.get("details_log"),
64
+ "blockers": preflight_result.get("blockers", []),
65
+ "next_actions": preflight_result.get("next_actions", []),
66
+ "checks": preflight_result.get("checks", []),
67
+ }
68
+ dangerous = bool(compiled["spec"].get("runtime", {}).get("dangerous_auto_approve"))
69
+ if dangerous and not yes:
70
+ raise RuntimeError("quick-start requires --yes when dangerous_auto_approve is true")
71
+ launched = launch(spec_path, auto_approve=True, skip_profile_smoke=True)
72
+ from team_agent.leader import autobind_leader_receiver_from_env
73
+ leader_provider = str(compiled["spec"].get("leader", {}).get("provider") or "codex")
74
+ autobind_leader_receiver_from_env(workspace, leader_provider, source="quick_start")
75
+ coordinator = start_coordinator(workspace)
76
+ ready = wait_ready(workspace, timeout=120)
77
+ summary = (
78
+ f"team {compiled['spec']['team']['name']} ready: "
79
+ f"{len(launched.get('agents', []))} agent"
80
+ f"{'' if len(launched.get('agents', [])) == 1 else 's'} "
81
+ f"in session {launched.get('session_name')} (coordinator pid {coordinator.get('pid')})"
82
+ )
83
+ ready_signal = (
84
+ "quick-start completed; workers are ready. "
85
+ "Do not wait, sleep, or poll status after this success line unless diagnosing a failure."
86
+ )
87
+ details_log = logs_dir(workspace) / f"quick-start-{int(time.time())}.json"
88
+ details_log.write_text(
89
+ json.dumps(
90
+ {
91
+ "team_dir": str(team_dir),
92
+ "preflight": preflight_result,
93
+ "compile": compiled,
94
+ "launch": launched,
95
+ "ready": ready,
96
+ "coordinator": coordinator,
97
+ },
98
+ indent=2,
99
+ ensure_ascii=False,
100
+ ),
101
+ encoding="utf-8",
102
+ )
103
+ return {
104
+ "ok": bool(launched.get("ok") and ready.get("ok") and coordinator.get("ok")),
105
+ "summary": summary,
106
+ "ready_signal": ready_signal,
107
+ "next_actions": ["Dispatch work with team-agent send, or return control to the user."],
108
+ "team_dir": str(team_dir),
109
+ "spec": str(spec_path),
110
+ "session_name": launched.get("session_name"),
111
+ "coordinator": coordinator,
112
+ "details_log": str(details_log),
113
+ }
114
+
115
+
116
+ def prepare_quick_start_team(agents_dir: Path, workspace: Path, name: str | None, team_id: str | None = None) -> Path:
117
+ from team_agent.runtime import RuntimeError, _safe_snapshot_name
118
+
119
+ if (agents_dir / "TEAM.md").exists() and (agents_dir / "agents").is_dir():
120
+ return agents_dir
121
+ team_source = agents_dir / "TEAM.md"
122
+ role_docs = [path for path in sorted(agents_dir.glob("*.md")) if path.name != "TEAM.md"] if agents_dir.is_dir() else []
123
+ if not role_docs:
124
+ raise RuntimeError(f"{agents_dir}: expected .team/current or a directory of role .md files")
125
+ team_dir = workspace / ".team" / (_safe_snapshot_name(team_id) if team_id else "current")
126
+ target_agents = team_dir / "agents"
127
+ target_profiles = team_dir / "profiles"
128
+ target_agents.mkdir(parents=True, exist_ok=True)
129
+ target_profiles.mkdir(parents=True, exist_ok=True)
130
+ for role_doc in role_docs:
131
+ shutil.copy2(role_doc, target_agents / role_doc.name)
132
+ team_doc = team_dir / "TEAM.md"
133
+ if team_source.exists():
134
+ shutil.copy2(team_source, team_doc)
135
+ if name:
136
+ EventLog(workspace).write("quick_start.name_ignored_existing_team_doc", name=name, team_doc=str(team_doc))
137
+ elif not team_doc.exists():
138
+ team_name = name or agents_dir.name.replace(" ", "-") or "team-agent-team"
139
+ team_doc.write_text(
140
+ f"---\nname: {team_name}\nobjective: Quick-start Team Agent team.\n---\n\nQuick-start team.\n",
141
+ encoding="utf-8",
142
+ )
143
+ elif name:
144
+ # Keep the existing body; name override is only for fresh TEAM.md to avoid hand-editing user docs.
145
+ EventLog(workspace).write("quick_start.name_ignored_existing_team_doc", name=name, team_doc=str(team_doc))
146
+ return team_dir
147
+
148
+
149
+ def wait_ready(workspace: Path, timeout: int = 120) -> dict[str, Any]:
150
+ from team_agent.runtime import status
151
+
152
+ start_time = time.monotonic()
153
+ last: dict[str, Any] = {}
154
+ while time.monotonic() - start_time <= timeout:
155
+ last = status(workspace, as_json=True)
156
+ agents = last.get("agents", {})
157
+ if agents and all(agent.get("tmux_window_present") and agent.get("status") in {"running", "busy"} for agent in agents.values()):
158
+ break
159
+ time.sleep(1.0)
160
+ readiness = {
161
+ "process_started": bool(last.get("tmux_session_present")),
162
+ "cli_prompt_ready": all(agent.get("status") in {"running", "busy"} for agent in last.get("agents", {}).values()) if last.get("agents") else False,
163
+ "mcp_ready": all(Path(agent.get("mcp_config", "")).exists() for agent in last.get("agents", {}).values()) if last.get("agents") else False,
164
+ "task_prompt_delivered": bool(MessageStore(workspace).message_counts()),
165
+ }
166
+ ok = readiness["process_started"] and readiness["cli_prompt_ready"] and readiness["mcp_ready"]
167
+ details_log = logs_dir(workspace) / f"wait-ready-{int(time.time())}.json"
168
+ details_log.write_text(json.dumps({"readiness": readiness, "status": last}, indent=2, ensure_ascii=False), encoding="utf-8")
169
+ return {
170
+ "ok": ok,
171
+ "summary": "workers ready" if ok else "workers not fully ready before timeout",
172
+ "next_actions": ["Dispatch a task with team-agent send."] if ok else ["Run team-agent diagnose --json."],
173
+ "details_log": str(details_log),
174
+ "readiness": readiness,
175
+ }
176
+
177
+
178
+ def settle(workspace: Path) -> dict[str, Any]:
179
+ from team_agent.runtime import collect, status
180
+
181
+ collected = collect(workspace)
182
+ current = status(workspace, as_json=True)
183
+ details_log = logs_dir(workspace) / f"settle-{int(time.time())}.json"
184
+ details_log.write_text(json.dumps({"collect": collected, "status": current}, indent=2, ensure_ascii=False), encoding="utf-8")
185
+ return {
186
+ "ok": collected.get("ok", False),
187
+ "summary": f"collected {len(collected.get('collected', []))} result(s)",
188
+ "next_actions": ["Review team_state.md and decide whether to continue or shutdown."],
189
+ "details_log": str(details_log),
190
+ "collect": collected,
191
+ }
192
+
193
+
194
+ def repair_state(
195
+ workspace: Path,
196
+ task_id: str,
197
+ assignee: str | None = None,
198
+ status_value: str | None = None,
199
+ summary: str | None = None,
200
+ ) -> dict[str, Any]:
201
+ from team_agent.runtime import RuntimeError, _find_task, _leader_id
202
+
203
+ state = load_runtime_state(workspace)
204
+ spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
205
+ spec = load_spec(spec_path)
206
+ task = _find_task(state.get("tasks", []), task_id)
207
+ if assignee is not None:
208
+ valid_agents = {agent["id"] for agent in spec.get("agents", [])}
209
+ valid_agents.add(_leader_id(state, spec))
210
+ if assignee not in valid_agents:
211
+ raise RuntimeError(f"unknown agent id for repair: {assignee}")
212
+ if status_value is not None and status_value not in TASK_STATUSES:
213
+ raise RuntimeError(f"unknown task status for repair: {status_value}")
214
+ before = {
215
+ "assignee": task.get("assignee"),
216
+ "status": task.get("status"),
217
+ "last_result_summary": task.get("last_result_summary"),
218
+ }
219
+ if assignee is not None:
220
+ task["assignee"] = assignee
221
+ if status_value is not None:
222
+ task["status"] = status_value
223
+ if summary is not None:
224
+ task["last_result_summary"] = summary
225
+ after = {
226
+ "assignee": task.get("assignee"),
227
+ "status": task.get("status"),
228
+ "last_result_summary": task.get("last_result_summary"),
229
+ }
230
+ save_runtime_state(workspace, state)
231
+ state_path = write_team_state(workspace, spec, state)
232
+ EventLog(workspace).write("repair_state.task", task_id=task_id, before=before, after=after)
233
+ return {"ok": True, "task_id": task_id, "before": before, "after": after, "state_file": str(state_path)}