@team-agent/installer 0.3.2 → 0.3.3

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 (78) hide show
  1. package/Cargo.lock +34 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -1
  4. package/crates/team-agent/src/cli/adapters.rs +196 -19
  5. package/crates/team-agent/src/cli/diagnose.rs +144 -10
  6. package/crates/team-agent/src/cli/emit.rs +286 -52
  7. package/crates/team-agent/src/cli/leader.rs +37 -8
  8. package/crates/team-agent/src/cli/mod.rs +799 -316
  9. package/crates/team-agent/src/cli/status_port.rs +25 -2
  10. package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
  11. package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
  12. package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
  13. package/crates/team-agent/src/cli/tests/run_delegation.rs +57 -3
  14. package/crates/team-agent/src/cli/types.rs +17 -0
  15. package/crates/team-agent/src/compiler.rs +15 -5
  16. package/crates/team-agent/src/coordinator/health.rs +89 -20
  17. package/crates/team-agent/src/coordinator/mod.rs +4 -0
  18. package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
  19. package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
  20. package/crates/team-agent/src/coordinator/tick.rs +222 -69
  21. package/crates/team-agent/src/coordinator/types.rs +15 -3
  22. package/crates/team-agent/src/db/schema.rs +37 -2
  23. package/crates/team-agent/src/diagnose/comms.rs +226 -0
  24. package/crates/team-agent/src/diagnose/mod.rs +45 -0
  25. package/crates/team-agent/src/diagnose/orphans.rs +658 -0
  26. package/crates/team-agent/src/fake_worker.rs +146 -3
  27. package/crates/team-agent/src/leader/start.rs +121 -23
  28. package/crates/team-agent/src/leader/types.rs +44 -1
  29. package/crates/team-agent/src/lib.rs +3 -0
  30. package/crates/team-agent/src/lifecycle/display.rs +645 -47
  31. package/crates/team-agent/src/lifecycle/launch.rs +818 -116
  32. package/crates/team-agent/src/lifecycle/mod.rs +2 -0
  33. package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
  34. package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
  35. package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
  36. package/crates/team-agent/src/lifecycle/restart/common.rs +177 -83
  37. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +443 -9
  38. package/crates/team-agent/src/lifecycle/restart/remove.rs +22 -6
  39. package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
  40. package/crates/team-agent/src/lifecycle/restart.rs +4 -1
  41. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
  42. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
  43. package/crates/team-agent/src/lifecycle/types.rs +19 -0
  44. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  45. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
  46. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
  47. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
  48. package/crates/team-agent/src/mcp_server/mod.rs +3 -74
  49. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
  50. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
  51. package/crates/team-agent/src/mcp_server/tools.rs +312 -111
  52. package/crates/team-agent/src/mcp_server/types.rs +6 -4
  53. package/crates/team-agent/src/mcp_server/wire.rs +19 -7
  54. package/crates/team-agent/src/message_store.rs +21 -4
  55. package/crates/team-agent/src/messaging/delivery.rs +87 -37
  56. package/crates/team-agent/src/messaging/mod.rs +9 -6
  57. package/crates/team-agent/src/messaging/results.rs +153 -16
  58. package/crates/team-agent/src/messaging/selftest.rs +199 -12
  59. package/crates/team-agent/src/messaging/send.rs +35 -3
  60. package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
  61. package/crates/team-agent/src/messaging/types.rs +11 -3
  62. package/crates/team-agent/src/os_probe.rs +119 -0
  63. package/crates/team-agent/src/packaging/migrate.rs +10 -2
  64. package/crates/team-agent/src/packaging/tests.rs +23 -0
  65. package/crates/team-agent/src/provider/adapter.rs +483 -67
  66. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
  67. package/crates/team-agent/src/provider/classify.rs +51 -4
  68. package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
  69. package/crates/team-agent/src/provider/types.rs +47 -0
  70. package/crates/team-agent/src/session_capture.rs +616 -0
  71. package/crates/team-agent/src/state/persist.rs +57 -0
  72. package/crates/team-agent/src/state/projection.rs +32 -23
  73. package/crates/team-agent/src/state/selector.rs +5 -2
  74. package/crates/team-agent/src/tmux_backend.rs +97 -60
  75. package/crates/team-agent/src/transport/test_support.rs +9 -0
  76. package/crates/team-agent/src/transport/tests/wire.rs +4 -0
  77. package/crates/team-agent/src/transport.rs +13 -2
  78. package/package.json +4 -4
package/Cargo.lock CHANGED
@@ -153,6 +153,16 @@ version = "0.1.9"
153
153
  source = "registry+https://github.com/rust-lang/crates.io-index"
154
154
  checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
155
155
 
156
+ [[package]]
157
+ name = "fslock"
158
+ version = "0.2.1"
159
+ source = "registry+https://github.com/rust-lang/crates.io-index"
160
+ checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb"
161
+ dependencies = [
162
+ "libc",
163
+ "winapi",
164
+ ]
165
+
156
166
  [[package]]
157
167
  name = "futures-core"
158
168
  version = "0.3.32"
@@ -494,6 +504,7 @@ version = "3.5.0"
494
504
  source = "registry+https://github.com/rust-lang/crates.io-index"
495
505
  checksum = "699f4197115b8a7e7ff19c9a315a4bd6fffec26cc4626ef45ecaea389e081c6d"
496
506
  dependencies = [
507
+ "fslock",
497
508
  "futures-executor",
498
509
  "futures-util",
499
510
  "log",
@@ -555,7 +566,7 @@ dependencies = [
555
566
 
556
567
  [[package]]
557
568
  name = "team-agent"
558
- version = "0.3.2"
569
+ version = "0.3.3"
559
570
  dependencies = [
560
571
  "anyhow",
561
572
  "chrono",
@@ -658,6 +669,28 @@ dependencies = [
658
669
  "unicode-ident",
659
670
  ]
660
671
 
672
+ [[package]]
673
+ name = "winapi"
674
+ version = "0.3.9"
675
+ source = "registry+https://github.com/rust-lang/crates.io-index"
676
+ checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
677
+ dependencies = [
678
+ "winapi-i686-pc-windows-gnu",
679
+ "winapi-x86_64-pc-windows-gnu",
680
+ ]
681
+
682
+ [[package]]
683
+ name = "winapi-i686-pc-windows-gnu"
684
+ version = "0.4.0"
685
+ source = "registry+https://github.com/rust-lang/crates.io-index"
686
+ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
687
+
688
+ [[package]]
689
+ name = "winapi-x86_64-pc-windows-gnu"
690
+ version = "0.4.0"
691
+ source = "registry+https://github.com/rust-lang/crates.io-index"
692
+ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
693
+
661
694
  [[package]]
662
695
  name = "windows-core"
663
696
  version = "0.62.2"
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent"]
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.3.2"
12
+ version = "0.3.3"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -27,7 +27,7 @@ libc.workspace = true
27
27
  rusqlite.workspace = true # step 3 db(bundled SQLite,静态链接)
28
28
 
29
29
  [dev-dependencies]
30
- serial_test = "3"
30
+ serial_test = { version = "3", features = ["file_locks"] }
31
31
 
32
32
  [lints]
33
33
  workspace = true
@@ -44,6 +44,9 @@ pub fn cmd_init(args: &InitArgs) -> Result<CmdResult, CliError> {
44
44
  let team_root = args.workspace.join(".team");
45
45
  let spec_path = team_root.join("current").join("team.spec.yaml");
46
46
  let state_path = args.workspace.join("team_state.md");
47
+ let team_md_path = args.workspace.join("TEAM.md");
48
+ let agents_dir = args.workspace.join("agents");
49
+ let default_agent_path = agents_dir.join("worker.md");
47
50
  if spec_path.exists() && !args.force {
48
51
  return Err(CliError::Runtime(format!(
49
52
  "{} already exists; pass --force to overwrite",
@@ -57,10 +60,23 @@ pub fn cmd_init(args: &InitArgs) -> Result<CmdResult, CliError> {
57
60
  team_root.join("logs"),
58
61
  team_root.join("messages"),
59
62
  team_root.join("artifacts"),
63
+ agents_dir.clone(),
60
64
  ] {
61
65
  std::fs::create_dir_all(&dir)?;
62
66
  }
63
67
  std::fs::write(&spec_path, INIT_SPEC_TEMPLATE)?;
68
+ if args.force || !team_md_path.exists() {
69
+ std::fs::write(
70
+ &team_md_path,
71
+ "---\nname: current\nobjective: Pending.\nprovider: fake\n---\n\nPending.\n",
72
+ )?;
73
+ }
74
+ if args.force || !default_agent_path.exists() {
75
+ std::fs::write(
76
+ &default_agent_path,
77
+ "---\nname: worker\nrole: Worker\nprovider: fake\ntools:\n - mcp_team\n---\n\nWait for instructions.\n",
78
+ )?;
79
+ }
64
80
  if args.force || !state_path.exists() {
65
81
  std::fs::write(&state_path, INIT_STATE_TEMPLATE)?;
66
82
  }
@@ -92,8 +108,30 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
92
108
  args.yes,
93
109
  args.fresh,
94
110
  )?;
95
- if args.json || value.get("ok").and_then(Value::as_bool) == Some(false) {
96
- Ok(CmdResult::from_json(value, args.json))
111
+ let readiness = value.get("readiness").and_then(Value::as_object);
112
+ let all_resumable_have_session = readiness
113
+ .and_then(|readiness| readiness.get("all_resumable_have_session"))
114
+ .and_then(Value::as_bool)
115
+ .unwrap_or(true);
116
+ let session_capture_incomplete = readiness
117
+ .and_then(|readiness| readiness.get("session_capture_incomplete"))
118
+ .and_then(Value::as_bool)
119
+ .unwrap_or(!all_resumable_have_session);
120
+ let readiness_ready = readiness
121
+ .and_then(|readiness| readiness.get("ready"))
122
+ .and_then(Value::as_bool)
123
+ .unwrap_or(true);
124
+ let status = value.get("status").and_then(Value::as_str).map(str::to_string);
125
+ if args.json
126
+ || value.get("ok").and_then(Value::as_bool) == Some(false)
127
+ || session_capture_incomplete
128
+ || !readiness_ready
129
+ {
130
+ let mut result = CmdResult::from_json(value, args.json);
131
+ if args.json && status.as_deref() == Some("pending_tool_load") {
132
+ result.exit = ExitCode::Ok;
133
+ }
134
+ Ok(result)
97
135
  } else {
98
136
  Ok(CmdResult::human(
99
137
  value
@@ -360,7 +398,18 @@ pub fn cmd_settle(args: &SettleArgs) -> Result<CmdResult, CliError> {
360
398
  }
361
399
 
362
400
  fn settle_value(workspace: &Path) -> Result<Value, CliError> {
363
- let mut collect = messaging::collect(workspace, None, false)?;
401
+ let selected = crate::state::selector::resolve_active_team(
402
+ workspace,
403
+ None,
404
+ crate::state::selector::SelectorMode::RuntimeOnly,
405
+ )
406
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
407
+ let mut collect = messaging::collect_for_team(
408
+ &selected.run_workspace,
409
+ None,
410
+ false,
411
+ Some(&selected.team_key),
412
+ )?;
364
413
  if collect.get("ok").and_then(Value::as_bool) == Some(false) {
365
414
  let message = collect
366
415
  .get("error")
@@ -369,7 +418,7 @@ fn settle_value(workspace: &Path) -> Result<Value, CliError> {
369
418
  return Err(CliError::Runtime(message.to_string()));
370
419
  }
371
420
  let coordinator_log = crate::coordinator::coordinator_log_path(
372
- &crate::coordinator::WorkspacePath::new(workspace.to_path_buf()),
421
+ &crate::coordinator::WorkspacePath::new(selected.run_workspace.clone()),
373
422
  );
374
423
  let collect_object = collect
375
424
  .as_object_mut()
@@ -382,21 +431,92 @@ fn settle_value(workspace: &Path) -> Result<Value, CliError> {
382
431
  "log": coordinator_log.to_string_lossy().to_string(),
383
432
  }),
384
433
  );
385
- let status = status_port::status(workspace, true, false)?;
386
- let details_log = write_settle_details_log(workspace, &collect, &status)?;
434
+ collect_object.insert("team_key".to_string(), json!(selected.team_key.clone()));
435
+ collect_object.insert("active_team_key".to_string(), json!(selected.team_key.clone()));
436
+ collect_object.insert("team".to_string(), json!(selected.team_key.clone()));
437
+ if let Some(collected_results) = collect_object.get("collected_results").cloned() {
438
+ collect_object.insert("collected".to_string(), collected_results);
439
+ }
440
+ let status_state =
441
+ crate::state::projection::select_runtime_state(&selected.run_workspace, Some(&selected.team_key))?;
442
+ let state_file = match (selected.spec_path.as_ref(), selected.spec_workspace.as_ref()) {
443
+ (Some(spec_path), Some(spec_workspace)) => match load_team_spec_at(spec_path)? {
444
+ Some(spec) => crate::lifecycle::restart::write_team_state(spec_workspace, &spec, &status_state)
445
+ .map_err(|e| CliError::Runtime(e.to_string()))?
446
+ .to_string_lossy()
447
+ .to_string(),
448
+ None => collect
449
+ .get("state_file")
450
+ .and_then(Value::as_str)
451
+ .unwrap_or("")
452
+ .to_string(),
453
+ },
454
+ _ => collect
455
+ .get("state_file")
456
+ .and_then(Value::as_str)
457
+ .unwrap_or("")
458
+ .to_string(),
459
+ };
460
+ if let Some(obj) = collect.as_object_mut() {
461
+ obj.insert("state_file".to_string(), json!(state_file.clone()));
462
+ }
463
+ let mut status = status_port::status_scoped(
464
+ &selected.run_workspace,
465
+ &status_state,
466
+ Some(&selected.team_key),
467
+ true,
468
+ false,
469
+ )?;
470
+ if let Some(obj) = status.as_object_mut() {
471
+ obj.insert("team_key".to_string(), json!(selected.team_key.clone()));
472
+ obj.insert("active_team_key".to_string(), json!(selected.team_key.clone()));
473
+ obj.insert("team".to_string(), json!(selected.team_key.clone()));
474
+ }
475
+ let details_log = write_settle_details_log(&selected.run_workspace, &collect, &status)?;
387
476
  let collected_count = collect
388
477
  .get("collected")
389
478
  .and_then(Value::as_array)
390
479
  .map_or(0, Vec::len);
480
+ let settled_results = settle_collected_results_for_team(
481
+ collect.get("collected_results"),
482
+ &selected.team_key,
483
+ );
391
484
  Ok(json!({
392
485
  "ok": true,
393
486
  "summary": format!("collected {collected_count} result(s)"),
394
487
  "next_actions": ["Review team_state.md and decide whether to continue or shutdown."],
395
488
  "details_log": details_log.to_string_lossy().to_string(),
489
+ "collected_results": settled_results,
490
+ "collected": collect.get("collected").cloned().unwrap_or_else(|| json!([])),
491
+ "results": collect.get("results").cloned().unwrap_or_else(|| json!({})),
492
+ "state_file": state_file,
493
+ "status": status,
396
494
  "collect": collect,
495
+ "team_key": selected.team_key,
496
+ "active_team_key": selected.team_key,
497
+ "team": selected.team_key,
498
+ "workspace": selected.run_workspace.to_string_lossy().to_string(),
397
499
  }))
398
500
  }
399
501
 
502
+ fn settle_collected_results_for_team(value: Option<&Value>, team_key: &str) -> Value {
503
+ let Some(Value::Array(results)) = value else {
504
+ return json!([]);
505
+ };
506
+ Value::Array(
507
+ results
508
+ .iter()
509
+ .map(|result| {
510
+ let mut result = result.clone();
511
+ if let Some(obj) = result.as_object_mut() {
512
+ obj.insert("owner_team_id".to_string(), json!(team_key));
513
+ }
514
+ result
515
+ })
516
+ .collect(),
517
+ )
518
+ }
519
+
400
520
  fn write_settle_details_log(workspace: &Path, collect: &Value, status: &Value) -> Result<PathBuf, CliError> {
401
521
  let logs = workspace.join(".team").join("logs");
402
522
  std::fs::create_dir_all(&logs)?;
@@ -427,7 +547,13 @@ pub fn cmd_repair_state(args: &RepairStateArgs) -> Result<CmdResult, CliError> {
427
547
  args.status
428
548
  )));
429
549
  }
430
- let mut state = crate::state::persist::load_runtime_state(&args.workspace)?;
550
+ let selected = crate::state::selector::resolve_active_team(
551
+ &args.workspace,
552
+ None,
553
+ crate::state::selector::SelectorMode::RequireSpec,
554
+ )
555
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
556
+ let mut state = selected.state;
431
557
  let before = find_task_projection(&state, &args.task_id).unwrap_or_else(repair_task_projection_null);
432
558
  update_task(
433
559
  &mut state,
@@ -437,12 +563,20 @@ pub fn cmd_repair_state(args: &RepairStateArgs) -> Result<CmdResult, CliError> {
437
563
  args.summary.as_deref(),
438
564
  );
439
565
  let after = find_task_projection(&state, &args.task_id).unwrap_or_else(repair_task_projection_null);
440
- crate::state::persist::save_runtime_state(&args.workspace, &state)?;
441
- let spec = load_team_spec_optional(&args.workspace, &state)?
566
+ crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)?;
567
+ let spec_path = selected
568
+ .spec_path
569
+ .as_ref()
442
570
  .ok_or_else(|| CliError::Runtime("team.spec.yaml not found".to_string()))?;
443
- let state_file = crate::lifecycle::restart::write_team_state(&args.workspace, &spec, &state)
571
+ let spec = load_team_spec_at(spec_path)?
572
+ .ok_or_else(|| CliError::Runtime("team.spec.yaml not found".to_string()))?;
573
+ let spec_workspace = selected
574
+ .spec_workspace
575
+ .as_ref()
576
+ .ok_or_else(|| CliError::Runtime("active team spec workspace not found".to_string()))?;
577
+ let state_file = crate::lifecycle::restart::write_team_state(spec_workspace, &spec, &state)
444
578
  .map_err(|e| CliError::Runtime(e.to_string()))?;
445
- crate::event_log::EventLog::new(&args.workspace)
579
+ crate::event_log::EventLog::new(&selected.run_workspace)
446
580
  .write(
447
581
  "repair_state.task",
448
582
  json!({
@@ -466,9 +600,15 @@ pub fn cmd_repair_state(args: &RepairStateArgs) -> Result<CmdResult, CliError> {
466
600
 
467
601
  /// `cmd_diagnose`(`parser.py:298`)。
468
602
  pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
469
- let state = crate::state::persist::load_runtime_state(&args.workspace)?;
470
- let event_log = args.workspace.join(".team").join("logs").join("events.jsonl");
471
- let backend = crate::tmux_backend::TmuxBackend::for_workspace(&args.workspace);
603
+ let selected = crate::state::selector::resolve_active_team(
604
+ &args.workspace,
605
+ None,
606
+ crate::state::selector::SelectorMode::RuntimeOnly,
607
+ )
608
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
609
+ let state = selected.state;
610
+ let event_log = selected.run_workspace.join(".team").join("logs").join("events.jsonl");
611
+ let backend = crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace);
472
612
  let (issues, suggested_repairs) = diagnose_runtime(&state, &backend);
473
613
  let ok = issues.as_array().is_some_and(Vec::is_empty);
474
614
  Ok(CmdResult::from_json(
@@ -479,6 +619,7 @@ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
479
619
  "providers": provider_doctor_checks(),
480
620
  "runtime": {
481
621
  "workspace": args.workspace.to_string_lossy().to_string(),
622
+ "team_key": selected.team_key,
482
623
  "session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
483
624
  "leader_receiver": state.get("leader_receiver").cloned().unwrap_or(Value::Null),
484
625
  "agent_count": state.get("agents").and_then(Value::as_object).map_or(0, serde_json::Map::len),
@@ -559,18 +700,33 @@ pub fn cmd_peek(args: &PeekArgs) -> Result<CmdResult, CliError> {
559
700
  if !windows.iter().any(|w| w.as_str() == window) {
560
701
  return Ok(peek_unavailable(&args.agent, args.json));
561
702
  }
703
+ let range = args
704
+ .head
705
+ .map(|head| crate::transport::CaptureRange::Head(head as u32))
706
+ .unwrap_or_else(|| crate::transport::CaptureRange::Tail(args.tail as u32));
562
707
  let capture = backend
563
708
  .capture(
564
709
  &crate::transport::Target::Pane(crate::transport::PaneId::new(target.clone())),
565
- crate::transport::CaptureRange::Tail(args.tail as u32),
710
+ range,
566
711
  )
567
712
  .map_err(|e| CliError::Runtime(e.to_string()))?;
713
+ let matches = args.search.as_ref().map(|needle| {
714
+ capture
715
+ .text
716
+ .lines()
717
+ .filter(|line| line.contains(needle))
718
+ .map(str::to_string)
719
+ .collect::<Vec<_>>()
720
+ });
568
721
  Ok(CmdResult::from_json(
569
722
  json!({
570
723
  "ok": true,
571
724
  "agent_id": args.agent,
572
725
  "workspace": args.workspace.to_string_lossy().to_string(),
573
726
  "tail": args.tail,
727
+ "head": args.head,
728
+ "search": args.search,
729
+ "matches": matches,
574
730
  "pane_id": target,
575
731
  "text": capture.text,
576
732
  }),
@@ -634,6 +790,12 @@ fn run_fake_e2e(workspace: &Path) -> Result<Value, CliError> {
634
790
  "reason": send.reason,
635
791
  });
636
792
  if send.ok {
793
+ if let Some(message_id) = send.message_id.as_deref() {
794
+ crate::message_store::MessageStore::open(workspace)
795
+ .map_err(crate::messaging::MessagingError::from)?
796
+ .mark(message_id, "delivered", None)
797
+ .map_err(crate::messaging::MessagingError::from)?;
798
+ }
637
799
  let _ = messaging::report_result(
638
800
  workspace,
639
801
  &json!({
@@ -1083,6 +1245,16 @@ fn load_team_spec_optional(workspace: &Path, state: &Value) -> Result<Option<cra
1083
1245
  .map_err(|e| CliError::Runtime(e.to_string()))
1084
1246
  }
1085
1247
 
1248
+ fn load_team_spec_at(spec_path: &Path) -> Result<Option<crate::model::yaml::Value>, CliError> {
1249
+ if !spec_path.exists() {
1250
+ return Ok(None);
1251
+ }
1252
+ let text = std::fs::read_to_string(spec_path)?;
1253
+ crate::model::yaml::loads(&text)
1254
+ .map(Some)
1255
+ .map_err(|e| CliError::Runtime(e.to_string()))
1256
+ }
1257
+
1086
1258
  /// `cmd_approvals`(`commands.py:112`)。
1087
1259
  pub fn cmd_approvals(args: &ApprovalsArgs) -> Result<CmdResult, CliError> {
1088
1260
  let value = status_port::approvals(&args.workspace, args.agent.as_deref(), args.json)?;
@@ -1198,7 +1370,12 @@ pub fn cmd_shutdown(args: &ShutdownArgs) -> Result<CmdResult, CliError> {
1198
1370
  /// `cmd_restart`(`commands.py:344`)。
1199
1371
  pub fn cmd_restart(args: &RestartArgs) -> Result<CmdResult, CliError> {
1200
1372
  Ok(CmdResult::from_json(
1201
- lifecycle_port::restart(&args.workspace, args.allow_fresh, args.team.as_deref())?,
1373
+ lifecycle_port::restart(
1374
+ &args.workspace,
1375
+ args.allow_fresh,
1376
+ args.team.as_deref(),
1377
+ args.session_converge_deadline_ms,
1378
+ )?,
1202
1379
  args.json,
1203
1380
  ))
1204
1381
  }
@@ -1314,7 +1491,7 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
1314
1491
  return Err(CliError::Runtime("--fix requires --gate".to_string()));
1315
1492
  }
1316
1493
  if args.comms || matches!(args.gate, Some(DoctorGate::Comms)) {
1317
- let value = diagnose_port::comms_selftest(&args.workspace, args.team.as_deref(), Some("comms"))?;
1494
+ let value = crate::diagnose::comms::doctor_comms_json(&args.workspace, args.team.as_deref(), Some("comms"))?;
1318
1495
  if !args.json {
1319
1496
  let json_tail = serde_json::to_string_pretty(&sort_json(&value))?;
1320
1497
  return Ok(CmdResult::human(format!("{COMMS_BOUNDARY_TEXT}\n{json_tail}")));
@@ -1322,9 +1499,9 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
1322
1499
  return Ok(CmdResult::from_json(value, true));
1323
1500
  }
1324
1501
  let value = if matches!(args.gate, Some(DoctorGate::Orphans)) {
1325
- diagnose_port::orphan_gate(args.fix, args.confirm)?
1502
+ crate::diagnose::orphans::orphan_gate_json(args.fix, args.confirm)?
1326
1503
  } else if args.cleanup_orphans {
1327
- diagnose_port::cleanup_orphans(args.confirm)?
1504
+ crate::diagnose::orphans::cleanup_orphans_json(args.confirm)?
1328
1505
  } else if args.fix_schema {
1329
1506
  diagnose_port::fix_schema(&args.workspace)?
1330
1507
  } else {
@@ -134,13 +134,18 @@ pub(crate) fn build_preflight_report(team: &std::path::Path) -> Result<Value, Cl
134
134
  || team.join("profiles").exists();
135
135
  let profile_dir_check = json!({
136
136
  "name": "profile_dir",
137
- "ok": profile_dir_exists,
138
- });
139
- let profile_smoke_check = json!({
140
- "name": "profile_smoke",
141
137
  "ok": true,
142
- "status": if profile_dir_exists { "passed" } else { "not_required" },
138
+ "status": if profile_dir_exists { "present" } else { "not_required" },
143
139
  });
140
+ let profile_smoke_check = build_profile_smoke_check_for_team(team)?;
141
+ if profile_smoke_check.get("ok").and_then(Value::as_bool) == Some(false) {
142
+ let reason = profile_smoke_check
143
+ .get("reason")
144
+ .or_else(|| profile_smoke_check.get("error"))
145
+ .and_then(Value::as_str)
146
+ .unwrap_or("profile smoke failed");
147
+ next_actions.push(json!(format!("fix compatible_api profile smoke: {reason}")));
148
+ }
144
149
  checks.push(json!({
145
150
  "name": "profiles",
146
151
  "ok": true,
@@ -197,14 +202,110 @@ pub(crate) fn build_preflight_report(team: &std::path::Path) -> Result<Value, Cl
197
202
  Ok(report)
198
203
  }
199
204
 
205
+ pub(crate) fn build_profile_smoke_check_for_team(team: &std::path::Path) -> Result<Value, CliError> {
206
+ let workspace = crate::model::paths::team_workspace(team)
207
+ .map_err(|error| CliError::Runtime(error.to_string()))?;
208
+ let spec = match crate::compiler::compile_team(team) {
209
+ Ok(spec) => spec,
210
+ Err(error) => {
211
+ return Ok(json!({
212
+ "name": "profile_smoke",
213
+ "ok": false,
214
+ "status": "profile_invalid",
215
+ "reason": error.to_string(),
216
+ "secret_values_printed": false,
217
+ "checks": [],
218
+ }));
219
+ }
220
+ };
221
+ let agents = spec
222
+ .get("agents")
223
+ .and_then(crate::model::yaml::Value::as_list)
224
+ .unwrap_or(&[]);
225
+ let checks = crate::lifecycle::profile_smoke::profile_smoke_checks_for_agents_with_profile_dir(
226
+ &workspace,
227
+ agents,
228
+ Some(&team.join("profiles")),
229
+ crate::lifecycle::profile_smoke::DEFAULT_PROFILE_SMOKE_TIMEOUT,
230
+ );
231
+ Ok(aggregate_profile_smoke_checks(checks))
232
+ }
233
+
234
+ fn aggregate_profile_smoke_checks(checks: Vec<Value>) -> Value {
235
+ let failed = checks
236
+ .iter()
237
+ .filter(|check| check.get("ok").and_then(Value::as_bool) == Some(false))
238
+ .cloned()
239
+ .collect::<Vec<_>>();
240
+ let ok = failed.is_empty();
241
+ let status = if !ok {
242
+ failed
243
+ .first()
244
+ .and_then(|check| check.get("status").and_then(Value::as_str))
245
+ .unwrap_or("smoke_failed")
246
+ } else if checks
247
+ .iter()
248
+ .any(|check| check.get("status").and_then(Value::as_str) == Some("smoke_passed"))
249
+ {
250
+ "smoke_passed"
251
+ } else if checks
252
+ .iter()
253
+ .any(|check| check.get("status").and_then(Value::as_str) == Some("skipped_by_profile"))
254
+ {
255
+ "skipped_by_profile"
256
+ } else {
257
+ "not_required"
258
+ };
259
+ let mut out = json!({
260
+ "name": "profile_smoke",
261
+ "ok": ok,
262
+ "status": status,
263
+ "checks": checks,
264
+ "secret_values_printed": false,
265
+ });
266
+ if let Some(first) = failed.first() {
267
+ copy_optional_field(first, &mut out, "reason");
268
+ copy_optional_field(first, &mut out, "http_status");
269
+ copy_optional_field(first, &mut out, "endpoint");
270
+ copy_optional_field(first, &mut out, "error");
271
+ } else if let Some(first_passed) = checks
272
+ .iter()
273
+ .find(|check| check.get("status").and_then(Value::as_str) == Some("smoke_passed"))
274
+ {
275
+ copy_optional_field(first_passed, &mut out, "http_status");
276
+ copy_optional_field(first_passed, &mut out, "endpoint");
277
+ }
278
+ out
279
+ }
280
+
281
+ fn copy_optional_field(from: &Value, to: &mut Value, key: &str) {
282
+ let Some(value) = from.get(key).cloned() else {
283
+ return;
284
+ };
285
+ let Some(obj) = to.as_object_mut() else {
286
+ return;
287
+ };
288
+ obj.insert(key.to_string(), value);
289
+ }
290
+
200
291
  pub(crate) fn build_wait_ready_report(workspace: &std::path::Path, timeout: f64) -> Result<Value, CliError> {
292
+ let selected = crate::state::selector::resolve_active_team(
293
+ workspace,
294
+ None,
295
+ crate::state::selector::SelectorMode::RuntimeOnly,
296
+ )
297
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
201
298
  let timeout = if timeout.is_finite() && timeout > 0.0 { timeout } else { 0.0 };
202
299
  let deadline = std::time::Instant::now() + std::time::Duration::from_secs_f64(timeout);
203
300
  let mut readiness;
204
301
  loop {
205
- let mut state = crate::state::persist::load_runtime_state(workspace)?;
206
- inject_tmux_session_present(workspace, &mut state);
207
- inject_message_counts(workspace, &mut state)?;
302
+ let mut state = crate::state::projection::select_runtime_state(
303
+ &selected.run_workspace,
304
+ Some(&selected.team_key),
305
+ )
306
+ .unwrap_or_else(|_| selected.state.clone());
307
+ inject_tmux_session_present(&selected.run_workspace, &mut state);
308
+ inject_message_counts(&selected.run_workspace, &mut state)?;
208
309
  readiness = wait_readiness(&state);
209
310
  let awaiting_trust = readiness
210
311
  .get("awaiting_trust_prompt")
@@ -231,6 +332,14 @@ pub(crate) fn build_wait_ready_report(workspace: &std::path::Path, timeout: f64)
231
332
  )
232
333
  } else if ready {
233
334
  (true, "ready", "ready", "workers ready", Vec::new())
335
+ } else if readiness.get("session_capture_complete").and_then(Value::as_bool) == Some(false) {
336
+ (
337
+ false,
338
+ "pending",
339
+ "session_capture_incomplete",
340
+ "provider session capture is incomplete",
341
+ vec![json!("wait for provider session capture before restart")],
342
+ )
234
343
  } else {
235
344
  (
236
345
  false,
@@ -241,7 +350,7 @@ pub(crate) fn build_wait_ready_report(workspace: &std::path::Path, timeout: f64)
241
350
  )
242
351
  };
243
352
  let details_log = write_details_log(
244
- workspace,
353
+ &selected.run_workspace,
245
354
  "wait-ready",
246
355
  &json!({
247
356
  "ok": ok,
@@ -282,6 +391,20 @@ pub(crate) fn wait_readiness(state: &Value) -> Value {
282
391
  let mut mcp_ready = false;
283
392
  let mut task_prompt_delivered = false;
284
393
  let mut awaiting_trust_prompt = false;
394
+ let mut incomplete_sessions = Vec::new();
395
+ let all_attached_receiver = state
396
+ .get("leader_receiver")
397
+ .and_then(Value::as_object)
398
+ .is_none_or(|receiver| {
399
+ receiver
400
+ .get("status")
401
+ .and_then(Value::as_str)
402
+ == Some("attached")
403
+ || receiver
404
+ .get("pane_id")
405
+ .and_then(Value::as_str)
406
+ .is_some_and(|pane| !pane.is_empty() && pane != "__team_agent_unbound__")
407
+ });
285
408
 
286
409
  if let Some(agents) = agents {
287
410
  process_started = state
@@ -314,14 +437,25 @@ pub(crate) fn wait_readiness(state: &Value) -> Value {
314
437
  .and_then(Value::as_str)
315
438
  == Some("awaiting_trust_prompt")
316
439
  });
440
+ incomplete_sessions = crate::session_capture::incomplete_interacted_resumable_agent_ids(state);
317
441
  }
318
- let ready = process_started && cli_prompt_ready && mcp_ready && task_prompt_delivered;
442
+ let all_resumable_have_session = incomplete_sessions.is_empty();
443
+ let session_capture_incomplete = !all_resumable_have_session;
444
+ let all_spawned = process_started && cli_prompt_ready && mcp_ready;
445
+ let ready = all_spawned && all_attached_receiver && all_resumable_have_session;
319
446
  json!({
447
+ "all_attached_receiver": all_attached_receiver,
448
+ "all_resumable_have_session": all_resumable_have_session,
449
+ "all_spawned": all_spawned,
320
450
  "awaiting_trust_prompt": awaiting_trust_prompt,
321
451
  "cli_prompt_ready": cli_prompt_ready,
452
+ "incomplete_session_capture_agents": incomplete_sessions.clone(),
322
453
  "mcp_ready": mcp_ready,
323
454
  "process_started": process_started,
324
455
  "ready": ready,
456
+ "session_capture_complete": all_resumable_have_session,
457
+ "session_capture_incomplete": session_capture_incomplete,
458
+ "pending_session_agent_ids": incomplete_sessions,
325
459
  "task_prompt_delivered": task_prompt_delivered,
326
460
  })
327
461
  }