@team-agent/installer 0.3.2 → 0.3.4

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 (82) hide show
  1. package/Cargo.lock +34 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -1
  4. package/crates/team-agent/src/cli/adapters.rs +196 -19
  5. package/crates/team-agent/src/cli/diagnose.rs +145 -11
  6. package/crates/team-agent/src/cli/emit.rs +287 -53
  7. package/crates/team-agent/src/cli/leader.rs +37 -8
  8. package/crates/team-agent/src/cli/mod.rs +807 -316
  9. package/crates/team-agent/src/cli/status_port.rs +25 -2
  10. package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
  11. package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
  12. package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
  13. package/crates/team-agent/src/cli/tests/run_delegation.rs +57 -3
  14. package/crates/team-agent/src/cli/types.rs +17 -0
  15. package/crates/team-agent/src/compiler/tests.rs +2 -2
  16. package/crates/team-agent/src/compiler.rs +16 -6
  17. package/crates/team-agent/src/coordinator/health.rs +89 -20
  18. package/crates/team-agent/src/coordinator/mod.rs +4 -0
  19. package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
  20. package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
  21. package/crates/team-agent/src/coordinator/tests/watch.rs +4 -2
  22. package/crates/team-agent/src/coordinator/tick.rs +222 -69
  23. package/crates/team-agent/src/coordinator/types.rs +15 -3
  24. package/crates/team-agent/src/db/schema.rs +37 -2
  25. package/crates/team-agent/src/diagnose/comms.rs +226 -0
  26. package/crates/team-agent/src/diagnose/mod.rs +45 -0
  27. package/crates/team-agent/src/diagnose/orphans.rs +658 -0
  28. package/crates/team-agent/src/fake_worker.rs +146 -3
  29. package/crates/team-agent/src/leader/start.rs +121 -23
  30. package/crates/team-agent/src/leader/types.rs +44 -1
  31. package/crates/team-agent/src/lib.rs +3 -0
  32. package/crates/team-agent/src/lifecycle/display.rs +648 -50
  33. package/crates/team-agent/src/lifecycle/launch.rs +1048 -264
  34. package/crates/team-agent/src/lifecycle/mod.rs +3 -0
  35. package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
  36. package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
  37. package/crates/team-agent/src/lifecycle/restart/agent.rs +113 -26
  38. package/crates/team-agent/src/lifecycle/restart/common.rs +189 -102
  39. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +465 -25
  40. package/crates/team-agent/src/lifecycle/restart/remove.rs +22 -6
  41. package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
  42. package/crates/team-agent/src/lifecycle/restart.rs +4 -1
  43. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -4
  44. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
  45. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +39 -9
  46. package/crates/team-agent/src/lifecycle/types.rs +23 -0
  47. package/crates/team-agent/src/lifecycle/worker_command_context.rs +326 -0
  48. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  49. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
  50. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
  51. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
  52. package/crates/team-agent/src/mcp_server/mod.rs +3 -74
  53. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
  54. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
  55. package/crates/team-agent/src/mcp_server/tools.rs +312 -111
  56. package/crates/team-agent/src/mcp_server/types.rs +6 -4
  57. package/crates/team-agent/src/mcp_server/wire.rs +19 -7
  58. package/crates/team-agent/src/message_store.rs +21 -4
  59. package/crates/team-agent/src/messaging/delivery.rs +87 -37
  60. package/crates/team-agent/src/messaging/mod.rs +9 -6
  61. package/crates/team-agent/src/messaging/results.rs +153 -16
  62. package/crates/team-agent/src/messaging/selftest.rs +199 -12
  63. package/crates/team-agent/src/messaging/send.rs +35 -3
  64. package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
  65. package/crates/team-agent/src/messaging/types.rs +11 -3
  66. package/crates/team-agent/src/os_probe.rs +119 -0
  67. package/crates/team-agent/src/packaging/migrate.rs +10 -2
  68. package/crates/team-agent/src/packaging/tests.rs +23 -0
  69. package/crates/team-agent/src/provider/adapter.rs +483 -67
  70. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
  71. package/crates/team-agent/src/provider/classify.rs +51 -4
  72. package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
  73. package/crates/team-agent/src/provider/types.rs +47 -0
  74. package/crates/team-agent/src/session_capture.rs +616 -0
  75. package/crates/team-agent/src/state/persist.rs +57 -0
  76. package/crates/team-agent/src/state/projection.rs +32 -23
  77. package/crates/team-agent/src/state/selector.rs +5 -2
  78. package/crates/team-agent/src/tmux_backend.rs +151 -60
  79. package/crates/team-agent/src/transport/test_support.rs +9 -0
  80. package/crates/team-agent/src/transport/tests/wire.rs +4 -0
  81. package/crates/team-agent/src/transport.rs +13 -2
  82. package/package.json +4 -4
@@ -72,9 +72,8 @@ pub fn launch_with_transport_in_workspace(
72
72
  spec_path.display()
73
73
  )));
74
74
  }
75
- let text = std::fs::read_to_string(spec_path).map_err(|e| {
76
- LifecycleError::Compile(format!("{}: {e}", spec_path.display()))
77
- })?;
75
+ let text = std::fs::read_to_string(spec_path)
76
+ .map_err(|e| LifecycleError::Compile(format!("{}: {e}", spec_path.display())))?;
78
77
  let spec = yaml::loads(&text).map_err(|e| LifecycleError::Compile(e.to_string()))?;
79
78
  let session_name = spec_session_name(&spec);
80
79
  let safety = effective_runtime_config(&spec)?;
@@ -101,8 +100,23 @@ pub fn launch_with_transport_in_workspace(
101
100
  let started = if dry_run {
102
101
  Vec::new()
103
102
  } else {
104
- let started = spawn_agents(workspace, spec_path, &spec, &session_name, &safety, transport)?;
105
- persist_spawn_agent_state(workspace, spec_path, &spec, &session_name, transport, &started)?;
103
+ let started = spawn_agents(
104
+ workspace,
105
+ spec_path,
106
+ &spec,
107
+ &session_name,
108
+ &safety,
109
+ transport,
110
+ )?;
111
+ persist_spawn_agent_state(
112
+ workspace,
113
+ spec_path,
114
+ &spec,
115
+ &session_name,
116
+ transport,
117
+ &started,
118
+ &safety,
119
+ )?;
106
120
  started
107
121
  };
108
122
  Ok(LaunchReport {
@@ -113,6 +127,7 @@ pub fn launch_with_transport_in_workspace(
113
127
  permissions,
114
128
  safety,
115
129
  leader_receiver_attached: false,
130
+ session_capture_incomplete_agents: Vec::new(),
116
131
  })
117
132
  }
118
133
 
@@ -151,9 +166,19 @@ fn spawn_agents(
151
166
  // has both the role instruction AND the callable Team Agent MCP capability.
152
167
  // probe5 RED proved that `build_command(.., None, None, ..)` left the worker
153
168
  // without `report_result`; placeholders are substituted at spawn time.
154
- let role = agent.get("role").and_then(Value::as_str);
155
- let tools = worker_tool_refs(agent_tool_strings(agent), safety);
156
- let tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
169
+ let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
170
+ agent,
171
+ Some(agent_id_raw),
172
+ provider,
173
+ );
174
+ let system_prompt =
175
+ crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
176
+ let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
177
+ &command_agent,
178
+ provider,
179
+ safety,
180
+ )?;
181
+ let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
157
182
  let mcp_team_id =
158
183
  runtime_active_team_key_for_spawn(workspace, spec_path, spec, session_name);
159
184
  let mcp_config = adapter
@@ -161,32 +186,38 @@ fn spawn_agents(
161
186
  .map_err(|e| LifecycleError::Provider(e.to_string()))?;
162
187
  let mcp_config = resolve_mcp_config(mcp_config, workspace, agent_id_raw, &mcp_team_id);
163
188
  let mcp_config_path = write_worker_mcp_config(workspace, agent_id_raw, &mcp_config)?;
164
- let mut argv = adapter
165
- .build_command_with_tools(
166
- auth_mode,
189
+ let profile_dir = team_dir.join("profiles");
190
+ let profile_launch =
191
+ crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
192
+ workspace,
193
+ agent_id_raw,
194
+ agent,
195
+ Some(&profile_dir),
167
196
  Some(&mcp_config),
168
- role,
169
- model,
170
- &tool_refs,
171
- )
197
+ )?;
198
+ let command_model = profile_launch.command_overrides.model.as_deref().or(model);
199
+ let mut plan = adapter
200
+ .build_command_plan(crate::provider::ProviderCommandContext {
201
+ auth_mode,
202
+ mcp_config: Some(&mcp_config),
203
+ system_prompt: Some(system_prompt.as_str()),
204
+ model: command_model,
205
+ tools: &resolved_tool_refs,
206
+ profile_launch: Some(&profile_launch),
207
+ })
172
208
  .map_err(|e| LifecycleError::Provider(e.to_string()))?;
173
- point_native_mcp_config_at_file(&mut argv, provider, &mcp_config_path);
174
- fill_spawn_placeholders_full(
175
- &mut argv,
176
- workspace,
177
- agent_id_raw,
178
- Some(&mcp_team_id),
179
- );
209
+ if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
210
+ point_native_mcp_config_at_file(&mut plan.argv, provider, &mcp_config_path);
211
+ }
212
+ fill_spawn_placeholders_full(&mut plan.argv, workspace, agent_id_raw, Some(&mcp_team_id));
180
213
  let window = WindowName::new(agent_id_raw);
181
- let env = inherited_env_with_team_overrides(
182
- workspace,
183
- agent_id_raw,
184
- Some(&mcp_team_id),
185
- );
214
+ let mut env =
215
+ inherited_env_with_team_overrides(workspace, agent_id_raw, Some(&mcp_team_id));
216
+ apply_profile_launch_env(&mut env, &profile_launch);
186
217
  let spawn = if started.is_empty() {
187
- transport.spawn_first(session_name, &window, &argv, team_dir, &env)
218
+ transport.spawn_first(session_name, &window, &plan.argv, team_dir, &env)
188
219
  } else {
189
- transport.spawn_into(session_name, &window, &argv, team_dir, &env)
220
+ transport.spawn_into(session_name, &window, &plan.argv, team_dir, &env)
190
221
  }
191
222
  .map_err(|e| LifecycleError::Transport(e.to_string()))?;
192
223
  let _ = adapter.handle_startup_prompts(
@@ -204,6 +235,13 @@ fn spawn_agents(
204
235
  target: spawn.pane_id.as_str().to_string(),
205
236
  session_id: None,
206
237
  rollout_path: None,
238
+ pending_session_id: plan.expected_session_id.clone(),
239
+ claude_config_dir: profile_launch.claude_config_dir.clone(),
240
+ provider_projects_root: plan
241
+ .provider_projects_root
242
+ .clone()
243
+ .or_else(|| profile_launch.claude_projects_root.clone()),
244
+ managed_mcp_config: plan.managed_mcp_config || profile_launch.managed_mcp_config,
207
245
  display: WorkerDisplay::Blocked {
208
246
  reason: AdaptiveBlockReason::NotImplementedThisPlatform,
209
247
  },
@@ -219,6 +257,7 @@ fn persist_spawn_agent_state(
219
257
  session_name: &SessionName,
220
258
  transport: &dyn Transport,
221
259
  started: &[StartedAgent],
260
+ safety: &DangerousApproval,
222
261
  ) -> Result<(), LifecycleError> {
223
262
  let state_path = crate::state::persist::runtime_state_path(workspace);
224
263
  let mut state = if state_path.exists() {
@@ -232,12 +271,7 @@ fn persist_spawn_agent_state(
232
271
  let team_id = explicit_active_team_key(&state)
233
272
  .unwrap_or_else(|| runtime_team_key_for_spec(spec_path, spec, session_name));
234
273
  let worker_tmux_socket = launched_worker_tmux_socket(transport, workspace);
235
- drop_worker_pane_seeded_owner(
236
- &mut state,
237
- &team_id,
238
- started,
239
- worker_tmux_socket.as_deref(),
240
- );
274
+ drop_worker_pane_seeded_owner(&mut state, &team_id, started, worker_tmux_socket.as_deref());
241
275
  // Only persist running state for agents whose spawn still has a live target.
242
276
  let live_windows: BTreeSet<String> = transport
243
277
  .list_windows(session_name)
@@ -250,8 +284,9 @@ fn persist_spawn_agent_state(
250
284
  .map(|agent| agent.agent_id.as_str().to_string())
251
285
  .collect();
252
286
  let pane_pids_by_agent = pane_pids_by_started_agent(transport, started);
287
+ let profile_dir = spec_path.parent().unwrap_or(workspace).join("profiles");
253
288
  let mut agents = serde_json::Map::new();
254
- let spawned_at = spawn_timestamp();
289
+ let mut spawn_index = 0_u32;
255
290
  for agent in spec_agent_values(spec) {
256
291
  let Some(id) = agent.get("id").and_then(Value::as_str) else {
257
292
  continue;
@@ -285,9 +320,25 @@ fn persist_spawn_agent_state(
285
320
  continue;
286
321
  }
287
322
  let pane_pid = pane_pids_by_agent.get(id).copied();
323
+ let spawned_at = spawn_timestamp_for_agent(spawn_index);
324
+ spawn_index = spawn_index.saturating_add(1);
325
+ let started_agent = started.iter().find(|agent| agent.agent_id.as_str() == id);
288
326
  agents.insert(
289
327
  id.to_string(),
290
- running_agent_state(agent, id, provider, workspace, &spawned_at, &team_id, pane_pid)?,
328
+ running_agent_state(
329
+ agent,
330
+ id,
331
+ provider,
332
+ workspace,
333
+ spec_path.parent().unwrap_or(workspace),
334
+ &spawned_at,
335
+ &team_id,
336
+ Some(agent_id_to_pane_id(started, id)),
337
+ pane_pid,
338
+ safety,
339
+ started_agent,
340
+ Some(&profile_dir),
341
+ )?,
291
342
  );
292
343
  }
293
344
  if let Some(obj) = state.as_object_mut() {
@@ -317,7 +368,18 @@ fn pane_pids_by_started_agent(
317
368
  .collect()
318
369
  }
319
370
 
320
- fn save_launched_team_state(workspace: &Path, launched: &serde_json::Value) -> Result<(), LifecycleError> {
371
+ fn agent_id_to_pane_id<'a>(started: &'a [StartedAgent], agent_id: &str) -> &'a str {
372
+ started
373
+ .iter()
374
+ .find(|agent| agent.agent_id.as_str() == agent_id)
375
+ .map(|agent| agent.target.as_str())
376
+ .unwrap_or("")
377
+ }
378
+
379
+ fn save_launched_team_state(
380
+ workspace: &Path,
381
+ launched: &serde_json::Value,
382
+ ) -> Result<(), LifecycleError> {
321
383
  save_launched_team_state_for_key(workspace, launched, None)
322
384
  }
323
385
 
@@ -340,6 +402,7 @@ fn save_launched_team_state_for_key(
340
402
  }
341
403
  promote_launched_binding_from_team_entry(&mut launched, &launched_key);
342
404
  drop_foreign_seeded_owner(&existing, &launched_key, &mut launched);
405
+ drop_bare_worker_seeded_owner(&mut launched, &launched_key);
343
406
  let merged = if team_key.is_some() {
344
407
  merge_workspace_team_state_with_key(&existing, &launched, &launched_key)
345
408
  } else {
@@ -347,7 +410,22 @@ fn save_launched_team_state_for_key(
347
410
  };
348
411
  let mut projected = crate::state::projection::project_top_level_view(&merged, &launched_key);
349
412
  drop_unbound_top_level_owner(&mut projected);
350
- save_runtime_state(workspace, &projected).map_err(|e| LifecycleError::StatePersist(e.to_string()))
413
+ save_runtime_state(workspace, &projected)
414
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))
415
+ }
416
+
417
+ fn drop_bare_worker_seeded_owner(launched: &mut serde_json::Value, launched_key: &str) {
418
+ if has_positive_caller_leader_env() {
419
+ return;
420
+ }
421
+ let pane = launched
422
+ .get("team_owner")
423
+ .and_then(|owner| owner.get("pane_id"))
424
+ .and_then(serde_json::Value::as_str)
425
+ .unwrap_or("");
426
+ if pane.ends_with("-first") {
427
+ seed_unbound_launched_owner(launched, launched_key);
428
+ }
351
429
  }
352
430
 
353
431
  fn merge_workspace_team_state_with_key(
@@ -435,7 +513,11 @@ fn drop_unbound_top_level_owner(state: &mut serde_json::Value) {
435
513
  }
436
514
  }
437
515
 
438
- fn drop_foreign_seeded_owner(existing: &serde_json::Value, launched_key: &str, launched: &mut serde_json::Value) {
516
+ fn drop_foreign_seeded_owner(
517
+ existing: &serde_json::Value,
518
+ launched_key: &str,
519
+ launched: &mut serde_json::Value,
520
+ ) {
439
521
  let Some(pane) = launched
440
522
  .get("team_owner")
441
523
  .and_then(|owner| owner.get("pane_id"))
@@ -445,7 +527,15 @@ fn drop_foreign_seeded_owner(existing: &serde_json::Value, launched_key: &str, l
445
527
  return;
446
528
  };
447
529
  if owner_pane_belongs_to_other_team(existing, launched_key, pane) {
448
- seed_unbound_launched_owner(launched, launched_key);
530
+ let replacement = unbound_launched_owner(launched, launched_key);
531
+ if let Some(obj) = launched.as_object_mut() {
532
+ if let Some(owner) = replacement {
533
+ obj.insert("team_owner".to_string(), owner);
534
+ } else {
535
+ obj.remove("team_owner");
536
+ }
537
+ obj.remove("owner_epoch");
538
+ }
449
539
  }
450
540
  }
451
541
 
@@ -469,27 +559,28 @@ fn drop_worker_pane_seeded_owner(
469
559
  let tmux_pane = std::env::var("TMUX_PANE")
470
560
  .ok()
471
561
  .filter(|value| !value.is_empty());
472
- let has_leader_identity_env = leader_pane.is_some()
473
- || env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID")
474
- || env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE")
475
- || env_nonempty("TEAM_AGENT_LEADER_PROVIDER")
476
- || env_nonempty("TEAM_AGENT_ID")
477
- || env_nonempty("TEAM_AGENT_TEAM_ID");
478
- let seeded_from_bare_tmux =
479
- !has_leader_identity_env && tmux_pane.as_deref() == Some(pane);
562
+ let has_leader_identity_env = has_positive_caller_leader_env();
563
+ let seeded_from_bare_tmux = !has_leader_identity_env && tmux_pane.as_deref() == Some(pane);
480
564
  let caller_tmux_socket = crate::tmux_backend::socket_name_from_tmux_env();
481
565
  if seeded_from_bare_tmux
482
- && tmux_sockets_match_or_unknown(caller_tmux_socket.as_deref(), worker_tmux_socket)
483
- && started.iter().any(|agent| agent.target == pane)
566
+ && (tmux_sockets_match_or_unknown(caller_tmux_socket.as_deref(), worker_tmux_socket)
567
+ || pane.ends_with("-first"))
568
+ && seeded_pane_looks_like_worker(pane, started)
484
569
  {
485
570
  seed_unbound_launched_owner(launched, launched_key);
486
571
  }
487
572
  }
488
573
 
489
- fn launched_worker_tmux_socket(
490
- transport: &dyn Transport,
491
- workspace: &Path,
492
- ) -> Option<String> {
574
+ fn seeded_pane_looks_like_worker(pane: &str, started: &[StartedAgent]) -> bool {
575
+ pane.ends_with("-first")
576
+ || started.iter().any(|agent| {
577
+ pane == agent.target
578
+ || pane.starts_with(agent.target.as_str())
579
+ || agent.target.starts_with(pane)
580
+ })
581
+ }
582
+
583
+ fn launched_worker_tmux_socket(transport: &dyn Transport, workspace: &Path) -> Option<String> {
493
584
  if matches!(transport.kind(), crate::transport::BackendKind::Tmux) {
494
585
  Some(crate::tmux_backend::socket_name_for_workspace(workspace))
495
586
  } else {
@@ -497,10 +588,7 @@ fn launched_worker_tmux_socket(
497
588
  }
498
589
  }
499
590
 
500
- fn tmux_sockets_match_or_unknown(
501
- caller_socket: Option<&str>,
502
- worker_socket: Option<&str>,
503
- ) -> bool {
591
+ fn tmux_sockets_match_or_unknown(caller_socket: Option<&str>, worker_socket: Option<&str>) -> bool {
504
592
  match (caller_socket, worker_socket) {
505
593
  (Some(caller), Some(worker)) => caller == worker,
506
594
  (Some(_), None) => false,
@@ -509,10 +597,41 @@ fn tmux_sockets_match_or_unknown(
509
597
  }
510
598
 
511
599
  fn env_nonempty(key: &str) -> bool {
512
- std::env::var(key).ok().is_some_and(|value| !value.is_empty())
600
+ std::env::var(key)
601
+ .ok()
602
+ .is_some_and(|value| !value.is_empty())
513
603
  }
514
604
 
515
605
  fn seed_unbound_launched_owner(launched: &mut serde_json::Value, launched_key: &str) {
606
+ let Some(owner) = unbound_launched_owner(launched, launched_key) else {
607
+ return;
608
+ };
609
+ let provider = launched
610
+ .get("team_owner")
611
+ .and_then(|owner| owner.get("provider"))
612
+ .and_then(serde_json::Value::as_str)
613
+ .filter(|provider| !provider.is_empty())
614
+ .unwrap_or("codex");
615
+ let owner_epoch = 1u64;
616
+ let receiver = serde_json::json!({
617
+ "mode": "direct_tmux",
618
+ "status": "unbound",
619
+ "provider": provider,
620
+ "leader_session_uuid": owner.get("leader_session_uuid").cloned().unwrap_or(serde_json::Value::Null),
621
+ "owner_epoch": owner_epoch,
622
+ "discovery": "quick_start",
623
+ });
624
+ if let Some(obj) = launched.as_object_mut() {
625
+ obj.insert("leader_receiver".to_string(), receiver);
626
+ obj.insert("team_owner".to_string(), owner);
627
+ obj.insert("owner_epoch".to_string(), serde_json::json!(owner_epoch));
628
+ }
629
+ }
630
+
631
+ fn unbound_launched_owner(
632
+ launched: &serde_json::Value,
633
+ launched_key: &str,
634
+ ) -> Option<serde_json::Value> {
516
635
  let provider = launched
517
636
  .get("team_owner")
518
637
  .and_then(|owner| owner.get("provider"))
@@ -531,43 +650,29 @@ fn seed_unbound_launched_owner(launched: &mut serde_json::Value, launched_key: &
531
650
  let os_user = std::env::var("USER")
532
651
  .or_else(|_| std::env::var("USERNAME"))
533
652
  .unwrap_or_default();
534
- let Ok(uuid) = crate::model::ids::LeaderSessionUuid::derive(
653
+ let uuid = crate::model::ids::LeaderSessionUuid::derive(
535
654
  machine_fingerprint,
536
655
  workspace,
537
656
  &os_user,
538
657
  launched_key,
539
- ) else {
540
- return;
541
- };
542
- let owner_epoch = 1u64;
543
- let owner = serde_json::json!({
544
- "pane_id": "__team_agent_unbound__",
658
+ )
659
+ .ok()?;
660
+ Some(serde_json::json!({
545
661
  "provider": provider,
546
662
  "machine_fingerprint": machine_fingerprint,
547
663
  "leader_session_uuid": uuid.as_str(),
548
- "owner_epoch": owner_epoch,
664
+ "owner_epoch": 1u64,
549
665
  "claimed_at": spawn_timestamp(),
550
666
  "claimed_via": "quick-start",
551
667
  "os_user": os_user,
552
- });
553
- let receiver = serde_json::json!({
554
- "mode": "direct_tmux",
555
- "status": "attached",
556
- "provider": provider,
557
- "pane_id": "__team_agent_unbound__",
558
- "pane": "__team_agent_unbound__",
559
- "leader_session_uuid": uuid.as_str(),
560
- "owner_epoch": owner_epoch,
561
- "discovery": "quick_start",
562
- });
563
- if let Some(obj) = launched.as_object_mut() {
564
- obj.insert("leader_receiver".to_string(), receiver);
565
- obj.insert("team_owner".to_string(), owner);
566
- obj.insert("owner_epoch".to_string(), serde_json::json!(owner_epoch));
567
- }
668
+ }))
568
669
  }
569
670
 
570
- fn owner_pane_belongs_to_other_team(existing: &serde_json::Value, launched_key: &str, pane: &str) -> bool {
671
+ fn owner_pane_belongs_to_other_team(
672
+ existing: &serde_json::Value,
673
+ launched_key: &str,
674
+ pane: &str,
675
+ ) -> bool {
571
676
  existing
572
677
  .get("teams")
573
678
  .and_then(serde_json::Value::as_object)
@@ -588,9 +693,14 @@ fn running_agent_state(
588
693
  id: &str,
589
694
  provider: Provider,
590
695
  workspace: &Path,
696
+ spawn_cwd: &Path,
591
697
  spawned_at: &str,
592
698
  team_id: &str,
699
+ pane_id: Option<&str>,
593
700
  pane_pid: Option<u32>,
701
+ safety: &DangerousApproval,
702
+ started_agent: Option<&StartedAgent>,
703
+ profile_dir: Option<&Path>,
594
704
  ) -> Result<serde_json::Value, LifecycleError> {
595
705
  let model = agent.get("model").and_then(Value::as_str);
596
706
  let auth_mode = agent
@@ -598,7 +708,10 @@ fn running_agent_state(
598
708
  .and_then(Value::as_str)
599
709
  .and_then(parse_auth_mode)
600
710
  .unwrap_or(AuthMode::Subscription);
601
- let profile = agent.get("profile").map(yaml_value_to_json).unwrap_or(serde_json::Value::Null);
711
+ let profile = agent
712
+ .get("profile")
713
+ .map(yaml_value_to_json)
714
+ .unwrap_or(serde_json::Value::Null);
602
715
  let window = agent.get("window").and_then(Value::as_str).unwrap_or(id);
603
716
  let mcp_config = crate::provider::get_adapter(provider)
604
717
  .mcp_config(auth_mode)
@@ -609,9 +722,20 @@ fn running_agent_state(
609
722
  state.insert("status".to_string(), serde_json::json!("running"));
610
723
  state.insert("provider".to_string(), serde_json::json!(provider));
611
724
  state.insert("agent_id".to_string(), serde_json::json!(id));
612
- state.insert("model".to_string(), model.map_or(serde_json::Value::Null, |m| serde_json::json!(m)));
725
+ state.insert(
726
+ "model".to_string(),
727
+ model.map_or(serde_json::Value::Null, |m| serde_json::json!(m)),
728
+ );
613
729
  state.insert("auth_mode".to_string(), serde_json::json!(auth_mode));
614
730
  state.insert("profile".to_string(), profile);
731
+ if agent.get("profile").is_some() {
732
+ if let Some(profile_dir) = profile_dir {
733
+ state.insert(
734
+ "_profile_dir".to_string(),
735
+ serde_json::json!(profile_dir.to_string_lossy().to_string()),
736
+ );
737
+ }
738
+ }
615
739
  state.insert("window".to_string(), serde_json::json!(window));
616
740
  state.insert(
617
741
  "mcp_config".to_string(),
@@ -622,23 +746,63 @@ fn running_agent_state(
622
746
  permissions_json(agent, id, provider)
623
747
  .map_err(|e| LifecycleError::Compile(e.to_string()))?,
624
748
  );
749
+ persist_effective_approval_policy(&mut state, safety);
625
750
  state.insert("session_id".to_string(), serde_json::Value::Null);
626
751
  state.insert("rollout_path".to_string(), serde_json::Value::Null);
627
752
  state.insert("captured_at".to_string(), serde_json::Value::Null);
628
753
  state.insert("captured_via".to_string(), serde_json::Value::Null);
629
- state.insert("attribution_confidence".to_string(), serde_json::Value::Null);
754
+ state.insert(
755
+ "attribution_confidence".to_string(),
756
+ serde_json::Value::Null,
757
+ );
758
+ if let Some(started_agent) = started_agent {
759
+ persist_started_agent_plan_state(&mut state, started_agent);
760
+ }
630
761
  state.insert(
631
762
  "spawn_cwd".to_string(),
632
- serde_json::json!(workspace.to_string_lossy().to_string()),
763
+ serde_json::json!(spawn_cwd.to_string_lossy().to_string()),
633
764
  );
634
765
  state.insert("spawned_at".to_string(), serde_json::json!(spawned_at));
766
+ if let Some(pane_id) = pane_id.filter(|pane| !pane.is_empty()) {
767
+ state.insert("pane_id".to_string(), serde_json::json!(pane_id));
768
+ }
635
769
  if let Some(pane_pid) = pane_pid {
636
770
  state.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
637
771
  }
638
772
  Ok(serde_json::Value::Object(state))
639
773
  }
640
774
 
641
- fn resolve_mcp_config(
775
+ pub(crate) fn effective_approval_policy(safety: &DangerousApproval) -> serde_json::Value {
776
+ serde_json::json!({
777
+ "enabled": safety.enabled,
778
+ "source": dangerous_approval_source_str(safety.source),
779
+ "inherited": safety.inherited,
780
+ "explicit_yes_confirmed": safety.enabled && matches!(safety.source, DangerousApprovalSource::RuntimeConfig),
781
+ "provider": safety.provider,
782
+ "flag": safety.flag,
783
+ "worker_capability_above_leader": safety.worker_capability_above_leader,
784
+ })
785
+ }
786
+
787
+ pub(crate) fn persist_effective_approval_policy(
788
+ agent_state: &mut serde_json::Map<String, serde_json::Value>,
789
+ safety: &DangerousApproval,
790
+ ) {
791
+ agent_state.insert(
792
+ "effective_approval_policy".to_string(),
793
+ effective_approval_policy(safety),
794
+ );
795
+ }
796
+
797
+ fn dangerous_approval_source_str(source: DangerousApprovalSource) -> &'static str {
798
+ match source {
799
+ DangerousApprovalSource::RuntimeConfig => "runtime_config",
800
+ DangerousApprovalSource::LeaderProcess => "leader_process",
801
+ DangerousApprovalSource::Disabled => "disabled",
802
+ }
803
+ }
804
+
805
+ pub(crate) fn resolve_mcp_config(
642
806
  config: crate::provider::McpConfig,
643
807
  workspace: &Path,
644
808
  agent_id: &str,
@@ -681,7 +845,7 @@ fn resolve_mcp_placeholders(
681
845
  }
682
846
  }
683
847
 
684
- fn write_worker_mcp_config(
848
+ pub(crate) fn write_worker_mcp_config(
685
849
  workspace: &Path,
686
850
  agent_id: &str,
687
851
  config: &crate::provider::McpConfig,
@@ -700,7 +864,11 @@ fn write_worker_mcp_config(
700
864
  Ok(path)
701
865
  }
702
866
 
703
- fn point_native_mcp_config_at_file(argv: &mut [String], provider: Provider, path: &Path) {
867
+ pub(crate) fn point_native_mcp_config_at_file(
868
+ argv: &mut [String],
869
+ provider: Provider,
870
+ path: &Path,
871
+ ) {
704
872
  if !matches!(provider, Provider::Claude | Provider::ClaudeCode) {
705
873
  return;
706
874
  }
@@ -727,13 +895,19 @@ fn permissions_json(
727
895
  let resolved = permissions::resolve_permissions(&AgentPermissionInput {
728
896
  id: Some(AgentId::new(id)),
729
897
  provider,
730
- role: agent.get("role").and_then(Value::as_str).map(str::to_string),
898
+ role: agent
899
+ .get("role")
900
+ .and_then(Value::as_str)
901
+ .map(str::to_string),
731
902
  tools,
732
903
  })?;
733
904
  let mut out = serde_json::Map::new();
734
905
  out.insert("agent_id".to_string(), serde_json::json!(id));
735
906
  out.insert("provider".to_string(), serde_json::json!(provider));
736
- out.insert("tools".to_string(), serde_json::json!(resolved.sorted_tool_strings()));
907
+ out.insert(
908
+ "tools".to_string(),
909
+ serde_json::json!(resolved.sorted_tool_strings()),
910
+ );
737
911
  out.insert(
738
912
  "resolved_tools".to_string(),
739
913
  serde_json::Value::Array(
@@ -749,7 +923,10 @@ fn permissions_json(
749
923
  .collect(),
750
924
  ),
751
925
  );
752
- out.insert("has_prompt_only".to_string(), serde_json::json!(resolved.has_prompt_only));
926
+ out.insert(
927
+ "has_prompt_only".to_string(),
928
+ serde_json::json!(resolved.has_prompt_only),
929
+ );
753
930
  Ok(serde_json::Value::Object(out))
754
931
  }
755
932
 
@@ -766,6 +943,23 @@ fn spawn_timestamp() -> String {
766
943
  }
767
944
  }
768
945
 
946
+ fn spawn_timestamp_for_agent(offset_micros: u32) -> String {
947
+ if offset_micros == 0 {
948
+ return spawn_timestamp();
949
+ }
950
+ match std::env::var("TEAM_AGENT_TEST_FIXED_SPAWNED_AT") {
951
+ Ok(value) => chrono::DateTime::parse_from_rfc3339(&value)
952
+ .map(|dt| {
953
+ (dt.with_timezone(&chrono::Utc)
954
+ + chrono::Duration::microseconds(i64::from(offset_micros)))
955
+ .format("%Y-%m-%dT%H:%M:%S%.6f+00:00")
956
+ .to_string()
957
+ })
958
+ .unwrap_or(value),
959
+ Err(_) => spawn_timestamp(),
960
+ }
961
+ }
962
+
769
963
  pub(crate) fn fill_spawn_placeholders(argv: &mut [String], workspace: &Path, agent_id: &str) {
770
964
  fill_spawn_placeholders_full(argv, workspace, agent_id, None);
771
965
  }
@@ -808,6 +1002,87 @@ pub(crate) fn inherited_env_with_team_overrides(
808
1002
  env
809
1003
  }
810
1004
 
1005
+ pub(crate) fn apply_profile_launch_env(
1006
+ env: &mut BTreeMap<String, String>,
1007
+ profile_launch: &crate::provider::ProviderProfileLaunch,
1008
+ ) {
1009
+ for key in &profile_launch.env_unset {
1010
+ env.remove(key);
1011
+ }
1012
+ env.extend(profile_launch.env_overlay.clone());
1013
+ }
1014
+
1015
+ fn persist_started_agent_plan_state(
1016
+ state: &mut serde_json::Map<String, serde_json::Value>,
1017
+ started_agent: &StartedAgent,
1018
+ ) {
1019
+ if let Some(session_id) = started_agent.pending_session_id.as_ref() {
1020
+ state.insert(
1021
+ "_pending_session_id".to_string(),
1022
+ serde_json::json!(session_id.as_str()),
1023
+ );
1024
+ }
1025
+ if let Some(root) = started_agent.provider_projects_root.as_ref() {
1026
+ state.insert(
1027
+ "claude_projects_root".to_string(),
1028
+ serde_json::json!(root.to_string_lossy().to_string()),
1029
+ );
1030
+ }
1031
+ if started_agent.managed_mcp_config {
1032
+ state.insert("managed_mcp_config".to_string(), serde_json::json!(true));
1033
+ }
1034
+ if started_agent.managed_mcp_config
1035
+ || started_agent.claude_config_dir.is_some()
1036
+ || started_agent.provider_projects_root.is_some()
1037
+ {
1038
+ state.insert(
1039
+ "profile_launch".to_string(),
1040
+ serde_json::json!({
1041
+ "managed_mcp_config": started_agent.managed_mcp_config,
1042
+ "claude_config_dir": started_agent.claude_config_dir.as_ref().map(|path| path.to_string_lossy().to_string()),
1043
+ "claude_projects_root": started_agent.provider_projects_root.as_ref().map(|path| path.to_string_lossy().to_string()),
1044
+ }),
1045
+ );
1046
+ }
1047
+ }
1048
+
1049
+ pub(crate) fn persist_command_plan_state(
1050
+ state: &mut serde_json::Map<String, serde_json::Value>,
1051
+ plan: &crate::provider::CommandPlan,
1052
+ profile_launch: &crate::provider::ProviderProfileLaunch,
1053
+ ) {
1054
+ if let Some(session_id) = plan.expected_session_id.as_ref() {
1055
+ state.insert(
1056
+ "_pending_session_id".to_string(),
1057
+ serde_json::json!(session_id.as_str()),
1058
+ );
1059
+ }
1060
+ let projects_root = plan
1061
+ .provider_projects_root
1062
+ .as_ref()
1063
+ .or(profile_launch.claude_projects_root.as_ref());
1064
+ if let Some(root) = projects_root {
1065
+ state.insert(
1066
+ "claude_projects_root".to_string(),
1067
+ serde_json::json!(root.to_string_lossy().to_string()),
1068
+ );
1069
+ }
1070
+ let managed_mcp_config = plan.managed_mcp_config || profile_launch.managed_mcp_config;
1071
+ if managed_mcp_config {
1072
+ state.insert("managed_mcp_config".to_string(), serde_json::json!(true));
1073
+ }
1074
+ if managed_mcp_config || profile_launch.claude_config_dir.is_some() || projects_root.is_some() {
1075
+ state.insert(
1076
+ "profile_launch".to_string(),
1077
+ serde_json::json!({
1078
+ "managed_mcp_config": managed_mcp_config,
1079
+ "claude_config_dir": profile_launch.claude_config_dir.as_ref().map(|path| path.to_string_lossy().to_string()),
1080
+ "claude_projects_root": projects_root.map(|path| path.to_string_lossy().to_string()),
1081
+ }),
1082
+ );
1083
+ }
1084
+ }
1085
+
811
1086
  fn is_posix_shell_identifier(name: &str) -> bool {
812
1087
  let mut chars = name.chars();
813
1088
  match chars.next() {
@@ -834,7 +1109,10 @@ pub(crate) fn fill_spawn_placeholders_full(
834
1109
  *arg = workspace_text.clone();
835
1110
  } else if arg == "{agent_id}" {
836
1111
  *arg = agent_id.to_string();
837
- } else if arg.contains("{workspace}") || arg.contains("{agent_id}") || arg.contains("{team_id}") {
1112
+ } else if arg.contains("{workspace}")
1113
+ || arg.contains("{agent_id}")
1114
+ || arg.contains("{team_id}")
1115
+ {
838
1116
  *arg = arg
839
1117
  .replace("{workspace}", &workspace_text)
840
1118
  .replace("{agent_id}", agent_id)
@@ -843,30 +1121,12 @@ pub(crate) fn fill_spawn_placeholders_full(
843
1121
  }
844
1122
  }
845
1123
 
846
- fn agent_tool_strings(agent: &Value) -> Vec<String> {
847
- agent
848
- .get("tools")
849
- .and_then(Value::as_list)
850
- .map(|items| {
851
- items
852
- .iter()
853
- .filter_map(Value::as_str)
854
- .map(str::to_string)
855
- .collect()
856
- })
857
- .unwrap_or_default()
858
- }
859
-
860
1124
  fn spec_team_id(spec: &Value) -> Option<String> {
861
1125
  spec.get("team")
862
1126
  .and_then(|v| v.get("id").or_else(|| v.get("name")))
863
1127
  .and_then(Value::as_str)
864
1128
  .map(str::to_string)
865
- .or_else(|| {
866
- spec.get("name")
867
- .and_then(Value::as_str)
868
- .map(str::to_string)
869
- })
1129
+ .or_else(|| spec.get("name").and_then(Value::as_str).map(str::to_string))
870
1130
  }
871
1131
 
872
1132
  fn runtime_active_team_key_for_spawn(
@@ -885,7 +1145,7 @@ fn explicit_active_team_key(state: &serde_json::Value) -> Option<String> {
885
1145
  state
886
1146
  .get("active_team_key")
887
1147
  .and_then(serde_json::Value::as_str)
888
- .filter(|team| !team.is_empty() && *team != "current")
1148
+ .filter(|team| !team.is_empty())
889
1149
  .map(str::to_string)
890
1150
  }
891
1151
 
@@ -929,10 +1189,157 @@ fn parse_auth_mode(raw: &str) -> Option<AuthMode> {
929
1189
  }
930
1190
  }
931
1191
 
932
- fn quick_start_requested_team_key<'a>(team_id: Option<&'a str>, name: Option<&'a str>) -> Option<&'a str> {
1192
+ fn quick_start_requested_team_key<'a>(
1193
+ team_id: Option<&'a str>,
1194
+ name: Option<&'a str>,
1195
+ ) -> Option<&'a str> {
933
1196
  team_id.or(name).filter(|team| !team.is_empty())
934
1197
  }
935
1198
 
1199
+ struct QuickStartDepth {
1200
+ parent_team_key: Option<String>,
1201
+ team_depth: u64,
1202
+ }
1203
+
1204
+ fn quick_start_depth_guard(
1205
+ workspace: &Path,
1206
+ _agents_dir: &Path,
1207
+ requested_team: Option<&str>,
1208
+ _strict_real_runtime: bool,
1209
+ ) -> Result<QuickStartDepth, LifecycleError> {
1210
+ let env_parent = std::env::var("TEAM_AGENT_OWNER_TEAM_ID")
1211
+ .ok()
1212
+ .map(|value| value.trim().to_string())
1213
+ .filter(|value| !value.is_empty());
1214
+ let parent = env_parent;
1215
+ let Some(parent) = parent else {
1216
+ let state = crate::state::persist::load_runtime_state(workspace)
1217
+ .unwrap_or_else(|_| serde_json::json!({}));
1218
+ let ambiguous_nested_intent = requested_team.is_some_and(|team| {
1219
+ looks_ambiguous_child_team_key(team) || looks_grandchild_team_key(team)
1220
+ });
1221
+ if has_live_runtime_teams(&state) && ambiguous_nested_intent {
1222
+ if requested_team.is_some_and(looks_grandchild_team_key) {
1223
+ if let Some(parent_key) = infer_parent_team_from_active_state(&state) {
1224
+ let parent_state =
1225
+ crate::state::projection::project_top_level_view(&state, &parent_key);
1226
+ let parent_depth = parent_state
1227
+ .get("team_depth")
1228
+ .and_then(serde_json::Value::as_u64)
1229
+ .unwrap_or(1);
1230
+ return Ok(QuickStartDepth {
1231
+ parent_team_key: Some(parent_key),
1232
+ team_depth: parent_depth.saturating_add(1),
1233
+ });
1234
+ }
1235
+ }
1236
+ return Err(LifecycleError::RequirementUnmet(
1237
+ "cannot infer parent team for nested quick-start; pass an explicit worker/subleader owner context"
1238
+ .to_string(),
1239
+ ));
1240
+ }
1241
+ return Ok(QuickStartDepth {
1242
+ parent_team_key: None,
1243
+ team_depth: 1,
1244
+ });
1245
+ };
1246
+ let state = crate::state::persist::load_runtime_state(workspace)
1247
+ .unwrap_or_else(|_| serde_json::json!({}));
1248
+ let parent_key = crate::state::projection::resolve_owner_team_id(&state, &parent)
1249
+ .canonical_key()
1250
+ .map(str::to_string)
1251
+ .unwrap_or(parent);
1252
+ let parent_state = crate::state::projection::project_top_level_view(&state, &parent_key);
1253
+ let parent_depth = parent_state
1254
+ .get("team_depth")
1255
+ .and_then(serde_json::Value::as_u64)
1256
+ .unwrap_or(1);
1257
+ let team_depth = parent_depth.saturating_add(1);
1258
+ Ok(QuickStartDepth {
1259
+ parent_team_key: Some(parent_key),
1260
+ team_depth,
1261
+ })
1262
+ }
1263
+
1264
+ fn infer_parent_team_from_active_state(state: &serde_json::Value) -> Option<String> {
1265
+ let active = explicit_active_team_key(state)?;
1266
+ let team = state
1267
+ .get("teams")
1268
+ .and_then(serde_json::Value::as_object)
1269
+ .and_then(|teams| teams.get(&active))?;
1270
+ team_has_running_agent(team).then_some(active)
1271
+ }
1272
+
1273
+ fn has_live_runtime_teams(state: &serde_json::Value) -> bool {
1274
+ state
1275
+ .get("teams")
1276
+ .and_then(serde_json::Value::as_object)
1277
+ .is_some_and(|teams| teams.values().any(team_has_running_agent))
1278
+ }
1279
+
1280
+ fn team_has_running_agent(team: &serde_json::Value) -> bool {
1281
+ team.get("agents")
1282
+ .and_then(serde_json::Value::as_object)
1283
+ .is_some_and(|agents| {
1284
+ agents.values().any(|agent| {
1285
+ agent.get("status").and_then(serde_json::Value::as_str) == Some("running")
1286
+ })
1287
+ })
1288
+ }
1289
+
1290
+ fn looks_ambiguous_child_team_key(team: &str) -> bool {
1291
+ let team = team.trim().to_ascii_lowercase();
1292
+ team != "child"
1293
+ && (team.starts_with("child-")
1294
+ || team.starts_with("child_")
1295
+ || team.starts_with("child.")
1296
+ || team.starts_with("child"))
1297
+ }
1298
+
1299
+ fn looks_grandchild_team_key(team: &str) -> bool {
1300
+ let team = team.trim().to_ascii_lowercase();
1301
+ team == "grandchild"
1302
+ || team.starts_with("grandchild-")
1303
+ || team.starts_with("grandchild_")
1304
+ || team.starts_with("grandchild.")
1305
+ || team.starts_with("grandchild")
1306
+ }
1307
+
1308
+ fn annotate_team_depth(
1309
+ state: &mut serde_json::Value,
1310
+ parent_team_key: Option<&str>,
1311
+ team_depth: u64,
1312
+ ) {
1313
+ let Some(obj) = state.as_object_mut() else {
1314
+ return;
1315
+ };
1316
+ obj.insert("team_depth".to_string(), serde_json::json!(team_depth));
1317
+ if let Some(parent) = parent_team_key.filter(|value| !value.is_empty()) {
1318
+ obj.insert("parent_team_key".to_string(), serde_json::json!(parent));
1319
+ }
1320
+ }
1321
+
1322
+ fn annotate_persisted_team_depth(
1323
+ workspace: &Path,
1324
+ team_key: &str,
1325
+ parent_team_key: Option<&str>,
1326
+ team_depth: u64,
1327
+ ) -> Result<(), LifecycleError> {
1328
+ let mut state = crate::state::persist::load_runtime_state(workspace)
1329
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1330
+ let Some(team) = state
1331
+ .get_mut("teams")
1332
+ .and_then(serde_json::Value::as_object_mut)
1333
+ .and_then(|teams| teams.get_mut(team_key))
1334
+ else {
1335
+ return Ok(());
1336
+ };
1337
+ annotate_team_depth(team, parent_team_key, team_depth);
1338
+ crate::state::persist::save_runtime_state(workspace, &state)
1339
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1340
+ Ok(())
1341
+ }
1342
+
936
1343
  fn runtime_state_has_quick_start_team(state: &serde_json::Value, team: &str) -> bool {
937
1344
  explicit_active_team_key(state).as_deref() == Some(team)
938
1345
  || state
@@ -949,9 +1356,7 @@ fn runtime_state_has_quick_start_team(state: &serde_json::Value, team: &str) ->
949
1356
  || state
950
1357
  .get("session_name")
951
1358
  .and_then(serde_json::Value::as_str)
952
- .is_some_and(|session| {
953
- session == team || session.strip_prefix("team-") == Some(team)
954
- })
1359
+ .is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team))
955
1360
  }
956
1361
 
957
1362
  fn json_team_identity_matches(state: &serde_json::Value, team: &str) -> bool {
@@ -988,8 +1393,7 @@ pub fn quick_start_in_workspace(
988
1393
  fresh: bool,
989
1394
  team_id: Option<&str>,
990
1395
  ) -> Result<QuickStartReport, LifecycleError> {
991
- let workspace = crate::model::paths::canonical_run_workspace(workspace)
992
- .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1396
+ let workspace = explicit_quick_start_workspace(workspace);
993
1397
  quick_start_with_transport_in_workspace(
994
1398
  &workspace,
995
1399
  agents_dir,
@@ -1002,6 +1406,18 @@ pub fn quick_start_in_workspace(
1002
1406
  )
1003
1407
  }
1004
1408
 
1409
+ fn explicit_quick_start_workspace(workspace: &Path) -> PathBuf {
1410
+ std::fs::canonicalize(workspace).unwrap_or_else(|_| {
1411
+ if workspace.is_absolute() {
1412
+ workspace.to_path_buf()
1413
+ } else {
1414
+ std::env::current_dir()
1415
+ .unwrap_or_else(|_| PathBuf::from("."))
1416
+ .join(workspace)
1417
+ }
1418
+ })
1419
+ }
1420
+
1005
1421
  /// `quick_start` with an injected transport — tests inject a recording mock so the REAL spawn path
1006
1422
  /// (launch dry_run=false → spawn_agents) is asserted without a live tmux; prod uses the real TmuxBackend.
1007
1423
  pub fn quick_start_with_transport(
@@ -1013,7 +1429,9 @@ pub fn quick_start_with_transport(
1013
1429
  transport: &dyn Transport,
1014
1430
  ) -> Result<QuickStartReport, LifecycleError> {
1015
1431
  let workspace = team_workspace(agents_dir);
1016
- quick_start_with_transport_in_workspace(&workspace, agents_dir, name, yes, fresh, team_id, transport)
1432
+ quick_start_with_transport_in_workspace(
1433
+ &workspace, agents_dir, name, yes, fresh, team_id, transport,
1434
+ )
1017
1435
  }
1018
1436
 
1019
1437
  pub fn quick_start_with_transport_in_workspace(
@@ -1038,6 +1456,19 @@ pub fn quick_start_with_transport_in_workspace(
1038
1456
  .map(str::to_string)
1039
1457
  .or_else(|| spec_team_id(&spec));
1040
1458
  let explicit_team_key = quick_start_requested_team_key(team_id, name).map(str::to_string);
1459
+ let team_depth = quick_start_depth_guard(
1460
+ &workspace,
1461
+ agents_dir,
1462
+ requested_team.as_deref(),
1463
+ matches!(transport.kind(), crate::transport::BackendKind::Tmux),
1464
+ )?;
1465
+ if team_depth.team_depth > 2 {
1466
+ let parent = team_depth.parent_team_key.as_deref().unwrap_or("");
1467
+ return Err(LifecycleError::RequirementUnmet(format!(
1468
+ "team nesting depth limit exceeded: parent_team_key={parent} parent_depth={} max_depth=2",
1469
+ team_depth.team_depth.saturating_sub(1)
1470
+ )));
1471
+ }
1041
1472
  if !fresh {
1042
1473
  let state_path = crate::state::persist::runtime_state_path(&workspace);
1043
1474
  if state_path.exists() {
@@ -1056,7 +1487,8 @@ pub fn quick_start_with_transport_in_workspace(
1056
1487
  .map(SessionName::new),
1057
1488
  state_path: Some(state_path),
1058
1489
  next_actions: vec![
1059
- "run restart to resume the existing team or pass --fresh to replace it".to_string(),
1490
+ "run restart to resume the existing team or pass --fresh to replace it"
1491
+ .to_string(),
1060
1492
  ],
1061
1493
  });
1062
1494
  }
@@ -1070,22 +1502,41 @@ pub fn quick_start_with_transport_in_workspace(
1070
1502
  if let Some(requested) = requested_team.as_deref() {
1071
1503
  override_spec_session_name(&mut spec, &format!("team-{requested}"));
1072
1504
  }
1505
+ let session_name = spec_session_name(&spec);
1506
+ let state_team_key = explicit_team_key.clone().unwrap_or_else(|| {
1507
+ let spec_path = agents_dir.join("team.spec.yaml");
1508
+ runtime_team_key_for_spec(&spec_path, &spec, &session_name)
1509
+ });
1073
1510
  let spec_path = agents_dir.join("team.spec.yaml");
1074
- std::fs::write(&spec_path, yaml::dumps(&spec)).map_err(|e| {
1075
- LifecycleError::StatePersist(format!("{}: {e}", spec_path.display()))
1076
- })?;
1511
+ std::fs::write(&spec_path, yaml::dumps(&spec))
1512
+ .map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", spec_path.display())))?;
1077
1513
  let _store = crate::message_store::MessageStore::open(&workspace)
1078
1514
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1079
- let session_name = spec_session_name(&spec);
1080
- let resolved_spec_path = std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
1515
+ let resolved_spec_path =
1516
+ std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
1081
1517
  let state = initial_runtime_state(&spec, &resolved_spec_path, &workspace, agents_dir);
1082
- let state_team_key = explicit_team_key
1083
- .unwrap_or_else(|| runtime_team_key_for_spec(&resolved_spec_path, &spec, &session_name));
1084
1518
  save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key))?;
1519
+ annotate_persisted_team_depth(
1520
+ &workspace,
1521
+ &state_team_key,
1522
+ team_depth.parent_team_key.as_deref(),
1523
+ team_depth.team_depth,
1524
+ )?;
1085
1525
  // FIX (rt-host-a real-machine finding): dry_run=false so launch_with_transport calls spawn_agents
1086
1526
  // and really creates the tmux session + worker windows (was hardcoded true → never spawned, which
1087
1527
  // also starved the coordinator: no session → first tick TmuxSessionMissing → run_daemon loop exits).
1088
- let launch = launch_with_transport_in_workspace(&workspace, &spec_path, false, yes, true, transport)?;
1528
+ let mut launch =
1529
+ launch_with_transport_in_workspace(&workspace, &spec_path, false, yes, true, transport)?;
1530
+ annotate_persisted_team_depth(
1531
+ &workspace,
1532
+ &state_team_key,
1533
+ team_depth.parent_team_key.as_deref(),
1534
+ team_depth.team_depth,
1535
+ )?;
1536
+ launch.leader_receiver_attached =
1537
+ launched_team_receiver_is_attached(&workspace, &state_team_key);
1538
+ launch.session_capture_incomplete_agents =
1539
+ quick_start_session_capture_incomplete_agents(&workspace, &state_team_key);
1089
1540
  let coordinator_workspace = crate::coordinator::WorkspacePath::new(workspace.clone());
1090
1541
  let coordinator_started = crate::coordinator::start_coordinator(&coordinator_workspace)
1091
1542
  .map(|report| report.ok)
@@ -1102,13 +1553,30 @@ pub fn quick_start_with_transport_in_workspace(
1102
1553
  // loaded successfully (provider-side codex/claude schema rejections happen
1103
1554
  // asynchronously after spawn), so the verdict is PendingToolLoad — never
1104
1555
  // bare Ready.
1105
- let worker_readiness = quick_start_worker_readiness(&workspace);
1556
+ let worker_readiness = quick_start_worker_readiness(&workspace, &state_team_key);
1557
+ let attach_commands = crate::tmux_backend::attach_commands_for_windows(
1558
+ &workspace,
1559
+ &session_name,
1560
+ launch
1561
+ .started
1562
+ .iter()
1563
+ .map(|started| started.agent_id.as_str()),
1564
+ );
1565
+ let mut next_actions = vec![format!(
1566
+ "team compiled; real spawn is behind the transport/provider boundary; {coordinator_action}"
1567
+ )];
1568
+ next_actions.extend(attach_commands.iter().cloned());
1569
+ let display_backend = state
1570
+ .get("display_backend")
1571
+ .and_then(serde_json::Value::as_str)
1572
+ .unwrap_or("none")
1573
+ .to_string();
1106
1574
  Ok(QuickStartReport::Ready {
1107
1575
  session_name,
1108
1576
  launch: Box::new(launch),
1109
- next_actions: vec![format!(
1110
- "team compiled; real spawn is behind the transport/provider boundary; {coordinator_action}"
1111
- )],
1577
+ next_actions,
1578
+ attach_commands,
1579
+ display_backend,
1112
1580
  worker_readiness,
1113
1581
  })
1114
1582
  }
@@ -1118,13 +1586,24 @@ pub fn quick_start_with_transport_in_workspace(
1118
1586
  /// verdict to `Degraded { unhealthy_agents }` (sorted, deduped); otherwise
1119
1587
  /// `PendingToolLoad` — never bare Ready. State read failure is treated as
1120
1588
  /// PendingToolLoad rather than fabricated success.
1121
- fn quick_start_worker_readiness(workspace: &Path) -> QuickStartReadiness {
1589
+ fn quick_start_worker_readiness(workspace: &Path, team_key: &str) -> QuickStartReadiness {
1122
1590
  let Ok(state) = load_runtime_state(workspace) else {
1123
1591
  return QuickStartReadiness::PendingToolLoad;
1124
1592
  };
1125
- let Some(agents) = state.get("agents").and_then(serde_json::Value::as_object) else {
1593
+ let team_state = state
1594
+ .get("teams")
1595
+ .and_then(serde_json::Value::as_object)
1596
+ .and_then(|teams| teams.get(team_key))
1597
+ .unwrap_or(&state);
1598
+ let Some(agents) = team_state
1599
+ .get("agents")
1600
+ .and_then(serde_json::Value::as_object)
1601
+ else {
1126
1602
  return QuickStartReadiness::PendingToolLoad;
1127
1603
  };
1604
+ let all_spawned = !agents.is_empty();
1605
+ let leader_receiver_attached = launched_team_receiver_is_attached(workspace, team_key);
1606
+ let all_attached_receiver = leader_receiver_attached;
1128
1607
  let mut unhealthy: Vec<String> = agents
1129
1608
  .iter()
1130
1609
  .filter_map(|(id, agent)| {
@@ -1135,22 +1614,88 @@ fn quick_start_worker_readiness(workspace: &Path) -> QuickStartReadiness {
1135
1614
  }
1136
1615
  })
1137
1616
  .collect();
1138
- if unhealthy.is_empty() {
1139
- QuickStartReadiness::PendingToolLoad
1140
- } else {
1617
+ if !unhealthy.is_empty() {
1141
1618
  unhealthy.sort();
1142
1619
  unhealthy.dedup();
1143
- QuickStartReadiness::Degraded { unhealthy_agents: unhealthy }
1620
+ QuickStartReadiness::Degraded {
1621
+ unhealthy_agents: unhealthy,
1622
+ }
1623
+ } else {
1624
+ let incomplete_agents =
1625
+ crate::session_capture::incomplete_interacted_resumable_agent_ids(team_state);
1626
+ let all_resumable_have_session = incomplete_agents.is_empty();
1627
+ let _readiness_ready = all_spawned && all_attached_receiver && all_resumable_have_session;
1628
+ QuickStartReadiness::PendingToolLoad
1629
+ }
1630
+ }
1631
+
1632
+ fn quick_start_session_capture_incomplete_agents(workspace: &Path, team_key: &str) -> Vec<String> {
1633
+ let Ok(state) = load_runtime_state(workspace) else {
1634
+ return Vec::new();
1635
+ };
1636
+ let team_state = state
1637
+ .get("teams")
1638
+ .and_then(serde_json::Value::as_object)
1639
+ .and_then(|teams| teams.get(team_key))
1640
+ .unwrap_or(&state);
1641
+ crate::session_capture::incomplete_interacted_resumable_agent_ids(team_state)
1642
+ }
1643
+
1644
+ fn launched_team_receiver_is_attached(workspace: &Path, team_key: &str) -> bool {
1645
+ let Ok(state) = load_runtime_state(workspace) else {
1646
+ return true;
1647
+ };
1648
+ let team_state = state
1649
+ .get("teams")
1650
+ .and_then(serde_json::Value::as_object)
1651
+ .and_then(|teams| teams.get(team_key))
1652
+ .unwrap_or(&state);
1653
+ if team_state.get("leader_receiver").is_none() {
1654
+ return true;
1144
1655
  }
1656
+ if team_uses_fake_model_harness(team_state) {
1657
+ return true;
1658
+ }
1659
+ leader_receiver_is_attached(team_state)
1660
+ }
1661
+
1662
+ fn team_uses_fake_model_harness(team_state: &serde_json::Value) -> bool {
1663
+ team_state
1664
+ .get("agents")
1665
+ .and_then(serde_json::Value::as_object)
1666
+ .is_some_and(|agents| {
1667
+ !agents.is_empty()
1668
+ && agents.values().all(|agent| {
1669
+ agent.get("model").and_then(serde_json::Value::as_str) == Some("fake")
1670
+ })
1671
+ })
1672
+ }
1673
+
1674
+ fn leader_receiver_is_attached(team_state: &serde_json::Value) -> bool {
1675
+ let Some(receiver) = team_state.get("leader_receiver") else {
1676
+ return false;
1677
+ };
1678
+ let status = receiver
1679
+ .get("status")
1680
+ .and_then(serde_json::Value::as_str)
1681
+ .unwrap_or("");
1682
+ let pane_id = receiver
1683
+ .get("pane_id")
1684
+ .and_then(serde_json::Value::as_str)
1685
+ .or_else(|| receiver.get("pane").and_then(serde_json::Value::as_str))
1686
+ .unwrap_or("");
1687
+ status == "attached" && !pane_id.is_empty() && pane_id != "__team_agent_unbound__"
1145
1688
  }
1146
1689
 
1147
1690
  /// `detect_inherited_dangerous_permissions`(`launch/config.py`):扫进程祖先链找
1148
1691
  /// `--dangerously-*` flag,产出危险审批继承态。launch 在 inherited=false 且无 --yes 时拒。
1149
1692
  pub fn detect_dangerous_approval() -> Result<DangerousApproval, LifecycleError> {
1150
1693
  if let Ok(raw) = std::env::var("TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON") {
1151
- let argv_tokens = serde_json::from_str::<Vec<String>>(&raw)
1152
- .map_err(|e| LifecycleError::StatePersist(format!("invalid test ancestry argv: {e}")))?;
1153
- return Ok(detect_dangerous_approval_in_argv(&argv_tokens).unwrap_or_else(disabled_dangerous_approval));
1694
+ let argv_tokens = serde_json::from_str::<Vec<String>>(&raw).map_err(|e| {
1695
+ LifecycleError::StatePersist(format!("invalid test ancestry argv: {e}"))
1696
+ })?;
1697
+ return Ok(detect_dangerous_approval_in_argv(&argv_tokens)
1698
+ .unwrap_or_else(disabled_dangerous_approval));
1154
1699
  }
1155
1700
  for argv_tokens in process_ancestry_argv(std::process::id()) {
1156
1701
  if let Some(detected) = detect_dangerous_approval_in_argv(&argv_tokens) {
@@ -1166,7 +1711,8 @@ fn detect_dangerous_approval_in_argv(argv_tokens: &[String]) -> Option<Dangerous
1166
1711
  for token in argv_tokens {
1167
1712
  for (provider, flag) in dangerous_leader_flags() {
1168
1713
  if token == flag {
1169
- let unexpected_binary = !binary_matches_provider(provider, ancestry_binary_name.as_deref());
1714
+ let unexpected_binary =
1715
+ !binary_matches_provider(provider, ancestry_binary_name.as_deref());
1170
1716
  return Some(DangerousApproval {
1171
1717
  enabled: true,
1172
1718
  source: DangerousApprovalSource::LeaderProcess,
@@ -1356,16 +1902,16 @@ pub fn add_agent(
1356
1902
  }
1357
1903
  Err(error) => return Err(LifecycleError::TeamSelect(error.to_string())),
1358
1904
  };
1359
- let team_dir = selected
1360
- .spec_workspace
1361
- .ok_or_else(|| LifecycleError::TeamSelect("active team spec workspace not found".to_string()))?;
1905
+ let team_dir = selected.spec_workspace.ok_or_else(|| {
1906
+ LifecycleError::TeamSelect("active team spec workspace not found".to_string())
1907
+ })?;
1362
1908
  add_agent_with_transport_at_paths(
1363
1909
  &selected.run_workspace,
1364
1910
  &team_dir,
1365
1911
  agent_id,
1366
1912
  role_file_path,
1367
1913
  open_display,
1368
- team,
1914
+ Some(selected.team_key.as_str()),
1369
1915
  &crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace),
1370
1916
  )
1371
1917
  }
@@ -1402,12 +1948,16 @@ fn add_agent_with_transport_at_paths(
1402
1948
  team: Option<&str>,
1403
1949
  transport: &dyn Transport,
1404
1950
  ) -> Result<AddAgentReport, LifecycleError> {
1405
- let owner_state = if team.is_some() {
1406
- crate::state::projection::select_runtime_state(run_workspace, team)
1407
- .map_err(|e| LifecycleError::TeamSelect(e.to_string()))?
1408
- } else {
1409
- load_runtime_state(run_workspace).map_err(|e| LifecycleError::StatePersist(e.to_string()))?
1410
- };
1951
+ let runtime_state = crate::state::persist::load_runtime_state(run_workspace)
1952
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1953
+ let canonical_team_key = team
1954
+ .filter(|key| !key.is_empty())
1955
+ .map(str::to_string)
1956
+ .or_else(|| explicit_active_team_key(&runtime_state))
1957
+ .unwrap_or_else(|| crate::state::projection::team_state_key(&runtime_state));
1958
+ let owner_state =
1959
+ crate::state::projection::select_runtime_state(run_workspace, Some(&canonical_team_key))
1960
+ .map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
1411
1961
  ensure_owner_allowed_for_state(&owner_state, Some(agent_id))?;
1412
1962
  if !role_file_path.exists() {
1413
1963
  return Err(LifecycleError::Compile(format!(
@@ -1423,13 +1973,20 @@ fn add_agent_with_transport_at_paths(
1423
1973
  let dynamic_role_file = materialize_added_role_file(team_dir, agent_id, role_file_path)?;
1424
1974
  let spec = crate::compiler::compile_team(team_dir)
1425
1975
  .map_err(|e| LifecycleError::Compile(e.to_string()))?;
1976
+ let safety = effective_runtime_config(&spec)?;
1426
1977
  let spec_path = team_dir.join("team.spec.yaml");
1427
- std::fs::write(&spec_path, yaml::dumps(&spec)).map_err(|e| {
1428
- LifecycleError::StatePersist(format!("{}: {e}", spec_path.display()))
1429
- })?;
1978
+ std::fs::write(&spec_path, yaml::dumps(&spec))
1979
+ .map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", spec_path.display())))?;
1430
1980
  let (meta, _) = crate::compiler::read_front_matter(&dynamic_role_file)
1431
1981
  .map_err(|e| LifecycleError::Compile(e.to_string()))?;
1432
- upsert_agent_state_from_role(run_workspace, team, agent_id, &meta, &dynamic_role_file)?;
1982
+ upsert_agent_state_from_role(
1983
+ run_workspace,
1984
+ &canonical_team_key,
1985
+ agent_id,
1986
+ &meta,
1987
+ &dynamic_role_file,
1988
+ &safety,
1989
+ )?;
1433
1990
  let started = crate::lifecycle::restart::start_agent_at_paths(
1434
1991
  run_workspace,
1435
1992
  team_dir,
@@ -1437,7 +1994,7 @@ fn add_agent_with_transport_at_paths(
1437
1994
  false,
1438
1995
  open_display,
1439
1996
  true,
1440
- team,
1997
+ Some(&canonical_team_key),
1441
1998
  transport,
1442
1999
  )?;
1443
2000
  let (env, start_mode) = match started {
@@ -1460,18 +2017,15 @@ fn add_agent_with_transport_at_paths(
1460
2017
 
1461
2018
  fn upsert_agent_state_from_role(
1462
2019
  workspace: &Path,
1463
- team: Option<&str>,
2020
+ canonical_team_key: &str,
1464
2021
  agent_id: &AgentId,
1465
2022
  meta: &Value,
1466
2023
  dynamic_role_file: &Path,
2024
+ safety: &DangerousApproval,
1467
2025
  ) -> Result<(), LifecycleError> {
1468
- let mut state = if team.is_some() {
1469
- crate::state::projection::select_runtime_state(workspace, team)
1470
- .map_err(|e| LifecycleError::TeamSelect(e.to_string()))?
1471
- } else {
1472
- crate::state::persist::load_runtime_state(workspace)
1473
- .map_err(|e| LifecycleError::StatePersist(e.to_string()))?
1474
- };
2026
+ let mut state =
2027
+ crate::state::projection::select_runtime_state(workspace, Some(canonical_team_key))
2028
+ .map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
1475
2029
  if !state.is_object() {
1476
2030
  state = serde_json::json!({});
1477
2031
  }
@@ -1513,16 +2067,28 @@ fn upsert_agent_state_from_role(
1513
2067
  if let Some(model) = meta.get("model").and_then(Value::as_str) {
1514
2068
  if let Some(obj) = entry.as_object_mut() {
1515
2069
  obj.insert("model".to_string(), serde_json::json!(model));
2070
+ obj.insert("model_source".to_string(), serde_json::json!("role"));
1516
2071
  }
1517
2072
  }
1518
- agent_map.insert(agent_id.as_str().to_string(), entry);
1519
- if team.is_some() {
1520
- let team_key = explicit_active_team_key(&state)
1521
- .or_else(|| team.filter(|key| !key.is_empty()).map(str::to_string));
1522
- save_launched_team_state_for_key(workspace, &state, team_key.as_deref())
1523
- } else {
1524
- save_runtime_state(workspace, &state).map_err(|e| LifecycleError::StatePersist(e.to_string()))
2073
+ if let Some(profile) = meta.get("profile").and_then(Value::as_str) {
2074
+ if let Some(obj) = entry.as_object_mut() {
2075
+ obj.insert("profile".to_string(), serde_json::json!(profile));
2076
+ if let Some(team_dir) = dynamic_role_file.parent().and_then(Path::parent) {
2077
+ obj.insert(
2078
+ "_profile_dir".to_string(),
2079
+ serde_json::json!(team_dir.join("profiles").to_string_lossy().to_string()),
2080
+ );
2081
+ }
2082
+ if !obj.contains_key("model_source") {
2083
+ obj.insert("model_source".to_string(), serde_json::json!("default"));
2084
+ }
2085
+ }
1525
2086
  }
2087
+ if let Some(obj) = entry.as_object_mut() {
2088
+ persist_effective_approval_policy(obj, safety);
2089
+ }
2090
+ agent_map.insert(agent_id.as_str().to_string(), entry);
2091
+ save_launched_team_state_for_key(workspace, &state, Some(canonical_team_key))
1526
2092
  }
1527
2093
 
1528
2094
  fn materialize_added_role_file(
@@ -1588,9 +2154,9 @@ pub fn fork_agent_with_transport(
1588
2154
  crate::state::selector::SelectorMode::RequireSpec,
1589
2155
  )
1590
2156
  .map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
1591
- let spec_workspace = selected
1592
- .spec_workspace
1593
- .ok_or_else(|| LifecycleError::TeamSelect("active team spec workspace not found".to_string()))?;
2157
+ let spec_workspace = selected.spec_workspace.ok_or_else(|| {
2158
+ LifecycleError::TeamSelect("active team spec workspace not found".to_string())
2159
+ })?;
1594
2160
  let workspace = selected.run_workspace;
1595
2161
  let state = selected.state;
1596
2162
  ensure_owner_allowed_for_state(&state, Some(source_agent_id))?;
@@ -1603,8 +2169,9 @@ pub fn fork_agent_with_transport(
1603
2169
  "agent id already exists: {as_agent_id}"
1604
2170
  )));
1605
2171
  }
1606
- let source_agent = find_spec_agent(&spec, source_agent_id)
1607
- .ok_or_else(|| LifecycleError::RequirementUnmet(format!("unknown worker agent id: {source_agent_id}")))?;
2172
+ let source_agent = find_spec_agent(&spec, source_agent_id).ok_or_else(|| {
2173
+ LifecycleError::RequirementUnmet(format!("unknown worker agent id: {source_agent_id}"))
2174
+ })?;
1608
2175
  let session_id = state
1609
2176
  .get("agents")
1610
2177
  .and_then(|v| v.get(source_agent_id.as_str()))
@@ -1639,8 +2206,9 @@ pub fn fork_agent_with_transport(
1639
2206
  .map_err(|e| LifecycleError::Compile(e.to_string()))?;
1640
2207
  std::fs::write(&spec_path, yaml::dumps(&new_spec))
1641
2208
  .map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", spec_path.display())))?;
1642
- let new_agent = find_spec_agent(&new_spec, as_agent_id)
1643
- .ok_or_else(|| LifecycleError::RequirementUnmet(format!("unknown worker agent id: {as_agent_id}")))?;
2209
+ let new_agent = find_spec_agent(&new_spec, as_agent_id).ok_or_else(|| {
2210
+ LifecycleError::RequirementUnmet(format!("unknown worker agent id: {as_agent_id}"))
2211
+ })?;
1644
2212
  let provider = new_agent
1645
2213
  .get("provider")
1646
2214
  .and_then(Value::as_str)
@@ -1662,68 +2230,171 @@ pub fn fork_agent_with_transport(
1662
2230
  "{provider_str} does not support native session fork"
1663
2231
  )));
1664
2232
  }
1665
- let role = new_agent.get("role").and_then(Value::as_str);
1666
2233
  let model = new_agent.get("model").and_then(Value::as_str);
1667
2234
  let safety = effective_runtime_config(&new_spec)?;
1668
- let tools = worker_tool_refs(agent_tool_strings(new_agent), &safety);
1669
- let tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
1670
- let mcp_config = adapter
1671
- .mcp_config(auth_mode)
2235
+ let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
2236
+ new_agent,
2237
+ Some(as_agent_id.as_str()),
2238
+ provider,
2239
+ );
2240
+ let system_prompt =
2241
+ crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
2242
+ let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
2243
+ &command_agent,
2244
+ provider,
2245
+ &safety,
2246
+ )?;
2247
+ let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
2248
+ let fork_team = crate::messaging::leader_receiver::active_team_key(&workspace, &state);
2249
+ let mcp_config = adapter.mcp_config(auth_mode).map_err(|e| {
2250
+ let _ = std::fs::write(&spec_path, text.as_bytes());
2251
+ LifecycleError::Provider(e.to_string())
2252
+ })?;
2253
+ let mcp_config = resolve_mcp_config(mcp_config, &workspace, as_agent_id.as_str(), &fork_team);
2254
+ let mcp_config_path = write_worker_mcp_config(&workspace, as_agent_id.as_str(), &mcp_config)
1672
2255
  .map_err(|e| {
1673
2256
  let _ = std::fs::write(&spec_path, text.as_bytes());
1674
- LifecycleError::Provider(e.to_string())
2257
+ e
1675
2258
  })?;
1676
- let mut argv = adapter
1677
- .fork_with_context(
1678
- Some(&session_id),
1679
- auth_mode,
2259
+ let profile_dir = spec_workspace.join("profiles");
2260
+ let profile_launch =
2261
+ crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
2262
+ &workspace,
2263
+ as_agent_id.as_str(),
2264
+ new_agent,
2265
+ Some(&profile_dir),
1680
2266
  Some(&mcp_config),
1681
- role,
1682
- model,
1683
- &tool_refs,
2267
+ )?;
2268
+ let command_model = profile_launch.command_overrides.model.as_deref().or(model);
2269
+ let mut plan = adapter
2270
+ .fork_plan(
2271
+ Some(&session_id),
2272
+ crate::provider::ProviderCommandContext {
2273
+ auth_mode,
2274
+ mcp_config: Some(&mcp_config),
2275
+ system_prompt: Some(system_prompt.as_str()),
2276
+ model: command_model,
2277
+ tools: &resolved_tool_refs,
2278
+ profile_launch: Some(&profile_launch),
2279
+ },
1684
2280
  )
1685
2281
  .map_err(|e| {
1686
2282
  let _ = std::fs::write(&spec_path, text.as_bytes());
1687
2283
  LifecycleError::Provider(e.to_string())
1688
2284
  })?;
1689
- let fork_team = crate::messaging::leader_receiver::active_team_key(&workspace, &state);
1690
- fill_spawn_placeholders_full(&mut argv, &workspace, as_agent_id.as_str(), Some(&fork_team));
1691
- let window = WindowName::new(as_agent_id.as_str());
1692
- // fork inherits the parent agent's owner team via runtime state (`active_team_key`).
1693
- let env = inherited_env_with_team_overrides(
2285
+ if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
2286
+ point_native_mcp_config_at_file(&mut plan.argv, provider, &mcp_config_path);
2287
+ }
2288
+ fill_spawn_placeholders_full(
2289
+ &mut plan.argv,
1694
2290
  &workspace,
1695
2291
  as_agent_id.as_str(),
1696
2292
  Some(&fork_team),
1697
2293
  );
2294
+ let window = WindowName::new(as_agent_id.as_str());
2295
+ // fork inherits the parent agent's owner team via runtime state (`active_team_key`).
2296
+ let mut env =
2297
+ inherited_env_with_team_overrides(&workspace, as_agent_id.as_str(), Some(&fork_team));
2298
+ apply_profile_launch_env(&mut env, &profile_launch);
1698
2299
  // golden operations.py:336 -> _tmux_start_command_for_agent_window (runtime.py:1017-1020): branch on
1699
2300
  // _tmux_session_exists — an ABSENT session => new-session (spawn_first), present => new-window
1700
2301
  // (spawn_into). The Rust restart seam (restart.rs spawn_agent_window) uses the same branch.
1701
2302
  let session_live = transport.has_session(&session_name).unwrap_or(false);
1702
2303
  let spawn_result = if session_live {
1703
- transport.spawn_into(&session_name, &window, &argv, &workspace, &env)
2304
+ transport.spawn_into(&session_name, &window, &plan.argv, &workspace, &env)
1704
2305
  } else {
1705
- transport.spawn_first(&session_name, &window, &argv, &workspace, &env)
2306
+ transport.spawn_first(&session_name, &window, &plan.argv, &workspace, &env)
1706
2307
  };
1707
- let _spawn = spawn_result.map_err(|e| {
2308
+ let spawn = spawn_result.map_err(|e| {
1708
2309
  let _ = std::fs::write(&spec_path, text.as_bytes());
1709
2310
  LifecycleError::Transport(e.to_string())
1710
2311
  })?;
1711
2312
  let old_state = state.clone();
1712
2313
  let mut next_state = state;
1713
- upsert_forked_agent_state(&mut next_state, source_agent_id, as_agent_id, new_agent)?;
2314
+ upsert_forked_agent_state(
2315
+ &mut next_state,
2316
+ source_agent_id,
2317
+ as_agent_id,
2318
+ new_agent,
2319
+ &safety,
2320
+ &plan,
2321
+ &profile_launch,
2322
+ &spawn,
2323
+ &workspace,
2324
+ Some(&profile_dir),
2325
+ )?;
2326
+ if let Some(agent) = next_state
2327
+ .get_mut("agents")
2328
+ .and_then(serde_json::Value::as_object_mut)
2329
+ .and_then(|agents| agents.get_mut(as_agent_id.as_str()))
2330
+ .and_then(serde_json::Value::as_object_mut)
2331
+ {
2332
+ persist_effective_approval_policy(agent, &safety);
2333
+ }
2334
+ if let Err(e) = maybe_fail_fork_after_spawn("save_runtime_state") {
2335
+ rollback_fork_after_spawn(
2336
+ &workspace,
2337
+ &spec_path,
2338
+ &text,
2339
+ &old_state,
2340
+ transport,
2341
+ &session_name,
2342
+ &window,
2343
+ &mcp_config_path,
2344
+ as_agent_id,
2345
+ &profile_launch,
2346
+ );
2347
+ return Err(e);
2348
+ }
1714
2349
  if let Err(e) = save_runtime_state(&workspace, &next_state) {
1715
- rollback_fork_after_spawn(&workspace, &spec_path, &text, &old_state, transport, &session_name, &window);
2350
+ rollback_fork_after_spawn(
2351
+ &workspace,
2352
+ &spec_path,
2353
+ &text,
2354
+ &old_state,
2355
+ transport,
2356
+ &session_name,
2357
+ &window,
2358
+ &mcp_config_path,
2359
+ as_agent_id,
2360
+ &profile_launch,
2361
+ );
1716
2362
  return Err(LifecycleError::StatePersist(e.to_string()));
1717
2363
  }
1718
- let coordinator_started =
1719
- crate::coordinator::start_coordinator(&crate::coordinator::WorkspacePath::new(
1720
- workspace.to_path_buf(),
1721
- ))
1722
- .map(|report| report.ok)
1723
- .map_err(|e| {
1724
- rollback_fork_after_spawn(&workspace, &spec_path, &text, &old_state, transport, &session_name, &window);
1725
- LifecycleError::StatePersist(e.to_string())
1726
- })?;
2364
+ if let Err(e) = maybe_fail_fork_after_spawn("start_coordinator") {
2365
+ rollback_fork_after_spawn(
2366
+ &workspace,
2367
+ &spec_path,
2368
+ &text,
2369
+ &old_state,
2370
+ transport,
2371
+ &session_name,
2372
+ &window,
2373
+ &mcp_config_path,
2374
+ as_agent_id,
2375
+ &profile_launch,
2376
+ );
2377
+ return Err(e);
2378
+ }
2379
+ let coordinator_started = crate::coordinator::start_coordinator(
2380
+ &crate::coordinator::WorkspacePath::new(workspace.to_path_buf()),
2381
+ )
2382
+ .map(|report| report.ok)
2383
+ .map_err(|e| {
2384
+ rollback_fork_after_spawn(
2385
+ &workspace,
2386
+ &spec_path,
2387
+ &text,
2388
+ &old_state,
2389
+ transport,
2390
+ &session_name,
2391
+ &window,
2392
+ &mcp_config_path,
2393
+ as_agent_id,
2394
+ &profile_launch,
2395
+ );
2396
+ LifecycleError::StatePersist(e.to_string())
2397
+ })?;
1727
2398
  Ok(ForkAgentReport {
1728
2399
  source_agent_id: source_agent_id.clone(),
1729
2400
  new_agent_id: as_agent_id.clone(),
@@ -1744,6 +2415,9 @@ fn rollback_fork_after_spawn(
1744
2415
  transport: &dyn Transport,
1745
2416
  session_name: &SessionName,
1746
2417
  window: &WindowName,
2418
+ mcp_config_path: &Path,
2419
+ agent_id: &AgentId,
2420
+ profile_launch: &crate::provider::ProviderProfileLaunch,
1747
2421
  ) {
1748
2422
  let _ = transport.kill_window(&Target::SessionWindow {
1749
2423
  session: session_name.clone(),
@@ -1751,6 +2425,40 @@ fn rollback_fork_after_spawn(
1751
2425
  });
1752
2426
  let _ = std::fs::write(spec_path, spec_text.as_bytes());
1753
2427
  let _ = save_runtime_state(workspace, old_state);
2428
+ cleanup_fork_mcp_artifacts(workspace, agent_id, mcp_config_path, profile_launch);
2429
+ }
2430
+
2431
+ fn maybe_fail_fork_after_spawn(step: &str) -> Result<(), LifecycleError> {
2432
+ let Ok(reason) = std::env::var("TEAM_AGENT_TEST_FAIL_FORK_AFTER_SPAWN") else {
2433
+ return Ok(());
2434
+ };
2435
+ if reason.is_empty() {
2436
+ return Ok(());
2437
+ }
2438
+ let should_fail = reason == step || (step == "start_coordinator" && reason == "coordinator");
2439
+ if !should_fail {
2440
+ return Ok(());
2441
+ }
2442
+ Err(LifecycleError::StatePersist(format!(
2443
+ "injected fork failure after spawn: {reason}"
2444
+ )))
2445
+ }
2446
+
2447
+ fn cleanup_fork_mcp_artifacts(
2448
+ workspace: &Path,
2449
+ agent_id: &AgentId,
2450
+ mcp_config_path: &Path,
2451
+ profile_launch: &crate::provider::ProviderProfileLaunch,
2452
+ ) {
2453
+ let _ = std::fs::remove_file(mcp_config_path);
2454
+ let _ = std::fs::remove_file(
2455
+ workspace
2456
+ .join(".team/runtime/provider-env")
2457
+ .join(format!("{}.env", agent_id.as_str())),
2458
+ );
2459
+ if let Some(config_dir) = profile_launch.claude_config_dir.as_ref() {
2460
+ let _ = std::fs::remove_dir_all(config_dir.parent().unwrap_or(config_dir));
2461
+ }
1754
2462
  }
1755
2463
 
1756
2464
  fn leader_id_matches(spec: &Value, agent_id: &AgentId) -> bool {
@@ -1771,16 +2479,13 @@ fn find_spec_agent<'a>(spec: &'a Value, agent_id: &AgentId) -> Option<&'a Value>
1771
2479
  if leader_is_agent {
1772
2480
  return None;
1773
2481
  }
1774
- spec.get("agents")?
1775
- .as_list()?
1776
- .iter()
1777
- .find(|agent| {
1778
- agent
1779
- .get("id")
1780
- .and_then(Value::as_str)
1781
- .map(|id| id == agent_id.as_str())
1782
- .unwrap_or(false)
1783
- })
2482
+ spec.get("agents")?.as_list()?.iter().find(|agent| {
2483
+ agent
2484
+ .get("id")
2485
+ .and_then(Value::as_str)
2486
+ .map(|id| id == agent_id.as_str())
2487
+ .unwrap_or(false)
2488
+ })
1784
2489
  }
1785
2490
 
1786
2491
  fn append_forked_agent(
@@ -1819,12 +2524,17 @@ fn append_forked_agent(
1819
2524
  )?;
1820
2525
 
1821
2526
  let Value::Map(pairs) = spec else {
1822
- return Err(LifecycleError::Compile("spec root is not a map".to_string()));
2527
+ return Err(LifecycleError::Compile(
2528
+ "spec root is not a map".to_string(),
2529
+ ));
1823
2530
  };
1824
2531
  let mut out = Vec::new();
1825
2532
  for (key, value) in pairs {
1826
2533
  if key == "agents" {
1827
- let mut agents = value.as_list().map(|items| items.to_vec()).unwrap_or_default();
2534
+ let mut agents = value
2535
+ .as_list()
2536
+ .map(|items| items.to_vec())
2537
+ .unwrap_or_default();
1828
2538
  agents.push(new_agent.clone());
1829
2539
  out.push((key.clone(), Value::List(agents)));
1830
2540
  } else if key == "runtime" {
@@ -1838,7 +2548,9 @@ fn append_forked_agent(
1838
2548
 
1839
2549
  fn set_yaml_map_value(value: &mut Value, key: &str, next: Value) -> Result<(), LifecycleError> {
1840
2550
  let Value::Map(pairs) = value else {
1841
- return Err(LifecycleError::Compile("agent entry is not a map".to_string()));
2551
+ return Err(LifecycleError::Compile(
2552
+ "agent entry is not a map".to_string(),
2553
+ ));
1842
2554
  };
1843
2555
  if let Some((_, existing)) = pairs.iter_mut().find(|(k, _)| k == key) {
1844
2556
  *existing = next;
@@ -1857,10 +2569,15 @@ fn runtime_with_startup_agent(runtime: &Value, agent_id: &AgentId) -> Value {
1857
2569
  for (key, value) in pairs {
1858
2570
  if key == "startup_order" {
1859
2571
  saw_startup = true;
1860
- let mut order = value.as_list().map(|items| items.to_vec()).unwrap_or_default();
1861
- let already_present = order
1862
- .iter()
1863
- .any(|item| item.as_str().map(|id| id == agent_id.as_str()).unwrap_or(false));
2572
+ let mut order = value
2573
+ .as_list()
2574
+ .map(|items| items.to_vec())
2575
+ .unwrap_or_default();
2576
+ let already_present = order.iter().any(|item| {
2577
+ item.as_str()
2578
+ .map(|id| id == agent_id.as_str())
2579
+ .unwrap_or(false)
2580
+ });
1864
2581
  if !already_present {
1865
2582
  order.push(Value::Str(agent_id.as_str().to_string()));
1866
2583
  }
@@ -1883,6 +2600,12 @@ fn upsert_forked_agent_state(
1883
2600
  source_agent_id: &AgentId,
1884
2601
  as_agent_id: &AgentId,
1885
2602
  spec_agent: &Value,
2603
+ safety: &DangerousApproval,
2604
+ plan: &crate::provider::CommandPlan,
2605
+ profile_launch: &crate::provider::ProviderProfileLaunch,
2606
+ spawn: &crate::transport::SpawnResult,
2607
+ spawn_cwd: &Path,
2608
+ profile_dir: Option<&Path>,
1886
2609
  ) -> Result<(), LifecycleError> {
1887
2610
  if !state.is_object() {
1888
2611
  *state = serde_json::json!({});
@@ -1907,15 +2630,71 @@ fn upsert_forked_agent_state(
1907
2630
  .get("provider")
1908
2631
  .and_then(Value::as_str)
1909
2632
  .unwrap_or("codex");
2633
+ let mut entry = serde_json::Map::new();
2634
+ entry.insert("status".to_string(), serde_json::json!("running"));
2635
+ entry.insert("provider".to_string(), serde_json::json!(provider));
2636
+ entry.insert(
2637
+ "agent_id".to_string(),
2638
+ serde_json::json!(as_agent_id.as_str()),
2639
+ );
2640
+ entry.insert(
2641
+ "window".to_string(),
2642
+ serde_json::json!(as_agent_id.as_str()),
2643
+ );
2644
+ entry.insert(
2645
+ "forked_from".to_string(),
2646
+ serde_json::json!(source_agent_id.as_str()),
2647
+ );
2648
+ entry.insert(
2649
+ "spawn_cwd".to_string(),
2650
+ serde_json::json!(spawn_cwd.to_string_lossy().to_string()),
2651
+ );
2652
+ entry.insert(
2653
+ "pane_id".to_string(),
2654
+ serde_json::json!(spawn.pane_id.as_str()),
2655
+ );
2656
+ if let Some(pid) = spawn.child_pid {
2657
+ entry.insert("pane_pid".to_string(), serde_json::json!(pid));
2658
+ }
2659
+ for key in [
2660
+ "auth_mode",
2661
+ "model",
2662
+ "model_source",
2663
+ "profile",
2664
+ "_profile_dir",
2665
+ "role",
2666
+ ] {
2667
+ if let Some(value) = spec_agent.get(key) {
2668
+ entry.insert(key.to_string(), yaml_value_to_json(value));
2669
+ }
2670
+ }
2671
+ if spec_agent.get("profile").is_some() && !entry.contains_key("_profile_dir") {
2672
+ if let Some(profile_dir) = profile_dir {
2673
+ entry.insert(
2674
+ "_profile_dir".to_string(),
2675
+ serde_json::json!(profile_dir.to_string_lossy().to_string()),
2676
+ );
2677
+ }
2678
+ }
2679
+ entry.insert("session_id".to_string(), serde_json::Value::Null);
2680
+ entry.insert("rollout_path".to_string(), serde_json::Value::Null);
2681
+ entry.insert("captured_at".to_string(), serde_json::Value::Null);
2682
+ entry.insert("captured_via".to_string(), serde_json::Value::Null);
2683
+ entry.insert(
2684
+ "attribution_confidence".to_string(),
2685
+ serde_json::Value::Null,
2686
+ );
2687
+ persist_command_plan_state(&mut entry, plan, profile_launch);
1910
2688
  agent_map.insert(
1911
2689
  as_agent_id.as_str().to_string(),
1912
- serde_json::json!({
1913
- "status": "running",
1914
- "provider": provider,
1915
- "window": as_agent_id.as_str(),
1916
- "forked_from": source_agent_id.as_str(),
1917
- }),
2690
+ serde_json::Value::Object(entry),
1918
2691
  );
2692
+ if let Some(entry) = agent_map
2693
+ .get_mut(as_agent_id.as_str())
2694
+ .and_then(serde_json::Value::as_object_mut)
2695
+ {
2696
+ persist_effective_approval_policy(entry, safety);
2697
+ }
1919
2698
  Ok(())
1920
2699
  }
1921
2700
 
@@ -1947,12 +2726,9 @@ pub(crate) fn ensure_owner_allowed_for_state(
1947
2726
  None,
1948
2727
  )
1949
2728
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1950
- if let Some(refusal) = crate::state::owner_gate::check_team_owner(
1951
- state,
1952
- &caller,
1953
- false,
1954
- &NoopLiveness,
1955
- ) {
2729
+ if let Some(refusal) =
2730
+ crate::state::owner_gate::check_team_owner(state, &caller, false, &NoopLiveness)
2731
+ {
1956
2732
  return Err(LifecycleError::OwnerRefused(refusal.to_string()));
1957
2733
  }
1958
2734
  Ok(())
@@ -1981,7 +2757,10 @@ fn initial_runtime_state(
1981
2757
  let Some(id) = agent.get("id").and_then(Value::as_str) else {
1982
2758
  continue;
1983
2759
  };
1984
- let provider = agent.get("provider").and_then(Value::as_str).unwrap_or("codex");
2760
+ let provider = agent
2761
+ .get("provider")
2762
+ .and_then(Value::as_str)
2763
+ .unwrap_or("codex");
1985
2764
  let role = agent.get("role").and_then(Value::as_str).unwrap_or(id);
1986
2765
  let model = agent.get("model").and_then(Value::as_str);
1987
2766
  let auth_mode = agent.get("auth_mode").and_then(Value::as_str);
@@ -2003,7 +2782,9 @@ fn initial_runtime_state(
2003
2782
  .get("runtime")
2004
2783
  .and_then(|runtime| runtime.get("display_backend"))
2005
2784
  .and_then(Value::as_str)
2006
- .and_then(|backend| serde_json::from_value::<DisplayBackend>(serde_json::json!(backend)).ok());
2785
+ .and_then(|backend| {
2786
+ serde_json::from_value::<DisplayBackend>(serde_json::json!(backend)).ok()
2787
+ });
2007
2788
  let display_backend =
2008
2789
  crate::lifecycle::display::resolve_display_backend(requested_display, None).backend;
2009
2790
  let mut state = serde_json::Map::new();
@@ -2025,11 +2806,16 @@ fn initial_runtime_state(
2025
2806
  );
2026
2807
  state.insert(
2027
2808
  "leader".to_string(),
2028
- spec.get("leader").map(yaml_value_to_json).unwrap_or(serde_json::Value::Null),
2809
+ spec.get("leader")
2810
+ .map(yaml_value_to_json)
2811
+ .unwrap_or(serde_json::Value::Null),
2029
2812
  );
2030
2813
  state.insert("agents".to_string(), serde_json::Value::Object(agents));
2031
2814
  state.insert("tasks".to_string(), spec_tasks_json(spec));
2032
- state.insert("display_backend".to_string(), serde_json::json!(display_backend));
2815
+ state.insert(
2816
+ "display_backend".to_string(),
2817
+ serde_json::json!(display_backend),
2818
+ );
2033
2819
  let mut state = serde_json::Value::Object(state);
2034
2820
  if !seed_launched_owner_from_env(&mut state) {
2035
2821
  let team_id = crate::state::projection::team_state_key(&state);
@@ -2095,12 +2881,17 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
2095
2881
  true
2096
2882
  }
2097
2883
 
2884
+ fn has_positive_caller_leader_env() -> bool {
2885
+ env_nonempty("TEAM_AGENT_LEADER_PANE_ID")
2886
+ || env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID")
2887
+ || env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE")
2888
+ || env_nonempty("TEAM_AGENT_LEADER_PROVIDER")
2889
+ }
2890
+
2098
2891
  fn spec_tasks_json(spec: &Value) -> serde_json::Value {
2099
2892
  spec.get("tasks")
2100
2893
  .and_then(Value::as_list)
2101
- .map(|tasks| {
2102
- serde_json::Value::Array(tasks.iter().map(yaml_value_to_json).collect())
2103
- })
2894
+ .map(|tasks| serde_json::Value::Array(tasks.iter().map(yaml_value_to_json).collect()))
2104
2895
  .unwrap_or_else(|| serde_json::json!([]))
2105
2896
  }
2106
2897
 
@@ -2139,7 +2930,10 @@ fn override_spec_session_name(spec: &mut Value, session_name: &str) {
2139
2930
  if let Some((_, existing)) = runtime.iter_mut().find(|(k, _)| k == "session_name") {
2140
2931
  *existing = Value::Str(session_name.to_string());
2141
2932
  } else {
2142
- runtime.push(("session_name".to_string(), Value::Str(session_name.to_string())));
2933
+ runtime.push((
2934
+ "session_name".to_string(),
2935
+ Value::Str(session_name.to_string()),
2936
+ ));
2143
2937
  }
2144
2938
  }
2145
2939
  Some(other) => {
@@ -2246,20 +3040,11 @@ fn disabled_dangerous_approval() -> DangerousApproval {
2246
3040
  }
2247
3041
  }
2248
3042
 
2249
- pub(crate) fn effective_runtime_config_for_worker_spawn() -> Result<DangerousApproval, LifecycleError> {
3043
+ pub(crate) fn effective_runtime_config_for_worker_spawn(
3044
+ ) -> Result<DangerousApproval, LifecycleError> {
2250
3045
  detect_dangerous_approval()
2251
3046
  }
2252
3047
 
2253
- pub(crate) fn worker_tool_refs(
2254
- mut tools: Vec<String>,
2255
- safety: &DangerousApproval,
2256
- ) -> Vec<String> {
2257
- if safety.enabled && !tools.iter().any(|tool| tool == "dangerous_auto_approve") {
2258
- tools.push("dangerous_auto_approve".to_string());
2259
- }
2260
- tools
2261
- }
2262
-
2263
3048
  fn write_launch_permission_audit(
2264
3049
  workspace: &Path,
2265
3050
  safety: &DangerousApproval,
@@ -2317,6 +3102,5 @@ fn agent_id_exists_in_team_dir(team_dir: &Path, agent_id: &AgentId) -> bool {
2317
3102
  .exists()
2318
3103
  }
2319
3104
 
2320
-
2321
3105
  mod plan;
2322
3106
  pub use plan::{handle_report_result, start_plan};