@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
@@ -4,7 +4,6 @@
4
4
  //! `cmd_doctor`(gate/comms/fix-schema/cleanup-orphans 分派)。
5
5
 
6
6
  use super::*;
7
- use crate::transport::Transport;
8
7
 
9
8
  /// `cmd_quick_start`(`commands.py:18`)。`--json` 或 `!ok` → 整 dict;否则 `result["summary"]`。
10
9
  pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
@@ -16,6 +15,9 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
16
15
  args.yes,
17
16
  args.backend.as_deref(),
18
17
  )?;
18
+ if let Some(result) = quick_start_config_guidance(&mut value, args) {
19
+ return Ok(result);
20
+ }
19
21
  append_send_guidance(&mut value, &args.workspace, args.team_id.as_deref());
20
22
  let readiness = value.get("readiness").and_then(Value::as_object);
21
23
  let all_resumable_have_session = readiness
@@ -46,6 +48,29 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
46
48
  if args.json && status.as_deref() == Some("pending_tool_load") {
47
49
  result.exit = ExitCode::Ok;
48
50
  }
51
+ if !args.json {
52
+ if let CmdOutput::Json(value) = &result.output {
53
+ let explanation = if value.get("ok").and_then(Value::as_bool) == Some(false) {
54
+ format!(
55
+ "启动未完成:{}",
56
+ value
57
+ .get("error")
58
+ .and_then(Value::as_str)
59
+ .unwrap_or("队伍尚未准备好接收任务")
60
+ )
61
+ } else {
62
+ "队伍尚未准备好接收任务;请等待工具完成启动。".to_string()
63
+ };
64
+ let mut doctor = format!(
65
+ "team-agent doctor --workspace {}",
66
+ shell_quote(&args.workspace.to_string_lossy())
67
+ );
68
+ if let Some(team) = args.team_id.as_deref() {
69
+ doctor.push_str(&format!(" --team {}", shell_quote(team)));
70
+ }
71
+ result.output = CmdOutput::Human(format!("{explanation}\n下一步:运行 {doctor},按体检提示处理;用 status 查看状态,不反复起队或重发任务。"));
72
+ }
73
+ }
49
74
  Ok(result)
50
75
  } else {
51
76
  // E13:happy 人类路径必须带 attach_commands(json 路径 cli/mod.rs:1775 已有)。
@@ -53,13 +78,99 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
53
78
  }
54
79
  }
55
80
 
81
+ // Project only an actual compiler rejection; never reject a valid lifecycle fallback.
82
+ fn quick_start_config_guidance(value: &mut Value, args: &QuickStartArgs) -> Option<CmdResult> {
83
+ if value.get("ok").and_then(Value::as_bool) != Some(false) {
84
+ return None;
85
+ }
86
+ let error = value.get("error")?.as_str()?;
87
+ let compile = error.strip_prefix("spec compile failed: ")?;
88
+ let detail = compile
89
+ .strip_prefix("validation error: ")
90
+ .unwrap_or(compile);
91
+ let team_path = args.agents_dir.join("TEAM.md");
92
+ let agents_path = args.agents_dir.join("agents");
93
+ let missing_team = detail == format!("{}: missing TEAM.md", team_path.display())
94
+ && team_path.try_exists().ok() == Some(false);
95
+ let missing_roles = detail == format!("{}: missing agents directory", agents_path.display())
96
+ || detail == format!("{}: no role docs found", agents_path.display());
97
+ let missing_field = detail.split_once(": ").is_some_and(|(path, reason)| {
98
+ Path::new(path).parent() == Some(agents_path.as_path())
99
+ && (reason.contains("missing required field") || reason.contains("is required"))
100
+ });
101
+ if !missing_team && !(team_path.is_file() && (missing_roles || missing_field)) {
102
+ return None;
103
+ }
104
+ let role_path = agents_path.join("worker.md");
105
+ let mut retry = format!(
106
+ "team-agent quick-start {} --workspace {}",
107
+ shell_quote(&args.agents_dir.to_string_lossy()),
108
+ shell_quote(&args.workspace.to_string_lossy())
109
+ );
110
+ if let Some(team) = args.team_id.as_deref() {
111
+ retry.push_str(&format!(" --team {}", shell_quote(team)));
112
+ }
113
+ if let Some(name) = args.name.as_deref() {
114
+ retry.push_str(&format!(" --name {}", shell_quote(name)));
115
+ }
116
+ if let Some(backend) = args.backend.as_deref() {
117
+ retry.push_str(&format!(" --backend {}", shell_quote(backend)));
118
+ }
119
+ if args.yes {
120
+ retry.push_str(" --yes");
121
+ }
122
+ if args.detail {
123
+ retry.push_str(" --detail");
124
+ }
125
+ if args.json {
126
+ retry.push_str(" --json");
127
+ }
128
+ let explanation = if missing_team {
129
+ format!("这里还没有队伍配置:{}。没有启动任何队友。请自行创建下面两个文件;本命令没有写入这些文件。", team_path.display())
130
+ } else {
131
+ format!("队友配置尚不完整:{detail}。请补齐角色目录或必需字段;下面仅给安全示例,不覆盖已有文件。")
132
+ };
133
+ let next = format!("确认 Pi 已安装并登录;运行 team-agent models --provider pi 核对模型。\n运行 team-agent pi,在该主控的命令行/工具上下文再次执行:\n{retry}");
134
+ let mut templates =
135
+ vec![serde_json::json!({"path": role_path, "content": super::emit::WORKER_TEMPLATE})];
136
+ let mut human = explanation.clone();
137
+ if missing_team {
138
+ templates.insert(
139
+ 0,
140
+ serde_json::json!({"path": team_path, "content": super::emit::TEAM_TEMPLATE}),
141
+ );
142
+ human.push_str(&format!(
143
+ "\n\n{}:\n{}",
144
+ team_path.display(),
145
+ super::emit::TEAM_TEMPLATE
146
+ ));
147
+ }
148
+ human.push_str(&format!(
149
+ "\n\n{}:\n{}\n下一步:\n{next}",
150
+ role_path.display(),
151
+ super::emit::WORKER_TEMPLATE
152
+ ));
153
+ value["action"] = serde_json::json!(explanation);
154
+ value["next_actions"] =
155
+ serde_json::json!(["team-agent models --provider pi", "team-agent pi", retry]);
156
+ value["templates"] = serde_json::json!(templates);
157
+ if args.json {
158
+ Some(CmdResult::from_json(value.clone(), true))
159
+ } else {
160
+ let mut result = CmdResult::human(&human);
161
+ result.exit = ExitCode::Error;
162
+ Some(result)
163
+ }
164
+ }
165
+
56
166
  /// E13:quick-start "team 起了" 人类输出 = summary + attach 块。所有成功出口共用(别每分支手拷)。
57
167
  /// attach_commands 缺/空 → 只 summary(向后兼容)。
58
168
  fn quickstart_human(value: &Value) -> String {
59
- let summary = value
60
- .get("summary")
61
- .and_then(Value::as_str)
62
- .unwrap_or("quick-start complete");
169
+ let summary = if value.get("summary").and_then(Value::as_str) == Some("existing runtime") {
170
+ "发现已有队伍;请查看返回的连接方式,不会另建同名队伍。"
171
+ } else {
172
+ "队伍已启动;下面是连接方式和派发任务的命令。"
173
+ };
63
174
  let attach: Vec<&str> = value
64
175
  .get("attach_commands")
65
176
  .and_then(Value::as_array)
@@ -72,14 +183,14 @@ fn quickstart_human(value: &Value) -> String {
72
183
  .unwrap_or_default();
73
184
  let mut out = String::from(summary);
74
185
  if !attach.is_empty() {
75
- out.push_str("\n\nattach:");
186
+ out.push_str("\n\n连接队伍:");
76
187
  for cmd in attach {
77
188
  out.push_str("\n ");
78
189
  out.push_str(cmd);
79
190
  }
80
191
  }
81
192
  if !sends.is_empty() {
82
- out.push_str("\n\nsend:");
193
+ out.push_str("\n\n派发任务(请替换任务内容):");
83
194
  for cmd in sends {
84
195
  out.push_str("\n ");
85
196
  out.push_str(cmd);
@@ -146,7 +257,7 @@ pub(crate) fn send_command(agent: &str, workspace: &Path, team: Option<&str>) ->
146
257
  let mut command = format!(
147
258
  "team-agent send {} {} --workspace {}",
148
259
  shell_quote(agent),
149
- shell_quote("MESSAGE"),
260
+ shell_quote("请完成任务并把答案回复给 leader。"),
150
261
  shell_quote(workspace)
151
262
  );
152
263
  if let Some(team) = team {
@@ -156,7 +267,7 @@ pub(crate) fn send_command(agent: &str, workspace: &Path, team: Option<&str>) ->
156
267
  Some(command)
157
268
  }
158
269
 
159
- fn shell_quote(value: &str) -> String {
270
+ pub(crate) fn shell_quote(value: &str) -> String {
160
271
  if value.bytes().all(|byte| {
161
272
  byte.is_ascii_alphanumeric() || matches!(byte, b'/' | b'.' | b'_' | b'-' | b':')
162
273
  }) {
@@ -238,701 +349,11 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
238
349
  )))
239
350
  }
240
351
 
241
- /// `cmd_watch`(`commands.py:103`)。委派 `coordinator::run_watch`;KeyboardInterrupt/正常 → `SystemExit(0)`。
242
- /// 返回 [`CmdResult::none`](不经 emit)。
243
- pub fn cmd_watch(args: &WatchArgs) -> Result<CmdResult, CliError> {
244
- let workspace = crate::coordinator::WorkspacePath::new(args.workspace.clone());
245
- #[cfg(not(test))]
246
- {
247
- let mut output = |line: &str| {
248
- println!("{line}");
249
- let _ = std::io::Write::flush(&mut std::io::stdout());
250
- };
251
- crate::coordinator::run_watch(&workspace, args.team.as_deref(), 1.0, &mut output)
252
- .map_err(|e| CliError::Runtime(e.to_string()))?;
253
- Ok(CmdResult::none())
254
- }
255
- #[cfg(test)]
256
- {
257
- let store = crate::message_store::MessageStore::open(&args.workspace)
258
- .map_err(|e| CliError::Runtime(e.to_string()))?;
259
- let mut cursor = crate::coordinator::WatchCursor::default();
260
- let lines = crate::coordinator::collect_watch_lines(
261
- &workspace,
262
- &mut cursor,
263
- &store,
264
- args.team.as_deref(),
265
- )
266
- .map_err(|e| CliError::Runtime(e.to_string()))?;
267
- Ok(CmdResult::human(lines.join("\n")))
268
- }
269
- }
270
-
271
- /// `cmd_sessions`(`parser.py:230`)。
272
- pub fn cmd_sessions(args: &SessionsArgs) -> Result<CmdResult, CliError> {
273
- let selected = crate::state::selector::resolve_active_team(
274
- &args.workspace,
275
- args.team.as_deref(),
276
- crate::state::selector::SelectorMode::RuntimeOnly,
277
- )
278
- .map_err(|e| CliError::Runtime(e.to_string()))?;
279
- let spec = match selected.spec_path.as_ref() {
280
- Some(path) => load_team_spec_at(path)?,
281
- None => load_team_spec_optional(&selected.run_workspace, &selected.state)?,
282
- };
283
- Ok(CmdResult::from_json(
284
- json!({
285
- "ok": true,
286
- "sessions": sessions_overview(&selected.state, spec.as_ref()),
287
- "workspace": args.workspace.to_string_lossy().to_string(),
288
- }),
289
- args.json,
290
- ))
291
- }
292
-
293
- pub fn cmd_results(args: &ResultsArgs) -> Result<CmdResult, CliError> {
294
- let selected = match crate::state::selector::resolve_active_team(
295
- &args.workspace,
296
- args.team.as_deref(),
297
- crate::state::selector::SelectorMode::RuntimeOnly,
298
- ) {
299
- Ok(selected) => selected,
300
- Err(error) => {
301
- return Ok(CmdResult::from_json(
302
- json!({
303
- "ok": false,
304
- "error": error.to_string(),
305
- "workspace": args.workspace.to_string_lossy().to_string(),
306
- }),
307
- args.json,
308
- ));
309
- }
310
- };
311
- let results = messaging::results::results_for_case(
312
- &selected.run_workspace,
313
- &args.case_id,
314
- Some(selected.team_key.as_str()),
315
- args.team.as_deref(),
316
- )?;
317
- let value = json!({
318
- "ok": true,
319
- "case_id": args.case_id,
320
- "results": results,
321
- "workspace": selected.run_workspace.to_string_lossy().to_string(),
322
- });
323
- Ok(if args.json {
324
- CmdResult::from_ordered_json(value)
325
- } else {
326
- CmdResult::from_json(value, false)
327
- })
328
- }
329
-
330
352
  pub fn cmd_wait(args: &WaitArgs) -> Result<CmdResult, CliError> {
331
353
  let result = messaging::wait_for_result(&args.workspace, &args.task_id)?;
332
354
  Ok(CmdResult::from_json(result.to_json(), args.json))
333
355
  }
334
356
 
335
- /// `cmd_allow_peer_talk`(`parser.py allow-peer-talk`).
336
- pub fn cmd_allow_peer_talk(args: &AllowPeerTalkArgs) -> Result<CmdResult, CliError> {
337
- if args.team.is_some() {
338
- return Err(CliError::Usage(
339
- "allow-peer-talk --team is not supported yet because peer allowlist storage is not team-scoped".to_string(),
340
- ));
341
- }
342
- let value = messaging::allow_peer_talk(&args.workspace, &args.a, &args.b)?;
343
- Ok(CmdResult::from_json(value, args.json))
344
- }
345
-
346
- /// `cmd_preflight`(`parser.py:160`)。
347
- pub fn cmd_preflight(args: &PreflightArgs) -> Result<CmdResult, CliError> {
348
- let report = build_preflight_report(&args.team)?;
349
- Ok(CmdResult::from_json(report, args.json))
350
- }
351
-
352
- /// `cmd_wait_ready`(`parser.py:171`)。
353
- pub fn cmd_wait_ready(args: &WaitReadyArgs) -> Result<CmdResult, CliError> {
354
- let report = build_wait_ready_report(&args.workspace, args.timeout, args.team.as_deref())?;
355
- Ok(CmdResult::from_json(report, args.json))
356
- }
357
-
358
- /// `cmd_e2e`(`parser.py:449`)。
359
- pub fn cmd_e2e(args: &E2eArgs) -> Result<CmdResult, CliError> {
360
- let mut providers = serde_json::Map::new();
361
- for provider in &args.providers {
362
- let result = if provider == "fake" {
363
- run_fake_e2e(&args.workspace)?
364
- } else {
365
- skipped_provider_e2e(provider, args.real)
366
- };
367
- providers.insert(provider.clone(), result);
368
- }
369
- let ok = providers
370
- .values()
371
- .all(|value| value.get("ok").and_then(Value::as_bool) == Some(true));
372
- Ok(CmdResult::from_json(
373
- json!({
374
- "workspace": args.workspace.to_string_lossy().to_string(),
375
- "providers": Value::Object(providers),
376
- "ok": ok,
377
- }),
378
- args.json,
379
- ))
380
- }
381
-
382
- /// `cmd_peek`(`commands.py:118`)。
383
- pub fn cmd_peek(args: &PeekArgs) -> Result<CmdResult, CliError> {
384
- if !args.allow_raw_screen {
385
- return Err(CliError::Usage(
386
- "peek requires --allow-raw-screen".to_string(),
387
- ));
388
- }
389
- let state = crate::state::persist::load_runtime_state(&args.workspace)?;
390
- let Some(agent_state) = state
391
- .get("agents")
392
- .and_then(|agents| agents.get(&args.agent))
393
- else {
394
- return Ok(CmdResult::from_json(
395
- json!({
396
- "ok": false,
397
- "agent_id": args.agent,
398
- "error": format!("unknown agent id: {}", args.agent),
399
- }),
400
- args.json,
401
- ));
402
- };
403
- let Some((session, window, target)) = agent_pane_id(&state, &args.agent, agent_state) else {
404
- return Ok(peek_unavailable(&args.agent, args.json));
405
- };
406
- // 0.5.x Phase 1d Batch 3: factory-routed peek backend so conpty
407
- // teams peek from the shim scrollback rather than a workspace
408
- // tmux capture that would always be empty. Tmux teams keep
409
- // byte-equivalent behavior.
410
- let backend: Box<dyn crate::transport::Transport> =
411
- match crate::transport_factory::resolve_read_only_transport(
412
- &args.workspace,
413
- Some(&state),
414
- crate::transport_factory::TransportPurpose::Status,
415
- ) {
416
- Ok(r) => r.backend,
417
- Err(_) => Box::new(crate::tmux_backend::TmuxBackend::for_workspace(
418
- &args.workspace,
419
- )),
420
- };
421
- let windows = backend
422
- .list_windows(&crate::transport::SessionName::new(session.clone()))
423
- .map_err(|e| CliError::Runtime(e.to_string()))?;
424
- if !windows.iter().any(|w| w.as_str() == window) {
425
- return Ok(peek_unavailable(&args.agent, args.json));
426
- }
427
- let range = args
428
- .head
429
- .map(|head| crate::transport::CaptureRange::Head(head as u32))
430
- .unwrap_or_else(|| crate::transport::CaptureRange::Tail(args.tail as u32));
431
- let capture = backend
432
- .capture(
433
- &crate::transport::Target::Pane(crate::transport::PaneId::new(target.clone())),
434
- range,
435
- )
436
- .map_err(|e| CliError::Runtime(e.to_string()))?;
437
- let matches = args.search.as_ref().map(|needle| {
438
- capture
439
- .text
440
- .lines()
441
- .filter(|line| line.contains(needle))
442
- .map(str::to_string)
443
- .collect::<Vec<_>>()
444
- });
445
- Ok(CmdResult::from_json(
446
- json!({
447
- "ok": true,
448
- "agent_id": args.agent,
449
- "workspace": args.workspace.to_string_lossy().to_string(),
450
- "tail": args.tail,
451
- "head": args.head,
452
- "search": args.search,
453
- "matches": matches,
454
- "pane_id": target,
455
- "text": capture.text,
456
- }),
457
- args.json,
458
- ))
459
- }
460
-
461
- fn peek_unavailable(agent: &str, json: bool) -> CmdResult {
462
- CmdResult::from_json(
463
- json!({
464
- "ok": false,
465
- "agent_id": agent,
466
- "error": format!("agent terminal is not available: {agent}"),
467
- }),
468
- json,
469
- )
470
- }
471
-
472
- fn agent_pane_id(
473
- state: &Value,
474
- agent: &str,
475
- agent_state: &Value,
476
- ) -> Option<(String, String, String)> {
477
- let session = state
478
- .get("session_name")
479
- .and_then(Value::as_str)
480
- .filter(|s| !s.is_empty())?;
481
- let window = ["window", "window_name"]
482
- .iter()
483
- .find_map(|key| {
484
- agent_state
485
- .get(*key)
486
- .and_then(Value::as_str)
487
- .filter(|s| !s.is_empty())
488
- })
489
- .unwrap_or(agent);
490
- Some((
491
- session.to_string(),
492
- window.to_string(),
493
- format!("{session}:{window}"),
494
- ))
495
- }
496
-
497
- fn run_fake_e2e(workspace: &Path) -> Result<Value, CliError> {
498
- std::fs::create_dir_all(workspace)?;
499
- let spec_path = workspace.join("team.spec.yaml");
500
- std::fs::write(&spec_path, fake_spec_yaml(workspace))?;
501
- let launch = match crate::lifecycle::launch(&spec_path, true, true, true) {
502
- Ok(report) => json!({
503
- "ok": true,
504
- "dry_run": report.dry_run,
505
- "session_name": report.session_name.as_str(),
506
- }),
507
- Err(error) => json!({
508
- "ok": false,
509
- "error": error.to_string(),
510
- }),
511
- };
512
- seed_fake_e2e_state(workspace)?;
513
- let send = messaging::send_message(
514
- workspace,
515
- &MessageTarget::Single("fake_impl".to_string()),
516
- "implement fake task",
517
- &SendOptions {
518
- task_id: Some(TaskId::new("task_impl")),
519
- route_task_id: false,
520
- sender: messaging::TrustedSender::leader(),
521
- requires_ack: true,
522
- ..SendOptions::default()
523
- },
524
- )?;
525
- let send_value = json!({
526
- "ok": send.ok,
527
- "status": send.status,
528
- "message_status": send.message_status.0,
529
- "message_id": send.message_id,
530
- "reason": send.reason,
531
- });
532
- if send.ok {
533
- if let Some(message_id) = send.message_id.as_deref() {
534
- crate::message_store::MessageStore::open(workspace)
535
- .map_err(crate::messaging::MessagingError::from)?
536
- .mark(message_id, "delivered", None)
537
- .map_err(crate::messaging::MessagingError::from)?;
538
- }
539
- let report = messaging::report_result(
540
- workspace,
541
- &json!({
542
- "schema_version": "result_envelope_v1",
543
- "task_id": "task_impl",
544
- "agent_id": "fake_impl",
545
- "status": "success",
546
- "summary": "fake result finalized",
547
- "changes": [],
548
- "tests": [],
549
- "risks": [],
550
- "artifacts": [],
551
- "next_actions": [],
552
- }),
553
- )?;
554
- let auto_finalized = report
555
- .get("finalization_status")
556
- .and_then(Value::as_str)
557
- == Some("auto_finalized");
558
- let shutdown = fake_shutdown(workspace)?;
559
- let ok = launch.get("ok").and_then(Value::as_bool) == Some(true)
560
- && send.ok
561
- && auto_finalized
562
- && shutdown.get("ok").and_then(Value::as_bool) == Some(true);
563
- return Ok(json!({
564
- "ok": ok,
565
- "launch": launch,
566
- "send": send_value,
567
- "report": report,
568
- "shutdown": shutdown,
569
- }));
570
- }
571
- let shutdown = fake_shutdown(workspace)?;
572
- Ok(json!({
573
- "ok": false,
574
- "launch": launch,
575
- "send": send_value,
576
- "report": null,
577
- "shutdown": shutdown,
578
- }))
579
- }
580
-
581
- fn skipped_provider_e2e(provider: &str, real: bool) -> Value {
582
- let command = provider_command(provider);
583
- if !command_on_path(command) {
584
- return json!({
585
- "ok": false,
586
- "skipped": true,
587
- "reason": format!("{command} not installed"),
588
- "version": null,
589
- });
590
- }
591
- let version = command_version(command);
592
- if !real {
593
- return json!({
594
- "ok": false,
595
- "skipped": true,
596
- "reason": "real provider launch disabled; rerun with --real on an authenticated machine",
597
- "version": version,
598
- });
599
- }
600
- json!({
601
- "ok": false,
602
- "skipped": true,
603
- "reason": "real provider e2e is not available in this build",
604
- "version": version,
605
- })
606
- }
607
-
608
- fn command_on_path(command: &str) -> bool {
609
- std::env::var_os("PATH").is_some_and(|paths| {
610
- std::env::split_paths(&paths).any(|dir| {
611
- let candidate = dir.join(command);
612
- candidate.is_file()
613
- })
614
- })
615
- }
616
-
617
- fn command_version(command: &str) -> Value {
618
- match std::process::Command::new(command)
619
- .arg("--version")
620
- .output()
621
- {
622
- Ok(output) if output.status.success() => {
623
- let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
624
- if text.is_empty() {
625
- Value::Null
626
- } else {
627
- Value::String(text)
628
- }
629
- }
630
- _ => Value::Null,
631
- }
632
- }
633
-
634
- fn provider_command(provider: &str) -> &str {
635
- // E2E provider command grammar, not persisted provider identity parsing.
636
- match provider {
637
- "claude" | "claude_code" => "claude",
638
- "gemini" | "gemini_cli" => "gemini",
639
- other => other,
640
- }
641
- }
642
-
643
- fn seed_fake_e2e_state(workspace: &Path) -> Result<(), CliError> {
644
- crate::state::repository::StateRepository::new(workspace).save(
645
- crate::state::repository::StateWriteIntent::FakeE2eSeed,
646
- &json!({
647
- "leader": {"id": "leader"},
648
- "session_name": "team-agent-fake-e2e",
649
- "agents": {
650
- "fake_impl": {
651
- "provider": "fake",
652
- "status": "running",
653
- "window": "fake_impl",
654
- "pane_id": "%fake_impl",
655
- "spawn_cwd": workspace.to_string_lossy().to_string(),
656
- }
657
- },
658
- "tasks": [{
659
- "id": "task_impl",
660
- "title": "Fake implementation",
661
- "type": "implementation",
662
- "assignee": "fake_impl",
663
- "status": "pending",
664
- }]
665
- }),
666
- )?;
667
- Ok(())
668
- }
669
-
670
- fn fake_shutdown(workspace: &Path) -> Result<Value, CliError> {
671
- let mut state = crate::state::persist::load_runtime_state(workspace)?;
672
- if let Some(agents) = state.get_mut("agents").and_then(Value::as_object_mut) {
673
- for agent in agents.values_mut() {
674
- if let Some(obj) = agent.as_object_mut() {
675
- obj.insert("status".to_string(), Value::String("stopped".to_string()));
676
- }
677
- }
678
- }
679
- crate::state::repository::StateRepository::new(workspace).save(
680
- crate::state::repository::StateWriteIntent::FakeE2eSeed,
681
- &state,
682
- )?;
683
- Ok(json!({
684
- "ok": true,
685
- "session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
686
- "session_killed": false,
687
- "coordinator": {"status": "missing", "pid": null},
688
- }))
689
- }
690
-
691
- fn fake_spec_yaml(workspace: &Path) -> String {
692
- let ws = workspace.to_string_lossy();
693
- format!(
694
- r#"version: 1
695
- team:
696
- name: "fake-e2e"
697
- mode: "supervisor_worker"
698
- objective: "Exercise fake provider orchestration."
699
- workspace: "{ws}"
700
- leader:
701
- id: "leader"
702
- role: "leader"
703
- provider: "fake"
704
- model: null
705
- tools:
706
- - "fs_read"
707
- - "fs_list"
708
- - "mcp_team"
709
- context_policy:
710
- keep_user_thread: true
711
- receive_worker_outputs: "structured_only"
712
- max_worker_result_tokens: 2000
713
- agents:
714
- - id: "fake_impl"
715
- role: "implementation_engineer"
716
- provider: "fake"
717
- model: null
718
- working_directory: "{ws}"
719
- system_prompt:
720
- inline: "Handle fake implementation tasks."
721
- file: null
722
- tools:
723
- - "fs_read"
724
- - "fs_write"
725
- - "fs_list"
726
- - "execute_bash"
727
- - "git_diff"
728
- - "mcp_team"
729
- - "provider_builtin"
730
- permission_mode: "restricted"
731
- preferred_for:
732
- - "implementation"
733
- avoid_for: []
734
- output_contract:
735
- format: "result_envelope_v1"
736
- required_fields:
737
- - "task_id"
738
- - "status"
739
- - "summary"
740
- - "artifacts"
741
- routing:
742
- default_assignee: "leader"
743
- rules:
744
- - id: "implementation-to-fake"
745
- match:
746
- type:
747
- - "implementation"
748
- assign_to: "fake_impl"
749
- priority: 10
750
- communication:
751
- protocol: "mcp_inbox"
752
- topology: "leader_centered"
753
- worker_to_worker: true
754
- ack_timeout_sec: 2
755
- result_format: "result_envelope_v1"
756
- message_store:
757
- sqlite: ".team/runtime/team.db"
758
- mirror_files: ".team/messages"
759
- runtime:
760
- backend: "tmux"
761
- session_name: "team-agent-fake-e2e"
762
- auto_launch: true
763
- require_user_approval_before_launch: false
764
- max_active_agents: 1
765
- startup_order:
766
- - "fake_impl"
767
- context:
768
- state_file: "team_state.md"
769
- artifact_dir: ".team/artifacts"
770
- log_dir: ".team/logs"
771
- summarization:
772
- worker_full_logs: "retain_outside_leader_context"
773
- state_update: "after_each_result"
774
- tasks:
775
- - id: "task_impl"
776
- title: "Fake implementation"
777
- type: "implementation"
778
- assignee: "fake_impl"
779
- deps: []
780
- acceptance:
781
- - "fake result collected"
782
- status: "pending"
783
- requires_tools:
784
- - "fs_write"
785
- - "execute_bash"
786
- files:
787
- - "src/example.py"
788
- risk: "low"
789
- "#
790
- )
791
- }
792
-
793
- fn sessions_overview(state: &Value, spec: Option<&crate::model::yaml::Value>) -> Value {
794
- let mut rows = Vec::new();
795
- if let Some(agents) = spec
796
- .and_then(|v| v.get("agents"))
797
- .and_then(crate::model::yaml::Value::as_list)
798
- {
799
- for agent in agents {
800
- let Some(agent_id) = agent.get("id").and_then(crate::model::yaml::Value::as_str) else {
801
- continue;
802
- };
803
- let agent_state = state
804
- .get("agents")
805
- .and_then(|agents| agents.get(agent_id))
806
- .unwrap_or(&Value::Null);
807
- rows.push(session_row(state, agent, agent_id, agent_state));
808
- }
809
- }
810
- Value::Array(rows)
811
- }
812
-
813
- fn session_row(
814
- state: &Value,
815
- spec_agent: &crate::model::yaml::Value,
816
- agent_id: &str,
817
- agent_state: &Value,
818
- ) -> Value {
819
- json!({
820
- "agent_id": agent_id,
821
- "provider": yaml_str(spec_agent, "provider").map(Value::String).unwrap_or(Value::Null),
822
- "model": yaml_str(spec_agent, "model").map(Value::String).unwrap_or(Value::Null),
823
- "profile": yaml_str(spec_agent, "profile").map(Value::String).unwrap_or(Value::Null),
824
- "session_id": field_or_null(agent_state, &["session_id"]),
825
- "resume_id": field_or_null(agent_state, &["resume_id"]),
826
- "rollout_path": field_or_null(agent_state, &["rollout_path"]),
827
- "captured_at": field_or_null(agent_state, &["captured_at"]),
828
- "captured_via": field_or_null(agent_state, &["captured_via"]),
829
- "attribution_confidence": field_or_null(agent_state, &["attribution_confidence"]),
830
- "spawn_cwd": field_or_null(agent_state, &["spawn_cwd", "working_directory"]),
831
- "context_usage": field_or_null(agent_state, &["context_usage"]),
832
- "status": field_or_default(agent_state, "status", "unknown"),
833
- "last_task": last_task_for_agent(state, agent_id),
834
- "handoff_path": field_or_null(agent_state, &["handoff_path"]),
835
- "display_target": field_or_null(agent_state, &["display"]),
836
- "terminal_target": terminal_target(state, agent_id, agent_state),
837
- })
838
- }
839
-
840
- fn yaml_str(value: &crate::model::yaml::Value, key: &str) -> Option<String> {
841
- value
842
- .get(key)
843
- .and_then(crate::model::yaml::Value::as_str)
844
- .map(ToString::to_string)
845
- }
846
-
847
- fn field_or_null(value: &Value, keys: &[&str]) -> Value {
848
- keys.iter()
849
- .find_map(|key| value.get(*key).cloned())
850
- .unwrap_or(Value::Null)
851
- }
852
-
853
- fn field_or_default(value: &Value, key: &str, default: &str) -> Value {
854
- value
855
- .get(key)
856
- .cloned()
857
- .unwrap_or_else(|| Value::String(default.to_string()))
858
- }
859
-
860
- fn last_task_for_agent(state: &Value, agent_id: &str) -> Value {
861
- state
862
- .get("tasks")
863
- .and_then(Value::as_array)
864
- .and_then(|tasks| {
865
- tasks
866
- .iter()
867
- .rev()
868
- .find(|task| task.get("assignee").and_then(Value::as_str) == Some(agent_id))
869
- })
870
- .and_then(|task| {
871
- task.get("id")
872
- .and_then(Value::as_str)
873
- .map(ToString::to_string)
874
- })
875
- .map(Value::String)
876
- .unwrap_or(Value::Null)
877
- }
878
-
879
- fn terminal_target(state: &Value, agent_id: &str, agent_state: &Value) -> Value {
880
- json!({
881
- "session": agent_state
882
- .get("session_name")
883
- .cloned()
884
- .or_else(|| state.get("session_name").cloned())
885
- .unwrap_or(Value::Null),
886
- "window": window_target(agent_state, agent_id),
887
- "pane": field_or_null(agent_state, &["pane_id"]),
888
- })
889
- }
890
-
891
- fn window_target(agent_state: &Value, agent_id: &str) -> Value {
892
- let window = field_or_null(agent_state, &["window", "window_name"]);
893
- if window.is_null() {
894
- Value::String(agent_id.to_string())
895
- } else {
896
- window
897
- }
898
- }
899
-
900
- fn load_team_spec_optional(
901
- workspace: &Path,
902
- state: &Value,
903
- ) -> Result<Option<crate::model::yaml::Value>, CliError> {
904
- let spec_path = state
905
- .get("spec_path")
906
- .and_then(Value::as_str)
907
- .filter(|path| !path.is_empty())
908
- .map(PathBuf::from)
909
- .or_else(|| {
910
- state
911
- .get("team_dir")
912
- .and_then(Value::as_str)
913
- .filter(|path| !path.is_empty())
914
- .map(|path| PathBuf::from(path).join("team.spec.yaml"))
915
- })
916
- .unwrap_or_else(|| workspace.join("team.spec.yaml"));
917
- if !spec_path.exists() {
918
- return Ok(None);
919
- }
920
- let text = std::fs::read_to_string(&spec_path)?;
921
- crate::model::yaml::loads(&text)
922
- .map(Some)
923
- .map_err(|e| CliError::Runtime(e.to_string()))
924
- }
925
-
926
- fn load_team_spec_at(spec_path: &Path) -> Result<Option<crate::model::yaml::Value>, CliError> {
927
- if !spec_path.exists() {
928
- return Ok(None);
929
- }
930
- let text = std::fs::read_to_string(spec_path)?;
931
- crate::model::yaml::loads(&text)
932
- .map(Some)
933
- .map_err(|e| CliError::Runtime(e.to_string()))
934
- }
935
-
936
357
  /// `cmd_approvals`(`commands.py:112`)。
937
358
  pub fn cmd_approvals(args: &ApprovalsArgs) -> Result<CmdResult, CliError> {
938
359
  let selected = crate::state::selector::resolve_active_team(
@@ -1015,27 +436,24 @@ fn format_inbox_human(agent: &str, value: &Value) -> String {
1015
436
  .and_then(Value::as_str)
1016
437
  .unwrap_or("-"),
1017
438
  );
1018
- let sender = clean_inbox_field(
1019
- message.get("sender").and_then(Value::as_str).unwrap_or("-"),
1020
- );
439
+ let sender =
440
+ clean_inbox_field(message.get("sender").and_then(Value::as_str).unwrap_or("-"));
1021
441
  let recipient = clean_inbox_field(
1022
442
  message
1023
443
  .get("recipient")
1024
444
  .and_then(Value::as_str)
1025
445
  .unwrap_or("-"),
1026
446
  );
1027
- let status = clean_inbox_field(
1028
- message.get("status").and_then(Value::as_str).unwrap_or("-"),
1029
- );
447
+ let status =
448
+ clean_inbox_field(message.get("status").and_then(Value::as_str).unwrap_or("-"));
1030
449
  let time = clean_inbox_field(
1031
450
  message
1032
451
  .get("created_at")
1033
452
  .and_then(Value::as_str)
1034
453
  .unwrap_or("-"),
1035
454
  );
1036
- let summary = clean_inbox_field(
1037
- message.get("summary").and_then(Value::as_str).unwrap_or(""),
1038
- );
455
+ let summary =
456
+ clean_inbox_field(message.get("summary").and_then(Value::as_str).unwrap_or(""));
1039
457
  truncate_inbox_line(&format!(
1040
458
  "[{id}] [{sender} -> {recipient}] [{status}] [{time}] [{summary}]"
1041
459
  ))
@@ -1054,22 +472,13 @@ pub fn cmd_takeover(args: &TakeoverArgs) -> Result<CmdResult, CliError> {
1054
472
 
1055
473
  /// `cmd_claim_leader`(`commands.py:156`)。
1056
474
  pub fn cmd_claim_leader(args: &ClaimLeaderArgs) -> Result<CmdResult, CliError> {
1057
- let mut value =
1058
- leader_port::claim_leader(&args.workspace, args.team.as_deref(), args.confirm)?;
475
+ let mut value = leader_port::claim_leader(&args.workspace, args.team.as_deref(), args.confirm)?;
1059
476
  if !args.detail {
1060
477
  leader_port::compact_lease_value(&mut value);
1061
478
  }
1062
479
  Ok(CmdResult::from_json(value, args.json))
1063
480
  }
1064
481
 
1065
- /// `cmd_identity`(`commands.py:160`)。
1066
- pub fn cmd_identity(args: &IdentityArgs) -> Result<CmdResult, CliError> {
1067
- Ok(CmdResult::from_json(
1068
- leader_port::leader_identity(&args.workspace, args.team.as_deref())?,
1069
- args.json,
1070
- ))
1071
- }
1072
-
1073
482
  /// `cmd_shutdown`(`commands.py:340`)。
1074
483
  pub fn cmd_shutdown(args: &ShutdownArgs) -> Result<CmdResult, CliError> {
1075
484
  Ok(CmdResult::from_json(
@@ -1104,10 +513,7 @@ pub fn cmd_start_agent(args: &StartAgentArgs) -> Result<CmdResult, CliError> {
1104
513
  )?;
1105
514
  if value.get("agent_ids").is_none() {
1106
515
  if let Some(object) = value.as_object_mut() {
1107
- object.insert(
1108
- "agent_ids".to_string(),
1109
- json!([args.agent.as_str()]),
1110
- );
516
+ object.insert("agent_ids".to_string(), json!([args.agent.as_str()]));
1111
517
  }
1112
518
  }
1113
519
  append_send_guidance(&mut value, &args.workspace, args.team.as_deref());
@@ -1147,10 +553,7 @@ pub fn cmd_add_agent(args: &AddAgentArgs) -> Result<CmdResult, CliError> {
1147
553
  )?;
1148
554
  if value.get("agent_ids").is_none() {
1149
555
  if let Some(object) = value.as_object_mut() {
1150
- object.insert(
1151
- "agent_ids".to_string(),
1152
- json!([args.agent.as_str()]),
1153
- );
556
+ object.insert("agent_ids".to_string(), json!([args.agent.as_str()]));
1154
557
  }
1155
558
  }
1156
559
  append_send_guidance(&mut value, &args.workspace, args.team.as_deref());
@@ -1222,10 +625,8 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
1222
625
  }
1223
626
 
1224
627
  let explicit_comms = args.comms || matches!(args.gate, Some(DoctorGate::Comms));
1225
- let default_report = !explicit_comms
1226
- && args.gate.is_none()
1227
- && !args.cleanup_orphans
1228
- && !args.fix_schema;
628
+ let default_report =
629
+ !explicit_comms && args.gate.is_none() && !args.cleanup_orphans && !args.fix_schema;
1229
630
  let mut value = if explicit_comms {
1230
631
  crate::diagnose::comms::doctor_comms_json(
1231
632
  &args.workspace,
@@ -1282,22 +683,28 @@ fn unified_default_doctor_report(args: &DoctorArgs, mut value: Value) -> Value {
1282
683
  args.team.as_deref(),
1283
684
  crate::state::selector::SelectorMode::RuntimeOnly,
1284
685
  );
1285
- let run_workspace = selected.as_ref().map(|selected| selected.run_workspace.clone())
1286
- .unwrap_or_else(|_| crate::model::paths::canonical_run_workspace(&args.workspace)
1287
- .unwrap_or_else(|_| args.workspace.clone()));
686
+ let run_workspace = selected
687
+ .as_ref()
688
+ .map(|selected| selected.run_workspace.clone())
689
+ .unwrap_or_else(|_| {
690
+ crate::model::paths::canonical_run_workspace(&args.workspace)
691
+ .unwrap_or_else(|_| args.workspace.clone())
692
+ });
1288
693
  let runtime_present = selected.as_ref().is_ok_and(|selected| {
1289
694
  crate::cli::diagnose::workspace_has_existing_team_runtime(
1290
695
  &selected.run_workspace,
1291
696
  &selected.team_key,
1292
697
  )
1293
698
  });
1294
- let db_present = crate::model::paths::runtime_dir(&run_workspace).join("team.db").exists();
699
+ let db_present = crate::model::paths::runtime_dir(&run_workspace)
700
+ .join("team.db")
701
+ .exists();
1295
702
  // One read-only observation supplies both issue classification and details.
1296
703
  // A physical database must be checked even before state.json is created.
1297
704
  let health = (runtime_present || db_present).then(|| {
1298
- crate::coordinator::coordinator_health_read_only(
1299
- &crate::coordinator::WorkspacePath::new(run_workspace.clone()),
1300
- )
705
+ crate::coordinator::coordinator_health_read_only(&crate::coordinator::WorkspacePath::new(
706
+ run_workspace.clone(),
707
+ ))
1301
708
  });
1302
709
  match selected {
1303
710
  Ok(selected) => {
@@ -1358,10 +765,17 @@ fn unified_default_doctor_report(args: &DoctorArgs, mut value: Value) -> Value {
1358
765
  }
1359
766
  if let Some(object) = value.as_object_mut() {
1360
767
  object.insert("runtime".to_string(), runtime);
1361
- object.insert("event_log".to_string(), Value::String(
1362
- run_workspace.join(".team").join("logs").join("events.jsonl")
1363
- .to_string_lossy().to_string(),
1364
- ));
768
+ object.insert(
769
+ "event_log".to_string(),
770
+ Value::String(
771
+ run_workspace
772
+ .join(".team")
773
+ .join("logs")
774
+ .join("events.jsonl")
775
+ .to_string_lossy()
776
+ .to_string(),
777
+ ),
778
+ );
1365
779
  }
1366
780
  finalize_doctor_report(&mut value, true);
1367
781
  value
@@ -1385,7 +799,10 @@ fn merge_report_array(report: &mut Value, key: &str, incoming: &Value) {
1385
799
  .expect("report array normalized before merge");
1386
800
  for item in items {
1387
801
  let identity = report_item_identity(item);
1388
- if !target.iter().any(|existing| report_item_identity(existing) == identity) {
802
+ if !target
803
+ .iter()
804
+ .any(|existing| report_item_identity(existing) == identity)
805
+ {
1389
806
  target.push(item.clone());
1390
807
  }
1391
808
  }
@@ -1450,11 +867,17 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
1450
867
  .and_then(|value| value.as_array().cloned())
1451
868
  .unwrap_or_default();
1452
869
  let mut add = |issue: Value, repair: Option<Value>| {
1453
- if !issues.iter().any(|existing| report_item_identity(existing) == report_item_identity(&issue)) {
870
+ if !issues
871
+ .iter()
872
+ .any(|existing| report_item_identity(existing) == report_item_identity(&issue))
873
+ {
1454
874
  issues.push(issue);
1455
875
  }
1456
876
  if let Some(repair) = repair {
1457
- if !repairs.iter().any(|existing| report_item_identity(existing) == report_item_identity(&repair)) {
877
+ if !repairs
878
+ .iter()
879
+ .any(|existing| report_item_identity(existing) == report_item_identity(&repair))
880
+ {
1458
881
  repairs.push(repair);
1459
882
  }
1460
883
  }
@@ -1467,10 +890,10 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
1467
890
  .or_else(|| object.get("reason").and_then(Value::as_str))
1468
891
  .unwrap_or("doctor_failed");
1469
892
  let issue = if status == "failed" {
1470
- object
1471
- .get("gate")
1472
- .and_then(Value::as_str)
1473
- .map_or_else(|| "doctor_failed".to_string(), |gate| format!("{gate}_failed"))
893
+ object.get("gate").and_then(Value::as_str).map_or_else(
894
+ || "doctor_failed".to_string(),
895
+ |gate| format!("{gate}_failed"),
896
+ )
1474
897
  } else if status.ends_with("_failed") || status == "refused" {
1475
898
  status.to_string()
1476
899
  } else {
@@ -1481,7 +904,10 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
1481
904
  .or_else(|| object.get("next_action"))
1482
905
  .or_else(|| object.get("reason"))
1483
906
  .cloned();
1484
- add(Value::String(issue), repair.map(|value| json!({"action": value})));
907
+ add(
908
+ Value::String(issue),
909
+ repair.map(|value| json!({"action": value})),
910
+ );
1485
911
  }
1486
912
  if object
1487
913
  .get("workspace")
@@ -1512,9 +938,9 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
1512
938
  {
1513
939
  add(
1514
940
  json!("grok_slot_mismatch"),
1515
- grok.get("reason").cloned().map(|reason| {
1516
- json!({"issue": "grok_slot_mismatch", "action": reason})
1517
- }),
941
+ grok.get("reason")
942
+ .cloned()
943
+ .map(|reason| json!({"issue": "grok_slot_mismatch", "action": reason})),
1518
944
  );
1519
945
  }
1520
946
  }
@@ -1559,7 +985,9 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
1559
985
  if runtime_status == "unresolved" {
1560
986
  add(
1561
987
  json!("runtime_selection_failed"),
1562
- Some(json!({"issue": "runtime_selection_failed", "action": "select an existing Team runtime"})),
988
+ Some(
989
+ json!({"issue": "runtime_selection_failed", "action": "select an existing Team runtime"}),
990
+ ),
1563
991
  );
1564
992
  }
1565
993
  if runtime_status == "present"
@@ -1589,7 +1017,7 @@ mod tests {
1589
1017
  #![allow(clippy::unwrap_used)]
1590
1018
 
1591
1019
  use super::{
1592
- agent_pane_id, append_send_guidance, format_inbox_human, quickstart_human, send_command,
1020
+ append_send_guidance, format_inbox_human, quickstart_human, send_command,
1593
1021
  split_shell_argv,
1594
1022
  };
1595
1023
  use serde_json::{json, Value};
@@ -1610,7 +1038,10 @@ mod tests {
1610
1038
  let output = format_inbox_human("worker", &value);
1611
1039
  for line in output.lines() {
1612
1040
  assert!(line.len() <= 160, "line is {} bytes: {line:?}", line.len());
1613
- assert!(!line.chars().any(char::is_control), "controls leaked: {line:?}");
1041
+ assert!(
1042
+ !line.chars().any(char::is_control),
1043
+ "controls leaked: {line:?}"
1044
+ );
1614
1045
  }
1615
1046
  }
1616
1047
 
@@ -1625,9 +1056,12 @@ mod tests {
1625
1056
  ],
1626
1057
  });
1627
1058
  let out = quickstart_human(&value);
1628
- assert!(out.contains("team started"), "must keep summary; got {out}");
1629
1059
  assert!(
1630
- out.contains("attach:"),
1060
+ out.contains("队伍已启动"),
1061
+ "must show startup summary; got {out}"
1062
+ );
1063
+ assert!(
1064
+ out.contains("连接队伍:"),
1631
1065
  "must render attach block; got {out}"
1632
1066
  );
1633
1067
  assert!(
@@ -1643,14 +1077,20 @@ mod tests {
1643
1077
  #[test]
1644
1078
  fn reminders_only_recommend_supported_result_commands() {
1645
1079
  let reminder = crate::cli::QUICK_START_REMINDER;
1646
- assert!(reminder.contains("Use team-agent status / inbox instead."));
1080
+ assert!(reminder.contains("team-agent status"));
1081
+ assert!(reminder.contains("team-agent inbox"));
1647
1082
  for text in [
1648
1083
  reminder,
1649
1084
  crate::cli::STATUS_REMINDER,
1650
1085
  crate::cli::SEND_REMINDER,
1651
1086
  &quickstart_human(&json!({"summary": "team started"})),
1652
1087
  ] {
1653
- assert!(!text.contains("collect"), "removed CLI command leaked: {text}");
1088
+ for hidden in ["collect", "team-agent results", "team-agent wait"] {
1089
+ assert!(
1090
+ !text.contains(hidden),
1091
+ "private/removed CLI command leaked: {text}"
1092
+ );
1093
+ }
1654
1094
  }
1655
1095
  }
1656
1096
 
@@ -1659,13 +1099,19 @@ mod tests {
1659
1099
  let value = json!({"summary": "quick-start complete"});
1660
1100
  assert_eq!(
1661
1101
  quickstart_human(&value),
1662
- format!("quick-start complete\n{}", crate::cli::QUICK_START_REMINDER)
1102
+ format!(
1103
+ "队伍已启动;下面是连接方式和派发任务的命令。\n{}",
1104
+ crate::cli::QUICK_START_REMINDER
1105
+ )
1663
1106
  );
1664
- // 空数组也只 summary。
1107
+ // 空数组也只显示人类摘要与下一步,不凭空编造连接方式。
1665
1108
  let value2 = json!({"summary": "s", "attach_commands": []});
1666
1109
  assert_eq!(
1667
1110
  quickstart_human(&value2),
1668
- format!("s\n{}", crate::cli::QUICK_START_REMINDER)
1111
+ format!(
1112
+ "队伍已启动;下面是连接方式和派发任务的命令。\n{}",
1113
+ crate::cli::QUICK_START_REMINDER
1114
+ )
1669
1115
  );
1670
1116
  }
1671
1117
 
@@ -1684,7 +1130,7 @@ mod tests {
1684
1130
  "team-agent",
1685
1131
  "send",
1686
1132
  "worker name; echo unsafe",
1687
- "MESSAGE",
1133
+ "请完成任务并把答案回复给 leader。",
1688
1134
  "--workspace",
1689
1135
  "/tmp/my workspace",
1690
1136
  "--team",
@@ -1695,11 +1141,15 @@ mod tests {
1695
1141
 
1696
1142
  #[test]
1697
1143
  fn send_guidance_is_copyable_and_preserves_explicit_scope() {
1698
- let command = send_command("worker name; echo unsafe", Path::new("/tmp/my workspace"), Some("team-a"))
1699
- .unwrap();
1144
+ let command = send_command(
1145
+ "worker name; echo unsafe",
1146
+ Path::new("/tmp/my workspace"),
1147
+ Some("team-a"),
1148
+ )
1149
+ .unwrap();
1700
1150
  assert_eq!(
1701
1151
  command,
1702
- "team-agent send 'worker name; echo unsafe' MESSAGE --workspace '/tmp/my workspace' --team team-a"
1152
+ "team-agent send 'worker name; echo unsafe' '请完成任务并把答案回复给 leader。' --workspace '/tmp/my workspace' --team team-a"
1703
1153
  );
1704
1154
  }
1705
1155
 
@@ -1720,7 +1170,10 @@ mod tests {
1720
1170
  fn quick_start_guidance_lists_every_known_agent_without_guessing() {
1721
1171
  let mut value = json!({"ok": true, "agent_ids": ["sol", "luna"]});
1722
1172
  append_send_guidance(&mut value, Path::new("/tmp/ws"), Some("team"));
1723
- let commands = value.get("send_commands").and_then(|v| v.as_array()).unwrap();
1173
+ let commands = value
1174
+ .get("send_commands")
1175
+ .and_then(|v| v.as_array())
1176
+ .unwrap();
1724
1177
  assert_eq!(commands.len(), 2);
1725
1178
  assert!(commands[0].as_str().unwrap().contains("send sol"));
1726
1179
  assert!(commands[1].as_str().unwrap().contains("send luna"));
@@ -1747,53 +1200,19 @@ mod tests {
1747
1200
  .unwrap();
1748
1201
  assert!(command.contains("send worker"));
1749
1202
  assert!(command.contains("--team team-a"));
1750
- let parts: Vec<&str> = command.split_whitespace().collect();
1751
- assert_eq!(parts[3], "MESSAGE");
1752
- }
1753
-
1754
- #[test]
1755
- fn agent_pane_id_resolves_session_window_even_with_recorded_pane_id() {
1756
- let state = json!({
1757
- "session_name": "team-x",
1758
- "agents": {
1759
- "w1": {"pane_id": "%7", "window": "w1"}
1760
- }
1761
- });
1762
- let agent = state
1763
- .get("agents")
1764
- .and_then(|agents| agents.get("w1"))
1765
- .unwrap();
1766
-
1767
1203
  assert_eq!(
1768
- agent_pane_id(&state, "w1", agent).unwrap(),
1769
- (
1770
- "team-x".to_string(),
1771
- "w1".to_string(),
1772
- "team-x:w1".to_string()
1773
- )
1204
+ split_shell_argv(command),
1205
+ [
1206
+ "team-agent",
1207
+ "send",
1208
+ "worker",
1209
+ "请完成任务并把答案回复给 leader。",
1210
+ "--workspace",
1211
+ "/tmp/ws",
1212
+ "--team",
1213
+ "team-a",
1214
+ ]
1774
1215
  );
1775
1216
  }
1776
1217
 
1777
- #[test]
1778
- fn agent_pane_id_falls_back_to_session_window_target() {
1779
- let state = json!({
1780
- "session_name": "team-x",
1781
- "agents": {
1782
- "w1": {"window": "worker-one"}
1783
- }
1784
- });
1785
- let agent = state
1786
- .get("agents")
1787
- .and_then(|agents| agents.get("w1"))
1788
- .unwrap();
1789
-
1790
- assert_eq!(
1791
- agent_pane_id(&state, "w1", agent).unwrap(),
1792
- (
1793
- "team-x".to_string(),
1794
- "worker-one".to_string(),
1795
- "team-x:worker-one".to_string()
1796
- )
1797
- );
1798
- }
1799
1218
  }