@team-agent/installer 0.5.112 → 0.5.114

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 (77) hide show
  1. package/Cargo.lock +252 -84
  2. package/Cargo.toml +3 -1
  3. package/crates/team-agent/Cargo.toml +3 -0
  4. package/crates/team-agent/src/cli/adapters.rs +119 -50
  5. package/crates/team-agent/src/cli/diagnose.rs +36 -13
  6. package/crates/team-agent/src/cli/emit.rs +173 -109
  7. package/crates/team-agent/src/cli/leader.rs +16 -0
  8. package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
  9. package/crates/team-agent/src/cli/leaders.rs +32 -32
  10. package/crates/team-agent/src/cli/mod.rs +158 -14
  11. package/crates/team-agent/src/cli/models.rs +68 -18
  12. package/crates/team-agent/src/cli/named_address.rs +17 -16
  13. package/crates/team-agent/src/cli/route.rs +4 -4
  14. package/crates/team-agent/src/cli/send/presentation.rs +19 -18
  15. package/crates/team-agent/src/cli/send/resolve.rs +11 -11
  16. package/crates/team-agent/src/cli/send.rs +4 -4
  17. package/crates/team-agent/src/cli/spec.rs +35 -29
  18. package/crates/team-agent/src/cli/status.rs +7 -7
  19. package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
  20. package/crates/team-agent/src/cli/tests/base.rs +94 -6
  21. package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
  22. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
  23. package/crates/team-agent/src/cli/tests/named_address.rs +3 -3
  24. package/crates/team-agent/src/cli/tests/status_send.rs +9 -7
  25. package/crates/team-agent/src/cli/triage.rs +53 -2
  26. package/crates/team-agent/src/cli/types.rs +8 -8
  27. package/crates/team-agent/src/compiler/tests.rs +12 -0
  28. package/crates/team-agent/src/compiler.rs +65 -26
  29. package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
  30. package/crates/team-agent/src/contract_runtime/config.rs +309 -0
  31. package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
  32. package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
  33. package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
  34. package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
  35. package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
  36. package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
  37. package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
  38. package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
  39. package/crates/team-agent/src/coordinator/tick.rs +26 -8
  40. package/crates/team-agent/src/leader/mod.rs +1 -0
  41. package/crates/team-agent/src/leader/prompt.rs +906 -0
  42. package/crates/team-agent/src/leader/start.rs +265 -16
  43. package/crates/team-agent/src/leader/tests/identity.rs +21 -0
  44. package/crates/team-agent/src/leader/tests/mod.rs +2 -0
  45. package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
  46. package/crates/team-agent/src/leader/types.rs +6 -0
  47. package/crates/team-agent/src/lib.rs +4 -0
  48. package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
  49. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +760 -365
  50. package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
  51. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
  52. package/crates/team-agent/src/lifecycle/launch.rs +39 -28
  53. package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
  54. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
  55. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  56. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -2
  57. package/crates/team-agent/src/lifecycle/types.rs +4 -1
  58. package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
  59. package/crates/team-agent/src/main.rs +6 -0
  60. package/crates/team-agent/src/messaging/delivery.rs +50 -1
  61. package/crates/team-agent/src/messaging/send.rs +10 -0
  62. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
  63. package/crates/team-agent/src/model/spec.rs +24 -12
  64. package/crates/team-agent/src/provider/adapter.rs +8 -59
  65. package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
  66. package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
  67. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
  68. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
  69. package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
  70. package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
  71. package/crates/team-agent/src/provider/command_helpers.rs +45 -0
  72. package/crates/team-agent/src/provider/mod.rs +1 -0
  73. package/crates/team-agent/src/provider/model_catalog.rs +21 -28
  74. package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
  75. package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
  76. package/crates/team-agent/src/state/persist.rs +73 -0
  77. package/package.json +4 -4
@@ -28,6 +28,9 @@ chrono.workspace = true
28
28
  libc.workspace = true
29
29
  rusqlite.workspace = true # step 3 db(bundled SQLite,静态链接)
30
30
 
31
+ [target.'cfg(unix)'.dependencies]
32
+ team-agent-contract = { path = "../team-agent-contract" }
33
+
31
34
  # 0.5.x Windows portability Batch 2/3: uses the same `windows = "0.61"`
32
35
  # crate as `win-conpty-phase0` so the workspace shares a single
33
36
  # Windows-API crate + already-locked Cargo.lock version.
@@ -7,6 +7,9 @@ use super::*;
7
7
 
8
8
  /// `cmd_quick_start`(`commands.py:18`)。`--json` 或 `!ok` → 整 dict;否则 `result["summary"]`。
9
9
  pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
10
+ #[cfg(unix)]
11
+ crate::contract_runtime::config::read_team(args)
12
+ .map_err(|error| CliError::Runtime(error.to_string()))?;
10
13
  let mut value = lifecycle_port::quick_start(
11
14
  &args.workspace,
12
15
  &args.agents_dir,
@@ -19,6 +22,10 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
19
22
  return Ok(result);
20
23
  }
21
24
  append_send_guidance(&mut value, &args.workspace, args.team_id.as_deref());
25
+ finish_quick_start(value, args)
26
+ }
27
+
28
+ fn finish_quick_start(mut value: Value, args: &QuickStartArgs) -> Result<CmdResult, CliError> {
22
29
  let readiness = value.get("readiness").and_then(Value::as_object);
23
30
  let all_resumable_have_session = readiness
24
31
  .and_then(|readiness| readiness.get("all_resumable_have_session"))
@@ -36,7 +43,7 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
36
43
  .get("status")
37
44
  .and_then(Value::as_str)
38
45
  .map(str::to_string);
39
- if !args.detail {
46
+ if !args.detail && value.get("ok").and_then(Value::as_bool) != Some(false) {
40
47
  lifecycle_port::compact_quick_start_value(&mut value);
41
48
  }
42
49
  if args.json
@@ -45,30 +52,18 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
45
52
  || !readiness_ready
46
53
  {
47
54
  let mut result = CmdResult::from_json(value, args.json);
48
- if args.json && status.as_deref() == Some("pending_tool_load") {
55
+ if status.as_deref() == Some("pending_tool_load") {
49
56
  result.exit = ExitCode::Ok;
50
57
  }
51
58
  if !args.json {
52
59
  if let CmdOutput::Json(value) = &result.output {
53
- let explanation = if value.get("ok").and_then(Value::as_bool) == Some(false) {
54
- format!(
55
- "启动未完成:{}",
56
- value
57
- .get("error")
58
- .and_then(Value::as_str)
59
- .unwrap_or("队伍尚未准备好接收任务")
60
- )
61
- } else {
62
- "队伍尚未准备好接收任务;请等待工具完成启动。".to_string()
63
- };
64
- let mut doctor = format!(
65
- "team-agent doctor --workspace {}",
66
- shell_quote(&args.workspace.to_string_lossy())
67
- );
68
- if let Some(team) = args.team_id.as_deref() {
69
- doctor.push_str(&format!(" --team {}", shell_quote(team)));
70
- }
71
- result.output = CmdOutput::Human(format!("{explanation}\n下一步:运行 {doctor},按体检提示处理;用 status 查看状态,不反复起队或重发任务。"));
60
+ // Typed refusals often have no `error`. Preserve the complete
61
+ // report instead of replacing its reason/actions with a guess.
62
+ let safe = crate::redaction::redact_external_value(value);
63
+ result.output = CmdOutput::Human(format!(
64
+ "quick-start report:\n{}",
65
+ serde_json::to_string_pretty(&safe)?
66
+ ));
72
67
  }
73
68
  }
74
69
  Ok(result)
@@ -126,11 +121,11 @@ fn quick_start_config_guidance(value: &mut Value, args: &QuickStartArgs) -> Opti
126
121
  retry.push_str(" --json");
127
122
  }
128
123
  let explanation = if missing_team {
129
- format!("这里还没有队伍配置:{}。没有启动任何队友。请自行创建下面两个文件;本命令没有写入这些文件。", team_path.display())
124
+ 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())
130
125
  } else {
131
- format!("队友配置尚不完整:{detail}。请补齐角色目录或必需字段;下面仅给安全示例,不覆盖已有文件。")
126
+ format!("Worker configuration is incomplete: {detail}. Add the role directory or required fields. The examples below do not overwrite existing files. Error: {error}")
132
127
  };
133
- let next = format!("确认 Pi 已安装并登录;运行 team-agent models --provider pi 核对模型。\n运行 team-agent pi,在该主控的命令行/工具上下文再次执行:\n{retry}");
128
+ 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}");
134
129
  let mut templates =
135
130
  vec![serde_json::json!({"path": role_path, "content": super::emit::WORKER_TEMPLATE})];
136
131
  let mut human = explanation.clone();
@@ -140,13 +135,13 @@ fn quick_start_config_guidance(value: &mut Value, args: &QuickStartArgs) -> Opti
140
135
  serde_json::json!({"path": team_path, "content": super::emit::TEAM_TEMPLATE}),
141
136
  );
142
137
  human.push_str(&format!(
143
- "\n\n{}:\n{}",
138
+ "\n\n{}:\n{}",
144
139
  team_path.display(),
145
140
  super::emit::TEAM_TEMPLATE
146
141
  ));
147
142
  }
148
143
  human.push_str(&format!(
149
- "\n\n{}:\n{}\n下一步:\n{next}",
144
+ "\n\n{}:\n{}\nNext steps:\n{next}",
150
145
  role_path.display(),
151
146
  super::emit::WORKER_TEMPLATE
152
147
  ));
@@ -166,11 +161,10 @@ fn quick_start_config_guidance(value: &mut Value, args: &QuickStartArgs) -> Opti
166
161
  /// E13:quick-start "team 起了" 人类输出 = summary + attach 块。所有成功出口共用(别每分支手拷)。
167
162
  /// attach_commands 缺/空 → 只 summary(向后兼容)。
168
163
  fn quickstart_human(value: &Value) -> String {
169
- let summary = if value.get("summary").and_then(Value::as_str) == Some("existing runtime") {
170
- "发现已有队伍;请查看返回的连接方式,不会另建同名队伍。"
171
- } else {
172
- "队伍已启动;下面是连接方式和派发任务的命令。"
173
- };
164
+ let summary = value
165
+ .get("summary")
166
+ .and_then(Value::as_str)
167
+ .unwrap_or("Team started.");
174
168
  let attach: Vec<&str> = value
175
169
  .get("attach_commands")
176
170
  .and_then(Value::as_array)
@@ -183,14 +177,14 @@ fn quickstart_human(value: &Value) -> String {
183
177
  .unwrap_or_default();
184
178
  let mut out = String::from(summary);
185
179
  if !attach.is_empty() {
186
- out.push_str("\n\n连接队伍:");
180
+ out.push_str("\n\nConnect to the team:");
187
181
  for cmd in attach {
188
182
  out.push_str("\n ");
189
183
  out.push_str(cmd);
190
184
  }
191
185
  }
192
186
  if !sends.is_empty() {
193
- out.push_str("\n\n派发任务(请替换任务内容):");
187
+ out.push_str("\n\nSend a task (replace the example message):");
194
188
  for cmd in sends {
195
189
  out.push_str("\n ");
196
190
  out.push_str(cmd);
@@ -257,7 +251,7 @@ pub(crate) fn send_command(agent: &str, workspace: &Path, team: Option<&str>) ->
257
251
  let mut command = format!(
258
252
  "team-agent send {} {} --workspace {}",
259
253
  shell_quote(agent),
260
- shell_quote("请完成任务并把答案回复给 leader。"),
254
+ shell_quote("Complete the task and reply to the leader."),
261
255
  shell_quote(workspace)
262
256
  );
263
257
  if let Some(team) = team {
@@ -1014,12 +1008,13 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
1014
1008
 
1015
1009
  #[cfg(test)]
1016
1010
  mod tests {
1017
- #![allow(clippy::unwrap_used)]
1011
+ #![allow(clippy::unwrap_used, clippy::panic)]
1018
1012
 
1019
1013
  use super::{
1020
- append_send_guidance, format_inbox_human, quickstart_human, send_command,
1021
- split_shell_argv,
1014
+ append_send_guidance, finish_quick_start, format_inbox_human, quickstart_human,
1015
+ send_command, split_shell_argv,
1022
1016
  };
1017
+ use crate::cli::{CmdOutput, ExitCode, QuickStartArgs};
1023
1018
  use serde_json::{json, Value};
1024
1019
  use std::path::{Path, PathBuf};
1025
1020
 
@@ -1057,11 +1052,11 @@ mod tests {
1057
1052
  });
1058
1053
  let out = quickstart_human(&value);
1059
1054
  assert!(
1060
- out.contains("队伍已启动"),
1055
+ out.contains("team started"),
1061
1056
  "must show startup summary; got {out}"
1062
1057
  );
1063
1058
  assert!(
1064
- out.contains("连接队伍:"),
1059
+ out.contains("Connect to the team:"),
1065
1060
  "must render attach block; got {out}"
1066
1061
  );
1067
1062
  assert!(
@@ -1099,19 +1094,13 @@ mod tests {
1099
1094
  let value = json!({"summary": "quick-start complete"});
1100
1095
  assert_eq!(
1101
1096
  quickstart_human(&value),
1102
- format!(
1103
- "队伍已启动;下面是连接方式和派发任务的命令。\n{}",
1104
- crate::cli::QUICK_START_REMINDER
1105
- )
1097
+ format!("quick-start complete\n{}", crate::cli::QUICK_START_REMINDER)
1106
1098
  );
1107
1099
  // 空数组也只显示人类摘要与下一步,不凭空编造连接方式。
1108
1100
  let value2 = json!({"summary": "s", "attach_commands": []});
1109
1101
  assert_eq!(
1110
1102
  quickstart_human(&value2),
1111
- format!(
1112
- "队伍已启动;下面是连接方式和派发任务的命令。\n{}",
1113
- crate::cli::QUICK_START_REMINDER
1114
- )
1103
+ format!("s\n{}", crate::cli::QUICK_START_REMINDER)
1115
1104
  );
1116
1105
  }
1117
1106
 
@@ -1130,7 +1119,7 @@ mod tests {
1130
1119
  "team-agent",
1131
1120
  "send",
1132
1121
  "worker name; echo unsafe",
1133
- "请完成任务并把答案回复给 leader。",
1122
+ "Complete the task and reply to the leader.",
1134
1123
  "--workspace",
1135
1124
  "/tmp/my workspace",
1136
1125
  "--team",
@@ -1149,7 +1138,7 @@ mod tests {
1149
1138
  .unwrap();
1150
1139
  assert_eq!(
1151
1140
  command,
1152
- "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"
1153
1142
  );
1154
1143
  }
1155
1144
 
@@ -1206,7 +1195,7 @@ mod tests {
1206
1195
  "team-agent",
1207
1196
  "send",
1208
1197
  "worker",
1209
- "请完成任务并把答案回复给 leader。",
1198
+ "Complete the task and reply to the leader.",
1210
1199
  "--workspace",
1211
1200
  "/tmp/ws",
1212
1201
  "--team",
@@ -1215,4 +1204,84 @@ mod tests {
1215
1204
  );
1216
1205
  }
1217
1206
 
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
+ }
1219
+
1220
+ #[test]
1221
+ fn quick_start_human_preserves_typed_refusals_without_error_field() {
1222
+ for status in [
1223
+ "existing_runtime",
1224
+ "preflight_blocked",
1225
+ "leader_binding_refused",
1226
+ ] {
1227
+ let value = json!({
1228
+ "ok": false,
1229
+ "status": status,
1230
+ "reason": "the exact underlying refusal",
1231
+ "summary": "creation refused",
1232
+ "blockers": [{"stage": "identity", "reason": "owner mismatch"}],
1233
+ "next_actions": ["team-agent restart --workspace /tmp/first-launch --team current"],
1234
+ "state_path": "/tmp/first-launch/.team/runtime/state.json",
1235
+ "readiness": {"reason": "underlying readiness evidence"}
1236
+ });
1237
+ let result = finish_quick_start(value.clone(), &quick_start_args(false)).unwrap();
1238
+ assert_eq!(result.exit, ExitCode::Error);
1239
+ let CmdOutput::Human(text) = result.output else {
1240
+ panic!("expected human report")
1241
+ };
1242
+ let rendered: Value =
1243
+ serde_json::from_str(text.strip_prefix("quick-start report:\n").unwrap()).unwrap();
1244
+ assert_eq!(rendered, value, "human projection lost fields for {status}");
1245
+ assert!(
1246
+ !text.contains("team-agent doctor"),
1247
+ "must not replace actual actions"
1248
+ );
1249
+ let json_result = finish_quick_start(value.clone(), &quick_start_args(true)).unwrap();
1250
+ assert_eq!(json_result.exit, ExitCode::Error);
1251
+ assert_eq!(json_result.output, CmdOutput::Json(value));
1252
+ }
1253
+ }
1254
+
1255
+ #[test]
1256
+ fn quick_start_human_preserves_original_error_and_actions() {
1257
+ let value = json!({
1258
+ "ok": false,
1259
+ "error": "database initialization failed at /tmp/first-launch/.team/team.db: permission denied",
1260
+ "action": "repair directory permissions",
1261
+ "next_actions": ["inspect the exact database path"]
1262
+ });
1263
+ let result = finish_quick_start(value.clone(), &quick_start_args(false)).unwrap();
1264
+ assert_eq!(result.exit, ExitCode::Error);
1265
+ let CmdOutput::Human(text) = result.output else {
1266
+ panic!("expected human report")
1267
+ };
1268
+ let rendered: Value =
1269
+ serde_json::from_str(text.strip_prefix("quick-start report:\n").unwrap()).unwrap();
1270
+ assert_eq!(rendered, value);
1271
+ }
1272
+
1273
+ #[test]
1274
+ fn pending_tool_load_is_not_reported_as_failed_team_creation() {
1275
+ for as_json in [false, true] {
1276
+ let value = json!({
1277
+ "ok": true,
1278
+ "status": "pending_tool_load",
1279
+ "summary": "Team started; worker tool loading is not yet verified",
1280
+ "readiness": {"ready": false},
1281
+ "next_actions": ["send a task and wait for the actual reply"]
1282
+ });
1283
+ let result = finish_quick_start(value, &quick_start_args(as_json)).unwrap();
1284
+ assert_eq!(result.exit, ExitCode::Ok);
1285
+ }
1286
+ }
1218
1287
  }
@@ -342,7 +342,13 @@ pub(crate) fn diagnose_runtime_for_workspace(
342
342
  &mut repairs,
343
343
  );
344
344
  append_legacy_snapshot_issue(workspace, state, &mut issues);
345
- append_coordinator_health_issue(workspace, state, coordinator_health, &mut issues, &mut repairs);
345
+ append_coordinator_health_issue(
346
+ workspace,
347
+ state,
348
+ coordinator_health,
349
+ &mut issues,
350
+ &mut repairs,
351
+ );
346
352
  append_runtime_bindings_stale_after_boot_issue(workspace, state, &mut issues, &mut repairs);
347
353
  (issues, repairs)
348
354
  }
@@ -373,7 +379,9 @@ pub(crate) fn workspace_has_existing_team_runtime(
373
379
  let terminal = team_state
374
380
  .get("status")
375
381
  .and_then(Value::as_str)
376
- .is_some_and(|status| matches!(status, "stopped" | "shutdown" | "archived" | "terminal"));
382
+ .is_some_and(|status| {
383
+ matches!(status, "stopped" | "shutdown" | "archived" | "terminal")
384
+ });
377
385
  if terminal {
378
386
  return false;
379
387
  }
@@ -695,14 +703,13 @@ pub(crate) fn append_registry_channel_unbound_to_report(
695
703
  repairs.push(recovery_hint(&team_key, issue_id, repair));
696
704
  object.insert("issues".to_string(), Value::Array(issues));
697
705
  object.insert("suggested_repairs".to_string(), Value::Array(repairs));
698
- let is_healthy_unattached_host =
699
- class == crate::lifecycle::launch::LeaderBindingClass::Unbound
700
- && object.get("ok").and_then(Value::as_bool) == Some(true)
701
- && object
702
- .get("profile_smoke")
703
- .and_then(|profile| profile.get("checks"))
704
- .and_then(Value::as_array)
705
- .is_some_and(|checks| !checks.is_empty());
706
+ let is_healthy_unattached_host = class == crate::lifecycle::launch::LeaderBindingClass::Unbound
707
+ && object.get("ok").and_then(Value::as_bool) == Some(true)
708
+ && object
709
+ .get("profile_smoke")
710
+ .and_then(|profile| profile.get("checks"))
711
+ .and_then(Value::as_array)
712
+ .is_some_and(|checks| !checks.is_empty());
706
713
  if !is_healthy_unattached_host {
707
714
  object.insert("ok".to_string(), Value::Bool(false));
708
715
  }
@@ -1054,8 +1061,7 @@ mod tests {
1054
1061
  pane_pid: None,
1055
1062
  leader_env: Default::default(),
1056
1063
  }]);
1057
- let Some((_, repair)) =
1058
- live_leader_workspace_mismatch(&workspace, &state, &transport)
1064
+ let Some((_, repair)) = live_leader_workspace_mismatch(&workspace, &state, &transport)
1059
1065
  else {
1060
1066
  panic!("workspace mismatch repair required");
1061
1067
  };
@@ -1109,7 +1115,8 @@ mod tests {
1109
1115
  .as_nanos()
1110
1116
  ));
1111
1117
  std::fs::create_dir_all(&workspace).unwrap();
1112
- let state = json!({"agents": {}, "tasks": [], "session_name": null, "active_team_key": null});
1118
+ let state =
1119
+ json!({"agents": {}, "tasks": [], "session_name": null, "active_team_key": null});
1113
1120
  let mut issues = json!([]);
1114
1121
  let mut repairs = json!([]);
1115
1122
  append_registry_channel_unbound_issue(
@@ -1574,6 +1581,22 @@ pub(crate) fn provider_doctor_checks() -> Value {
1574
1581
  }),
1575
1582
  );
1576
1583
  }
1584
+ #[cfg(unix)]
1585
+ for descriptor in crate::contract_runtime::registry::descriptors() {
1586
+ let identity = &descriptor.identity;
1587
+ // Inventory only: finding the launcher is not an engine, auth or readiness probe.
1588
+ providers.insert(
1589
+ identity.id.to_string(),
1590
+ json!({
1591
+ "auth": crate::provider::AuthHintStatus::Unknown,
1592
+ "command": identity.binary,
1593
+ "installed": crate::contract_runtime::discovery::executable(identity.binary).is_ok(),
1594
+ "version": "unknown",
1595
+ "probe_status": "not_run",
1596
+ "provider_probe_status": "not_run",
1597
+ }),
1598
+ );
1599
+ }
1577
1600
  Value::Object(providers)
1578
1601
  }
1579
1602