@team-agent/installer 0.1.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 (36) hide show
  1. package/README.md +201 -0
  2. package/crates/team-agent-core/Cargo.toml +12 -0
  3. package/crates/team-agent-core/src/lib.rs +287 -0
  4. package/crates/team-agent-core/src/main.rs +152 -0
  5. package/examples/team.spec.yaml +206 -0
  6. package/examples/team_state.md +35 -0
  7. package/npm/install.mjs +266 -0
  8. package/package.json +28 -0
  9. package/pyproject.toml +18 -0
  10. package/schemas/result-envelope.schema.json +76 -0
  11. package/schemas/team.schema.json +241 -0
  12. package/scripts/install.py +88 -0
  13. package/scripts/run_regression_tests.py +79 -0
  14. package/skills/team-agent/SKILL.md +173 -0
  15. package/src/team_agent/__init__.py +3 -0
  16. package/src/team_agent/__main__.py +5 -0
  17. package/src/team_agent/cli.py +857 -0
  18. package/src/team_agent/compiler.py +269 -0
  19. package/src/team_agent/coordinator.py +62 -0
  20. package/src/team_agent/errors.py +10 -0
  21. package/src/team_agent/events.py +37 -0
  22. package/src/team_agent/fake_worker.py +80 -0
  23. package/src/team_agent/mcp_server.py +579 -0
  24. package/src/team_agent/message_store.py +497 -0
  25. package/src/team_agent/paths.py +45 -0
  26. package/src/team_agent/permissions.py +123 -0
  27. package/src/team_agent/profiles.py +882 -0
  28. package/src/team_agent/providers.py +1045 -0
  29. package/src/team_agent/routing.py +84 -0
  30. package/src/team_agent/runtime.py +5213 -0
  31. package/src/team_agent/rust_core.py +156 -0
  32. package/src/team_agent/simple_yaml.py +236 -0
  33. package/src/team_agent/spec.py +308 -0
  34. package/src/team_agent/state.py +112 -0
  35. package/src/team_agent/task_graph.py +80 -0
  36. package/templates/team_state.md +32 -0
@@ -0,0 +1,857 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import time
6
+ import shutil
7
+ import sys
8
+ import tempfile
9
+ import traceback
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from team_agent import runtime
14
+ from team_agent import compiler
15
+ from team_agent import profiles
16
+ from team_agent.errors import TeamAgentError
17
+ from team_agent.paths import repo_root, team_workspace
18
+ from team_agent.simple_yaml import dumps
19
+ from team_agent.spec import validate_result_envelope
20
+
21
+
22
+ SEND_ORDER_HINT = (
23
+ "options must appear before target/message. Use: "
24
+ "team-agent send --task <task_id> --json \"<message>\" or "
25
+ "team-agent send --no-ack --json <agent_id> \"<message>\""
26
+ )
27
+
28
+
29
+ class TeamAgentArgumentParser(argparse.ArgumentParser):
30
+ def error(self, message: str) -> None:
31
+ send_command = "send" in sys.argv[1:]
32
+ if (getattr(self, "send_order_hint", False) or send_command) and "unrecognized arguments" in message:
33
+ message = f"{message}\nHint: {SEND_ORDER_HINT}"
34
+ super().error(message)
35
+
36
+
37
+ def main(argv: list[str] | None = None) -> None:
38
+ raw_argv = list(sys.argv[1:] if argv is None else argv)
39
+ if raw_argv and raw_argv[0] in {"codex", "claude"}:
40
+ _run_leader_passthrough(raw_argv[0], raw_argv[1:])
41
+ return
42
+
43
+ parser = TeamAgentArgumentParser(
44
+ prog="team-agent",
45
+ description="TeamSpec Agent Mode CLI",
46
+ epilog="See `team-agent advanced --help` for low-level commands (debugging only).",
47
+ )
48
+ sub = parser.add_subparsers(dest="command", required=True, parser_class=TeamAgentArgumentParser)
49
+
50
+ p = sub.add_parser("codex", help="Start a tmux-managed Codex leader in the current directory")
51
+ p.add_argument("provider_args", nargs=argparse.REMAINDER, help="Arguments passed through to codex")
52
+ p.set_defaults(func=cmd_codex)
53
+
54
+ p = sub.add_parser("claude", help="Start a tmux-managed Claude leader in the current directory")
55
+ p.add_argument("provider_args", nargs=argparse.REMAINDER, help="Arguments passed through to claude")
56
+ p.set_defaults(func=cmd_claude)
57
+
58
+ p = sub.add_parser("quick-start", help="Start a team from a role-doc directory")
59
+ p.add_argument("agents_dir")
60
+ p.add_argument("--name")
61
+ p.add_argument("--team-id", help="Store loose role docs under .team/<team-id> instead of .team/current")
62
+ p.add_argument("--yes", action="store_true")
63
+ p.add_argument("--fresh", action="store_true", help="Start fresh worker sessions even when prior runtime state exists")
64
+ add_json(p)
65
+ p.set_defaults(func=cmd_quick_start)
66
+
67
+ p = sub.add_parser("init", help=argparse.SUPPRESS)
68
+ p.add_argument("--workspace", default=".")
69
+ p.add_argument("--force", action="store_true")
70
+ add_json(p)
71
+ p.set_defaults(func=cmd_init)
72
+
73
+ p = sub.add_parser("validate", help=argparse.SUPPRESS)
74
+ p.add_argument("spec", nargs="?", default="team.spec.yaml")
75
+ add_json(p)
76
+ p.set_defaults(func=cmd_validate)
77
+
78
+ p = sub.add_parser("compile", help=argparse.SUPPRESS)
79
+ p.add_argument("--team", required=True, help="Team doc directory, for example .team/current")
80
+ p.add_argument("--out", default="team.spec.yaml")
81
+ add_json(p)
82
+ p.set_defaults(func=cmd_compile)
83
+
84
+ p = sub.add_parser("profile", help=argparse.SUPPRESS)
85
+ profile_sub = p.add_subparsers(dest="profile_command", required=True)
86
+ p_init = profile_sub.add_parser("init", help="Create an example profile template without real secrets")
87
+ p_init.add_argument("name")
88
+ p_init.add_argument("--workspace", default=".")
89
+ p_init.add_argument("--team", help="Team directory whose profiles/ directory should be used")
90
+ p_init.add_argument("--auth-mode", required=True, choices=sorted(profiles.AUTH_MODES))
91
+ add_json(p_init)
92
+ p_init.set_defaults(func=cmd_profile_init)
93
+ p_doctor = profile_sub.add_parser("doctor", help="Check whether a profile exists without printing secrets")
94
+ p_doctor.add_argument("name")
95
+ p_doctor.add_argument("--workspace", default=".")
96
+ p_doctor.add_argument("--team", help="Team directory whose profiles/ directory should be used")
97
+ add_json(p_doctor)
98
+ p_doctor.set_defaults(func=cmd_profile_doctor)
99
+ p_show = profile_sub.add_parser("show", help="Show redacted profile status without printing secrets")
100
+ p_show.add_argument("name")
101
+ p_show.add_argument("--workspace", default=".")
102
+ p_show.add_argument("--team", help="Team directory whose profiles/ directory should be used")
103
+ add_json(p_show)
104
+ p_show.set_defaults(func=cmd_profile_show)
105
+
106
+ p = sub.add_parser("launch", help=argparse.SUPPRESS)
107
+ p.add_argument("spec", nargs="?", default="team.spec.yaml")
108
+ p.add_argument("--yes", action="store_true", help="Confirm launch after permission summary review")
109
+ p.add_argument("--dry-run", action="store_true")
110
+ add_json(p)
111
+ p.set_defaults(func=cmd_launch)
112
+
113
+ p = sub.add_parser("preflight", help=argparse.SUPPRESS)
114
+ p.add_argument("--team", required=True)
115
+ add_json(p)
116
+ p.set_defaults(func=cmd_preflight)
117
+
118
+ p = sub.add_parser("start", help=argparse.SUPPRESS)
119
+ p.add_argument("--team", required=True)
120
+ p.add_argument("--yes", action="store_true")
121
+ add_json(p)
122
+ p.set_defaults(func=cmd_start)
123
+
124
+ p = sub.add_parser("wait-ready", help=argparse.SUPPRESS)
125
+ p.add_argument("--workspace", default=".")
126
+ p.add_argument("--timeout", type=int, default=120)
127
+ add_json(p)
128
+ p.set_defaults(func=cmd_wait_ready)
129
+
130
+ p = sub.add_parser("settle", help=argparse.SUPPRESS)
131
+ p.add_argument("--workspace", default=".")
132
+ add_json(p)
133
+ p.set_defaults(func=cmd_settle)
134
+
135
+ p = sub.add_parser("status", help="Show team runtime status")
136
+ p.add_argument("agent", nargs="?")
137
+ p.add_argument("--workspace", default=".")
138
+ add_json(p)
139
+ p.set_defaults(func=cmd_status)
140
+
141
+ p = sub.add_parser("approvals", help="Show structured pending worker approval prompts")
142
+ p.add_argument("agent", nargs="?")
143
+ p.add_argument("--workspace", default=".")
144
+ add_json(p)
145
+ p.set_defaults(func=cmd_approvals)
146
+
147
+ p = sub.add_parser("peek", help=argparse.SUPPRESS, description="Explicit raw-screen diagnostic only")
148
+ p.add_argument("agent")
149
+ p.add_argument("--workspace", default=".")
150
+ p.add_argument(
151
+ "--allow-raw-screen",
152
+ action="store_true",
153
+ help="Required after explicit user authorization to capture worker terminal output",
154
+ )
155
+ mode = p.add_mutually_exclusive_group(required=True)
156
+ mode.add_argument("--head", type=int, help="Show the first N lines from the bounded recent capture")
157
+ mode.add_argument("--tail", type=int, help="Show the last N lines")
158
+ mode.add_argument("--search", help="Search the bounded recent capture and show matching context only")
159
+ p.add_argument("--context", type=int, default=3, help="Context lines around --search matches, max 10")
160
+ add_json(p)
161
+ p.set_defaults(func=cmd_peek)
162
+
163
+ p = sub.add_parser("inbox", help="Show message history for one agent")
164
+ p.add_argument("agent")
165
+ p.add_argument("--workspace", default=".")
166
+ p.add_argument("--limit", type=int, default=20)
167
+ add_json(p)
168
+ p.set_defaults(func=cmd_inbox)
169
+
170
+ p = sub.add_parser("sessions", help=argparse.SUPPRESS)
171
+ p.add_argument("--workspace", default=".")
172
+ add_json(p)
173
+ p.set_defaults(func=cmd_sessions)
174
+
175
+ p = sub.add_parser("attach-leader", help=argparse.SUPPRESS)
176
+ p.add_argument("--workspace", default=".")
177
+ p.add_argument("--pane", help="Explicit tmux pane id or target, for example %%173")
178
+ p.add_argument("--provider", default="codex")
179
+ add_json(p)
180
+ p.set_defaults(func=cmd_attach_leader)
181
+
182
+ p = sub.add_parser(
183
+ "send",
184
+ help="Send a message to an agent, task assignee, or attached leader",
185
+ epilog=(
186
+ "Canonical examples:\n"
187
+ " team-agent send --task <task_id> --json \"<message>\"\n"
188
+ " team-agent send --no-ack --json <agent_id> \"<message>\""
189
+ ),
190
+ formatter_class=argparse.RawDescriptionHelpFormatter,
191
+ )
192
+ p.send_order_hint = True
193
+ p.add_argument("target", nargs="?")
194
+ p.add_argument("message", nargs="+")
195
+ p.add_argument("--workspace", default=".")
196
+ p.add_argument("--task")
197
+ p.add_argument("--from", dest="sender", default="leader")
198
+ p.add_argument("--no-ack", action="store_true")
199
+ p.add_argument("--no-wait", action="store_true", help="Return after injection without visible verification")
200
+ p.add_argument(
201
+ "--watch-result",
202
+ action="store_true",
203
+ help="Return after delivery and let the coordinator collect/report the task result asynchronously",
204
+ )
205
+ p.add_argument("--timeout", type=float, default=30.0)
206
+ p.add_argument("--confirm-human", action="store_true", help="Confirm dispatch for a task marked human_confirmation: true")
207
+ add_json(p)
208
+ p.set_defaults(func=cmd_send)
209
+
210
+ p = sub.add_parser("collect", help=argparse.SUPPRESS)
211
+ p.add_argument("--workspace", default=".")
212
+ p.add_argument("--result-file")
213
+ add_json(p)
214
+ p.set_defaults(func=cmd_collect)
215
+
216
+ p = sub.add_parser("diagnose", help=argparse.SUPPRESS)
217
+ p.add_argument("--workspace", default=".")
218
+ add_json(p)
219
+ p.set_defaults(func=cmd_diagnose)
220
+
221
+ p = sub.add_parser("repair-state", help=argparse.SUPPRESS)
222
+ p.add_argument("--workspace", default=".")
223
+ p.add_argument("--task", required=True)
224
+ p.add_argument("--assignee")
225
+ p.add_argument("--status")
226
+ p.add_argument("--summary")
227
+ add_json(p)
228
+ p.set_defaults(func=cmd_repair_state)
229
+
230
+ p = sub.add_parser("validate-result", help=argparse.SUPPRESS)
231
+ p.add_argument("result", nargs="?", help="JSON string. If omitted, read stdin.")
232
+ p.add_argument("--file", help="Read JSON envelope from a file")
233
+ add_json(p)
234
+ p.set_defaults(func=cmd_validate_result)
235
+
236
+ p = sub.add_parser("doctor", help="Check local dependencies, providers, auth hints, tmux, and MCP")
237
+ p.add_argument("spec", nargs="?")
238
+ add_json(p)
239
+ p.set_defaults(func=cmd_doctor)
240
+
241
+ p = sub.add_parser("shutdown", help="Shutdown team tmux session and keep logs")
242
+ p.add_argument("--workspace", default=".")
243
+ p.add_argument("--keep-logs", action="store_true", default=True)
244
+ add_json(p)
245
+ p.set_defaults(func=cmd_shutdown)
246
+
247
+ p = sub.add_parser("restart", help="Restart a stopped team from stored worker sessions")
248
+ p.add_argument("workspace", nargs="?", default=".")
249
+ p.add_argument("--team", help="Restart a specific stored team/session when the workspace has multiple teams")
250
+ p.add_argument("--allow-fresh", action="store_true", help="Allow fresh worker sessions if stored sessions cannot resume")
251
+ add_json(p)
252
+ p.set_defaults(func=cmd_restart)
253
+
254
+ p = sub.add_parser("start-agent", help="Start or repair one worker in the current team")
255
+ p.add_argument("agent")
256
+ p.add_argument("--workspace", default=".")
257
+ p.add_argument("--force", action="store_true", help="Replace an existing tmux window for this worker")
258
+ p.add_argument("--allow-fresh", action="store_true", help="Allow a fresh session if the stored session cannot resume")
259
+ p.add_argument("--no-display", action="store_true", help="Do not open a Ghostty display window")
260
+ add_json(p)
261
+ p.set_defaults(func=cmd_start_agent)
262
+
263
+ p = sub.add_parser("install-skill", help=argparse.SUPPRESS)
264
+ p.add_argument("--target", choices=["codex", "claude", "all"], default="codex")
265
+ p.add_argument("--dest", help="Explicit destination directory; overrides --target")
266
+ p.add_argument("--dry-run", action="store_true")
267
+ add_json(p)
268
+ p.set_defaults(func=cmd_install_skill)
269
+
270
+ p = sub.add_parser("e2e", help=argparse.SUPPRESS)
271
+ p.add_argument("--providers", default="fake")
272
+ p.add_argument("--workspace")
273
+ p.add_argument("--real", action="store_true", help="Launch real provider CLIs; may use authenticated accounts")
274
+ add_json(p)
275
+ p.set_defaults(func=cmd_e2e)
276
+
277
+ p = sub.add_parser("allow-peer-talk", help=argparse.SUPPRESS)
278
+ p.add_argument("agent_a")
279
+ p.add_argument("agent_b")
280
+ p.add_argument("--workspace", default=".")
281
+ add_json(p)
282
+ p.set_defaults(func=cmd_allow_peer_talk)
283
+
284
+ p = sub.add_parser(
285
+ "advanced",
286
+ help=argparse.SUPPRESS,
287
+ description="Low-level Team Agent commands",
288
+ epilog=(
289
+ "Commands: init validate compile profile launch preflight start wait-ready settle "
290
+ "sessions attach-leader collect diagnose repair-state validate-result install-skill e2e"
291
+ ),
292
+ formatter_class=argparse.RawDescriptionHelpFormatter,
293
+ )
294
+ p.set_defaults(func=cmd_advanced)
295
+
296
+ sub._choices_actions = [ # type: ignore[attr-defined]
297
+ action for action in sub._choices_actions if action.help != argparse.SUPPRESS # type: ignore[attr-defined]
298
+ ]
299
+ sub.metavar = "{codex,claude,quick-start,send,status,approvals,inbox,shutdown,restart,start-agent,doctor}"
300
+
301
+ args = parser.parse_args(raw_argv)
302
+ try:
303
+ result = args.func(args)
304
+ except TeamAgentError as exc:
305
+ _emit_cli_error(exc, args)
306
+ raise SystemExit(1)
307
+ except Exception as exc:
308
+ _emit_cli_error(exc, args)
309
+ raise SystemExit(1)
310
+ emit(result, getattr(args, "json", False))
311
+ if isinstance(result, dict) and result.get("ok") is False:
312
+ raise SystemExit(1)
313
+
314
+
315
+ def add_json(parser: argparse.ArgumentParser) -> None:
316
+ parser.add_argument("--json", action="store_true", help="Emit stable machine-readable JSON")
317
+
318
+
319
+ def _run_leader_passthrough(command: str, provider_args: list[str]) -> None:
320
+ if provider_args in (["-h"], ["--help"]):
321
+ print(f"usage: team-agent {command} [args passed to {command}]")
322
+ print()
323
+ print(f"Start a tmux-managed {command} leader in the current directory.")
324
+ print(f"Use `team-agent {command} -- --help` to pass --help to the provider CLI.")
325
+ return
326
+ args = argparse.Namespace(command=command, workspace=".")
327
+ try:
328
+ provider = "codex" if command == "codex" else "claude_code"
329
+ runtime.start_leader(provider, _provider_args(provider_args), Path.cwd().resolve())
330
+ except TeamAgentError as exc:
331
+ _emit_cli_error(exc, args)
332
+ raise SystemExit(1)
333
+ except Exception as exc:
334
+ _emit_cli_error(exc, args)
335
+ raise SystemExit(1)
336
+
337
+
338
+ def emit(result: Any, as_json: bool) -> None:
339
+ if as_json:
340
+ print(json.dumps(result, indent=2, ensure_ascii=False, sort_keys=True))
341
+ return
342
+ if isinstance(result, dict):
343
+ for key, value in result.items():
344
+ if isinstance(value, (dict, list)):
345
+ print(f"{key}: {json.dumps(value, ensure_ascii=False)}")
346
+ else:
347
+ print(f"{key}: {value}")
348
+ else:
349
+ print(result)
350
+
351
+
352
+ def _workspace_from_args(args: argparse.Namespace) -> Path:
353
+ return Path(getattr(args, "workspace", ".")).resolve()
354
+
355
+
356
+ def _emit_cli_error(exc: Exception, args: argparse.Namespace) -> None:
357
+ workspace = _workspace_from_args(args)
358
+ log_dir = workspace / ".team" / "logs"
359
+ try:
360
+ log_dir.mkdir(parents=True, exist_ok=True)
361
+ except OSError:
362
+ log_dir = Path.cwd()
363
+ log_path = log_dir / f"cli-error-{int(time.time())}.log"
364
+ log_path.write_text("".join(traceback.format_exception(type(exc), exc, exc.__traceback__)), encoding="utf-8")
365
+ payload = _cli_error_payload(exc, args, log_path)
366
+ if getattr(args, "json", False):
367
+ print(json.dumps(payload, ensure_ascii=False))
368
+ return
369
+ print(f"error: {payload['error']}", file=sys.stderr)
370
+ print(f"action: {payload['action']}", file=sys.stderr)
371
+ print(f"log: {payload['log']}", file=sys.stderr)
372
+
373
+
374
+ def _cli_error_payload(exc: Exception, args: argparse.Namespace, log_path: Path) -> dict[str, Any]:
375
+ error = str(exc)
376
+ payload = {
377
+ "ok": False,
378
+ "error": error,
379
+ "action": "run `team-agent doctor` or inspect the log path shown here",
380
+ "log": str(log_path),
381
+ }
382
+ session_name = _tmux_session_conflict_name(error)
383
+ if session_name:
384
+ payload.update(
385
+ {
386
+ "reason": "tmux_session_name_conflict",
387
+ "session_name": session_name,
388
+ "action": _tmux_session_conflict_action(session_name, getattr(args, "command", "")),
389
+ "next_actions": [_tmux_session_conflict_next_action(getattr(args, "command", ""))],
390
+ }
391
+ )
392
+ return payload
393
+
394
+
395
+ def _tmux_session_conflict_name(error: str) -> str | None:
396
+ marker = "tmux session already exists:"
397
+ if marker not in error:
398
+ return None
399
+ name = error.split(marker, 1)[1].strip()
400
+ name = name.split(";", 1)[0].splitlines()[0].strip()
401
+ if ". Startup" in name:
402
+ name = name.split(". Startup", 1)[0].strip()
403
+ name = name.rstrip(".").strip()
404
+ return name or None
405
+
406
+
407
+ def _tmux_session_conflict_next_action(command: str) -> str:
408
+ if command == "quick-start":
409
+ return "Change `name:` in TEAM.md and run `team-agent quick-start` again."
410
+ return "Use a different team name or runtime.session_name before starting again."
411
+
412
+
413
+ def _tmux_session_conflict_action(session_name: str, command: str) -> str:
414
+ if command == "quick-start":
415
+ return (
416
+ f"tmux session `{session_name}` already exists. It may be an active team. "
417
+ "Do not terminate existing tmux sessions from quick-start; "
418
+ "change `name:` in TEAM.md and run quick-start again."
419
+ )
420
+ return (
421
+ f"tmux session `{session_name}` already exists. It may be an active team. "
422
+ "Do not terminate existing tmux sessions from startup; "
423
+ "use a different team name or runtime.session_name and start again."
424
+ )
425
+
426
+
427
+ def cmd_quick_start(args: argparse.Namespace) -> dict[str, Any]:
428
+ result = runtime.quick_start(Path(args.agents_dir), name=args.name, yes=args.yes, fresh=args.fresh, team_id=args.team_id)
429
+ if args.json or not result.get("ok"):
430
+ return result
431
+ return result["summary"]
432
+
433
+
434
+ def cmd_codex(args: argparse.Namespace) -> None:
435
+ runtime.start_leader("codex", _provider_args(args.provider_args), Path.cwd().resolve())
436
+
437
+
438
+ def cmd_claude(args: argparse.Namespace) -> None:
439
+ runtime.start_leader("claude_code", _provider_args(args.provider_args), Path.cwd().resolve())
440
+
441
+
442
+ def _provider_args(values: list[str]) -> list[str]:
443
+ if values and values[0] == "--":
444
+ return values[1:]
445
+ return values
446
+
447
+
448
+ def cmd_init(args: argparse.Namespace) -> dict[str, Any]:
449
+ paths = runtime.init_workspace(Path(args.workspace).resolve(), force=args.force)
450
+ return {"ok": True, "spec": str(paths["spec"]), "state": str(paths["state"])}
451
+
452
+
453
+ def cmd_validate(args: argparse.Namespace) -> dict[str, Any]:
454
+ return runtime.validate_file(Path(args.spec).resolve())
455
+
456
+
457
+ def cmd_compile(args: argparse.Namespace) -> dict[str, Any]:
458
+ result = compiler.compile_team(Path(args.team).resolve(), Path(args.out).resolve())
459
+ return {"ok": True, "team_dir": result["team_dir"], "out": result["out"], "agents": [a["id"] for a in result["spec"]["agents"]]}
460
+
461
+
462
+ def _profile_scope(args: argparse.Namespace) -> tuple[Path, Path | None]:
463
+ team = getattr(args, "team", None)
464
+ if team:
465
+ team_dir = Path(team).resolve()
466
+ return team_workspace(team_dir), team_dir / "profiles"
467
+ return Path(args.workspace).resolve(), None
468
+
469
+
470
+ def cmd_profile_init(args: argparse.Namespace) -> dict[str, Any]:
471
+ workspace, profiles_dir = _profile_scope(args)
472
+ return profiles.init_profile(workspace, args.name, args.auth_mode, profiles_dir=profiles_dir)
473
+
474
+
475
+ def cmd_profile_doctor(args: argparse.Namespace) -> dict[str, Any]:
476
+ workspace, profiles_dir = _profile_scope(args)
477
+ return profiles.doctor_profile(workspace, args.name, profiles_dir=profiles_dir)
478
+
479
+
480
+ def cmd_profile_show(args: argparse.Namespace) -> dict[str, Any]:
481
+ workspace, profiles_dir = _profile_scope(args)
482
+ return profiles.show_profile(workspace, args.name, profiles_dir=profiles_dir)
483
+
484
+
485
+ def cmd_launch(args: argparse.Namespace) -> dict[str, Any]:
486
+ return runtime.launch(Path(args.spec).resolve(), dry_run=args.dry_run, auto_approve=args.yes)
487
+
488
+
489
+ def cmd_preflight(args: argparse.Namespace) -> dict[str, Any]:
490
+ return runtime.preflight(Path(args.team).resolve())
491
+
492
+
493
+ def cmd_start(args: argparse.Namespace) -> dict[str, Any]:
494
+ return runtime.start(Path(args.team).resolve(), yes=args.yes)
495
+
496
+
497
+ def cmd_wait_ready(args: argparse.Namespace) -> dict[str, Any]:
498
+ return runtime.wait_ready(Path(args.workspace).resolve(), timeout=args.timeout)
499
+
500
+
501
+ def cmd_settle(args: argparse.Namespace) -> dict[str, Any]:
502
+ return runtime.settle(Path(args.workspace).resolve())
503
+
504
+
505
+ def cmd_status(args: argparse.Namespace) -> dict[str, Any]:
506
+ if args.json:
507
+ return runtime.status(Path(args.workspace).resolve(), as_json=True)
508
+ return runtime.format_status(Path(args.workspace).resolve(), args.agent)
509
+
510
+
511
+ def cmd_approvals(args: argparse.Namespace) -> dict[str, Any]:
512
+ if args.json:
513
+ return runtime.approvals(Path(args.workspace).resolve(), agent_id=args.agent)
514
+ return runtime.format_approvals(Path(args.workspace).resolve(), agent_id=args.agent)
515
+
516
+
517
+ def cmd_peek(args: argparse.Namespace) -> dict[str, Any]:
518
+ if not args.allow_raw_screen:
519
+ raise TeamAgentError(
520
+ "raw worker terminal inspection requires explicit user authorization and --allow-raw-screen; "
521
+ "normal operation must use status, approvals, inbox, collect, or event logs"
522
+ )
523
+ result = runtime.peek(
524
+ Path(args.workspace).resolve(),
525
+ args.agent,
526
+ head=args.head,
527
+ tail=args.tail,
528
+ search=args.search,
529
+ context=args.context,
530
+ )
531
+ if args.json:
532
+ return result
533
+ return result["text"]
534
+
535
+
536
+ def cmd_inbox(args: argparse.Namespace) -> dict[str, Any]:
537
+ if args.json:
538
+ return runtime.inbox(Path(args.workspace).resolve(), args.agent, limit=args.limit)
539
+ return runtime.format_inbox(Path(args.workspace).resolve(), args.agent, limit=args.limit)
540
+
541
+
542
+ def cmd_sessions(args: argparse.Namespace) -> dict[str, Any]:
543
+ return runtime.sessions(Path(args.workspace).resolve())
544
+
545
+
546
+ def cmd_attach_leader(args: argparse.Namespace) -> dict[str, Any]:
547
+ return runtime.attach_leader(Path(args.workspace).resolve(), pane=args.pane, provider=args.provider)
548
+
549
+
550
+ def cmd_send(args: argparse.Namespace) -> dict[str, Any]:
551
+ return runtime.send_message(
552
+ Path(args.workspace).resolve(),
553
+ args.target,
554
+ " ".join(args.message),
555
+ task_id=args.task,
556
+ sender=args.sender,
557
+ requires_ack=not args.no_ack,
558
+ confirm_human=args.confirm_human,
559
+ wait_visible=not args.no_wait,
560
+ timeout=args.timeout,
561
+ watch_result=args.watch_result,
562
+ )
563
+
564
+
565
+ def cmd_collect(args: argparse.Namespace) -> dict[str, Any]:
566
+ result_file = Path(args.result_file).resolve() if args.result_file else None
567
+ return runtime.collect(Path(args.workspace).resolve(), result_file=result_file)
568
+
569
+
570
+ def cmd_diagnose(args: argparse.Namespace) -> dict[str, Any]:
571
+ return runtime.diagnose(Path(args.workspace).resolve())
572
+
573
+
574
+ def cmd_repair_state(args: argparse.Namespace) -> dict[str, Any]:
575
+ return runtime.repair_state(
576
+ Path(args.workspace).resolve(),
577
+ task_id=args.task,
578
+ assignee=args.assignee,
579
+ status_value=args.status,
580
+ summary=args.summary,
581
+ )
582
+
583
+
584
+ def cmd_validate_result(args: argparse.Namespace) -> dict[str, Any]:
585
+ if args.file:
586
+ raw = Path(args.file).read_text(encoding="utf-8")
587
+ elif args.result:
588
+ raw = args.result
589
+ else:
590
+ raw = sys.stdin.read()
591
+ envelope = json.loads(raw)
592
+ validate_result_envelope(envelope)
593
+ return {"ok": True, "task_id": envelope["task_id"], "agent_id": envelope["agent_id"], "status": envelope["status"]}
594
+
595
+
596
+ def cmd_doctor(args: argparse.Namespace) -> dict[str, Any]:
597
+ spec = Path(args.spec).resolve() if args.spec else None
598
+ return runtime.doctor(spec)
599
+
600
+
601
+ def cmd_shutdown(args: argparse.Namespace) -> dict[str, Any]:
602
+ return runtime.shutdown(Path(args.workspace).resolve(), keep_logs=args.keep_logs)
603
+
604
+
605
+ def cmd_restart(args: argparse.Namespace) -> dict[str, Any]:
606
+ return runtime.restart(Path(args.workspace).resolve(), allow_fresh=args.allow_fresh, team=args.team)
607
+
608
+
609
+ def cmd_start_agent(args: argparse.Namespace) -> dict[str, Any]:
610
+ return runtime.start_agent(
611
+ Path(args.workspace).resolve(),
612
+ args.agent,
613
+ force=args.force,
614
+ open_display=not args.no_display,
615
+ allow_fresh=args.allow_fresh,
616
+ )
617
+
618
+
619
+ def cmd_allow_peer_talk(args: argparse.Namespace) -> dict[str, Any]:
620
+ return runtime.allow_peer_talk(Path(args.workspace).resolve(), args.agent_a, args.agent_b)
621
+
622
+
623
+ def cmd_advanced(args: argparse.Namespace) -> str:
624
+ return "\n".join(
625
+ [
626
+ "Low-level commands:",
627
+ " init validate compile profile launch preflight start wait-ready settle",
628
+ " sessions attach-leader collect diagnose repair-state validate-result",
629
+ " install-skill e2e",
630
+ ]
631
+ )
632
+
633
+
634
+ def cmd_install_skill(args: argparse.Namespace) -> dict[str, Any]:
635
+ source = repo_root() / "skills" / "team-agent"
636
+ if args.dest and args.target == "all":
637
+ raise TeamAgentError("--dest cannot be combined with --target all")
638
+ if args.dest:
639
+ dest_dir = Path(args.dest).expanduser().resolve()
640
+ return _install_skill_to(source, dest_dir, args.dry_run)
641
+ if args.target == "all":
642
+ results = [
643
+ _install_skill_to(source, _skill_dest_dir("codex"), args.dry_run),
644
+ _install_skill_to(source, _skill_dest_dir("claude"), args.dry_run),
645
+ ]
646
+ return {"ok": all(item["ok"] for item in results), "targets": results}
647
+ return _install_skill_to(source, _skill_dest_dir(args.target), args.dry_run)
648
+
649
+
650
+ def _skill_dest_dir(target: str) -> Path:
651
+ if target == "claude":
652
+ dest_dir = Path.home() / ".claude" / "skills" / "team-agent"
653
+ else:
654
+ dest_dir = Path.home() / ".codex" / "skills" / "team-agent"
655
+ return dest_dir
656
+
657
+
658
+ def _install_skill_to(source: Path, dest_dir: Path, dry_run: bool) -> dict[str, Any]:
659
+ dest = dest_dir / "SKILL.md"
660
+ if dry_run:
661
+ return {"ok": True, "source": str(source / "SKILL.md"), "dest": str(dest), "dry_run": True}
662
+ dest_dir.mkdir(parents=True, exist_ok=True)
663
+ shutil.copytree(source, dest_dir, dirs_exist_ok=True)
664
+ return {"ok": True, "source": str(source / "SKILL.md"), "dest": str(dest)}
665
+
666
+
667
+ def cmd_e2e(args: argparse.Namespace) -> dict[str, Any]:
668
+ providers = [item.strip() for item in args.providers.split(",") if item.strip()]
669
+ workspace = Path(args.workspace).resolve() if args.workspace else Path(tempfile.mkdtemp(prefix="team-agent-e2e-"))
670
+ workspace.mkdir(parents=True, exist_ok=True)
671
+ results: dict[str, Any] = {"workspace": str(workspace), "providers": {}, "ok": True}
672
+ if "fake" in providers:
673
+ spec_path = workspace / "team.spec.yaml"
674
+ spec_path.write_text(dumps(_fake_spec(workspace)), encoding="utf-8")
675
+ results["providers"]["fake"] = _run_fake_e2e(spec_path, workspace)
676
+ results["ok"] = results["ok"] and results["providers"]["fake"]["ok"]
677
+ for provider in [p for p in providers if p != "fake"]:
678
+ from team_agent.providers import get_adapter
679
+
680
+ adapter = get_adapter(provider)
681
+ installed = adapter.is_installed()
682
+ if not installed:
683
+ provider_result = {
684
+ "ok": False,
685
+ "skipped": True,
686
+ "reason": f"{adapter.command_name} not installed",
687
+ "version": None,
688
+ }
689
+ elif not args.real:
690
+ provider_result = {
691
+ "ok": False,
692
+ "skipped": True,
693
+ "reason": "real provider launch disabled; rerun with --real on an authenticated machine",
694
+ "version": adapter.version(),
695
+ }
696
+ else:
697
+ provider_result = _run_real_launch_smoke(provider, workspace)
698
+ results["providers"][provider] = provider_result
699
+ results["ok"] = results["ok"] and provider_result["ok"]
700
+ return results
701
+
702
+
703
+ def _run_fake_e2e(spec_path: Path, workspace: Path) -> dict[str, Any]:
704
+ launched = runtime.launch(spec_path, auto_approve=True)
705
+ sent = runtime.send_message(workspace, None, "implement fake task", task_id="task_impl", requires_ack=True)
706
+ import time
707
+
708
+ time.sleep(1.0)
709
+ collected = runtime.collect(workspace)
710
+ stopped = runtime.shutdown(workspace)
711
+ return {"ok": bool(launched["ok"] and sent["ok"] and collected["collected"] and stopped["ok"]), "launch": launched, "send": sent, "collect": collected, "shutdown": stopped}
712
+
713
+
714
+ def _run_real_launch_smoke(provider: str, workspace: Path) -> dict[str, Any]:
715
+ spec_path = workspace / f"team.{provider}.spec.yaml"
716
+ spec = _fake_spec(workspace)
717
+ spec["team"]["name"] = f"real-{provider}-smoke"
718
+ spec["leader"]["provider"] = provider
719
+ spec["agents"][0]["provider"] = provider
720
+ spec["agents"][0]["id"] = f"{provider}_smoke"
721
+ spec["agents"][0]["tools"] = ["fs_read", "fs_list", "git_diff", "mcp_team", "provider_builtin"]
722
+ spec["agents"][0]["role"] = "reviewer"
723
+ spec["agents"][0]["system_prompt"]["inline"] = (
724
+ "Real provider smoke. Do not edit files or run shell. "
725
+ "Do not call team-agent launch and do not create a nested Team Agent team. "
726
+ "When asked, call team_orchestrator.report_result exactly once with result_envelope_v1."
727
+ )
728
+ spec["routing"]["rules"][0]["assign_to"] = spec["agents"][0]["id"]
729
+ spec["runtime"]["session_name"] = f"team-agent-real-{provider}"
730
+ spec["runtime"]["startup_order"] = [spec["agents"][0]["id"]]
731
+ spec["tasks"][0]["id"] = f"task_real_{provider}_callback"
732
+ spec["tasks"][0]["title"] = f"Real {provider} callback smoke"
733
+ spec["tasks"][0]["assignee"] = spec["agents"][0]["id"]
734
+ spec["tasks"][0]["requires_tools"] = ["fs_read", "git_diff"]
735
+ spec["tasks"][0]["type"] = "review"
736
+ spec_path.write_text(dumps(spec), encoding="utf-8")
737
+ launched = runtime.launch(spec_path, auto_approve=True)
738
+ import time
739
+
740
+ time.sleep(10.0 if provider == "codex" else 3.0)
741
+ collected = None
742
+ sent = None
743
+ if provider == "codex":
744
+ task_id = spec["tasks"][0]["id"]
745
+ agent_id = spec["agents"][0]["id"]
746
+ message = (
747
+ "Do not call team-agent launch and do not create a nested Team Agent team. "
748
+ "Do not edit files or run shell. "
749
+ "Call team_orchestrator.report_result with envelope "
750
+ f'{{"schema_version":"result_envelope_v1","task_id":"{task_id}",'
751
+ f'"agent_id":"{agent_id}","status":"success","summary":"ok",'
752
+ '"changes":[],"tests":[{"command":"real-codex-callback-smoke","status":"passed"}],'
753
+ '"risks":[],"artifacts":[],"next_actions":[]}. Do not edit files or run shell.'
754
+ )
755
+ sent = runtime.send_message(workspace, agent_id, message, task_id=task_id, requires_ack=True)
756
+ for _ in range(24):
757
+ time.sleep(5.0)
758
+ result = runtime.collect(workspace)
759
+ if result["collected"]:
760
+ collected = result
761
+ break
762
+ status = runtime.status(workspace, as_json=True)
763
+ stopped = runtime.shutdown(workspace)
764
+ agent_id = spec["agents"][0]["id"]
765
+ agent_status = status["agents"].get(agent_id, {})
766
+ callback_ok = provider != "codex" or bool(collected and collected["collected"])
767
+ return {
768
+ "ok": bool(launched["ok"] and stopped["ok"] and agent_status.get("tmux_window_present") and callback_ok),
769
+ "launch": launched,
770
+ "send": sent,
771
+ "collect": collected,
772
+ "status": status,
773
+ "shutdown": stopped,
774
+ }
775
+
776
+
777
+ def _fake_spec(workspace: Path) -> dict[str, Any]:
778
+ return {
779
+ "version": 1,
780
+ "team": {
781
+ "name": "fake-e2e",
782
+ "mode": "supervisor_worker",
783
+ "objective": "Exercise fake provider orchestration.",
784
+ "workspace": str(workspace),
785
+ },
786
+ "leader": {
787
+ "id": "leader",
788
+ "role": "leader",
789
+ "provider": "fake",
790
+ "model": None,
791
+ "tools": ["fs_read", "fs_list", "mcp_team"],
792
+ "context_policy": {
793
+ "keep_user_thread": True,
794
+ "receive_worker_outputs": "structured_only",
795
+ "max_worker_result_tokens": 2000,
796
+ },
797
+ },
798
+ "agents": [
799
+ {
800
+ "id": "fake_impl",
801
+ "role": "implementation_engineer",
802
+ "provider": "fake",
803
+ "model": None,
804
+ "working_directory": str(workspace),
805
+ "system_prompt": {"inline": "Handle fake implementation tasks.", "file": None},
806
+ "tools": ["fs_read", "fs_write", "fs_list", "execute_bash", "git_diff", "mcp_team", "provider_builtin"],
807
+ "permission_mode": "restricted",
808
+ "preferred_for": ["implementation"],
809
+ "avoid_for": [],
810
+ "output_contract": {"format": "result_envelope_v1", "required_fields": ["task_id", "status", "summary", "artifacts"]},
811
+ }
812
+ ],
813
+ "routing": {
814
+ "default_assignee": "leader",
815
+ "rules": [{"id": "implementation-to-fake", "match": {"type": ["implementation"]}, "assign_to": "fake_impl", "priority": 10}],
816
+ },
817
+ "communication": {
818
+ "protocol": "mcp_inbox",
819
+ "topology": "leader_centered",
820
+ "worker_to_worker": True,
821
+ "ack_timeout_sec": 2,
822
+ "result_format": "result_envelope_v1",
823
+ "message_store": {"sqlite": ".team/runtime/team.db", "mirror_files": ".team/messages"},
824
+ },
825
+ "runtime": {
826
+ "backend": "tmux",
827
+ "display_backend": "none",
828
+ "session_name": "team-agent-fake-e2e",
829
+ "auto_launch": True,
830
+ "require_user_approval_before_launch": False,
831
+ "max_active_agents": 1,
832
+ "startup_order": ["fake_impl"],
833
+ },
834
+ "context": {
835
+ "state_file": "team_state.md",
836
+ "artifact_dir": ".team/artifacts",
837
+ "log_dir": ".team/logs",
838
+ "summarization": {
839
+ "worker_full_logs": "retain_outside_leader_context",
840
+ "state_update": "after_each_result",
841
+ },
842
+ },
843
+ "tasks": [
844
+ {
845
+ "id": "task_impl",
846
+ "title": "Fake implementation",
847
+ "type": "implementation",
848
+ "assignee": None,
849
+ "deps": [],
850
+ "acceptance": ["fake result collected"],
851
+ "status": "pending",
852
+ "requires_tools": ["fs_write", "execute_bash"],
853
+ "files": ["src/example.py"],
854
+ "risk": "low",
855
+ }
856
+ ],
857
+ }