@team-agent/installer 0.5.111 → 0.5.113

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 (73) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +93 -779
  4. package/crates/team-agent/src/cli/diagnose.rs +2 -424
  5. package/crates/team-agent/src/cli/emit.rs +158 -373
  6. package/crates/team-agent/src/cli/leaders.rs +32 -32
  7. package/crates/team-agent/src/cli/mod.rs +8 -15
  8. package/crates/team-agent/src/cli/named_address.rs +17 -16
  9. package/crates/team-agent/src/cli/route.rs +4 -4
  10. package/crates/team-agent/src/cli/send/presentation.rs +19 -18
  11. package/crates/team-agent/src/cli/send/resolve.rs +11 -11
  12. package/crates/team-agent/src/cli/send.rs +4 -4
  13. package/crates/team-agent/src/cli/spec.rs +29 -32
  14. package/crates/team-agent/src/cli/status.rs +7 -7
  15. package/crates/team-agent/src/cli/tests/base.rs +6 -6
  16. package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
  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 +3 -3
  23. package/crates/team-agent/src/cli/tests/status_send.rs +7 -7
  24. package/crates/team-agent/src/cli/triage.rs +53 -2
  25. package/crates/team-agent/src/cli/types.rs +12 -116
  26. package/crates/team-agent/src/compiler/tests.rs +12 -0
  27. package/crates/team-agent/src/compiler.rs +0 -9
  28. package/crates/team-agent/src/coordinator/health.rs +3 -283
  29. package/crates/team-agent/src/coordinator/mod.rs +0 -5
  30. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
  31. package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
  32. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
  33. package/crates/team-agent/src/coordinator/types.rs +1 -21
  34. package/crates/team-agent/src/db/message_store.rs +0 -42
  35. package/crates/team-agent/src/db/migration.rs +49 -9
  36. package/crates/team-agent/src/db/schema.rs +27 -20
  37. package/crates/team-agent/src/leader/owner_bind.rs +0 -24
  38. package/crates/team-agent/src/leader/start.rs +5 -0
  39. package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
  40. package/crates/team-agent/src/leader/tests/identity.rs +21 -32
  41. package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
  42. package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
  43. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +325 -14
  44. package/crates/team-agent/src/lifecycle/launch.rs +2 -2
  45. package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
  46. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
  47. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  48. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
  49. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -2
  50. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
  51. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
  52. package/crates/team-agent/src/messaging/mod.rs +0 -2
  53. package/crates/team-agent/src/messaging/results.rs +0 -58
  54. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
  55. package/crates/team-agent/src/provider/adapter.rs +8 -59
  56. package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
  57. package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
  58. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
  59. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
  60. package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
  61. package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
  62. package/crates/team-agent/src/provider/command_helpers.rs +45 -0
  63. package/crates/team-agent/src/provider/mod.rs +1 -0
  64. package/crates/team-agent/src/provider/model_catalog.rs +21 -28
  65. package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
  66. package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
  67. package/crates/team-agent/src/state/repository.rs +1 -5
  68. package/package.json +4 -4
  69. package/templates/team_state.md +1 -1
  70. package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
  71. package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
  72. package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
  73. 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> {
@@ -20,6 +19,10 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
20
19
  return Ok(result);
21
20
  }
22
21
  append_send_guidance(&mut value, &args.workspace, args.team_id.as_deref());
22
+ finish_quick_start(value, args)
23
+ }
24
+
25
+ fn finish_quick_start(mut value: Value, args: &QuickStartArgs) -> Result<CmdResult, CliError> {
23
26
  let readiness = value.get("readiness").and_then(Value::as_object);
24
27
  let all_resumable_have_session = readiness
25
28
  .and_then(|readiness| readiness.get("all_resumable_have_session"))
@@ -37,7 +40,7 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
37
40
  .get("status")
38
41
  .and_then(Value::as_str)
39
42
  .map(str::to_string);
40
- if !args.detail {
43
+ if !args.detail && value.get("ok").and_then(Value::as_bool) != Some(false) {
41
44
  lifecycle_port::compact_quick_start_value(&mut value);
42
45
  }
43
46
  if args.json
@@ -46,30 +49,18 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
46
49
  || !readiness_ready
47
50
  {
48
51
  let mut result = CmdResult::from_json(value, args.json);
49
- if args.json && status.as_deref() == Some("pending_tool_load") {
52
+ if status.as_deref() == Some("pending_tool_load") {
50
53
  result.exit = ExitCode::Ok;
51
54
  }
52
55
  if !args.json {
53
56
  if let CmdOutput::Json(value) = &result.output {
54
- let explanation = if value.get("ok").and_then(Value::as_bool) == Some(false) {
55
- format!(
56
- "启动未完成:{}",
57
- value
58
- .get("error")
59
- .and_then(Value::as_str)
60
- .unwrap_or("队伍尚未准备好接收任务")
61
- )
62
- } else {
63
- "队伍尚未准备好接收任务;请等待工具完成启动。".to_string()
64
- };
65
- let mut doctor = format!(
66
- "team-agent doctor --workspace {}",
67
- shell_quote(&args.workspace.to_string_lossy())
68
- );
69
- if let Some(team) = args.team_id.as_deref() {
70
- doctor.push_str(&format!(" --team {}", shell_quote(team)));
71
- }
72
- result.output = CmdOutput::Human(format!("{explanation}\n下一步:运行 {doctor},按体检提示处理;用 status 查看状态,不反复起队或重发任务。"));
57
+ // Typed refusals often have no `error`. Preserve the complete
58
+ // report instead of replacing its reason/actions with a guess.
59
+ let safe = crate::redaction::redact_external_value(value);
60
+ result.output = CmdOutput::Human(format!(
61
+ "quick-start report:\n{}",
62
+ serde_json::to_string_pretty(&safe)?
63
+ ));
73
64
  }
74
65
  }
75
66
  Ok(result)
@@ -127,11 +118,11 @@ fn quick_start_config_guidance(value: &mut Value, args: &QuickStartArgs) -> Opti
127
118
  retry.push_str(" --json");
128
119
  }
129
120
  let explanation = if missing_team {
130
- format!("这里还没有队伍配置:{}。没有启动任何队友。请自行创建下面两个文件;本命令没有写入这些文件。", team_path.display())
121
+ format!("Team configuration is missing: {}. No workers were started. Create the two files below; this command did not write them. Error: {error}", team_path.display())
131
122
  } else {
132
- format!("队友配置尚不完整:{detail}。请补齐角色目录或必需字段;下面仅给安全示例,不覆盖已有文件。")
123
+ format!("Worker configuration is incomplete: {detail}. Add the role directory or required fields. The examples below do not overwrite existing files. Error: {error}")
133
124
  };
134
- let next = format!("确认 Pi 已安装并登录;运行 team-agent models --provider pi 核对模型。\n运行 team-agent pi,在该主控的命令行/工具上下文再次执行:\n{retry}");
125
+ let next = format!("Check that Pi is installed and signed in; verify the model with team-agent models --provider pi.\nRun team-agent pi, then execute this command in that leader's tool context:\n{retry}");
135
126
  let mut templates =
136
127
  vec![serde_json::json!({"path": role_path, "content": super::emit::WORKER_TEMPLATE})];
137
128
  let mut human = explanation.clone();
@@ -141,13 +132,13 @@ fn quick_start_config_guidance(value: &mut Value, args: &QuickStartArgs) -> Opti
141
132
  serde_json::json!({"path": team_path, "content": super::emit::TEAM_TEMPLATE}),
142
133
  );
143
134
  human.push_str(&format!(
144
- "\n\n{}:\n{}",
135
+ "\n\n{}:\n{}",
145
136
  team_path.display(),
146
137
  super::emit::TEAM_TEMPLATE
147
138
  ));
148
139
  }
149
140
  human.push_str(&format!(
150
- "\n\n{}:\n{}\n下一步:\n{next}",
141
+ "\n\n{}:\n{}\nNext steps:\n{next}",
151
142
  role_path.display(),
152
143
  super::emit::WORKER_TEMPLATE
153
144
  ));
@@ -167,11 +158,7 @@ fn quick_start_config_guidance(value: &mut Value, args: &QuickStartArgs) -> Opti
167
158
  /// E13:quick-start "team 起了" 人类输出 = summary + attach 块。所有成功出口共用(别每分支手拷)。
168
159
  /// attach_commands 缺/空 → 只 summary(向后兼容)。
169
160
  fn quickstart_human(value: &Value) -> String {
170
- let summary = if value.get("summary").and_then(Value::as_str) == Some("existing runtime") {
171
- "发现已有队伍;请查看返回的连接方式,不会另建同名队伍。"
172
- } else {
173
- "队伍已启动;下面是连接方式和派发任务的命令。"
174
- };
161
+ let summary = value.get("summary").and_then(Value::as_str).unwrap_or("Team started.");
175
162
  let attach: Vec<&str> = value
176
163
  .get("attach_commands")
177
164
  .and_then(Value::as_array)
@@ -184,14 +171,14 @@ fn quickstart_human(value: &Value) -> String {
184
171
  .unwrap_or_default();
185
172
  let mut out = String::from(summary);
186
173
  if !attach.is_empty() {
187
- out.push_str("\n\n连接队伍:");
174
+ out.push_str("\n\nConnect to the team:");
188
175
  for cmd in attach {
189
176
  out.push_str("\n ");
190
177
  out.push_str(cmd);
191
178
  }
192
179
  }
193
180
  if !sends.is_empty() {
194
- out.push_str("\n\n派发任务(请替换任务内容):");
181
+ out.push_str("\n\nSend a task (replace the example message):");
195
182
  for cmd in sends {
196
183
  out.push_str("\n ");
197
184
  out.push_str(cmd);
@@ -258,7 +245,7 @@ pub(crate) fn send_command(agent: &str, workspace: &Path, team: Option<&str>) ->
258
245
  let mut command = format!(
259
246
  "team-agent send {} {} --workspace {}",
260
247
  shell_quote(agent),
261
- shell_quote("请完成任务并把答案回复给 leader。"),
248
+ shell_quote("Complete the task and reply to the leader."),
262
249
  shell_quote(workspace)
263
250
  );
264
251
  if let Some(team) = team {
@@ -350,699 +337,11 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
350
337
  )))
351
338
  }
352
339
 
353
- /// `cmd_watch`(`commands.py:103`)。委派 `coordinator::run_watch`;KeyboardInterrupt/正常 → `SystemExit(0)`。
354
- /// 返回 [`CmdResult::none`](不经 emit)。
355
- pub fn cmd_watch(args: &WatchArgs) -> Result<CmdResult, CliError> {
356
- let workspace = crate::coordinator::WorkspacePath::new(args.workspace.clone());
357
- #[cfg(not(test))]
358
- {
359
- let mut output = |line: &str| {
360
- println!("{line}");
361
- let _ = std::io::Write::flush(&mut std::io::stdout());
362
- };
363
- crate::coordinator::run_watch(&workspace, args.team.as_deref(), 1.0, &mut output)
364
- .map_err(|e| CliError::Runtime(e.to_string()))?;
365
- Ok(CmdResult::none())
366
- }
367
- #[cfg(test)]
368
- {
369
- let store = crate::message_store::MessageStore::open(&args.workspace)
370
- .map_err(|e| CliError::Runtime(e.to_string()))?;
371
- let mut cursor = crate::coordinator::WatchCursor::default();
372
- let lines = crate::coordinator::collect_watch_lines(
373
- &workspace,
374
- &mut cursor,
375
- &store,
376
- args.team.as_deref(),
377
- )
378
- .map_err(|e| CliError::Runtime(e.to_string()))?;
379
- Ok(CmdResult::human(lines.join("\n")))
380
- }
381
- }
382
-
383
- /// `cmd_sessions`(`parser.py:230`)。
384
- pub fn cmd_sessions(args: &SessionsArgs) -> Result<CmdResult, CliError> {
385
- let selected = crate::state::selector::resolve_active_team(
386
- &args.workspace,
387
- args.team.as_deref(),
388
- crate::state::selector::SelectorMode::RuntimeOnly,
389
- )
390
- .map_err(|e| CliError::Runtime(e.to_string()))?;
391
- let spec = match selected.spec_path.as_ref() {
392
- Some(path) => load_team_spec_at(path)?,
393
- None => load_team_spec_optional(&selected.run_workspace, &selected.state)?,
394
- };
395
- Ok(CmdResult::from_json(
396
- json!({
397
- "ok": true,
398
- "sessions": sessions_overview(&selected.state, spec.as_ref()),
399
- "workspace": args.workspace.to_string_lossy().to_string(),
400
- }),
401
- args.json,
402
- ))
403
- }
404
-
405
- pub fn cmd_results(args: &ResultsArgs) -> Result<CmdResult, CliError> {
406
- let selected = match crate::state::selector::resolve_active_team(
407
- &args.workspace,
408
- args.team.as_deref(),
409
- crate::state::selector::SelectorMode::RuntimeOnly,
410
- ) {
411
- Ok(selected) => selected,
412
- Err(error) => {
413
- return Ok(CmdResult::from_json(
414
- json!({
415
- "ok": false,
416
- "error": error.to_string(),
417
- "workspace": args.workspace.to_string_lossy().to_string(),
418
- }),
419
- args.json,
420
- ));
421
- }
422
- };
423
- let results = messaging::results::results_for_case(
424
- &selected.run_workspace,
425
- &args.case_id,
426
- Some(selected.team_key.as_str()),
427
- args.team.as_deref(),
428
- )?;
429
- let value = json!({
430
- "ok": true,
431
- "case_id": args.case_id,
432
- "results": results,
433
- "workspace": selected.run_workspace.to_string_lossy().to_string(),
434
- });
435
- Ok(if args.json {
436
- CmdResult::from_ordered_json(value)
437
- } else {
438
- CmdResult::from_json(value, false)
439
- })
440
- }
441
-
442
340
  pub fn cmd_wait(args: &WaitArgs) -> Result<CmdResult, CliError> {
443
341
  let result = messaging::wait_for_result(&args.workspace, &args.task_id)?;
444
342
  Ok(CmdResult::from_json(result.to_json(), args.json))
445
343
  }
446
344
 
447
- /// `cmd_allow_peer_talk`(`parser.py allow-peer-talk`).
448
- pub fn cmd_allow_peer_talk(args: &AllowPeerTalkArgs) -> Result<CmdResult, CliError> {
449
- if args.team.is_some() {
450
- return Err(CliError::Usage(
451
- "allow-peer-talk --team is not supported yet because peer allowlist storage is not team-scoped".to_string(),
452
- ));
453
- }
454
- let value = messaging::allow_peer_talk(&args.workspace, &args.a, &args.b)?;
455
- Ok(CmdResult::from_json(value, args.json))
456
- }
457
-
458
- /// `cmd_preflight`(`parser.py:160`)。
459
- pub fn cmd_preflight(args: &PreflightArgs) -> Result<CmdResult, CliError> {
460
- let report = build_preflight_report(&args.team)?;
461
- Ok(CmdResult::from_json(report, args.json))
462
- }
463
-
464
- /// `cmd_wait_ready`(`parser.py:171`)。
465
- pub fn cmd_wait_ready(args: &WaitReadyArgs) -> Result<CmdResult, CliError> {
466
- let report = build_wait_ready_report(&args.workspace, args.timeout, args.team.as_deref())?;
467
- Ok(CmdResult::from_json(report, args.json))
468
- }
469
-
470
- /// `cmd_e2e`(`parser.py:449`)。
471
- pub fn cmd_e2e(args: &E2eArgs) -> Result<CmdResult, CliError> {
472
- let mut providers = serde_json::Map::new();
473
- for provider in &args.providers {
474
- let result = if provider == "fake" {
475
- run_fake_e2e(&args.workspace)?
476
- } else {
477
- skipped_provider_e2e(provider, args.real)
478
- };
479
- providers.insert(provider.clone(), result);
480
- }
481
- let ok = providers
482
- .values()
483
- .all(|value| value.get("ok").and_then(Value::as_bool) == Some(true));
484
- Ok(CmdResult::from_json(
485
- json!({
486
- "workspace": args.workspace.to_string_lossy().to_string(),
487
- "providers": Value::Object(providers),
488
- "ok": ok,
489
- }),
490
- args.json,
491
- ))
492
- }
493
-
494
- /// `cmd_peek`(`commands.py:118`)。
495
- pub fn cmd_peek(args: &PeekArgs) -> Result<CmdResult, CliError> {
496
- if !args.allow_raw_screen {
497
- return Err(CliError::Usage(
498
- "peek requires --allow-raw-screen".to_string(),
499
- ));
500
- }
501
- let state = crate::state::persist::load_runtime_state(&args.workspace)?;
502
- let Some(agent_state) = state
503
- .get("agents")
504
- .and_then(|agents| agents.get(&args.agent))
505
- else {
506
- return Ok(CmdResult::from_json(
507
- json!({
508
- "ok": false,
509
- "agent_id": args.agent,
510
- "error": format!("unknown agent id: {}", args.agent),
511
- }),
512
- args.json,
513
- ));
514
- };
515
- let Some((session, window, target)) = agent_pane_id(&state, &args.agent, agent_state) else {
516
- return Ok(peek_unavailable(&args.agent, args.json));
517
- };
518
- // 0.5.x Phase 1d Batch 3: factory-routed peek backend so conpty
519
- // teams peek from the shim scrollback rather than a workspace
520
- // tmux capture that would always be empty. Tmux teams keep
521
- // byte-equivalent behavior.
522
- let backend: Box<dyn crate::transport::Transport> =
523
- match crate::transport_factory::resolve_read_only_transport(
524
- &args.workspace,
525
- Some(&state),
526
- crate::transport_factory::TransportPurpose::Status,
527
- ) {
528
- Ok(r) => r.backend,
529
- Err(_) => Box::new(crate::tmux_backend::TmuxBackend::for_workspace(
530
- &args.workspace,
531
- )),
532
- };
533
- let windows = backend
534
- .list_windows(&crate::transport::SessionName::new(session.clone()))
535
- .map_err(|e| CliError::Runtime(e.to_string()))?;
536
- if !windows.iter().any(|w| w.as_str() == window) {
537
- return Ok(peek_unavailable(&args.agent, args.json));
538
- }
539
- let range = args
540
- .head
541
- .map(|head| crate::transport::CaptureRange::Head(head as u32))
542
- .unwrap_or_else(|| crate::transport::CaptureRange::Tail(args.tail as u32));
543
- let capture = backend
544
- .capture(
545
- &crate::transport::Target::Pane(crate::transport::PaneId::new(target.clone())),
546
- range,
547
- )
548
- .map_err(|e| CliError::Runtime(e.to_string()))?;
549
- let matches = args.search.as_ref().map(|needle| {
550
- capture
551
- .text
552
- .lines()
553
- .filter(|line| line.contains(needle))
554
- .map(str::to_string)
555
- .collect::<Vec<_>>()
556
- });
557
- Ok(CmdResult::from_json(
558
- json!({
559
- "ok": true,
560
- "agent_id": args.agent,
561
- "workspace": args.workspace.to_string_lossy().to_string(),
562
- "tail": args.tail,
563
- "head": args.head,
564
- "search": args.search,
565
- "matches": matches,
566
- "pane_id": target,
567
- "text": capture.text,
568
- }),
569
- args.json,
570
- ))
571
- }
572
-
573
- fn peek_unavailable(agent: &str, json: bool) -> CmdResult {
574
- CmdResult::from_json(
575
- json!({
576
- "ok": false,
577
- "agent_id": agent,
578
- "error": format!("agent terminal is not available: {agent}"),
579
- }),
580
- json,
581
- )
582
- }
583
-
584
- fn agent_pane_id(
585
- state: &Value,
586
- agent: &str,
587
- agent_state: &Value,
588
- ) -> Option<(String, String, String)> {
589
- let session = state
590
- .get("session_name")
591
- .and_then(Value::as_str)
592
- .filter(|s| !s.is_empty())?;
593
- let window = ["window", "window_name"]
594
- .iter()
595
- .find_map(|key| {
596
- agent_state
597
- .get(*key)
598
- .and_then(Value::as_str)
599
- .filter(|s| !s.is_empty())
600
- })
601
- .unwrap_or(agent);
602
- Some((
603
- session.to_string(),
604
- window.to_string(),
605
- format!("{session}:{window}"),
606
- ))
607
- }
608
-
609
- fn run_fake_e2e(workspace: &Path) -> Result<Value, CliError> {
610
- std::fs::create_dir_all(workspace)?;
611
- let spec_path = workspace.join("team.spec.yaml");
612
- std::fs::write(&spec_path, fake_spec_yaml(workspace))?;
613
- let launch = match crate::lifecycle::launch(&spec_path, true, true, true) {
614
- Ok(report) => json!({
615
- "ok": true,
616
- "dry_run": report.dry_run,
617
- "session_name": report.session_name.as_str(),
618
- }),
619
- Err(error) => json!({
620
- "ok": false,
621
- "error": error.to_string(),
622
- }),
623
- };
624
- seed_fake_e2e_state(workspace)?;
625
- let send = messaging::send_message(
626
- workspace,
627
- &MessageTarget::Single("fake_impl".to_string()),
628
- "implement fake task",
629
- &SendOptions {
630
- task_id: Some(TaskId::new("task_impl")),
631
- route_task_id: false,
632
- sender: messaging::TrustedSender::leader(),
633
- requires_ack: true,
634
- ..SendOptions::default()
635
- },
636
- )?;
637
- let send_value = json!({
638
- "ok": send.ok,
639
- "status": send.status,
640
- "message_status": send.message_status.0,
641
- "message_id": send.message_id,
642
- "reason": send.reason,
643
- });
644
- if send.ok {
645
- if let Some(message_id) = send.message_id.as_deref() {
646
- crate::message_store::MessageStore::open(workspace)
647
- .map_err(crate::messaging::MessagingError::from)?
648
- .mark(message_id, "delivered", None)
649
- .map_err(crate::messaging::MessagingError::from)?;
650
- }
651
- let report = messaging::report_result(
652
- workspace,
653
- &json!({
654
- "schema_version": "result_envelope_v1",
655
- "task_id": "task_impl",
656
- "agent_id": "fake_impl",
657
- "status": "success",
658
- "summary": "fake result finalized",
659
- "changes": [],
660
- "tests": [],
661
- "risks": [],
662
- "artifacts": [],
663
- "next_actions": [],
664
- }),
665
- )?;
666
- let auto_finalized =
667
- report.get("finalization_status").and_then(Value::as_str) == Some("auto_finalized");
668
- let shutdown = fake_shutdown(workspace)?;
669
- let ok = launch.get("ok").and_then(Value::as_bool) == Some(true)
670
- && send.ok
671
- && auto_finalized
672
- && shutdown.get("ok").and_then(Value::as_bool) == Some(true);
673
- return Ok(json!({
674
- "ok": ok,
675
- "launch": launch,
676
- "send": send_value,
677
- "report": report,
678
- "shutdown": shutdown,
679
- }));
680
- }
681
- let shutdown = fake_shutdown(workspace)?;
682
- Ok(json!({
683
- "ok": false,
684
- "launch": launch,
685
- "send": send_value,
686
- "report": null,
687
- "shutdown": shutdown,
688
- }))
689
- }
690
-
691
- fn skipped_provider_e2e(provider: &str, real: bool) -> Value {
692
- let command = provider_command(provider);
693
- if !command_on_path(command) {
694
- return json!({
695
- "ok": false,
696
- "skipped": true,
697
- "reason": format!("{command} not installed"),
698
- "version": null,
699
- });
700
- }
701
- let version = command_version(command);
702
- if !real {
703
- return json!({
704
- "ok": false,
705
- "skipped": true,
706
- "reason": "real provider launch disabled; rerun with --real on an authenticated machine",
707
- "version": version,
708
- });
709
- }
710
- json!({
711
- "ok": false,
712
- "skipped": true,
713
- "reason": "real provider e2e is not available in this build",
714
- "version": version,
715
- })
716
- }
717
-
718
- fn command_on_path(command: &str) -> bool {
719
- std::env::var_os("PATH").is_some_and(|paths| {
720
- std::env::split_paths(&paths).any(|dir| {
721
- let candidate = dir.join(command);
722
- candidate.is_file()
723
- })
724
- })
725
- }
726
-
727
- fn command_version(command: &str) -> Value {
728
- match std::process::Command::new(command)
729
- .arg("--version")
730
- .output()
731
- {
732
- Ok(output) if output.status.success() => {
733
- let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
734
- if text.is_empty() {
735
- Value::Null
736
- } else {
737
- Value::String(text)
738
- }
739
- }
740
- _ => Value::Null,
741
- }
742
- }
743
-
744
- fn provider_command(provider: &str) -> &str {
745
- // E2E provider command grammar, not persisted provider identity parsing.
746
- match provider {
747
- "claude" | "claude_code" => "claude",
748
- "gemini" | "gemini_cli" => "gemini",
749
- other => other,
750
- }
751
- }
752
-
753
- fn seed_fake_e2e_state(workspace: &Path) -> Result<(), CliError> {
754
- crate::state::repository::StateRepository::new(workspace).save(
755
- crate::state::repository::StateWriteIntent::FakeE2eSeed,
756
- &json!({
757
- "leader": {"id": "leader"},
758
- "session_name": "team-agent-fake-e2e",
759
- "agents": {
760
- "fake_impl": {
761
- "provider": "fake",
762
- "status": "running",
763
- "window": "fake_impl",
764
- "pane_id": "%fake_impl",
765
- "spawn_cwd": workspace.to_string_lossy().to_string(),
766
- }
767
- },
768
- "tasks": [{
769
- "id": "task_impl",
770
- "title": "Fake implementation",
771
- "type": "implementation",
772
- "assignee": "fake_impl",
773
- "status": "pending",
774
- }]
775
- }),
776
- )?;
777
- Ok(())
778
- }
779
-
780
- fn fake_shutdown(workspace: &Path) -> Result<Value, CliError> {
781
- let mut state = crate::state::persist::load_runtime_state(workspace)?;
782
- if let Some(agents) = state.get_mut("agents").and_then(Value::as_object_mut) {
783
- for agent in agents.values_mut() {
784
- if let Some(obj) = agent.as_object_mut() {
785
- obj.insert("status".to_string(), Value::String("stopped".to_string()));
786
- }
787
- }
788
- }
789
- crate::state::repository::StateRepository::new(workspace).save(
790
- crate::state::repository::StateWriteIntent::FakeE2eSeed,
791
- &state,
792
- )?;
793
- Ok(json!({
794
- "ok": true,
795
- "session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
796
- "session_killed": false,
797
- "coordinator": {"status": "missing", "pid": null},
798
- }))
799
- }
800
-
801
- fn fake_spec_yaml(workspace: &Path) -> String {
802
- let ws = workspace.to_string_lossy();
803
- format!(
804
- r#"version: 1
805
- team:
806
- name: "fake-e2e"
807
- mode: "supervisor_worker"
808
- objective: "Exercise fake provider orchestration."
809
- workspace: "{ws}"
810
- leader:
811
- id: "leader"
812
- role: "leader"
813
- provider: "fake"
814
- model: null
815
- tools:
816
- - "fs_read"
817
- - "fs_list"
818
- - "mcp_team"
819
- context_policy:
820
- keep_user_thread: true
821
- receive_worker_outputs: "structured_only"
822
- max_worker_result_tokens: 2000
823
- agents:
824
- - id: "fake_impl"
825
- role: "implementation_engineer"
826
- provider: "fake"
827
- model: null
828
- working_directory: "{ws}"
829
- system_prompt:
830
- inline: "Handle fake implementation tasks."
831
- file: null
832
- tools:
833
- - "fs_read"
834
- - "fs_write"
835
- - "fs_list"
836
- - "execute_bash"
837
- - "git_diff"
838
- - "mcp_team"
839
- - "provider_builtin"
840
- permission_mode: "restricted"
841
- preferred_for:
842
- - "implementation"
843
- avoid_for: []
844
- output_contract:
845
- format: "result_envelope_v1"
846
- required_fields:
847
- - "task_id"
848
- - "status"
849
- - "summary"
850
- - "artifacts"
851
- routing:
852
- default_assignee: "leader"
853
- rules:
854
- - id: "implementation-to-fake"
855
- match:
856
- type:
857
- - "implementation"
858
- assign_to: "fake_impl"
859
- priority: 10
860
- communication:
861
- protocol: "mcp_inbox"
862
- topology: "leader_centered"
863
- worker_to_worker: true
864
- ack_timeout_sec: 2
865
- result_format: "result_envelope_v1"
866
- message_store:
867
- sqlite: ".team/runtime/team.db"
868
- mirror_files: ".team/messages"
869
- runtime:
870
- backend: "tmux"
871
- session_name: "team-agent-fake-e2e"
872
- auto_launch: true
873
- require_user_approval_before_launch: false
874
- max_active_agents: 1
875
- startup_order:
876
- - "fake_impl"
877
- context:
878
- state_file: "team_state.md"
879
- artifact_dir: ".team/artifacts"
880
- log_dir: ".team/logs"
881
- summarization:
882
- worker_full_logs: "retain_outside_leader_context"
883
- state_update: "after_each_result"
884
- tasks:
885
- - id: "task_impl"
886
- title: "Fake implementation"
887
- type: "implementation"
888
- assignee: "fake_impl"
889
- deps: []
890
- acceptance:
891
- - "fake result collected"
892
- status: "pending"
893
- requires_tools:
894
- - "fs_write"
895
- - "execute_bash"
896
- files:
897
- - "src/example.py"
898
- risk: "low"
899
- "#
900
- )
901
- }
902
-
903
- fn sessions_overview(state: &Value, spec: Option<&crate::model::yaml::Value>) -> Value {
904
- let mut rows = Vec::new();
905
- if let Some(agents) = spec
906
- .and_then(|v| v.get("agents"))
907
- .and_then(crate::model::yaml::Value::as_list)
908
- {
909
- for agent in agents {
910
- let Some(agent_id) = agent.get("id").and_then(crate::model::yaml::Value::as_str) else {
911
- continue;
912
- };
913
- let agent_state = state
914
- .get("agents")
915
- .and_then(|agents| agents.get(agent_id))
916
- .unwrap_or(&Value::Null);
917
- rows.push(session_row(state, agent, agent_id, agent_state));
918
- }
919
- }
920
- Value::Array(rows)
921
- }
922
-
923
- fn session_row(
924
- state: &Value,
925
- spec_agent: &crate::model::yaml::Value,
926
- agent_id: &str,
927
- agent_state: &Value,
928
- ) -> Value {
929
- json!({
930
- "agent_id": agent_id,
931
- "provider": yaml_str(spec_agent, "provider").map(Value::String).unwrap_or(Value::Null),
932
- "model": yaml_str(spec_agent, "model").map(Value::String).unwrap_or(Value::Null),
933
- "profile": yaml_str(spec_agent, "profile").map(Value::String).unwrap_or(Value::Null),
934
- "session_id": field_or_null(agent_state, &["session_id"]),
935
- "resume_id": field_or_null(agent_state, &["resume_id"]),
936
- "rollout_path": field_or_null(agent_state, &["rollout_path"]),
937
- "captured_at": field_or_null(agent_state, &["captured_at"]),
938
- "captured_via": field_or_null(agent_state, &["captured_via"]),
939
- "attribution_confidence": field_or_null(agent_state, &["attribution_confidence"]),
940
- "spawn_cwd": field_or_null(agent_state, &["spawn_cwd", "working_directory"]),
941
- "context_usage": field_or_null(agent_state, &["context_usage"]),
942
- "status": field_or_default(agent_state, "status", "unknown"),
943
- "last_task": last_task_for_agent(state, agent_id),
944
- "handoff_path": field_or_null(agent_state, &["handoff_path"]),
945
- "display_target": field_or_null(agent_state, &["display"]),
946
- "terminal_target": terminal_target(state, agent_id, agent_state),
947
- })
948
- }
949
-
950
- fn yaml_str(value: &crate::model::yaml::Value, key: &str) -> Option<String> {
951
- value
952
- .get(key)
953
- .and_then(crate::model::yaml::Value::as_str)
954
- .map(ToString::to_string)
955
- }
956
-
957
- fn field_or_null(value: &Value, keys: &[&str]) -> Value {
958
- keys.iter()
959
- .find_map(|key| value.get(*key).cloned())
960
- .unwrap_or(Value::Null)
961
- }
962
-
963
- fn field_or_default(value: &Value, key: &str, default: &str) -> Value {
964
- value
965
- .get(key)
966
- .cloned()
967
- .unwrap_or_else(|| Value::String(default.to_string()))
968
- }
969
-
970
- fn last_task_for_agent(state: &Value, agent_id: &str) -> Value {
971
- state
972
- .get("tasks")
973
- .and_then(Value::as_array)
974
- .and_then(|tasks| {
975
- tasks
976
- .iter()
977
- .rev()
978
- .find(|task| task.get("assignee").and_then(Value::as_str) == Some(agent_id))
979
- })
980
- .and_then(|task| {
981
- task.get("id")
982
- .and_then(Value::as_str)
983
- .map(ToString::to_string)
984
- })
985
- .map(Value::String)
986
- .unwrap_or(Value::Null)
987
- }
988
-
989
- fn terminal_target(state: &Value, agent_id: &str, agent_state: &Value) -> Value {
990
- json!({
991
- "session": agent_state
992
- .get("session_name")
993
- .cloned()
994
- .or_else(|| state.get("session_name").cloned())
995
- .unwrap_or(Value::Null),
996
- "window": window_target(agent_state, agent_id),
997
- "pane": field_or_null(agent_state, &["pane_id"]),
998
- })
999
- }
1000
-
1001
- fn window_target(agent_state: &Value, agent_id: &str) -> Value {
1002
- let window = field_or_null(agent_state, &["window", "window_name"]);
1003
- if window.is_null() {
1004
- Value::String(agent_id.to_string())
1005
- } else {
1006
- window
1007
- }
1008
- }
1009
-
1010
- fn load_team_spec_optional(
1011
- workspace: &Path,
1012
- state: &Value,
1013
- ) -> Result<Option<crate::model::yaml::Value>, CliError> {
1014
- let spec_path = state
1015
- .get("spec_path")
1016
- .and_then(Value::as_str)
1017
- .filter(|path| !path.is_empty())
1018
- .map(PathBuf::from)
1019
- .or_else(|| {
1020
- state
1021
- .get("team_dir")
1022
- .and_then(Value::as_str)
1023
- .filter(|path| !path.is_empty())
1024
- .map(|path| PathBuf::from(path).join("team.spec.yaml"))
1025
- })
1026
- .unwrap_or_else(|| workspace.join("team.spec.yaml"));
1027
- if !spec_path.exists() {
1028
- return Ok(None);
1029
- }
1030
- let text = std::fs::read_to_string(&spec_path)?;
1031
- crate::model::yaml::loads(&text)
1032
- .map(Some)
1033
- .map_err(|e| CliError::Runtime(e.to_string()))
1034
- }
1035
-
1036
- fn load_team_spec_at(spec_path: &Path) -> Result<Option<crate::model::yaml::Value>, CliError> {
1037
- if !spec_path.exists() {
1038
- return Ok(None);
1039
- }
1040
- let text = std::fs::read_to_string(spec_path)?;
1041
- crate::model::yaml::loads(&text)
1042
- .map(Some)
1043
- .map_err(|e| CliError::Runtime(e.to_string()))
1044
- }
1045
-
1046
345
  /// `cmd_approvals`(`commands.py:112`)。
1047
346
  pub fn cmd_approvals(args: &ApprovalsArgs) -> Result<CmdResult, CliError> {
1048
347
  let selected = crate::state::selector::resolve_active_team(
@@ -1168,14 +467,6 @@ pub fn cmd_claim_leader(args: &ClaimLeaderArgs) -> Result<CmdResult, CliError> {
1168
467
  Ok(CmdResult::from_json(value, args.json))
1169
468
  }
1170
469
 
1171
- /// `cmd_identity`(`commands.py:160`)。
1172
- pub fn cmd_identity(args: &IdentityArgs) -> Result<CmdResult, CliError> {
1173
- Ok(CmdResult::from_json(
1174
- leader_port::leader_identity(&args.workspace, args.team.as_deref())?,
1175
- args.json,
1176
- ))
1177
- }
1178
-
1179
470
  /// `cmd_shutdown`(`commands.py:340`)。
1180
471
  pub fn cmd_shutdown(args: &ShutdownArgs) -> Result<CmdResult, CliError> {
1181
472
  Ok(CmdResult::from_json(
@@ -1711,12 +1002,13 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
1711
1002
 
1712
1003
  #[cfg(test)]
1713
1004
  mod tests {
1714
- #![allow(clippy::unwrap_used)]
1005
+ #![allow(clippy::unwrap_used, clippy::panic)]
1715
1006
 
1716
1007
  use super::{
1717
- agent_pane_id, append_send_guidance, format_inbox_human, quickstart_human, send_command,
1718
- split_shell_argv,
1008
+ append_send_guidance, finish_quick_start, format_inbox_human, quickstart_human,
1009
+ send_command, split_shell_argv,
1719
1010
  };
1011
+ use crate::cli::{CmdOutput, ExitCode, QuickStartArgs};
1720
1012
  use serde_json::{json, Value};
1721
1013
  use std::path::{Path, PathBuf};
1722
1014
 
@@ -1754,11 +1046,11 @@ mod tests {
1754
1046
  });
1755
1047
  let out = quickstart_human(&value);
1756
1048
  assert!(
1757
- out.contains("队伍已启动"),
1049
+ out.contains("team started"),
1758
1050
  "must show startup summary; got {out}"
1759
1051
  );
1760
1052
  assert!(
1761
- out.contains("连接队伍:"),
1053
+ out.contains("Connect to the team:"),
1762
1054
  "must render attach block; got {out}"
1763
1055
  );
1764
1056
  assert!(
@@ -1797,7 +1089,7 @@ mod tests {
1797
1089
  assert_eq!(
1798
1090
  quickstart_human(&value),
1799
1091
  format!(
1800
- "队伍已启动;下面是连接方式和派发任务的命令。\n{}",
1092
+ "quick-start complete\n{}",
1801
1093
  crate::cli::QUICK_START_REMINDER
1802
1094
  )
1803
1095
  );
@@ -1806,7 +1098,7 @@ mod tests {
1806
1098
  assert_eq!(
1807
1099
  quickstart_human(&value2),
1808
1100
  format!(
1809
- "队伍已启动;下面是连接方式和派发任务的命令。\n{}",
1101
+ "s\n{}",
1810
1102
  crate::cli::QUICK_START_REMINDER
1811
1103
  )
1812
1104
  );
@@ -1827,7 +1119,7 @@ mod tests {
1827
1119
  "team-agent",
1828
1120
  "send",
1829
1121
  "worker name; echo unsafe",
1830
- "请完成任务并把答案回复给 leader。",
1122
+ "Complete the task and reply to the leader.",
1831
1123
  "--workspace",
1832
1124
  "/tmp/my workspace",
1833
1125
  "--team",
@@ -1846,7 +1138,7 @@ mod tests {
1846
1138
  .unwrap();
1847
1139
  assert_eq!(
1848
1140
  command,
1849
- "team-agent send 'worker name; echo unsafe' '请完成任务并把答案回复给 leader。' --workspace '/tmp/my workspace' --team team-a"
1141
+ "team-agent send 'worker name; echo unsafe' 'Complete the task and reply to the leader.' --workspace '/tmp/my workspace' --team team-a"
1850
1142
  );
1851
1143
  }
1852
1144
 
@@ -1903,7 +1195,7 @@ mod tests {
1903
1195
  "team-agent",
1904
1196
  "send",
1905
1197
  "worker",
1906
- "请完成任务并把答案回复给 leader。",
1198
+ "Complete the task and reply to the leader.",
1907
1199
  "--workspace",
1908
1200
  "/tmp/ws",
1909
1201
  "--team",
@@ -1912,49 +1204,71 @@ mod tests {
1912
1204
  );
1913
1205
  }
1914
1206
 
1915
- #[test]
1916
- fn agent_pane_id_resolves_session_window_even_with_recorded_pane_id() {
1917
- let state = json!({
1918
- "session_name": "team-x",
1919
- "agents": {
1920
- "w1": {"pane_id": "%7", "window": "w1"}
1921
- }
1922
- });
1923
- let agent = state
1924
- .get("agents")
1925
- .and_then(|agents| agents.get("w1"))
1926
- .unwrap();
1207
+ fn quick_start_args(as_json: bool) -> QuickStartArgs {
1208
+ QuickStartArgs {
1209
+ workspace: PathBuf::from("/tmp/first-launch"),
1210
+ agents_dir: PathBuf::from("/tmp/first-launch/.team/current"),
1211
+ name: None,
1212
+ team_id: None,
1213
+ yes: true,
1214
+ json: as_json,
1215
+ detail: false,
1216
+ backend: None,
1217
+ }
1218
+ }
1927
1219
 
1928
- assert_eq!(
1929
- agent_pane_id(&state, "w1", agent).unwrap(),
1930
- (
1931
- "team-x".to_string(),
1932
- "w1".to_string(),
1933
- "team-x:w1".to_string()
1934
- )
1935
- );
1220
+ #[test]
1221
+ fn quick_start_human_preserves_typed_refusals_without_error_field() {
1222
+ for status in ["existing_runtime", "preflight_blocked", "leader_binding_refused"] {
1223
+ let value = json!({
1224
+ "ok": false,
1225
+ "status": status,
1226
+ "reason": "the exact underlying refusal",
1227
+ "summary": "creation refused",
1228
+ "blockers": [{"stage": "identity", "reason": "owner mismatch"}],
1229
+ "next_actions": ["team-agent restart --workspace /tmp/first-launch --team current"],
1230
+ "state_path": "/tmp/first-launch/.team/runtime/state.json",
1231
+ "readiness": {"reason": "underlying readiness evidence"}
1232
+ });
1233
+ let result = finish_quick_start(value.clone(), &quick_start_args(false)).unwrap();
1234
+ assert_eq!(result.exit, ExitCode::Error);
1235
+ let CmdOutput::Human(text) = result.output else { panic!("expected human report") };
1236
+ let rendered: Value = serde_json::from_str(text.strip_prefix("quick-start report:\n").unwrap()).unwrap();
1237
+ assert_eq!(rendered, value, "human projection lost fields for {status}");
1238
+ assert!(!text.contains("team-agent doctor"), "must not replace actual actions");
1239
+ let json_result = finish_quick_start(value.clone(), &quick_start_args(true)).unwrap();
1240
+ assert_eq!(json_result.exit, ExitCode::Error);
1241
+ assert_eq!(json_result.output, CmdOutput::Json(value));
1242
+ }
1936
1243
  }
1937
1244
 
1938
1245
  #[test]
1939
- fn agent_pane_id_falls_back_to_session_window_target() {
1940
- let state = json!({
1941
- "session_name": "team-x",
1942
- "agents": {
1943
- "w1": {"window": "worker-one"}
1944
- }
1246
+ fn quick_start_human_preserves_original_error_and_actions() {
1247
+ let value = json!({
1248
+ "ok": false,
1249
+ "error": "database initialization failed at /tmp/first-launch/.team/team.db: permission denied",
1250
+ "action": "repair directory permissions",
1251
+ "next_actions": ["inspect the exact database path"]
1945
1252
  });
1946
- let agent = state
1947
- .get("agents")
1948
- .and_then(|agents| agents.get("w1"))
1949
- .unwrap();
1253
+ let result = finish_quick_start(value.clone(), &quick_start_args(false)).unwrap();
1254
+ assert_eq!(result.exit, ExitCode::Error);
1255
+ let CmdOutput::Human(text) = result.output else { panic!("expected human report") };
1256
+ let rendered: Value = serde_json::from_str(text.strip_prefix("quick-start report:\n").unwrap()).unwrap();
1257
+ assert_eq!(rendered, value);
1258
+ }
1950
1259
 
1951
- assert_eq!(
1952
- agent_pane_id(&state, "w1", agent).unwrap(),
1953
- (
1954
- "team-x".to_string(),
1955
- "worker-one".to_string(),
1956
- "team-x:worker-one".to_string()
1957
- )
1958
- );
1260
+ #[test]
1261
+ fn pending_tool_load_is_not_reported_as_failed_team_creation() {
1262
+ for as_json in [false, true] {
1263
+ let value = json!({
1264
+ "ok": true,
1265
+ "status": "pending_tool_load",
1266
+ "summary": "Team started; worker tool loading is not yet verified",
1267
+ "readiness": {"ready": false},
1268
+ "next_actions": ["send a task and wait for the actual reply"]
1269
+ });
1270
+ let result = finish_quick_start(value, &quick_start_args(as_json)).unwrap();
1271
+ assert_eq!(result.exit, ExitCode::Ok);
1272
+ }
1959
1273
  }
1960
1274
  }