@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
@@ -45,7 +45,7 @@ use super::*;
45
45
  /// params:
46
46
  /// spec_path: spec 路径,team 目录优先取 state 里的 team_dir
47
47
  /// session_name: 目标 tmux session
48
- /// returns: 已起席位清单,含目标 pane、启动模式、布局与显示信息;spawn 后 pane 已死的席位被跳过
48
+ /// returns: legacy 启动收据与独立 contract 已提交席位 ID;不为私有 socket 伪造 legacy pane
49
49
  /// errors: 命令拼装或 transport spawn 失败时返回 LifecycleError
50
50
  /// ---
51
51
  pub(super) fn spawn_agents(
@@ -54,7 +54,7 @@ pub(super) fn spawn_agents(
54
54
  spec: &Value,
55
55
  session_name: &SessionName,
56
56
  transport: &dyn Transport,
57
- ) -> Result<Vec<StartedAgent>, LifecycleError> {
57
+ ) -> Result<(Vec<StartedAgent>, Vec<AgentId>), LifecycleError> {
58
58
  // E5 解耦:team_dir(角色定义 + profiles 所在)≠ spec_path.parent()(spec 已迁出到 .team/runtime)。
59
59
  // 优先取 state.team_dir(角色目录),回落 spec_path.parent()(legacy 同目录布局)。
60
60
  let team_dir_buf = crate::state::persist::load_runtime_state(workspace)
@@ -74,6 +74,10 @@ pub(super) fn spawn_agents(
74
74
  Some(Value::Bool(true))
75
75
  );
76
76
  let mut started = Vec::new();
77
+ #[cfg(unix)]
78
+ let mut contract_started = Vec::new();
79
+ #[cfg(not(unix))]
80
+ let contract_started = Vec::new();
77
81
  for agent in spec_agent_values(spec) {
78
82
  let Some(agent_id_raw) = agent.get("id").and_then(Value::as_str) else {
79
83
  continue;
@@ -81,6 +85,26 @@ pub(super) fn spawn_agents(
81
85
  if agent_is_paused(agent) {
82
86
  continue;
83
87
  }
88
+ #[cfg(unix)]
89
+ if agent
90
+ .get("provider")
91
+ .and_then(Value::as_str)
92
+ .is_some_and(|provider| {
93
+ crate::contract_runtime::registry::descriptor(provider).is_some()
94
+ })
95
+ {
96
+ let team = runtime_team_key_for_spec(spec_path, spec, session_name);
97
+ crate::contract_runtime::framework::start_role(
98
+ workspace,
99
+ team_dir,
100
+ &team,
101
+ agent_id_raw,
102
+ )?;
103
+ contract_started.push(AgentId::new(agent_id_raw));
104
+ // No legacy target is manufactured for the private native socket.
105
+ // persist_spawn_agent_state projects the actual K3 seat separately.
106
+ continue;
107
+ }
84
108
  let agent_id = AgentId::new(agent_id_raw);
85
109
  let provider = agent
86
110
  .get("provider")
@@ -99,11 +123,12 @@ pub(super) fn spawn_agents(
99
123
  // has both the role instruction AND the callable Team Agent MCP capability.
100
124
  // probe5 RED proved that `build_command(.., None, None, ..)` left the worker
101
125
  // without `report_result`; placeholders are substituted at spawn time.
102
- let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
103
- agent,
104
- Some(agent_id_raw),
105
- provider,
106
- )?;
126
+ let command_agent =
127
+ crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
128
+ agent,
129
+ Some(agent_id_raw),
130
+ provider,
131
+ )?;
107
132
  let system_prompt =
108
133
  crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
109
134
  // 0.5.66 bypass 单源:per-agent safety 只服务 env 注入与审计,不参与 argv 决策。
@@ -431,7 +456,7 @@ pub(super) fn spawn_agents(
431
456
  managed_mcp_config: plan.managed_mcp_config || profile_launch.managed_mcp_config,
432
457
  });
433
458
  }
434
- Ok(started)
459
+ Ok((started, contract_started))
435
460
  }
436
461
 
437
462
  /// ---
@@ -451,7 +476,11 @@ pub(crate) fn agent_id_spec_source_path(workspace: &Path, agent_id: &str) -> Str
451
476
  });
452
477
  team_dir
453
478
  .map(|dir| dir.join("agents").join(format!("{agent_id}.md")))
454
- .unwrap_or_else(|| workspace.join(".team/current/agents").join(format!("{agent_id}.md")))
479
+ .unwrap_or_else(|| {
480
+ workspace
481
+ .join(".team/current/agents")
482
+ .join(format!("{agent_id}.md"))
483
+ })
455
484
  .display()
456
485
  .to_string()
457
486
  }
@@ -119,6 +119,23 @@ pub(super) fn persist_spawn_agent_state(
119
119
  let Some(id) = agent.get("id").and_then(Value::as_str) else {
120
120
  continue;
121
121
  };
122
+ #[cfg(unix)]
123
+ if agent
124
+ .get("provider")
125
+ .and_then(Value::as_str)
126
+ .is_some_and(|provider| {
127
+ crate::contract_runtime::registry::descriptor(provider).is_some()
128
+ })
129
+ {
130
+ let projected = if agent_is_paused(agent) {
131
+ serde_json::json!({"agent_id":id,"provider":"kiro","runtime_family":"contract","status":"paused","pane_id":null})
132
+ } else {
133
+ crate::contract_runtime::framework::project_seat(workspace, &team_id, id)
134
+ .map_err(|error| LifecycleError::StatePersist(error.to_string()))?
135
+ };
136
+ agents.insert(id.to_string(), projected);
137
+ continue;
138
+ }
122
139
  let provider = agent
123
140
  .get("provider")
124
141
  .and_then(Value::as_str)
@@ -133,7 +133,11 @@ pub fn launch_with_transport_in_workspace(
133
133
  // 所有角色都未声明 dangerously_skip_permissions=true → 写 warning 事件不阻塞。
134
134
  // TODO(0.6.0): remove(0.6.0 彻底删源头 A 后此警示无意义)。
135
135
  emit_dangerous_flag_no_role_declared_warning(workspace, &spec);
136
- phase_timer.emit(workspace, "launch.phase", "compile_spec");
136
+ phase_timer.emit(
137
+ workspace,
138
+ "launch.phase",
139
+ super::restart::LifecyclePhase::CompileSpec,
140
+ );
137
141
  let session_name = spec_session_name(&spec);
138
142
  if !dry_run && transport_has_session(transport, &session_name) {
139
143
  return Err(LifecycleError::SessionConflict(format!(
@@ -142,11 +146,16 @@ pub fn launch_with_transport_in_workspace(
142
146
  )));
143
147
  }
144
148
  let routes = spec_routes(&spec);
145
- let started = if dry_run {
146
- Vec::new()
149
+ let (started, contract_started) = if dry_run {
150
+ (Vec::new(), Vec::new())
147
151
  } else {
148
- phase_timer.emit(workspace, "launch.phase", "spawn_all");
149
- let started = spawn_agents(workspace, spec_path, &spec, &session_name, transport)?;
152
+ phase_timer.emit(
153
+ workspace,
154
+ "launch.phase",
155
+ super::restart::LifecyclePhase::SpawnAll,
156
+ );
157
+ let (started, contract_started) =
158
+ spawn_agents(workspace, spec_path, &spec, &session_name, transport)?;
150
159
  persist_spawn_agent_state(
151
160
  workspace,
152
161
  spec_path,
@@ -164,14 +173,16 @@ pub fn launch_with_transport_in_workspace(
164
173
  .first()
165
174
  .map(|agent| agent.spawned_at.as_str())
166
175
  .unwrap_or(session_name.as_str());
167
- transport
168
- .set_session_owner_with_generation(
169
- &session_name,
170
- workspace,
171
- &owner_team,
172
- generation,
173
- )
174
- .map_err(|error| LifecycleError::Transport(error.to_string()))?;
176
+ if !started.is_empty() {
177
+ transport
178
+ .set_session_owner_with_generation(
179
+ &session_name,
180
+ workspace,
181
+ &owner_team,
182
+ generation,
183
+ )
184
+ .map_err(|error| LifecycleError::Transport(error.to_string()))?;
185
+ }
175
186
  // 0.5.38: per-worker timing tags (source="launch") so operators can
176
187
  // trace which worker's spawn dominates wall time. Zeros for now on
177
188
  // the sub-timings — Step 1 first enables shape assertion; a later
@@ -208,7 +219,7 @@ pub fn launch_with_transport_in_workspace(
208
219
  "launch",
209
220
  );
210
221
  }
211
- started
222
+ (started, contract_started)
212
223
  };
213
224
  // 0.3.28 Step 1: topology invariant guard (warn-only during migration).
214
225
  // Logs each violation to stderr; never panics. Promoted to hard error at
@@ -223,6 +234,7 @@ pub fn launch_with_transport_in_workspace(
223
234
  Ok(LaunchReport {
224
235
  session_name,
225
236
  started,
237
+ contract_started,
226
238
  dry_run,
227
239
  tmux_endpoint: transport.tmux_endpoint(),
228
240
  routes,
@@ -348,13 +360,13 @@ pub use cursor_mcp_iso::{
348
360
  materialize_cursor_mcp_project,
349
361
  };
350
362
  mod cursor_mcp;
363
+ pub(crate) use cursor_mcp::apply_cursor_mcp_enable_profile_env;
351
364
  pub use cursor_mcp::{
352
365
  apply_cursor_mcp_overlay, apply_cursor_spawn_workspace_pointers,
353
366
  apply_cursor_workspace_physical_path, cursor_mcp_enable_argv, cursor_mcp_enable_working_dir,
354
367
  enable_cursor_workspace_mcp, enable_cursor_workspace_mcp_with_profile, physical_workspace_path,
355
368
  prepare_cursor_seat_mcp, refuse_second_cursor_occupant,
356
369
  };
357
- pub(crate) use cursor_mcp::apply_cursor_mcp_enable_profile_env;
358
370
 
359
371
  mod cursor_create_chat;
360
372
 
@@ -363,9 +375,9 @@ pub(super) use worker_env::*;
363
375
  pub(crate) use worker_env::{
364
376
  apply_copilot_instructions_overlay, apply_cursor_agent_rules_overlay,
365
377
  apply_cursor_subscription_proxy_env_with_profile, apply_mcp_auto_approval_env,
366
- apply_profile_launch_env, auth_mode_env_value,
367
- fill_spawn_placeholders, fill_spawn_placeholders_full, inherited_env_with_team_overrides,
368
- persist_command_plan_state, spawn_timestamp,
378
+ apply_profile_launch_env, auth_mode_env_value, fill_spawn_placeholders,
379
+ fill_spawn_placeholders_full, inherited_env_with_team_overrides, persist_command_plan_state,
380
+ spawn_timestamp,
369
381
  };
370
382
  pub use worker_env::{apply_cursor_subscription_proxy_env, CursorProxyPresence};
371
383
 
@@ -393,18 +405,18 @@ pub(crate) use quick_start_transport::{
393
405
  };
394
406
 
395
407
  pub mod readiness;
408
+ pub(super) use readiness::*;
396
409
  pub use readiness::{
397
410
  classify_leader_binding, launched_team_receiver_is_attached, selected_team_leader_receiver,
398
411
  LeaderBindingClass,
399
412
  };
400
- pub(super) use readiness::*;
401
413
 
402
414
  mod add_agent;
403
415
  pub(super) use add_agent::*;
404
416
  pub use add_agent::{add_agent, add_agent_force};
405
417
  pub(crate) use add_agent::{
406
- add_agent_with_transport, add_agent_with_transport_force,
407
- add_agent_with_transport_at_paths_locked, force_recreate_with_transport_locked,
418
+ add_agent_with_transport, add_agent_with_transport_at_paths_locked,
419
+ add_agent_with_transport_force, force_recreate_with_transport_locked,
408
420
  };
409
421
 
410
422
  pub(crate) mod add_agent_state;
@@ -428,21 +440,20 @@ pub(super) use ownership::*;
428
440
  pub(crate) use ownership::{ensure_owner_allowed, ensure_owner_allowed_for_state, state_path};
429
441
 
430
442
  pub mod spec_state;
431
- pub use spec_state::{
432
- seed_launched_owner_from_caller_with_provider_lookup, worker_session_name_pub,
433
- };
434
443
  pub(crate) use spec_state::{
435
444
  effective_runtime_config_for_worker_spawn, effective_runtime_config_for_worker_spawn_json,
436
445
  };
437
446
  use spec_state::{
438
- env_nonempty, has_positive_caller_leader_env, initial_runtime_state,
439
- override_spec_runtime_str, seed_launched_owner_from_env,
440
- spec_agent_values, spec_agents, spec_default_assignee, spec_routes, spec_session_name,
441
- spec_tasks_json, team_workspace, yaml_value_to_json,
447
+ env_nonempty, has_positive_caller_leader_env, initial_runtime_state, override_spec_runtime_str,
448
+ seed_launched_owner_from_env, spec_agent_values, spec_agents, spec_default_assignee,
449
+ spec_routes, spec_session_name, spec_tasks_json, team_workspace, yaml_value_to_json,
442
450
  };
443
451
  pub(crate) use spec_state::{
444
452
  override_spec_session_name, override_spec_workspace, spec_agent_id_set, write_spec_atomic,
445
453
  };
454
+ pub use spec_state::{
455
+ seed_launched_owner_from_caller_with_provider_lookup, worker_session_name_pub,
456
+ };
446
457
 
447
458
  #[cfg(test)]
448
459
  mod tests {
@@ -31,6 +31,7 @@ use crate::lifecycle::LifecycleError;
31
31
  use crate::model::enums::{AuthMode, Provider};
32
32
  use crate::model::yaml::Value as YamlValue;
33
33
  use crate::provider::{McpConfig, ProviderCommandOverrides, ProviderProfileLaunch};
34
+ use crate::provider::command_helpers::append_env_pair;
34
35
 
35
36
  const COMPATIBLE_NETWORK_ENV_KEYS: &[&str] = &[
36
37
  "HTTPS_PROXY",
@@ -463,40 +464,26 @@ fn provider_env_exports(
463
464
  }
464
465
  match provider {
465
466
  Provider::Claude | Provider::ClaudeCode => {
466
- if let Some(value) = value_or_alternate(values, "ANTHROPIC_BASE_URL", "BASE_URL") {
467
- exports.insert("ANTHROPIC_BASE_URL".to_string(), value.to_string());
468
- }
467
+ append_env_pair(&mut exports, "ANTHROPIC_BASE_URL", value_or_alternate(values, "ANTHROPIC_BASE_URL", "BASE_URL"));
469
468
  if auth_mode == AuthMode::OfficialApi {
470
- if let Some(value) = value_or_alternate(values, "ANTHROPIC_API_KEY", "API_KEY") {
471
- exports.insert("ANTHROPIC_API_KEY".to_string(), value.to_string());
472
- }
469
+ append_env_pair(&mut exports, "ANTHROPIC_API_KEY", value_or_alternate(values, "ANTHROPIC_API_KEY", "API_KEY"));
473
470
  }
474
- if let Some(value) = value_or_alternate(values, "ANTHROPIC_AUTH_TOKEN", "AUTH_TOKEN")
471
+ append_env_pair(&mut exports, "ANTHROPIC_AUTH_TOKEN", value_or_alternate(values, "ANTHROPIC_AUTH_TOKEN", "AUTH_TOKEN")
475
472
  .or_else(|| {
476
473
  (auth_mode == AuthMode::CompatibleApi)
477
474
  .then(|| value_or_alternate(values, "ANTHROPIC_API_KEY", "API_KEY"))
478
475
  .flatten()
479
- })
480
- {
481
- exports.insert("ANTHROPIC_AUTH_TOKEN".to_string(), value.to_string());
482
- }
483
- if let Some(value) = profile_model(values) {
484
- exports.insert("ANTHROPIC_MODEL".to_string(), value.to_string());
485
- }
476
+ }));
477
+ append_env_pair(&mut exports, "ANTHROPIC_MODEL", profile_model(values));
486
478
  }
487
479
  Provider::Codex => {
488
- if let Some(value) = value_or_alternate(values, "OPENAI_API_KEY", "API_KEY") {
489
- exports.insert("TEAM_AGENT_PROVIDER_API_KEY".to_string(), value.to_string());
490
- exports.insert("OPENAI_API_KEY".to_string(), value.to_string());
491
- }
492
- if let Some(value) = values.get("BASE_URL").filter(|value| !value.is_empty()) {
493
- exports.insert("OPENAI_BASE_URL".to_string(), value.to_string());
494
- }
480
+ let key = value_or_alternate(values, "OPENAI_API_KEY", "API_KEY");
481
+ append_env_pair(&mut exports, "TEAM_AGENT_PROVIDER_API_KEY", key);
482
+ append_env_pair(&mut exports, "OPENAI_API_KEY", key);
483
+ append_env_pair(&mut exports, "OPENAI_BASE_URL", values.get("BASE_URL").filter(|value| !value.is_empty()).map(String::as_str));
495
484
  }
496
485
  Provider::GeminiCli => {
497
- if let Some(value) = value_or_alternate(values, "GEMINI_API_KEY", "API_KEY") {
498
- exports.insert("GEMINI_API_KEY".to_string(), value.to_string());
499
- }
486
+ append_env_pair(&mut exports, "GEMINI_API_KEY", value_or_alternate(values, "GEMINI_API_KEY", "API_KEY"));
500
487
  }
501
488
  // C-A-4 cr verdict v2 — copilot BYOK(== compatible_api 档,help-providers 原文
502
489
  // "GitHub authentication is not required" when COPILOT_PROVIDER_BASE_URL set):
@@ -506,28 +493,14 @@ fn provider_env_exports(
506
493
  // Subscription/OfficialApi 不导出 COPILOT_PROVIDER_*(避免误改 auth 通道)。
507
494
  Provider::Copilot => {
508
495
  if auth_mode == AuthMode::CompatibleApi {
509
- if let Some(value) =
510
- value_or_alternate(values, "COPILOT_PROVIDER_BASE_URL", "BASE_URL")
511
- {
512
- exports.insert("COPILOT_PROVIDER_BASE_URL".to_string(), value.to_string());
513
- }
514
- if let Some(value) =
515
- value_or_alternate(values, "COPILOT_PROVIDER_TYPE", "PROVIDER_TYPE")
516
- {
517
- exports.insert("COPILOT_PROVIDER_TYPE".to_string(), value.to_string());
518
- }
519
- if let Some(value) =
520
- value_or_alternate(values, "COPILOT_PROVIDER_API_KEY", "API_KEY")
521
- {
522
- exports.insert("COPILOT_PROVIDER_API_KEY".to_string(), value.to_string());
523
- }
524
- if let Some(value) =
525
- value_or_alternate(values, "COPILOT_PROVIDER_WIRE_API", "WIRE_API")
526
- {
527
- exports.insert("COPILOT_PROVIDER_WIRE_API".to_string(), value.to_string());
528
- }
529
- if let Some(value) = value_or_alternate(values, "COPILOT_MODEL", "MODEL") {
530
- exports.insert("COPILOT_MODEL".to_string(), value.to_string());
496
+ for (key, alternate) in [
497
+ ("COPILOT_PROVIDER_BASE_URL", "BASE_URL"),
498
+ ("COPILOT_PROVIDER_TYPE", "PROVIDER_TYPE"),
499
+ ("COPILOT_PROVIDER_API_KEY", "API_KEY"),
500
+ ("COPILOT_PROVIDER_WIRE_API", "WIRE_API"),
501
+ ("COPILOT_MODEL", "MODEL"),
502
+ ] {
503
+ append_env_pair(&mut exports, key, value_or_alternate(values, key, alternate));
531
504
  }
532
505
  }
533
506
  }
@@ -539,6 +512,45 @@ fn provider_env_exports(
539
512
  exports
540
513
  }
541
514
 
515
+ #[cfg(test)]
516
+ mod append_env_contract {
517
+ use super::*;
518
+
519
+ #[test]
520
+ fn profile_append_keeps_native_keys_alternates_and_auth_gates() {
521
+ let values: BTreeMap<String, String> = [
522
+ ("API_KEY", "dummy"), ("BASE_URL", " raw\nbase "),
523
+ ("AUTH_TOKEN", "dummy-token"), ("MODEL", " raw model "),
524
+ ("PROVIDER_TYPE", "custom"), ("WIRE_API", "responses"),
525
+ ("OPENAI_API_KEY", "native-dummy"),
526
+ ].into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
527
+ let map = |pairs: &[(&str, &str)]| -> BTreeMap<String, String> {
528
+ pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
529
+ };
530
+ for provider in [Provider::Claude, Provider::ClaudeCode] {
531
+ assert_eq!(provider_env_exports(provider, AuthMode::OfficialApi, &values), map(&[
532
+ ("ANTHROPIC_BASE_URL", " raw\nbase "), ("ANTHROPIC_API_KEY", "dummy"),
533
+ ("ANTHROPIC_AUTH_TOKEN", "dummy-token"), ("ANTHROPIC_MODEL", " raw model "),
534
+ ]));
535
+ }
536
+ assert_eq!(provider_env_exports(Provider::Codex, AuthMode::CompatibleApi, &values), map(&[
537
+ ("TEAM_AGENT_PROVIDER_API_KEY", "native-dummy"), ("OPENAI_API_KEY", "native-dummy"),
538
+ ("OPENAI_BASE_URL", " raw\nbase "),
539
+ ]));
540
+ assert_eq!(provider_env_exports(Provider::GeminiCli, AuthMode::OfficialApi, &values), map(&[("GEMINI_API_KEY", "dummy")]));
541
+ assert_eq!(provider_env_exports(Provider::Copilot, AuthMode::CompatibleApi, &values), map(&[
542
+ ("COPILOT_PROVIDER_BASE_URL", " raw\nbase "), ("COPILOT_PROVIDER_TYPE", "custom"),
543
+ ("COPILOT_PROVIDER_API_KEY", "dummy"), ("COPILOT_PROVIDER_WIRE_API", "responses"),
544
+ ("COPILOT_MODEL", " raw model "),
545
+ ]));
546
+ assert!(provider_env_exports(Provider::Copilot, AuthMode::OfficialApi, &values).is_empty());
547
+ for provider in crate::provider::wire::all_providers() {
548
+ assert!(provider_env_exports(*provider, AuthMode::Subscription, &values).is_empty());
549
+ assert!(provider_env_exports(*provider, AuthMode::CompatibleApi, &BTreeMap::new()).is_empty());
550
+ }
551
+ }
552
+ }
553
+
542
554
  /// ---
543
555
  /// purpose: 给出该 provider 与 auth_mode 下必须从子进程环境里清掉的变量名集
544
556
  /// returns: 变量名集合;claude 系清掉会让子进程并入父会话的那组标识
@@ -41,6 +41,71 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
41
41
  // `pane_verify_ms`, `startup_prompt_handler_ms`, `tmux_start_mode`) so
42
42
  // bounded-concurrency spawn can be justified with real numbers.
43
43
 
44
+ /// Stable wire labels for launch/restart timing events, not readiness states.
45
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
46
+ pub(crate) enum LifecyclePhase {
47
+ ResolveContext,
48
+ CompileSpec,
49
+ PlanClassification,
50
+ Teardown,
51
+ SpawnAll,
52
+ SaveState,
53
+ CoordinatorStart,
54
+ ReadinessWait,
55
+ Completed,
56
+ }
57
+
58
+ impl LifecyclePhase {
59
+ pub(crate) fn as_str(self) -> &'static str {
60
+ match self {
61
+ Self::ResolveContext => "resolve_context",
62
+ Self::CompileSpec => "compile_spec",
63
+ Self::PlanClassification => "plan_classification",
64
+ Self::Teardown => "teardown",
65
+ Self::SpawnAll => "spawn_all",
66
+ Self::SaveState => "save_state",
67
+ Self::CoordinatorStart => "coordinator_start",
68
+ Self::ReadinessWait => "readiness_wait",
69
+ Self::Completed => "completed",
70
+ }
71
+ }
72
+
73
+ /// Pure event formatting shared by launch/restart; no clock, I/O or readiness policy.
74
+ pub(crate) fn event_fields(self, elapsed_ms: u64) -> serde_json::Value {
75
+ serde_json::json!({
76
+ "phase": self.as_str(),
77
+ "elapsed_ms": elapsed_ms,
78
+ })
79
+ }
80
+ }
81
+
82
+ #[test]
83
+ fn lifecycle_phase_wire_labels_remain_stable() {
84
+ for (phase, wire) in [
85
+ (LifecyclePhase::ResolveContext, "resolve_context"),
86
+ (LifecyclePhase::CompileSpec, "compile_spec"),
87
+ (LifecyclePhase::PlanClassification, "plan_classification"),
88
+ (LifecyclePhase::Teardown, "teardown"),
89
+ (LifecyclePhase::SpawnAll, "spawn_all"),
90
+ (LifecyclePhase::SaveState, "save_state"),
91
+ (LifecyclePhase::CoordinatorStart, "coordinator_start"),
92
+ (LifecyclePhase::ReadinessWait, "readiness_wait"),
93
+ (LifecyclePhase::Completed, "completed"),
94
+ ] {
95
+ assert_eq!(phase.as_str(), wire);
96
+ for elapsed_ms in [0, 123, u64::MAX] {
97
+ assert_eq!(phase.event_fields(elapsed_ms), serde_json::json!({
98
+ "phase": wire,
99
+ "elapsed_ms": elapsed_ms,
100
+ }));
101
+ assert_eq!(
102
+ serde_json::to_string(&phase.event_fields(elapsed_ms)).expect("phase JSON"),
103
+ format!("{{\"phase\":\"{wire}\",\"elapsed_ms\":{elapsed_ms}}}"),
104
+ );
105
+ }
106
+ }
107
+ }
108
+
44
109
  pub(crate) struct RestartPhaseTimer {
45
110
  started_at: std::time::Instant,
46
111
  }
@@ -71,15 +136,9 @@ impl RestartPhaseTimer {
71
136
  /// phase: 阶段名
72
137
  /// returns: 写事件失败被吞掉,不影响主流程
73
138
  /// ---
74
- pub(crate) fn emit(&self, workspace: &Path, kind: &'static str, phase: &'static str) {
139
+ pub(crate) fn emit(&self, workspace: &Path, kind: &'static str, phase: LifecyclePhase) {
75
140
  let event_log = crate::event_log::EventLog::new(workspace);
76
- let _ = event_log.write(
77
- kind,
78
- serde_json::json!({
79
- "phase": phase,
80
- "elapsed_ms": self.elapsed_ms(),
81
- }),
82
- );
141
+ let _ = event_log.write(kind, phase.event_fields(self.elapsed_ms()));
83
142
  }
84
143
  }
85
144
 
@@ -305,7 +364,7 @@ fn restart_with_selected_team_and_transport(
305
364
  // resolved a context; downstream phases are emitted with monotonic
306
365
  // `elapsed_ms` for at-a-glance latency triage.
307
366
  let phase_timer = RestartPhaseTimer::start();
308
- phase_timer.emit(&selected.run_workspace, "restart.phase", "resolve_context");
367
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::ResolveContext);
309
368
  let lifecycle_lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
310
369
  workspace: &selected.run_workspace,
311
370
  operation: "restart",
@@ -345,7 +404,7 @@ fn restart_with_selected_team_and_transport(
345
404
  // 写 runtime spec。角色缺(TEAM.md/agents 不在)→ 显式拒(列缺哪些),旧 spec 原地保留不删不用。
346
405
  let spec =
347
406
  rebuild_runtime_spec_from_roles(&selected.run_workspace, &selected.team_key, &state)?;
348
- phase_timer.emit(&selected.run_workspace, "restart.phase", "compile_spec");
407
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::CompileSpec);
349
408
  // 重建后 spec_workspace 恒为 runtime spec 的父目录(.team/runtime/<team_key>/)。
350
409
  let runtime_spec =
351
410
  crate::model::paths::runtime_spec_path(&selected.run_workspace, &selected.team_key);
@@ -441,7 +500,7 @@ fn restart_with_selected_team_and_transport(
441
500
  phase_timer.emit(
442
501
  &selected.run_workspace,
443
502
  "restart.phase",
444
- "plan_classification",
503
+ LifecyclePhase::PlanClassification,
445
504
  );
446
505
  write_restart_resume_decision_events(
447
506
  &selected.run_workspace,
@@ -558,8 +617,8 @@ fn restart_with_selected_team_and_transport(
558
617
  &topology_authority_agent_ids,
559
618
  )?;
560
619
  }
561
- phase_timer.emit(&selected.run_workspace, "restart.phase", "teardown");
562
- phase_timer.emit(&selected.run_workspace, "restart.phase", "spawn_all");
620
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::Teardown);
621
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::SpawnAll);
563
622
  // 0.5.40 Slice 3 (tmux-server-death-locate §7 Slice 3): when the
564
623
  // pre-spawn teardown was DEFERRED (live worker session with
565
624
  // authoritative running agents), snapshot the pre-spawn agent rows
@@ -582,8 +641,8 @@ fn restart_with_selected_team_and_transport(
582
641
  // tmux session is created deterministically; the remaining independent
583
642
  // decisions run their `transport.spawn_into` in parallel via a thread
584
643
  // scope with concurrency capped at `min(4, workers-1)`. Every spawn
585
- // result is collected in-memory; `verify_spawned_agent_live` +
586
- // `mark_agent_respawned` are then applied in ORIGINAL PLAN ORDER so
644
+ // result is collected in-memory; `mark_agent_respawned` is then
645
+ // applied in ORIGINAL PLAN ORDER so
587
646
  // persisted `spawn_epoch` / `spawned_at` / `pane_id` / `window` stay
588
647
  // deterministic regardless of thread completion order. Failure
589
648
  // aggregation stays equivalent to serial: per-agent errors go into
@@ -837,17 +896,14 @@ fn restart_with_selected_team_and_transport(
837
896
  agent.get("dangerously_skip_permissions"),
838
897
  Some(serde_json::Value::Bool(true))
839
898
  );
840
- if let Err(error) = verify_spawned_agent_live(&decision.agent_id, &spawn, transport)
841
- .and_then(|_| {
842
- mark_agent_respawned(
843
- &mut state,
844
- &decision.agent_id,
845
- decision.restart_mode,
846
- &spawn,
847
- transport,
848
- launched_bypass,
849
- )
850
- })
899
+ if let Err(error) = mark_agent_respawned(
900
+ &mut state,
901
+ &decision.agent_id,
902
+ decision.restart_mode,
903
+ &spawn,
904
+ transport,
905
+ launched_bypass,
906
+ )
851
907
  {
852
908
  let error = error.to_string();
853
909
  mark_agent_restart_failed(&mut state, decision, &error);
@@ -1026,7 +1082,7 @@ fn restart_with_selected_team_and_transport(
1026
1082
  &capture_backfill_skip_agent_ids,
1027
1083
  &topology_authority_agent_ids,
1028
1084
  )?;
1029
- phase_timer.emit(&selected.run_workspace, "restart.phase", "save_state");
1085
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::SaveState);
1030
1086
  if fatal_resume_failure {
1031
1087
  let attach_commands = Vec::new();
1032
1088
  let next_actions = restart_failure_next_actions(&failed_agents);
@@ -1304,10 +1360,10 @@ fn restart_with_selected_team_and_transport(
1304
1360
  phase_timer.emit(
1305
1361
  &selected.run_workspace,
1306
1362
  "restart.phase",
1307
- "coordinator_start",
1363
+ LifecyclePhase::CoordinatorStart,
1308
1364
  );
1309
1365
  let coordinator_started = coordinator.ok;
1310
- phase_timer.emit(&selected.run_workspace, "restart.phase", "readiness_wait");
1366
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::ReadinessWait);
1311
1367
  wait_restart_readiness_or_timeout(
1312
1368
  &selected.run_workspace,
1313
1369
  &state,
@@ -1331,7 +1387,7 @@ fn restart_with_selected_team_and_transport(
1331
1387
  let mut next_actions = Vec::new();
1332
1388
  if !failed_agents.is_empty() {
1333
1389
  next_actions.extend(restart_failure_next_actions(&failed_agents));
1334
- phase_timer.emit(&selected.run_workspace, "restart.phase", "completed");
1390
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::Completed);
1335
1391
  write_restart_completed_event(
1336
1392
  &selected.run_workspace,
1337
1393
  &successful_agents,
@@ -1360,7 +1416,7 @@ fn restart_with_selected_team_and_transport(
1360
1416
  leader_bind_reason,
1361
1417
  });
1362
1418
  }
1363
- phase_timer.emit(&selected.run_workspace, "restart.phase", "completed");
1419
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::Completed);
1364
1420
  write_restart_completed_event(
1365
1421
  &selected.run_workspace,
1366
1422
  &successful_agents,
@@ -1852,14 +1908,6 @@ fn yes_no(value: bool) -> &'static str {
1852
1908
  }
1853
1909
  }
1854
1910
 
1855
- fn verify_spawned_agent_live(
1856
- _agent_id: &AgentId,
1857
- _spawn: &SpawnedAgentWindow,
1858
- _transport: &dyn crate::transport::Transport,
1859
- ) -> Result<(), LifecycleError> {
1860
- Ok(())
1861
- }
1862
-
1863
1911
  /// 0.3.30 Bug 1: restart success path auto-attach.
1864
1912
  /// When `TMUX_PANE` is present in the caller's env, treat the restart as if
1865
1913
  /// the user had also run `attach-leader` from that pane. Mirrors quick-start's
@@ -3088,8 +3136,8 @@ fn run_bounded_parallel_worker_spawns(
3088
3136
  outcomes
3089
3137
  }
3090
3138
 
3091
- /// 0.5.38 Step 2: serial post-spawn stage. Runs verify_spawned_agent_live +
3092
- /// mark_agent_respawned in plan order, emits worker.spawn_timing, and
3139
+ /// 0.5.38 Step 2: serial post-spawn stage. Runs mark_agent_respawned
3140
+ /// in plan order, emits worker.spawn_timing, and
3093
3141
  /// updates the successful/failed agent lists exactly like the pre-0.5.38
3094
3142
  /// serial loop. Keeping this serial guarantees deterministic persisted
3095
3143
  /// state (`spawn_epoch`, `spawned_at`, etc.) regardless of parallel
@@ -3111,17 +3159,14 @@ fn apply_marked_respawn(
3111
3159
  launched_bypass: bool,
3112
3160
  ) {
3113
3161
  let verify_start = std::time::Instant::now();
3114
- if let Err(error) =
3115
- verify_spawned_agent_live(&decision.agent_id, spawn, transport).and_then(|_| {
3116
- mark_agent_respawned(
3117
- state,
3118
- &decision.agent_id,
3119
- decision.restart_mode,
3120
- spawn,
3121
- transport,
3122
- launched_bypass,
3123
- )
3124
- })
3162
+ if let Err(error) = mark_agent_respawned(
3163
+ state,
3164
+ &decision.agent_id,
3165
+ decision.restart_mode,
3166
+ spawn,
3167
+ transport,
3168
+ launched_bypass,
3169
+ )
3125
3170
  {
3126
3171
  let error = error.to_string();
3127
3172
  mark_agent_restart_failed(state, decision, &error);