@team-agent/installer 0.3.13 → 0.3.15

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 (38) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -0
  4. package/crates/team-agent/src/cli/emit.rs +92 -11
  5. package/crates/team-agent/src/cli/mod.rs +228 -85
  6. package/crates/team-agent/src/cli/send.rs +252 -0
  7. package/crates/team-agent/src/cli/tests/run_delegation.rs +15 -3
  8. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +159 -2
  9. package/crates/team-agent/src/cli/types.rs +30 -1
  10. package/crates/team-agent/src/compiler/tests.rs +2 -2
  11. package/crates/team-agent/src/compiler.rs +1 -1
  12. package/crates/team-agent/src/fake_worker.rs +32 -145
  13. package/crates/team-agent/src/leader/start.rs +279 -4
  14. package/crates/team-agent/src/lifecycle/display.rs +3 -3
  15. package/crates/team-agent/src/lifecycle/launch.rs +692 -92
  16. package/crates/team-agent/src/lifecycle/restart/agent.rs +137 -17
  17. package/crates/team-agent/src/lifecycle/restart/common.rs +88 -42
  18. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +197 -33
  19. package/crates/team-agent/src/lifecycle/restart/selection.rs +9 -6
  20. package/crates/team-agent/src/lifecycle/tests/core.rs +195 -50
  21. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +934 -72
  22. package/crates/team-agent/src/lifecycle/types.rs +5 -0
  23. package/crates/team-agent/src/lifecycle/worker_command_context.rs +8 -0
  24. package/crates/team-agent/src/mcp_server/wire.rs +153 -3
  25. package/crates/team-agent/src/messaging/leader_receiver.rs +276 -43
  26. package/crates/team-agent/src/messaging/mod.rs +6 -3
  27. package/crates/team-agent/src/messaging/results.rs +240 -158
  28. package/crates/team-agent/src/messaging/send.rs +3 -2
  29. package/crates/team-agent/src/messaging/tests/e23.rs +35 -0
  30. package/crates/team-agent/src/messaging/tests/mod.rs +6 -2
  31. package/crates/team-agent/src/messaging/tests/runtime.rs +9 -9
  32. package/crates/team-agent/src/os_probe.rs +11 -0
  33. package/crates/team-agent/src/state/persist.rs +6 -0
  34. package/crates/team-agent/src/tmux_backend.rs +90 -0
  35. package/crates/team-agent/src/transport/test_support.rs +46 -1
  36. package/crates/team-agent/src/transport.rs +31 -0
  37. package/package.json +4 -4
  38. package/skills/team-agent/references/recovery-runbook.md +277 -0
@@ -115,9 +115,16 @@ pub mod lifecycle_port {
115
115
  team_id: Option<&str>,
116
116
  yes: bool,
117
117
  fresh: bool,
118
+ open_display: bool,
118
119
  ) -> Result<Value, CliError> {
119
- match crate::lifecycle::quick_start_in_workspace(
120
- workspace, agents_dir, name, yes, fresh, team_id,
120
+ match crate::lifecycle::quick_start_in_workspace_with_display(
121
+ workspace,
122
+ agents_dir,
123
+ name,
124
+ yes,
125
+ fresh,
126
+ team_id,
127
+ open_display,
121
128
  ) {
122
129
  Ok(report) => Ok(quick_start_value(report)),
123
130
  Err(e) => Ok(error_value(e)),
@@ -168,48 +175,12 @@ pub mod lifecycle_port {
168
175
  let run_ws = crate::model::paths::canonical_run_workspace(workspace)
169
176
  .map_err(|e| CliError::Runtime(e.to_string()))?;
170
177
  let state = shutdown_state_for_team(&run_ws, team)?;
171
- let state_for_kill = state.clone();
172
178
  let transport = if let Some(endpoint) = legacy_worker_tmux_endpoint(&state) {
173
- crate::tmux_backend::TmuxBackend::for_tmux_endpoint(endpoint)
179
+ shutdown_transport_for_endpoint(endpoint)
174
180
  } else {
175
181
  shutdown_workspace_transport(&run_ws)
176
182
  };
177
- let result =
178
- shutdown_with_transport_and_state(workspace, keep_logs, team, &transport, Some(state));
179
- if team.is_none() {
180
- // E12 (P0): the leader terminal lives on this socket by design. A bare shutdown must
181
- // NOT `kill-server` it away. spare = state-anchor sessions ∪ `team-agent-leader-*`
182
- // prefix sessions (union; cr E12 ①). kill_server only when the socket is exclusively
183
- // ours (no spare + no foreign session); shared socket → kill our sessions individually
184
- // (cr E12 ②). All spare derivation comes from ONE snapshot (list_targets + the state
185
- // already loaded) — no independent ps/tmux re-derivation (N39).
186
- let transport_dyn: &dyn crate::transport::Transport = &transport;
187
- let pane_targets = transport_dyn.list_targets().unwrap_or_default();
188
- let sessions = socket_session_names(transport_dyn);
189
- let event_log = crate::event_log::EventLog::new(&run_ws);
190
- let anchor_sessions =
191
- anchor_sessions_from_state(&state_for_kill, &pane_targets, &event_log);
192
- let decision = sessions_to_kill(&sessions, &anchor_sessions);
193
- match decision {
194
- KillDecision::KillServerExclusive => transport.kill_server(),
195
- KillDecision::KillIndividually { to_kill, spared } => {
196
- if !spared.is_empty() || to_kill.len() != sessions.len() {
197
- // shared socket / leader spared → never whole-server teardown.
198
- let _ = event_log.write(
199
- "shutdown.kill_server_skipped_shared_socket",
200
- json!({
201
- "spared_sessions": spared.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
202
- "killed_sessions": to_kill.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
203
- }),
204
- );
205
- }
206
- for session in &to_kill {
207
- let _ = transport_dyn.kill_session(session);
208
- }
209
- }
210
- }
211
- }
212
- result
183
+ shutdown_with_transport_and_state(workspace, keep_logs, team, &transport, Some(state))
213
184
  }
214
185
 
215
186
  /// E12 ①:从 state 锚 pane_id(leader_receiver/team_owner,top+teams)映射到其所在 session
@@ -235,15 +206,13 @@ pub mod lifecycle_port {
235
206
  .collect()
236
207
  }
237
208
 
238
- fn socket_session_names(
239
- transport: &dyn crate::transport::Transport,
209
+ fn socket_session_names_from_targets(
210
+ pane_targets: &[crate::transport::PaneInfo],
240
211
  ) -> Vec<crate::transport::SessionName> {
241
212
  let mut seen = std::collections::BTreeSet::new();
242
- transport
243
- .list_targets()
244
- .unwrap_or_default()
245
- .into_iter()
246
- .map(|pane| pane.session)
213
+ pane_targets
214
+ .iter()
215
+ .map(|pane| pane.session.clone())
247
216
  .filter(|session| seen.insert(session.as_str().to_string()))
248
217
  .collect()
249
218
  }
@@ -281,6 +250,103 @@ pub mod lifecycle_port {
281
250
  }
282
251
  }
283
252
 
253
+ #[derive(Debug, Default)]
254
+ struct ShutdownSocketCleanup {
255
+ killed_sessions: Vec<crate::transport::SessionName>,
256
+ spared_sessions: Vec<crate::transport::SessionName>,
257
+ error: Option<String>,
258
+ }
259
+
260
+ fn push_unique_session(
261
+ sessions: &mut Vec<crate::transport::SessionName>,
262
+ session: crate::transport::SessionName,
263
+ ) {
264
+ if !sessions
265
+ .iter()
266
+ .any(|existing| existing.as_str() == session.as_str())
267
+ {
268
+ sessions.push(session);
269
+ }
270
+ }
271
+
272
+ fn bare_shutdown_socket_cleanup(
273
+ transport: &dyn crate::transport::Transport,
274
+ state: &Value,
275
+ event_log: &crate::event_log::EventLog,
276
+ ) -> ShutdownSocketCleanup {
277
+ // E12 (P0): the leader terminal lives on this socket by design. A bare shutdown must
278
+ // NOT `kill-server` it away. spare = state-anchor sessions ∪ `team-agent-leader-*`
279
+ // prefix sessions (union; cr E12 ①). kill_server only when the socket is exclusively
280
+ // ours (no spare + no foreign session); shared socket → kill our sessions individually
281
+ // (cr E12 ②). All spare derivation comes from ONE snapshot (list_targets + state) —
282
+ // no independent ps/tmux re-derivation (N39).
283
+ let pane_targets = transport.list_targets().unwrap_or_default();
284
+ let sessions = socket_session_names_from_targets(&pane_targets);
285
+ let anchor_sessions = anchor_sessions_from_state(state, &pane_targets, event_log);
286
+ match sessions_to_kill(&sessions, &anchor_sessions) {
287
+ KillDecision::KillServerExclusive => {
288
+ if state
289
+ .get("tmux_socket_source")
290
+ .and_then(Value::as_str)
291
+ == Some("leader_env")
292
+ {
293
+ let _ = event_log.write(
294
+ "shutdown.kill_server_skipped_shared_socket",
295
+ json!({
296
+ "reason": "leader_env_tmux_socket",
297
+ "spared_sessions": [],
298
+ "killed_sessions": sessions.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
299
+ }),
300
+ );
301
+ let mut error = None;
302
+ for session in &sessions {
303
+ if let Err(err) = transport.kill_session(session) {
304
+ if !tmux_absent_error(&err.to_string()) {
305
+ error.get_or_insert_with(|| err.to_string());
306
+ }
307
+ }
308
+ }
309
+ return ShutdownSocketCleanup {
310
+ killed_sessions: sessions,
311
+ spared_sessions: Vec::new(),
312
+ error,
313
+ };
314
+ }
315
+ let error = transport.kill_server().err().map(|error| error.to_string());
316
+ ShutdownSocketCleanup {
317
+ killed_sessions: sessions,
318
+ spared_sessions: Vec::new(),
319
+ error,
320
+ }
321
+ }
322
+ KillDecision::KillIndividually { to_kill, spared } => {
323
+ if !spared.is_empty() || to_kill.len() != sessions.len() {
324
+ // shared socket / leader spared → never whole-server teardown.
325
+ let _ = event_log.write(
326
+ "shutdown.kill_server_skipped_shared_socket",
327
+ json!({
328
+ "spared_sessions": spared.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
329
+ "killed_sessions": to_kill.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
330
+ }),
331
+ );
332
+ }
333
+ let mut error = None;
334
+ for session in &to_kill {
335
+ if let Err(err) = transport.kill_session(session) {
336
+ if !tmux_absent_error(&err.to_string()) {
337
+ error.get_or_insert_with(|| err.to_string());
338
+ }
339
+ }
340
+ }
341
+ ShutdownSocketCleanup {
342
+ killed_sessions: to_kill,
343
+ spared_sessions: spared,
344
+ error,
345
+ }
346
+ }
347
+ }
348
+ }
349
+
284
350
  pub fn shutdown_with_transport(
285
351
  workspace: &Path,
286
352
  keep_logs: bool,
@@ -358,8 +424,11 @@ pub mod lifecycle_port {
358
424
  reap_process_tree(&root_pids, &protected, &entry_table);
359
425
  reap_process_groups(&root_pgids, &protected);
360
426
  let mut kill_error: Option<String> = None;
427
+ let mut killed_sessions = Vec::new();
428
+ let mut spared_sessions = Vec::new();
361
429
  deadline.check("kill_session")?;
362
430
  if let Some(session) = session_name.as_ref() {
431
+ push_unique_session(&mut killed_sessions, session.clone());
363
432
  if let Err(error) = transport.kill_session(session) {
364
433
  if !tmux_absent_error(&error.to_string()) {
365
434
  kill_error = Some(error.to_string());
@@ -376,21 +445,28 @@ pub mod lifecycle_port {
376
445
  reap_scope,
377
446
  &mut probe_degraded,
378
447
  );
379
- deadline.check("session_residuals")?;
380
- let session_residuals = if let Some(session) = session_name.as_ref() {
381
- let (residuals, error) = session_residuals_after_reap(
382
- transport,
383
- &run_workspace,
384
- session,
385
- !captured_missing_sessions,
386
- );
387
- if let Some(error) = error {
448
+ if team.is_none() {
449
+ deadline.check("shared_socket_cleanup")?;
450
+ let event_log = crate::event_log::EventLog::new(&run_workspace);
451
+ let cleanup = bare_shutdown_socket_cleanup(transport, &state, &event_log);
452
+ for session in cleanup.killed_sessions {
453
+ push_unique_session(&mut killed_sessions, session);
454
+ }
455
+ spared_sessions = cleanup.spared_sessions;
456
+ if let Some(error) = cleanup.error {
388
457
  kill_error.get_or_insert(error);
389
458
  }
390
- residuals
391
- } else {
392
- Vec::new()
393
- };
459
+ }
460
+ deadline.check("session_residuals")?;
461
+ let (session_residuals, session_residual_error) = session_residuals_after_reap_many(
462
+ transport,
463
+ &run_workspace,
464
+ &killed_sessions,
465
+ !captured_missing_sessions,
466
+ );
467
+ if let Some(error) = session_residual_error {
468
+ kill_error.get_or_insert(error);
469
+ }
394
470
  deadline.check("process_residuals")?;
395
471
  // C-①: the post-verify gets ONE fresh verification snapshot (reaps changed
396
472
  // the world; #248 post-verify facts must be current, not the entry view).
@@ -424,10 +500,18 @@ pub mod lifecycle_port {
424
500
  None
425
501
  };
426
502
  let probe_timeout = crate::os_probe::probe_timeout();
427
- // swallow batch 1: a failed ps probe degrades verification truthfully — the
428
- // empty table must never read as a clean "no residual processes".
429
- let verification_degraded = probe_timeout.is_some() || probe_degraded;
430
- let session_killed = session_name.is_some()
503
+ let probe_timeout_kind = probe_timeout.as_ref().map(|timeout| timeout.probe);
504
+ // swallow batch 1: a failed ps probe degrades cleanup truthfully — the
505
+ // empty table must never read as a clean "no residual processes". A slow
506
+ // per-process cwd probe is diagnostic only once session/process residuals
507
+ // are otherwise clean.
508
+ let cleanup_truth_degraded = probe_degraded || probe_timeout_kind == Some("ps_table");
509
+ let diagnostic_probe_degraded = probe_timeout_kind == Some("lsof_cwd");
510
+ let other_probe_timeout_degraded =
511
+ probe_timeout.is_some() && !cleanup_truth_degraded && !diagnostic_probe_degraded;
512
+ let verification_degraded =
513
+ cleanup_truth_degraded || diagnostic_probe_degraded || other_probe_timeout_degraded;
514
+ let session_killed = !killed_sessions.is_empty()
431
515
  && kill_error.is_none()
432
516
  && session_residuals.is_empty()
433
517
  && process_residuals.is_empty();
@@ -458,13 +542,13 @@ pub mod lifecycle_port {
458
542
  && kill_error.is_none()
459
543
  && session_residuals.is_empty()
460
544
  && process_residuals.is_empty()
461
- && !verification_degraded
545
+ && !cleanup_truth_degraded
462
546
  && !coordinator_timeout;
463
547
  let status = if ok {
464
548
  "ok"
465
549
  } else if coordinator_timeout {
466
550
  "timeout"
467
- } else if verification_degraded {
551
+ } else if cleanup_truth_degraded {
468
552
  "partial"
469
553
  } else if kill_error.is_some() {
470
554
  "failed"
@@ -473,12 +557,11 @@ pub mod lifecycle_port {
473
557
  };
474
558
  let phase = if coordinator_timeout {
475
559
  Some("stop_coordinator")
476
- } else if verification_degraded {
560
+ } else if cleanup_truth_degraded {
477
561
  Some("os_probe")
478
562
  } else {
479
563
  None
480
564
  };
481
- let probe_timeout_kind = probe_timeout.as_ref().map(|timeout| timeout.probe);
482
565
  let probe_timeout_value = probe_timeout.as_ref().map(|timeout| {
483
566
  json!({
484
567
  "probe": timeout.probe,
@@ -494,6 +577,8 @@ pub mod lifecycle_port {
494
577
  "team": team,
495
578
  "session_name": session_name.as_ref().map(|s| s.as_str().to_string()),
496
579
  "session_killed": session_killed,
580
+ "killed_sessions": killed_sessions.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
581
+ "spared_sessions": spared_sessions.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
497
582
  "coordinator_status": coordinator_status,
498
583
  "status": status,
499
584
  "phase": phase,
@@ -515,6 +600,8 @@ pub mod lifecycle_port {
515
600
  "team": team,
516
601
  "session_name": session_name.map(|s| s.as_str().to_string()),
517
602
  "session_killed": session_killed,
603
+ "killed_sessions": killed_sessions.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
604
+ "spared_sessions": spared_sessions.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
518
605
  "residuals": {
519
606
  "sessions": session_residuals,
520
607
  "processes": process_residuals,
@@ -606,10 +693,19 @@ pub mod lifecycle_port {
606
693
  crate::tmux_backend::TmuxBackend::for_workspace(workspace)
607
694
  }
608
695
 
696
+ fn shutdown_transport_for_endpoint(endpoint: &str) -> crate::tmux_backend::TmuxBackend {
697
+ if Path::new(endpoint).is_absolute() {
698
+ crate::tmux_backend::TmuxBackend::for_tmux_endpoint(endpoint)
699
+ } else {
700
+ crate::tmux_backend::TmuxBackend::for_socket_name(endpoint)
701
+ }
702
+ }
703
+
609
704
  fn legacy_worker_tmux_endpoint(state: &Value) -> Option<&str> {
610
705
  state
611
706
  .get("tmux_endpoint")
612
707
  .and_then(Value::as_str)
708
+ .or_else(|| state.get("tmux_socket").and_then(Value::as_str))
613
709
  .filter(|endpoint| !endpoint.is_empty())
614
710
  }
615
711
 
@@ -673,6 +769,33 @@ pub mod lifecycle_port {
673
769
  (sessions, error)
674
770
  }
675
771
 
772
+ fn session_residuals_after_reap_many(
773
+ transport: &dyn crate::transport::Transport,
774
+ workspace: &Path,
775
+ sessions: &[crate::transport::SessionName],
776
+ check_primary_transport: bool,
777
+ ) -> (Vec<String>, Option<String>) {
778
+ let mut residuals = Vec::new();
779
+ let mut error = None;
780
+ for session in sessions {
781
+ let (session_residuals, session_error) = session_residuals_after_reap(
782
+ transport,
783
+ workspace,
784
+ session,
785
+ check_primary_transport,
786
+ );
787
+ for residual in session_residuals {
788
+ if !residuals.iter().any(|seen| seen == &residual) {
789
+ residuals.push(residual);
790
+ }
791
+ }
792
+ if let Some(session_error) = session_error {
793
+ error.get_or_insert(session_error);
794
+ }
795
+ }
796
+ (residuals, error)
797
+ }
798
+
676
799
  #[derive(Clone, Copy, Debug, Eq, PartialEq)]
677
800
  enum ShutdownReapScope {
678
801
  Workspace,
@@ -721,11 +844,7 @@ pub mod lifecycle_port {
721
844
  /// pids; Gap 37 escalation order TERM -> grace -> KILL preserved), then a single
722
845
  /// bounded wait for the whole union. kill/wait sets derive from the SAME snapshot
723
846
  /// as the protected set (N39).
724
- fn reap_process_tree(
725
- root_pids: &[u32],
726
- protected: &ShutdownProtection,
727
- table: &[ProcessInfo],
728
- ) {
847
+ fn reap_process_tree(root_pids: &[u32], protected: &ShutdownProtection, table: &[ProcessInfo]) {
729
848
  let mut pids = Vec::new();
730
849
  let mut seen = std::collections::BTreeSet::new();
731
850
  for root in root_pids {
@@ -787,12 +906,21 @@ pub mod lifecycle_port {
787
906
  let mut protected = shutdown_protection_set(&round_table);
788
907
  extend_protection_with_leader_panes(&mut protected, transport, state, &round_table);
789
908
  let residuals = matched_processes(
790
- workspace, state, root_pids, root_pgids, &protected, scope, &round_table,
909
+ workspace,
910
+ state,
911
+ root_pids,
912
+ root_pgids,
913
+ &protected,
914
+ scope,
915
+ &round_table,
791
916
  );
792
917
  if residuals.is_empty() {
793
918
  return;
794
919
  }
795
- let residual_pids = residuals.iter().map(|process| process.pid).collect::<Vec<_>>();
920
+ let residual_pids = residuals
921
+ .iter()
922
+ .map(|process| process.pid)
923
+ .collect::<Vec<_>>();
796
924
  reap_process_tree(&residual_pids, &protected, &round_table);
797
925
  let pgids = residuals
798
926
  .iter()
@@ -906,7 +1034,9 @@ pub mod lifecycle_port {
906
1034
  /// - E4b team-in-team:子 team state 的 team_owner.pane_id 指父 team worker pane;
907
1035
  /// 父 team state 的 teams.<child>.team_owner.pane_id 同义(若有该字段)
908
1036
  /// → 任一 team 的 shutdown 都不杀任何 team 的 leader 锚 pane
909
- pub fn collect_state_leader_anchor_pane_ids(state: &Value) -> std::collections::BTreeSet<String> {
1037
+ pub fn collect_state_leader_anchor_pane_ids(
1038
+ state: &Value,
1039
+ ) -> std::collections::BTreeSet<String> {
910
1040
  let mut out = std::collections::BTreeSet::new();
911
1041
  push_anchor_pane_id(state, &mut out);
912
1042
  if let Some(teams) = state.get("teams").and_then(Value::as_object) {
@@ -1042,7 +1172,10 @@ pub mod lifecycle_port {
1042
1172
  for socket_endpoint in collect_state_recorded_tmux_sockets(state) {
1043
1173
  let cross_backend =
1044
1174
  crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&socket_endpoint);
1045
- let cross_panes = <crate::tmux_backend::TmuxBackend as crate::transport::Transport>::list_targets(&cross_backend)
1175
+ let cross_panes =
1176
+ <crate::tmux_backend::TmuxBackend as crate::transport::Transport>::list_targets(
1177
+ &cross_backend,
1178
+ )
1046
1179
  .unwrap_or_default();
1047
1180
  leader_pane_pids.extend(
1048
1181
  cross_panes
@@ -1167,8 +1300,9 @@ pub mod lifecycle_port {
1167
1300
  scope: ShutdownReapScope,
1168
1301
  table: &[ProcessInfo],
1169
1302
  ) -> Vec<Value> {
1170
- let mut residuals =
1171
- matched_processes(workspace, state, root_pids, root_pgids, protected, scope, table);
1303
+ let mut residuals = matched_processes(
1304
+ workspace, state, root_pids, root_pgids, protected, scope, table,
1305
+ );
1172
1306
  let mut seen = residuals
1173
1307
  .iter()
1174
1308
  .map(|process| process.pid)
@@ -1891,7 +2025,7 @@ pub mod lifecycle_port {
1891
2025
  state_path: Some(PathBuf::from("/tmp/state.json")),
1892
2026
  next_actions: vec!["restart".to_string()],
1893
2027
  attach_commands: vec![
1894
- "tmux -S /tmp/tmux-501/ta-test attach -t team-teamA:worker".to_string(),
2028
+ "tmux -S /tmp/tmux-501/ta-test attach -t team-teamA:worker".to_string()
1895
2029
  ],
1896
2030
  });
1897
2031
  assert_eq!(
@@ -2157,7 +2291,10 @@ pub mod lifecycle_port {
2157
2291
  let msg = "agent start requirement unmet: agent foo not found";
2158
2292
  let na = error_next_action(msg).expect("not-found must carry next_action");
2159
2293
  assert!(na.contains("add-agent"), "must steer to add-agent: {na}");
2160
- assert!(na.contains("--role-file"), "must show the role-file flag: {na}");
2294
+ assert!(
2295
+ na.contains("--role-file"),
2296
+ "must show the role-file flag: {na}"
2297
+ );
2161
2298
  }
2162
2299
 
2163
2300
  #[test]
@@ -2176,7 +2313,10 @@ pub mod lifecycle_port {
2176
2313
 
2177
2314
  #[test]
2178
2315
  fn unrelated_error_has_no_next_action() {
2179
- assert_eq!(error_next_action("state persistence failed: disk full"), None);
2316
+ assert_eq!(
2317
+ error_next_action("state persistence failed: disk full"),
2318
+ None
2319
+ );
2180
2320
  }
2181
2321
 
2182
2322
  #[test]
@@ -2187,7 +2327,10 @@ pub mod lifecycle_port {
2187
2327
  let v = error_value(err);
2188
2328
  assert_eq!(v["ok"], serde_json::json!(false));
2189
2329
  assert!(
2190
- v["next_action"].as_str().unwrap_or("").contains("add-agent"),
2330
+ v["next_action"]
2331
+ .as_str()
2332
+ .unwrap_or("")
2333
+ .contains("add-agent"),
2191
2334
  "error_value must attach the add-agent guidance: {v}"
2192
2335
  );
2193
2336
  }
@@ -2242,8 +2385,8 @@ pub mod diagnose_port {
2242
2385
  .unwrap_or(true);
2243
2386
  // legacy 降级面(legacy_team_invalid)不下拉整体 ok —— 用户没显式让我们
2244
2387
  // 体检这个 team,失败是降级诊断信息,不是 install 自检失败。
2245
- let legacy_only_failure =
2246
- !profile_smoke_ok && profile_smoke_value.get("status").and_then(Value::as_str)
2388
+ let legacy_only_failure = !profile_smoke_ok
2389
+ && profile_smoke_value.get("status").and_then(Value::as_str)
2247
2390
  == Some("legacy_team_invalid");
2248
2391
  let effective_smoke_ok = profile_smoke_ok || legacy_only_failure;
2249
2392
  let ok = workspace_valid && (team_context || workspace_has_entries) && effective_smoke_ok;