@team-agent/installer 0.5.110 → 0.5.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +231 -812
  4. package/crates/team-agent/src/cli/diagnose.rs +2 -424
  5. package/crates/team-agent/src/cli/emit.rs +493 -451
  6. package/crates/team-agent/src/cli/leader.rs +4 -9
  7. package/crates/team-agent/src/cli/leaders.rs +90 -29
  8. package/crates/team-agent/src/cli/mod.rs +348 -169
  9. package/crates/team-agent/src/cli/models.rs +80 -24
  10. package/crates/team-agent/src/cli/named_address.rs +114 -25
  11. package/crates/team-agent/src/cli/route.rs +10 -5
  12. package/crates/team-agent/src/cli/send/presentation.rs +60 -41
  13. package/crates/team-agent/src/cli/send/resolve.rs +25 -18
  14. package/crates/team-agent/src/cli/send.rs +4 -4
  15. package/crates/team-agent/src/cli/spec.rs +53 -109
  16. package/crates/team-agent/src/cli/tests/base.rs +189 -82
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -2
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
  21. package/crates/team-agent/src/cli/tests/mod.rs +0 -2
  22. package/crates/team-agent/src/cli/tests/named_address.rs +19 -22
  23. package/crates/team-agent/src/cli/tests/status_send.rs +15 -10
  24. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +2 -2
  25. package/crates/team-agent/src/cli/types.rs +26 -121
  26. package/crates/team-agent/src/coordinator/health.rs +3 -283
  27. package/crates/team-agent/src/coordinator/mod.rs +0 -5
  28. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
  29. package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
  30. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
  31. package/crates/team-agent/src/coordinator/types.rs +1 -21
  32. package/crates/team-agent/src/db/message_store.rs +0 -42
  33. package/crates/team-agent/src/db/migration.rs +49 -9
  34. package/crates/team-agent/src/db/schema.rs +27 -20
  35. package/crates/team-agent/src/leader/owner_bind.rs +0 -24
  36. package/crates/team-agent/src/leader/registry.rs +3 -3
  37. package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
  38. package/crates/team-agent/src/leader/tests/identity.rs +0 -32
  39. package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
  40. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +4 -9
  41. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
  42. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -1
  43. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
  44. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
  45. package/crates/team-agent/src/messaging/mod.rs +0 -2
  46. package/crates/team-agent/src/messaging/results.rs +0 -58
  47. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
  48. package/crates/team-agent/src/state/repository.rs +1 -5
  49. package/package.json +4 -4
  50. package/templates/team_state.md +1 -1
  51. package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
  52. package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
  53. package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
  54. package/crates/team-agent/src/messaging/peers.rs +0 -34
@@ -54,8 +54,17 @@ 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
- return emit_missing_subcommand_usage();
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
+ }
59
68
  if command == "route" {
60
69
  return super::route::run(&argv[1..]);
61
70
  }
@@ -101,7 +110,44 @@ fn emit_cli_error_for_command(
101
110
  cwd: &Path,
102
111
  error: &CliError,
103
112
  ) -> ExitCode {
104
- if command == "status" {
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" {
105
151
  emit_status_cli_error(error)
106
152
  } else {
107
153
  emit_cli_error(command, args, cwd, error)
@@ -133,7 +179,7 @@ pub(super) fn emit_result(r: CmdResult) -> ExitCode {
133
179
  let mut stderr = stderr.lock();
134
180
  let _ = writeln!(
135
181
  stderr,
136
- "stdout unavailable after durable send; persisted_message_id={message_id}"
182
+ "输出暂不可用;消息已保存,消息编号={message_id}。用 team-agent inbox leader -n 3 查看回复,不要重发。"
137
183
  );
138
184
  }
139
185
  return if error.kind() == ErrorKind::BrokenPipe {
@@ -174,6 +220,16 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
174
220
  .cloned()
175
221
  .collect();
176
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
+ }
177
233
  let Some(spec) = command_spec(command) else {
178
234
  return Ok(emit_unknown_subcommand_usage(command));
179
235
  };
@@ -183,12 +239,28 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
183
239
  return Ok(emit_unknown_subcommand_usage(command));
184
240
  }
185
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
+ | "profile"
255
+ )
256
+ {
257
+ return Err(CliError::Usage(
258
+ "请填写队友名、任务内容或命令所需设置;见下方用法与 Examples".to_string(),
259
+ ));
260
+ }
186
261
  match command {
187
262
  "quick-start" => cmd_quick_start(&quick_start_args(args, cwd)?).map(emit_result),
188
263
  "send" => cmd_send(&send_args(args, cwd)?).map(emit_result),
189
- "allow-peer-talk" => {
190
- cmd_allow_peer_talk(&allow_peer_talk_args(args, cwd)?).map(emit_result)
191
- }
192
264
  "status" => cmd_status_for_team(&status_args(args, cwd), parse_args(args).team.as_deref())
193
265
  .map(emit_result),
194
266
  "shutdown" => cmd_shutdown(&shutdown_args(args, cwd)?).map(emit_result),
@@ -204,16 +276,11 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
204
276
  "claim-leader" => cmd_claim_leader(&claim_leader_args(args, cwd)).map(emit_result),
205
277
  // Real dispatch: `cmd_attach_leader` writes the `leader_receiver` binding.
206
278
  "attach-leader" => cmd_attach_leader(&attach_leader_args(args, cwd)?).map(emit_result),
207
- "attach-app-server-leader" => {
208
- cmd_attach_app_server_leader(&attach_app_server_leader_args(args, cwd)?)
209
- .map(emit_result)
210
- }
211
- "identity" => cmd_identity(&identity_args(args, cwd)).map(emit_result),
279
+
212
280
  "approvals" => cmd_approvals(&approvals_args(args, cwd)).map(emit_result),
213
281
  "inbox" => cmd_inbox(&inbox_args(args, cwd)?).map(emit_result),
214
282
  "doctor" => cmd_doctor(&doctor_args(args, cwd)).map(emit_result),
215
- "watch" => cmd_watch(&watch_args(args, cwd)).map(emit_result),
216
- "sessions" => cmd_sessions(&sessions_args(args, cwd)).map(emit_result),
283
+
217
284
  // 0.5.9 E7 host-leader-registry: `leaders` is the host-level derived
218
285
  // discovery command. It reads ~/.team-agent/leaders, validates each
219
286
  // entry against canonical state, and reports LIVE/STALE/AMBIGUOUS
@@ -223,16 +290,30 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
223
290
  // delivery path — no separate route authority.
224
291
  "leaders" => cmd_leaders(&leaders_args(args, cwd)?).map(emit_result),
225
292
  "models" => cmd_models(&models_args(args)?).map(emit_result),
226
- "validate" => cmd_validate(&validate_args(args, cwd)).map(emit_result),
293
+
227
294
  "install-skill" => cmd_install_skill(&install_skill_args(args)?).map(emit_result),
228
295
  "profile" => cmd_profile(&profile_args(args, cwd)?).map(emit_result),
229
- "results" => cmd_results(&results_args(args, cwd)?).map(emit_result),
296
+
297
+ _ => Ok(emit_unknown_subcommand_usage(command)),
298
+ }
299
+ }
300
+
301
+ // Script compatibility is deliberately outside the human catalog and suggestions.
302
+ fn is_machine_command(command: &str) -> bool {
303
+ matches!(
304
+ command,
305
+ "wait" | "coordinator" | "attach-app-server-leader"
306
+ )
307
+ }
308
+
309
+ fn dispatch_machine(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliError> {
310
+ match command {
230
311
  "wait" => cmd_wait(&wait_args(args, cwd)?).map(emit_result),
231
- "preflight" => cmd_preflight(&preflight_args(args, cwd)).map(emit_result),
232
- "wait-ready" => cmd_wait_ready(&wait_ready_args(args, cwd)).map(emit_result),
233
- "e2e" => cmd_e2e(&e2e_args(args, cwd)).map(emit_result),
234
- "peek" => cmd_peek(&peek_args(args, cwd)?).map(emit_result),
235
312
  "coordinator" => run_coordinator(args, cwd),
313
+ "attach-app-server-leader" => {
314
+ cmd_attach_app_server_leader(&attach_app_server_leader_args(args, cwd)?)
315
+ .map(emit_result)
316
+ }
236
317
  _ => Ok(emit_unknown_subcommand_usage(command)),
237
318
  }
238
319
  }
@@ -250,11 +331,6 @@ pub(crate) fn is_leader_passthrough_command(command: &str) -> bool {
250
331
  )
251
332
  }
252
333
 
253
- fn emit_missing_subcommand_usage() -> ExitCode {
254
- emit_usage_error("the following arguments are required: {codex,claude,...,doctor}");
255
- ExitCode::Usage
256
- }
257
-
258
334
  /// Registered subcommands (the dispatch table) plus spec-only verbs that have no
259
335
  /// dispatch arm yet but must still respond to `--help` per CR-063/G4.
260
336
  /// Used by the `--help` short-circuit gate so unknown commands keep falling through
@@ -264,41 +340,43 @@ fn is_known_subcommand(command: &str) -> bool {
264
340
  }
265
341
 
266
342
  pub(crate) fn default_help() -> String {
267
- let mut out = String::from("usage: team-agent <command> [options]\n");
343
+ 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");
268
344
  append_help_section(
269
345
  &mut out,
270
- "Core",
271
- &[
272
- "quick-start",
273
- "send",
274
- "status",
275
- "results",
276
- "models",
277
- "leaders",
278
- ],
346
+ "开始协作",
347
+ &["quick-start", "send", "status", "models", "inbox"],
279
348
  );
349
+ append_help_section(&mut out, "队伍管理", &["restart", "shutdown"]);
280
350
  append_help_section(
281
351
  &mut out,
282
- "Lifecycle",
352
+ "队友管理",
283
353
  &[
284
- "restart",
285
- "shutdown",
286
354
  "add-agent",
287
355
  "start-agent",
288
356
  "stop-agent",
289
357
  "reset-agent",
358
+ "clone-agent",
359
+ "fork-agent",
360
+ "remove-agent",
290
361
  ],
291
362
  );
292
- append_help_section(&mut out, "Diagnose", &["doctor"]);
293
363
  append_help_section(
294
364
  &mut out,
295
- "Guided recovery",
365
+ "观察与协作",
366
+ &["leaders", "doctor", "approvals"],
367
+ );
368
+ append_help_section(&mut out, "设置", &["route", "profile", "install-skill"]);
369
+ append_help_section(
370
+ &mut out,
371
+ "按体检提示恢复",
296
372
  &["claim-leader", "takeover", "attach-leader"],
297
373
  );
298
- out.push_str("\nWorkflow: `team-agent models --provider pi`, then quick-start/add-agent/start-agent, then `team-agent send AGENT MESSAGE`.\n");
299
- out.push_str("Model catalogs: pi, cursor_agent, codex, claude, claude_code, grok.\n");
300
- out.push_str("Provider launchers:\n team-agent codex|claude|copilot|grok|cursor|pi ...\n");
301
- out.push_str("\nRun `team-agent <command> --help` for command flags.");
374
+ append_help_section(
375
+ &mut out,
376
+ "启动主控",
377
+ &["pi", "codex", "claude", "copilot", "grok", "cursor"],
378
+ );
379
+ out.push_str("\n每个操作的参数和例子:team-agent <command> --help");
302
380
  out
303
381
  }
304
382
 
@@ -332,71 +410,107 @@ pub fn __test_quick_start_args(
332
410
  quick_start_args(args, cwd)
333
411
  }
334
412
 
335
- fn command_help(command: Option<&str>) -> String {
336
- match command {
337
- None => default_help(),
338
- Some("quick-start") => "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--backend tmux|conpty] [--json] [--detail]\n\n--backend selects the worker transport: tmux (default on POSIX) or conpty (Windows-native ConPTY worker transport; requires the shim binary and Windows host).\n\n--detail includes internal receiver/topology diagnostics in JSON output.\n\nAfter a successful start, use the returned `send_commands` (or choose an agent explicitly) with `team-agent send AGENT MESSAGE`.".to_string(),
339
- Some("send") => concat!(
340
- "usage: team-agent send TO MESSAGE... ",
341
- "[--workspace WORKSPACE] [--team TEAM] ",
342
- "[--mailbox] ",
343
- "[--json]\n\n",
344
- "TO is a logical recipient; send returns after the message is persisted. ",
345
- "--mailbox stores durably without live injection; omitted sends to the live conversation."
346
- )
347
- .to_string(),
348
- Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
349
- Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n输出九字段:name/provider/model/effort/runtime_status/activity/health/session_name/tmux_command;人读与 --json 使用同一投影字段(多节点人读紧凑显示 tmux_command)。--json 始终保留每节点完整可复制的 tmux_command;人读单节点保留完整旧格式,多节点按(socket, tmux session)显示每组一次共享 attach 模板,ATTACH列为<group>:<window>.<pane>,模板中的 <target> 替换为其 window.pane 部分。model/effort 为已接受启动配置,未设置为 null,不猜 provider 默认值;缺少可靠定位或原生 tmux/process 采样时显示 unknown,不推导attach信息。--summary/--detail 仅保留兼容性,不增加诊断字段。".to_string(),
350
- Some("route") => super::route::HELP.to_string(),
351
- Some("models") => "usage: team-agent models [--provider pi|cursor_agent|codex|claude|claude_code|grok] [QUERY|--search TEXT] [--json]\n\nLists exact provider model ids with case-insensitive multi-word search across provider, vendor, id, display name, and source aliases. Uses each provider's native catalog; Cursor uses `agent --list-models`, Codex uses `codex debug models`, Claude uses SDK stream-json initialize, and Grok uses the PATH-first `grok models` command.".to_string(),
352
- Some("leaders") => "usage: team-agent leaders [QUERY|--search TEXT] [--all|--stale] [--json] | --prune [--dry-run] [--json]\n\nLists LIVE leaders by default. Use --all to include retained STALE entries, --stale to inspect only STALE entries, QUERY or --search TEXT to match workspace/team/name fields, and --prune to remove only entries proven terminal by canonical state. --dry-run is valid only with --prune.".to_string(),
353
- Some("shutdown") => "usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]".to_string(),
354
- Some("restart") => "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json] [--detail]".to_string(),
355
- Some("reset-agent") => "usage: team-agent reset-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--discard-session] [--json]".to_string(),
356
- Some(name @ ("start-agent" | "add-agent")) => command_spec(name)
357
- .map(|spec| format!(
358
- "{}\n\n{}\n\nAfter a successful {}, use the returned `send_commands` with `team-agent send AGENT MESSAGE`.",
359
- spec.usage,
360
- spec.summary,
361
- if name == "start-agent" { "start" } else { "add" },
362
- ))
363
- .unwrap_or_else(|| format!("usage: team-agent {name} [options]")),
364
- Some("stop-agent") => "usage: team-agent stop-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
365
- Some("clone-agent") => "usage: team-agent clone-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
366
- Some("fork-agent") => "usage: team-agent fork-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
367
- Some("remove-agent") => "usage: team-agent remove-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--from-spec] [--confirm] [--force] [--json]".to_string(),
368
- // 0.5.26 (§7.6): removed from help; dispatch was never wired.
369
- Some("takeover") => "usage: team-agent takeover [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]".to_string(),
370
- Some("claim-leader") => "usage: team-agent claim-leader [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json] [--detail]".to_string(),
371
- Some("attach-leader") => "usage: team-agent attach-leader [--workspace WORKSPACE] [--team TEAM] [--pane PANE] [--provider PROVIDER] [--confirm] [--json]".to_string(),
372
- Some("attach-app-server-leader") => "usage: team-agent attach-app-server-leader [--workspace WORKSPACE] [--team TEAM] --socket unix:///path.sock --thread-id THREAD_ID [--json]".to_string(),
373
- Some("identity") => "usage: team-agent identity [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
374
- Some("approvals") => "usage: team-agent approvals [AGENT] [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
375
- Some("inbox") => "usage: team-agent inbox AGENT [-n N|--limit N] [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
376
- Some("doctor") => "usage: team-agent doctor [SPEC] [--workspace WORKSPACE] [--team TEAM] [--gate orphans|comms] [--comms] [--fix] [--fix-schema] [--cleanup-orphans] [--confirm] [--json]".to_string(),
377
- Some("watch") => "usage: team-agent watch [--workspace WORKSPACE] [--team TEAM]".to_string(),
378
- Some("sessions") => "usage: team-agent sessions [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
379
- Some("validate") => "usage: team-agent validate [SPEC] [--json]".to_string(),
380
- Some("install-skill") => "usage: team-agent install-skill (--source DIR | --uninstall) [--target codex|claude|copilot|all] [--dest DIR] [--dry-run] [--json]".to_string(),
381
- Some("profile") => "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--proxy-mode direct|inherit] [--json]".to_string(),
382
- Some("results") => "usage: team-agent results --case CASE_ID [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
383
- Some("wait") => "usage: team-agent wait --task TASK [--workspace WORKSPACE] [--json]".to_string(),
384
- Some("preflight") => "usage: team-agent preflight [TEAMDIR] [--json]".to_string(),
385
- Some("wait-ready") => "usage: team-agent wait-ready [--workspace WORKSPACE] [--team TEAM] [--timeout SECONDS] [--json]".to_string(),
386
- Some("e2e") => "usage: team-agent e2e [--workspace WORKSPACE] [--providers LIST] [--real] [--json]".to_string(),
387
- Some("peek") => "usage: team-agent peek AGENT [--workspace WORKSPACE] [--tail N|--head N] [--search TEXT] [--allow-raw-screen] [--json]".to_string(),
388
- Some("coordinator") => "usage: team-agent coordinator [--workspace WORKSPACE] [--once] [--tick-interval SECONDS]".to_string(),
389
- Some(other) => format!("usage: team-agent {other} [options]"),
413
+ pub(super) const TEAM_TEMPLATE: &str =
414
+ "---\nname: help-demo\n---\nA small team for a command-line example.\n";
415
+ 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";
416
+ const HUMAN_NAVIGATION: &str = "请用 team-agent --help 查看可用操作。";
417
+
418
+ pub(super) fn command_help(command: Option<&str>) -> String {
419
+ let Some(name) = command else {
420
+ return default_help();
421
+ };
422
+ let Some(spec) = command_spec(name) else {
423
+ return HUMAN_NAVIGATION.to_string();
424
+ };
425
+ if name == "route" {
426
+ return super::route::HELP.to_string();
427
+ }
428
+ let (details, examples, next) = match name {
429
+ "quick-start" => (
430
+ "TEAMDIR 是含 TEAM.md 和 agents/ 的角色目录;--workspace 选择运行项目。\n--name 设置队伍名称;--team/--team-id 选择队伍;--yes 确认已有提示;--detail 查看详细返回。\n工具须已安装并登录;工具/模型/思考强度写在角色文件中;跳过权限询问默认 false。\n--backend tmux 用于 POSIX;Windows 的 conpty 需要 Windows 主机和已安装的 ConPTY shim。",
431
+ "team-agent quick-start\nteam-agent quick-start ./roles --workspace ./project\nteam-agent quick-start . --json",
432
+ "确认 Pi 已安装并登录,用 team-agent models --provider pi 核对模型。\n运行 team-agent pi,在该主控的命令行/工具上下文再运行 quick-start;已有队伍看 restart --help。"),
433
+ "send" => ("<agent> 是 status 中的队友名;MESSAGE 是任务内容。\n--mailbox 只留言,不发送到当前对话;默认发送到队友对话。",
434
+ "team-agent send worker '计算 245 × 37,把答案回复给 leader。'\nteam-agent send worker '检查改动' --workspace . --team help-demo\nteam-agent send worker '下次上线时查看' --mailbox",
435
+ "已收下任务不等于送达或完成。等真实回复,用 team-agent inbox leader -n 3;有疑问看 status/doctor,不反复重发。"),
436
+ "inbox" => ("<agent> 是收信队友名;-n/--limit 限制条数。查看回复不触发重发。",
437
+ "team-agent inbox leader -n 3\nteam-agent inbox worker --limit 5\nteam-agent inbox leader --workspace . --team help-demo --json", "没有回复时先等队友自然返回,再看 team-agent status;不要读取队友终端内容。"),
438
+ "status" => ("可选 <agent> 只查看一位队友;--summary/--detail 保留兼容,不增加字段。\n九字段:name/provider/model/effort/runtime_status/activity/health/session_name/tmux_command。\n运行状态、忙闲、健康各有含义;unknown 表示证据不足,null 表示未设置,不猜默认模型。",
439
+ "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 .。"),
440
+ "models" => ("--provider 选择工具,默认 pi;QUERY 或 --search 按关键词搜索,不能同时用。\nCursor 的工具名是 cursor_agent;Copilot 暂无此模型查询入口。返回真实目录,不猜模型。",
441
+ "team-agent models --provider pi\nteam-agent models --provider pi --search luna\nteam-agent models --provider cursor_agent --json", "把返回的完整模型名称填入角色文件或 add-agent --model,再起队或加人。"),
442
+ "restart" => ("WORKSPACE 默认当前项目;优先恢复已保存会话。\n只有用户明确允许丢弃旧对话时才加 --allow-fresh;--session-converge-deadline 设置等待秒数。",
443
+ "team-agent restart .\nteam-agent restart . --team help-demo --json", "用 status 查看恢复结果;需要新会话先征得用户同意,不用 takeover 代替恢复。"),
444
+ "shutdown" => ("只关闭所选项目/队伍;日志默认保留,--keep-logs 保留兼容。\n--json 查看关闭范围、降级情况及本队残留,返回成功不代表其他队伍已关闭。",
445
+ "team-agent shutdown --workspace .\nteam-agent shutdown --workspace . --team help-demo --json", "核对本队残留为空,再用 team-agent doctor --workspace .;不要广域清理其他队伍。"),
446
+ "add-agent" => ("<agent> 是新队友名;--provider 选择工具;--model 模型名称;--effort 思考强度。\n--bypass 是否跳过权限询问,示例 false;--prompt 任务职责;--profile 登录/代理设置。\n工具和 bypass 必须由参数或 --role-file 明确提供;冲突会拒绝,不猜设置。--force 替换已有队友,仅在用户明确授权后用。",
447
+ "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 '检查改动'。"),
448
+ "start-agent" => ("只启动已有队友;仍在运行时先 stop-agent。\n--provider 工具;--model 模型名称;--effort 思考强度;--bypass 是否跳过权限询问。\n--prompt/--profile 更换职责/登录设置;只有明确获准丢弃旧对话才用 --allow-fresh。",
449
+ "team-agent start-agent worker\nteam-agent start-agent worker --model openai-codex/gpt-6-luna", "新增队友用 add-agent;启动后看 status,再 send 分派任务。"),
450
+ "stop-agent" => ("<agent> 指定要暂停的队友;暂停不是删除,配置与会话记录保留。",
451
+ "team-agent stop-agent worker\nteam-agent stop-agent worker --workspace . --team help-demo --json", "用 status 确认停止;恢复用 team-agent start-agent worker。"),
452
+ "reset-agent" => ("必须明确加 --discard-session;此操作会停止旧进程、清除保存的会话关联,并尝试以新会话重新启动。已暂停的队友不会自动启动;不承诺删除工具的全部历史记录。",
453
+ "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。"),
454
+ "clone-agent" => ("<agent> 是源队友;--as 新队友名;--label 可读标签。\n复制配置创建新队友,不复制完整对话。",
455
+ "team-agent clone-agent worker --as reviewer\nteam-agent clone-agent worker --as reviewer --label '审阅队友' --workspace .", "用 status 查看新队友,再 send reviewer 分派独立任务。"),
456
+ "fork-agent" => ("<agent> 是源队友;--as 新队友名;--label 可读标签。\n需要源会话已保存且工具支持分支;不满足条件会拒绝,不保证所有工具都可分支。",
457
+ "team-agent fork-agent worker --as experiment\nteam-agent fork-agent worker --as experiment --workspace . --team help-demo --json", "分支成功会占用新资源;看 status,再向 experiment 派发任务,用完停止或移除。"),
458
+ "remove-agent" => ("必须 --confirm;配置定义的队友还需 --from-spec,动态队友可以不加。\n只删除托管角色副本,不删除外部用户角色文件;运行中的队友先 stop-agent。\n--force 可停止并移除运行中的队友,仅在理解风险且明确授权后使用。",
459
+ "team-agent remove-agent reviewer --confirm\nteam-agent remove-agent reviewer --from-spec --confirm --workspace . --team help-demo --json", "先保存所需回复;移除后核对 status 和实际配置范围,不对其他队友使用强制清理。"),
460
+ "leaders" => ("默认列出可用主控;--all 包含已失效登记,--stale 只看失效项。\nQUERY/--search 按项目/队伍/名称筛选;--prune 仅清理已确认退役的登记,--dry-run 先预览。",
461
+ "team-agent leaders\nteam-agent leaders --all --json\nteam-agent leaders --prune --dry-run", "根据项目和队伍选择 --workspace/--team;清理登记不等于关闭进程。"),
462
+ "doctor" => ("默认只检查,不修复;SPEC 可选配置路径。\n--comms/--gate comms 检查连接,不证明队友实际回复;--gate orphans 检查残留。\n--fix/--fix-schema/--cleanup-orphans 仅在获准的范围内使用;危险修复需要 --confirm。",
463
+ "team-agent doctor --workspace .\nteam-agent doctor --workspace . --team help-demo --json", "按实际问题与所选队伍范围处理;不要把体检建议当成已执行的修复。"),
464
+ "approvals" => ("可选 <agent> 查看一位队友;仅观察权限询问,不自动批准。",
465
+ "team-agent approvals\nteam-agent approvals worker --json", "确认权限用途后,在工具的实际权限询问处处理,再看 status。"),
466
+ "profile" => ("init 创建配置,doctor 检查配置,show 显示设置;NAME 是配置名。\n--auth-mode 登录方式;--proxy-mode direct 不走代理、inherit 沿用环境代理。\n配置存放在所选工作区,不因 --team 自动变成队伍独立配置;不要输出凭证。",
467
+ "team-agent profile init local --auth-mode subscription\nteam-agent profile doctor local\nteam-agent profile show local", "检查通过后用 add-agent/start-agent --profile local;需要登录时使用工具原生登录入口。"),
468
+ "install-skill" => ("--source 必须是已核实的指南目录;--target 选择 codex/claude/copilot/all。\n--dest 覆盖安装位置;--dry-run 只预览;--uninstall 移除目标指南。此项不是起队前提。",
469
+ "team-agent install-skill --source \"$SKILL_DIR\" --target codex --dry-run\nteam-agent install-skill --source \"$SKILL_DIR\" --target codex", "先把 SKILL_DIR 设为实际已核验目录;核对预览后再安装,不覆盖未知用户文件。"),
470
+ "claim-leader" => ("仅在 doctor 明确提示登记主控、且当前终端确为主控时使用。\n--confirm 表示授权登记;不会自动打开新工具。",
471
+ "team-agent claim-leader --workspace . --json\nteam-agent claim-leader --workspace . --team help-demo --confirm --json", "先核对 doctor 与当前终端归属;未确认前的拒绝不是一次成功登记。"),
472
+ "takeover" => ("仅在 doctor 提示接管且用户已授权时使用;接管会修改主控归属。\n有可用的队伍终端时,即使省略 --confirm 也可能执行接管;不能用作只读预演。正常恢复用 restart。",
473
+ "team-agent takeover --workspace . --team help-demo --confirm\nteam-agent takeover --workspace . --team help-demo --confirm --json", "核实主控和队伍归属再确认;完成后查看 status/doctor,不接管其他人的队伍。"),
474
+ "attach-leader" => ("PANE 必须来自实际核验的主控终端,不能猜终端编号。\n--provider 选择实际工具;--confirm 表示授权连接已有主控,仅按 doctor 提示使用。",
475
+ "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 确认,再派发任务。"),
476
+ _ => ("先确保工具已安装并完成原生登录;当前目录作为项目。\n--attach-existing 与 --confirm 只在已核验主控归属并明确授权后使用;--attach-session 选择已核验的会话。\n--external-leader 选择外部主控;--allow-nested-attach 仅在明确授权使用嵌套终端时添加。\n-- 后面的参数原样交给工具;纯 --help/-h 只显示这里的帮助,不启动工具。",
477
+ "", "在打开的主控命令行/工具上下文运行 team-agent quick-start;不是在普通队友对话中起队。"),
478
+ };
479
+ let examples = if matches!(spec.kind, CommandKind::LeaderPassthrough { .. }) {
480
+ let native = if name == "cursor" { "agent" } else { name };
481
+ if name == "pi" {
482
+ "team-agent pi\nteam-agent pi -- --model openai-codex/gpt-6-luna\npi --help".to_string()
483
+ } else {
484
+ format!("team-agent {name}\nteam-agent {name} --help\n{native} --help")
485
+ }
486
+ } else {
487
+ examples.to_string()
488
+ };
489
+ let scope = if matches!(spec.kind, CommandKind::LeaderPassthrough { .. }) {
490
+ "--json 给程序读取;不把 --workspace/--team 当作主控选项传入,它们会交给原生工具。"
491
+ } else {
492
+ "--json 给程序读取;--workspace 选项目,支持 --team 的操作用它选队伍,不猜对象。"
493
+ };
494
+ 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);
495
+ if name == "quick-start" {
496
+ out.push_str(&format!("\n\n最简两文件(请自行创建,不会自动写入):\nTEAM.md:\n{TEAM_TEMPLATE}\nagents/worker.md:\n{WORKER_TEMPLATE}"));
390
497
  }
498
+ if name == "cursor" {
499
+ out.push_str("\n查询模型用 team-agent models --provider cursor_agent。");
500
+ }
501
+ if name == "copilot" {
502
+ out.push_str("\nCopilot 暂无 team-agent models 查询入口,请看工具原生帮助。");
503
+ }
504
+ out
391
505
  }
392
506
 
393
507
  fn emit_unknown_subcommand_usage(command: &str) -> ExitCode {
394
508
  emit_usage_error(&format!(
395
- "argument {{codex,claude,...,doctor}}: invalid choice: '{command}' (choose from codex, claude, ..., doctor)"
509
+ "没有这个操作:'{command}'。请用 team-agent --help 查看可用操作。"
396
510
  ));
397
511
  // E8 (N38): 错路引导 —— 拼写近似时建议最接近的真子命令(additive,不改既有 golden 行)。
398
512
  if let Some(suggestion) = nearest_subcommand(command) {
399
- eprintln!("team-agent: did you mean `{suggestion}`?");
513
+ eprintln!("你是否想使用 `{suggestion}`?运行 team-agent {suggestion} --help 查看例子。");
400
514
  }
401
515
  // 0.5.45 naming-addressing (RED-6): unknown subcommand exits 1
402
516
  // (Error), aligned with the family of typo refusals throughout
@@ -428,11 +542,10 @@ fn nearest_subcommand(input: &str) -> Option<&'static str> {
428
542
  }
429
543
 
430
544
  fn emit_usage_error(message: &str) {
431
- eprintln!("usage: team-agent [-h] {{codex,claude,...,doctor}} ...");
432
- eprintln!("team-agent: error: {message}");
545
+ eprintln!("用法:team-agent <command>;查看所有操作:team-agent --help");
546
+ eprintln!("错误:{message}");
433
547
  }
434
548
 
435
- /// `cmd_validate` delegates to runtime validate_file.
436
549
  /// `install-skill` 参数(RED-1 根治:把 skill 安装单源收敛到二进制,install.mjs 调它)。
437
550
  struct InstallSkillArgs {
438
551
  target: crate::packaging::SkillTarget,
@@ -548,93 +661,10 @@ fn cmd_install_skill(args: &InstallSkillArgs) -> Result<CmdResult, CliError> {
548
661
  ))
549
662
  }
550
663
 
551
- pub fn cmd_validate(args: &ValidateArgs) -> Result<CmdResult, CliError> {
552
- let spec = resolve_path(&args.spec);
553
- let value = if spec.is_dir() {
554
- validate_team_dir(&spec)?
555
- } else {
556
- validate_spec_file(&spec)?
557
- };
558
- Ok(CmdResult::from_json(value, args.json))
559
- }
560
-
561
- fn validate_spec_file(spec_path: &Path) -> Result<Value, CliError> {
562
- let text = std::fs::read_to_string(spec_path)?;
563
- let base_dir = spec_path.parent().unwrap_or_else(|| Path::new("."));
564
- let spec =
565
- crate::model::spec::load_and_validate_spec(&text, base_dir).map_err(model_error_to_cli)?;
566
- let team = spec
567
- .get("team")
568
- .and_then(|team| team.get("name"))
569
- .and_then(crate::model::yaml::Value::as_str)
570
- .unwrap_or("");
571
- let workspace = spec
572
- .get("team")
573
- .and_then(|team| team.get("workspace"))
574
- .and_then(crate::model::yaml::Value::as_str)
575
- .map(PathBuf::from)
576
- .unwrap_or_else(|| base_dir.to_path_buf());
577
- let mut obj = Map::new();
578
- obj.insert("ok".to_string(), Value::Bool(true));
579
- obj.insert("team".to_string(), Value::String(team.to_string()));
580
- obj.insert(
581
- "workspace".to_string(),
582
- Value::String(workspace.to_string_lossy().to_string()),
583
- );
584
- Ok(Value::Object(obj))
585
- }
586
-
587
- fn validate_team_dir(team_dir: &Path) -> Result<Value, CliError> {
588
- let spec = crate::compiler::compile_team(team_dir).map_err(model_error_to_cli)?;
589
- let team = spec
590
- .get("team")
591
- .and_then(|team| team.get("name"))
592
- .and_then(crate::model::yaml::Value::as_str)
593
- .unwrap_or("");
594
- let workspace = spec
595
- .get("team")
596
- .and_then(|team| team.get("workspace"))
597
- .and_then(crate::model::yaml::Value::as_str)
598
- .map(PathBuf::from)
599
- .unwrap_or_else(|| team_dir.to_path_buf());
600
- let agents = spec
601
- .get("agents")
602
- .and_then(crate::model::yaml::Value::as_list)
603
- .map(|agents| {
604
- agents
605
- .iter()
606
- .filter_map(|agent| {
607
- agent
608
- .get("id")
609
- .and_then(crate::model::yaml::Value::as_str)
610
- .map(|id| Value::String(id.to_string()))
611
- })
612
- .collect::<Vec<_>>()
613
- })
614
- .unwrap_or_default();
615
- let mut obj = Map::new();
616
- obj.insert("ok".to_string(), Value::Bool(true));
617
- obj.insert("type".to_string(), Value::String("team_dir".to_string()));
618
- obj.insert(
619
- "workspace".to_string(),
620
- Value::String(workspace.to_string_lossy().to_string()),
621
- );
622
- obj.insert("team".to_string(), Value::String(team.to_string()));
623
- obj.insert("agents".to_string(), Value::Array(agents));
624
- Ok(Value::Object(obj))
625
- }
626
-
627
664
  fn resolve_path(path: &Path) -> PathBuf {
628
665
  std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
629
666
  }
630
667
 
631
- fn model_error_to_cli(error: crate::model::errors::ModelError) -> CliError {
632
- match error {
633
- crate::model::errors::ModelError::Validation(message) => CliError::Runtime(message),
634
- other => CliError::Runtime(other.to_string()),
635
- }
636
- }
637
-
638
668
  fn emit_cli_error(command: &str, args: &[String], cwd: &Path, error: &CliError) -> ExitCode {
639
669
  let parsed = parse_args(args);
640
670
  let workspace = workspace(&parsed, cwd);
@@ -648,6 +678,36 @@ fn emit_cli_error(command: &str, args: &[String], cwd: &Path, error: &CliError)
648
678
  let _ = std::fs::write(&log_path, format!("{safe_error}\n"));
649
679
  let mut payload = payload_error.to_payload(&log_path, command);
650
680
  payload.error = safe_error;
681
+ if let CliError::AddressRefusal { error, action, .. } = payload_error {
682
+ if !has_arg(args, "--json") {
683
+ payload.error = crate::redaction::redact_external_text(error);
684
+ payload.action = action.clone();
685
+ }
686
+ } else if let Some(reason) = payload
687
+ .error
688
+ .strip_prefix("usage error: ")
689
+ .unwrap_or(&payload.error)
690
+ .strip_prefix("Error: ")
691
+ .and_then(|text| text.split_once(':').map(|(reason, _)| reason))
692
+ {
693
+ if let Some(message) = super::named_address::human_address_reason(reason) {
694
+ payload.error = message.to_string();
695
+ payload.action = "先运行 team-agent status,再用列表中的队友名发送;用 --workspace/--team 选择项目和队伍,连接问题先 doctor。".to_string();
696
+ }
697
+ }
698
+ if command_spec(command).is_some()
699
+ && payload.action
700
+ == "先运行 team-agent doctor --workspace . 检查所选队伍,或查看此处列出的错误日志。"
701
+ {
702
+ let mut doctor = format!(
703
+ "team-agent doctor --workspace {}",
704
+ super::adapters::shell_quote(&workspace.to_string_lossy())
705
+ );
706
+ if let Some(team) = parsed.team.as_deref() {
707
+ doctor.push_str(&format!(" --team {}", super::adapters::shell_quote(team)));
708
+ }
709
+ payload.action = format!("先运行 {doctor} 检查所选队伍,或查看此处列出的错误日志。");
710
+ }
651
711
  payload.action = crate::redaction::redact_external_text(&payload.action);
652
712
  payload.log = crate::redaction::redact_external_text(&payload.log);
653
713
  payload.reason = payload
@@ -669,7 +729,11 @@ fn emit_cli_error(command: &str, args: &[String], cwd: &Path, error: &CliError)
669
729
  } else {
670
730
  eprintln!("error: {}", payload.error);
671
731
  eprintln!("action: {}", payload.action);
672
- eprintln!("log: {}", payload.log);
732
+ if matches!(payload_error, CliError::AddressRefusal { .. }) {
733
+ eprintln!("log: {:?}", payload.log);
734
+ } else {
735
+ eprintln!("log: {}", payload.log);
736
+ }
673
737
  }
674
738
  ExitCode::Error
675
739
  }
@@ -777,14 +841,9 @@ struct ParsedArgs {
777
841
  once: bool,
778
842
  tick_interval: Option<f64>,
779
843
  status_value: Option<String>,
780
- providers: Option<String>,
781
- allow_raw_screen: bool,
782
- tail: Option<usize>,
783
- head: Option<usize>,
784
844
  search: Option<String>,
785
845
  file: Option<PathBuf>,
786
846
  result: Option<String>,
787
- real: bool,
788
847
  assignee: Option<String>,
789
848
  auth_mode: Option<String>,
790
849
  proxy_mode: Option<String>,
@@ -871,14 +930,9 @@ fn parse_args(args: &[String]) -> ParsedArgs {
871
930
  parsed.tick_interval = next_arg(args, &mut i).and_then(|v| v.parse::<f64>().ok())
872
931
  }
873
932
  "--status" => parsed.status_value = next_arg(args, &mut i),
874
- "--providers" => parsed.providers = next_arg(args, &mut i),
875
- "--allow-raw-screen" => parsed.allow_raw_screen = true,
876
- "--tail" => parsed.tail = next_arg(args, &mut i).and_then(|v| v.parse::<usize>().ok()),
877
- "--head" => parsed.head = next_arg(args, &mut i).and_then(|v| v.parse::<usize>().ok()),
878
933
  "--search" => parsed.search = next_arg(args, &mut i),
879
934
  "--file" => parsed.file = next_arg(args, &mut i).map(PathBuf::from),
880
935
  "--result" => parsed.result = next_arg(args, &mut i),
881
- "--real" => parsed.real = true,
882
936
  "--assignee" => parsed.assignee = next_arg(args, &mut i),
883
937
  "--auth-mode" => parsed.auth_mode = next_arg(args, &mut i),
884
938
  "--proxy-mode" => parsed.proxy_mode = next_arg(args, &mut i),
@@ -1006,7 +1060,9 @@ fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliEr
1006
1060
  let path = positional_agents_dir.as_ref()?;
1007
1061
  if let Some(run_workspace) = path
1008
1062
  .ancestors()
1009
- .find(|ancestor| ancestor.file_name().and_then(|name| name.to_str()) == Some(".team"))
1063
+ .find(|ancestor| {
1064
+ ancestor.file_name().and_then(|name| name.to_str()) == Some(".team")
1065
+ })
1010
1066
  .and_then(Path::parent)
1011
1067
  {
1012
1068
  return Some(run_workspace.to_path_buf());
@@ -1027,9 +1083,7 @@ fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliEr
1027
1083
  let normalized = literal.trim().to_ascii_lowercase();
1028
1084
  if normalized != "tmux" && normalized != "conpty" {
1029
1085
  return Err(CliError::Usage(format!(
1030
- "--backend must be `tmux` or `conpty`, got {literal:?}. \
1031
- `pty` is not a supported literal in Phase 1d (design \
1032
- §Non-Goals + CR C-1 ②)."
1086
+ "--backend 只能是 tmux 或 conpty,收到 {literal:?}。POSIX 使用 tmux;Windows ConPTY 需要 Windows 主机和已安装的 shim。"
1033
1087
  )));
1034
1088
  }
1035
1089
  }
@@ -1076,6 +1130,16 @@ fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
1076
1130
  validate_send_flags(args)?;
1077
1131
  warn_send_legacy_delivery_flags(args);
1078
1132
  let parsed = parse_args(args);
1133
+ if parsed.targets.is_none()
1134
+ && parsed.pane.is_none()
1135
+ && parsed.to_name.is_none()
1136
+ && parsed.to_leader.is_none()
1137
+ && parsed.positionals.len() < 2
1138
+ {
1139
+ return Err(CliError::Usage(
1140
+ "请填写收信队友名和任务内容,例如 team-agent send worker '检查改动'".to_string(),
1141
+ ));
1142
+ }
1079
1143
  let target = if parsed.targets.is_some()
1080
1144
  || parsed.pane.is_some()
1081
1145
  || parsed.to_name.is_some()
@@ -1106,8 +1170,8 @@ fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
1106
1170
  presentation_value.as_ref(),
1107
1171
  )
1108
1172
  .map_err(|error| CliError::Usage(format!("invalid send routing: {error}")))?;
1109
- if let Some(deprecation) = normalized.deprecation {
1110
- eprintln!("warning: {deprecation}");
1173
+ if normalized.deprecation.is_some() {
1174
+ eprintln!("提示:旧发送设置已弃用。直接用 team-agent send <agent> '任务内容';只留言请加 --mailbox。");
1111
1175
  }
1112
1176
  Ok(SendArgs {
1113
1177
  target,
@@ -1186,18 +1250,16 @@ fn warn_send_legacy_delivery_flags(args: &[String]) {
1186
1250
  "--message-id",
1187
1251
  ];
1188
1252
  let spec = command_spec("send");
1189
- let sunset = spec
1190
- .and_then(|spec| spec.sunset)
1191
- .unwrap_or("next compatibility release");
1253
+ let sunset = spec.and_then(|spec| spec.sunset).unwrap_or("后续兼容版本");
1192
1254
  let action = spec
1193
1255
  .and_then(|spec| spec.action)
1194
- .unwrap_or("use positional logical TO and the returned message id");
1256
+ .unwrap_or("使用 team-agent send <agent> '任务内容',通过 --workspace/--team 选项目和队伍");
1195
1257
  for flag in FLAGS {
1196
1258
  if args
1197
1259
  .iter()
1198
1260
  .any(|arg| arg == flag || arg.starts_with(&format!("{flag}=")))
1199
1261
  {
1200
- eprintln!("warning: {flag} is deprecated; sunset: {sunset}; action: {action}");
1262
+ eprintln!("提示:{flag} 已弃用,将在{sunset}移除。下一步:{action}");
1201
1263
  }
1202
1264
  }
1203
1265
  }
@@ -1276,17 +1338,6 @@ fn refuse_if_multi_alive_team_missing_scope(
1276
1338
  Ok(())
1277
1339
  }
1278
1340
 
1279
- fn allow_peer_talk_args(args: &[String], cwd: &Path) -> Result<AllowPeerTalkArgs, CliError> {
1280
- let parsed = parse_args(args);
1281
- Ok(AllowPeerTalkArgs {
1282
- a: required_pos(&parsed, 0, "a")?,
1283
- b: required_pos(&parsed, 1, "b")?,
1284
- workspace: workspace(&parsed, cwd),
1285
- json: parsed.json,
1286
- team: parsed.team,
1287
- })
1288
- }
1289
-
1290
1341
  fn status_args(args: &[String], cwd: &Path) -> StatusArgs {
1291
1342
  let parsed = parse_args(args);
1292
1343
  StatusArgs {
@@ -1299,14 +1350,6 @@ fn status_args(args: &[String], cwd: &Path) -> StatusArgs {
1299
1350
  }
1300
1351
  }
1301
1352
 
1302
- fn watch_args(args: &[String], cwd: &Path) -> WatchArgs {
1303
- let parsed = parse_args(args);
1304
- WatchArgs {
1305
- workspace: workspace(&parsed, cwd),
1306
- team: parsed.team,
1307
- }
1308
- }
1309
-
1310
1353
  fn wait_args(args: &[String], cwd: &Path) -> Result<WaitArgs, CliError> {
1311
1354
  let parsed = parse_args(args);
1312
1355
  let workspace = workspace(&parsed, cwd);
@@ -1404,15 +1447,6 @@ fn attach_app_server_leader_args(
1404
1447
  })
1405
1448
  }
1406
1449
 
1407
- fn identity_args(args: &[String], cwd: &Path) -> IdentityArgs {
1408
- let parsed = parse_args(args);
1409
- IdentityArgs {
1410
- workspace: workspace(&parsed, cwd),
1411
- team: parsed.team,
1412
- json: parsed.json,
1413
- }
1414
- }
1415
-
1416
1450
  fn shutdown_args(args: &[String], cwd: &Path) -> Result<ShutdownArgs, CliError> {
1417
1451
  let parsed = parse_args(args);
1418
1452
  let workspace = workspace(&parsed, cwd);
@@ -1444,7 +1478,10 @@ fn restart_args(args: &[String], cwd: &Path) -> Result<RestartArgs, CliError> {
1444
1478
  }
1445
1479
 
1446
1480
  // Keep strict role CLI parsing separate from the legacy shared parser.
1447
- fn role_agent_args(args: &[String], add: bool) -> Result<(ParsedArgs, crate::lifecycle::role_config::RoleConfigPatch), CliError> {
1481
+ fn role_agent_args(
1482
+ args: &[String],
1483
+ add: bool,
1484
+ ) -> Result<(ParsedArgs, crate::lifecycle::role_config::RoleConfigPatch), CliError> {
1448
1485
  let mut role = crate::lifecycle::role_config::RoleConfigPatch::default();
1449
1486
  let mut forwarded = Vec::new();
1450
1487
  let mut seen = std::collections::BTreeSet::new();
@@ -1456,8 +1493,12 @@ fn role_agent_args(args: &[String], add: bool) -> Result<(ParsedArgs, crate::lif
1456
1493
  index += 1;
1457
1494
  continue;
1458
1495
  }
1459
- let (flag, inline) = argument.split_once('=').map_or((argument.as_str(), None), |(key, value)| (key, Some(value)));
1460
- if !seen.insert(flag.to_string()) { return Err(CliError::Usage(format!("duplicate {flag}"))); }
1496
+ let (flag, inline) = argument
1497
+ .split_once('=')
1498
+ .map_or((argument.as_str(), None), |(key, value)| (key, Some(value)));
1499
+ if !seen.insert(flag.to_string()) {
1500
+ return Err(CliError::Usage(format!("duplicate {flag}")));
1501
+ }
1461
1502
  if matches!(flag, "--json" | "--allow-fresh" | "--force") {
1462
1503
  if inline.is_some() || (add && flag == "--allow-fresh") {
1463
1504
  return Err(CliError::Usage(format!("unsupported option: {argument}")));
@@ -1466,26 +1507,46 @@ fn role_agent_args(args: &[String], add: bool) -> Result<(ParsedArgs, crate::lif
1466
1507
  index += 1;
1467
1508
  continue;
1468
1509
  }
1469
- if !matches!(flag, "--provider" | "--model" | "--effort" | "--bypass" | "--prompt" | "--profile" | "--workspace" | "--team" | "--role-file")
1470
- || (!add && flag == "--role-file") {
1510
+ if !matches!(
1511
+ flag,
1512
+ "--provider"
1513
+ | "--model"
1514
+ | "--effort"
1515
+ | "--bypass"
1516
+ | "--prompt"
1517
+ | "--profile"
1518
+ | "--workspace"
1519
+ | "--team"
1520
+ | "--role-file"
1521
+ ) || (!add && flag == "--role-file")
1522
+ {
1471
1523
  return Err(CliError::Usage(format!("unknown option: {flag}")));
1472
1524
  }
1473
- let value = if let Some(value) = inline { value.to_string() } else {
1525
+ let value = if let Some(value) = inline {
1526
+ value.to_string()
1527
+ } else {
1474
1528
  index += 1;
1475
- args.get(index).filter(|value| !value.starts_with("--"))
1476
- .ok_or_else(|| CliError::Usage(format!("missing value for {flag}")))?.clone()
1529
+ args.get(index)
1530
+ .filter(|value| !value.starts_with("--"))
1531
+ .ok_or_else(|| CliError::Usage(format!("missing value for {flag}")))?
1532
+ .clone()
1477
1533
  };
1478
- if value.trim().is_empty() { return Err(CliError::Usage(format!("empty value for {flag}"))); }
1534
+ if value.trim().is_empty() {
1535
+ return Err(CliError::Usage(format!("empty value for {flag}")));
1536
+ }
1479
1537
  match flag {
1480
1538
  "--provider" => {
1481
1539
  let provider = crate::provider::wire::parse_provider(&value)
1482
1540
  .ok_or_else(|| CliError::Usage(format!("unknown provider: {value}")))?;
1483
1541
  role.provider = Some(crate::provider::wire::provider_wire(provider).to_string());
1484
1542
  }
1485
- "--bypass" => role.bypass = Some(match value.as_str() {
1486
- "true" => true, "false" => false,
1487
- _ => return Err(CliError::Usage("--bypass requires true or false".into())),
1488
- }),
1543
+ "--bypass" => {
1544
+ role.bypass = Some(match value.as_str() {
1545
+ "true" => true,
1546
+ "false" => false,
1547
+ _ => return Err(CliError::Usage("--bypass requires true or false".into())),
1548
+ })
1549
+ }
1489
1550
  "--model" => role.model = Some(value),
1490
1551
  "--effort" => {
1491
1552
  if crate::model::enums::ProviderEffort::parse(&value).is_none() {
@@ -1495,35 +1556,71 @@ fn role_agent_args(args: &[String], add: bool) -> Result<(ParsedArgs, crate::lif
1495
1556
  }
1496
1557
  "--prompt" => role.prompt = Some(value),
1497
1558
  "--profile" => role.profile = Some(value),
1498
- _ => { forwarded.push(flag.to_string()); forwarded.push(value); }
1559
+ _ => {
1560
+ forwarded.push(flag.to_string());
1561
+ forwarded.push(value);
1562
+ }
1499
1563
  }
1500
1564
  index += 1;
1501
1565
  }
1502
1566
  let parsed = parse_args(&forwarded);
1503
1567
  if add && parsed.role_file.is_none() {
1504
- if role.provider.is_none() { return Err(CliError::Usage("add-agent requires --provider <name>".into())); }
1505
- if role.bypass.is_none() { return Err(CliError::Usage("add-agent requires --bypass <true|false>".into())); }
1568
+ if role.provider.is_none() {
1569
+ return Err(CliError::Usage(
1570
+ "add-agent requires --provider <name>".into(),
1571
+ ));
1572
+ }
1573
+ if role.bypass.is_none() {
1574
+ return Err(CliError::Usage(
1575
+ "add-agent requires --bypass <true|false>".into(),
1576
+ ));
1577
+ }
1578
+ }
1579
+ if parsed.positionals.len() != 1 {
1580
+ return Err(CliError::Usage(
1581
+ if parsed.positionals.is_empty() {
1582
+ "missing agent"
1583
+ } else {
1584
+ "expected exactly one agent id"
1585
+ }
1586
+ .into(),
1587
+ ));
1506
1588
  }
1507
- if parsed.positionals.len() != 1 { return Err(CliError::Usage("expected exactly one agent id".into())); }
1508
1589
  Ok((parsed, role))
1509
1590
  }
1510
1591
 
1511
1592
  #[cfg(test)]
1512
1593
  mod role_cli_tests {
1513
1594
  use super::*;
1514
- fn strings(values: &[&str]) -> Vec<String> { values.iter().map(|value| (*value).to_string()).collect() }
1595
+ fn strings(values: &[&str]) -> Vec<String> {
1596
+ values.iter().map(|value| (*value).to_string()).collect()
1597
+ }
1515
1598
 
1516
1599
  #[test]
1517
1600
  fn add_requires_decisions_without_a_template_but_accepts_file_input() {
1518
- for args in [vec!["w"], vec!["w", "--provider", "pi"], vec!["w", "--bypass", "false"]] {
1601
+ for args in [
1602
+ vec!["w"],
1603
+ vec!["w", "--provider", "pi"],
1604
+ vec!["w", "--bypass", "false"],
1605
+ ] {
1519
1606
  assert!(role_agent_args(&strings(&args), true).is_err());
1520
1607
  }
1521
1608
  assert!(role_agent_args(&strings(&["w", "--role-file", "w.md"]), true).is_ok());
1522
- let (parsed, _) = role_agent_args(&strings(&["w", "--role-file", "w.md", "--force"]), true).unwrap();
1609
+ let (parsed, _) =
1610
+ role_agent_args(&strings(&["w", "--role-file", "w.md", "--force"]), true).unwrap();
1523
1611
  assert!(parsed.force);
1524
- assert!(role_agent_args(&strings(&["w", "--role-file", "w.md", "--force=true"]), true).is_err());
1525
- assert!(role_agent_args(&strings(&["w", "--role-file", "w.md", "--allow-fresh"]), true).is_err());
1526
- let (_, patch) = role_agent_args(&strings(&["w", "--provider=pi", "--bypass=false"]), true).unwrap();
1612
+ assert!(role_agent_args(
1613
+ &strings(&["w", "--role-file", "w.md", "--force=true"]),
1614
+ true
1615
+ )
1616
+ .is_err());
1617
+ assert!(role_agent_args(
1618
+ &strings(&["w", "--role-file", "w.md", "--allow-fresh"]),
1619
+ true
1620
+ )
1621
+ .is_err());
1622
+ let (_, patch) =
1623
+ role_agent_args(&strings(&["w", "--provider=pi", "--bypass=false"]), true).unwrap();
1527
1624
  assert_eq!(patch.provider.as_deref(), Some("pi"));
1528
1625
  assert_eq!(patch.bypass, Some(false));
1529
1626
  }
@@ -1531,12 +1628,20 @@ mod role_cli_tests {
1531
1628
  #[test]
1532
1629
  fn strict_role_options_reject_ambiguous_values() {
1533
1630
  for args in [
1534
- vec!["w", "--bypass"], vec!["w", "--bypass", "yes"],
1535
- vec!["w", "--no-bypass"], vec!["w", "--bypass", "true", "--bypass=false"],
1536
- vec!["w", "--unknown", "x"], vec!["w", "extra"],
1537
- ] { assert!(role_agent_args(&strings(&args), false).is_err()); }
1631
+ vec!["w", "--bypass"],
1632
+ vec!["w", "--bypass", "yes"],
1633
+ vec!["w", "--no-bypass"],
1634
+ vec!["w", "--bypass", "true", "--bypass=false"],
1635
+ vec!["w", "--unknown", "x"],
1636
+ vec!["w", "extra"],
1637
+ ] {
1638
+ assert!(role_agent_args(&strings(&args), false).is_err());
1639
+ }
1538
1640
  let (_, patch) = role_agent_args(&strings(&["w"]), false).unwrap();
1539
- assert_eq!(patch, crate::lifecycle::role_config::RoleConfigPatch::default());
1641
+ assert_eq!(
1642
+ patch,
1643
+ crate::lifecycle::role_config::RoleConfigPatch::default()
1644
+ );
1540
1645
  }
1541
1646
  }
1542
1647
 
@@ -1565,10 +1670,16 @@ fn stop_agent_args(args: &[String], cwd: &Path) -> Result<StopAgentArgs, CliErro
1565
1670
 
1566
1671
  fn reset_agent_args(args: &[String], cwd: &Path) -> Result<ResetAgentArgs, CliError> {
1567
1672
  let parsed = parse_args(args);
1673
+ let agent = required_pos(&parsed, 0, "agent")?;
1674
+ if !parsed.discard_session {
1675
+ return Err(CliError::Usage(
1676
+ "missing --discard-session;只有用户明确同意清除会话关联后才能添加此参数".to_string(),
1677
+ ));
1678
+ }
1568
1679
  let workspace = workspace(&parsed, cwd);
1569
1680
  refuse_if_multi_alive_team_missing_scope("reset-agent", &workspace, parsed.team.as_deref())?;
1570
1681
  Ok(ResetAgentArgs {
1571
- agent: required_pos(&parsed, 0, "agent")?,
1682
+ agent,
1572
1683
  workspace,
1573
1684
  team: parsed.team,
1574
1685
  discard_session: parsed.discard_session,
@@ -1619,10 +1730,11 @@ fn clone_agent_args(args: &[String], cwd: &Path) -> Result<CloneAgentArgs, CliEr
1619
1730
 
1620
1731
  fn remove_agent_args(args: &[String], cwd: &Path) -> Result<RemoveAgentArgs, CliError> {
1621
1732
  let parsed = parse_args(args);
1733
+ let agent = required_pos(&parsed, 0, "agent")?;
1622
1734
  let workspace = workspace(&parsed, cwd);
1623
1735
  refuse_if_multi_alive_team_missing_scope("remove-agent", &workspace, parsed.team.as_deref())?;
1624
1736
  Ok(RemoveAgentArgs {
1625
- agent: required_pos(&parsed, 0, "agent")?,
1737
+ agent,
1626
1738
  workspace,
1627
1739
  team: parsed.team,
1628
1740
  from_spec: parsed.from_spec,
@@ -1657,15 +1769,6 @@ fn doctor_gate(raw: Option<&str>) -> Option<DoctorGate> {
1657
1769
  }
1658
1770
  }
1659
1771
 
1660
- fn sessions_args(args: &[String], cwd: &Path) -> SessionsArgs {
1661
- let parsed = parse_args(args);
1662
- SessionsArgs {
1663
- workspace: workspace(&parsed, cwd),
1664
- json: parsed.json,
1665
- team: parsed.team,
1666
- }
1667
- }
1668
-
1669
1772
  fn leaders_args(args: &[String], _cwd: &Path) -> Result<LeadersArgs, CliError> {
1670
1773
  let mut view = None;
1671
1774
  let mut query = None;
@@ -1771,18 +1874,6 @@ fn leaders_args(args: &[String], _cwd: &Path) -> Result<LeadersArgs, CliError> {
1771
1874
  })
1772
1875
  }
1773
1876
 
1774
- fn validate_args(args: &[String], cwd: &Path) -> ValidateArgs {
1775
- let parsed = parse_args(args);
1776
- ValidateArgs {
1777
- spec: parsed
1778
- .positionals
1779
- .first()
1780
- .map(PathBuf::from)
1781
- .unwrap_or_else(|| cwd.join("team.spec.yaml")),
1782
- json: parsed.json,
1783
- }
1784
- }
1785
-
1786
1877
  fn profile_args(args: &[String], cwd: &Path) -> Result<ProfileArgs, CliError> {
1787
1878
  let parsed = parse_args(args);
1788
1879
  let workspace = resolve_cli_path(cwd, &workspace(&parsed, cwd));
@@ -1797,106 +1888,6 @@ fn profile_args(args: &[String], cwd: &Path) -> Result<ProfileArgs, CliError> {
1797
1888
  })
1798
1889
  }
1799
1890
 
1800
- fn results_args(args: &[String], cwd: &Path) -> Result<ResultsArgs, CliError> {
1801
- if args
1802
- .iter()
1803
- .any(|arg| arg == "--to" || arg.starts_with("--to="))
1804
- {
1805
- return Err(CliError::Usage(
1806
- "results does not accept --to; it is a read-only command".to_string(),
1807
- ));
1808
- }
1809
- let parsed = parse_args(args);
1810
- let case_id = option_value(args, "--case")
1811
- .filter(|value| !value.is_empty())
1812
- .ok_or_else(|| CliError::Usage("missing --case".to_string()))?;
1813
- Ok(ResultsArgs {
1814
- case_id,
1815
- workspace: workspace(&parsed, cwd),
1816
- team: parsed.team,
1817
- json: parsed.json,
1818
- })
1819
- }
1820
-
1821
- fn option_value(args: &[String], flag: &str) -> Option<String> {
1822
- let prefix = format!("{flag}=");
1823
- let mut i = 0usize;
1824
- while i < args.len() {
1825
- let arg = args.get(i)?;
1826
- if let Some(value) = arg.strip_prefix(&prefix) {
1827
- return Some(value.to_string());
1828
- }
1829
- if arg == flag {
1830
- return args
1831
- .get(i.saturating_add(1))
1832
- .filter(|value| !value.starts_with('-'))
1833
- .cloned();
1834
- }
1835
- i = i.saturating_add(1);
1836
- }
1837
- None
1838
- }
1839
-
1840
- fn preflight_args(args: &[String], cwd: &Path) -> PreflightArgs {
1841
- let parsed = parse_args(args);
1842
- let team = parsed
1843
- .team
1844
- .as_deref()
1845
- .map(PathBuf::from)
1846
- .or_else(|| parsed.positionals.first().map(PathBuf::from))
1847
- .unwrap_or_else(|| cwd.to_path_buf());
1848
- PreflightArgs {
1849
- team,
1850
- json: parsed.json,
1851
- }
1852
- }
1853
-
1854
- fn wait_ready_args(args: &[String], cwd: &Path) -> WaitReadyArgs {
1855
- let parsed = parse_args(args);
1856
- WaitReadyArgs {
1857
- workspace: workspace(&parsed, cwd),
1858
- timeout: parsed.timeout.unwrap_or(60.0),
1859
- json: parsed.json,
1860
- team: parsed.team,
1861
- }
1862
- }
1863
-
1864
- fn e2e_args(args: &[String], cwd: &Path) -> E2eArgs {
1865
- let parsed = parse_args(args);
1866
- let providers = parsed
1867
- .providers
1868
- .as_deref()
1869
- .unwrap_or("fake")
1870
- .split(',')
1871
- .filter_map(|p| {
1872
- let trimmed = p.trim();
1873
- if trimmed.is_empty() {
1874
- None
1875
- } else {
1876
- Some(trimmed.to_string())
1877
- }
1878
- })
1879
- .collect();
1880
- E2eArgs {
1881
- workspace: workspace(&parsed, cwd),
1882
- providers,
1883
- real: parsed.real,
1884
- json: parsed.json,
1885
- }
1886
- }
1887
-
1888
- fn peek_args(args: &[String], cwd: &Path) -> Result<PeekArgs, CliError> {
1889
- let parsed = parse_args(args);
1890
- Ok(PeekArgs {
1891
- agent: required_pos(&parsed, 0, "agent")?,
1892
- workspace: workspace(&parsed, cwd),
1893
- tail: parsed.tail.unwrap_or(80),
1894
- head: parsed.head,
1895
- search: parsed.search,
1896
- allow_raw_screen: parsed.allow_raw_screen,
1897
- json: parsed.json,
1898
- })
1899
- }
1900
1891
  fn run_coordinator(args: &[String], cwd: &Path) -> Result<ExitCode, CliError> {
1901
1892
  let parsed = parse_args(args);
1902
1893
  let workspace = crate::coordinator::WorkspacePath::new(workspace(&parsed, cwd));
@@ -1985,14 +1976,15 @@ mod tests {
1985
1976
  help.lines()
1986
1977
  .filter_map(|line| {
1987
1978
  let trimmed = line.strip_prefix(" ")?;
1988
- if trimmed.starts_with("team-agent ") {
1979
+ if trimmed.starts_with("team-agent ") || trimmed.starts_with(char::is_whitespace) {
1989
1980
  return None;
1990
1981
  }
1991
1982
  let command = trimmed.split_whitespace().next()?;
1992
- command
1993
- .chars()
1994
- .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-')
1995
- .then(|| command.to_string())
1983
+ (command.chars().next()?.is_ascii_lowercase()
1984
+ && command
1985
+ .chars()
1986
+ .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-'))
1987
+ .then(|| command.to_string())
1996
1988
  })
1997
1989
  .collect()
1998
1990
  }
@@ -2082,6 +2074,7 @@ mod tests {
2082
2074
  .filter(|spec| spec.default_help)
2083
2075
  .map(|spec| spec.name)
2084
2076
  .collect();
2077
+ assert_eq!(expected.len(), 29, "the public catalog is Human29");
2085
2078
  for spec_name in &expected {
2086
2079
  assert!(
2087
2080
  visible.iter().any(|command| command == spec_name),
@@ -2095,7 +2088,7 @@ mod tests {
2095
2088
  expected_sorted.sort();
2096
2089
  assert_eq!(
2097
2090
  actual, expected_sorted,
2098
- "default help must match the exact default_help spec set (d40 ∪ results when present), not a slack threshold; got {visible:?}"
2091
+ "default help must match the exact Human29 public spec set, not a slack threshold; got {visible:?}"
2099
2092
  );
2100
2093
  assert!(
2101
2094
  top_help.contains("copilot"),
@@ -2108,7 +2101,21 @@ mod tests {
2108
2101
  let top_help = command_help(None);
2109
2102
  let visible = visible_help_commands(&top_help);
2110
2103
  assert!(visible.iter().any(|command| command == "doctor"));
2111
- for command in ["leaders", "e2e", "peek", "coordinator"] {
2104
+ assert!(visible.iter().any(|command| command == "leaders"));
2105
+ for command in [
2106
+ "results",
2107
+ "wait",
2108
+ "attach-app-server-leader",
2109
+ "identity",
2110
+ "watch",
2111
+ "sessions",
2112
+ "validate",
2113
+ "preflight",
2114
+ "wait-ready",
2115
+ "e2e",
2116
+ "peek",
2117
+ "coordinator",
2118
+ ] {
2112
2119
  assert!(
2113
2120
  !visible.iter().any(|visible| visible == command),
2114
2121
  "`{command}` must stay hidden from default help"
@@ -2116,6 +2123,32 @@ mod tests {
2116
2123
  }
2117
2124
  }
2118
2125
 
2126
+ #[test]
2127
+ fn retired_commands_have_no_catalog_machine_or_help_gate() {
2128
+ for command in [
2129
+ "e2e",
2130
+ "allow-peer-talk",
2131
+ "results",
2132
+ "validate",
2133
+ "identity",
2134
+ "sessions",
2135
+ "watch",
2136
+ "peek",
2137
+ "wait-ready",
2138
+ "preflight",
2139
+ ] {
2140
+ assert!(
2141
+ command_spec(command).is_none(),
2142
+ "retired catalog entry: {command}"
2143
+ );
2144
+ assert!(
2145
+ !is_machine_command(command),
2146
+ "retired Machine route: {command}"
2147
+ );
2148
+ assert!(!is_known_subcommand(command), "retired help gate: {command}");
2149
+ }
2150
+ }
2151
+
2119
2152
  #[test]
2120
2153
  fn leaders_help_publishes_selectors_and_prune() {
2121
2154
  let help = command_help(Some("leaders"));
@@ -2129,10 +2162,9 @@ mod tests {
2129
2162
  #[test]
2130
2163
  fn observation_a_commands_have_terminal_tiers() {
2131
2164
  for (command, tier) in [
2132
- ("allow-peer-talk", CommandTier::Secondary),
2133
- ("approvals", CommandTier::Secondary),
2134
- ("profile", CommandTier::Secondary),
2135
- ("install-skill", CommandTier::Secondary),
2165
+ ("approvals", CommandTier::Core),
2166
+ ("profile", CommandTier::Core),
2167
+ ("install-skill", CommandTier::Core),
2136
2168
  ] {
2137
2169
  assert_eq!(command_spec(command).map(|spec| spec.tier), Some(tier));
2138
2170
  }
@@ -2141,7 +2173,7 @@ mod tests {
2141
2173
  #[test]
2142
2174
  fn suggestion_index_excludes_hidden_commands() {
2143
2175
  assert_eq!(nearest_subcommand("statu"), Some("status"));
2144
- assert_eq!(nearest_subcommand("leader"), None);
2176
+ assert_eq!(nearest_subcommand("leader"), Some("leaders"));
2145
2177
  assert_eq!(nearest_subcommand("fallback-send-leade"), None);
2146
2178
  assert_eq!(nearest_subcommand("coordinato"), None);
2147
2179
  }
@@ -2186,36 +2218,42 @@ mod tests {
2186
2218
  (
2187
2219
  "start-agent",
2188
2220
  &[
2189
- "--model", "--effort", "--bypass", "--prompt", "--profile", "--provider",
2190
- "--workspace", "--team", "--allow-fresh", "--json",
2191
- ][..],
2192
- ),
2193
- (
2194
- "reset-agent",
2195
- &[
2221
+ "--model",
2222
+ "--effort",
2223
+ "--bypass",
2224
+ "--prompt",
2225
+ "--profile",
2226
+ "--provider",
2196
2227
  "--workspace",
2197
2228
  "--team",
2198
- "--discard-session",
2229
+ "--allow-fresh",
2199
2230
  "--json",
2200
2231
  ][..],
2201
2232
  ),
2202
2233
  (
2203
- "add-agent",
2204
- &[
2205
- "--role-file", "--provider", "--bypass", "--model", "--effort", "--prompt", "--profile",
2206
- "--force", "--workspace", "--team", "--json",
2207
- ][..],
2234
+ "reset-agent",
2235
+ &["--workspace", "--team", "--discard-session", "--json"][..],
2208
2236
  ),
2209
2237
  (
2210
- "fork-agent",
2238
+ "add-agent",
2211
2239
  &[
2212
- "--as",
2213
- "--label",
2240
+ "--role-file",
2241
+ "--provider",
2242
+ "--bypass",
2243
+ "--model",
2244
+ "--effort",
2245
+ "--prompt",
2246
+ "--profile",
2247
+ "--force",
2214
2248
  "--workspace",
2215
2249
  "--team",
2216
2250
  "--json",
2217
2251
  ][..],
2218
2252
  ),
2253
+ (
2254
+ "fork-agent",
2255
+ &["--as", "--label", "--workspace", "--team", "--json"][..],
2256
+ ),
2219
2257
  (
2220
2258
  "remove-agent",
2221
2259
  &[
@@ -2255,26 +2293,6 @@ mod tests {
2255
2293
  "inbox",
2256
2294
  &["--workspace", "--team", "-n", "--limit", "--json"][..],
2257
2295
  ),
2258
- ("sessions", &["--workspace", "--team", "--json"][..]),
2259
- (
2260
- "wait-ready",
2261
- &["--workspace", "--team", "--timeout", "--json"][..],
2262
- ),
2263
- (
2264
- "peek",
2265
- &[
2266
- "--workspace",
2267
- "--tail",
2268
- "--head",
2269
- "--search",
2270
- "--allow-raw-screen",
2271
- "--json",
2272
- ][..],
2273
- ),
2274
- (
2275
- "coordinator",
2276
- &["--workspace", "--once", "--tick-interval"][..],
2277
- ),
2278
2296
  ] {
2279
2297
  let help = command_help(Some(command));
2280
2298
  for flag in flags {
@@ -2284,6 +2302,15 @@ mod tests {
2284
2302
  );
2285
2303
  }
2286
2304
  }
2305
+ let cwd = tmp_workspace();
2306
+ for command in ["wait", "attach-app-server-leader", "coordinator"] {
2307
+ assert_eq!(
2308
+ run(&cli_argv(&[command, "--help"]), &cwd),
2309
+ ExitCode::Usage,
2310
+ "Machine help must be generic Usage2, not a private flag tutorial"
2311
+ );
2312
+ }
2313
+ std::fs::remove_dir_all(cwd).unwrap();
2287
2314
  assert!(
2288
2315
  !command_help(Some("quick-start")).contains("--fresh"),
2289
2316
  "quick-start help must not advertise removed reset semantics"
@@ -2295,18 +2322,25 @@ mod tests {
2295
2322
  for name in ["start-agent", "add-agent"] {
2296
2323
  let spec = command_spec(name).expect("registered worker lifecycle command");
2297
2324
  let help = command_help(Some(name));
2298
- assert_eq!(help.lines().next(), Some(spec.usage));
2299
- assert!(help.contains(spec.summary));
2300
- for flag in ["--model SLUG", "--effort LEVEL", "--bypass true|false", "--prompt TEXT", "--profile NAME", "--provider NAME"] {
2325
+ assert!(help.contains(spec.usage));
2326
+ assert!(help.contains("做什么:"));
2327
+ for flag in [
2328
+ "--model MODEL",
2329
+ "--effort LEVEL",
2330
+ "--bypass true|false",
2331
+ "--prompt TEXT",
2332
+ "--profile NAME",
2333
+ "--provider TOOL",
2334
+ ] {
2301
2335
  assert!(help.contains(flag), "{name} help missing {flag}: {help}");
2302
2336
  }
2303
- assert!(help.contains("team-agent send AGENT MESSAGE"));
2337
+ assert!(help.contains("下一步") && help.contains("send"));
2304
2338
  }
2305
2339
  assert!(!command_help(Some("start-agent")).contains("--force"));
2306
2340
  let add = command_help(Some("add-agent"));
2307
2341
  assert!(add.contains("[--role-file FILE]"));
2308
- assert!(add.contains("provider/bypass required from CLI or role file"));
2309
- assert!(add.contains("conflicting values rejected"));
2342
+ assert!(add.contains("provider") && add.contains("bypass") && add.contains("--role-file"));
2343
+ assert!(add.contains("冲突") && add.contains("拒绝"));
2310
2344
  }
2311
2345
 
2312
2346
  #[test]
@@ -2314,12 +2348,15 @@ mod tests {
2314
2348
  let help = command_help(Some("status"));
2315
2349
  for marker in [
2316
2350
  "name/provider/model/effort/runtime_status/activity/health/session_name/tmux_command",
2317
- "人读与 --json 使用同一投影",
2318
- "显示 unknown",
2351
+ "--json 给程序读取",
2352
+ "unknown 表示证据不足",
2319
2353
  "tmux_command",
2320
- "不增加诊断字段",
2354
+ "不增加字段",
2321
2355
  ] {
2322
- assert!(help.contains(marker), "status help missing {marker}: {help}");
2356
+ assert!(
2357
+ help.contains(marker),
2358
+ "status help missing {marker}: {help}"
2359
+ );
2323
2360
  }
2324
2361
  assert!(!help.contains("错误细分走 status --summary"));
2325
2362
  }
@@ -2460,14 +2497,14 @@ mod tests {
2460
2497
  .unwrap();
2461
2498
  let err = cmd_send(&args).unwrap_err();
2462
2499
  assert!(
2463
- matches!(err, CliError::Usage(ref message) if message.contains("--pane and TARGET/--to are mutually exclusive")),
2500
+ matches!(err, CliError::Usage(ref message) if message.contains("--pane") && message.contains("--to") && message.contains("不能") && message.contains("同时")),
2464
2501
  "expected --pane/--to mutual-exclusion usage error, got {err:?}"
2465
2502
  );
2466
2503
 
2467
2504
  let args = send_args(&cli_argv(&["--pane", "%1596"]), &cwd).unwrap();
2468
2505
  let err = cmd_send(&args).unwrap_err();
2469
2506
  assert!(
2470
- matches!(err, CliError::Usage(ref message) if message == "--pane requires a non-empty message"),
2507
+ matches!(err, CliError::Usage(ref message) if message.contains("--pane") && message.contains("不能发送空消息")),
2471
2508
  "expected empty-message usage error, got {err:?}"
2472
2509
  );
2473
2510
 
@@ -2604,10 +2641,15 @@ mod tests {
2604
2641
  }
2605
2642
 
2606
2643
  #[test]
2607
- fn reset_agent_args_builder_refuses_on_multi_alive_team_before_agent_validation() {
2644
+ fn reset_agent_complete_input_refuses_multi_alive_team_before_agent_validation() {
2608
2645
  let ws = tmp_workspace();
2609
2646
  seed_two_alive_teams_in(&ws);
2610
- let argv = cli_argv(&["--workspace", &ws.to_string_lossy()]);
2647
+ let argv = cli_argv(&[
2648
+ "nonexistent-agent",
2649
+ "--discard-session",
2650
+ "--workspace",
2651
+ &ws.to_string_lossy(),
2652
+ ]);
2611
2653
  let err = reset_agent_args(&argv, &ws).expect_err("must refuse");
2612
2654
  assert!(
2613
2655
  err.to_string().contains("multiple alive teams"),
@@ -2616,10 +2658,10 @@ mod tests {
2616
2658
  }
2617
2659
 
2618
2660
  #[test]
2619
- fn remove_agent_args_builder_refuses_on_multi_alive_team_before_agent_validation() {
2661
+ fn remove_agent_complete_input_refuses_multi_alive_team_before_agent_validation() {
2620
2662
  let ws = tmp_workspace();
2621
2663
  seed_two_alive_teams_in(&ws);
2622
- let argv = cli_argv(&["--workspace", &ws.to_string_lossy()]);
2664
+ let argv = cli_argv(&["nonexistent-agent", "--workspace", &ws.to_string_lossy()]);
2623
2665
  let err = remove_agent_args(&argv, &ws).expect_err("must refuse");
2624
2666
  assert!(
2625
2667
  err.to_string().contains("multiple alive teams"),