@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.
- package/crates/team-agent-core/src/lib.rs +50 -5
- package/package.json +1 -1
- package/schemas/team.schema.json +1 -0
- package/skills/team-agent/SKILL.md +1 -1
- package/src/team_agent/approvals/__init__.py +65 -0
- package/src/team_agent/approvals/constants.py +6 -0
- package/src/team_agent/approvals/parsing.py +176 -0
- package/src/team_agent/approvals/runtime_prompts.py +171 -0
- package/src/team_agent/approvals/status.py +165 -0
- package/src/team_agent/cli/__init__.py +135 -0
- package/src/team_agent/cli/commands.py +335 -0
- package/src/team_agent/cli/e2e.py +202 -0
- package/src/team_agent/cli/helpers.py +137 -0
- package/src/team_agent/cli/parser.py +470 -0
- package/src/team_agent/compiler.py +98 -33
- package/src/team_agent/coordinator/__init__.py +53 -0
- package/src/team_agent/{coordinator.py → coordinator/__main__.py} +3 -1
- package/src/team_agent/coordinator/lifecycle.py +319 -0
- package/src/team_agent/coordinator/metadata.py +61 -0
- package/src/team_agent/coordinator/paths.py +17 -0
- package/src/team_agent/diagnose/__init__.py +48 -0
- package/src/team_agent/diagnose/checks.py +101 -0
- package/src/team_agent/diagnose/health.py +241 -0
- package/src/team_agent/diagnose/preflight.py +194 -0
- package/src/team_agent/diagnose/quick_start.py +233 -0
- package/src/team_agent/display/__init__.py +61 -0
- package/src/team_agent/display/close.py +147 -0
- package/src/team_agent/display/ghostty.py +77 -0
- package/src/team_agent/display/worker_window.py +110 -0
- package/src/team_agent/display/workspace.py +473 -0
- package/src/team_agent/launch/__init__.py +41 -0
- package/src/team_agent/launch/bootstrap.py +85 -0
- package/src/team_agent/launch/config.py +106 -0
- package/src/team_agent/launch/core.py +291 -0
- package/src/team_agent/launch/requirements.py +57 -0
- package/src/team_agent/leader/__init__.py +320 -0
- package/src/team_agent/lifecycle/__init__.py +5 -0
- package/src/team_agent/lifecycle/agents.py +226 -0
- package/src/team_agent/lifecycle/operations.py +321 -0
- package/src/team_agent/lifecycle/start.py +360 -0
- package/src/team_agent/mcp_server/__init__.py +42 -0
- package/src/team_agent/mcp_server/__main__.py +7 -0
- package/src/team_agent/mcp_server/contracts.py +148 -0
- package/src/team_agent/mcp_server/normalize.py +257 -0
- package/src/team_agent/mcp_server/server.py +150 -0
- package/src/team_agent/mcp_server/tools.py +205 -0
- package/src/team_agent/message_store/__init__.py +23 -0
- package/src/team_agent/message_store/agent_health.py +109 -0
- package/src/team_agent/{message_store.py → message_store/core.py} +188 -245
- package/src/team_agent/message_store/result_watchers.py +102 -0
- package/src/team_agent/message_store/schema.py +266 -0
- package/src/team_agent/messaging/__init__.py +1 -0
- package/src/team_agent/messaging/activity_detector.py +190 -0
- package/src/team_agent/messaging/delivery.py +128 -0
- package/src/team_agent/messaging/deps.py +263 -0
- package/src/team_agent/messaging/idle_alerts.py +217 -0
- package/src/team_agent/messaging/internal_delivery.py +46 -0
- package/src/team_agent/messaging/leader.py +317 -0
- package/src/team_agent/messaging/leader_panes.py +343 -0
- package/src/team_agent/messaging/result_delivery.py +300 -0
- package/src/team_agent/messaging/results.py +456 -0
- package/src/team_agent/messaging/scheduler.py +418 -0
- package/src/team_agent/messaging/send.py +493 -0
- package/src/team_agent/messaging/tmux_io.py +337 -0
- package/src/team_agent/messaging/tmux_prompt.py +229 -0
- package/src/team_agent/orchestrator/__init__.py +376 -0
- package/src/team_agent/orchestrator/plan.py +122 -0
- package/src/team_agent/orchestrator/state.py +128 -0
- package/src/team_agent/profiles/__init__.py +82 -0
- package/src/team_agent/profiles/constants.py +19 -0
- package/src/team_agent/profiles/core.py +407 -0
- package/src/team_agent/profiles/helpers.py +69 -0
- package/src/team_agent/profiles/provider_env.py +188 -0
- package/src/team_agent/profiles/smoke.py +201 -0
- package/src/team_agent/provider_cli/__init__.py +43 -0
- package/src/team_agent/provider_cli/adapter.py +167 -0
- package/src/team_agent/provider_cli/base.py +48 -0
- package/src/team_agent/provider_cli/claude.py +457 -0
- package/src/team_agent/provider_cli/codex.py +319 -0
- package/src/team_agent/provider_cli/copilot.py +8 -0
- package/src/team_agent/provider_cli/fake.py +39 -0
- package/src/team_agent/provider_cli/gemini.py +95 -0
- package/src/team_agent/provider_cli/opencode.py +8 -0
- package/src/team_agent/provider_cli/prompt.py +62 -0
- package/src/team_agent/provider_cli/registry.py +18 -0
- package/src/team_agent/provider_cli/unsupported.py +32 -0
- package/src/team_agent/providers.py +67 -949
- package/src/team_agent/quality_gates.py +104 -0
- package/src/team_agent/restart/__init__.py +34 -0
- package/src/team_agent/restart/orchestration.py +328 -0
- package/src/team_agent/restart/selection.py +89 -0
- package/src/team_agent/restart/snapshot.py +70 -0
- package/src/team_agent/runtime.py +802 -5740
- package/src/team_agent/rust_core.py +22 -5
- package/src/team_agent/sessions/__init__.py +25 -0
- package/src/team_agent/sessions/capture.py +93 -0
- package/src/team_agent/sessions/inventory.py +44 -0
- package/src/team_agent/sessions/resume.py +135 -0
- package/src/team_agent/spec.py +3 -1
- package/src/team_agent/state.py +204 -4
- package/src/team_agent/status/__init__.py +63 -0
- package/src/team_agent/status/approvals.py +52 -0
- package/src/team_agent/status/compact.py +158 -0
- package/src/team_agent/status/constants.py +18 -0
- package/src/team_agent/status/inbox.py +28 -0
- package/src/team_agent/status/peek.py +117 -0
- package/src/team_agent/status/queries.py +168 -0
- package/src/team_agent/terminal.py +57 -0
- package/src/team_agent/cli.py +0 -857
- package/src/team_agent/mcp_server.py +0 -579
- 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)}
|