@team-agent/installer 0.3.3 → 0.3.5

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 (63) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +8 -0
  4. package/crates/team-agent/src/cli/diagnose.rs +52 -11
  5. package/crates/team-agent/src/cli/emit.rs +3 -2
  6. package/crates/team-agent/src/cli/mod.rs +225 -80
  7. package/crates/team-agent/src/cli/send.rs +1 -0
  8. package/crates/team-agent/src/cli/status_port.rs +135 -7
  9. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +8 -1
  10. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  11. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +39 -0
  12. package/crates/team-agent/src/cli/types.rs +5 -1
  13. package/crates/team-agent/src/compiler/tests.rs +2 -2
  14. package/crates/team-agent/src/compiler.rs +1 -1
  15. package/crates/team-agent/src/coordinator/backoff.rs +57 -9
  16. package/crates/team-agent/src/coordinator/health.rs +65 -2
  17. package/crates/team-agent/src/coordinator/runtime_detectors.rs +28 -16
  18. package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +87 -0
  19. package/crates/team-agent/src/coordinator/tests/mod.rs +1 -0
  20. package/crates/team-agent/src/coordinator/tests/watch.rs +4 -2
  21. package/crates/team-agent/src/coordinator/tick.rs +195 -43
  22. package/crates/team-agent/src/leader/helpers.rs +2 -0
  23. package/crates/team-agent/src/leader/rediscover.rs +1 -0
  24. package/crates/team-agent/src/leader/start.rs +9 -1
  25. package/crates/team-agent/src/leader/takeover.rs +18 -1
  26. package/crates/team-agent/src/lifecycle/display.rs +3 -3
  27. package/crates/team-agent/src/lifecycle/launch.rs +772 -285
  28. package/crates/team-agent/src/lifecycle/mod.rs +1 -0
  29. package/crates/team-agent/src/lifecycle/profile_launch.rs +110 -4
  30. package/crates/team-agent/src/lifecycle/profile_smoke.rs +4 -1
  31. package/crates/team-agent/src/lifecycle/restart/agent.rs +16 -5
  32. package/crates/team-agent/src/lifecycle/restart/common.rs +35 -25
  33. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +31 -25
  34. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -2
  35. package/crates/team-agent/src/lifecycle/tests/core.rs +5 -5
  36. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +4 -4
  37. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +5 -3
  38. package/crates/team-agent/src/lifecycle/types.rs +4 -0
  39. package/crates/team-agent/src/lifecycle/worker_command_context.rs +361 -0
  40. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +2 -1
  41. package/crates/team-agent/src/mcp_server/tests/scoped.rs +14 -1
  42. package/crates/team-agent/src/mcp_server/tests/send.rs +15 -1
  43. package/crates/team-agent/src/mcp_server/tools.rs +65 -9
  44. package/crates/team-agent/src/mcp_server/wire.rs +2 -1
  45. package/crates/team-agent/src/message_store.rs +80 -0
  46. package/crates/team-agent/src/messaging/results.rs +76 -5
  47. package/crates/team-agent/src/messaging/send.rs +3 -1
  48. package/crates/team-agent/src/messaging/types.rs +15 -1
  49. package/crates/team-agent/src/messaging/watchers.rs +68 -30
  50. package/crates/team-agent/src/model/enums.rs +7 -1
  51. package/crates/team-agent/src/model/permissions.rs +7 -0
  52. package/crates/team-agent/src/model/spec.rs +3 -1
  53. package/crates/team-agent/src/provider/adapter.rs +472 -7
  54. package/crates/team-agent/src/provider/classify.rs +6 -2
  55. package/crates/team-agent/src/provider/faults.rs +3 -2
  56. package/crates/team-agent/src/provider/startup_prompt.rs +25 -7
  57. package/crates/team-agent/src/provider/types.rs +11 -0
  58. package/crates/team-agent/src/session_capture.rs +1 -0
  59. package/crates/team-agent/src/state/persist.rs +95 -19
  60. package/crates/team-agent/src/tmux_backend/tests.rs +8 -7
  61. package/crates/team-agent/src/tmux_backend.rs +134 -6
  62. package/crates/team-agent/src/transport.rs +32 -0
  63. package/package.json +4 -4
@@ -40,6 +40,7 @@ pub(crate) mod profile_launch;
40
40
  pub(crate) mod profile_smoke;
41
41
  pub mod restart;
42
42
  pub mod types;
43
+ pub(crate) mod worker_command_context;
43
44
 
44
45
  use std::collections::BTreeMap;
45
46
  use std::path::PathBuf;
@@ -385,6 +385,31 @@ fn provider_env_exports(
385
385
  exports.insert("GEMINI_API_KEY".to_string(), value.to_string());
386
386
  }
387
387
  }
388
+ // C-A-4 cr verdict v2 — copilot BYOK(== compatible_api 档,help-providers 原文
389
+ // "GitHub authentication is not required" when COPILOT_PROVIDER_BASE_URL set):
390
+ // profile 值经 env_overlay 落 COPILOT_PROVIDER_BASE_URL/TYPE/API_KEY/WIRE_API
391
+ // + COPILOT_MODEL。BYOK 必须含 MODEL("A model is required for BYOK")—
392
+ // 这一硬性校验在 launch 路径处理(profile compile 时无法判别 model 来源)。
393
+ // Subscription/OfficialApi 不导出 COPILOT_PROVIDER_*(避免误改 auth 通道)。
394
+ Provider::Copilot => {
395
+ if auth_mode == AuthMode::CompatibleApi {
396
+ if let Some(value) = value_or_alternate(values, "COPILOT_PROVIDER_BASE_URL", "BASE_URL") {
397
+ exports.insert("COPILOT_PROVIDER_BASE_URL".to_string(), value.to_string());
398
+ }
399
+ if let Some(value) = value_or_alternate(values, "COPILOT_PROVIDER_TYPE", "PROVIDER_TYPE") {
400
+ exports.insert("COPILOT_PROVIDER_TYPE".to_string(), value.to_string());
401
+ }
402
+ if let Some(value) = value_or_alternate(values, "COPILOT_PROVIDER_API_KEY", "API_KEY") {
403
+ exports.insert("COPILOT_PROVIDER_API_KEY".to_string(), value.to_string());
404
+ }
405
+ if let Some(value) = value_or_alternate(values, "COPILOT_PROVIDER_WIRE_API", "WIRE_API") {
406
+ exports.insert("COPILOT_PROVIDER_WIRE_API".to_string(), value.to_string());
407
+ }
408
+ if let Some(value) = value_or_alternate(values, "COPILOT_MODEL", "MODEL") {
409
+ exports.insert("COPILOT_MODEL".to_string(), value.to_string());
410
+ }
411
+ }
412
+ }
388
413
  Provider::Fake => {}
389
414
  }
390
415
  exports
@@ -412,6 +437,9 @@ fn provider_env_unsets(provider: Provider, auth_mode: AuthMode) -> BTreeSet<Stri
412
437
  unsets.insert("GEMINI_API_KEY".to_string());
413
438
  }
414
439
  }
440
+ // C-7-1 cr verdict: 一期 subscription-only(已登录态),exports/unsets 空;
441
+ // BYOK(COPILOT_PROVIDER_*)二期立项时单独 cr verdict 再开。
442
+ Provider::Copilot => {}
415
443
  Provider::Fake => {}
416
444
  }
417
445
  unsets
@@ -429,7 +457,48 @@ fn provider_command_overrides(
429
457
  _ if agent.model.is_some() => agent.model.clone(),
430
458
  _ => profile_model(values).map(str::to_string),
431
459
  };
432
- ProviderCommandOverrides { model }
460
+ let mut codex_profile = None;
461
+ let mut codex_config = Vec::new();
462
+ // Python provider_env.py:62-79 (_provider_command_overrides codex branch).
463
+ match agent.provider {
464
+ Provider::Codex => {
465
+ codex_profile = value_or_alternate(values, "CODEX_PROFILE", "NATIVE_PROFILE")
466
+ .map(str::to_string);
467
+ let model_provider = values.get("MODEL_PROVIDER").filter(|v| !v.is_empty());
468
+ let base_url = values.get("BASE_URL").filter(|v| !v.is_empty());
469
+ if agent.auth_mode == AuthMode::CompatibleApi {
470
+ if let (Some(model_provider), Some(base_url)) = (model_provider, base_url) {
471
+ if safe_codex_provider_id(model_provider) {
472
+ codex_config.push(format!("model_provider=\"{model_provider}\""));
473
+ let prefix = format!("model_providers.{model_provider}");
474
+ codex_config.push(format!("{prefix}.base_url=\"{base_url}\""));
475
+ codex_config
476
+ .push(format!("{prefix}.env_key=\"TEAM_AGENT_PROVIDER_API_KEY\""));
477
+ if let Some(wire_api) = values.get("WIRE_API").filter(|v| !v.is_empty()) {
478
+ codex_config.push(format!("{prefix}.wire_api=\"{wire_api}\""));
479
+ }
480
+ if let Some(name) = values.get("PROVIDER_NAME").filter(|v| !v.is_empty()) {
481
+ codex_config.push(format!("{prefix}.name=\"{name}\""));
482
+ }
483
+ }
484
+ }
485
+ }
486
+ }
487
+ _ => {}
488
+ }
489
+ ProviderCommandOverrides {
490
+ model,
491
+ codex_profile,
492
+ codex_config,
493
+ }
494
+ }
495
+
496
+ /// Python helpers.py:33-34 `_safe_codex_provider_id`: `[A-Za-z0-9_-]+`.
497
+ fn safe_codex_provider_id(value: &str) -> bool {
498
+ !value.is_empty()
499
+ && value
500
+ .chars()
501
+ .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
433
502
  }
434
503
 
435
504
  fn write_runtime_env_file(
@@ -636,15 +705,48 @@ fn mcp_server_names(mcp_servers: &serde_json::Value) -> Vec<String> {
636
705
  .unwrap_or_default()
637
706
  }
638
707
 
708
+ /// swallow batch 4 C1-C4 (MUST-NOT-9/13): a CORRUPT provider-config JSON fails
709
+ /// EXPLICITLY — the old `unwrap_or_default` turned a parse failure into an empty map
710
+ /// that the subsequent write flattened back over the user's file (silent destructive
711
+ /// rewrite). A missing file stays Ok(empty) — only an existing-but-unparseable file
712
+ /// errors, naming the path + parse detail + next action; the file is never touched.
639
713
  fn read_json_object(
640
714
  path: &Path,
641
715
  ) -> Result<serde_json::Map<String, serde_json::Value>, LifecycleError> {
642
- let Ok(text) = std::fs::read_to_string(path) else {
643
- return Ok(serde_json::Map::new());
716
+ let text = match std::fs::read_to_string(path) {
717
+ Ok(text) => text,
718
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
719
+ return Ok(serde_json::Map::new());
720
+ }
721
+ Err(error) => {
722
+ return Err(LifecycleError::RequirementUnmet(format!(
723
+ "profile_invalid_json: {} could not be read: {error}; fix or remove the file (or restore a backup) and relaunch",
724
+ path.display()
725
+ )));
726
+ }
644
727
  };
645
728
  match serde_json::from_str::<serde_json::Value>(&text) {
646
729
  Ok(serde_json::Value::Object(map)) => Ok(map),
647
- Ok(_) | Err(_) => Ok(serde_json::Map::new()),
730
+ Ok(other) => Err(LifecycleError::RequirementUnmet(format!(
731
+ "profile_invalid_json: {} is not a JSON object (found {}); fix or remove the file (or restore a backup) and relaunch",
732
+ path.display(),
733
+ json_kind(&other)
734
+ ))),
735
+ Err(error) => Err(LifecycleError::RequirementUnmet(format!(
736
+ "profile_invalid_json: {} could not be parsed: {error}; fix or remove the file (or restore a backup) and relaunch",
737
+ path.display()
738
+ ))),
739
+ }
740
+ }
741
+
742
+ fn json_kind(value: &serde_json::Value) -> &'static str {
743
+ match value {
744
+ serde_json::Value::Null => "null",
745
+ serde_json::Value::Bool(_) => "a boolean",
746
+ serde_json::Value::Number(_) => "a number",
747
+ serde_json::Value::String(_) => "a string",
748
+ serde_json::Value::Array(_) => "an array",
749
+ serde_json::Value::Object(_) => "an object",
648
750
  }
649
751
  }
650
752
 
@@ -732,6 +834,9 @@ fn required_profile_keys(provider: Provider, auth_mode: AuthMode) -> &'static [&
732
834
  AuthMode::Subscription => &[],
733
835
  },
734
836
  Provider::GeminiCli => &["API_KEY"],
837
+ // C-7-1 cr verdict: copilot 一期 subscription-only,无 BYOK required key 集合;
838
+ // 二期 BYOK 立项时填(向后兼容字段保留)。
839
+ Provider::Copilot => &[],
735
840
  Provider::Fake => &[],
736
841
  }
737
842
  }
@@ -794,6 +899,7 @@ pub(crate) fn parse_provider(raw: &str) -> Option<Provider> {
794
899
  "claude" => Some(Provider::Claude),
795
900
  "claude_code" => Some(Provider::ClaudeCode),
796
901
  "codex" => Some(Provider::Codex),
902
+ "copilot" => Some(Provider::Copilot),
797
903
  "gemini_cli" => Some(Provider::GeminiCli),
798
904
  "fake" => Some(Provider::Fake),
799
905
  _ => None,
@@ -184,7 +184,9 @@ impl SmokeTarget {
184
184
  kind: SmokeKind::OpenAi,
185
185
  })
186
186
  }
187
- Provider::GeminiCli | Provider::Fake => Err("unsupported_provider_smoke_skipped"),
187
+ // C-7-1 cr verdict: copilot 一期 subscription-only,无 BYOK HTTP smoke
188
+ // 入口;同 GeminiCli/Fake 走 unsupported_provider_smoke_skipped。
189
+ Provider::Copilot | Provider::GeminiCli | Provider::Fake => Err("unsupported_provider_smoke_skipped"),
188
190
  }
189
191
  }
190
192
 
@@ -508,6 +510,7 @@ fn provider_wire(provider: Provider) -> &'static str {
508
510
  Provider::Claude => "claude",
509
511
  Provider::ClaudeCode => "claude_code",
510
512
  Provider::Codex => "codex",
513
+ Provider::Copilot => "copilot",
511
514
  Provider::GeminiCli => "gemini_cli",
512
515
  Provider::Fake => "fake",
513
516
  }
@@ -451,10 +451,21 @@ fn write_start_agent_start_event(
451
451
  let adapter = crate::provider::get_adapter(provider);
452
452
  // Contract C / F6.4: event log must record the same context-aware argv that the
453
453
  // actual spawn used — so the role/tools/MCP context appears in `start_agent.agent_start`.
454
- let role = agent.get("role").and_then(|v| v.as_str());
455
454
  let safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
456
- let tools = crate::lifecycle::launch::worker_tool_refs(agent_tool_strings(agent), &safety);
457
- let tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
455
+ let command_agent =
456
+ crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
457
+ agent,
458
+ Some(agent_id.as_str()),
459
+ provider,
460
+ );
461
+ let system_prompt =
462
+ crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
463
+ let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
464
+ &command_agent,
465
+ provider,
466
+ &safety,
467
+ )?;
468
+ let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
458
469
  let mcp_config = adapter
459
470
  .mcp_config(auth_mode)
460
471
  .map_err(|e| LifecycleError::Provider(e.to_string()))?;
@@ -484,9 +495,9 @@ fn write_start_agent_start_event(
484
495
  let context = crate::provider::ProviderCommandContext {
485
496
  auth_mode,
486
497
  mcp_config: Some(&mcp_config),
487
- system_prompt: role,
498
+ system_prompt: Some(system_prompt.as_str()),
488
499
  model: command_model,
489
- tools: &tool_refs,
500
+ tools: &resolved_tool_refs,
490
501
  profile_launch: Some(&profile_launch),
491
502
  };
492
503
  let mut plan = match session_id {
@@ -29,7 +29,6 @@ pub(super) fn spawn_agent_window(
29
29
  // Contract C / F6.4: thread compiled role/tools/MCP context through restart as well —
30
30
  // a restarted worker must come back up with the SAME callable MCP capability + role
31
31
  // prompt as a fresh launch, else `report_result` becomes unreachable after every restart.
32
- let role = agent.get("role").and_then(|v| v.as_str());
33
32
  let detected_safety;
34
33
  let safety = if let Some(safety) = safety {
35
34
  safety
@@ -37,8 +36,20 @@ pub(super) fn spawn_agent_window(
37
36
  detected_safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
38
37
  &detected_safety
39
38
  };
40
- let tools = crate::lifecycle::launch::worker_tool_refs(agent_tool_strings(agent), safety);
41
- let tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
39
+ let command_agent =
40
+ crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
41
+ agent,
42
+ Some(agent_id.as_str()),
43
+ provider,
44
+ );
45
+ let system_prompt =
46
+ crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
47
+ let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
48
+ &command_agent,
49
+ provider,
50
+ safety,
51
+ )?;
52
+ let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
42
53
  // owner_team_id resolution: prefer the runtime-state row's `owner_team_id` (set by
43
54
  // launch/restart); fall back to the active team key for paths that don't write the
44
55
  // row first (e.g. add-agent calls spawn before upserting team metadata).
@@ -78,9 +89,9 @@ pub(super) fn spawn_agent_window(
78
89
  let context = crate::provider::ProviderCommandContext {
79
90
  auth_mode,
80
91
  mcp_config: Some(&mcp_config),
81
- system_prompt: role,
92
+ system_prompt: Some(system_prompt.as_str()),
82
93
  model: command_model,
83
- tools: &tool_refs,
94
+ tools: &resolved_tool_refs,
84
95
  profile_launch: Some(&profile_launch),
85
96
  };
86
97
  let mut plan = match resume_session_id {
@@ -120,10 +131,25 @@ pub(super) fn spawn_agent_window(
120
131
  .map(Path::new)
121
132
  })
122
133
  .unwrap_or(workspace);
134
+ let env_unset: Vec<String> = profile_launch.env_unset.iter().cloned().collect();
123
135
  let result = if into_existing_session {
124
- transport.spawn_into(session_name, &window, &plan.argv, spawn_cwd, &env)
136
+ transport.spawn_into_with_env_unset(
137
+ session_name,
138
+ &window,
139
+ &plan.argv,
140
+ spawn_cwd,
141
+ &env,
142
+ &env_unset,
143
+ )
125
144
  } else {
126
- transport.spawn_first(session_name, &window, &plan.argv, spawn_cwd, &env)
145
+ transport.spawn_first_with_env_unset(
146
+ session_name,
147
+ &window,
148
+ &plan.argv,
149
+ spawn_cwd,
150
+ &env,
151
+ &env_unset,
152
+ )
127
153
  };
128
154
  let spawn = result.map_err(|e| LifecycleError::Transport(e.to_string()))?;
129
155
  let _ = adapter.handle_startup_prompts(
@@ -319,24 +345,6 @@ pub(crate) fn restart_required_missing_session_agent_ids(state: &serde_json::Val
319
345
  missing.sort();
320
346
  missing
321
347
  }
322
- /// Tools list off an agent's runtime state entry (`tools: [...]`). Restart paths
323
- /// don't have the full spec object, only the runtime state — so they read tools from
324
- /// the state row, falling back to an empty list. Contract C requires the worker
325
- /// command be built with the tool list, even on restart.
326
- pub(super) fn agent_tool_strings(agent: &serde_json::Value) -> Vec<String> {
327
- agent
328
- .get("tools")
329
- .and_then(|v| v.as_array())
330
- .map(|items| {
331
- items
332
- .iter()
333
- .filter_map(|v| v.as_str())
334
- .map(str::to_string)
335
- .collect()
336
- })
337
- .unwrap_or_default()
338
- }
339
-
340
348
  pub(super) fn agent_window(agent: &serde_json::Value, agent_id: &AgentId) -> String {
341
349
  agent
342
350
  .get("window")
@@ -351,6 +359,7 @@ pub(super) fn parse_provider(raw: &str) -> Option<Provider> {
351
359
  "claude" => Some(Provider::Claude),
352
360
  "claude_code" => Some(Provider::ClaudeCode),
353
361
  "codex" => Some(Provider::Codex),
362
+ "copilot" => Some(Provider::Copilot),
354
363
  "gemini_cli" => Some(Provider::GeminiCli),
355
364
  "fake" => Some(Provider::Fake),
356
365
  _ => None,
@@ -362,6 +371,7 @@ pub(super) fn provider_wire(provider: Provider) -> &'static str {
362
371
  Provider::Claude => "claude",
363
372
  Provider::ClaudeCode => "claude_code",
364
373
  Provider::Codex => "codex",
374
+ Provider::Copilot => "copilot",
365
375
  Provider::GeminiCli => "gemini_cli",
366
376
  Provider::Fake => "fake",
367
377
  }
@@ -1,6 +1,6 @@
1
- use super::*;
2
1
  use super::common::*;
3
2
  use super::selection::classify_restart_plan;
3
+ use super::*;
4
4
 
5
5
  // ── lifecycle::restart —— 整队 Route B resume-or-fresh 重建 ──────────────────
6
6
 
@@ -101,10 +101,9 @@ pub fn restart_with_transport_with_session_convergence_deadline(
101
101
  .map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
102
102
  let mut state = selected.state;
103
103
  crate::lifecycle::launch::ensure_owner_allowed_for_state(&state, None)?;
104
- let spec_workspace = selected
105
- .spec_workspace
106
- .as_ref()
107
- .ok_or_else(|| LifecycleError::TeamSelect("active team spec workspace not found".to_string()))?;
104
+ let spec_workspace = selected.spec_workspace.as_ref().ok_or_else(|| {
105
+ LifecycleError::TeamSelect("active team spec workspace not found".to_string())
106
+ })?;
108
107
  let spec = load_team_spec(spec_workspace)?;
109
108
  let safety = crate::lifecycle::launch::effective_runtime_config(&spec)?;
110
109
  let mut convergence = converge_missing_provider_sessions(
@@ -236,10 +235,20 @@ pub fn restart_with_transport_with_session_convergence_deadline(
236
235
  crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)
237
236
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
238
237
  let coordinator_started = start_coordinator_for_workspace(&selected.run_workspace)?;
238
+ let attach_commands = crate::tmux_backend::attach_commands_for_windows(
239
+ &selected.run_workspace,
240
+ &session_name,
241
+ plan.decisions
242
+ .iter()
243
+ .map(|decision| decision.agent_id.as_str()),
244
+ );
245
+ let next_actions = attach_commands.clone();
239
246
  Ok(RestartReport::Restarted {
240
247
  session_name,
241
248
  agents: plan.decisions,
242
249
  coordinator_started,
250
+ next_actions,
251
+ attach_commands,
243
252
  })
244
253
  }
245
254
 
@@ -456,10 +465,7 @@ fn mark_leader_receiver_rebind_required(state: &mut serde_json::Value, session_n
456
465
  .and_then(|v| v.as_str())
457
466
  .is_some_and(|status| status == "attached")
458
467
  {
459
- receiver.insert(
460
- "status".to_string(),
461
- serde_json::json!("rebind_required"),
462
- );
468
+ receiver.insert("status".to_string(), serde_json::json!("rebind_required"));
463
469
  }
464
470
  }
465
471
 
@@ -520,11 +526,7 @@ fn mark_agent_respawned(
520
526
  if let Some(pane_pid) = pane_pid {
521
527
  agent.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
522
528
  }
523
- crate::lifecycle::launch::persist_command_plan_state(
524
- agent,
525
- &spawn.plan,
526
- &spawn.profile_launch,
527
- );
529
+ crate::lifecycle::launch::persist_command_plan_state(agent, &spawn.plan, &spawn.profile_launch);
528
530
  persist_effective_approval_policy_for_restart(agent, safety);
529
531
  agent.remove("startup_prompts");
530
532
  agent.remove("startup_prompt_status");
@@ -583,8 +585,7 @@ fn write_restart_resume_decision_event(
583
585
 
584
586
  let path = workspace.join(".team").join("logs").join("events.jsonl");
585
587
  if let Some(parent) = path.parent() {
586
- std::fs::create_dir_all(parent)
587
- .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
588
+ std::fs::create_dir_all(parent).map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
588
589
  }
589
590
  let mut event = serde_json::json!({
590
591
  "ts": chrono::Utc::now().to_rfc3339(),
@@ -615,8 +616,8 @@ fn write_restart_resume_decision_event(
615
616
  }
616
617
  }
617
618
  }
618
- let line = serde_json::to_string(&event)
619
- .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
619
+ let line =
620
+ serde_json::to_string(&event).map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
620
621
  let mut file = std::fs::OpenOptions::new()
621
622
  .create(true)
622
623
  .append(true)
@@ -667,7 +668,10 @@ pub fn select_restart_state(
667
668
  .get("active_team_key")
668
669
  .and_then(serde_json::Value::as_str)
669
670
  .filter(|s| !s.is_empty())
670
- .map_or_else(|| crate::state::projection::team_state_key(&selected), str::to_string);
671
+ .map_or_else(
672
+ || crate::state::projection::team_state_key(&selected),
673
+ str::to_string,
674
+ );
671
675
  Ok(restart_candidate_from_state(workspace, &key, &selected))
672
676
  }
673
677
 
@@ -733,12 +737,14 @@ fn restart_candidate_has_context(state: &serde_json::Value) -> bool {
733
737
  .and_then(serde_json::Value::as_object)
734
738
  .is_some_and(|agents| {
735
739
  agents.values().any(|agent| {
736
- ["session_id", "rollout_path", "first_send_at"].iter().any(|key| {
737
- agent
738
- .get(*key)
739
- .and_then(serde_json::Value::as_str)
740
- .is_some_and(|s| !s.is_empty())
741
- })
740
+ ["session_id", "rollout_path", "first_send_at"]
741
+ .iter()
742
+ .any(|key| {
743
+ agent
744
+ .get(*key)
745
+ .and_then(serde_json::Value::as_str)
746
+ .is_some_and(|s| !s.is_empty())
747
+ })
742
748
  })
743
749
  })
744
750
  }
@@ -90,7 +90,7 @@ fn lanea_stop_agent_unknown_agent_is_unknown_worker_not_owner_refused() {
90
90
  #[test]
91
91
  fn lanea_fork_agent_unknown_source_is_unknown_worker_before_session_check() {
92
92
  let ws = lanea_team_ws("stopped");
93
- let text = format!("{:?}", fork_agent(&ws, &aid("ghost"), &aid("newfork"), false, None));
93
+ let text = format!("{:?}", fork_agent(&ws, &aid("ghost"), &aid("newfork"), None, false, None));
94
94
  assert!(
95
95
  text.contains("unknown worker"),
96
96
  "fork_agent must reject an UNKNOWN source as 'unknown worker agent id: ghost' BEFORE the session-id \
@@ -105,7 +105,7 @@ fn lanea_fork_agent_unknown_source_is_unknown_worker_before_session_check() {
105
105
  fn lanea_fork_agent_duplicate_target_is_already_exists_before_session_check() {
106
106
  let ws = lanea_team_ws("stopped");
107
107
  // target 'bravo' already exists in the spec -> duplicate; source 'alpha' exists (its session_id is irrelevant).
108
- let text = format!("{:?}", fork_agent(&ws, &aid("alpha"), &aid("bravo"), false, None));
108
+ let text = format!("{:?}", fork_agent(&ws, &aid("alpha"), &aid("bravo"), None, false, None));
109
109
  assert!(
110
110
  text.contains("already exists"),
111
111
  "fork_agent must reject a DUPLICATE target 'bravo' as 'agent id already exists' BEFORE the session-id \
@@ -185,14 +185,14 @@ fn plan_condition_rejects_out_of_grammar() {
185
185
 
186
186
  // ───────────────────────────────────────────────────────────────────────
187
187
  // resolve_display_backend — display/backend.py
188
- // 默认 adaptive;非默认非静默(non_default=true 触发 display.backend_resolved)。
188
+ // 默认 none;非默认非静默(non_default=true 触发 display.backend_resolved)。
189
189
  // ───────────────────────────────────────────────────────────────────────
190
190
 
191
191
  #[test]
192
- fn resolve_backend_defaults_to_adaptive_when_none_requested() {
192
+ fn resolve_backend_defaults_to_none_when_none_requested() {
193
193
  let r = resolve_display_backend(None, None);
194
- assert_eq!(r.backend, DisplayBackend::Adaptive);
195
- assert!(!r.non_default, "默认 adaptive 不应标记 non_default");
194
+ assert_eq!(r.backend, DisplayBackend::None);
195
+ assert!(!r.non_default, "默认 none 不应标记 non_default");
196
196
  }
197
197
 
198
198
  #[test]
@@ -841,7 +841,7 @@ fn close_team_display_empty_workspace_closes_nothing_not_error() {
841
841
  #[test]
842
842
  fn fork_agent_on_unowned_workspace_does_not_silently_fork() {
843
843
  let ws = temp_ws();
844
- match fork_agent(&ws, &aid("src"), &aid("dst"), false, None) {
844
+ match fork_agent(&ws, &aid("src"), &aid("dst"), None, false, None) {
845
845
  // 允许:owner 门 / team 选择 / 缺 spec(provider 命令构造前的上游门)。
846
846
  Err(LifecycleError::OwnerRefused(_))
847
847
  | Err(LifecycleError::TeamSelect(_))
@@ -464,7 +464,7 @@ fn lanea_remove_rollback_restores_via_spec_state_file_path() {
464
464
  fn lanea_fork_dup_target_leader_id_is_already_exists() {
465
465
  let ws = fork_ws(DELEG_ROLE_ALPHA);
466
466
  let tx = LaneTransport::new("team-laneateam", &[]);
467
- let text = format!("{:?}", fork_agent_with_transport(&ws, &aid("alpha"), &aid("leader"), false, None, &tx));
467
+ let text = format!("{:?}", fork_agent_with_transport(&ws, &aid("alpha"), &aid("leader"), None, false, None, &tx));
468
468
  assert!(
469
469
  text.contains("already exists"),
470
470
  "golden operations.py:301-302 (_find_agent matches the leader): forking ONTO the leader id must raise \
@@ -481,7 +481,7 @@ fn lanea_fork_dup_target_leader_id_is_already_exists() {
481
481
  fn lanea_fork_window_already_exists_guard_before_spec_mutation() {
482
482
  let ws = fork_ws(DELEG_ROLE_ALPHA);
483
483
  let tx = LaneTransport::new("team-laneateam", &["newfork"]); // the target window already exists
484
- let text = format!("{:?}", fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), false, None, &tx));
484
+ let text = format!("{:?}", fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), None, false, None, &tx));
485
485
  assert!(
486
486
  text.contains("tmux window already exists for fork target: team-laneateam:newfork"),
487
487
  "golden operations.py:310-312: a pre-existing target window must raise 'tmux window already exists for \
@@ -505,7 +505,7 @@ fn lanea_fork_window_already_exists_guard_before_spec_mutation() {
505
505
  fn lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm() {
506
506
  let ws = fork_ws(DELEG_ROLE_ALPHA_COMPAT); // source alpha auth_mode=compatible_api -> native fork unsupported
507
507
  let tx = LaneTransport::new("team-laneateam", &[]);
508
- let result = fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), false, None, &tx);
508
+ let result = fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), None, false, None, &tx);
509
509
  let text = format!("{result:?}");
510
510
  assert!(
511
511
  text.contains("codex does not support native session fork"),
@@ -530,7 +530,7 @@ fn lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm() {
530
530
  fn lanea_fork_report_session_id_is_not_pane_id() {
531
531
  let ws = fork_ws(DELEG_ROLE_ALPHA); // codex+subscription -> native fork supported -> full success path
532
532
  let tx = LaneTransport::new("team-laneateam", &[]);
533
- let report = fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), false, None, &tx).expect("fork ok (codex subscription supports fork)");
533
+ let report = fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), None, false, None, &tx).expect("fork ok (codex subscription supports fork)");
534
534
  assert_ne!(
535
535
  report.session_id,
536
536
  Some(crate::provider::SessionId::new("%newfork")),
@@ -226,6 +226,7 @@ fn fork_agent_proceeds_past_owner_gate_for_unowned_workspace() {
226
226
  &ws,
227
227
  &AgentId::new("w1"),
228
228
  &AgentId::new("w1-fork"),
229
+ None,
229
230
  false,
230
231
  None,
231
232
  &transport,
@@ -886,8 +887,8 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
886
887
  );
887
888
  assert_eq!(
888
889
  state["display_backend"],
889
- json!("adaptive"),
890
- "golden resolve_display_backend(None, source='launch') defaults to adaptive"
890
+ json!("none"),
891
+ "golden resolve_display_backend(None, source='launch') defaults to none"
891
892
  );
892
893
  }
893
894
 
@@ -960,7 +961,8 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
960
961
  assert!(agent["captured_at"].is_null());
961
962
  assert!(agent["captured_via"].is_null());
962
963
  assert!(agent["attribution_confidence"].is_null());
963
- assert_eq!(agent["spawn_cwd"], json!(team.to_string_lossy()));
964
+ // D5 (#264) / Python launch/core.py:253 — fresh launch persists spawn_cwd=workspace.
965
+ assert_eq!(agent["spawn_cwd"], json!(workspace.to_string_lossy()));
964
966
  assert_eq!(agent["spawned_at"], json!(FIXED_SPAWNED_AT));
965
967
  assert_eq!(
966
968
  agent["pane_id"],
@@ -483,6 +483,8 @@ pub enum QuickStartReport {
483
483
  session_name: SessionName,
484
484
  launch: Box<LaunchReport>,
485
485
  next_actions: Vec<String>,
486
+ attach_commands: Vec<String>,
487
+ display_backend: String,
486
488
  /// BUG-7: real readiness verdict. `Ready` ⇒ the wrapper completed AND the
487
489
  /// caller already verified tool-set availability; the framework itself
488
490
  /// never emits this without an external observable confirming worker
@@ -613,6 +615,8 @@ pub enum RestartReport {
613
615
  session_name: SessionName,
614
616
  agents: Vec<RestartedAgent>,
615
617
  coordinator_started: bool,
618
+ next_actions: Vec<String>,
619
+ attach_commands: Vec<String>,
616
620
  },
617
621
  /// atomic refusal(`reason=resume_atomicity`):某 interacted worker 不可 resume
618
622
  /// 且非 allow_fresh。**nothing created yet**,无需回滚。