@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.
Files changed (113) 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/src/team_agent/approvals/__init__.py +65 -0
  5. package/src/team_agent/approvals/constants.py +6 -0
  6. package/src/team_agent/approvals/parsing.py +176 -0
  7. package/src/team_agent/approvals/runtime_prompts.py +171 -0
  8. package/src/team_agent/approvals/status.py +165 -0
  9. package/src/team_agent/cli/__init__.py +137 -0
  10. package/src/team_agent/cli/commands.py +339 -0
  11. package/src/team_agent/cli/e2e.py +202 -0
  12. package/src/team_agent/cli/helpers.py +137 -0
  13. package/src/team_agent/cli/parser.py +477 -0
  14. package/src/team_agent/compiler.py +98 -33
  15. package/src/team_agent/coordinator/__init__.py +53 -0
  16. package/src/team_agent/{coordinator.py → coordinator/__main__.py} +3 -1
  17. package/src/team_agent/coordinator/lifecycle.py +334 -0
  18. package/src/team_agent/coordinator/metadata.py +61 -0
  19. package/src/team_agent/coordinator/paths.py +17 -0
  20. package/src/team_agent/diagnose/__init__.py +48 -0
  21. package/src/team_agent/diagnose/checks.py +101 -0
  22. package/src/team_agent/diagnose/health.py +241 -0
  23. package/src/team_agent/diagnose/preflight.py +194 -0
  24. package/src/team_agent/diagnose/quick_start.py +233 -0
  25. package/src/team_agent/display/__init__.py +61 -0
  26. package/src/team_agent/display/close.py +147 -0
  27. package/src/team_agent/display/ghostty.py +77 -0
  28. package/src/team_agent/display/worker_window.py +110 -0
  29. package/src/team_agent/display/workspace.py +473 -0
  30. package/src/team_agent/launch/__init__.py +41 -0
  31. package/src/team_agent/launch/bootstrap.py +85 -0
  32. package/src/team_agent/launch/config.py +106 -0
  33. package/src/team_agent/launch/core.py +291 -0
  34. package/src/team_agent/launch/requirements.py +57 -0
  35. package/src/team_agent/leader/__init__.py +320 -0
  36. package/src/team_agent/lifecycle/__init__.py +5 -0
  37. package/src/team_agent/lifecycle/agents.py +226 -0
  38. package/src/team_agent/lifecycle/operations.py +321 -0
  39. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +39 -0
  40. package/src/team_agent/lifecycle/start.py +363 -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 +138 -0
  55. package/src/team_agent/messaging/deps.py +263 -0
  56. package/src/team_agent/messaging/idle_alerts.py +323 -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/owner_bypass.py +29 -0
  61. package/src/team_agent/messaging/result_delivery.py +300 -0
  62. package/src/team_agent/messaging/results.py +456 -0
  63. package/src/team_agent/messaging/scheduler.py +428 -0
  64. package/src/team_agent/messaging/send.py +500 -0
  65. package/src/team_agent/messaging/session_drift.py +94 -0
  66. package/src/team_agent/messaging/tmux_io.py +337 -0
  67. package/src/team_agent/messaging/tmux_prompt.py +229 -0
  68. package/src/team_agent/orchestrator/__init__.py +376 -0
  69. package/src/team_agent/orchestrator/plan.py +122 -0
  70. package/src/team_agent/orchestrator/state.py +128 -0
  71. package/src/team_agent/profiles/__init__.py +82 -0
  72. package/src/team_agent/profiles/constants.py +19 -0
  73. package/src/team_agent/profiles/core.py +407 -0
  74. package/src/team_agent/profiles/helpers.py +69 -0
  75. package/src/team_agent/profiles/provider_env.py +188 -0
  76. package/src/team_agent/profiles/smoke.py +201 -0
  77. package/src/team_agent/provider_cli/__init__.py +43 -0
  78. package/src/team_agent/provider_cli/adapter.py +167 -0
  79. package/src/team_agent/provider_cli/base.py +48 -0
  80. package/src/team_agent/provider_cli/claude.py +457 -0
  81. package/src/team_agent/provider_cli/codex.py +319 -0
  82. package/src/team_agent/provider_cli/copilot.py +8 -0
  83. package/src/team_agent/provider_cli/fake.py +39 -0
  84. package/src/team_agent/provider_cli/gemini.py +95 -0
  85. package/src/team_agent/provider_cli/opencode.py +8 -0
  86. package/src/team_agent/provider_cli/prompt.py +62 -0
  87. package/src/team_agent/provider_cli/registry.py +18 -0
  88. package/src/team_agent/provider_cli/unsupported.py +32 -0
  89. package/src/team_agent/providers.py +67 -949
  90. package/src/team_agent/quality_gates.py +104 -0
  91. package/src/team_agent/restart/__init__.py +34 -0
  92. package/src/team_agent/restart/orchestration.py +328 -0
  93. package/src/team_agent/restart/selection.py +89 -0
  94. package/src/team_agent/restart/snapshot.py +70 -0
  95. package/src/team_agent/runtime.py +809 -5892
  96. package/src/team_agent/rust_core.py +22 -5
  97. package/src/team_agent/sessions/__init__.py +25 -0
  98. package/src/team_agent/sessions/capture.py +93 -0
  99. package/src/team_agent/sessions/inventory.py +44 -0
  100. package/src/team_agent/sessions/resume.py +135 -0
  101. package/src/team_agent/spec.py +3 -1
  102. package/src/team_agent/state.py +218 -4
  103. package/src/team_agent/status/__init__.py +63 -0
  104. package/src/team_agent/status/approvals.py +52 -0
  105. package/src/team_agent/status/compact.py +158 -0
  106. package/src/team_agent/status/constants.py +18 -0
  107. package/src/team_agent/status/inbox.py +28 -0
  108. package/src/team_agent/status/peek.py +117 -0
  109. package/src/team_agent/status/queries.py +168 -0
  110. package/src/team_agent/terminal.py +57 -0
  111. package/src/team_agent/cli.py +0 -858
  112. package/src/team_agent/mcp_server.py +0 -579
  113. package/src/team_agent/profiles.py +0 -882
@@ -0,0 +1,319 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import subprocess
6
+ import time
7
+ from datetime import datetime, timedelta, timezone
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from team_agent.permissions import resolve_permissions
12
+ from team_agent.provider_cli.adapter import (
13
+ ProviderAdapter,
14
+ ResumeUnavailable,
15
+ agent_model,
16
+ parse_time,
17
+ )
18
+ from team_agent.provider_cli.prompt import compile_system_prompt
19
+
20
+
21
+ class CodexAdapter(ProviderAdapter):
22
+ provider = "codex"
23
+ command_name = "codex"
24
+ _model_catalog_cache: dict[str, Any] | None = None
25
+
26
+ def build_command(self, agent: dict[str, Any], workspace: Path, mcp_config: dict[str, Any]) -> list[str]:
27
+ cmd = self._base_command(agent, mcp_config, resume=False)
28
+ return cmd
29
+
30
+ def build_resume_command(
31
+ self,
32
+ agent_state: dict[str, Any],
33
+ workspace: Path,
34
+ mcp_config: dict[str, Any] | None = None,
35
+ ) -> list[str]:
36
+ _ = workspace
37
+ session_id = agent_state.get("session_id")
38
+ if not session_id:
39
+ raise ResumeUnavailable("codex resume requires session_id")
40
+ agent = dict(agent_state.get("_agent_spec") or agent_state)
41
+ cmd = self._base_command(agent, mcp_config or {}, resume=True)
42
+ cmd.append(str(session_id))
43
+ return cmd
44
+
45
+ def supports_session_fork(self, agent: dict[str, Any] | None = None) -> bool:
46
+ return not agent or agent.get("auth_mode") != "compatible_api"
47
+
48
+ def build_fork_command(
49
+ self,
50
+ agent: dict[str, Any],
51
+ source_session_id: str,
52
+ workspace: Path,
53
+ mcp_config: dict[str, Any],
54
+ ) -> list[str]:
55
+ _ = workspace
56
+ if not source_session_id:
57
+ raise ResumeUnavailable("codex fork requires source session_id")
58
+ cmd = self._base_command(agent, mcp_config, resume=False, fork=True)
59
+ cmd.append(str(source_session_id))
60
+ return cmd
61
+
62
+ def capture_session_id(
63
+ self,
64
+ agent_id: str,
65
+ spawn_context: dict[str, Any],
66
+ timeout_s: float = 3.0,
67
+ ) -> dict[str, Any] | None:
68
+ _ = agent_id
69
+ cwd = spawn_context.get("cwd")
70
+ if not cwd:
71
+ return None
72
+ start = parse_time(spawn_context.get("spawn_time")) or datetime.now(timezone.utc)
73
+ root = Path(spawn_context.get("sessions_root") or Path.home() / ".codex" / "sessions")
74
+ deadline = time.monotonic() + max(timeout_s, 0.0)
75
+ exclude = {str(item) for item in spawn_context.get("exclude_session_ids", []) if item}
76
+ while True:
77
+ match = find_codex_rollout(root, Path(str(cwd)), start, exclude_session_ids=exclude)
78
+ if match:
79
+ return {
80
+ "session_id": match["session_id"],
81
+ "rollout_path": match["rollout_path"],
82
+ "captured_at": datetime.now(timezone.utc).isoformat(),
83
+ "captured_via": "fs_watch",
84
+ "attribution_confidence": match["confidence"],
85
+ "spawn_cwd": str(cwd),
86
+ }
87
+ if time.monotonic() >= deadline:
88
+ return None
89
+ time.sleep(0.2)
90
+
91
+ def _base_command(
92
+ self,
93
+ agent: dict[str, Any],
94
+ mcp_config: dict[str, Any],
95
+ resume: bool,
96
+ fork: bool = False,
97
+ ) -> list[str]:
98
+ prompt = compile_system_prompt(agent)
99
+ cmd = ["codex"]
100
+ if resume:
101
+ cmd.append("resume")
102
+ elif fork:
103
+ cmd.append("fork")
104
+ cmd.extend(["--no-alt-screen", "--disable", "shell_snapshot", "--disable", "apps"])
105
+ profile_overrides = agent.get("_provider_profile", {}).get("command_overrides", {})
106
+ if profile_overrides.get("codex_profile"):
107
+ cmd.extend(["--profile", str(profile_overrides["codex_profile"])])
108
+ if agent.get("_runtime", {}).get("dangerous_auto_approve"):
109
+ cmd.append("--dangerously-bypass-approvals-and-sandbox")
110
+ else:
111
+ tools = set(resolve_permissions(agent)["tools"])
112
+ sandbox = "workspace-write" if {"fs_write", "execute_bash"} & tools else "read-only"
113
+ cmd.extend(["--sandbox", sandbox, "--ask-for-approval", "on-request"])
114
+ model = agent_model(agent)
115
+ if model:
116
+ cmd.extend(["--model", model])
117
+ for config in profile_overrides.get("codex_config", []):
118
+ cmd.extend(["-c", str(config)])
119
+ if prompt:
120
+ escaped = prompt.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
121
+ cmd.extend(["-c", f'developer_instructions="{escaped}"'])
122
+ for server_name, cfg in mcp_config.items():
123
+ prefix = f"mcp_servers.{server_name}"
124
+ cmd.extend(["-c", f'{prefix}.command="{cfg["command"]}"'])
125
+ args = "[" + ", ".join(json.dumps(str(arg)) for arg in cfg.get("args", [])) + "]"
126
+ cmd.extend(["-c", f"{prefix}.args={args}"])
127
+ for env_key, env_val in cfg.get("env", {}).items():
128
+ cmd.extend(["-c", f'{prefix}.env.{env_key}="{env_val}"'])
129
+ cmd.extend(["-c", f"{prefix}.tool_timeout_sec=600.0"])
130
+ return cmd
131
+
132
+ def auth_hint(self) -> dict[str, Any]:
133
+ if "OPENAI_API_KEY" in __import__("os").environ:
134
+ return {"status": "present", "detail": "OPENAI_API_KEY is set"}
135
+ if Path.home().joinpath(".codex").exists():
136
+ return {"status": "present", "detail": "~/.codex exists; run codex login if startup fails"}
137
+ return {"status": "missing_or_unknown", "detail": "run codex login or set OPENAI_API_KEY"}
138
+
139
+ def status_patterns(self) -> dict[str, str]:
140
+ return {"idle": r"(›|❯|codex>)", "processing": r"•.*esc to interrupt", "error": "Error|Traceback|panic"}
141
+
142
+ def handle_startup_prompts(
143
+ self,
144
+ session_name: str,
145
+ window_name: str,
146
+ checks: int = 30,
147
+ sleep_s: float = 0.5,
148
+ ) -> list[dict[str, Any]]:
149
+ handled: list[dict[str, Any]] = []
150
+ target = f"{session_name}:{window_name}"
151
+ for _ in range(max(checks, 0)):
152
+ proc = subprocess.run(
153
+ ["tmux", "capture-pane", "-p", "-S", "-", "-t", target],
154
+ text=True,
155
+ capture_output=True,
156
+ timeout=5,
157
+ check=False,
158
+ )
159
+ output = proc.stdout if proc.returncode == 0 else ""
160
+ trust_pos = max(
161
+ output.rfind("Do you trust the contents of this directory?"),
162
+ output.rfind("Do you trust the files in this folder?"),
163
+ output.rfind("Do you trust this folder?"),
164
+ )
165
+ update_pos = max(output.rfind("Update available!"), output.rfind("Update now"))
166
+ ready_pos = max(output.rfind("OpenAI Codex"), output.rfind("›"), output.rfind("codex>"))
167
+ if update_pos >= 0 and update_pos > ready_pos:
168
+ subprocess.run(["tmux", "send-keys", "-t", target, "Down", "Enter"], check=False)
169
+ handled.append({"prompt": "codex_update_available", "action": "sent_skip"})
170
+ if sleep_s > 0:
171
+ time.sleep(sleep_s)
172
+ continue
173
+ if trust_pos >= 0 and trust_pos > ready_pos:
174
+ subprocess.run(["tmux", "send-keys", "-t", target, "Enter"], check=False)
175
+ handled.append({"prompt": "codex_workspace_trust", "action": "sent_enter"})
176
+ if sleep_s > 0:
177
+ time.sleep(sleep_s)
178
+ continue
179
+ if ready_pos >= 0:
180
+ break
181
+ if sleep_s > 0:
182
+ time.sleep(sleep_s)
183
+ return handled
184
+
185
+ def handle_runtime_prompts(self, session_name: str, window_name: str) -> list[dict[str, Any]]:
186
+ _ = session_name, window_name
187
+ return []
188
+
189
+ def validate_model(self, model: str | None) -> dict[str, Any]:
190
+ if not model:
191
+ return {"ok": True, "status": "model_not_set", "provider": self.provider, "model": model}
192
+ catalog = self._model_catalog()
193
+ if not catalog.get("ok"):
194
+ details = {key: value for key, value in catalog.items() if key != "ok"}
195
+ return {"ok": False, "status": "model_catalog_unavailable", "provider": self.provider, "model": model, **details}
196
+ models = catalog.get("models", [])
197
+ slugs = {str(item.get("slug") or "") for item in models if item.get("slug")}
198
+ if model in slugs:
199
+ return {"ok": True, "status": "model_supported", "provider": self.provider, "model": model}
200
+ slug_by_lower = {slug.lower(): slug for slug in slugs}
201
+ display_to_slug = {
202
+ str(item.get("display_name") or "").lower(): str(item.get("slug"))
203
+ for item in models
204
+ if item.get("display_name") and item.get("slug")
205
+ }
206
+ normalized = model.lower()
207
+ suggested = slug_by_lower.get(normalized) or display_to_slug.get(normalized)
208
+ result = {
209
+ "ok": False,
210
+ "status": "unsupported_model",
211
+ "reason": "model_id_not_found",
212
+ "provider": self.provider,
213
+ "model": model,
214
+ "available_models": sorted(slugs),
215
+ }
216
+ if suggested:
217
+ result["reason"] = "model_id_not_exact"
218
+ result["suggested_model"] = suggested
219
+ return result
220
+
221
+ def _model_catalog(self) -> dict[str, Any]:
222
+ if self._model_catalog_cache is not None:
223
+ return self._model_catalog_cache
224
+ if not self.is_installed():
225
+ return {"ok": False, "reason": "codex_command_missing", "command": self.command_name}
226
+ try:
227
+ proc = subprocess.run(
228
+ [self.command_name, "debug", "models"],
229
+ text=True,
230
+ capture_output=True,
231
+ timeout=12,
232
+ check=False,
233
+ )
234
+ except (OSError, subprocess.TimeoutExpired) as exc:
235
+ return {"ok": False, "reason": "model_catalog_command_failed", "command": "codex debug models", "error": str(exc)}
236
+ if proc.returncode != 0:
237
+ return {
238
+ "ok": False,
239
+ "reason": "model_catalog_command_failed",
240
+ "command": "codex debug models",
241
+ "stderr": proc.stderr.strip(),
242
+ }
243
+ try:
244
+ data = json.loads(proc.stdout or "{}")
245
+ except json.JSONDecodeError as exc:
246
+ return {"ok": False, "reason": "model_catalog_parse_failed", "command": "codex debug models", "error": str(exc)}
247
+ models = data.get("models")
248
+ if not isinstance(models, list):
249
+ return {"ok": False, "reason": "model_catalog_shape_invalid", "command": "codex debug models"}
250
+ self._model_catalog_cache = {"ok": True, "command": "codex debug models", "models": models}
251
+ return self._model_catalog_cache
252
+
253
+
254
+ def find_codex_rollout(
255
+ root: Path,
256
+ cwd: Path,
257
+ spawn_time: datetime,
258
+ exclude_session_ids: set[str] | None = None,
259
+ ) -> dict[str, Any] | None:
260
+ if not root.exists():
261
+ return None
262
+ exclude_session_ids = exclude_session_ids or set()
263
+ lower_bound = spawn_time - timedelta(seconds=2)
264
+ upper_bound = datetime.now(timezone.utc) + timedelta(seconds=5)
265
+ candidates: list[dict[str, Any]] = []
266
+ for path in sorted(root.glob("**/rollout-*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True)[:1500]:
267
+ meta = read_codex_session_meta(path)
268
+ if not meta:
269
+ continue
270
+ meta_cwd = meta.get("cwd")
271
+ if not meta_cwd:
272
+ continue
273
+ try:
274
+ same_cwd = Path(str(meta_cwd)).resolve() == cwd.resolve()
275
+ except OSError:
276
+ same_cwd = str(meta_cwd) == str(cwd)
277
+ if not same_cwd:
278
+ continue
279
+ ts = parse_time(meta.get("timestamp"))
280
+ if ts and (ts < lower_bound or ts > upper_bound):
281
+ continue
282
+ originator = meta.get("originator")
283
+ origin_ok = originator in {"codex-tui", "codex_exec"}
284
+ session_id = meta.get("id") or rollout_id_from_name(path)
285
+ if not session_id:
286
+ continue
287
+ if str(session_id) in exclude_session_ids:
288
+ continue
289
+ candidates.append(
290
+ {
291
+ "session_id": str(session_id),
292
+ "rollout_path": str(path),
293
+ "timestamp": ts or datetime.fromtimestamp(path.stat().st_mtime, timezone.utc),
294
+ "confidence": "high" if origin_ok and ts else "medium",
295
+ }
296
+ )
297
+ if not candidates:
298
+ return None
299
+ candidates.sort(key=lambda item: item["timestamp"])
300
+ return candidates[0]
301
+
302
+
303
+ def read_codex_session_meta(path: Path) -> dict[str, Any] | None:
304
+ try:
305
+ with path.open(encoding="utf-8") as handle:
306
+ first = handle.readline()
307
+ data = json.loads(first)
308
+ except (OSError, json.JSONDecodeError):
309
+ return None
310
+ if "session_meta" in data:
311
+ payload = data.get("session_meta", {}).get("payload")
312
+ else:
313
+ payload = data.get("payload")
314
+ return payload if isinstance(payload, dict) else None
315
+
316
+
317
+ def rollout_id_from_name(path: Path) -> str | None:
318
+ match = re.search(r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\.jsonl$", path.name)
319
+ return match.group(1) if match else None
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from team_agent.provider_cli.unsupported import UnsupportedCliPlug
4
+
5
+
6
+ class CopilotCliPlug(UnsupportedCliPlug):
7
+ provider = "copilot"
8
+ command_name = "copilot"
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from team_agent.provider_cli.adapter import ProviderAdapter
8
+
9
+
10
+ class FakeAdapter(ProviderAdapter):
11
+ provider = "fake"
12
+ command_name = sys.executable
13
+
14
+ def build_command(self, agent: dict[str, Any], workspace: Path, mcp_config: dict[str, Any]) -> list[str]:
15
+ return [
16
+ sys.executable,
17
+ "-m",
18
+ "team_agent.fake_worker",
19
+ "--workspace",
20
+ str(workspace),
21
+ "--agent-id",
22
+ agent["id"],
23
+ ]
24
+
25
+ def build_resume_command(
26
+ self,
27
+ agent_state: dict[str, Any],
28
+ workspace: Path,
29
+ mcp_config: dict[str, Any] | None = None,
30
+ ) -> list[str]:
31
+ agent = dict(agent_state.get("_agent_spec") or agent_state)
32
+ agent.setdefault("id", agent_state.get("agent_id") or agent_state.get("id"))
33
+ return self.build_command(agent, workspace, mcp_config or {})
34
+
35
+ def auth_hint(self) -> dict[str, Any]:
36
+ return {"status": "present", "detail": "fake provider is local test worker"}
37
+
38
+ def status_patterns(self) -> dict[str, str]:
39
+ return {"idle": "TEAM_AGENT_FAKE_READY", "processing": "TEAM_AGENT_FAKE_WORKING", "error": "Traceback"}
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from team_agent.provider_cli.adapter import (
8
+ ProviderAdapter,
9
+ agent_model,
10
+ read_json_object,
11
+ )
12
+ from team_agent.provider_cli.prompt import compile_system_prompt
13
+
14
+
15
+ class GeminiCliAdapter(ProviderAdapter):
16
+ provider = "gemini_cli"
17
+ command_name = "gemini"
18
+
19
+ def build_command(self, agent: dict[str, Any], workspace: Path, mcp_config: dict[str, Any]) -> list[str]:
20
+ prompt = compile_system_prompt(agent)
21
+ cmd = ["gemini"]
22
+ if agent.get("_runtime", {}).get("dangerous_auto_approve"):
23
+ cmd.extend(["--yolo", "--sandbox", "false"])
24
+ model = agent_model(agent)
25
+ if model:
26
+ cmd.extend(["--model", model])
27
+ if prompt:
28
+ cmd.extend(["-i", prompt])
29
+ return cmd
30
+
31
+ def install_mcp(self, workspace: Path, agent_id: str, config: dict[str, Any]) -> Path:
32
+ path = super().install_mcp(workspace, agent_id, config)
33
+ self._register_mcp_servers(path, config)
34
+ return path
35
+
36
+ def cleanup_mcp(self, workspace: Path, agent_id: str, mcp_path: Path | None = None) -> None:
37
+ path = mcp_path or workspace / ".team" / "runtime" / "mcp" / f"{agent_id}.json"
38
+ self._restore_mcp_servers(path)
39
+
40
+ def _register_mcp_servers(self, mcp_path: Path, config: dict[str, Any]) -> None:
41
+ settings_path = Path.home() / ".gemini" / "settings.json"
42
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
43
+ settings = read_json_object(settings_path)
44
+ mcp_servers = settings.setdefault("mcpServers", {})
45
+ if not isinstance(mcp_servers, dict):
46
+ raise ValueError(f"{settings_path}: mcpServers must be an object")
47
+
48
+ backup = {
49
+ "settings_path": str(settings_path),
50
+ "servers": {name: mcp_servers.get(name) for name in config},
51
+ }
52
+ gemini_backup_path(mcp_path).write_text(json.dumps(backup, indent=2), encoding="utf-8")
53
+
54
+ for name, server in config.items():
55
+ mcp_servers[name] = {
56
+ "command": server["command"],
57
+ "args": server.get("args", []),
58
+ "env": server.get("env", {}),
59
+ }
60
+ settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8")
61
+
62
+ def _restore_mcp_servers(self, mcp_path: Path) -> None:
63
+ backup_path = gemini_backup_path(mcp_path)
64
+ if not backup_path.exists():
65
+ return
66
+ backup = json.loads(backup_path.read_text(encoding="utf-8"))
67
+ settings_path = Path(backup["settings_path"])
68
+ settings = read_json_object(settings_path)
69
+ mcp_servers = settings.setdefault("mcpServers", {})
70
+ if not isinstance(mcp_servers, dict):
71
+ raise ValueError(f"{settings_path}: mcpServers must be an object")
72
+ for name, previous in backup.get("servers", {}).items():
73
+ if previous is None:
74
+ mcp_servers.pop(name, None)
75
+ else:
76
+ mcp_servers[name] = previous
77
+ settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8")
78
+ backup_path.unlink(missing_ok=True)
79
+
80
+ def auth_hint(self) -> dict[str, Any]:
81
+ if "GEMINI_API_KEY" in __import__("os").environ:
82
+ return {"status": "present", "detail": "GEMINI_API_KEY is set"}
83
+ if Path.home().joinpath(".gemini").exists():
84
+ return {"status": "present", "detail": "~/.gemini exists; run gemini to verify OAuth"}
85
+ return {"status": "missing_or_unknown", "detail": "run gemini OAuth setup or set GEMINI_API_KEY"}
86
+
87
+ def status_patterns(self) -> dict[str, str]:
88
+ return {"idle": r"\*\s+Type your message", "processing": r"\(esc to cancel", "error": "Error|APIError|Traceback"}
89
+
90
+ def exit_text(self) -> str:
91
+ return "\x04"
92
+
93
+
94
+ def gemini_backup_path(mcp_path: Path) -> Path:
95
+ return mcp_path.with_suffix(".gemini-backup.json")
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from team_agent.provider_cli.unsupported import UnsupportedCliPlug
4
+
5
+
6
+ class OpenCodeCliPlug(UnsupportedCliPlug):
7
+ provider = "opencode"
8
+ command_name = "opencode"
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from team_agent.permissions import resolve_permissions
7
+
8
+
9
+ TEAMMATE_SYSTEM_PROMPT = """# Team Agent Teammate Runtime Contract
10
+
11
+ You are a teammate in a Team Agent runtime, not the user's primary assistant.
12
+ The user normally talks to the team lead. Plain text you write in this worker
13
+ session is local to this session and is not a team message.
14
+
15
+ Use Team Agent MCP tools for team-visible coordination:
16
+ - Send progress, blockers, permission needs, tool failures, scope changes, and
17
+ long-running status updates with team_orchestrator.send_message(to='leader',
18
+ content='<short message>').
19
+ - Send to another teammate by agent id when coordination is useful, or use
20
+ to='*' to notify every other team member. The runtime resolves only this team
21
+ and excludes your own worker.
22
+ - When the task is complete, call team_orchestrator.report_result exactly once.
23
+ - Do not pass sender, task_id, agent_id, schema_version, or ack fields unless
24
+ doing a low-level compatibility diagnostic. The MCP runtime fills protocol
25
+ fields from the current worker and task state.
26
+
27
+ If you are blocked or cannot continue, message the leader promptly instead of
28
+ waiting silently. If work takes several minutes, send a short progress update.
29
+ """
30
+
31
+
32
+ def compile_system_prompt(agent: dict[str, Any]) -> str:
33
+ prompt_cfg = agent.get("system_prompt", {})
34
+ identity = (
35
+ f"You are Team Agent worker `{agent.get('id')}` with role `{agent.get('role')}`. "
36
+ "When asked about your role or identity, answer with this Team Agent worker identity first, "
37
+ "not only the generic provider product identity."
38
+ )
39
+ chunks: list[str] = [identity, TEAMMATE_SYSTEM_PROMPT]
40
+ if prompt_cfg.get("inline"):
41
+ chunks.append(str(prompt_cfg["inline"]))
42
+ if prompt_cfg.get("file"):
43
+ chunks.append(Path(prompt_cfg["file"]).read_text(encoding="utf-8"))
44
+ contract = agent.get("output_contract", {})
45
+ if contract.get("format") == "result_envelope_v1":
46
+ chunks.append(
47
+ "For progress or blockers, call team_orchestrator.send_message(to='leader', content='<short message>'); "
48
+ "for teammate coordination, send to another agent id or to='*' for every other team member. "
49
+ "do not pass sender, task_id, or requires_ack because the MCP runtime fills protocol fields. "
50
+ "the runtime injects it into the attached Codex leader pane when the leader has run attach-leader. "
51
+ "If no leader is attached, the tool returns a fallback/failed result instead of completion. "
52
+ "Final completion must call team_orchestrator.report_result exactly once with a short summary "
53
+ "and optional status/changes/tests; MCP fills schema_version, task_id, and agent_id."
54
+ )
55
+ perms = resolve_permissions(agent)
56
+ if perms["has_prompt_only"]:
57
+ prompt_only = [e["tool"] for e in perms["resolved_tools"] if e["enforcement"] == "prompt_only"]
58
+ chunks.append(
59
+ "Permission note: these tools are prompt-only for this provider and not hard-enforced: "
60
+ + ", ".join(prompt_only)
61
+ )
62
+ return "\n\n".join(chunk for chunk in chunks if chunk)
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from team_agent.provider_cli.base import ProviderCliSocket
4
+ from team_agent.provider_cli.copilot import CopilotCliPlug
5
+ from team_agent.provider_cli.opencode import OpenCodeCliPlug
6
+
7
+
8
+ PLUG_TYPES = {
9
+ "opencode": OpenCodeCliPlug,
10
+ "copilot": CopilotCliPlug,
11
+ }
12
+
13
+
14
+ def build_plug(provider: str) -> ProviderCliSocket:
15
+ try:
16
+ return PLUG_TYPES[provider]()
17
+ except KeyError as exc:
18
+ raise KeyError(f"Unsupported provider plug: {provider}") from exc
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from team_agent.provider_cli.base import ProviderCapabilityError, ProviderStartupInput
6
+
7
+
8
+ class UnsupportedCliPlug:
9
+ provider = ""
10
+ command_name = ""
11
+
12
+ def build_command(self, startup: ProviderStartupInput) -> list[str]:
13
+ _ = startup
14
+ self._unsupported("start")
15
+
16
+ def build_resume_command(self, startup: ProviderStartupInput) -> list[str]:
17
+ _ = startup
18
+ self._unsupported("resume")
19
+
20
+ def build_fork_command(self, startup: ProviderStartupInput, source_session_id: str) -> list[str]:
21
+ _ = startup, source_session_id
22
+ self._unsupported("fork_or_branch")
23
+
24
+ def capture_session_id(self, startup: ProviderStartupInput, timeout_s: float = 3.0) -> dict[str, Any] | None:
25
+ _ = startup, timeout_s
26
+ return None
27
+
28
+ def cleanup(self, startup: ProviderStartupInput) -> None:
29
+ _ = startup
30
+
31
+ def _unsupported(self, capability: str) -> Any:
32
+ raise ProviderCapabilityError(self.provider, capability, "provider plug exists but is not implemented yet")