@team-agent/installer 0.5.109 → 0.5.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +33 -0
  4. package/README.zh.md +30 -0
  5. package/crates/team-agent/src/cli/adapters.rs +234 -73
  6. package/crates/team-agent/src/cli/emit.rs +494 -201
  7. package/crates/team-agent/src/cli/leader.rs +4 -9
  8. package/crates/team-agent/src/cli/leaders.rs +90 -29
  9. package/crates/team-agent/src/cli/mod.rs +348 -161
  10. package/crates/team-agent/src/cli/models.rs +80 -24
  11. package/crates/team-agent/src/cli/named_address.rs +114 -25
  12. package/crates/team-agent/src/cli/route.rs +212 -0
  13. package/crates/team-agent/src/cli/send/presentation.rs +60 -41
  14. package/crates/team-agent/src/cli/send/resolve.rs +25 -18
  15. package/crates/team-agent/src/cli/send.rs +4 -4
  16. package/crates/team-agent/src/cli/spec.rs +56 -106
  17. package/crates/team-agent/src/cli/tests/base.rs +189 -82
  18. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -2
  19. package/crates/team-agent/src/cli/tests/named_address.rs +19 -22
  20. package/crates/team-agent/src/cli/tests/status_send.rs +15 -10
  21. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +2 -2
  22. package/crates/team-agent/src/cli/types.rs +22 -13
  23. package/crates/team-agent/src/leader/registry.rs +3 -3
  24. package/crates/team-agent/src/leader/start.rs +5 -1
  25. package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -0
  26. package/crates/team-agent/src/lifecycle/restart/common.rs +2 -0
  27. package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +1457 -0
  28. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +4 -9
  29. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -1
  30. package/crates/team-agent/src/lifecycle/tests.rs +1 -0
  31. package/crates/team-agent/src/provider/argv_route.rs +409 -0
  32. package/crates/team-agent/src/provider/mod.rs +3 -2
  33. package/package.json +4 -4
  34. package/skills/team-agent/SKILL.md +32 -0
@@ -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})),
@@ -291,7 +291,7 @@ fn leader_start_plan_with_ambient_authority(
291
291
  ),
292
292
  _ => None,
293
293
  };
294
- let provider_argv = if provider == Provider::Pi && mode != LeaderStartMode::AttachExisting {
294
+ let mut provider_argv = if provider == Provider::Pi && mode != LeaderStartMode::AttachExisting {
295
295
  let parsed = crate::lifecycle::launch::pi_mcp::parse_pi_leader_args(provider_args)
296
296
  .map_err(|error| LeaderError::Start(error.to_string()))?;
297
297
  let mcp_config = adapter
@@ -329,6 +329,10 @@ fn leader_start_plan_with_ambient_authority(
329
329
  } else {
330
330
  provider_command_argv(provider, provider_args)
331
331
  };
332
+ if mode != LeaderStartMode::AttachExisting {
333
+ crate::provider::argv_route::apply(provider, &mut provider_argv)
334
+ .map_err(|error| LeaderError::Start(error.to_string()))?;
335
+ }
332
336
  let argv = start_argv(
333
337
  mode,
334
338
  provider,
@@ -330,6 +330,8 @@ pub(super) fn spawn_agents(
330
330
  ensure_grok_login_and_folder_trust(workspace)?;
331
331
  apply_grok_mcp_overlay(workspace, &mcp_config)?;
332
332
  }
333
+ crate::provider::argv_route::apply(provider, &mut plan.argv)
334
+ .map_err(|error| LifecycleError::Provider(error.to_string()))?;
333
335
  let spawn_epoch = u64::try_from(started.len()).unwrap_or(u64::MAX);
334
336
  let spawned_at = spawn_timestamp_for_agent(u32::try_from(spawn_epoch).unwrap_or(u32::MAX));
335
337
  // E6 层1 实证3 + 诊断留痕:落最终 worker argv(spawn 前的真实形态)。