@team-agent/installer 0.1.11 → 0.2.1
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/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 +137 -0
- package/src/team_agent/cli/commands.py +339 -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 +477 -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 +334 -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/paste_buffer_hygiene.py +39 -0
- package/src/team_agent/lifecycle/start.py +363 -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 +138 -0
- package/src/team_agent/messaging/deps.py +263 -0
- package/src/team_agent/messaging/idle_alerts.py +323 -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/owner_bypass.py +29 -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 +428 -0
- package/src/team_agent/messaging/send.py +500 -0
- package/src/team_agent/messaging/session_drift.py +94 -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 +809 -5892
- 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 +218 -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 -858
- package/src/team_agent/mcp_server.py +0 -579
- package/src/team_agent/profiles.py +0 -882
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from team_agent.diagnose.checks import (
|
|
4
|
+
compact_model_checks,
|
|
5
|
+
format_model_check_failures,
|
|
6
|
+
format_profile_check_failures,
|
|
7
|
+
format_profile_smoke_failures,
|
|
8
|
+
model_checks_for_agents,
|
|
9
|
+
profile_checks_for_agents,
|
|
10
|
+
profile_smoke_checks_for_agents,
|
|
11
|
+
)
|
|
12
|
+
from team_agent.diagnose.health import diagnose, doctor
|
|
13
|
+
from team_agent.diagnose.preflight import (
|
|
14
|
+
ensure_profiles_for_roles,
|
|
15
|
+
preflight,
|
|
16
|
+
preflight_blockers,
|
|
17
|
+
preflight_next_actions,
|
|
18
|
+
start,
|
|
19
|
+
)
|
|
20
|
+
from team_agent.diagnose.quick_start import (
|
|
21
|
+
prepare_quick_start_team,
|
|
22
|
+
quick_start,
|
|
23
|
+
repair_state,
|
|
24
|
+
settle,
|
|
25
|
+
wait_ready,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"compact_model_checks",
|
|
30
|
+
"diagnose",
|
|
31
|
+
"doctor",
|
|
32
|
+
"ensure_profiles_for_roles",
|
|
33
|
+
"format_model_check_failures",
|
|
34
|
+
"format_profile_check_failures",
|
|
35
|
+
"format_profile_smoke_failures",
|
|
36
|
+
"model_checks_for_agents",
|
|
37
|
+
"prepare_quick_start_team",
|
|
38
|
+
"preflight",
|
|
39
|
+
"preflight_blockers",
|
|
40
|
+
"preflight_next_actions",
|
|
41
|
+
"profile_checks_for_agents",
|
|
42
|
+
"profile_smoke_checks_for_agents",
|
|
43
|
+
"quick_start",
|
|
44
|
+
"repair_state",
|
|
45
|
+
"settle",
|
|
46
|
+
"start",
|
|
47
|
+
"wait_ready",
|
|
48
|
+
]
|
|
@@ -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")
|