@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,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from team_agent.approvals.parsing import capture_has_approval_prompt
|
|
10
|
+
from team_agent.events import EventLog
|
|
11
|
+
from team_agent.message_store import MessageStore
|
|
12
|
+
from team_agent.state import team_state_key
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def refresh_agent_runtime_statuses(workspace: Path, state: dict[str, Any], event_log: EventLog) -> None:
|
|
16
|
+
from team_agent.runtime import _tmux_session_exists, _tmux_window_exists
|
|
17
|
+
_ = workspace
|
|
18
|
+
session_name = state.get("session_name")
|
|
19
|
+
tmux_exists = _tmux_session_exists(session_name) if session_name else False
|
|
20
|
+
for agent_id, agent_state in state.get("agents", {}).items():
|
|
21
|
+
if agent_state.get("status") in {"paused", "stopped"}:
|
|
22
|
+
continue
|
|
23
|
+
old_status = agent_state.get("status")
|
|
24
|
+
window = agent_state.get("window", agent_id)
|
|
25
|
+
window_present = _tmux_window_exists(session_name, window) if tmux_exists else False
|
|
26
|
+
agent_state["tmux_window_present"] = window_present
|
|
27
|
+
if not window_present:
|
|
28
|
+
if session_name:
|
|
29
|
+
agent_state["status"] = "missing"
|
|
30
|
+
else:
|
|
31
|
+
detected = detect_provider_status(agent_state["provider"], session_name, window)
|
|
32
|
+
if detected:
|
|
33
|
+
agent_state["status"] = detected
|
|
34
|
+
else:
|
|
35
|
+
agent_state.setdefault("status", "running")
|
|
36
|
+
if old_status != agent_state.get("status"):
|
|
37
|
+
event_log.write(
|
|
38
|
+
"runtime.status_detected",
|
|
39
|
+
agent_id=agent_id,
|
|
40
|
+
provider=agent_state.get("provider"),
|
|
41
|
+
old_status=old_status,
|
|
42
|
+
status=agent_state.get("status"),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def sync_agent_health(workspace: Path, state: dict[str, Any], store: MessageStore | None = None) -> dict[str, dict[str, Any]]:
|
|
47
|
+
from team_agent.runtime import _tmux_window_exists, _tmux_pane_info, run_cmd
|
|
48
|
+
from team_agent.messaging.activity_detector import classify_agent_activity
|
|
49
|
+
store = store or MessageStore(workspace)
|
|
50
|
+
session_name = state.get("session_name")
|
|
51
|
+
owner_team_id = team_state_key(state)
|
|
52
|
+
captures: dict[str, dict[str, Any]] = {}
|
|
53
|
+
for agent_id, agent_state in state.get("agents", {}).items():
|
|
54
|
+
health_status = agent_health_status(agent_state)
|
|
55
|
+
last_output_at = agent_state.get("last_output_at")
|
|
56
|
+
window = agent_state.get("window", agent_id)
|
|
57
|
+
scrollback = ""
|
|
58
|
+
pane_info: dict[str, Any] | None = None
|
|
59
|
+
if session_name and _tmux_window_exists(session_name, window):
|
|
60
|
+
proc = run_cmd(["tmux", "capture-pane", "-p", "-S", "-40", "-t", f"{session_name}:{window}"], timeout=5)
|
|
61
|
+
if proc.returncode == 0:
|
|
62
|
+
scrollback = proc.stdout
|
|
63
|
+
digest = hashlib.sha256(proc.stdout.encode("utf-8", errors="ignore")).hexdigest()
|
|
64
|
+
if digest != agent_state.get("last_output_hash"):
|
|
65
|
+
last_output_at = datetime.now(timezone.utc).isoformat()
|
|
66
|
+
agent_state["last_output_hash"] = digest
|
|
67
|
+
agent_state["last_output_at"] = last_output_at
|
|
68
|
+
if capture_has_approval_prompt(proc.stdout):
|
|
69
|
+
health_status = "AWAITING_APPROVAL"
|
|
70
|
+
try:
|
|
71
|
+
pane_info = _tmux_pane_info(f"{session_name}:{window}")
|
|
72
|
+
except Exception:
|
|
73
|
+
pane_info = None
|
|
74
|
+
if scrollback and health_status != "AWAITING_APPROVAL":
|
|
75
|
+
activity = classify_agent_activity(
|
|
76
|
+
agent_id,
|
|
77
|
+
agent_state.get("provider") or "",
|
|
78
|
+
last_output_at,
|
|
79
|
+
pane_info,
|
|
80
|
+
scrollback,
|
|
81
|
+
)
|
|
82
|
+
agent_state["activity"] = {
|
|
83
|
+
"status": activity.get("status"),
|
|
84
|
+
"confidence": activity.get("confidence"),
|
|
85
|
+
"rationale": activity.get("rationale"),
|
|
86
|
+
"classified_at": datetime.now(timezone.utc).isoformat(),
|
|
87
|
+
}
|
|
88
|
+
if activity.get("confidence", 0) >= 0.85:
|
|
89
|
+
raw = str(activity.get("status") or "")
|
|
90
|
+
mapping = {"idle": "IDLE", "working": "WORKING", "stuck": "STUCK", "uncertain": "UNCERTAIN"}
|
|
91
|
+
mapped = mapping.get(raw)
|
|
92
|
+
if mapped:
|
|
93
|
+
health_status = mapped
|
|
94
|
+
current_task = current_task_for_agent(state.get("tasks", []), agent_id)
|
|
95
|
+
store.upsert_agent_health(
|
|
96
|
+
agent_id,
|
|
97
|
+
health_status,
|
|
98
|
+
last_output_at=last_output_at,
|
|
99
|
+
context_usage_pct=agent_state.get("context_usage_pct"),
|
|
100
|
+
current_task_id=current_task,
|
|
101
|
+
owner_team_id=owner_team_id,
|
|
102
|
+
)
|
|
103
|
+
captures[agent_id] = {"scrollback": scrollback, "pane_info": pane_info, "health_status": health_status}
|
|
104
|
+
return captures
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def agent_health_status(agent_state: dict[str, Any]) -> str:
|
|
108
|
+
raw = str(agent_state.get("status") or "").lower()
|
|
109
|
+
if raw in {"busy", "running"}:
|
|
110
|
+
return "RUNNING" if raw == "busy" else "IDLE"
|
|
111
|
+
if raw in {"paused", "blocked"}:
|
|
112
|
+
return "BLOCKED"
|
|
113
|
+
if raw in {"error", "missing", "interrupted"}:
|
|
114
|
+
return "ERROR"
|
|
115
|
+
if raw in {"stopped", "done"}:
|
|
116
|
+
return "DONE"
|
|
117
|
+
return "IDLE"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def current_task_for_agent(tasks: list[dict[str, Any]], agent_id: str) -> str | None:
|
|
121
|
+
active = {"pending", "ready", "running", "blocked", "needs_retry"}
|
|
122
|
+
for task in reversed(tasks):
|
|
123
|
+
if task.get("assignee") == agent_id and task.get("status", "pending") in active:
|
|
124
|
+
return task.get("id")
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def age_text(iso_text: str | None) -> str:
|
|
129
|
+
if not iso_text:
|
|
130
|
+
return "-"
|
|
131
|
+
try:
|
|
132
|
+
dt = datetime.fromisoformat(iso_text)
|
|
133
|
+
if dt.tzinfo is None:
|
|
134
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
135
|
+
seconds = max(0, int((datetime.now(timezone.utc) - dt).total_seconds()))
|
|
136
|
+
except ValueError:
|
|
137
|
+
return "-"
|
|
138
|
+
if seconds < 60:
|
|
139
|
+
return f"{seconds}s ago"
|
|
140
|
+
minutes = seconds // 60
|
|
141
|
+
if minutes < 60:
|
|
142
|
+
return f"{minutes}m ago"
|
|
143
|
+
return f"{minutes // 60}h ago"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def detect_provider_status(provider: str, session_name: str, window: str) -> str | None:
|
|
147
|
+
from team_agent.runtime import get_adapter, run_cmd
|
|
148
|
+
proc = run_cmd(["tmux", "capture-pane", "-p", "-t", f"{session_name}:{window}"], timeout=5)
|
|
149
|
+
if proc.returncode != 0:
|
|
150
|
+
return None
|
|
151
|
+
patterns = get_adapter(provider).status_patterns()
|
|
152
|
+
positions: dict[str, int] = {}
|
|
153
|
+
for status_name, pattern in patterns.items():
|
|
154
|
+
if not pattern:
|
|
155
|
+
continue
|
|
156
|
+
try:
|
|
157
|
+
matches = list(re.finditer(pattern, proc.stdout, re.MULTILINE))
|
|
158
|
+
except re.error:
|
|
159
|
+
continue
|
|
160
|
+
if matches:
|
|
161
|
+
positions[status_name] = matches[-1].start()
|
|
162
|
+
if not positions:
|
|
163
|
+
return None
|
|
164
|
+
latest = max(positions, key=positions.get)
|
|
165
|
+
return {"idle": "running", "processing": "busy", "error": "error"}.get(latest)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from team_agent.cli.parser import (
|
|
4
|
+
SEND_ORDER_HINT,
|
|
5
|
+
TeamAgentArgumentParser,
|
|
6
|
+
_run_leader_passthrough,
|
|
7
|
+
add_json,
|
|
8
|
+
main,
|
|
9
|
+
)
|
|
10
|
+
from team_agent.cli.helpers import (
|
|
11
|
+
_cli_error_payload,
|
|
12
|
+
_emit_cli_error,
|
|
13
|
+
_leader_launcher_args,
|
|
14
|
+
_provider_args,
|
|
15
|
+
_tmux_session_conflict_action,
|
|
16
|
+
_tmux_session_conflict_name,
|
|
17
|
+
_tmux_session_conflict_next_action,
|
|
18
|
+
_workspace_from_args,
|
|
19
|
+
emit,
|
|
20
|
+
)
|
|
21
|
+
from team_agent.cli.commands import (
|
|
22
|
+
cmd_quick_start,
|
|
23
|
+
cmd_codex,
|
|
24
|
+
cmd_claude,
|
|
25
|
+
cmd_init,
|
|
26
|
+
cmd_validate,
|
|
27
|
+
cmd_compile,
|
|
28
|
+
_profile_scope,
|
|
29
|
+
cmd_profile_init,
|
|
30
|
+
cmd_profile_doctor,
|
|
31
|
+
cmd_profile_show,
|
|
32
|
+
cmd_launch,
|
|
33
|
+
cmd_preflight,
|
|
34
|
+
cmd_start,
|
|
35
|
+
cmd_wait_ready,
|
|
36
|
+
cmd_settle,
|
|
37
|
+
cmd_status,
|
|
38
|
+
cmd_approvals,
|
|
39
|
+
cmd_peek,
|
|
40
|
+
cmd_inbox,
|
|
41
|
+
cmd_sessions,
|
|
42
|
+
cmd_attach_leader,
|
|
43
|
+
cmd_send,
|
|
44
|
+
cmd_collect,
|
|
45
|
+
cmd_diagnose,
|
|
46
|
+
cmd_repair_state,
|
|
47
|
+
cmd_validate_result,
|
|
48
|
+
cmd_doctor,
|
|
49
|
+
cmd_shutdown,
|
|
50
|
+
cmd_restart,
|
|
51
|
+
cmd_start_agent,
|
|
52
|
+
cmd_stop_agent,
|
|
53
|
+
cmd_reset_agent,
|
|
54
|
+
cmd_add_agent,
|
|
55
|
+
cmd_fork_agent,
|
|
56
|
+
cmd_remove_agent,
|
|
57
|
+
cmd_stuck_list,
|
|
58
|
+
cmd_stuck_cancel,
|
|
59
|
+
cmd_allow_peer_talk,
|
|
60
|
+
cmd_advanced,
|
|
61
|
+
cmd_install_skill,
|
|
62
|
+
_skill_dest_dir,
|
|
63
|
+
_install_skill_to,
|
|
64
|
+
|
|
65
|
+
)
|
|
66
|
+
from team_agent.cli.e2e import (
|
|
67
|
+
_fake_spec,
|
|
68
|
+
_run_fake_e2e,
|
|
69
|
+
_run_real_launch_smoke,
|
|
70
|
+
cmd_e2e,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
'SEND_ORDER_HINT',
|
|
75
|
+
'TeamAgentArgumentParser',
|
|
76
|
+
'main',
|
|
77
|
+
'add_json',
|
|
78
|
+
'_run_leader_passthrough',
|
|
79
|
+
'emit',
|
|
80
|
+
'_workspace_from_args',
|
|
81
|
+
'_emit_cli_error',
|
|
82
|
+
'_cli_error_payload',
|
|
83
|
+
'_tmux_session_conflict_name',
|
|
84
|
+
'_tmux_session_conflict_next_action',
|
|
85
|
+
'_tmux_session_conflict_action',
|
|
86
|
+
'_provider_args',
|
|
87
|
+
'_leader_launcher_args',
|
|
88
|
+
'cmd_quick_start',
|
|
89
|
+
'cmd_codex',
|
|
90
|
+
'cmd_claude',
|
|
91
|
+
'cmd_init',
|
|
92
|
+
'cmd_validate',
|
|
93
|
+
'cmd_compile',
|
|
94
|
+
'_profile_scope',
|
|
95
|
+
'cmd_profile_init',
|
|
96
|
+
'cmd_profile_doctor',
|
|
97
|
+
'cmd_profile_show',
|
|
98
|
+
'cmd_launch',
|
|
99
|
+
'cmd_preflight',
|
|
100
|
+
'cmd_start',
|
|
101
|
+
'cmd_wait_ready',
|
|
102
|
+
'cmd_settle',
|
|
103
|
+
'cmd_status',
|
|
104
|
+
'cmd_approvals',
|
|
105
|
+
'cmd_peek',
|
|
106
|
+
'cmd_inbox',
|
|
107
|
+
'cmd_sessions',
|
|
108
|
+
'cmd_attach_leader',
|
|
109
|
+
'cmd_send',
|
|
110
|
+
'cmd_collect',
|
|
111
|
+
'cmd_diagnose',
|
|
112
|
+
'cmd_repair_state',
|
|
113
|
+
'cmd_validate_result',
|
|
114
|
+
'cmd_doctor',
|
|
115
|
+
'cmd_shutdown',
|
|
116
|
+
'cmd_restart',
|
|
117
|
+
'cmd_start_agent',
|
|
118
|
+
'cmd_stop_agent',
|
|
119
|
+
'cmd_reset_agent',
|
|
120
|
+
'cmd_add_agent',
|
|
121
|
+
'cmd_fork_agent',
|
|
122
|
+
'cmd_remove_agent',
|
|
123
|
+
'cmd_stuck_list',
|
|
124
|
+
'cmd_stuck_cancel',
|
|
125
|
+
'cmd_allow_peer_talk',
|
|
126
|
+
'cmd_advanced',
|
|
127
|
+
'cmd_install_skill',
|
|
128
|
+
'_skill_dest_dir',
|
|
129
|
+
'_install_skill_to',
|
|
130
|
+
'cmd_e2e',
|
|
131
|
+
'_run_fake_e2e',
|
|
132
|
+
'_run_real_launch_smoke',
|
|
133
|
+
'_fake_spec',
|
|
134
|
+
|
|
135
|
+
]
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from team_agent import compiler, profiles, runtime
|
|
11
|
+
from team_agent.errors import TeamAgentError
|
|
12
|
+
from team_agent.paths import repo_root, team_workspace
|
|
13
|
+
from team_agent.spec import validate_result_envelope
|
|
14
|
+
|
|
15
|
+
from team_agent.cli.helpers import _provider_args
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def cmd_quick_start(args: argparse.Namespace) -> dict[str, Any]:
|
|
19
|
+
result = runtime.quick_start(Path(args.agents_dir), name=args.name, yes=args.yes, fresh=args.fresh, team_id=args.team_id)
|
|
20
|
+
if args.json or not result.get("ok"):
|
|
21
|
+
return result
|
|
22
|
+
return result["summary"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def cmd_codex(args: argparse.Namespace) -> None:
|
|
26
|
+
runtime.start_leader("codex", _provider_args(args.provider_args), Path.cwd().resolve())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cmd_claude(args: argparse.Namespace) -> None:
|
|
30
|
+
runtime.start_leader("claude_code", _provider_args(args.provider_args), Path.cwd().resolve())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def cmd_init(args: argparse.Namespace) -> dict[str, Any]:
|
|
34
|
+
paths = runtime.init_workspace(Path(args.workspace).resolve(), force=args.force)
|
|
35
|
+
return {"ok": True, "spec": str(paths["spec"]), "state": str(paths["state"])}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cmd_validate(args: argparse.Namespace) -> dict[str, Any]:
|
|
39
|
+
return runtime.validate_file(Path(args.spec).resolve())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def cmd_compile(args: argparse.Namespace) -> dict[str, Any]:
|
|
43
|
+
result = compiler.compile_team(Path(args.team).resolve(), Path(args.out).resolve())
|
|
44
|
+
return {"ok": True, "team_dir": result["team_dir"], "out": result["out"], "agents": [a["id"] for a in result["spec"]["agents"]]}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _profile_scope(args: argparse.Namespace) -> tuple[Path, Path | None]:
|
|
48
|
+
team = getattr(args, "team", None)
|
|
49
|
+
if team:
|
|
50
|
+
team_dir = Path(team).resolve()
|
|
51
|
+
return team_workspace(team_dir), team_dir / "profiles"
|
|
52
|
+
return Path(args.workspace).resolve(), None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def cmd_profile_init(args: argparse.Namespace) -> dict[str, Any]:
|
|
56
|
+
workspace, profiles_dir = _profile_scope(args)
|
|
57
|
+
return profiles.init_profile(workspace, args.name, args.auth_mode, profiles_dir=profiles_dir)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def cmd_profile_doctor(args: argparse.Namespace) -> dict[str, Any]:
|
|
61
|
+
workspace, profiles_dir = _profile_scope(args)
|
|
62
|
+
return profiles.doctor_profile(workspace, args.name, profiles_dir=profiles_dir)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def cmd_profile_show(args: argparse.Namespace) -> dict[str, Any]:
|
|
66
|
+
workspace, profiles_dir = _profile_scope(args)
|
|
67
|
+
return profiles.show_profile(workspace, args.name, profiles_dir=profiles_dir)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cmd_launch(args: argparse.Namespace) -> dict[str, Any]:
|
|
71
|
+
return runtime.launch(Path(args.spec).resolve(), dry_run=args.dry_run, auto_approve=args.yes)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def cmd_preflight(args: argparse.Namespace) -> dict[str, Any]:
|
|
75
|
+
return runtime.preflight(Path(args.team).resolve())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def cmd_start(args: argparse.Namespace) -> dict[str, Any]:
|
|
79
|
+
return runtime.start(Path(args.team).resolve(), yes=args.yes)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def cmd_wait_ready(args: argparse.Namespace) -> dict[str, Any]:
|
|
83
|
+
return runtime.wait_ready(Path(args.workspace).resolve(), timeout=args.timeout)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def cmd_settle(args: argparse.Namespace) -> dict[str, Any]:
|
|
87
|
+
return runtime.settle(Path(args.workspace).resolve())
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def cmd_status(args: argparse.Namespace) -> dict[str, Any]:
|
|
91
|
+
if args.json:
|
|
92
|
+
return runtime.status(Path(args.workspace).resolve(), as_json=True, compact=not args.detail)
|
|
93
|
+
return runtime.format_status(Path(args.workspace).resolve(), args.agent)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def cmd_approvals(args: argparse.Namespace) -> dict[str, Any]:
|
|
97
|
+
if args.json:
|
|
98
|
+
return runtime.approvals(Path(args.workspace).resolve(), agent_id=args.agent)
|
|
99
|
+
return runtime.format_approvals(Path(args.workspace).resolve(), agent_id=args.agent)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def cmd_peek(args: argparse.Namespace) -> dict[str, Any]:
|
|
103
|
+
if not args.allow_raw_screen:
|
|
104
|
+
raise TeamAgentError(
|
|
105
|
+
"raw worker terminal inspection requires explicit user authorization and --allow-raw-screen; "
|
|
106
|
+
"normal operation must use status, approvals, inbox, collect, or event logs"
|
|
107
|
+
)
|
|
108
|
+
result = runtime.peek(
|
|
109
|
+
Path(args.workspace).resolve(),
|
|
110
|
+
args.agent,
|
|
111
|
+
head=args.head,
|
|
112
|
+
tail=args.tail,
|
|
113
|
+
search=args.search,
|
|
114
|
+
context=args.context,
|
|
115
|
+
)
|
|
116
|
+
if args.json:
|
|
117
|
+
return result
|
|
118
|
+
return result["text"]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def cmd_inbox(args: argparse.Namespace) -> dict[str, Any]:
|
|
122
|
+
if args.json:
|
|
123
|
+
return runtime.inbox(Path(args.workspace).resolve(), args.agent, limit=args.limit)
|
|
124
|
+
return runtime.format_inbox(Path(args.workspace).resolve(), args.agent, limit=args.limit)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def cmd_sessions(args: argparse.Namespace) -> dict[str, Any]:
|
|
128
|
+
return runtime.sessions(Path(args.workspace).resolve())
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def cmd_attach_leader(args: argparse.Namespace) -> dict[str, Any]:
|
|
132
|
+
return runtime.attach_leader(Path(args.workspace).resolve(), pane=args.pane, provider=args.provider)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def cmd_takeover(args: argparse.Namespace) -> dict[str, Any]:
|
|
136
|
+
return runtime.takeover(Path(args.workspace).resolve(), team=args.team, confirm=args.confirm)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def cmd_send(args: argparse.Namespace) -> dict[str, Any]:
|
|
140
|
+
target = _send_target(args)
|
|
141
|
+
return runtime.send_message(
|
|
142
|
+
Path(args.workspace).resolve(),
|
|
143
|
+
target,
|
|
144
|
+
" ".join(args.message),
|
|
145
|
+
task_id=args.task,
|
|
146
|
+
sender=args.sender,
|
|
147
|
+
requires_ack=not args.no_ack,
|
|
148
|
+
confirm_human=args.confirm_human,
|
|
149
|
+
wait_visible=not args.no_wait,
|
|
150
|
+
timeout=args.timeout,
|
|
151
|
+
watch_result=args.watch_result,
|
|
152
|
+
team=args.team,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _send_target(args: argparse.Namespace) -> str | list[str] | None:
|
|
157
|
+
if getattr(args, "targets", None):
|
|
158
|
+
return [item.strip() for item in args.targets.split(",") if item.strip()]
|
|
159
|
+
return args.target
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def cmd_collect(args: argparse.Namespace) -> dict[str, Any]:
|
|
163
|
+
result_file = Path(args.result_file).resolve() if args.result_file else None
|
|
164
|
+
return runtime.collect(Path(args.workspace).resolve(), result_file=result_file)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def cmd_diagnose(args: argparse.Namespace) -> dict[str, Any]:
|
|
168
|
+
return runtime.diagnose(Path(args.workspace).resolve())
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def cmd_repair_state(args: argparse.Namespace) -> dict[str, Any]:
|
|
172
|
+
return runtime.repair_state(
|
|
173
|
+
Path(args.workspace).resolve(),
|
|
174
|
+
task_id=args.task,
|
|
175
|
+
assignee=args.assignee,
|
|
176
|
+
status_value=args.status,
|
|
177
|
+
summary=args.summary,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def cmd_validate_result(args: argparse.Namespace) -> dict[str, Any]:
|
|
182
|
+
if args.file:
|
|
183
|
+
raw = Path(args.file).read_text(encoding="utf-8")
|
|
184
|
+
elif args.result:
|
|
185
|
+
raw = args.result
|
|
186
|
+
else:
|
|
187
|
+
raw = sys.stdin.read()
|
|
188
|
+
envelope = json.loads(raw)
|
|
189
|
+
validate_result_envelope(envelope)
|
|
190
|
+
return {"ok": True, "task_id": envelope["task_id"], "agent_id": envelope["agent_id"], "status": envelope["status"]}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def cmd_doctor(args: argparse.Namespace) -> dict[str, Any]:
|
|
194
|
+
spec = Path(args.spec).resolve() if args.spec else None
|
|
195
|
+
return runtime.doctor(spec)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def cmd_shutdown(args: argparse.Namespace) -> dict[str, Any]:
|
|
199
|
+
return runtime.shutdown(Path(args.workspace).resolve(), keep_logs=args.keep_logs, team=args.team)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def cmd_restart(args: argparse.Namespace) -> dict[str, Any]:
|
|
203
|
+
return runtime.restart(Path(args.workspace).resolve(), allow_fresh=args.allow_fresh, team=args.team)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def cmd_start_agent(args: argparse.Namespace) -> dict[str, Any]:
|
|
207
|
+
return runtime.start_agent(
|
|
208
|
+
Path(args.workspace).resolve(),
|
|
209
|
+
args.agent,
|
|
210
|
+
force=args.force,
|
|
211
|
+
open_display=not args.no_display,
|
|
212
|
+
allow_fresh=args.allow_fresh,
|
|
213
|
+
team=args.team,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def cmd_stop_agent(args: argparse.Namespace) -> dict[str, Any]:
|
|
218
|
+
return runtime.stop_agent(Path(args.workspace).resolve(), args.agent, team=args.team)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def cmd_reset_agent(args: argparse.Namespace) -> dict[str, Any]:
|
|
222
|
+
return runtime.reset_agent(
|
|
223
|
+
Path(args.workspace).resolve(),
|
|
224
|
+
args.agent,
|
|
225
|
+
discard_session=args.discard_session,
|
|
226
|
+
open_display=not args.no_display,
|
|
227
|
+
team=args.team,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def cmd_add_agent(args: argparse.Namespace) -> dict[str, Any]:
|
|
232
|
+
return runtime.add_agent(
|
|
233
|
+
Path(args.workspace).resolve(),
|
|
234
|
+
args.agent,
|
|
235
|
+
role_file_path=args.role_file,
|
|
236
|
+
open_display=not args.no_display,
|
|
237
|
+
team=args.team,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def cmd_fork_agent(args: argparse.Namespace) -> dict[str, Any]:
|
|
242
|
+
return runtime.fork_agent(
|
|
243
|
+
Path(args.workspace).resolve(),
|
|
244
|
+
args.source_agent,
|
|
245
|
+
as_agent_id=args.as_agent,
|
|
246
|
+
label=args.label,
|
|
247
|
+
open_display=not args.no_display,
|
|
248
|
+
team=args.team,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def cmd_remove_agent(args: argparse.Namespace) -> dict[str, Any]:
|
|
253
|
+
return runtime.remove_agent(
|
|
254
|
+
Path(args.workspace).resolve(),
|
|
255
|
+
args.agent,
|
|
256
|
+
from_spec=args.from_spec,
|
|
257
|
+
confirm=args.confirm,
|
|
258
|
+
force=args.force,
|
|
259
|
+
team=args.team,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def cmd_stuck_list(args: argparse.Namespace) -> dict[str, Any]:
|
|
264
|
+
return runtime.stuck_list(Path(args.workspace).resolve())
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def cmd_stuck_cancel(args: argparse.Namespace) -> dict[str, Any]:
|
|
268
|
+
return runtime.stuck_cancel(
|
|
269
|
+
Path(args.workspace).resolve(),
|
|
270
|
+
args.agent,
|
|
271
|
+
alert_type=args.alert_type,
|
|
272
|
+
suppressed_by="leader",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def cmd_allow_peer_talk(args: argparse.Namespace) -> dict[str, Any]:
|
|
277
|
+
return runtime.allow_peer_talk(Path(args.workspace).resolve(), args.agent_a, args.agent_b)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def cmd_run_overnight(args: argparse.Namespace) -> dict[str, Any]:
|
|
281
|
+
from team_agent import orchestrator
|
|
282
|
+
workspace = Path(args.workspace).resolve()
|
|
283
|
+
if args.status:
|
|
284
|
+
return orchestrator.plan_status(workspace, plan_id=args.plan_id)
|
|
285
|
+
if args.halt:
|
|
286
|
+
if not args.plan_id:
|
|
287
|
+
raise TeamAgentError("--halt requires --plan-id")
|
|
288
|
+
return orchestrator.halt_plan(workspace, args.plan_id, reason=args.reason)
|
|
289
|
+
if not args.plan:
|
|
290
|
+
raise TeamAgentError("--plan PATH is required unless --status or --halt is used")
|
|
291
|
+
return orchestrator.start_plan(workspace, Path(args.plan).resolve(), start=not args.no_start)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def cmd_advanced(args: argparse.Namespace) -> str:
|
|
295
|
+
return "\n".join(
|
|
296
|
+
[
|
|
297
|
+
"Low-level commands:",
|
|
298
|
+
" init validate compile profile launch preflight start wait-ready settle",
|
|
299
|
+
" sessions attach-leader collect diagnose repair-state validate-result",
|
|
300
|
+
" install-skill e2e",
|
|
301
|
+
]
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def cmd_install_skill(args: argparse.Namespace) -> dict[str, Any]:
|
|
306
|
+
source = repo_root() / "skills" / "team-agent"
|
|
307
|
+
if args.dest and args.target == "all":
|
|
308
|
+
raise TeamAgentError("--dest cannot be combined with --target all")
|
|
309
|
+
if args.dest:
|
|
310
|
+
dest_dir = Path(args.dest).expanduser().resolve()
|
|
311
|
+
return _install_skill_to(source, dest_dir, args.dry_run)
|
|
312
|
+
if args.target == "all":
|
|
313
|
+
results = [
|
|
314
|
+
_install_skill_to(source, _skill_dest_dir("codex"), args.dry_run),
|
|
315
|
+
_install_skill_to(source, _skill_dest_dir("claude"), args.dry_run),
|
|
316
|
+
]
|
|
317
|
+
return {"ok": all(item["ok"] for item in results), "targets": results}
|
|
318
|
+
return _install_skill_to(source, _skill_dest_dir(args.target), args.dry_run)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _skill_dest_dir(target: str) -> Path:
|
|
322
|
+
if target == "claude":
|
|
323
|
+
dest_dir = Path.home() / ".claude" / "skills" / "team-agent"
|
|
324
|
+
else:
|
|
325
|
+
dest_dir = Path.home() / ".codex" / "skills" / "team-agent"
|
|
326
|
+
return dest_dir
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _install_skill_to(source: Path, dest_dir: Path, dry_run: bool) -> dict[str, Any]:
|
|
330
|
+
dest = dest_dir / "SKILL.md"
|
|
331
|
+
if dry_run:
|
|
332
|
+
return {"ok": True, "source": str(source / "SKILL.md"), "dest": str(dest), "dry_run": True}
|
|
333
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
334
|
+
shutil.copytree(source, dest_dir, dirs_exist_ok=True)
|
|
335
|
+
return {"ok": True, "source": str(source / "SKILL.md"), "dest": str(dest)}
|