@team-agent/installer 0.5.109 → 0.5.111
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/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +33 -0
- package/README.zh.md +30 -0
- package/crates/team-agent/src/cli/adapters.rs +234 -73
- package/crates/team-agent/src/cli/emit.rs +494 -201
- package/crates/team-agent/src/cli/leader.rs +4 -9
- package/crates/team-agent/src/cli/leaders.rs +90 -29
- package/crates/team-agent/src/cli/mod.rs +348 -161
- package/crates/team-agent/src/cli/models.rs +80 -24
- package/crates/team-agent/src/cli/named_address.rs +114 -25
- package/crates/team-agent/src/cli/route.rs +212 -0
- package/crates/team-agent/src/cli/send/presentation.rs +60 -41
- package/crates/team-agent/src/cli/send/resolve.rs +25 -18
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +56 -106
- package/crates/team-agent/src/cli/tests/base.rs +189 -82
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -2
- package/crates/team-agent/src/cli/tests/named_address.rs +19 -22
- package/crates/team-agent/src/cli/tests/status_send.rs +15 -10
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +2 -2
- package/crates/team-agent/src/cli/types.rs +22 -13
- package/crates/team-agent/src/leader/registry.rs +3 -3
- package/crates/team-agent/src/leader/start.rs +5 -1
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +2 -0
- package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +1457 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +4 -9
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -1
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/provider/argv_route.rs +409 -0
- package/crates/team-agent/src/provider/mod.rs +3 -2
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +32 -0
|
@@ -54,8 +54,20 @@ fn emit_with_json_order(
|
|
|
54
54
|
/// **行为入口**:契约可端到端跑 argv→(stdout, exit code)。
|
|
55
55
|
pub fn run(argv: &[String], cwd: &Path) -> ExitCode {
|
|
56
56
|
let Some(command) = argv.first().map(String::as_str) else {
|
|
57
|
-
|
|
57
|
+
println!("{}", default_help());
|
|
58
|
+
return ExitCode::Ok;
|
|
58
59
|
};
|
|
60
|
+
if is_machine_command(command)
|
|
61
|
+
&& argv[1..]
|
|
62
|
+
.iter()
|
|
63
|
+
.any(|arg| matches!(arg.as_str(), "-h" | "--help"))
|
|
64
|
+
{
|
|
65
|
+
eprintln!("{HUMAN_NAVIGATION}");
|
|
66
|
+
return ExitCode::Usage;
|
|
67
|
+
}
|
|
68
|
+
if command == "route" {
|
|
69
|
+
return super::route::run(&argv[1..]);
|
|
70
|
+
}
|
|
59
71
|
if is_leader_passthrough_command(command) {
|
|
60
72
|
return match cmd_leader_passthrough(command, &argv[1..], cwd) {
|
|
61
73
|
Ok(result) => emit_result(result),
|
|
@@ -98,7 +110,44 @@ fn emit_cli_error_for_command(
|
|
|
98
110
|
cwd: &Path,
|
|
99
111
|
error: &CliError,
|
|
100
112
|
) -> ExitCode {
|
|
101
|
-
if
|
|
113
|
+
let missing_input = matches!(error, CliError::Usage(message) if message.starts_with("missing ") || message.starts_with("请填写") || matches!(message.as_str(), "add-agent requires --provider <name>" | "add-agent requires --bypass <true|false>" | "--source is required unless --uninstall"));
|
|
114
|
+
if command_spec(command).is_some() && missing_input {
|
|
115
|
+
let help = command_help(Some(command));
|
|
116
|
+
let explanation = match error {
|
|
117
|
+
CliError::Usage(message) => match message.as_str() {
|
|
118
|
+
"missing agent" | "missing source_agent" => "请填写队友名。".to_string(),
|
|
119
|
+
"missing profile command" => {
|
|
120
|
+
"请填写 profile 操作:init、doctor 或 show。".to_string()
|
|
121
|
+
}
|
|
122
|
+
"missing profile name" => "请填写登录/代理配置的名称。".to_string(),
|
|
123
|
+
"add-agent requires --provider <name>" => {
|
|
124
|
+
"请用 --provider 明确选择工具,或用 --role-file 提供角色文件。".to_string()
|
|
125
|
+
}
|
|
126
|
+
"add-agent requires --bypass <true|false>" => {
|
|
127
|
+
"请用 --bypass false 明确保留权限询问,或在角色文件中明确此设置。".to_string()
|
|
128
|
+
}
|
|
129
|
+
other => other
|
|
130
|
+
.strip_prefix("missing ")
|
|
131
|
+
.map(|field| format!("请补齐必需参数:{field}。"))
|
|
132
|
+
.unwrap_or_else(|| other.to_string()),
|
|
133
|
+
},
|
|
134
|
+
_ => error.to_string(),
|
|
135
|
+
};
|
|
136
|
+
if has_arg(args, "--json") {
|
|
137
|
+
let payload = error.to_payload(Path::new(""), command);
|
|
138
|
+
let mut value = serde_json::to_value(payload)
|
|
139
|
+
.unwrap_or_else(|_| serde_json::json!({"ok": false, "error": error.to_string()}));
|
|
140
|
+
value["error"] = serde_json::json!(explanation);
|
|
141
|
+
value["action"] = serde_json::json!(format!("team-agent {command} --help"));
|
|
142
|
+
value["next_actions"] = serde_json::json!([format!(
|
|
143
|
+
"请按 team-agent {command} --help 的 Examples 补齐参数"
|
|
144
|
+
)]);
|
|
145
|
+
println!("{}", python_compact_json(&value));
|
|
146
|
+
} else {
|
|
147
|
+
eprintln!("{explanation}\n\n{help}");
|
|
148
|
+
}
|
|
149
|
+
ExitCode::Usage
|
|
150
|
+
} else if command == "status" {
|
|
102
151
|
emit_status_cli_error(error)
|
|
103
152
|
} else {
|
|
104
153
|
emit_cli_error(command, args, cwd, error)
|
|
@@ -115,7 +164,7 @@ fn emit_status_cli_error(error: &CliError) -> ExitCode {
|
|
|
115
164
|
|
|
116
165
|
/// Print a handler's CmdResult to stdout (emit formats json/human), then surface its exit code.
|
|
117
166
|
/// (parser.py: `print(emit(result, as_json))` then the ok→exit mapping.)
|
|
118
|
-
fn emit_result(r: CmdResult) -> ExitCode {
|
|
167
|
+
pub(super) fn emit_result(r: CmdResult) -> ExitCode {
|
|
119
168
|
let persisted_message_id = match &r.output {
|
|
120
169
|
CmdOutput::Json(value) => value
|
|
121
170
|
.get("message_id")
|
|
@@ -130,7 +179,7 @@ fn emit_result(r: CmdResult) -> ExitCode {
|
|
|
130
179
|
let mut stderr = stderr.lock();
|
|
131
180
|
let _ = writeln!(
|
|
132
181
|
stderr,
|
|
133
|
-
"
|
|
182
|
+
"输出暂不可用;消息已保存,消息编号={message_id}。用 team-agent inbox leader -n 3 查看回复,不要重发。"
|
|
134
183
|
);
|
|
135
184
|
}
|
|
136
185
|
return if error.kind() == ErrorKind::BrokenPipe {
|
|
@@ -160,6 +209,9 @@ pub(crate) fn __test_dispatch(
|
|
|
160
209
|
}
|
|
161
210
|
|
|
162
211
|
fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliError> {
|
|
212
|
+
if command == "route" {
|
|
213
|
+
return Ok(super::route::run(args));
|
|
214
|
+
}
|
|
163
215
|
// Keep the removed flag harmless for older scripts and persisted command
|
|
164
216
|
// lines; the default tmux backend is already the only runtime path.
|
|
165
217
|
let filtered_args: Vec<String> = args
|
|
@@ -168,6 +220,16 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
|
|
|
168
220
|
.cloned()
|
|
169
221
|
.collect();
|
|
170
222
|
let args = filtered_args.as_slice();
|
|
223
|
+
if is_machine_command(command) {
|
|
224
|
+
if args
|
|
225
|
+
.iter()
|
|
226
|
+
.any(|arg| matches!(arg.as_str(), "-h" | "--help"))
|
|
227
|
+
{
|
|
228
|
+
eprintln!("{HUMAN_NAVIGATION}");
|
|
229
|
+
return Ok(ExitCode::Usage);
|
|
230
|
+
}
|
|
231
|
+
return dispatch_machine(command, args, cwd);
|
|
232
|
+
}
|
|
171
233
|
let Some(spec) = command_spec(command) else {
|
|
172
234
|
return Ok(emit_unknown_subcommand_usage(command));
|
|
173
235
|
};
|
|
@@ -177,6 +239,26 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
|
|
|
177
239
|
return Ok(emit_unknown_subcommand_usage(command));
|
|
178
240
|
}
|
|
179
241
|
}
|
|
242
|
+
if args.iter().all(|arg| arg == "--json")
|
|
243
|
+
&& matches!(
|
|
244
|
+
command,
|
|
245
|
+
"send"
|
|
246
|
+
| "inbox"
|
|
247
|
+
| "add-agent"
|
|
248
|
+
| "start-agent"
|
|
249
|
+
| "stop-agent"
|
|
250
|
+
| "reset-agent"
|
|
251
|
+
| "clone-agent"
|
|
252
|
+
| "fork-agent"
|
|
253
|
+
| "remove-agent"
|
|
254
|
+
| "allow-peer-talk"
|
|
255
|
+
| "profile"
|
|
256
|
+
)
|
|
257
|
+
{
|
|
258
|
+
return Err(CliError::Usage(
|
|
259
|
+
"请填写队友名、任务内容或命令所需设置;见下方用法与 Examples".to_string(),
|
|
260
|
+
));
|
|
261
|
+
}
|
|
180
262
|
match command {
|
|
181
263
|
"quick-start" => cmd_quick_start(&quick_start_args(args, cwd)?).map(emit_result),
|
|
182
264
|
"send" => cmd_send(&send_args(args, cwd)?).map(emit_result),
|
|
@@ -198,16 +280,11 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
|
|
|
198
280
|
"claim-leader" => cmd_claim_leader(&claim_leader_args(args, cwd)).map(emit_result),
|
|
199
281
|
// Real dispatch: `cmd_attach_leader` writes the `leader_receiver` binding.
|
|
200
282
|
"attach-leader" => cmd_attach_leader(&attach_leader_args(args, cwd)?).map(emit_result),
|
|
201
|
-
|
|
202
|
-
cmd_attach_app_server_leader(&attach_app_server_leader_args(args, cwd)?)
|
|
203
|
-
.map(emit_result)
|
|
204
|
-
}
|
|
205
|
-
"identity" => cmd_identity(&identity_args(args, cwd)).map(emit_result),
|
|
283
|
+
|
|
206
284
|
"approvals" => cmd_approvals(&approvals_args(args, cwd)).map(emit_result),
|
|
207
285
|
"inbox" => cmd_inbox(&inbox_args(args, cwd)?).map(emit_result),
|
|
208
286
|
"doctor" => cmd_doctor(&doctor_args(args, cwd)).map(emit_result),
|
|
209
|
-
|
|
210
|
-
"sessions" => cmd_sessions(&sessions_args(args, cwd)).map(emit_result),
|
|
287
|
+
|
|
211
288
|
// 0.5.9 E7 host-leader-registry: `leaders` is the host-level derived
|
|
212
289
|
// discovery command. It reads ~/.team-agent/leaders, validates each
|
|
213
290
|
// entry against canonical state, and reports LIVE/STALE/AMBIGUOUS
|
|
@@ -217,16 +294,50 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
|
|
|
217
294
|
// delivery path — no separate route authority.
|
|
218
295
|
"leaders" => cmd_leaders(&leaders_args(args, cwd)?).map(emit_result),
|
|
219
296
|
"models" => cmd_models(&models_args(args)?).map(emit_result),
|
|
220
|
-
|
|
297
|
+
|
|
221
298
|
"install-skill" => cmd_install_skill(&install_skill_args(args)?).map(emit_result),
|
|
222
299
|
"profile" => cmd_profile(&profile_args(args, cwd)?).map(emit_result),
|
|
300
|
+
|
|
301
|
+
_ => Ok(emit_unknown_subcommand_usage(command)),
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Script compatibility is deliberately outside the human catalog and suggestions.
|
|
306
|
+
fn is_machine_command(command: &str) -> bool {
|
|
307
|
+
matches!(
|
|
308
|
+
command,
|
|
309
|
+
"results"
|
|
310
|
+
| "wait"
|
|
311
|
+
| "wait-ready"
|
|
312
|
+
| "preflight"
|
|
313
|
+
| "validate"
|
|
314
|
+
| "identity"
|
|
315
|
+
| "sessions"
|
|
316
|
+
| "watch"
|
|
317
|
+
| "e2e"
|
|
318
|
+
| "peek"
|
|
319
|
+
| "coordinator"
|
|
320
|
+
| "attach-app-server-leader"
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
fn dispatch_machine(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliError> {
|
|
325
|
+
match command {
|
|
223
326
|
"results" => cmd_results(&results_args(args, cwd)?).map(emit_result),
|
|
224
327
|
"wait" => cmd_wait(&wait_args(args, cwd)?).map(emit_result),
|
|
225
|
-
"preflight" => cmd_preflight(&preflight_args(args, cwd)).map(emit_result),
|
|
226
328
|
"wait-ready" => cmd_wait_ready(&wait_ready_args(args, cwd)).map(emit_result),
|
|
329
|
+
"preflight" => cmd_preflight(&preflight_args(args, cwd)).map(emit_result),
|
|
330
|
+
"validate" => cmd_validate(&validate_args(args, cwd)).map(emit_result),
|
|
331
|
+
"identity" => cmd_identity(&identity_args(args, cwd)).map(emit_result),
|
|
332
|
+
"sessions" => cmd_sessions(&sessions_args(args, cwd)).map(emit_result),
|
|
333
|
+
"watch" => cmd_watch(&watch_args(args, cwd)).map(emit_result),
|
|
227
334
|
"e2e" => cmd_e2e(&e2e_args(args, cwd)).map(emit_result),
|
|
228
335
|
"peek" => cmd_peek(&peek_args(args, cwd)?).map(emit_result),
|
|
229
336
|
"coordinator" => run_coordinator(args, cwd),
|
|
337
|
+
"attach-app-server-leader" => {
|
|
338
|
+
cmd_attach_app_server_leader(&attach_app_server_leader_args(args, cwd)?)
|
|
339
|
+
.map(emit_result)
|
|
340
|
+
}
|
|
230
341
|
_ => Ok(emit_unknown_subcommand_usage(command)),
|
|
231
342
|
}
|
|
232
343
|
}
|
|
@@ -244,11 +355,6 @@ pub(crate) fn is_leader_passthrough_command(command: &str) -> bool {
|
|
|
244
355
|
)
|
|
245
356
|
}
|
|
246
357
|
|
|
247
|
-
fn emit_missing_subcommand_usage() -> ExitCode {
|
|
248
|
-
emit_usage_error("the following arguments are required: {codex,claude,...,doctor}");
|
|
249
|
-
ExitCode::Usage
|
|
250
|
-
}
|
|
251
|
-
|
|
252
358
|
/// Registered subcommands (the dispatch table) plus spec-only verbs that have no
|
|
253
359
|
/// dispatch arm yet but must still respond to `--help` per CR-063/G4.
|
|
254
360
|
/// Used by the `--help` short-circuit gate so unknown commands keep falling through
|
|
@@ -258,41 +364,43 @@ fn is_known_subcommand(command: &str) -> bool {
|
|
|
258
364
|
}
|
|
259
365
|
|
|
260
366
|
pub(crate) fn default_help() -> String {
|
|
261
|
-
let mut out = String::from("
|
|
367
|
+
let mut out = String::from("Team Agent:起一支队伍,给队友发任务,看回复,安全关队。\n\n1. 准备队友\n 在项目目录运行 team-agent quick-start;空目录会给出 TEAM.md 和 agents/worker.md 两文件模板,请自行创建。\n2. 打开主控并起队\n 已安装且已登录 Pi?运行 team-agent pi。\n 在该主控的命令行/工具上下文运行 team-agent quick-start。\n 已有队伍要恢复?看 team-agent restart --help,不要接管或删除它。\n3. 发任务、看回复\n team-agent send worker '计算 245 × 37,把答案回复给 leader。'\n team-agent inbox leader -n 3\n team-agent status\n 发出任务不等于完成;等队友真正回复。\n4. 体检、关队\n team-agent doctor --workspace .\n team-agent shutdown --workspace . --json\n 检查本队残留为空,再用 doctor 确认所选项目没有待处理问题。\n\n多个项目/队伍用 --workspace/--team 选择,不猜对象;模型名称先用 models 查询。\nWindows 使用 ConPTY 需要 Windows 主机和已安装的 shim;tmux 指令不是通用 Windows 路径。\n");
|
|
262
368
|
append_help_section(
|
|
263
369
|
&mut out,
|
|
264
|
-
"
|
|
265
|
-
&[
|
|
266
|
-
"quick-start",
|
|
267
|
-
"send",
|
|
268
|
-
"status",
|
|
269
|
-
"results",
|
|
270
|
-
"models",
|
|
271
|
-
"leaders",
|
|
272
|
-
],
|
|
370
|
+
"开始协作",
|
|
371
|
+
&["quick-start", "send", "status", "models", "inbox"],
|
|
273
372
|
);
|
|
373
|
+
append_help_section(&mut out, "队伍管理", &["restart", "shutdown"]);
|
|
274
374
|
append_help_section(
|
|
275
375
|
&mut out,
|
|
276
|
-
"
|
|
376
|
+
"队友管理",
|
|
277
377
|
&[
|
|
278
|
-
"restart",
|
|
279
|
-
"shutdown",
|
|
280
378
|
"add-agent",
|
|
281
379
|
"start-agent",
|
|
282
380
|
"stop-agent",
|
|
283
381
|
"reset-agent",
|
|
382
|
+
"clone-agent",
|
|
383
|
+
"fork-agent",
|
|
384
|
+
"remove-agent",
|
|
284
385
|
],
|
|
285
386
|
);
|
|
286
|
-
append_help_section(&mut out, "Diagnose", &["doctor"]);
|
|
287
387
|
append_help_section(
|
|
288
388
|
&mut out,
|
|
289
|
-
"
|
|
389
|
+
"观察与协作",
|
|
390
|
+
&["leaders", "doctor", "approvals", "allow-peer-talk"],
|
|
391
|
+
);
|
|
392
|
+
append_help_section(&mut out, "设置", &["route", "profile", "install-skill"]);
|
|
393
|
+
append_help_section(
|
|
394
|
+
&mut out,
|
|
395
|
+
"按体检提示恢复",
|
|
290
396
|
&["claim-leader", "takeover", "attach-leader"],
|
|
291
397
|
);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
398
|
+
append_help_section(
|
|
399
|
+
&mut out,
|
|
400
|
+
"启动主控",
|
|
401
|
+
&["pi", "codex", "claude", "copilot", "grok", "cursor"],
|
|
402
|
+
);
|
|
403
|
+
out.push_str("\n每个操作的参数和例子:team-agent <command> --help");
|
|
296
404
|
out
|
|
297
405
|
}
|
|
298
406
|
|
|
@@ -326,70 +434,109 @@ pub fn __test_quick_start_args(
|
|
|
326
434
|
quick_start_args(args, cwd)
|
|
327
435
|
}
|
|
328
436
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
)
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
437
|
+
pub(super) const TEAM_TEMPLATE: &str =
|
|
438
|
+
"---\nname: help-demo\n---\nA small team for a command-line example.\n";
|
|
439
|
+
pub(super) const WORKER_TEMPLATE: &str = "---\nname: worker\nrole: assistant\nprovider: pi\nmodel: openai-codex/gpt-6-luna\nauth_mode: subscription\ndangerously_skip_permissions: false\n---\n完成任务后,把简明答案回复给 leader。\n";
|
|
440
|
+
const HUMAN_NAVIGATION: &str = "请用 team-agent --help 查看可用操作。";
|
|
441
|
+
|
|
442
|
+
pub(super) fn command_help(command: Option<&str>) -> String {
|
|
443
|
+
let Some(name) = command else {
|
|
444
|
+
return default_help();
|
|
445
|
+
};
|
|
446
|
+
let Some(spec) = command_spec(name) else {
|
|
447
|
+
return HUMAN_NAVIGATION.to_string();
|
|
448
|
+
};
|
|
449
|
+
if name == "route" {
|
|
450
|
+
return super::route::HELP.to_string();
|
|
451
|
+
}
|
|
452
|
+
let (details, examples, next) = match name {
|
|
453
|
+
"quick-start" => (
|
|
454
|
+
"TEAMDIR 是含 TEAM.md 和 agents/ 的角色目录;--workspace 选择运行项目。\n--name 设置队伍名称;--team/--team-id 选择队伍;--yes 确认已有提示;--detail 查看详细返回。\n工具须已安装并登录;工具/模型/思考强度写在角色文件中;跳过权限询问默认 false。\n--backend tmux 用于 POSIX;Windows 的 conpty 需要 Windows 主机和已安装的 ConPTY shim。",
|
|
455
|
+
"team-agent quick-start\nteam-agent quick-start ./roles --workspace ./project\nteam-agent quick-start . --json",
|
|
456
|
+
"确认 Pi 已安装并登录,用 team-agent models --provider pi 核对模型。\n运行 team-agent pi,在该主控的命令行/工具上下文再运行 quick-start;已有队伍看 restart --help。"),
|
|
457
|
+
"send" => ("<agent> 是 status 中的队友名;MESSAGE 是任务内容。\n--mailbox 只留言,不发送到当前对话;默认发送到队友对话。",
|
|
458
|
+
"team-agent send worker '计算 245 × 37,把答案回复给 leader。'\nteam-agent send worker '检查改动' --workspace . --team help-demo\nteam-agent send worker '下次上线时查看' --mailbox",
|
|
459
|
+
"已收下任务不等于送达或完成。等真实回复,用 team-agent inbox leader -n 3;有疑问看 status/doctor,不反复重发。"),
|
|
460
|
+
"inbox" => ("<agent> 是收信队友名;-n/--limit 限制条数。查看回复不触发重发。",
|
|
461
|
+
"team-agent inbox leader -n 3\nteam-agent inbox worker --limit 5\nteam-agent inbox leader --workspace . --team help-demo --json", "没有回复时先等队友自然返回,再看 team-agent status;不要读取队友终端内容。"),
|
|
462
|
+
"status" => ("可选 <agent> 只查看一位队友;--summary/--detail 保留兼容,不增加字段。\n九字段:name/provider/model/effort/runtime_status/activity/health/session_name/tmux_command。\n运行状态、忙闲、健康各有含义;unknown 表示证据不足,null 表示未设置,不猜默认模型。",
|
|
463
|
+
"team-agent status\nteam-agent status worker\nteam-agent status --workspace . --team help-demo --json", "看回复用 team-agent inbox leader -n 3;查问题用 team-agent doctor --workspace .。"),
|
|
464
|
+
"models" => ("--provider 选择工具,默认 pi;QUERY 或 --search 按关键词搜索,不能同时用。\nCursor 的工具名是 cursor_agent;Copilot 暂无此模型查询入口。返回真实目录,不猜模型。",
|
|
465
|
+
"team-agent models --provider pi\nteam-agent models --provider pi --search luna\nteam-agent models --provider cursor_agent --json", "把返回的完整模型名称填入角色文件或 add-agent --model,再起队或加人。"),
|
|
466
|
+
"restart" => ("WORKSPACE 默认当前项目;优先恢复已保存会话。\n只有用户明确允许丢弃旧对话时才加 --allow-fresh;--session-converge-deadline 设置等待秒数。",
|
|
467
|
+
"team-agent restart .\nteam-agent restart . --team help-demo --json", "用 status 查看恢复结果;需要新会话先征得用户同意,不用 takeover 代替恢复。"),
|
|
468
|
+
"shutdown" => ("只关闭所选项目/队伍;日志默认保留,--keep-logs 保留兼容。\n--json 查看关闭范围、降级情况及本队残留,返回成功不代表其他队伍已关闭。",
|
|
469
|
+
"team-agent shutdown --workspace .\nteam-agent shutdown --workspace . --team help-demo --json", "核对本队残留为空,再用 team-agent doctor --workspace .;不要广域清理其他队伍。"),
|
|
470
|
+
"add-agent" => ("<agent> 是新队友名;--provider 选择工具;--model 模型名称;--effort 思考强度。\n--bypass 是否跳过权限询问,示例 false;--prompt 任务职责;--profile 登录/代理设置。\n工具和 bypass 必须由参数或 --role-file 明确提供;冲突会拒绝,不猜设置。--force 替换已有队友,仅在用户明确授权后用。",
|
|
471
|
+
"team-agent add-agent reviewer --provider pi --model openai-codex/gpt-6-luna --bypass false --prompt '检查任务'\nteam-agent add-agent reviewer --role-file ./agents/reviewer.md", "用 status 确认队友,再运行 team-agent send reviewer '检查改动'。"),
|
|
472
|
+
"start-agent" => ("只启动已有队友;仍在运行时先 stop-agent。\n--provider 工具;--model 模型名称;--effort 思考强度;--bypass 是否跳过权限询问。\n--prompt/--profile 更换职责/登录设置;只有明确获准丢弃旧对话才用 --allow-fresh。",
|
|
473
|
+
"team-agent start-agent worker\nteam-agent start-agent worker --model openai-codex/gpt-6-luna", "新增队友用 add-agent;启动后看 status,再 send 分派任务。"),
|
|
474
|
+
"stop-agent" => ("<agent> 指定要暂停的队友;暂停不是删除,配置与会话记录保留。",
|
|
475
|
+
"team-agent stop-agent worker\nteam-agent stop-agent worker --workspace . --team help-demo --json", "用 status 确认停止;恢复用 team-agent start-agent worker。"),
|
|
476
|
+
"reset-agent" => ("必须明确加 --discard-session;此操作会停止旧进程、清除保存的会话关联,并尝试以新会话重新启动。已暂停的队友不会自动启动;不承诺删除工具的全部历史记录。",
|
|
477
|
+
"team-agent reset-agent worker --discard-session\nteam-agent reset-agent worker --discard-session --workspace . --team help-demo --json", "用 team-agent status 核对重置结果;正常重置后直接 send 分派新任务,无需再执行 start-agent。"),
|
|
478
|
+
"clone-agent" => ("<agent> 是源队友;--as 新队友名;--label 可读标签。\n复制配置创建新队友,不复制完整对话。",
|
|
479
|
+
"team-agent clone-agent worker --as reviewer\nteam-agent clone-agent worker --as reviewer --label '审阅队友' --workspace .", "用 status 查看新队友,再 send reviewer 分派独立任务。"),
|
|
480
|
+
"fork-agent" => ("<agent> 是源队友;--as 新队友名;--label 可读标签。\n需要源会话已保存且工具支持分支;不满足条件会拒绝,不保证所有工具都可分支。",
|
|
481
|
+
"team-agent fork-agent worker --as experiment\nteam-agent fork-agent worker --as experiment --workspace . --team help-demo --json", "分支成功会占用新资源;看 status,再向 experiment 派发任务,用完停止或移除。"),
|
|
482
|
+
"remove-agent" => ("必须 --confirm;配置定义的队友还需 --from-spec,动态队友可以不加。\n只删除托管角色副本,不删除外部用户角色文件;运行中的队友先 stop-agent。\n--force 可停止并移除运行中的队友,仅在理解风险且明确授权后使用。",
|
|
483
|
+
"team-agent remove-agent reviewer --confirm\nteam-agent remove-agent reviewer --from-spec --confirm --workspace . --team help-demo --json", "先保存所需回复;移除后核对 status 和实际配置范围,不对其他队友使用强制清理。"),
|
|
484
|
+
"leaders" => ("默认列出可用主控;--all 包含已失效登记,--stale 只看失效项。\nQUERY/--search 按项目/队伍/名称筛选;--prune 仅清理已确认退役的登记,--dry-run 先预览。",
|
|
485
|
+
"team-agent leaders\nteam-agent leaders --all --json\nteam-agent leaders --prune --dry-run", "根据项目和队伍选择 --workspace/--team;清理登记不等于关闭进程。"),
|
|
486
|
+
"doctor" => ("默认只检查,不修复;SPEC 可选配置路径。\n--comms/--gate comms 检查连接,不证明队友实际回复;--gate orphans 检查残留。\n--fix/--fix-schema/--cleanup-orphans 仅在获准的范围内使用;危险修复需要 --confirm。",
|
|
487
|
+
"team-agent doctor --workspace .\nteam-agent doctor --workspace . --team help-demo --json", "按实际问题与所选队伍范围处理;不要把体检建议当成已执行的修复。"),
|
|
488
|
+
"approvals" => ("可选 <agent> 查看一位队友;仅观察权限询问,不自动批准。",
|
|
489
|
+
"team-agent approvals\nteam-agent approvals worker --json", "确认权限用途后,在工具的实际权限询问处处理,再看 status。"),
|
|
490
|
+
"allow-peer-talk" => ("指定两位现有队友;--workspace 选择项目。此命令不支持 --team。",
|
|
491
|
+
"team-agent allow-peer-talk worker reviewer\nteam-agent allow-peer-talk worker reviewer --workspace . --json", "再向队友发送交流任务;允许交流不等于已经互发消息。"),
|
|
492
|
+
"profile" => ("init 创建配置,doctor 检查配置,show 显示设置;NAME 是配置名。\n--auth-mode 登录方式;--proxy-mode direct 不走代理、inherit 沿用环境代理。\n配置存放在所选工作区,不因 --team 自动变成队伍独立配置;不要输出凭证。",
|
|
493
|
+
"team-agent profile init local --auth-mode subscription\nteam-agent profile doctor local\nteam-agent profile show local", "检查通过后用 add-agent/start-agent --profile local;需要登录时使用工具原生登录入口。"),
|
|
494
|
+
"install-skill" => ("--source 必须是已核实的指南目录;--target 选择 codex/claude/copilot/all。\n--dest 覆盖安装位置;--dry-run 只预览;--uninstall 移除目标指南。此项不是起队前提。",
|
|
495
|
+
"team-agent install-skill --source \"$SKILL_DIR\" --target codex --dry-run\nteam-agent install-skill --source \"$SKILL_DIR\" --target codex", "先把 SKILL_DIR 设为实际已核验目录;核对预览后再安装,不覆盖未知用户文件。"),
|
|
496
|
+
"claim-leader" => ("仅在 doctor 明确提示登记主控、且当前终端确为主控时使用。\n--confirm 表示授权登记;不会自动打开新工具。",
|
|
497
|
+
"team-agent claim-leader --workspace . --json\nteam-agent claim-leader --workspace . --team help-demo --confirm --json", "先核对 doctor 与当前终端归属;未确认前的拒绝不是一次成功登记。"),
|
|
498
|
+
"takeover" => ("仅在 doctor 提示接管且用户已授权时使用;接管会修改主控归属。\n有可用的队伍终端时,即使省略 --confirm 也可能执行接管;不能用作只读预演。正常恢复用 restart。",
|
|
499
|
+
"team-agent takeover --workspace . --team help-demo --confirm\nteam-agent takeover --workspace . --team help-demo --confirm --json", "核实主控和队伍归属再确认;完成后查看 status/doctor,不接管其他人的队伍。"),
|
|
500
|
+
"attach-leader" => ("PANE 必须来自实际核验的主控终端,不能猜终端编号。\n--provider 选择实际工具;--confirm 表示授权连接已有主控,仅按 doctor 提示使用。",
|
|
501
|
+
"team-agent attach-leader --pane \"$PANE\" --provider pi --workspace .\nteam-agent attach-leader --pane \"$PANE\" --provider pi --workspace . --team help-demo --confirm --json", "先将 PANE 设为已核验的终端;连接后用 doctor 确认,再派发任务。"),
|
|
502
|
+
_ => ("先确保工具已安装并完成原生登录;当前目录作为项目。\n--attach-existing 与 --confirm 只在已核验主控归属并明确授权后使用;--attach-session 选择已核验的会话。\n--external-leader 选择外部主控;--allow-nested-attach 仅在明确授权使用嵌套终端时添加。\n-- 后面的参数原样交给工具;纯 --help/-h 只显示这里的帮助,不启动工具。",
|
|
503
|
+
"", "在打开的主控命令行/工具上下文运行 team-agent quick-start;不是在普通队友对话中起队。"),
|
|
504
|
+
};
|
|
505
|
+
let examples = if matches!(spec.kind, CommandKind::LeaderPassthrough { .. }) {
|
|
506
|
+
let native = if name == "cursor" { "agent" } else { name };
|
|
507
|
+
if name == "pi" {
|
|
508
|
+
"team-agent pi\nteam-agent pi -- --model openai-codex/gpt-6-luna\npi --help".to_string()
|
|
509
|
+
} else {
|
|
510
|
+
format!("team-agent {name}\nteam-agent {name} --help\n{native} --help")
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
examples.to_string()
|
|
514
|
+
};
|
|
515
|
+
let scope = if matches!(spec.kind, CommandKind::LeaderPassthrough { .. }) {
|
|
516
|
+
"--json 给程序读取;不把 --workspace/--team 当作主控选项传入,它们会交给原生工具。"
|
|
517
|
+
} else {
|
|
518
|
+
"--json 给程序读取;--workspace 选项目,支持 --team 的操作用它选队伍,不猜对象。"
|
|
519
|
+
};
|
|
520
|
+
let mut out = format!("做什么:\n{}。\n用法:{}\n\n怎么用:\n{}\n{}\nExamples(可复制):\n{}\n\n下一步(Next Action):\n{}", spec.summary, spec.usage, details, scope, examples, next);
|
|
521
|
+
if name == "quick-start" {
|
|
522
|
+
out.push_str(&format!("\n\n最简两文件(请自行创建,不会自动写入):\nTEAM.md:\n{TEAM_TEMPLATE}\nagents/worker.md:\n{WORKER_TEMPLATE}"));
|
|
523
|
+
}
|
|
524
|
+
if name == "cursor" {
|
|
525
|
+
out.push_str("\n查询模型用 team-agent models --provider cursor_agent。");
|
|
383
526
|
}
|
|
527
|
+
if name == "copilot" {
|
|
528
|
+
out.push_str("\nCopilot 暂无 team-agent models 查询入口,请看工具原生帮助。");
|
|
529
|
+
}
|
|
530
|
+
out
|
|
384
531
|
}
|
|
385
532
|
|
|
386
533
|
fn emit_unknown_subcommand_usage(command: &str) -> ExitCode {
|
|
387
534
|
emit_usage_error(&format!(
|
|
388
|
-
"
|
|
535
|
+
"没有这个操作:'{command}'。请用 team-agent --help 查看可用操作。"
|
|
389
536
|
));
|
|
390
537
|
// E8 (N38): 错路引导 —— 拼写近似时建议最接近的真子命令(additive,不改既有 golden 行)。
|
|
391
538
|
if let Some(suggestion) = nearest_subcommand(command) {
|
|
392
|
-
eprintln!("team-agent
|
|
539
|
+
eprintln!("你是否想使用 `{suggestion}`?运行 team-agent {suggestion} --help 查看例子。");
|
|
393
540
|
}
|
|
394
541
|
// 0.5.45 naming-addressing (RED-6): unknown subcommand exits 1
|
|
395
542
|
// (Error), aligned with the family of typo refusals throughout
|
|
@@ -421,8 +568,8 @@ fn nearest_subcommand(input: &str) -> Option<&'static str> {
|
|
|
421
568
|
}
|
|
422
569
|
|
|
423
570
|
fn emit_usage_error(message: &str) {
|
|
424
|
-
eprintln!("
|
|
425
|
-
eprintln!("
|
|
571
|
+
eprintln!("用法:team-agent <command>;查看所有操作:team-agent --help");
|
|
572
|
+
eprintln!("错误:{message}");
|
|
426
573
|
}
|
|
427
574
|
|
|
428
575
|
/// `cmd_validate` delegates to runtime validate_file.
|
|
@@ -641,6 +788,36 @@ fn emit_cli_error(command: &str, args: &[String], cwd: &Path, error: &CliError)
|
|
|
641
788
|
let _ = std::fs::write(&log_path, format!("{safe_error}\n"));
|
|
642
789
|
let mut payload = payload_error.to_payload(&log_path, command);
|
|
643
790
|
payload.error = safe_error;
|
|
791
|
+
if let CliError::AddressRefusal { error, action, .. } = payload_error {
|
|
792
|
+
if !has_arg(args, "--json") {
|
|
793
|
+
payload.error = crate::redaction::redact_external_text(error);
|
|
794
|
+
payload.action = action.clone();
|
|
795
|
+
}
|
|
796
|
+
} else if let Some(reason) = payload
|
|
797
|
+
.error
|
|
798
|
+
.strip_prefix("usage error: ")
|
|
799
|
+
.unwrap_or(&payload.error)
|
|
800
|
+
.strip_prefix("Error: ")
|
|
801
|
+
.and_then(|text| text.split_once(':').map(|(reason, _)| reason))
|
|
802
|
+
{
|
|
803
|
+
if let Some(message) = super::named_address::human_address_reason(reason) {
|
|
804
|
+
payload.error = message.to_string();
|
|
805
|
+
payload.action = "先运行 team-agent status,再用列表中的队友名发送;用 --workspace/--team 选择项目和队伍,连接问题先 doctor。".to_string();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if command_spec(command).is_some()
|
|
809
|
+
&& payload.action
|
|
810
|
+
== "先运行 team-agent doctor --workspace . 检查所选队伍,或查看此处列出的错误日志。"
|
|
811
|
+
{
|
|
812
|
+
let mut doctor = format!(
|
|
813
|
+
"team-agent doctor --workspace {}",
|
|
814
|
+
super::adapters::shell_quote(&workspace.to_string_lossy())
|
|
815
|
+
);
|
|
816
|
+
if let Some(team) = parsed.team.as_deref() {
|
|
817
|
+
doctor.push_str(&format!(" --team {}", super::adapters::shell_quote(team)));
|
|
818
|
+
}
|
|
819
|
+
payload.action = format!("先运行 {doctor} 检查所选队伍,或查看此处列出的错误日志。");
|
|
820
|
+
}
|
|
644
821
|
payload.action = crate::redaction::redact_external_text(&payload.action);
|
|
645
822
|
payload.log = crate::redaction::redact_external_text(&payload.log);
|
|
646
823
|
payload.reason = payload
|
|
@@ -662,7 +839,11 @@ fn emit_cli_error(command: &str, args: &[String], cwd: &Path, error: &CliError)
|
|
|
662
839
|
} else {
|
|
663
840
|
eprintln!("error: {}", payload.error);
|
|
664
841
|
eprintln!("action: {}", payload.action);
|
|
665
|
-
|
|
842
|
+
if matches!(payload_error, CliError::AddressRefusal { .. }) {
|
|
843
|
+
eprintln!("log: {:?}", payload.log);
|
|
844
|
+
} else {
|
|
845
|
+
eprintln!("log: {}", payload.log);
|
|
846
|
+
}
|
|
666
847
|
}
|
|
667
848
|
ExitCode::Error
|
|
668
849
|
}
|
|
@@ -999,7 +1180,9 @@ fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliEr
|
|
|
999
1180
|
let path = positional_agents_dir.as_ref()?;
|
|
1000
1181
|
if let Some(run_workspace) = path
|
|
1001
1182
|
.ancestors()
|
|
1002
|
-
.find(|ancestor|
|
|
1183
|
+
.find(|ancestor| {
|
|
1184
|
+
ancestor.file_name().and_then(|name| name.to_str()) == Some(".team")
|
|
1185
|
+
})
|
|
1003
1186
|
.and_then(Path::parent)
|
|
1004
1187
|
{
|
|
1005
1188
|
return Some(run_workspace.to_path_buf());
|
|
@@ -1020,9 +1203,7 @@ fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliEr
|
|
|
1020
1203
|
let normalized = literal.trim().to_ascii_lowercase();
|
|
1021
1204
|
if normalized != "tmux" && normalized != "conpty" {
|
|
1022
1205
|
return Err(CliError::Usage(format!(
|
|
1023
|
-
"--backend
|
|
1024
|
-
`pty` is not a supported literal in Phase 1d (design \
|
|
1025
|
-
§Non-Goals + CR C-1 ②)."
|
|
1206
|
+
"--backend 只能是 tmux 或 conpty,收到 {literal:?}。POSIX 使用 tmux;Windows ConPTY 需要 Windows 主机和已安装的 shim。"
|
|
1026
1207
|
)));
|
|
1027
1208
|
}
|
|
1028
1209
|
}
|
|
@@ -1069,6 +1250,16 @@ fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
|
|
|
1069
1250
|
validate_send_flags(args)?;
|
|
1070
1251
|
warn_send_legacy_delivery_flags(args);
|
|
1071
1252
|
let parsed = parse_args(args);
|
|
1253
|
+
if parsed.targets.is_none()
|
|
1254
|
+
&& parsed.pane.is_none()
|
|
1255
|
+
&& parsed.to_name.is_none()
|
|
1256
|
+
&& parsed.to_leader.is_none()
|
|
1257
|
+
&& parsed.positionals.len() < 2
|
|
1258
|
+
{
|
|
1259
|
+
return Err(CliError::Usage(
|
|
1260
|
+
"请填写收信队友名和任务内容,例如 team-agent send worker '检查改动'".to_string(),
|
|
1261
|
+
));
|
|
1262
|
+
}
|
|
1072
1263
|
let target = if parsed.targets.is_some()
|
|
1073
1264
|
|| parsed.pane.is_some()
|
|
1074
1265
|
|| parsed.to_name.is_some()
|
|
@@ -1099,8 +1290,8 @@ fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
|
|
|
1099
1290
|
presentation_value.as_ref(),
|
|
1100
1291
|
)
|
|
1101
1292
|
.map_err(|error| CliError::Usage(format!("invalid send routing: {error}")))?;
|
|
1102
|
-
if
|
|
1103
|
-
eprintln!("
|
|
1293
|
+
if normalized.deprecation.is_some() {
|
|
1294
|
+
eprintln!("提示:旧发送设置已弃用。直接用 team-agent send <agent> '任务内容';只留言请加 --mailbox。");
|
|
1104
1295
|
}
|
|
1105
1296
|
Ok(SendArgs {
|
|
1106
1297
|
target,
|
|
@@ -1179,18 +1370,16 @@ fn warn_send_legacy_delivery_flags(args: &[String]) {
|
|
|
1179
1370
|
"--message-id",
|
|
1180
1371
|
];
|
|
1181
1372
|
let spec = command_spec("send");
|
|
1182
|
-
let sunset = spec
|
|
1183
|
-
.and_then(|spec| spec.sunset)
|
|
1184
|
-
.unwrap_or("next compatibility release");
|
|
1373
|
+
let sunset = spec.and_then(|spec| spec.sunset).unwrap_or("后续兼容版本");
|
|
1185
1374
|
let action = spec
|
|
1186
1375
|
.and_then(|spec| spec.action)
|
|
1187
|
-
.unwrap_or("
|
|
1376
|
+
.unwrap_or("使用 team-agent send <agent> '任务内容',通过 --workspace/--team 选项目和队伍");
|
|
1188
1377
|
for flag in FLAGS {
|
|
1189
1378
|
if args
|
|
1190
1379
|
.iter()
|
|
1191
1380
|
.any(|arg| arg == flag || arg.starts_with(&format!("{flag}=")))
|
|
1192
1381
|
{
|
|
1193
|
-
eprintln!("
|
|
1382
|
+
eprintln!("提示:{flag} 已弃用,将在{sunset}移除。下一步:{action}");
|
|
1194
1383
|
}
|
|
1195
1384
|
}
|
|
1196
1385
|
}
|
|
@@ -1437,7 +1626,10 @@ fn restart_args(args: &[String], cwd: &Path) -> Result<RestartArgs, CliError> {
|
|
|
1437
1626
|
}
|
|
1438
1627
|
|
|
1439
1628
|
// Keep strict role CLI parsing separate from the legacy shared parser.
|
|
1440
|
-
fn role_agent_args(
|
|
1629
|
+
fn role_agent_args(
|
|
1630
|
+
args: &[String],
|
|
1631
|
+
add: bool,
|
|
1632
|
+
) -> Result<(ParsedArgs, crate::lifecycle::role_config::RoleConfigPatch), CliError> {
|
|
1441
1633
|
let mut role = crate::lifecycle::role_config::RoleConfigPatch::default();
|
|
1442
1634
|
let mut forwarded = Vec::new();
|
|
1443
1635
|
let mut seen = std::collections::BTreeSet::new();
|
|
@@ -1449,8 +1641,12 @@ fn role_agent_args(args: &[String], add: bool) -> Result<(ParsedArgs, crate::lif
|
|
|
1449
1641
|
index += 1;
|
|
1450
1642
|
continue;
|
|
1451
1643
|
}
|
|
1452
|
-
let (flag, inline) = argument
|
|
1453
|
-
|
|
1644
|
+
let (flag, inline) = argument
|
|
1645
|
+
.split_once('=')
|
|
1646
|
+
.map_or((argument.as_str(), None), |(key, value)| (key, Some(value)));
|
|
1647
|
+
if !seen.insert(flag.to_string()) {
|
|
1648
|
+
return Err(CliError::Usage(format!("duplicate {flag}")));
|
|
1649
|
+
}
|
|
1454
1650
|
if matches!(flag, "--json" | "--allow-fresh" | "--force") {
|
|
1455
1651
|
if inline.is_some() || (add && flag == "--allow-fresh") {
|
|
1456
1652
|
return Err(CliError::Usage(format!("unsupported option: {argument}")));
|
|
@@ -1459,26 +1655,46 @@ fn role_agent_args(args: &[String], add: bool) -> Result<(ParsedArgs, crate::lif
|
|
|
1459
1655
|
index += 1;
|
|
1460
1656
|
continue;
|
|
1461
1657
|
}
|
|
1462
|
-
if !matches!(
|
|
1463
|
-
|
|
1658
|
+
if !matches!(
|
|
1659
|
+
flag,
|
|
1660
|
+
"--provider"
|
|
1661
|
+
| "--model"
|
|
1662
|
+
| "--effort"
|
|
1663
|
+
| "--bypass"
|
|
1664
|
+
| "--prompt"
|
|
1665
|
+
| "--profile"
|
|
1666
|
+
| "--workspace"
|
|
1667
|
+
| "--team"
|
|
1668
|
+
| "--role-file"
|
|
1669
|
+
) || (!add && flag == "--role-file")
|
|
1670
|
+
{
|
|
1464
1671
|
return Err(CliError::Usage(format!("unknown option: {flag}")));
|
|
1465
1672
|
}
|
|
1466
|
-
let value = if let Some(value) = inline {
|
|
1673
|
+
let value = if let Some(value) = inline {
|
|
1674
|
+
value.to_string()
|
|
1675
|
+
} else {
|
|
1467
1676
|
index += 1;
|
|
1468
|
-
args.get(index)
|
|
1469
|
-
.
|
|
1677
|
+
args.get(index)
|
|
1678
|
+
.filter(|value| !value.starts_with("--"))
|
|
1679
|
+
.ok_or_else(|| CliError::Usage(format!("missing value for {flag}")))?
|
|
1680
|
+
.clone()
|
|
1470
1681
|
};
|
|
1471
|
-
if value.trim().is_empty() {
|
|
1682
|
+
if value.trim().is_empty() {
|
|
1683
|
+
return Err(CliError::Usage(format!("empty value for {flag}")));
|
|
1684
|
+
}
|
|
1472
1685
|
match flag {
|
|
1473
1686
|
"--provider" => {
|
|
1474
1687
|
let provider = crate::provider::wire::parse_provider(&value)
|
|
1475
1688
|
.ok_or_else(|| CliError::Usage(format!("unknown provider: {value}")))?;
|
|
1476
1689
|
role.provider = Some(crate::provider::wire::provider_wire(provider).to_string());
|
|
1477
1690
|
}
|
|
1478
|
-
"--bypass" =>
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1691
|
+
"--bypass" => {
|
|
1692
|
+
role.bypass = Some(match value.as_str() {
|
|
1693
|
+
"true" => true,
|
|
1694
|
+
"false" => false,
|
|
1695
|
+
_ => return Err(CliError::Usage("--bypass requires true or false".into())),
|
|
1696
|
+
})
|
|
1697
|
+
}
|
|
1482
1698
|
"--model" => role.model = Some(value),
|
|
1483
1699
|
"--effort" => {
|
|
1484
1700
|
if crate::model::enums::ProviderEffort::parse(&value).is_none() {
|
|
@@ -1488,35 +1704,71 @@ fn role_agent_args(args: &[String], add: bool) -> Result<(ParsedArgs, crate::lif
|
|
|
1488
1704
|
}
|
|
1489
1705
|
"--prompt" => role.prompt = Some(value),
|
|
1490
1706
|
"--profile" => role.profile = Some(value),
|
|
1491
|
-
_ => {
|
|
1707
|
+
_ => {
|
|
1708
|
+
forwarded.push(flag.to_string());
|
|
1709
|
+
forwarded.push(value);
|
|
1710
|
+
}
|
|
1492
1711
|
}
|
|
1493
1712
|
index += 1;
|
|
1494
1713
|
}
|
|
1495
1714
|
let parsed = parse_args(&forwarded);
|
|
1496
1715
|
if add && parsed.role_file.is_none() {
|
|
1497
|
-
if role.provider.is_none() {
|
|
1498
|
-
|
|
1716
|
+
if role.provider.is_none() {
|
|
1717
|
+
return Err(CliError::Usage(
|
|
1718
|
+
"add-agent requires --provider <name>".into(),
|
|
1719
|
+
));
|
|
1720
|
+
}
|
|
1721
|
+
if role.bypass.is_none() {
|
|
1722
|
+
return Err(CliError::Usage(
|
|
1723
|
+
"add-agent requires --bypass <true|false>".into(),
|
|
1724
|
+
));
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
if parsed.positionals.len() != 1 {
|
|
1728
|
+
return Err(CliError::Usage(
|
|
1729
|
+
if parsed.positionals.is_empty() {
|
|
1730
|
+
"missing agent"
|
|
1731
|
+
} else {
|
|
1732
|
+
"expected exactly one agent id"
|
|
1733
|
+
}
|
|
1734
|
+
.into(),
|
|
1735
|
+
));
|
|
1499
1736
|
}
|
|
1500
|
-
if parsed.positionals.len() != 1 { return Err(CliError::Usage("expected exactly one agent id".into())); }
|
|
1501
1737
|
Ok((parsed, role))
|
|
1502
1738
|
}
|
|
1503
1739
|
|
|
1504
1740
|
#[cfg(test)]
|
|
1505
1741
|
mod role_cli_tests {
|
|
1506
1742
|
use super::*;
|
|
1507
|
-
fn strings(values: &[&str]) -> Vec<String> {
|
|
1743
|
+
fn strings(values: &[&str]) -> Vec<String> {
|
|
1744
|
+
values.iter().map(|value| (*value).to_string()).collect()
|
|
1745
|
+
}
|
|
1508
1746
|
|
|
1509
1747
|
#[test]
|
|
1510
1748
|
fn add_requires_decisions_without_a_template_but_accepts_file_input() {
|
|
1511
|
-
for args in [
|
|
1749
|
+
for args in [
|
|
1750
|
+
vec!["w"],
|
|
1751
|
+
vec!["w", "--provider", "pi"],
|
|
1752
|
+
vec!["w", "--bypass", "false"],
|
|
1753
|
+
] {
|
|
1512
1754
|
assert!(role_agent_args(&strings(&args), true).is_err());
|
|
1513
1755
|
}
|
|
1514
1756
|
assert!(role_agent_args(&strings(&["w", "--role-file", "w.md"]), true).is_ok());
|
|
1515
|
-
let (parsed, _) =
|
|
1757
|
+
let (parsed, _) =
|
|
1758
|
+
role_agent_args(&strings(&["w", "--role-file", "w.md", "--force"]), true).unwrap();
|
|
1516
1759
|
assert!(parsed.force);
|
|
1517
|
-
assert!(role_agent_args(
|
|
1518
|
-
|
|
1519
|
-
|
|
1760
|
+
assert!(role_agent_args(
|
|
1761
|
+
&strings(&["w", "--role-file", "w.md", "--force=true"]),
|
|
1762
|
+
true
|
|
1763
|
+
)
|
|
1764
|
+
.is_err());
|
|
1765
|
+
assert!(role_agent_args(
|
|
1766
|
+
&strings(&["w", "--role-file", "w.md", "--allow-fresh"]),
|
|
1767
|
+
true
|
|
1768
|
+
)
|
|
1769
|
+
.is_err());
|
|
1770
|
+
let (_, patch) =
|
|
1771
|
+
role_agent_args(&strings(&["w", "--provider=pi", "--bypass=false"]), true).unwrap();
|
|
1520
1772
|
assert_eq!(patch.provider.as_deref(), Some("pi"));
|
|
1521
1773
|
assert_eq!(patch.bypass, Some(false));
|
|
1522
1774
|
}
|
|
@@ -1524,12 +1776,20 @@ mod role_cli_tests {
|
|
|
1524
1776
|
#[test]
|
|
1525
1777
|
fn strict_role_options_reject_ambiguous_values() {
|
|
1526
1778
|
for args in [
|
|
1527
|
-
vec!["w", "--bypass"],
|
|
1528
|
-
vec!["w", "--
|
|
1529
|
-
vec!["w", "--
|
|
1530
|
-
|
|
1779
|
+
vec!["w", "--bypass"],
|
|
1780
|
+
vec!["w", "--bypass", "yes"],
|
|
1781
|
+
vec!["w", "--no-bypass"],
|
|
1782
|
+
vec!["w", "--bypass", "true", "--bypass=false"],
|
|
1783
|
+
vec!["w", "--unknown", "x"],
|
|
1784
|
+
vec!["w", "extra"],
|
|
1785
|
+
] {
|
|
1786
|
+
assert!(role_agent_args(&strings(&args), false).is_err());
|
|
1787
|
+
}
|
|
1531
1788
|
let (_, patch) = role_agent_args(&strings(&["w"]), false).unwrap();
|
|
1532
|
-
assert_eq!(
|
|
1789
|
+
assert_eq!(
|
|
1790
|
+
patch,
|
|
1791
|
+
crate::lifecycle::role_config::RoleConfigPatch::default()
|
|
1792
|
+
);
|
|
1533
1793
|
}
|
|
1534
1794
|
}
|
|
1535
1795
|
|
|
@@ -1558,10 +1818,16 @@ fn stop_agent_args(args: &[String], cwd: &Path) -> Result<StopAgentArgs, CliErro
|
|
|
1558
1818
|
|
|
1559
1819
|
fn reset_agent_args(args: &[String], cwd: &Path) -> Result<ResetAgentArgs, CliError> {
|
|
1560
1820
|
let parsed = parse_args(args);
|
|
1821
|
+
let agent = required_pos(&parsed, 0, "agent")?;
|
|
1822
|
+
if !parsed.discard_session {
|
|
1823
|
+
return Err(CliError::Usage(
|
|
1824
|
+
"missing --discard-session;只有用户明确同意清除会话关联后才能添加此参数".to_string(),
|
|
1825
|
+
));
|
|
1826
|
+
}
|
|
1561
1827
|
let workspace = workspace(&parsed, cwd);
|
|
1562
1828
|
refuse_if_multi_alive_team_missing_scope("reset-agent", &workspace, parsed.team.as_deref())?;
|
|
1563
1829
|
Ok(ResetAgentArgs {
|
|
1564
|
-
agent
|
|
1830
|
+
agent,
|
|
1565
1831
|
workspace,
|
|
1566
1832
|
team: parsed.team,
|
|
1567
1833
|
discard_session: parsed.discard_session,
|
|
@@ -1612,10 +1878,11 @@ fn clone_agent_args(args: &[String], cwd: &Path) -> Result<CloneAgentArgs, CliEr
|
|
|
1612
1878
|
|
|
1613
1879
|
fn remove_agent_args(args: &[String], cwd: &Path) -> Result<RemoveAgentArgs, CliError> {
|
|
1614
1880
|
let parsed = parse_args(args);
|
|
1881
|
+
let agent = required_pos(&parsed, 0, "agent")?;
|
|
1615
1882
|
let workspace = workspace(&parsed, cwd);
|
|
1616
1883
|
refuse_if_multi_alive_team_missing_scope("remove-agent", &workspace, parsed.team.as_deref())?;
|
|
1617
1884
|
Ok(RemoveAgentArgs {
|
|
1618
|
-
agent
|
|
1885
|
+
agent,
|
|
1619
1886
|
workspace,
|
|
1620
1887
|
team: parsed.team,
|
|
1621
1888
|
from_spec: parsed.from_spec,
|
|
@@ -1978,14 +2245,15 @@ mod tests {
|
|
|
1978
2245
|
help.lines()
|
|
1979
2246
|
.filter_map(|line| {
|
|
1980
2247
|
let trimmed = line.strip_prefix(" ")?;
|
|
1981
|
-
if trimmed.starts_with("team-agent ") {
|
|
2248
|
+
if trimmed.starts_with("team-agent ") || trimmed.starts_with(char::is_whitespace) {
|
|
1982
2249
|
return None;
|
|
1983
2250
|
}
|
|
1984
2251
|
let command = trimmed.split_whitespace().next()?;
|
|
1985
|
-
command
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2252
|
+
(command.chars().next()?.is_ascii_lowercase()
|
|
2253
|
+
&& command
|
|
2254
|
+
.chars()
|
|
2255
|
+
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-'))
|
|
2256
|
+
.then(|| command.to_string())
|
|
1989
2257
|
})
|
|
1990
2258
|
.collect()
|
|
1991
2259
|
}
|
|
@@ -2075,6 +2343,7 @@ mod tests {
|
|
|
2075
2343
|
.filter(|spec| spec.default_help)
|
|
2076
2344
|
.map(|spec| spec.name)
|
|
2077
2345
|
.collect();
|
|
2346
|
+
assert_eq!(expected.len(), 30, "the public catalog is Human30");
|
|
2078
2347
|
for spec_name in &expected {
|
|
2079
2348
|
assert!(
|
|
2080
2349
|
visible.iter().any(|command| command == spec_name),
|
|
@@ -2088,7 +2357,7 @@ mod tests {
|
|
|
2088
2357
|
expected_sorted.sort();
|
|
2089
2358
|
assert_eq!(
|
|
2090
2359
|
actual, expected_sorted,
|
|
2091
|
-
"default help must match the exact
|
|
2360
|
+
"default help must match the exact Human30 public spec set, not a slack threshold; got {visible:?}"
|
|
2092
2361
|
);
|
|
2093
2362
|
assert!(
|
|
2094
2363
|
top_help.contains("copilot"),
|
|
@@ -2101,7 +2370,21 @@ mod tests {
|
|
|
2101
2370
|
let top_help = command_help(None);
|
|
2102
2371
|
let visible = visible_help_commands(&top_help);
|
|
2103
2372
|
assert!(visible.iter().any(|command| command == "doctor"));
|
|
2104
|
-
|
|
2373
|
+
assert!(visible.iter().any(|command| command == "leaders"));
|
|
2374
|
+
for command in [
|
|
2375
|
+
"results",
|
|
2376
|
+
"wait",
|
|
2377
|
+
"attach-app-server-leader",
|
|
2378
|
+
"identity",
|
|
2379
|
+
"watch",
|
|
2380
|
+
"sessions",
|
|
2381
|
+
"validate",
|
|
2382
|
+
"preflight",
|
|
2383
|
+
"wait-ready",
|
|
2384
|
+
"e2e",
|
|
2385
|
+
"peek",
|
|
2386
|
+
"coordinator",
|
|
2387
|
+
] {
|
|
2105
2388
|
assert!(
|
|
2106
2389
|
!visible.iter().any(|visible| visible == command),
|
|
2107
2390
|
"`{command}` must stay hidden from default help"
|
|
@@ -2122,10 +2405,10 @@ mod tests {
|
|
|
2122
2405
|
#[test]
|
|
2123
2406
|
fn observation_a_commands_have_terminal_tiers() {
|
|
2124
2407
|
for (command, tier) in [
|
|
2125
|
-
("allow-peer-talk", CommandTier::
|
|
2126
|
-
("approvals", CommandTier::
|
|
2127
|
-
("profile", CommandTier::
|
|
2128
|
-
("install-skill", CommandTier::
|
|
2408
|
+
("allow-peer-talk", CommandTier::Core),
|
|
2409
|
+
("approvals", CommandTier::Core),
|
|
2410
|
+
("profile", CommandTier::Core),
|
|
2411
|
+
("install-skill", CommandTier::Core),
|
|
2129
2412
|
] {
|
|
2130
2413
|
assert_eq!(command_spec(command).map(|spec| spec.tier), Some(tier));
|
|
2131
2414
|
}
|
|
@@ -2134,7 +2417,7 @@ mod tests {
|
|
|
2134
2417
|
#[test]
|
|
2135
2418
|
fn suggestion_index_excludes_hidden_commands() {
|
|
2136
2419
|
assert_eq!(nearest_subcommand("statu"), Some("status"));
|
|
2137
|
-
assert_eq!(nearest_subcommand("leader"),
|
|
2420
|
+
assert_eq!(nearest_subcommand("leader"), Some("leaders"));
|
|
2138
2421
|
assert_eq!(nearest_subcommand("fallback-send-leade"), None);
|
|
2139
2422
|
assert_eq!(nearest_subcommand("coordinato"), None);
|
|
2140
2423
|
}
|
|
@@ -2179,36 +2462,42 @@ mod tests {
|
|
|
2179
2462
|
(
|
|
2180
2463
|
"start-agent",
|
|
2181
2464
|
&[
|
|
2182
|
-
"--model",
|
|
2183
|
-
"--
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
&[
|
|
2465
|
+
"--model",
|
|
2466
|
+
"--effort",
|
|
2467
|
+
"--bypass",
|
|
2468
|
+
"--prompt",
|
|
2469
|
+
"--profile",
|
|
2470
|
+
"--provider",
|
|
2189
2471
|
"--workspace",
|
|
2190
2472
|
"--team",
|
|
2191
|
-
"--
|
|
2473
|
+
"--allow-fresh",
|
|
2192
2474
|
"--json",
|
|
2193
2475
|
][..],
|
|
2194
2476
|
),
|
|
2195
2477
|
(
|
|
2196
|
-
"
|
|
2197
|
-
&[
|
|
2198
|
-
"--role-file", "--provider", "--bypass", "--model", "--effort", "--prompt", "--profile",
|
|
2199
|
-
"--force", "--workspace", "--team", "--json",
|
|
2200
|
-
][..],
|
|
2478
|
+
"reset-agent",
|
|
2479
|
+
&["--workspace", "--team", "--discard-session", "--json"][..],
|
|
2201
2480
|
),
|
|
2202
2481
|
(
|
|
2203
|
-
"
|
|
2482
|
+
"add-agent",
|
|
2204
2483
|
&[
|
|
2205
|
-
"--
|
|
2206
|
-
"--
|
|
2484
|
+
"--role-file",
|
|
2485
|
+
"--provider",
|
|
2486
|
+
"--bypass",
|
|
2487
|
+
"--model",
|
|
2488
|
+
"--effort",
|
|
2489
|
+
"--prompt",
|
|
2490
|
+
"--profile",
|
|
2491
|
+
"--force",
|
|
2207
2492
|
"--workspace",
|
|
2208
2493
|
"--team",
|
|
2209
2494
|
"--json",
|
|
2210
2495
|
][..],
|
|
2211
2496
|
),
|
|
2497
|
+
(
|
|
2498
|
+
"fork-agent",
|
|
2499
|
+
&["--as", "--label", "--workspace", "--team", "--json"][..],
|
|
2500
|
+
),
|
|
2212
2501
|
(
|
|
2213
2502
|
"remove-agent",
|
|
2214
2503
|
&[
|
|
@@ -2248,26 +2537,6 @@ mod tests {
|
|
|
2248
2537
|
"inbox",
|
|
2249
2538
|
&["--workspace", "--team", "-n", "--limit", "--json"][..],
|
|
2250
2539
|
),
|
|
2251
|
-
("sessions", &["--workspace", "--team", "--json"][..]),
|
|
2252
|
-
(
|
|
2253
|
-
"wait-ready",
|
|
2254
|
-
&["--workspace", "--team", "--timeout", "--json"][..],
|
|
2255
|
-
),
|
|
2256
|
-
(
|
|
2257
|
-
"peek",
|
|
2258
|
-
&[
|
|
2259
|
-
"--workspace",
|
|
2260
|
-
"--tail",
|
|
2261
|
-
"--head",
|
|
2262
|
-
"--search",
|
|
2263
|
-
"--allow-raw-screen",
|
|
2264
|
-
"--json",
|
|
2265
|
-
][..],
|
|
2266
|
-
),
|
|
2267
|
-
(
|
|
2268
|
-
"coordinator",
|
|
2269
|
-
&["--workspace", "--once", "--tick-interval"][..],
|
|
2270
|
-
),
|
|
2271
2540
|
] {
|
|
2272
2541
|
let help = command_help(Some(command));
|
|
2273
2542
|
for flag in flags {
|
|
@@ -2277,6 +2546,15 @@ mod tests {
|
|
|
2277
2546
|
);
|
|
2278
2547
|
}
|
|
2279
2548
|
}
|
|
2549
|
+
let cwd = tmp_workspace();
|
|
2550
|
+
for command in ["sessions", "wait-ready", "peek", "coordinator"] {
|
|
2551
|
+
assert_eq!(
|
|
2552
|
+
run(&cli_argv(&[command, "--help"]), &cwd),
|
|
2553
|
+
ExitCode::Usage,
|
|
2554
|
+
"Machine help must be generic Usage2, not a private flag tutorial"
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
std::fs::remove_dir_all(cwd).unwrap();
|
|
2280
2558
|
assert!(
|
|
2281
2559
|
!command_help(Some("quick-start")).contains("--fresh"),
|
|
2282
2560
|
"quick-start help must not advertise removed reset semantics"
|
|
@@ -2288,18 +2566,25 @@ mod tests {
|
|
|
2288
2566
|
for name in ["start-agent", "add-agent"] {
|
|
2289
2567
|
let spec = command_spec(name).expect("registered worker lifecycle command");
|
|
2290
2568
|
let help = command_help(Some(name));
|
|
2291
|
-
|
|
2292
|
-
assert!(help.contains(
|
|
2293
|
-
for flag in [
|
|
2569
|
+
assert!(help.contains(spec.usage));
|
|
2570
|
+
assert!(help.contains("做什么:"));
|
|
2571
|
+
for flag in [
|
|
2572
|
+
"--model MODEL",
|
|
2573
|
+
"--effort LEVEL",
|
|
2574
|
+
"--bypass true|false",
|
|
2575
|
+
"--prompt TEXT",
|
|
2576
|
+
"--profile NAME",
|
|
2577
|
+
"--provider TOOL",
|
|
2578
|
+
] {
|
|
2294
2579
|
assert!(help.contains(flag), "{name} help missing {flag}: {help}");
|
|
2295
2580
|
}
|
|
2296
|
-
assert!(help.contains("
|
|
2581
|
+
assert!(help.contains("下一步") && help.contains("send"));
|
|
2297
2582
|
}
|
|
2298
2583
|
assert!(!command_help(Some("start-agent")).contains("--force"));
|
|
2299
2584
|
let add = command_help(Some("add-agent"));
|
|
2300
2585
|
assert!(add.contains("[--role-file FILE]"));
|
|
2301
|
-
assert!(add.contains("provider
|
|
2302
|
-
assert!(add.contains("
|
|
2586
|
+
assert!(add.contains("provider") && add.contains("bypass") && add.contains("--role-file"));
|
|
2587
|
+
assert!(add.contains("冲突") && add.contains("拒绝"));
|
|
2303
2588
|
}
|
|
2304
2589
|
|
|
2305
2590
|
#[test]
|
|
@@ -2307,12 +2592,15 @@ mod tests {
|
|
|
2307
2592
|
let help = command_help(Some("status"));
|
|
2308
2593
|
for marker in [
|
|
2309
2594
|
"name/provider/model/effort/runtime_status/activity/health/session_name/tmux_command",
|
|
2310
|
-
"
|
|
2311
|
-
"
|
|
2595
|
+
"--json 给程序读取",
|
|
2596
|
+
"unknown 表示证据不足",
|
|
2312
2597
|
"tmux_command",
|
|
2313
|
-
"
|
|
2598
|
+
"不增加字段",
|
|
2314
2599
|
] {
|
|
2315
|
-
assert!(
|
|
2600
|
+
assert!(
|
|
2601
|
+
help.contains(marker),
|
|
2602
|
+
"status help missing {marker}: {help}"
|
|
2603
|
+
);
|
|
2316
2604
|
}
|
|
2317
2605
|
assert!(!help.contains("错误细分走 status --summary"));
|
|
2318
2606
|
}
|
|
@@ -2453,14 +2741,14 @@ mod tests {
|
|
|
2453
2741
|
.unwrap();
|
|
2454
2742
|
let err = cmd_send(&args).unwrap_err();
|
|
2455
2743
|
assert!(
|
|
2456
|
-
matches!(err, CliError::Usage(ref message) if message.contains("--pane
|
|
2744
|
+
matches!(err, CliError::Usage(ref message) if message.contains("--pane") && message.contains("--to") && message.contains("不能") && message.contains("同时")),
|
|
2457
2745
|
"expected --pane/--to mutual-exclusion usage error, got {err:?}"
|
|
2458
2746
|
);
|
|
2459
2747
|
|
|
2460
2748
|
let args = send_args(&cli_argv(&["--pane", "%1596"]), &cwd).unwrap();
|
|
2461
2749
|
let err = cmd_send(&args).unwrap_err();
|
|
2462
2750
|
assert!(
|
|
2463
|
-
matches!(err, CliError::Usage(ref message) if message
|
|
2751
|
+
matches!(err, CliError::Usage(ref message) if message.contains("--pane") && message.contains("不能发送空消息")),
|
|
2464
2752
|
"expected empty-message usage error, got {err:?}"
|
|
2465
2753
|
);
|
|
2466
2754
|
|
|
@@ -2597,10 +2885,15 @@ mod tests {
|
|
|
2597
2885
|
}
|
|
2598
2886
|
|
|
2599
2887
|
#[test]
|
|
2600
|
-
fn
|
|
2888
|
+
fn reset_agent_complete_input_refuses_multi_alive_team_before_agent_validation() {
|
|
2601
2889
|
let ws = tmp_workspace();
|
|
2602
2890
|
seed_two_alive_teams_in(&ws);
|
|
2603
|
-
let argv = cli_argv(&[
|
|
2891
|
+
let argv = cli_argv(&[
|
|
2892
|
+
"nonexistent-agent",
|
|
2893
|
+
"--discard-session",
|
|
2894
|
+
"--workspace",
|
|
2895
|
+
&ws.to_string_lossy(),
|
|
2896
|
+
]);
|
|
2604
2897
|
let err = reset_agent_args(&argv, &ws).expect_err("must refuse");
|
|
2605
2898
|
assert!(
|
|
2606
2899
|
err.to_string().contains("multiple alive teams"),
|
|
@@ -2609,10 +2902,10 @@ mod tests {
|
|
|
2609
2902
|
}
|
|
2610
2903
|
|
|
2611
2904
|
#[test]
|
|
2612
|
-
fn
|
|
2905
|
+
fn remove_agent_complete_input_refuses_multi_alive_team_before_agent_validation() {
|
|
2613
2906
|
let ws = tmp_workspace();
|
|
2614
2907
|
seed_two_alive_teams_in(&ws);
|
|
2615
|
-
let argv = cli_argv(&["--workspace", &ws.to_string_lossy()]);
|
|
2908
|
+
let argv = cli_argv(&["nonexistent-agent", "--workspace", &ws.to_string_lossy()]);
|
|
2616
2909
|
let err = remove_agent_args(&argv, &ws).expect_err("must refuse");
|
|
2617
2910
|
assert!(
|
|
2618
2911
|
err.to_string().contains("multiple alive teams"),
|