@team-agent/installer 0.5.91 → 0.5.94

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.
package/Cargo.lock CHANGED
@@ -581,7 +581,7 @@ dependencies = [
581
581
 
582
582
  [[package]]
583
583
  name = "team-agent"
584
- version = "0.5.91"
584
+ version = "0.5.94"
585
585
  dependencies = [
586
586
  "anyhow",
587
587
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.91"
12
+ version = "0.5.94"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -500,7 +500,7 @@ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
500
500
  .join(".team")
501
501
  .join("logs")
502
502
  .join("events.jsonl");
503
- return Ok(CmdResult::from_json(
503
+ return Ok(crate::cli::triage::report(
504
504
  json!({
505
505
  "event_log": event_log.to_string_lossy().to_string(),
506
506
  "issues": issues,
@@ -518,9 +518,10 @@ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
518
518
  "suggested_repairs": suggested_repairs,
519
519
  }),
520
520
  args.json,
521
+ "diagnose",
521
522
  ));
522
523
  }
523
- let selected = crate::state::selector::resolve_active_team(
524
+ let selected = crate::state::selector::resolve_active_team_readonly(
524
525
  &args.workspace,
525
526
  args.team.as_deref(),
526
527
  crate::state::selector::SelectorMode::RuntimeOnly,
@@ -555,7 +556,7 @@ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
555
556
  Some(selected.team_key.as_str()),
556
557
  );
557
558
  let ok = issues.as_array().is_some_and(Vec::is_empty);
558
- Ok(CmdResult::from_json(
559
+ Ok(crate::cli::triage::report(
559
560
  json!({
560
561
  "event_log": event_log.to_string_lossy().to_string(),
561
562
  "issues": issues,
@@ -573,6 +574,7 @@ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
573
574
  "suggested_repairs": suggested_repairs,
574
575
  }),
575
576
  args.json,
577
+ "diagnose",
576
578
  ))
577
579
  }
578
580
 
@@ -1513,15 +1515,21 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
1513
1515
  args.team.as_deref(),
1514
1516
  Some("comms"),
1515
1517
  )?;
1518
+ let result = CmdResult::from_json(value, args.json);
1516
1519
  if !args.json {
1517
- let json_tail = serde_json::to_string_pretty(&sort_json(&value))?;
1518
- return Ok(CmdResult::human(format!(
1519
- "{COMMS_BOUNDARY_TEXT}\n{json_tail}"
1520
- )));
1520
+ let json_tail = match &result.output {
1521
+ CmdOutput::Json(value) => serde_json::to_string_pretty(&sort_json(value))?,
1522
+ _ => String::new(),
1523
+ };
1524
+ return Ok(CmdResult {
1525
+ output: CmdOutput::Human(format!("{COMMS_BOUNDARY_TEXT}\n{json_tail}")),
1526
+ ..result
1527
+ });
1521
1528
  }
1522
- return Ok(CmdResult::from_json(value, true));
1529
+ return Ok(result);
1523
1530
  }
1524
- let value = if matches!(args.gate, Some(DoctorGate::Orphans)) {
1531
+ let default_report = args.gate.is_none() && !args.cleanup_orphans && !args.fix_schema;
1532
+ let mut value = if matches!(args.gate, Some(DoctorGate::Orphans)) {
1525
1533
  crate::diagnose::orphans::orphan_gate_json(&args.workspace, args.fix, args.confirm)?
1526
1534
  } else if args.cleanup_orphans {
1527
1535
  crate::diagnose::orphans::cleanup_orphans_json(&args.workspace, args.confirm)?
@@ -1541,7 +1549,32 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
1541
1549
  );
1542
1550
  value
1543
1551
  };
1544
- Ok(CmdResult::from_json(value, args.json))
1552
+ if default_report {
1553
+ let base_ok = value.get("ok").and_then(Value::as_bool).unwrap_or(false);
1554
+ let has_blocking_issues = value
1555
+ .get("issues")
1556
+ .and_then(Value::as_array)
1557
+ .is_some_and(|issues| {
1558
+ issues.iter().any(|issue| {
1559
+ let id = issue
1560
+ .as_str()
1561
+ .or_else(|| issue.get("id").and_then(Value::as_str));
1562
+ id != Some("leader_not_attached")
1563
+ })
1564
+ });
1565
+ if let Some(object) = value.as_object_mut() {
1566
+ object.insert(
1567
+ "ok".to_string(),
1568
+ Value::Bool(base_ok && !has_blocking_issues),
1569
+ );
1570
+ }
1571
+ }
1572
+ let result = CmdResult::from_json(value, args.json);
1573
+ Ok(if default_report && !args.json {
1574
+ crate::cli::triage::human_result(result, "doctor")
1575
+ } else {
1576
+ result
1577
+ })
1545
1578
  }
1546
1579
 
1547
1580
  #[cfg(test)]
@@ -96,12 +96,6 @@ pub(crate) fn classify_tmux_server_error(error_text: &str) -> &'static str {
96
96
  pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value, Value) {
97
97
  let mut issues = Vec::new();
98
98
  let mut repairs = Vec::new();
99
- for issue in crate::topology::diagnose_topology_issues(state, backend) {
100
- if let Some(id) = crate::topology::issue_id(&issue) {
101
- repairs.push(topology_repair_hint(id));
102
- }
103
- issues.push(issue);
104
- }
105
99
 
106
100
  if state
107
101
  .get("leader_receiver")
@@ -118,41 +112,63 @@ pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value
118
112
  ));
119
113
  }
120
114
 
121
- if let Some(session_name) = state
115
+ // Probe the session before any topology enumeration. A missing session is
116
+ // terminal for all window/pane/socket probes; repeating those tmux calls
117
+ // only pays the dead-socket timeout again on every layer. The persisted
118
+ // endpoint/socket split is state-only, so preserve that fact before the
119
+ // short-circuit without probing either dead endpoint.
120
+ let persisted_socket_conflict = crate::topology::endpoint_socket_conflict(state);
121
+ let session_unavailable = state
122
122
  .get("session_name")
123
123
  .and_then(Value::as_str)
124
124
  .filter(|s| !s.is_empty())
125
- {
126
- match backend.has_session(&crate::transport::SessionName::new(
127
- session_name.to_string(),
128
- )) {
129
- Ok(true) => {}
130
- Ok(false) => {
131
- issues.push(json!("tmux_session_missing"));
132
- repairs.push(recovery_hint(
133
- session_name,
134
- "tmux_session_missing",
135
- "team-agent restart",
136
- ));
137
- }
138
- Err(error) => {
139
- // 0.5.39 Slice 1 (tmux-server-death-locate §11.1 B):
140
- // when the transport error stderr matches "server exited
141
- // unexpectedly", the physical layer that disappeared is
142
- // the tmux server itself — not just this team's session.
143
- // Surface `tmux_server_crashed` so the user's next
144
- // action is a coordinator/host-level recovery, not a
145
- // per-agent `restart <agent>`.
146
- let error_str = error.to_string();
147
- let issue_id = classify_tmux_server_error(&error_str);
148
- issues.push(json!(issue_id));
149
- let mut hint = recovery_hint(session_name, issue_id, "team-agent diagnose");
150
- if let Some(obj) = hint.as_object_mut() {
151
- obj.insert("reason".to_string(), Value::String(error_str));
125
+ .is_some_and(|session_name| {
126
+ match backend.has_session(&crate::transport::SessionName::new(
127
+ session_name.to_string(),
128
+ )) {
129
+ Ok(true) => false,
130
+ Ok(false) => {
131
+ issues.push(json!("tmux_session_missing"));
132
+ repairs.push(recovery_hint(
133
+ session_name,
134
+ "tmux_session_missing",
135
+ "team-agent restart",
136
+ ));
137
+ true
152
138
  }
153
- repairs.push(hint);
139
+ Err(error) => {
140
+ // 0.5.39 Slice 1 (tmux-server-death-locate §11.1 B):
141
+ // when the transport error stderr matches "server exited
142
+ // unexpectedly", the physical layer that disappeared is
143
+ // the tmux server itself — not just this team's session.
144
+ let error_str = error.to_string();
145
+ let issue_id = classify_tmux_server_error(&error_str);
146
+ issues.push(json!(issue_id));
147
+ let mut hint = recovery_hint(session_name, issue_id, "team-agent diagnose");
148
+ if let Some(obj) = hint.as_object_mut() {
149
+ obj.insert("reason".to_string(), Value::String(error_str));
150
+ }
151
+ repairs.push(hint);
152
+ true
153
+ }
154
+ }
155
+ });
156
+
157
+ if session_unavailable {
158
+ if let Some(issue) = persisted_socket_conflict {
159
+ if let Some(id) = crate::topology::issue_id(&issue) {
160
+ repairs.push(topology_repair_hint(id));
154
161
  }
162
+ issues.push(issue);
163
+ }
164
+ return (Value::Array(issues), Value::Array(repairs));
165
+ }
166
+
167
+ for issue in crate::topology::diagnose_topology_issues(state, backend) {
168
+ if let Some(id) = crate::topology::issue_id(&issue) {
169
+ repairs.push(topology_repair_hint(id));
155
170
  }
171
+ issues.push(issue);
156
172
  }
157
173
 
158
174
  if leader_receiver_attached(state) {
@@ -624,6 +640,17 @@ pub(crate) fn append_registry_channel_unbound_to_report(
624
640
  repairs.push(recovery_hint(&team_key, issue_id, repair));
625
641
  object.insert("issues".to_string(), Value::Array(issues));
626
642
  object.insert("suggested_repairs".to_string(), Value::Array(repairs));
643
+ let is_healthy_unattached_host =
644
+ class == crate::lifecycle::launch::LeaderBindingClass::Unbound
645
+ && object.get("ok").and_then(Value::as_bool) == Some(true)
646
+ && object
647
+ .get("profile_smoke")
648
+ .and_then(|profile| profile.get("checks"))
649
+ .and_then(Value::as_array)
650
+ .is_some_and(|checks| !checks.is_empty());
651
+ if !is_healthy_unattached_host {
652
+ object.insert("ok".to_string(), Value::Bool(false));
653
+ }
627
654
  }
628
655
 
629
656
  pub(crate) fn append_selected_live_leader_workspace_mismatch(
@@ -652,7 +679,7 @@ fn selected_live_leader_workspace_mismatch(
652
679
  workspace: &std::path::Path,
653
680
  team: Option<&str>,
654
681
  ) -> Option<(Value, Value)> {
655
- let selected = crate::state::selector::resolve_active_team(
682
+ let selected = crate::state::selector::resolve_active_team_readonly(
656
683
  workspace,
657
684
  team,
658
685
  crate::state::selector::SelectorMode::RuntimeOnly,
@@ -766,8 +793,10 @@ fn append_coordinator_health_issue(
766
793
  issues: &mut Value,
767
794
  repairs: &mut Value,
768
795
  ) {
796
+ // Diagnose must observe coordinator state without initializing or rewriting
797
+ // `.team/runtime/team.db`, including when the file is missing or empty.
769
798
  let workspace = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
770
- let health = crate::coordinator::coordinator_health(&workspace);
799
+ let health = crate::coordinator::coordinator_health_read_only(&workspace);
771
800
  let Some(id) = coordinator_issue_id(state, &health) else {
772
801
  return;
773
802
  };
@@ -1910,14 +1939,24 @@ pub(crate) fn provider_doctor_checks() -> Value {
1910
1939
  ] {
1911
1940
  let adapter = crate::provider::get_adapter(provider);
1912
1941
  let name = provider_wire(provider);
1913
- let version = adapter.version().unwrap_or_else(|error| error.to_string());
1942
+ let fake = matches!(provider, crate::provider::Provider::Fake);
1943
+ let (auth, version) = if fake {
1944
+ (crate::provider::AuthHintStatus::Present, "fake".to_string())
1945
+ } else {
1946
+ (
1947
+ crate::provider::AuthHintStatus::Unknown,
1948
+ "unknown".to_string(),
1949
+ )
1950
+ };
1914
1951
  providers.insert(
1915
1952
  name.to_string(),
1916
1953
  json!({
1917
- "auth": adapter.auth_hint(crate::provider::AuthMode::Subscription),
1954
+ "auth": auth,
1918
1955
  "command": provider_command(provider),
1919
1956
  "installed": adapter.is_installed(),
1920
1957
  "version": version,
1958
+ "probe_status": "not_run",
1959
+ "provider_probe_status": "not_run",
1921
1960
  }),
1922
1961
  );
1923
1962
  }