@team-agent/installer 0.1.10 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/crates/team-agent-core/src/lib.rs +50 -5
  2. package/package.json +1 -1
  3. package/schemas/team.schema.json +1 -0
  4. package/skills/team-agent/SKILL.md +1 -1
  5. package/src/team_agent/approvals/__init__.py +65 -0
  6. package/src/team_agent/approvals/constants.py +6 -0
  7. package/src/team_agent/approvals/parsing.py +176 -0
  8. package/src/team_agent/approvals/runtime_prompts.py +171 -0
  9. package/src/team_agent/approvals/status.py +165 -0
  10. package/src/team_agent/cli/__init__.py +135 -0
  11. package/src/team_agent/cli/commands.py +335 -0
  12. package/src/team_agent/cli/e2e.py +202 -0
  13. package/src/team_agent/cli/helpers.py +137 -0
  14. package/src/team_agent/cli/parser.py +470 -0
  15. package/src/team_agent/compiler.py +98 -33
  16. package/src/team_agent/coordinator/__init__.py +53 -0
  17. package/src/team_agent/{coordinator.py → coordinator/__main__.py} +3 -1
  18. package/src/team_agent/coordinator/lifecycle.py +319 -0
  19. package/src/team_agent/coordinator/metadata.py +61 -0
  20. package/src/team_agent/coordinator/paths.py +17 -0
  21. package/src/team_agent/diagnose/__init__.py +48 -0
  22. package/src/team_agent/diagnose/checks.py +101 -0
  23. package/src/team_agent/diagnose/health.py +241 -0
  24. package/src/team_agent/diagnose/preflight.py +194 -0
  25. package/src/team_agent/diagnose/quick_start.py +233 -0
  26. package/src/team_agent/display/__init__.py +61 -0
  27. package/src/team_agent/display/close.py +147 -0
  28. package/src/team_agent/display/ghostty.py +77 -0
  29. package/src/team_agent/display/worker_window.py +110 -0
  30. package/src/team_agent/display/workspace.py +473 -0
  31. package/src/team_agent/launch/__init__.py +41 -0
  32. package/src/team_agent/launch/bootstrap.py +85 -0
  33. package/src/team_agent/launch/config.py +106 -0
  34. package/src/team_agent/launch/core.py +291 -0
  35. package/src/team_agent/launch/requirements.py +57 -0
  36. package/src/team_agent/leader/__init__.py +320 -0
  37. package/src/team_agent/lifecycle/__init__.py +5 -0
  38. package/src/team_agent/lifecycle/agents.py +226 -0
  39. package/src/team_agent/lifecycle/operations.py +321 -0
  40. package/src/team_agent/lifecycle/start.py +360 -0
  41. package/src/team_agent/mcp_server/__init__.py +42 -0
  42. package/src/team_agent/mcp_server/__main__.py +7 -0
  43. package/src/team_agent/mcp_server/contracts.py +148 -0
  44. package/src/team_agent/mcp_server/normalize.py +257 -0
  45. package/src/team_agent/mcp_server/server.py +150 -0
  46. package/src/team_agent/mcp_server/tools.py +205 -0
  47. package/src/team_agent/message_store/__init__.py +23 -0
  48. package/src/team_agent/message_store/agent_health.py +109 -0
  49. package/src/team_agent/{message_store.py → message_store/core.py} +188 -245
  50. package/src/team_agent/message_store/result_watchers.py +102 -0
  51. package/src/team_agent/message_store/schema.py +266 -0
  52. package/src/team_agent/messaging/__init__.py +1 -0
  53. package/src/team_agent/messaging/activity_detector.py +190 -0
  54. package/src/team_agent/messaging/delivery.py +128 -0
  55. package/src/team_agent/messaging/deps.py +263 -0
  56. package/src/team_agent/messaging/idle_alerts.py +217 -0
  57. package/src/team_agent/messaging/internal_delivery.py +46 -0
  58. package/src/team_agent/messaging/leader.py +317 -0
  59. package/src/team_agent/messaging/leader_panes.py +343 -0
  60. package/src/team_agent/messaging/result_delivery.py +300 -0
  61. package/src/team_agent/messaging/results.py +456 -0
  62. package/src/team_agent/messaging/scheduler.py +418 -0
  63. package/src/team_agent/messaging/send.py +493 -0
  64. package/src/team_agent/messaging/tmux_io.py +337 -0
  65. package/src/team_agent/messaging/tmux_prompt.py +229 -0
  66. package/src/team_agent/orchestrator/__init__.py +376 -0
  67. package/src/team_agent/orchestrator/plan.py +122 -0
  68. package/src/team_agent/orchestrator/state.py +128 -0
  69. package/src/team_agent/profiles/__init__.py +82 -0
  70. package/src/team_agent/profiles/constants.py +19 -0
  71. package/src/team_agent/profiles/core.py +407 -0
  72. package/src/team_agent/profiles/helpers.py +69 -0
  73. package/src/team_agent/profiles/provider_env.py +188 -0
  74. package/src/team_agent/profiles/smoke.py +201 -0
  75. package/src/team_agent/provider_cli/__init__.py +43 -0
  76. package/src/team_agent/provider_cli/adapter.py +167 -0
  77. package/src/team_agent/provider_cli/base.py +48 -0
  78. package/src/team_agent/provider_cli/claude.py +457 -0
  79. package/src/team_agent/provider_cli/codex.py +319 -0
  80. package/src/team_agent/provider_cli/copilot.py +8 -0
  81. package/src/team_agent/provider_cli/fake.py +39 -0
  82. package/src/team_agent/provider_cli/gemini.py +95 -0
  83. package/src/team_agent/provider_cli/opencode.py +8 -0
  84. package/src/team_agent/provider_cli/prompt.py +62 -0
  85. package/src/team_agent/provider_cli/registry.py +18 -0
  86. package/src/team_agent/provider_cli/unsupported.py +32 -0
  87. package/src/team_agent/providers.py +67 -949
  88. package/src/team_agent/quality_gates.py +104 -0
  89. package/src/team_agent/restart/__init__.py +34 -0
  90. package/src/team_agent/restart/orchestration.py +328 -0
  91. package/src/team_agent/restart/selection.py +89 -0
  92. package/src/team_agent/restart/snapshot.py +70 -0
  93. package/src/team_agent/runtime.py +802 -5740
  94. package/src/team_agent/rust_core.py +22 -5
  95. package/src/team_agent/sessions/__init__.py +25 -0
  96. package/src/team_agent/sessions/capture.py +93 -0
  97. package/src/team_agent/sessions/inventory.py +44 -0
  98. package/src/team_agent/sessions/resume.py +135 -0
  99. package/src/team_agent/spec.py +3 -1
  100. package/src/team_agent/state.py +204 -4
  101. package/src/team_agent/status/__init__.py +63 -0
  102. package/src/team_agent/status/approvals.py +52 -0
  103. package/src/team_agent/status/compact.py +158 -0
  104. package/src/team_agent/status/constants.py +18 -0
  105. package/src/team_agent/status/inbox.py +28 -0
  106. package/src/team_agent/status/peek.py +117 -0
  107. package/src/team_agent/status/queries.py +168 -0
  108. package/src/team_agent/terminal.py +57 -0
  109. package/src/team_agent/cli.py +0 -857
  110. package/src/team_agent/mcp_server.py +0 -579
  111. package/src/team_agent/profiles.py +0 -882
@@ -0,0 +1,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)}