@team-agent/installer 0.5.110 → 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.
@@ -16,8 +16,16 @@ use serde_json::json;
16
16
  fn provider_args_cases() {
17
17
  // (old case-id, input, Python golden); values[1:] of a lone "--" is empty.
18
18
  const CASES: &[(&str, &[&str], &[&str])] = &[
19
- ("provider_args_strips_leading_dashdash", &["--", "-x"], &["-x"]),
20
- ("provider_args_keeps_when_no_leading_dashdash", &["-x", "-y"], &["-x", "-y"]),
19
+ (
20
+ "provider_args_strips_leading_dashdash",
21
+ &["--", "-x"],
22
+ &["-x"],
23
+ ),
24
+ (
25
+ "provider_args_keeps_when_no_leading_dashdash",
26
+ &["-x", "-y"],
27
+ &["-x", "-y"],
28
+ ),
21
29
  ("provider_args_empty_is_empty", &[], &[]),
22
30
  ("provider_args_lone_dashdash_yields_empty", &["--"], &[]),
23
31
  ];
@@ -36,41 +44,73 @@ fn provider_args_cases() {
36
44
  fn leader_launcher_args_success_cases() {
37
45
  // (old case-id, argv, complete expected structure), including default false flags.
38
46
  let cases: &[(&str, &[&str], LeaderLauncherArgs)] = &[
39
- ("leader_launcher_args_empty_all_default", &[], LeaderLauncherArgs::default()),
40
- ("leader_launcher_args_attach_and_confirm", &["--attach", "--confirm"], LeaderLauncherArgs {
41
- attach_existing: true,
42
- confirm_attach: true,
43
- ..Default::default()
44
- }),
47
+ (
48
+ "leader_launcher_args_empty_all_default",
49
+ &[],
50
+ LeaderLauncherArgs::default(),
51
+ ),
52
+ (
53
+ "leader_launcher_args_attach_and_confirm",
54
+ &["--attach", "--confirm"],
55
+ LeaderLauncherArgs {
56
+ attach_existing: true,
57
+ confirm_attach: true,
58
+ ..Default::default()
59
+ },
60
+ ),
45
61
  // --attach-existing is an alias of --attach.
46
- ("leader_launcher_args_attach_existing_alias", &["--attach-existing"], LeaderLauncherArgs {
47
- attach_existing: true,
48
- ..Default::default()
49
- }),
50
- ("leader_launcher_args_external_leader_opt_out", &["--external-leader", "--", "--model", "opus"], LeaderLauncherArgs {
51
- provider_args: vec!["--model".to_string(), "opus".to_string()],
52
- external_leader: true,
53
- ..Default::default()
54
- }),
55
- ("leader_launcher_args_attach_session_spaced", &["--attach-session", "mysess"], LeaderLauncherArgs {
56
- attach_session: Some("mysess".to_string()),
57
- ..Default::default()
58
- }),
59
- ("leader_launcher_args_attach_session_equals", &["--attach-session=mysess"], LeaderLauncherArgs {
60
- attach_session: Some("mysess".to_string()),
61
- ..Default::default()
62
- }),
62
+ (
63
+ "leader_launcher_args_attach_existing_alias",
64
+ &["--attach-existing"],
65
+ LeaderLauncherArgs {
66
+ attach_existing: true,
67
+ ..Default::default()
68
+ },
69
+ ),
70
+ (
71
+ "leader_launcher_args_external_leader_opt_out",
72
+ &["--external-leader", "--", "--model", "opus"],
73
+ LeaderLauncherArgs {
74
+ provider_args: vec!["--model".to_string(), "opus".to_string()],
75
+ external_leader: true,
76
+ ..Default::default()
77
+ },
78
+ ),
79
+ (
80
+ "leader_launcher_args_attach_session_spaced",
81
+ &["--attach-session", "mysess"],
82
+ LeaderLauncherArgs {
83
+ attach_session: Some("mysess".to_string()),
84
+ ..Default::default()
85
+ },
86
+ ),
87
+ (
88
+ "leader_launcher_args_attach_session_equals",
89
+ &["--attach-session=mysess"],
90
+ LeaderLauncherArgs {
91
+ attach_session: Some("mysess".to_string()),
92
+ ..Default::default()
93
+ },
94
+ ),
63
95
  // Known Team Agent launcher flags after `--` are rejected by the error table.
64
- ("leader_launcher_args_dashdash_passthrough_strips_separator", &["--attach", "--", "-x", "--provider-confirm"], LeaderLauncherArgs {
65
- provider_args: vec!["-x".to_string(), "--provider-confirm".to_string()],
66
- attach_existing: true,
67
- ..Default::default()
68
- }),
69
- ("leader_launcher_args_unknown_tokens_collect_as_provider_args", &["foo", "--attach", "bar"], LeaderLauncherArgs {
70
- provider_args: vec!["foo".to_string(), "bar".to_string()],
71
- attach_existing: true,
72
- ..Default::default()
73
- }),
96
+ (
97
+ "leader_launcher_args_dashdash_passthrough_strips_separator",
98
+ &["--attach", "--", "-x", "--provider-confirm"],
99
+ LeaderLauncherArgs {
100
+ provider_args: vec!["-x".to_string(), "--provider-confirm".to_string()],
101
+ attach_existing: true,
102
+ ..Default::default()
103
+ },
104
+ ),
105
+ (
106
+ "leader_launcher_args_unknown_tokens_collect_as_provider_args",
107
+ &["foo", "--attach", "bar"],
108
+ LeaderLauncherArgs {
109
+ provider_args: vec!["foo".to_string(), "bar".to_string()],
110
+ attach_existing: true,
111
+ ..Default::default()
112
+ },
113
+ ),
74
114
  ];
75
115
  for (id, input, expected) in cases {
76
116
  let argv: Vec<String> = input.iter().map(|arg| (*arg).to_string()).collect();
@@ -97,7 +137,10 @@ fn leader_launcher_args_error_cases() {
97
137
  for (id, input, expected) in CASES {
98
138
  let argv: Vec<String> = input.iter().map(|arg| (*arg).to_string()).collect();
99
139
  let err = leader_launcher_args(&argv).expect_err(id);
100
- assert!(err.to_string().contains(*expected), "{id}: unexpected error: {err}");
140
+ assert!(
141
+ err.to_string().contains(*expected),
142
+ "{id}: unexpected error: {err}"
143
+ );
101
144
  }
102
145
  }
103
146
 
@@ -110,14 +153,31 @@ fn send_target_cases() {
110
153
  // (old case-id, targets, target, complete expected enum).
111
154
  let cases: &[(&str, Option<&str>, Option<&str>, MessageTarget)] = &[
112
155
  (
113
- "send_target_fanout_strips_and_filters_empty", Some("a, b ,,c"), None,
156
+ "send_target_fanout_strips_and_filters_empty",
157
+ Some("a, b ,,c"),
158
+ None,
114
159
  MessageTarget::Fanout(vec!["a".to_string(), "b".to_string(), "c".to_string()]),
115
160
  ),
116
- ("send_target_single_target", None, Some("agent_x"), MessageTarget::Single("agent_x".to_string())),
161
+ (
162
+ "send_target_single_target",
163
+ None,
164
+ Some("agent_x"),
165
+ MessageTarget::Single("agent_x".to_string()),
166
+ ),
117
167
  // A bare "*" means broadcast to the whole team.
118
- ("send_target_broadcast_star", None, Some("*"), MessageTarget::Broadcast),
168
+ (
169
+ "send_target_broadcast_star",
170
+ None,
171
+ Some("*"),
172
+ MessageTarget::Broadcast,
173
+ ),
119
174
  // targets="" is falsy in Python, so it falls through to args.target.
120
- ("send_target_empty_targets_falls_through_to_target", Some(""), Some("fallback"), MessageTarget::Single("fallback".to_string())),
175
+ (
176
+ "send_target_empty_targets_falls_through_to_target",
177
+ Some(""),
178
+ Some("fallback"),
179
+ MessageTarget::Single("fallback".to_string()),
180
+ ),
121
181
  ];
122
182
  for (id, targets, target, expected) in cases {
123
183
  assert_eq!(send_target(*targets, *target), *expected, "{id}");
@@ -133,18 +193,52 @@ fn send_target_cases() {
133
193
  fn classify_agent_bucket_cases() {
134
194
  // (old case-id, all original (raw, health) inputs, expected bucket).
135
195
  const CASES: &[(&str, &[(&str, &str)], SummaryBucket)] = &[
136
- ("classify_failed_takes_priority", &[("failed", ""), ("error", ""), ("running", "error")], SummaryBucket::Failed),
137
- ("classify_stopped", &[("stopped", ""), ("done", ""), ("running", "done")], SummaryBucket::Stopped),
138
- ("classify_busy", &[("busy", ""), ("", "running"), ("", "working")], SummaryBucket::Busy),
196
+ (
197
+ "classify_failed_takes_priority",
198
+ &[("failed", ""), ("error", ""), ("running", "error")],
199
+ SummaryBucket::Failed,
200
+ ),
201
+ (
202
+ "classify_stopped",
203
+ &[("stopped", ""), ("done", ""), ("running", "done")],
204
+ SummaryBucket::Stopped,
205
+ ),
206
+ (
207
+ "classify_busy",
208
+ &[("busy", ""), ("", "running"), ("", "working")],
209
+ SummaryBucket::Busy,
210
+ ),
139
211
  // hstatus==idle precedes the raw==running branch.
140
- ("classify_hstatus_idle_beats_raw_running", &[("running", "idle")], SummaryBucket::Idle),
141
- ("classify_pure_running", &[("running", "")], SummaryBucket::Running),
212
+ (
213
+ "classify_hstatus_idle_beats_raw_running",
214
+ &[("running", "idle")],
215
+ SummaryBucket::Idle,
216
+ ),
217
+ (
218
+ "classify_pure_running",
219
+ &[("running", "")],
220
+ SummaryBucket::Running,
221
+ ),
142
222
  // bug-071/077/085: unmatched and blocked/stuck/missing are Unknown, NEVER idle.
143
- ("classify_blocked_and_unmatched_are_unknown_never_idle", &[("blocked", ""), ("stuck", ""), ("", "missing"), ("weird_value", ""), ("", "")], SummaryBucket::Unknown),
223
+ (
224
+ "classify_blocked_and_unmatched_are_unknown_never_idle",
225
+ &[
226
+ ("blocked", ""),
227
+ ("stuck", ""),
228
+ ("", "missing"),
229
+ ("weird_value", ""),
230
+ ("", ""),
231
+ ],
232
+ SummaryBucket::Unknown,
233
+ ),
144
234
  ];
145
235
  for (id, inputs, expected) in CASES {
146
236
  for &(raw, health) in inputs.iter() {
147
- assert_eq!(classify_agent_bucket(raw, health), *expected, "{id}: raw={raw:?}, health={health:?}");
237
+ assert_eq!(
238
+ classify_agent_bucket(raw, health),
239
+ *expected,
240
+ "{id}: raw={raw:?}, health={health:?}"
241
+ );
148
242
  }
149
243
  }
150
244
  }
@@ -395,7 +489,7 @@ fn cli_error_payload_plain_runtime() {
395
489
  assert_eq!(payload.error, "some other error");
396
490
  assert_eq!(
397
491
  payload.action,
398
- "run `team-agent doctor` or inspect the log path shown here"
492
+ "先运行 team-agent doctor --workspace . 检查所选队伍,或查看此处列出的错误日志。"
399
493
  );
400
494
  assert_eq!(payload.log, "/tmp/y.log");
401
495
  assert_eq!(payload.reason, None);
@@ -417,19 +511,14 @@ fn cli_error_payload_tmux_conflict_quick_start_enrichment() {
417
511
  // context reset is only through restart --allow-fresh with explicit consent.
418
512
  assert_eq!(
419
513
  payload.action,
420
- "tmux session `my-team` already exists. It may be your own existing team. \
421
- To resume it use `team-agent restart`. \
422
- If recovery is impossible, use `team-agent restart --allow-fresh` only after explicit context-loss consent. \
423
- Only if you want a separate team, change `name:` in TEAM.md and run quick-start again. \
424
- Never terminate existing tmux sessions from quick-start."
425
- );
426
- assert_eq!(
427
- payload.next_actions,
428
- Some(vec![
429
- "If this is your existing team, resume it with `team-agent restart`.".to_string(),
430
- "If you want a separate team, change `name:` in TEAM.md and run `team-agent quick-start` again.".to_string(),
431
- ])
514
+ "终端会话 `my-team` 已存在,可能是你的已有队伍。恢复用 team-agent restart;只有明确同意丢弃旧对话才用 --allow-fresh。另建队伍请修改 TEAM.md 的 name 后再 quick-start;不要关闭已有队伍凑成功。"
432
515
  );
516
+ let next = payload.next_actions.as_ref().unwrap();
517
+ assert_eq!(next.len(), 2);
518
+ assert!(next[0].contains("team-agent restart"));
519
+ assert!(
520
+ next[1].contains("TEAM.md") && next[1].contains("name") && next[1].contains("quick-start")
521
+ );
433
522
  }
434
523
 
435
524
  #[test]
@@ -440,16 +529,11 @@ fn cli_error_payload_tmux_conflict_non_quick_start_enrichment() {
440
529
  assert_eq!(payload.session_name.as_deref(), Some("my-team"));
441
530
  assert_eq!(
442
531
  payload.action,
443
- "tmux session `my-team` already exists. It may be an active team. \
444
- Do not terminate existing tmux sessions from startup; \
445
- use a different team name or runtime.session_name and start again."
446
- );
447
- assert_eq!(
448
- payload.next_actions,
449
- Some(vec![
450
- "Use a different team name or runtime.session_name before starting again.".to_string()
451
- ])
532
+ "终端会话 `my-team` 已存在,可能属于运行中的队伍。不要关闭它;请使用另一个队伍名称后再启动。"
452
533
  );
534
+ let next = payload.next_actions.as_ref().unwrap();
535
+ assert_eq!(next.len(), 1);
536
+ assert!(next[0].contains("队伍") && next[0].contains("名称"));
453
537
  }
454
538
 
455
539
  #[test]
@@ -641,19 +725,42 @@ fn cmd_status_summary_with_agent_rejected() {
641
725
  // =========================================================================
642
726
 
643
727
  #[test]
644
- fn cmd_leader_passthrough_help_returns_none() {
645
- // parser.py:516: provider_args in (["-h"],["--help"]) -> print usage, return (no emit).
646
- let r = cmd_leader_passthrough("codex", &["-h".into()], Path::new(".")).unwrap();
647
- assert_eq!(r.output, CmdOutput::None);
648
- assert_eq!(r.exit, ExitCode::Ok);
649
- let r2 = cmd_leader_passthrough("claude", &["--help".into()], Path::new(".")).unwrap();
650
- assert_eq!(r2.output, CmdOutput::None);
651
- let r3 = cmd_leader_passthrough("copilot", &["--help".into()], Path::new(".")).unwrap();
652
- assert_eq!(r3.output, CmdOutput::None);
653
- let r4 = cmd_leader_passthrough("grok", &["-h".into()], Path::new(".")).unwrap();
654
- assert_eq!(r4.output, CmdOutput::None);
655
- let r5 = cmd_leader_passthrough("cursor", &["--help".into()], Path::new(".")).unwrap();
656
- assert_eq!(r5.output, CmdOutput::None);
728
+ #[serial_test::serial(env)]
729
+ fn cmd_leader_passthrough_help_is_pure_human_guidance() {
730
+ let env = crate::cli::hermetic_test_support::HermeticTestEnv::enter("launcher-help-unit");
731
+ let cwd = env.workspace("empty");
732
+ let tools = [
733
+ "pi", "codex", "claude", "copilot", "grok", "agent", "cursor",
734
+ ];
735
+ let bin = env.root().join("canaries");
736
+ std::fs::create_dir(&bin).unwrap();
737
+ #[cfg(unix)]
738
+ let _path = {
739
+ use std::os::unix::fs::PermissionsExt;
740
+ for tool in tools {
741
+ let path = bin.join(tool);
742
+ std::fs::write(&path, "#!/bin/sh\nprintf called > \"$0.called\"\nexit 0\n").unwrap();
743
+ std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
744
+ }
745
+ env.with_env("PATH", &bin.to_string_lossy())
746
+ };
747
+ for provider in ["pi", "codex", "claude", "copilot", "grok", "cursor"] {
748
+ for alias in ["-h", "--help"] {
749
+ let result = cmd_leader_passthrough(provider, &[alias.into()], &cwd).unwrap();
750
+ assert_eq!(result.exit, ExitCode::Ok);
751
+ assert!(
752
+ matches!(&result.output, CmdOutput::Human(text) if text.contains(provider) && text.contains("用法") && text.contains("Examples") && text.contains("下一步"))
753
+ );
754
+ }
755
+ }
756
+ assert_eq!(std::fs::read_dir(&cwd).unwrap().count(), 0);
757
+ assert!(env.registry_entries().is_empty());
758
+ for tool in tools {
759
+ assert!(
760
+ !bin.join(format!("{tool}.called")).exists(),
761
+ "native tool invoked"
762
+ );
763
+ }
657
764
  }
658
765
 
659
766
  #[test]
@@ -257,8 +257,7 @@ fn inbox_excludes_leader_stage_result_notifications() {
257
257
  // the deterministic check sub-shapes (run_id is a random uuid; not value-locked). ────────────────
258
258
  #[test]
259
259
  fn comms_selftest_golden_boundary_scope_and_check_shapes() {
260
- let boundary = "validates live pane binding consistency and zero-token comms contracts. \
261
- Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
260
+ let boundary = "这里只检查终端连接是否一致,不实际发送消息,也不证明队友已回复。";
262
261
  let ws = tmp_workspace();
263
262
  let v = diagnose_port::comms_selftest(&ws, None, None).expect("comms_selftest");
264
263
  let obj = v.as_object().expect("comms dict");
@@ -248,12 +248,9 @@ fn resolve_qualified_name_prefers_unique_state_pane_over_same_window_idle_pane()
248
248
  pane("team-a", "qa", "%idle"),
249
249
  ]);
250
250
 
251
- let resolved = resolve_name_with_transport(
252
- &ws,
253
- &format!("{}::team-a/qa", ws.display()),
254
- &transport,
255
- )
256
- .expect("a qualified worker address should use its unique state pane");
251
+ let resolved =
252
+ resolve_name_with_transport(&ws, &format!("{}::team-a/qa", ws.display()), &transport)
253
+ .expect("a qualified worker address should use its unique state pane");
257
254
  assert_eq!(resolved.pane_id, "%worker");
258
255
  assert_eq!(resolved.state_pane_id.as_deref(), Some("%worker"));
259
256
  assert!(!resolved.state_pane_stale);
@@ -271,11 +268,17 @@ fn resolve_qualified_name_state_pane_must_uniquely_match_scoped_candidates() {
271
268
  .as_object_mut()
272
269
  .unwrap()
273
270
  .remove("pane_id");
274
- vec![pane("team-a", "qa", "%worker"), pane("team-a", "qa", "%idle")]
271
+ vec![
272
+ pane("team-a", "qa", "%worker"),
273
+ pane("team-a", "qa", "%idle"),
274
+ ]
275
275
  }
276
276
  "stale" => {
277
277
  team["agents"]["qa"]["pane_id"] = json!("%stale");
278
- vec![pane("team-a", "qa", "%worker"), pane("team-a", "qa", "%idle")]
278
+ vec![
279
+ pane("team-a", "qa", "%worker"),
280
+ pane("team-a", "qa", "%idle"),
281
+ ]
279
282
  }
280
283
  "cross-session" => vec![
281
284
  pane("old-team-a", "qa", "%worker"),
@@ -293,12 +296,9 @@ fn resolve_qualified_name_state_pane_must_uniquely_match_scoped_candidates() {
293
296
  };
294
297
  seed_state(&ws, state_with_teams(json!({"team-a": team})));
295
298
  let transport = OfflineTransport::new().with_targets(targets);
296
- let err = resolve_name_with_transport(
297
- &ws,
298
- &format!("{}::team-a/qa", ws.display()),
299
- &transport,
300
- )
301
- .expect_err("non-unique state pane evidence must remain fail-closed");
299
+ let err =
300
+ resolve_name_with_transport(&ws, &format!("{}::team-a/qa", ws.display()), &transport)
301
+ .expect_err("non-unique state pane evidence must remain fail-closed");
302
302
  assert_eq!(err.kind, NamedAddressErrorKind::NameAmbiguous, "{scenario}");
303
303
  let _ = std::fs::remove_dir_all(&ws);
304
304
  }
@@ -313,11 +313,8 @@ fn resolve_qualified_name_canonicalizes_legacy_session_alias() {
313
313
  "teamdir": worker_team("team-displayname", "qa", "%1", "qa")
314
314
  })),
315
315
  );
316
- let transport = OfflineTransport::new().with_targets(vec![pane(
317
- "team-displayname",
318
- "qa",
319
- "%1",
320
- )]);
316
+ let transport =
317
+ OfflineTransport::new().with_targets(vec![pane("team-displayname", "qa", "%1")]);
321
318
 
322
319
  let resolved = resolve_name_with_transport(&ws, "displayname/qa", &transport)
323
320
  .expect("qualified legacy alias must resolve through the canonical selector");
@@ -380,21 +377,21 @@ fn send_to_name_mutual_exclusion() {
380
377
  let args = named_send_args(&cwd, Some("team-a/qa"), Some("%1"), None, &["hello"]);
381
378
  let err = cmd_send(&args).unwrap_err();
382
379
  assert!(
383
- matches!(err, CliError::Usage(ref message) if message.contains("--to-name and --pane/TARGET/--to are mutually exclusive")),
380
+ matches!(err, CliError::Usage(ref message) if message.contains("--to-name/--pane/--to") && message.contains("不能同时")),
384
381
  "expected --to-name mutual exclusion, got {err:?}"
385
382
  );
386
383
 
387
384
  let args = named_send_args(&cwd, Some("team-a/qa"), None, Some("worker"), &["hello"]);
388
385
  let err = cmd_send(&args).unwrap_err();
389
386
  assert!(
390
- matches!(err, CliError::Usage(ref message) if message.contains("--to-name and --pane/TARGET/--to are mutually exclusive")),
387
+ matches!(err, CliError::Usage(ref message) if message.contains("--to-name/--pane/--to") && message.contains("不能同时")),
391
388
  "expected --to-name/--to mutual exclusion, got {err:?}"
392
389
  );
393
390
 
394
391
  let args = named_send_args(&cwd, Some("team-a/qa"), None, None, &[]);
395
392
  let err = cmd_send(&args).unwrap_err();
396
393
  assert!(
397
- matches!(err, CliError::Usage(ref message) if message == "--to-name requires a non-empty message"),
394
+ matches!(err, CliError::Usage(ref message) if message == "请填写任务内容;--to-name 不能发送空消息"),
398
395
  "expected empty-message usage error, got {err:?}"
399
396
  );
400
397
  let _ = std::fs::remove_dir_all(&cwd);
@@ -645,7 +645,7 @@ fn cmd_send_joins_message_with_single_space() {
645
645
  );
646
646
  assert_eq!(
647
647
  v.get("reminder").and_then(|reminder| reminder.as_str()),
648
- Some("Message queued; coordinator will notify when the worker receives it. Do not poll the worker terminal with capture-pane.")
648
+ Some("任务已收下,但还未确认送到队友对话。查看 team-agent status/doctor,等真实回复,不反复重发。")
649
649
  );
650
650
  }
651
651
  }
@@ -654,22 +654,27 @@ fn cmd_send_joins_message_with_single_space() {
654
654
  }
655
655
 
656
656
  #[test]
657
- fn cmd_send_default_human_output_is_one_line_without_false_delivered() {
657
+ fn cmd_send_default_human_output_is_brief_without_false_delivered() {
658
658
  let r = cmd_send(&queued_send_args_fixture(false)).expect("cmd_send returns CmdResult");
659
659
  assert!(!r.as_json);
660
660
  let text = emit(&r.output, r.as_json).expect("send should render human text");
661
661
  let lines: Vec<_> = text.lines().collect();
662
662
  assert_eq!(
663
663
  lines.len(),
664
- 1,
665
- "default send output must be one line: {text}"
664
+ 2,
665
+ "send must separate outcome and next action: {text}"
666
666
  );
667
667
  assert!(
668
- lines[0].contains("ok:")
669
- && lines[0].contains("status:")
670
- && lines[0].contains("message_id:")
671
- && lines[0].contains("target:"),
672
- "default send output must keep only the core fields; got {text}"
668
+ lines[0].contains("任务已收下") && lines[0].contains("还未送到队友的对话"),
669
+ "queued send must report its true pending outcome: {text}"
670
+ );
671
+ assert!(
672
+ lines[1].contains("下一步")
673
+ && lines[1].contains("team-agent status")
674
+ && lines[1].contains("doctor")
675
+ && lines[1].contains("等真实回复")
676
+ && lines[1].contains("不反复重发"),
677
+ "queued send must give honest next steps: {text}"
673
678
  );
674
679
  assert!(
675
680
  !text.contains("delivered"),
@@ -946,7 +951,7 @@ fn cmd_send_unknown_task_surfaces_golden_error_envelope_not_silent() {
946
951
  );
947
952
  assert_eq!(
948
953
  payload.action,
949
- "run `team-agent doctor` or inspect the log path shown here"
954
+ "先运行 team-agent doctor --workspace . 检查所选队伍,或查看此处列出的错误日志。"
950
955
  );
951
956
  let _ = std::fs::remove_dir_all(&ws);
952
957
  }
@@ -62,8 +62,8 @@ fn install_skill_missing_source_is_usage_error() {
62
62
  );
63
63
  assert_eq!(
64
64
  code,
65
- ExitCode::Error,
66
- "install-skill without --source must error (CliError::Usage -> emit_cli_error exit 1)"
65
+ ExitCode::Usage,
66
+ "known missing --source must give pure Usage2 guidance"
67
67
  );
68
68
  let _ = std::fs::remove_dir_all(&ws);
69
69
  }
@@ -41,6 +41,13 @@ pub enum CliError {
41
41
  /// `peek` 缺 `--allow-raw-screen`)。对应 Python 抛 `TeamAgentError` 或 parser.error。
42
42
  #[error("usage error: {0}")]
43
43
  Usage(String),
44
+ /// Preserve the original refusal for audit while carrying safe human guidance.
45
+ #[error("usage error: {audit}")]
46
+ AddressRefusal {
47
+ audit: String,
48
+ error: String,
49
+ action: String,
50
+ },
44
51
  /// state 解析失败(歧义/未找到 team 等)。透传 step 5。
45
52
  #[error("{0}")]
46
53
  State(#[from] crate::state::StateError),
@@ -76,7 +83,12 @@ impl CliError {
76
83
  let mut payload = CliErrorPayload {
77
84
  ok: false,
78
85
  error: error.clone(),
79
- action: "run `team-agent doctor` or inspect the log path shown here".to_string(),
86
+ action: if super::spec::command_spec(command).is_some() {
87
+ "先运行 team-agent doctor --workspace . 检查所选队伍,或查看此处列出的错误日志。"
88
+ } else {
89
+ "run `team-agent doctor` or inspect the log path shown here"
90
+ }
91
+ .to_string(),
80
92
  log: log_path.to_string_lossy().to_string(),
81
93
  reason: None,
82
94
  session_name: None,
@@ -87,30 +99,27 @@ impl CliError {
87
99
  payload.session_name = Some(session.clone());
88
100
  if command == "quick-start" {
89
101
  payload.action = format!(
90
- "tmux session `{session}` already exists. It may be your own existing team. To resume it use `team-agent restart`. If recovery is impossible, use `team-agent restart --allow-fresh` only after explicit context-loss consent. Only if you want a separate team, change `name:` in TEAM.md and run quick-start again. Never terminate existing tmux sessions from quick-start."
102
+ "终端会话 `{session}` 已存在,可能是你的已有队伍。恢复用 team-agent restart;只有明确同意丢弃旧对话才用 --allow-fresh。另建队伍请修改 TEAM.md 的 name 后再 quick-start;不要关闭已有队伍凑成功。"
91
103
  );
92
104
  payload.next_actions = Some(vec![
93
- "If this is your existing team, resume it with `team-agent restart`.".to_string(),
94
- "If you want a separate team, change `name:` in TEAM.md and run `team-agent quick-start` again.".to_string(),
105
+ "如果是自己的已有队伍,使用 team-agent restart 恢复。".to_string(),
106
+ "如要另建队伍,修改 TEAM.md 的 name 后再运行 team-agent quick-start。"
107
+ .to_string(),
95
108
  ]);
96
109
  } else {
97
110
  payload.action = format!(
98
- "tmux session `{session}` already exists. It may be an active team. Do not terminate existing tmux sessions from startup; use a different team name or runtime.session_name and start again."
111
+ "终端会话 `{session}` 已存在,可能属于运行中的队伍。不要关闭它;请使用另一个队伍名称后再启动。"
99
112
  );
100
- payload.next_actions = Some(vec![
101
- "Use a different team name or runtime.session_name before starting again."
102
- .to_string(),
103
- ]);
113
+ payload.next_actions = Some(vec!["请使用另一个队伍名称后再启动。".to_string()]);
104
114
  }
105
115
  } else if error.contains("Team Agent launcher flag")
106
116
  && error.contains("must appear before --")
107
117
  {
108
- payload.action = String::from(
109
- "move the Team Agent launcher flag before `--`; only provider flags belong after `--`",
110
- );
118
+ payload.action =
119
+ String::from("将主控启动选项放在 -- 前面;-- 后面只能是工具自己的参数。");
111
120
  } else if error.contains("managed launcher refuses a different ambient tmux server") {
112
121
  payload.action = String::from(
113
- "leave the current tmux client and retry, or pass `--allow-nested-attach` before `--` to opt in to a nested managed attach",
122
+ "先退出当前 tmux 客户端再重试;只有明确需要嵌套连接时才在 -- 前加 --allow-nested-attach。",
114
123
  );
115
124
  }
116
125
  payload
@@ -816,7 +816,7 @@ mod tests {
816
816
  }
817
817
 
818
818
  #[test]
819
- #[serial]
819
+ #[serial(env)]
820
820
  fn prune_removes_team_proven_terminal() {
821
821
  let (_home_guard, _home, workspace) = test_home("terminal");
822
822
  write_state(
@@ -834,7 +834,7 @@ mod tests {
834
834
  }
835
835
 
836
836
  #[test]
837
- #[serial]
837
+ #[serial(env)]
838
838
  fn prune_removes_missing_key_only_from_valid_teams_map() {
839
839
  let (_home_guard, _home, workspace) = test_home("missing-key");
840
840
  write_state(&workspace, json!({"teams": {"other": {"status": "alive"}}}));
@@ -850,7 +850,7 @@ mod tests {
850
850
  }
851
851
 
852
852
  #[test]
853
- #[serial]
853
+ #[serial(env)]
854
854
  fn prune_keeps_null_nonobject_and_legacy_teams_shapes() {
855
855
  for (label, state) in [
856
856
  ("null", json!({"teams": null})),
@@ -153,7 +153,8 @@ fn pi_leader_and_teammate_body(hermetic: &HermeticTestEnv) {
153
153
  );
154
154
  assert!(
155
155
  crate::cli::emit::default_help()
156
- .contains("team-agent codex|claude|copilot|grok|cursor|pi ..."),
156
+ .lines()
157
+ .any(|line| { line.split_whitespace().next() == Some("pi") }),
157
158
  "default help must advertise the dispatchable Pi launcher"
158
159
  );
159
160
  let leader = parse_pi_leader_args(&leader_argv[2..])
@@ -221,14 +222,8 @@ fn pi_leader_and_teammate_body(hermetic: &HermeticTestEnv) {
221
222
  .expect("write provider-default teammate role");
222
223
  let defaults = compile_role_agent(&role, &Value::Map(Vec::new()), "/workspace")
223
224
  .expect("provider: pi teammate may preserve provider-native defaults");
224
- assert_eq!(
225
- defaults.agent.get("model"),
226
- Some(&Value::Null)
227
- );
228
- assert_eq!(
229
- defaults.agent.get("effort"),
230
- None
231
- );
225
+ assert_eq!(defaults.agent.get("model"), Some(&Value::Null));
226
+ assert_eq!(defaults.agent.get("effort"), None);
232
227
  assert_eq!(
233
228
  defaults.agent.get("dangerously_skip_permissions"),
234
229
  Some(&Value::Bool(false))
@@ -170,7 +170,9 @@ fn purge_agent_help_and_dispatch_are_consistent() {
170
170
  );
171
171
  } else {
172
172
  assert!(
173
- command.contains("unknown subcommand") || command.contains("invalid choice"),
173
+ command.contains("unknown subcommand")
174
+ || command.contains("invalid choice")
175
+ || command.contains("没有这个操作"),
174
176
  "RED6: if purge-agent is not implemented, help and dispatch must agree it is absent; help={help} command={command}"
175
177
  );
176
178
  }