@team-agent/installer 0.5.71 → 0.5.73

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 (32) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +107 -9
  4. package/crates/team-agent/src/cli/emit.rs +35 -5
  5. package/crates/team-agent/src/cli/mod.rs +78 -7
  6. package/crates/team-agent/src/cli/models.rs +443 -0
  7. package/crates/team-agent/src/cli/spec.rs +4 -1
  8. package/crates/team-agent/src/cli/types.rs +8 -0
  9. package/crates/team-agent/src/compiler.rs +157 -0
  10. package/crates/team-agent/src/leader/owner_bind.rs +21 -0
  11. package/crates/team-agent/src/leader/provider_attribution.rs +25 -0
  12. package/crates/team-agent/src/leader/registry.rs +25 -1
  13. package/crates/team-agent/src/leader/start.rs +41 -9
  14. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +169 -10
  15. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +119 -9
  16. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +859 -12
  17. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -0
  18. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -0
  19. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +168 -0
  20. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +209 -0
  21. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +1 -1
  22. package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +71 -1
  23. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +3 -1
  24. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +396 -3
  25. package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +3 -1
  26. package/crates/team-agent/src/lifecycle/types.rs +8 -0
  27. package/crates/team-agent/src/provider/adapters/pi.rs +12 -6
  28. package/npm/check-skill-links.mjs +88 -0
  29. package/package.json +5 -4
  30. package/skills/team-agent/SKILL.md +15 -49
  31. package/skills/team-agent/command-coverage.json +8 -1
  32. package/skills/team-agent/references/team-agent-operator.md +665 -0
package/Cargo.lock CHANGED
@@ -581,7 +581,7 @@ dependencies = [
581
581
 
582
582
  [[package]]
583
583
  name = "team-agent"
584
- version = "0.5.71"
584
+ version = "0.5.73"
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.71"
12
+ version = "0.5.73"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -102,7 +102,7 @@ pub fn cmd_init(args: &InitArgs) -> Result<CmdResult, CliError> {
102
102
 
103
103
  /// `cmd_quick_start`(`commands.py:18`)。`--json` 或 `!ok` → 整 dict;否则 `result["summary"]`。
104
104
  pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
105
- let value = lifecycle_port::quick_start(
105
+ let mut value = lifecycle_port::quick_start(
106
106
  &args.workspace,
107
107
  &args.agents_dir,
108
108
  args.name.as_deref(),
@@ -111,6 +111,7 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
111
111
  !args.no_display,
112
112
  args.backend.as_deref(),
113
113
  )?;
114
+ append_send_guidance(&mut value, &args.workspace, args.team_id.as_deref());
114
115
  let readiness = value.get("readiness").and_then(Value::as_object);
115
116
  let all_resumable_have_session = readiness
116
117
  .and_then(|readiness| readiness.get("all_resumable_have_session"))
@@ -156,18 +157,74 @@ fn quickstart_human(value: &Value) -> String {
156
157
  .and_then(Value::as_array)
157
158
  .map(|items| items.iter().filter_map(Value::as_str).collect())
158
159
  .unwrap_or_default();
159
- if attach.is_empty() {
160
- return append_reminder(summary.to_string(), crate::cli::QUICK_START_REMINDER);
161
- }
160
+ let sends: Vec<&str> = value
161
+ .get("send_commands")
162
+ .and_then(Value::as_array)
163
+ .map(|items| items.iter().filter_map(Value::as_str).collect())
164
+ .unwrap_or_default();
162
165
  let mut out = String::from(summary);
163
- out.push_str("\n\nattach:");
164
- for cmd in attach {
165
- out.push_str("\n ");
166
- out.push_str(cmd);
166
+ if !attach.is_empty() {
167
+ out.push_str("\n\nattach:");
168
+ for cmd in attach {
169
+ out.push_str("\n ");
170
+ out.push_str(cmd);
171
+ }
172
+ }
173
+ if !sends.is_empty() {
174
+ out.push_str("\n\nsend:");
175
+ for cmd in sends {
176
+ out.push_str("\n ");
177
+ out.push_str(cmd);
178
+ }
167
179
  }
168
180
  append_reminder(out, crate::cli::QUICK_START_REMINDER)
169
181
  }
170
182
 
183
+ fn append_send_guidance(value: &mut Value, workspace: &Path, team: Option<&str>) {
184
+ if value.get("ok").and_then(Value::as_bool) != Some(true) {
185
+ return;
186
+ }
187
+ let agents = value
188
+ .get("agent_ids")
189
+ .and_then(Value::as_array)
190
+ .cloned()
191
+ .unwrap_or_default();
192
+ let commands: Vec<Value> = agents
193
+ .iter()
194
+ .filter_map(Value::as_str)
195
+ .filter_map(|agent| send_command(agent, workspace, team).map(Value::String))
196
+ .collect();
197
+ if !commands.is_empty() {
198
+ if let Some(object) = value.as_object_mut() {
199
+ object.insert("send_commands".to_string(), Value::Array(commands));
200
+ }
201
+ }
202
+ }
203
+
204
+ pub(crate) fn send_command(agent: &str, workspace: &Path, team: Option<&str>) -> Option<String> {
205
+ let workspace = workspace.to_str()?;
206
+ let mut command = format!(
207
+ "team-agent send {} \\\"MESSAGE\\\" --workspace {}",
208
+ shell_quote(agent),
209
+ shell_quote(workspace)
210
+ );
211
+ if let Some(team) = team {
212
+ command.push_str(" --team ");
213
+ command.push_str(&shell_quote(team));
214
+ }
215
+ Some(command)
216
+ }
217
+
218
+ fn shell_quote(value: &str) -> String {
219
+ if value.bytes().all(|byte| {
220
+ byte.is_ascii_alphanumeric() || matches!(byte, b'/' | b'.' | b'_' | b'-' | b':')
221
+ }) {
222
+ value.to_string()
223
+ } else {
224
+ format!("'{}'", value.replace('\'', "'\\''"))
225
+ }
226
+ }
227
+
171
228
  fn append_reminder(text: String, reminder: &str) -> String {
172
229
  if text.is_empty() {
173
230
  reminder.to_string()
@@ -1550,8 +1607,9 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
1550
1607
  mod tests {
1551
1608
  #![allow(clippy::unwrap_used)]
1552
1609
 
1553
- use super::{agent_pane_id, quickstart_human};
1610
+ use super::{agent_pane_id, append_send_guidance, quickstart_human, send_command};
1554
1611
  use serde_json::json;
1612
+ use std::path::{Path, PathBuf};
1555
1613
 
1556
1614
  // E13:happy 人类输出必须带 attach 块(此前 else 分支只打 summary 丢 attach_commands)。
1557
1615
  #[test]
@@ -1594,6 +1652,46 @@ mod tests {
1594
1652
  );
1595
1653
  }
1596
1654
 
1655
+ #[test]
1656
+ fn send_guidance_is_copyable_and_preserves_explicit_scope() {
1657
+ let command = send_command("worker name; echo unsafe", Path::new("/tmp/my workspace"), Some("team-a"))
1658
+ .unwrap();
1659
+ assert_eq!(
1660
+ command,
1661
+ "team-agent send 'worker name; echo unsafe' \\\"MESSAGE\\\" --workspace '/tmp/my workspace' --team team-a"
1662
+ );
1663
+ }
1664
+
1665
+ #[cfg(unix)]
1666
+ #[test]
1667
+ fn non_utf8_workspace_fails_closed_without_send_guidance() {
1668
+ use std::ffi::OsString;
1669
+ use std::os::unix::ffi::OsStringExt;
1670
+
1671
+ let workspace = PathBuf::from(OsString::from_vec(b"/tmp/workspace-\xff".to_vec()));
1672
+ assert!(send_command("worker", &workspace, None).is_none());
1673
+ let mut value = json!({"ok": true, "agent_ids": ["worker"]});
1674
+ append_send_guidance(&mut value, &workspace, None);
1675
+ assert!(value.get("send_commands").is_none());
1676
+ }
1677
+
1678
+ #[test]
1679
+ fn quick_start_guidance_lists_every_known_agent_without_guessing() {
1680
+ let mut value = json!({"ok": true, "agent_ids": ["sol", "luna"]});
1681
+ append_send_guidance(&mut value, Path::new("/tmp/ws"), Some("team"));
1682
+ let commands = value.get("send_commands").and_then(|v| v.as_array()).unwrap();
1683
+ assert_eq!(commands.len(), 2);
1684
+ assert!(commands[0].as_str().unwrap().contains("send sol"));
1685
+ assert!(commands[1].as_str().unwrap().contains("send luna"));
1686
+ }
1687
+
1688
+ #[test]
1689
+ fn error_results_do_not_get_send_guidance() {
1690
+ let mut value = json!({"ok": false, "agent_ids": ["worker"]});
1691
+ append_send_guidance(&mut value, Path::new("/tmp/ws"), None);
1692
+ assert!(value.get("send_commands").is_none());
1693
+ }
1694
+
1597
1695
  #[test]
1598
1696
  fn agent_pane_id_resolves_session_window_even_with_recorded_pane_id() {
1599
1697
  let state = json!({
@@ -126,6 +126,15 @@ fn write_stdout_line(text: &str) -> std::io::Result<()> {
126
126
  stdout.write_all(b"\n")
127
127
  }
128
128
 
129
+ #[cfg(test)]
130
+ pub(crate) fn __test_dispatch(
131
+ command: &str,
132
+ args: &[String],
133
+ cwd: &Path,
134
+ ) -> Result<ExitCode, CliError> {
135
+ dispatch(command, args, cwd)
136
+ }
137
+
129
138
  fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliError> {
130
139
  let Some(spec) = command_spec(command) else {
131
140
  return Ok(emit_unknown_subcommand_usage(command));
@@ -188,6 +197,7 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
188
197
  // (workspace, team_key) tuple and delegates to the E6 named-leader
189
198
  // delivery path — no separate route authority.
190
199
  "leaders" => cmd_leaders(&leaders_args(args, cwd)).map(emit_result),
200
+ "models" => cmd_models(&models_args(args)?).map(emit_result),
191
201
  "validate" => cmd_validate(&validate_args(args, cwd)).map(emit_result),
192
202
  "install-skill" => cmd_install_skill(&install_skill_args(args)?).map(emit_result),
193
203
  "profile" => cmd_profile(&profile_args(args, cwd)?).map(emit_result),
@@ -239,6 +249,7 @@ const DISPATCH_COMMANDS: &[&str] = &[
239
249
  "sessions",
240
250
  // 0.5.9 E7: host leader discovery command surface.
241
251
  "leaders",
252
+ "models",
242
253
  "validate",
243
254
  "install-skill",
244
255
  "profile",
@@ -291,7 +302,7 @@ pub(crate) fn default_help() -> String {
291
302
  append_help_section(
292
303
  &mut out,
293
304
  "Core",
294
- &["quick-start", "send", "status", "collect", "results"],
305
+ &["quick-start", "send", "status", "collect", "results", "models"],
295
306
  );
296
307
  append_help_section(
297
308
  &mut out,
@@ -311,7 +322,8 @@ pub(crate) fn default_help() -> String {
311
322
  "Guided recovery",
312
323
  &["claim-leader", "takeover", "attach-leader"],
313
324
  );
314
- out.push_str("\nProvider launchers:\n team-agent codex|claude|copilot|grok|cursor|pi ...\n");
325
+ out.push_str("\nWorkflow: `team-agent models --provider pi`, then quick-start/add-agent/start-agent, then `team-agent send AGENT MESSAGE`.\n");
326
+ out.push_str("Provider launchers:\n team-agent codex|claude|copilot|grok|cursor|pi ...\n");
315
327
  out.push_str("\nRun `team-agent <command> --help` for command flags.");
316
328
  out
317
329
  }
@@ -362,7 +374,7 @@ fn command_help(command: Option<&str>) -> String {
362
374
  match command {
363
375
  None => default_help(),
364
376
  Some("init") => compat_hidden_help("init", "usage: team-agent init [--workspace WORKSPACE] [--force] [--json]"),
365
- Some("quick-start") => "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]\n\ndefaults: display_backend=adaptive; set display_backend: none in TEAM.md or pass --no-display to use one worker window per agent.\n\n--backend selects the worker transport (Phase 1d Batch 2): tmux (default on POSIX; unchanged behavior), conpty (Windows-native ConPTY worker transport; requires the shim binary and Windows host).".to_string(),
377
+ Some("quick-start") => "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]\n\ndefaults: display_backend=adaptive; set display_backend: none in TEAM.md or pass --no-display to use one worker window per agent.\n\n--backend selects the worker transport (Phase 1d Batch 2): tmux (default on POSIX; unchanged behavior), conpty (Windows-native ConPTY worker transport; requires the shim binary and Windows host).\n\nAfter a successful start, use the returned `send_commands` (or choose an agent explicitly) with `team-agent send AGENT MESSAGE`.".to_string(),
366
378
  Some("start") => compat_hidden_help("start", "usage: team-agent start [TEAMDIR] [--yes] [--fresh] [--json]"),
367
379
  Some("compile") => "usage: team-agent compile --team TEAM [--out FILE] [--json]".to_string(),
368
380
  Some("send") => concat!(
@@ -376,14 +388,15 @@ fn command_help(command: Option<&str>) -> String {
376
388
  .to_string(),
377
389
  Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
378
390
  Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n默认输出: worker,空闲|工作|错误;错误细分走 status --summary".to_string(),
391
+ Some("models") => "usage: team-agent models --provider pi [--search TEXT] [--json]\n\nPrints models.v1 exact role_model entries; each entry includes current=true|false. Catalog readiness is reported as auth=ok|not_ready with auth_basis=catalog_visibility.".to_string(),
379
392
  Some("stop") => compat_hidden_help("stop", "usage: team-agent stop [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]"),
380
393
  Some("shutdown") => "usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]".to_string(),
381
394
  Some("restart") => "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]".to_string(),
382
395
  Some("restart-agent") => compat_hidden_help("restart-agent", "usage: team-agent restart-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--discard-session] [--no-display] [--json]"),
383
396
  Some("reset-agent") => "usage: team-agent reset-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--discard-session] [--no-display] [--json]".to_string(),
384
- Some("start-agent") => "usage: team-agent start-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--force] [--allow-fresh] [--no-display] [--json]".to_string(),
397
+ Some("start-agent") => "usage: team-agent start-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--force] [--allow-fresh] [--no-display] [--json]\n\nAfter a successful start, use the returned `send_commands` with `team-agent send AGENT MESSAGE`.".to_string(),
385
398
  Some("stop-agent") => "usage: team-agent stop-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
386
- Some("add-agent") => "usage: team-agent add-agent AGENT --role-file FILE [--force] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]".to_string(),
399
+ Some("add-agent") => "usage: team-agent add-agent AGENT --role-file FILE [--force] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]\n\nAfter a successful add, use the returned `send_commands` with `team-agent send AGENT MESSAGE`.".to_string(),
387
400
  Some("clone-agent") => "usage: team-agent clone-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]".to_string(),
388
401
  Some("fork-agent") => "usage: team-agent fork-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]".to_string(),
389
402
  Some("remove-agent") => "usage: team-agent remove-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--from-spec] [--confirm] [--force] [--json]".to_string(),
@@ -1054,6 +1067,23 @@ fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliEr
1054
1067
  })
1055
1068
  }
1056
1069
 
1070
+ fn models_args(args: &[String]) -> Result<ModelsArgs, CliError> {
1071
+ let parsed = parse_args(args);
1072
+ let provider = parsed
1073
+ .provider
1074
+ .ok_or_else(|| CliError::Usage("models requires --provider pi".to_string()))?;
1075
+ if provider != "pi" {
1076
+ return Err(CliError::Usage(format!(
1077
+ "models supports only --provider pi, got {provider:?}"
1078
+ )));
1079
+ }
1080
+ Ok(ModelsArgs {
1081
+ provider,
1082
+ search: parsed.search,
1083
+ json: parsed.json,
1084
+ })
1085
+ }
1086
+
1057
1087
  fn init_args(args: &[String], cwd: &Path) -> InitArgs {
1058
1088
  let parsed = parse_args(args);
1059
1089
  InitArgs {
@@ -60,6 +60,7 @@ pub mod emit;
60
60
  pub mod helpers;
61
61
  pub mod leader;
62
62
  pub mod leaders;
63
+ pub mod models;
63
64
  pub mod named_address;
64
65
  pub mod profile;
65
66
  pub mod send;
@@ -73,6 +74,7 @@ pub use diagnose::*;
73
74
  pub use emit::*;
74
75
  pub use leader::*;
75
76
  pub use leaders::*;
77
+ pub use models::*;
76
78
  pub use named_address::*;
77
79
  pub use profile::*;
78
80
  pub use send::*;
@@ -2641,9 +2643,48 @@ pub mod lifecycle_port {
2641
2643
  allow_fresh,
2642
2644
  team,
2643
2645
  ) {
2644
- Ok(report) => {
2645
- Ok(json!({"ok": true, "agent_id": agent, "report": format!("{report:?}")}))
2646
+ Ok(crate::lifecycle::StartAgentOutcome::Running {
2647
+ env,
2648
+ start_mode,
2649
+ target,
2650
+ session_id,
2651
+ new_session_id,
2652
+ rollout_path,
2653
+ }) => Ok(agent_action_value(
2654
+ agent,
2655
+ workspace,
2656
+ team,
2657
+ json!({
2658
+ "ok": true,
2659
+ "agent_id": env.agent_id.as_str(),
2660
+ "status": "running",
2661
+ "start_mode": start_mode,
2662
+ "target": target,
2663
+ "session_id": session_id.as_ref().map(|id| id.as_str()),
2664
+ "new_session_id": new_session_id.as_ref().map(|id| id.as_str()),
2665
+ "rollout_path": rollout_path
2666
+ .as_ref()
2667
+ .map(|path| path.as_path().to_string_lossy().to_string()),
2668
+ "coordinator_started": env.coordinator_started,
2669
+ "state_file": env.state_file.to_string_lossy(),
2670
+ }),
2671
+ )),
2672
+ Ok(crate::lifecycle::StartAgentOutcome::Noop { env, target }) => {
2673
+ Ok(agent_action_value(agent, workspace, team, json!({
2674
+ "ok": true,
2675
+ "agent_id": env.agent_id.as_str(),
2676
+ "status": "already_running",
2677
+ "target": target,
2678
+ "coordinator_started": env.coordinator_started,
2679
+ "state_file": env.state_file.to_string_lossy(),
2680
+ })))
2646
2681
  }
2682
+ Ok(crate::lifecycle::StartAgentOutcome::Paused { agent_id }) => Ok(json!({
2683
+ "ok": false,
2684
+ "agent_id": agent_id.as_str(),
2685
+ "status": "paused",
2686
+ "reason": "agent_paused",
2687
+ })),
2647
2688
  Err(e) => Ok(error_value(e)),
2648
2689
  }
2649
2690
  }
@@ -2730,14 +2771,34 @@ pub mod lifecycle_port {
2730
2771
  team,
2731
2772
  force,
2732
2773
  ) {
2733
- Ok(report) => Ok(json!({
2734
- "ok": true,
2735
- "agent_id": agent,
2736
- "role_file": report.role_file.to_string_lossy(),
2737
- })),
2774
+ Ok(report) => Ok(agent_action_value(
2775
+ agent,
2776
+ workspace,
2777
+ team,
2778
+ json!({
2779
+ "ok": true,
2780
+ "agent_id": report.env.agent_id.as_str(),
2781
+ "status": "running",
2782
+ "start_mode": report.start_mode,
2783
+ "role_file": report.role_file.to_string_lossy(),
2784
+ "coordinator_started": report.env.coordinator_started,
2785
+ "state_file": report.env.state_file.to_string_lossy(),
2786
+ }),
2787
+ )),
2738
2788
  Err(e) => Ok(error_value(e)),
2739
2789
  }
2740
2790
  }
2791
+ fn agent_action_value(agent: &str, workspace: &Path, team: Option<&str>, mut value: Value) -> Value {
2792
+ if value.get("ok").and_then(Value::as_bool) == Some(true) {
2793
+ if let Some(object) = value.as_object_mut() {
2794
+ if let Some(command) = super::adapters::send_command(agent, workspace, team) {
2795
+ object.insert("send_commands".to_string(), json!([command]));
2796
+ }
2797
+ }
2798
+ }
2799
+ value
2800
+ }
2801
+
2741
2802
  ///
2742
2803
  /// `runtime.fork_agent`(`cmd_fork_agent`;`--as` 必需)。
2743
2804
  pub fn fork_agent(
@@ -3014,6 +3075,15 @@ pub mod lifecycle_port {
3014
3075
  }
3015
3076
 
3016
3077
  fn error_value(error: crate::lifecycle::LifecycleError) -> Value {
3078
+ if let crate::lifecycle::LifecycleError::PiModelPreflight {
3079
+ requested,
3080
+ candidates,
3081
+ action,
3082
+ not_ready,
3083
+ } = error
3084
+ {
3085
+ return json!({"ok": false, "error": "pi model preflight failed", "pi": {"requested_model": requested, "candidates": candidates, "not_ready": not_ready, "action": action.clone(), "next_action": action}});
3086
+ }
3017
3087
  let message = error.to_string();
3018
3088
  let mut payload = json!({"ok": false, "error": message});
3019
3089
  if let Some(next_action) = error_next_action(&message) {
@@ -3295,6 +3365,7 @@ pub mod lifecycle_port {
3295
3365
  "reason": readiness_json.get("reason").cloned().unwrap_or(Value::Null),
3296
3366
  "ready": readiness_json.get("ready").cloned().unwrap_or(Value::Bool(false)),
3297
3367
  "session_name": session_name.as_str(),
3368
+ "agent_ids": launch.started.iter().map(|agent| agent.agent_id.as_str()).collect::<Vec<_>>(),
3298
3369
  "dry_run": launch.dry_run,
3299
3370
  "display_backend": display_backend,
3300
3371
  "next_actions": next_actions,