@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.
- package/README.md +201 -0
- package/crates/team-agent-core/Cargo.toml +12 -0
- package/crates/team-agent-core/src/lib.rs +287 -0
- package/crates/team-agent-core/src/main.rs +152 -0
- package/examples/team.spec.yaml +206 -0
- package/examples/team_state.md +35 -0
- package/npm/install.mjs +266 -0
- package/package.json +28 -0
- package/pyproject.toml +18 -0
- package/schemas/result-envelope.schema.json +76 -0
- package/schemas/team.schema.json +241 -0
- package/scripts/install.py +88 -0
- package/scripts/run_regression_tests.py +79 -0
- package/skills/team-agent/SKILL.md +173 -0
- package/src/team_agent/__init__.py +3 -0
- package/src/team_agent/__main__.py +5 -0
- package/src/team_agent/cli.py +857 -0
- package/src/team_agent/compiler.py +269 -0
- package/src/team_agent/coordinator.py +62 -0
- package/src/team_agent/errors.py +10 -0
- package/src/team_agent/events.py +37 -0
- package/src/team_agent/fake_worker.py +80 -0
- package/src/team_agent/mcp_server.py +579 -0
- package/src/team_agent/message_store.py +497 -0
- package/src/team_agent/paths.py +45 -0
- package/src/team_agent/permissions.py +123 -0
- package/src/team_agent/profiles.py +882 -0
- package/src/team_agent/providers.py +1045 -0
- package/src/team_agent/routing.py +84 -0
- package/src/team_agent/runtime.py +5213 -0
- package/src/team_agent/rust_core.py +156 -0
- package/src/team_agent/simple_yaml.py +236 -0
- package/src/team_agent/spec.py +308 -0
- package/src/team_agent/state.py +112 -0
- package/src/team_agent/task_graph.py +80 -0
- 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
|
+
}
|