@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.
@@ -298,7 +298,8 @@ mod tests {
298
298
  #[cfg(unix)]
299
299
  #[test]
300
300
  fn search_survives_unrelated_spaced_model_ids() {
301
- let catalog = include_str!("../lifecycle/tests/fixtures/pi_list_models_spaced_ids.stdout.txt");
301
+ let catalog =
302
+ include_str!("../lifecycle/tests/fixtures/pi_list_models_spaced_ids.stdout.txt");
302
303
  let path = fixture(&format!("printf '%s' '{catalog}'"));
303
304
  let result = cmd_models_with(
304
305
  &ModelsArgs {
@@ -370,7 +371,8 @@ mod tests {
370
371
  &["models", "--provider", "cursor_agent", "--json"],
371
372
  );
372
373
  let text = public_cli_text(&output);
373
- let calls = std::fs::read_to_string(agent.program.with_extension("count")).unwrap_or_default();
374
+ let calls =
375
+ std::fs::read_to_string(agent.program.with_extension("count")).unwrap_or_default();
374
376
  drop(agent);
375
377
  assert!(output.status.success(), "{text}");
376
378
  assert_eq!(calls, "1:--list-models\n");
@@ -405,12 +407,19 @@ mod tests {
405
407
  let no_match = run_public_models_cli(
406
408
  &grok.program,
407
409
  grok.response.as_deref(),
408
- &["models", "--provider", "grok", "--search", "definitely-absent"],
410
+ &[
411
+ "models",
412
+ "--provider",
413
+ "grok",
414
+ "--search",
415
+ "definitely-absent",
416
+ ],
409
417
  );
410
418
  let json_text = public_cli_text(&json);
411
419
  let human_text = public_cli_text(&human);
412
420
  let no_match_text = public_cli_text(&no_match);
413
- let calls = std::fs::read_to_string(grok.program.with_extension("count")).unwrap_or_default();
421
+ let calls =
422
+ std::fs::read_to_string(grok.program.with_extension("count")).unwrap_or_default();
414
423
  drop(grok);
415
424
  assert!(json.status.success(), "{json_text}");
416
425
  assert!(human.status.success(), "{human_text}");
@@ -422,10 +431,15 @@ mod tests {
422
431
  assert_eq!(value["auth_basis"], "catalog_visibility");
423
432
  let models = value["models"].as_array().unwrap();
424
433
  assert_eq!(
425
- models.iter().map(|model| model["model_id"].as_str().unwrap()).collect::<Vec<_>>(),
434
+ models
435
+ .iter()
436
+ .map(|model| model["model_id"].as_str().unwrap())
437
+ .collect::<Vec<_>>(),
426
438
  vec!["grok-4.7", "grok-4.7-build-fast", "grok-4.5", "grok-4.6"]
427
439
  );
428
- assert!(models.iter().all(|model| model["provider"] == "grok" && model["vendor"] == "xai" && model["aliases"] == json!([])));
440
+ assert!(models.iter().all(|model| model["provider"] == "grok"
441
+ && model["vendor"] == "xai"
442
+ && model["aliases"] == json!([])));
429
443
  assert_eq!(models[0]["default"], false);
430
444
  assert_eq!(models[3]["default"], true);
431
445
  assert!(human_text.contains("grok-4.7-build-fast"));
@@ -471,7 +485,8 @@ mod tests {
471
485
  &["models", "--provider", "grok", "--json"],
472
486
  );
473
487
  let text = public_cli_text(&output);
474
- let calls = std::fs::read_to_string(grok.program.with_extension("count")).unwrap_or_default();
488
+ let calls =
489
+ std::fs::read_to_string(grok.program.with_extension("count")).unwrap_or_default();
475
490
  outcomes.push((name, output.status.success(), text, calls));
476
491
  }
477
492
  for (name, success, text, calls) in outcomes {
@@ -481,21 +496,32 @@ mod tests {
481
496
  assert_eq!(value["schema_version"], "models.v1", "{name}");
482
497
  assert_eq!(value["ok"], false, "{name}");
483
498
  assert_eq!(value["models"], json!([]), "{name}");
484
- assert!(!value["error"].as_str().unwrap().to_ascii_lowercase().contains("unsupported model provider"), "{name}: {text}");
499
+ assert!(
500
+ !value["error"]
501
+ .as_str()
502
+ .unwrap()
503
+ .to_ascii_lowercase()
504
+ .contains("unsupported model provider"),
505
+ "{name}: {text}"
506
+ );
485
507
  }
486
508
  }
487
509
 
488
510
  #[cfg(unix)]
489
511
  #[test]
490
512
  fn grok_nonzero_native_command_is_not_a_successful_empty_catalog() {
491
- let grok = CliFixture::new(fixture_program("grok", "printf 'private diagnostic' >&2\nexit 7"));
513
+ let grok = CliFixture::new(fixture_program(
514
+ "grok",
515
+ "printf 'private diagnostic' >&2\nexit 7",
516
+ ));
492
517
  let output = run_public_models_cli(
493
518
  &grok.program,
494
519
  None,
495
520
  &["models", "--provider", "grok", "--json"],
496
521
  );
497
522
  let text = public_cli_text(&output);
498
- let calls = std::fs::read_to_string(grok.program.with_extension("count")).unwrap_or_default();
523
+ let calls =
524
+ std::fs::read_to_string(grok.program.with_extension("count")).unwrap_or_default();
499
525
  drop(grok);
500
526
  assert!(!output.status.success(), "{text}");
501
527
  assert_eq!(calls, "1:models\n");
@@ -503,7 +529,14 @@ mod tests {
503
529
  let value: Value = serde_json::from_str(text.trim()).unwrap();
504
530
  assert_eq!(value["ok"], false);
505
531
  assert_eq!(value["models"], json!([]));
506
- assert!(value["error"].as_str().unwrap().to_ascii_lowercase().contains("grok"), "{text}");
532
+ assert!(
533
+ value["error"]
534
+ .as_str()
535
+ .unwrap()
536
+ .to_ascii_lowercase()
537
+ .contains("grok"),
538
+ "{text}"
539
+ );
507
540
  }
508
541
 
509
542
  #[cfg(unix)]
@@ -518,10 +551,20 @@ mod tests {
518
551
  let text = public_cli_text(&output);
519
552
  let value: Value = serde_json::from_str(text.trim()).unwrap();
520
553
  assert_eq!(value["ok"], false);
521
- assert!(value["error"].as_str().unwrap().to_ascii_lowercase().contains("grok"), "{text}");
554
+ assert!(
555
+ value["error"]
556
+ .as_str()
557
+ .unwrap()
558
+ .to_ascii_lowercase()
559
+ .contains("grok"),
560
+ "{text}"
561
+ );
522
562
  assert!(value["action"].as_str().unwrap().contains("grok"), "{text}");
523
563
  assert!(crate::cli::emit::default_help().contains("grok"));
524
- assert!(crate::cli::spec::command_spec("models").unwrap().usage.contains("grok"));
564
+ assert!(crate::cli::spec::command_spec("models")
565
+ .unwrap()
566
+ .usage
567
+ .contains("grok"));
525
568
 
526
569
  let unsupported = run_public_models_cli(
527
570
  &missing.program,
@@ -533,7 +576,13 @@ mod tests {
533
576
  drop(missing);
534
577
  assert!(!output.status.success());
535
578
  assert!(!unsupported.status.success());
536
- assert!(unsupported_value["action"].as_str().unwrap().contains("grok"), "{unsupported_text}");
579
+ assert!(
580
+ unsupported_value["action"]
581
+ .as_str()
582
+ .unwrap()
583
+ .contains("grok"),
584
+ "{unsupported_text}"
585
+ );
537
586
  }
538
587
 
539
588
  #[cfg(unix)]
@@ -579,7 +628,10 @@ mod tests {
579
628
  #[cfg(unix)]
580
629
  impl CliFixture {
581
630
  fn new(program: std::path::PathBuf) -> Self {
582
- Self { program, response: None }
631
+ Self {
632
+ program,
633
+ response: None,
634
+ }
583
635
  }
584
636
  }
585
637
 
@@ -614,7 +666,10 @@ mod tests {
614
666
  "--nocapture",
615
667
  ])
616
668
  .env("TEAM_AGENT_MODELS_CHILD", "1")
617
- .env("TEAM_AGENT_MODELS_ARGS", serde_json::to_string(args).unwrap())
669
+ .env(
670
+ "TEAM_AGENT_MODELS_ARGS",
671
+ serde_json::to_string(args).unwrap(),
672
+ )
618
673
  .env("PATH", path.parent().unwrap())
619
674
  .env_remove("PI_PROVIDER")
620
675
  .env_remove("PI_MODEL")
@@ -719,7 +774,10 @@ mod tests {
719
774
  fn with_native_mode<T>(
720
775
  mode: &str,
721
776
  action: impl FnOnce() -> T,
722
- ) -> (T, crate::lifecycle::launch::pi_mcp::PiCatalogTestObservation) {
777
+ ) -> (
778
+ T,
779
+ crate::lifecycle::launch::pi_mcp::PiCatalogTestObservation,
780
+ ) {
723
781
  let receipt = native_timeout_receipt();
724
782
  assert!(!receipt.exists());
725
783
  let result = crate::lifecycle::launch::pi_mcp::with_pi_catalog_test_observation(
@@ -1122,9 +1180,8 @@ mod tests {
1122
1180
  fn runner_fails_closed_for_nonzero_oversize_timeout_and_unavailable() {
1123
1181
  let fail = native_timeout_fixture();
1124
1182
  let started = Instant::now();
1125
- let (failed_result, fail_observation) = with_native_mode("exit7", || {
1126
- run_catalog(&fail, Duration::from_secs(1), 1024)
1127
- });
1183
+ let (failed_result, fail_observation) =
1184
+ with_native_mode("exit7", || run_catalog(&fail, Duration::from_secs(1), 1024));
1128
1185
  let elapsed = started.elapsed();
1129
1186
  assert_eq!(
1130
1187
  failed_result.unwrap_err(),
@@ -1161,9 +1218,8 @@ mod tests {
1161
1218
  assert_eq!(failed.exit, ExitCode::Error);
1162
1219
  assert!(!text.contains("sensitive-token"));
1163
1220
  let big = native_timeout_fixture();
1164
- let (big_result, big_observation) = with_native_mode("oversize", || {
1165
- run_catalog(&big, Duration::from_secs(1), 8)
1166
- });
1221
+ let (big_result, big_observation) =
1222
+ with_native_mode("oversize", || run_catalog(&big, Duration::from_secs(1), 8));
1167
1223
  assert!(big_result.unwrap_err().contains("bounded output"));
1168
1224
  assert_eq!(big_observation.spawn_count, 1);
1169
1225
  assert_eq!(big_observation.argv, vec!["--list-models"]);
@@ -1217,7 +1273,7 @@ mod tests {
1217
1273
  );
1218
1274
  assert_eq!(
1219
1275
  crate::cli::spec::command_spec("models").unwrap().usage,
1220
- "usage: team-agent models [--provider pi|cursor_agent|codex|claude|claude_code|grok] [QUERY|--search TEXT] [--json]"
1276
+ "team-agent models [--provider pi|cursor_agent|codex|claude|claude_code|grok] [QUERY|--search TEXT] [--json]"
1221
1277
  );
1222
1278
  }
1223
1279
 
@@ -71,6 +71,25 @@ pub(crate) struct NamedAddressError {
71
71
  /// `requested_name`/`suggested_name` without re-parsing. Absent
72
72
  /// for exact refusals like empty-workspace.
73
73
  pub requested_name: Option<String>,
74
+ // Presentation only: never serialized or used as routing authority.
75
+ human_scope: Option<PathBuf>,
76
+ human_team: Option<String>,
77
+ }
78
+
79
+ pub(crate) fn human_address_reason(reason: &str) -> Option<&'static str> {
80
+ Some(match reason {
81
+ "name_invalid" => "收信队友名不正确;请使用队友列表中的名称。",
82
+ "workspace_not_found" => "所选项目目录不存在;请核对项目路径。",
83
+ "state_not_found" | "workspace_no_state" => {
84
+ "所选项目还没有可用队伍;首次起队用 quick-start,已有队伍恢复用 restart。"
85
+ }
86
+ "team_key_not_found" => "所选项目没有这支队伍;请核对 --workspace 和 --team。",
87
+ "leader_not_attached" => "主控还没有连接到这支队伍;请先体检,按提示恢复连接。",
88
+ "name_not_resolvable" => "当前队伍没有这个队友;请先查看队友列表。",
89
+ "name_not_live" => "该队友当前未运行或连接已失效;请查看状态再恢复。",
90
+ "name_ambiguous" => "有多个同名对象;请明确选择项目和队伍,不猜对象。",
91
+ _ => return None,
92
+ })
74
93
  }
75
94
 
76
95
  impl NamedAddressError {
@@ -86,7 +105,77 @@ impl NamedAddressError {
86
105
  available_team_keys: Vec::new(),
87
106
  suggested_name: None,
88
107
  requested_name: None,
108
+ human_scope: None,
109
+ human_team: None,
110
+ }
111
+ }
112
+
113
+ fn with_human_scope(mut self, workspace: Option<&Path>, team: Option<&str>) -> Self {
114
+ self.human_scope = workspace.map(Path::to_path_buf);
115
+ if self.human_team.is_none() {
116
+ self.human_team = team.map(str::to_string);
89
117
  }
118
+ self
119
+ }
120
+
121
+ pub(crate) fn human_guidance(&self, requested: &str) -> (String, String) {
122
+ let workspace = self.human_scope.as_deref();
123
+ let mut message = format!(
124
+ "{}\n你刚才填写的是 {requested:?}。",
125
+ human_address_reason(self.kind.as_str()).unwrap_or("未能找到可用的收信队友。")
126
+ );
127
+ if let Some(workspace) = workspace {
128
+ message.push_str(&format!("\n所选项目:{:?}。", workspace.to_string_lossy()));
129
+ }
130
+ if let Some(team) = self.human_team.as_deref() {
131
+ message.push_str(&format!("\n所选队伍:{team:?}。"));
132
+ }
133
+ let mut action = "没有可靠的拼写建议;请先核对项目和队伍,不猜对象。".to_string();
134
+ for candidate in &self.candidates {
135
+ if let (Some(agent), Some(team)) = (
136
+ candidate.get("agent_id").and_then(Value::as_str),
137
+ candidate.get("team_key").and_then(Value::as_str),
138
+ ) {
139
+ message.push_str(&format!(
140
+ "\n候选队伍 {team:?},队友 {agent:?};请确认是否是你要找的对象。"
141
+ ));
142
+ }
143
+ }
144
+ if let Some(candidate) = self.suggested_name.as_deref().and_then(|suggested| {
145
+ self.candidates
146
+ .iter()
147
+ .find(|candidate| candidate.get("name").and_then(Value::as_str) == Some(suggested))
148
+ }) {
149
+ if let (Some(workspace), Some(agent), Some(team)) = (
150
+ workspace,
151
+ candidate.get("agent_id").and_then(Value::as_str),
152
+ candidate.get("team_key").and_then(Value::as_str),
153
+ ) {
154
+ if !agent.is_empty()
155
+ && !team.is_empty()
156
+ && !agent
157
+ .chars()
158
+ .chain(team.chars())
159
+ .chain(workspace.to_string_lossy().chars())
160
+ .any(char::is_control)
161
+ {
162
+ if let Some(command) =
163
+ super::adapters::send_command(agent, workspace, Some(team))
164
+ {
165
+ action = format!("你是否想发给 {agent:?}、队伍 {team:?}?确认后将任务内容替换到这个例子;本次没有发送:\n{command}");
166
+ }
167
+ }
168
+ }
169
+ }
170
+ if let Some(workspace) =
171
+ workspace.filter(|path| !path.to_string_lossy().chars().any(char::is_control))
172
+ {
173
+ action.push_str(&format!(
174
+ "\n也可查看可用队友列表:\nteam-agent status --workspace {}",
175
+ super::adapters::shell_quote(&workspace.to_string_lossy())
176
+ ));
177
+ }
178
+ (message, action)
90
179
  }
91
180
 
92
181
  pub(crate) fn n38_message(&self) -> String {
@@ -188,9 +277,7 @@ impl NamedAddressError {
188
277
  })
189
278
  })
190
279
  .collect();
191
- let best = ranked
192
- .first()
193
- .map(|agent_id| format!("{team}/{agent_id}"));
280
+ let best = ranked.first().map(|agent_id| format!("{team}/{agent_id}"));
194
281
  self.with_scoped_suggestions(requested_display, candidate_values, best)
195
282
  }
196
283
 
@@ -216,9 +303,7 @@ impl NamedAddressError {
216
303
  // returned candidate (RED-4). Falls back to a status-JSON
217
304
  // hint when no candidate cleared the threshold.
218
305
  if let Some(best) = best_copyable {
219
- self.action = format!(
220
- "Did you mean `{best}`? Rerun with `--to-name {best}`."
221
- );
306
+ self.action = format!("Did you mean `{best}`? Rerun with `--to-name {best}`.");
222
307
  } else {
223
308
  self.action = format!(
224
309
  "No close matches for `{requested}`. \
@@ -430,8 +515,14 @@ pub(crate) fn resolve_name_for_cli(
430
515
  // registry-to-E6 internal helper deliberately pass `None` so
431
516
  // caller-supplied `--team` never overrides a full address.
432
517
  let parsed = normalize_bare_with_team_scope(parsed, bare_team_scope);
433
- let target_workspace = resolve_workspace(sender_workspace, parsed.workspace.as_deref())?;
434
- let state = load_state_checked(&target_workspace)?;
518
+ let requested_team = match &parsed.target {
519
+ ParsedTarget::TeamEntity { team, .. } => Some(team.as_str()),
520
+ _ => None,
521
+ };
522
+ let target_workspace = resolve_workspace(sender_workspace, parsed.workspace.as_deref())
523
+ .map_err(|error| error.with_human_scope(None, requested_team))?;
524
+ let state = load_state_checked(&target_workspace)
525
+ .map_err(|error| error.with_human_scope(Some(&target_workspace), requested_team))?;
435
526
  let transport = transport_for_cli_target(&target_workspace, &state, &parsed);
436
527
  let resolved = resolve_in_workspace(
437
528
  sender_workspace,
@@ -439,7 +530,8 @@ pub(crate) fn resolve_name_for_cli(
439
530
  &state,
440
531
  &parsed,
441
532
  transport.as_ref(),
442
- )?;
533
+ )
534
+ .map_err(|error| error.with_human_scope(Some(&target_workspace), requested_team))?;
443
535
  Ok((resolved, transport))
444
536
  }
445
537
 
@@ -487,8 +579,8 @@ pub(crate) fn parse_leader_target_workspace_and_team(
487
579
  match parsed.target {
488
580
  ParsedTarget::TeamEntity { team, entity } if entity == "leader" => {
489
581
  let state = load_state_checked(&target_workspace)?;
490
- let canonical = canonical_named_team_key(&target_workspace, &state, &team)
491
- .unwrap_or(team);
582
+ let canonical =
583
+ canonical_named_team_key(&target_workspace, &state, &team).unwrap_or(team);
492
584
  Ok(Some((target_workspace, canonical)))
493
585
  }
494
586
  _ => Ok(None),
@@ -594,7 +686,7 @@ fn resolve_in_workspace(
594
686
  ParsedTarget::TeamEntity { team, entity } => {
595
687
  let canonical = canonical_named_team_key(target_workspace, state, team)
596
688
  .unwrap_or_else(|| team.clone());
597
- if entity == "leader" {
689
+ let result = if entity == "leader" {
598
690
  resolve_leader(
599
691
  sender_workspace,
600
692
  target_workspace,
@@ -613,7 +705,11 @@ fn resolve_in_workspace(
613
705
  parsed,
614
706
  transport,
615
707
  )
616
- }
708
+ };
709
+ result.map_err(|mut error| {
710
+ error.human_team = Some(canonical);
711
+ error
712
+ })
617
713
  }
618
714
  ParsedTarget::SessionWindow { session, window } => resolve_session_window(
619
715
  sender_workspace,
@@ -976,11 +1072,7 @@ fn resolve_leader(
976
1072
  let socket = string_field(receiver, "tmux_socket")
977
1073
  .map(str::to_string)
978
1074
  .or_else(|| transport.tmux_endpoint());
979
- match crate::messaging::resolve_live_leader_channel(
980
- target_workspace,
981
- receiver,
982
- transport,
983
- ) {
1075
+ match crate::messaging::resolve_live_leader_channel(target_workspace, receiver, transport) {
984
1076
  crate::messaging::LeaderChannelResolution::Live(
985
1077
  crate::messaging::LiveLeaderChannel::DirectTmux(channel),
986
1078
  ) => Ok(ResolvedNamedAddress {
@@ -1194,8 +1286,8 @@ fn transport_for_cli_target(
1194
1286
  parsed: &ParsedNamedAddress,
1195
1287
  ) -> Box<dyn Transport> {
1196
1288
  if let ParsedTarget::TeamEntity { team, entity } = &parsed.target {
1197
- let canonical = canonical_named_team_key(target_workspace, state, team)
1198
- .unwrap_or_else(|| team.clone());
1289
+ let canonical =
1290
+ canonical_named_team_key(target_workspace, state, team).unwrap_or_else(|| team.clone());
1199
1291
  if entity == "leader" {
1200
1292
  let team_scoped_socket = team_entry(state, &canonical)
1201
1293
  .and_then(|entry| entry.get("leader_receiver"))
@@ -1302,11 +1394,8 @@ fn canonical_named_team_key(
1302
1394
  if team_entry(state, requested).is_some() || is_legacy_single_team_active(state, requested) {
1303
1395
  return Some(requested.to_string());
1304
1396
  }
1305
- let selected = crate::state::projection::select_runtime_state(
1306
- target_workspace,
1307
- Some(requested),
1308
- )
1309
- .ok()?;
1397
+ let selected =
1398
+ crate::state::projection::select_runtime_state(target_workspace, Some(requested)).ok()?;
1310
1399
  let canonical = selected
1311
1400
  .get("active_team_key")
1312
1401
  .and_then(Value::as_str)
@@ -5,7 +5,7 @@ use crate::provider::argv_route::{self, Config, Mutation, Override, RouteError};
5
5
  use crate::provider::Provider;
6
6
  use serde_json::{json, Value};
7
7
 
8
- pub(crate) const HELP: &str = "usage: team-agent route [status] [--json]\n team-agent route enable|disable [--json]\n team-agent route show [PROVIDER] [--json]\n team-agent route set|add PROVIDER [--json] -- ARG [ARG ...]\n team-agent route clear PROVIDER [--json]\n\nGlobal config: ~/.team-agent/argv-routing.json (default OFF).\nTEAM_AGENT_CLI_ARGV_ROUTING overrides the persisted switch.\nEverything after -- is literal argv data; routing affects the next native Agent launch only.";
8
+ pub(crate) const HELP: &str = "做什么:\n设置工具启动时额外使用的命令行参数,默认关闭。\n用法:team-agent route [status|enable|disable|show [TOOL]|set TOOL -- ARG...|add TOOL -- ARG...|clear TOOL] [--json]\n\n怎么用:\nstatus 看开关;enable 开启;disable 关闭;show 看全部或指定工具的参数。\nset 替换参数;add 追加参数;clear 清除指定工具的参数;--json 给程序读取。\nTOOL 是已支持的工具名称,如 pi/codex/claude/copilot/grok/cursor_agent。\n第一个 -- 后全是工具的字面参数,包含 --help/--json 也不会被这里解析。\n配置保存在 ~/.team-agent/argv-routing.json;影响全机,不按项目或队伍隔离。\nTEAM_AGENT_CLI_ARGV_ROUTING 环境变量优先于保存的开关。\nExamples(可复制):\nteam-agent route status\nteam-agent route set pi -- --mode text\nteam-agent route enable\n\n下一步(Next Action):\n用 route show pi 核对参数;仅影响下一次启动,不改变正在运行的对话。\n没有开启或没有配置时,工具仍按原参数启动;不了解参数用途时保持关闭。";
9
9
 
10
10
  struct Request {
11
11
  operation: String,
@@ -16,10 +16,9 @@ struct Request {
16
16
 
17
17
  fn usage() -> RouteError {
18
18
  RouteError {
19
- error: "invalid route arguments".to_string(),
19
+ error: "参数不完整或不正确;set/add 需要工具名、-- 和至少一个工具参数".to_string(),
20
20
  reason: "argv_route_usage",
21
- action: "run `team-agent route --help`; set/add require PROVIDER -- ARG [ARG ...]"
22
- .to_string(),
21
+ action: "请按 team-agent route --help 的 Examples 填写;例如 team-agent route set pi -- --mode text".to_string(),
23
22
  config_path: None,
24
23
  }
25
24
  }
@@ -119,7 +118,7 @@ fn execute(request: &Request) -> Result<Value, RouteError> {
119
118
  }
120
119
  if override_state != Override::Unset {
121
120
  value["notice"] = json!(format!(
122
- "effective switch is overridden by {} ({})",
121
+ "当前开关由环境变量 {} 决定({})",
123
122
  argv_route::ENV_NAME,
124
123
  override_state.status()
125
124
  ));
@@ -168,6 +167,9 @@ fn emit_error(error: RouteError, args: &[String], delimiter: usize, as_json: boo
168
167
  .map(String::as_str)
169
168
  .unwrap_or("status"));
170
169
  }
170
+ if is_usage {
171
+ value["next_actions"] = json!(["team-agent route --help"]);
172
+ }
171
173
  if let Some(text) = super::emit(&CmdOutput::Json(value), as_json) {
172
174
  if as_json {
173
175
  println!("{text}");
@@ -176,6 +178,9 @@ fn emit_error(error: RouteError, args: &[String], delimiter: usize, as_json: boo
176
178
  }
177
179
  }
178
180
  if is_usage {
181
+ if !as_json {
182
+ eprintln!("\n{HELP}");
183
+ }
179
184
  ExitCode::Usage
180
185
  } else {
181
186
  ExitCode::Error
@@ -20,7 +20,7 @@ pub(super) fn watch_notice_json(target: &MessageTarget, opts: &SendOptions) -> V
20
20
  "watcher_id": format!("watch-{agent_id}"),
21
21
  "task_id": opts.task_id.as_ref().map(|t| t.as_str().to_string()),
22
22
  "agent_id": agent_id,
23
- "notice": "Team Agent will collect the result and notify the leader when this task reports completion."
23
+ "notice": "队友回复后会通知主控;用 team-agent inbox leader -n 3 查看。"
24
24
  })
25
25
  }
26
26
 
@@ -183,39 +183,56 @@ pub(super) fn cmd_send_result(value: Value, as_json: bool) -> CmdResult {
183
183
  }
184
184
 
185
185
  pub(super) fn send_human_output(value: &Value) -> String {
186
- let mut parts = vec![
187
- send_human_field(value, "ok"),
188
- format!("status: {}", send_human_status(value)),
189
- send_human_field(value, "message_id"),
190
- format!("target: {}", send_human_target(value)),
191
- ];
192
- for key in ["verification", "stage", "reason", "channel", "turn_verification"] {
193
- if !value.get(key).is_none_or(Value::is_null) {
194
- parts.push(send_human_field(value, key));
186
+ let agent = send_human_target(value);
187
+ if value.get("ok").and_then(Value::as_bool) == Some(false) {
188
+ let reason = value.get("reason").and_then(Value::as_str).unwrap_or("");
189
+ let explanation =
190
+ crate::cli::named_address::human_address_reason(reason).unwrap_or(match reason {
191
+ "target_not_in_team" | "unknown_recipient" => "当前队伍没有这个队友。",
192
+ "missing_permissions" | "human_confirmation_required" => {
193
+ "发送被拒绝:需要先确认交流权限。"
194
+ }
195
+ "recipient_busy" | "recipient_pane_in_non_input_mode" => {
196
+ "队友当前不能接收任务;请先查看其状态。"
197
+ }
198
+ "routing_ambiguous" | "empty_target_list" | "ambiguous" => {
199
+ "没有明确选定收信队友;请填写队友名和任务内容。"
200
+ }
201
+ "team_owner_mismatch" | "PaneWorkspaceMismatch" | "session_drift" => {
202
+ "队伍或终端归属不匹配;已停止发送。"
203
+ }
204
+ "coordinator_unavailable" | "tmux_target_missing" => {
205
+ "队伍连接暂不可用;本次没有确认送达。"
206
+ }
207
+ "message_already_claimed" | "duplicate" => {
208
+ "这条消息已由其他发送处理接收;不要重复发送。"
209
+ }
210
+ "no_caller_pane" => "当前终端没有可用的队伍连接;请在主控的命令行/工具上下文发送。",
211
+ _ => value
212
+ .get("error")
213
+ .and_then(Value::as_str)
214
+ .unwrap_or("发送未成功;没有确认任务已送达。"),
215
+ });
216
+ let mut out = format!("{explanation}\n下一步:先运行 team-agent status,使用列表中的队友名;连接问题用 team-agent doctor --workspace .。不要反复重发。");
217
+ if let Some(suggested) = value
218
+ .get("suggested_name")
219
+ .and_then(Value::as_str)
220
+ .filter(|name| !name.is_empty() && !name.contains(['/', ':', ',', '\n', '\r']))
221
+ {
222
+ out.push_str(&format!(
223
+ "\n你是否想发给 {suggested}?确认后使用这个队友名。"
224
+ ));
195
225
  }
226
+ return out;
196
227
  }
197
- // 0.5.45 naming-addressing (design §3.4/§3.5, RED-3 positional):
198
- // when the refusal envelope carries a scope-safe suggestion,
199
- // surface it verbatim in human output so users can copy the
200
- // right short id. `requested_name` echoes the typo, `suggested_
201
- // name` is the copyable canonical.
202
- if let Some(requested) = value
203
- .get("requested_name")
204
- .and_then(Value::as_str)
205
- .filter(|s| !s.is_empty())
206
- {
207
- parts.push(format!("requested_name: {requested}"));
228
+ if value.get("status").and_then(Value::as_str) == Some("stored_only") {
229
+ return format!("已给{agent}留下消息;本次没有发送到当前对话。\n下一步:用 team-agent inbox leader -n 3 查看回复;留言不保证自动执行。");
208
230
  }
209
- if let Some(suggested) = value
210
- .get("suggested_name")
211
- .and_then(Value::as_str)
212
- .filter(|s| !s.is_empty())
213
- {
214
- parts.push(format!(
215
- "Did you mean `{suggested}`? suggested_name: {suggested}"
216
- ));
231
+ if value.get("delivered").and_then(Value::as_bool) == Some(true) {
232
+ format!("已发给{agent}。{}", crate::cli::SEND_REMINDER)
233
+ } else {
234
+ "任务已收下,但还未送到队友的对话。\n下一步:查看 team-agent status 或 team-agent doctor --workspace .,等真实回复,不反复重发。".to_string()
217
235
  }
218
- parts.join(" ")
219
236
  }
220
237
 
221
238
  pub(super) fn send_human_field(value: &Value, key: &str) -> String {
@@ -227,11 +244,14 @@ pub(super) fn send_human_field(value: &Value, key: &str) -> String {
227
244
  }
228
245
 
229
246
  pub(super) fn send_human_target(value: &Value) -> String {
230
- ["target", "agent_id", "pane_id", "to_name"]
231
- .iter()
232
- .find_map(|key| value.get(*key).filter(|v| !v.is_null()))
233
- .map(send_human_value)
234
- .unwrap_or_else(|| "None".to_string())
247
+ value
248
+ .get("agent_id")
249
+ .and_then(Value::as_str)
250
+ .filter(|name| {
251
+ !name.is_empty() && *name != "*" && !name.contains(['/', ':', ',', '\n', '\r'])
252
+ })
253
+ .map(str::to_string)
254
+ .unwrap_or_else(|| "该队友".to_string())
235
255
  }
236
256
 
237
257
  pub(super) fn send_human_status(value: &Value) -> String {
@@ -265,13 +285,12 @@ pub(super) fn send_reminder_for_value(value: &Value) -> &'static str {
265
285
  let delivered = value.get("delivered").and_then(Value::as_bool);
266
286
  let status = value.get("status").and_then(Value::as_str);
267
287
  let delivery_status = value.get("delivery_status").and_then(Value::as_str);
268
- if delivered == Some(false)
269
- || matches!(status, Some("queued"))
270
- || matches!(delivery_status, Some("pending"))
271
- {
272
- "Message queued; coordinator will notify when the worker receives it. Do not poll the worker terminal with capture-pane."
273
- } else {
288
+ if status == Some("stored_only") {
289
+ "本次只留言,没有发送到当前对话;留言不保证自动执行。"
290
+ } else if delivered == Some(true) && delivery_status != Some("pending") {
274
291
  crate::cli::SEND_REMINDER
292
+ } else {
293
+ "任务已收下,但还未确认送到队友对话。查看 team-agent status/doctor,等真实回复,不反复重发。"
275
294
  }
276
295
  }
277
296