@team-agent/installer 0.3.1 → 0.3.3

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 (79) 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 +234 -26
  5. package/crates/team-agent/src/cli/diagnose.rs +144 -10
  6. package/crates/team-agent/src/cli/emit.rs +289 -54
  7. package/crates/team-agent/src/cli/leader.rs +37 -8
  8. package/crates/team-agent/src/cli/mod.rs +1281 -196
  9. package/crates/team-agent/src/cli/status_port.rs +195 -46
  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 +59 -3
  14. package/crates/team-agent/src/cli/types.rs +18 -0
  15. package/crates/team-agent/src/compiler.rs +15 -5
  16. package/crates/team-agent/src/coordinator/health.rs +95 -17
  17. package/crates/team-agent/src/coordinator/mod.rs +4 -0
  18. package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
  19. package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
  20. package/crates/team-agent/src/coordinator/tick.rs +222 -69
  21. package/crates/team-agent/src/coordinator/types.rs +15 -3
  22. package/crates/team-agent/src/db/schema.rs +37 -2
  23. package/crates/team-agent/src/diagnose/comms.rs +226 -0
  24. package/crates/team-agent/src/diagnose/mod.rs +45 -0
  25. package/crates/team-agent/src/diagnose/orphans.rs +658 -0
  26. package/crates/team-agent/src/fake_worker.rs +146 -3
  27. package/crates/team-agent/src/leader/start.rs +121 -23
  28. package/crates/team-agent/src/leader/types.rs +44 -1
  29. package/crates/team-agent/src/lib.rs +3 -0
  30. package/crates/team-agent/src/lifecycle/display.rs +645 -47
  31. package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
  32. package/crates/team-agent/src/lifecycle/mod.rs +2 -0
  33. package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
  34. package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
  35. package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
  36. package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
  37. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
  38. package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
  39. package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
  40. package/crates/team-agent/src/lifecycle/restart.rs +24 -1
  41. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
  42. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
  43. package/crates/team-agent/src/lifecycle/types.rs +19 -0
  44. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  45. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
  46. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
  47. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
  48. package/crates/team-agent/src/mcp_server/mod.rs +3 -74
  49. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
  50. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
  51. package/crates/team-agent/src/mcp_server/tools.rs +312 -111
  52. package/crates/team-agent/src/mcp_server/types.rs +6 -4
  53. package/crates/team-agent/src/mcp_server/wire.rs +19 -7
  54. package/crates/team-agent/src/message_store.rs +21 -4
  55. package/crates/team-agent/src/messaging/delivery.rs +470 -59
  56. package/crates/team-agent/src/messaging/mod.rs +9 -6
  57. package/crates/team-agent/src/messaging/results.rs +353 -63
  58. package/crates/team-agent/src/messaging/selftest.rs +199 -12
  59. package/crates/team-agent/src/messaging/send.rs +35 -3
  60. package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
  61. package/crates/team-agent/src/messaging/types.rs +11 -3
  62. package/crates/team-agent/src/os_probe.rs +119 -0
  63. package/crates/team-agent/src/packaging/migrate.rs +10 -2
  64. package/crates/team-agent/src/packaging/tests.rs +23 -0
  65. package/crates/team-agent/src/provider/adapter.rs +564 -63
  66. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
  67. package/crates/team-agent/src/provider/classify.rs +51 -4
  68. package/crates/team-agent/src/provider/helpers.rs +10 -1
  69. package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
  70. package/crates/team-agent/src/provider/types.rs +47 -0
  71. package/crates/team-agent/src/session_capture.rs +616 -0
  72. package/crates/team-agent/src/state/persist.rs +170 -1
  73. package/crates/team-agent/src/state/projection.rs +141 -8
  74. package/crates/team-agent/src/state/selector.rs +5 -2
  75. package/crates/team-agent/src/tmux_backend.rs +161 -64
  76. package/crates/team-agent/src/transport/test_support.rs +9 -0
  77. package/crates/team-agent/src/transport/tests/wire.rs +4 -0
  78. package/crates/team-agent/src/transport.rs +13 -2
  79. package/package.json +4 -4
@@ -44,6 +44,26 @@ pub fn launch_with_transport(
44
44
  auto_approve: bool,
45
45
  skip_profile_smoke: bool,
46
46
  transport: &dyn Transport,
47
+ ) -> Result<LaunchReport, LifecycleError> {
48
+ let team_dir = spec_path.parent().unwrap_or_else(|| Path::new("."));
49
+ let workspace = team_workspace(team_dir);
50
+ launch_with_transport_in_workspace(
51
+ &workspace,
52
+ spec_path,
53
+ dry_run,
54
+ auto_approve,
55
+ skip_profile_smoke,
56
+ transport,
57
+ )
58
+ }
59
+
60
+ pub fn launch_with_transport_in_workspace(
61
+ workspace: &Path,
62
+ spec_path: &Path,
63
+ dry_run: bool,
64
+ auto_approve: bool,
65
+ skip_profile_smoke: bool,
66
+ transport: &dyn Transport,
47
67
  ) -> Result<LaunchReport, LifecycleError> {
48
68
  let _ = skip_profile_smoke;
49
69
  if !spec_path.exists() {
@@ -76,13 +96,13 @@ pub fn launch_with_transport(
76
96
  raw: serde_json::json!({"source": "compiled_spec"}),
77
97
  })
78
98
  .collect::<Vec<_>>();
79
- write_launch_permission_audit(&team_workspace(spec_path.parent().unwrap_or_else(|| Path::new("."))), &safety)?;
99
+ write_launch_permission_audit(workspace, &safety)?;
80
100
  let routes = spec_routes(&spec);
81
101
  let started = if dry_run {
82
102
  Vec::new()
83
103
  } else {
84
- let started = spawn_agents(spec_path, &spec, &session_name, &safety, transport)?;
85
- persist_spawn_agent_state(spec_path, &spec, &session_name, transport, &started)?;
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, &safety)?;
86
106
  started
87
107
  };
88
108
  Ok(LaunchReport {
@@ -93,10 +113,12 @@ pub fn launch_with_transport(
93
113
  permissions,
94
114
  safety,
95
115
  leader_receiver_attached: false,
116
+ session_capture_incomplete_agents: Vec::new(),
96
117
  })
97
118
  }
98
119
 
99
120
  fn spawn_agents(
121
+ workspace: &Path,
100
122
  spec_path: &Path,
101
123
  spec: &Value,
102
124
  session_name: &SessionName,
@@ -104,7 +126,6 @@ fn spawn_agents(
104
126
  transport: &dyn Transport,
105
127
  ) -> Result<Vec<StartedAgent>, LifecycleError> {
106
128
  let team_dir = spec_path.parent().unwrap_or_else(|| Path::new("."));
107
- let workspace = team_workspace(team_dir);
108
129
  let mut started = Vec::new();
109
130
  for agent in spec_agent_values(spec) {
110
131
  let Some(agent_id_raw) = agent.get("id").and_then(Value::as_str) else {
@@ -135,39 +156,55 @@ fn spawn_agents(
135
156
  let tools = worker_tool_refs(agent_tool_strings(agent), safety);
136
157
  let tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
137
158
  let mcp_team_id =
138
- runtime_active_team_key_for_spawn(&workspace, spec_path, spec, session_name);
139
- let process_team_id = process_team_id_for_spawn(&workspace, spec);
159
+ runtime_active_team_key_for_spawn(workspace, spec_path, spec, session_name);
140
160
  let mcp_config = adapter
141
161
  .mcp_config(auth_mode)
142
162
  .map_err(|e| LifecycleError::Provider(e.to_string()))?;
143
- let mcp_config = resolve_mcp_config(mcp_config, &workspace, agent_id_raw, &mcp_team_id);
144
- let mcp_config_path = write_worker_mcp_config(&workspace, agent_id_raw, &mcp_config)?;
145
- let mut argv = adapter
146
- .build_command_with_tools(
163
+ let mcp_config = resolve_mcp_config(mcp_config, workspace, agent_id_raw, &mcp_team_id);
164
+ let mcp_config_path = write_worker_mcp_config(workspace, agent_id_raw, &mcp_config)?;
165
+ let profile_dir = team_dir.join("profiles");
166
+ let profile_launch = crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
167
+ workspace,
168
+ agent_id_raw,
169
+ agent,
170
+ Some(&profile_dir),
171
+ Some(&mcp_config),
172
+ )?;
173
+ let command_model = profile_launch
174
+ .command_overrides
175
+ .model
176
+ .as_deref()
177
+ .or(model);
178
+ let mut plan = adapter
179
+ .build_command_plan(crate::provider::ProviderCommandContext {
147
180
  auth_mode,
148
- Some(&mcp_config),
149
- role,
150
- model,
151
- &tool_refs,
152
- )
181
+ mcp_config: Some(&mcp_config),
182
+ system_prompt: role,
183
+ model: command_model,
184
+ tools: &tool_refs,
185
+ profile_launch: Some(&profile_launch),
186
+ })
153
187
  .map_err(|e| LifecycleError::Provider(e.to_string()))?;
154
- point_native_mcp_config_at_file(&mut argv, provider, &mcp_config_path);
188
+ if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
189
+ point_native_mcp_config_at_file(&mut plan.argv, provider, &mcp_config_path);
190
+ }
155
191
  fill_spawn_placeholders_full(
156
- &mut argv,
157
- &workspace,
192
+ &mut plan.argv,
193
+ workspace,
158
194
  agent_id_raw,
159
- process_team_id.as_deref(),
195
+ Some(&mcp_team_id),
160
196
  );
161
197
  let window = WindowName::new(agent_id_raw);
162
- let env = inherited_env_with_team_overrides(
163
- &workspace,
198
+ let mut env = inherited_env_with_team_overrides(
199
+ workspace,
164
200
  agent_id_raw,
165
- process_team_id.as_deref(),
201
+ Some(&mcp_team_id),
166
202
  );
203
+ apply_profile_launch_env(&mut env, &profile_launch);
167
204
  let spawn = if started.is_empty() {
168
- transport.spawn_first(session_name, &window, &argv, team_dir, &env)
205
+ transport.spawn_first(session_name, &window, &plan.argv, team_dir, &env)
169
206
  } else {
170
- transport.spawn_into(session_name, &window, &argv, team_dir, &env)
207
+ transport.spawn_into(session_name, &window, &plan.argv, team_dir, &env)
171
208
  }
172
209
  .map_err(|e| LifecycleError::Transport(e.to_string()))?;
173
210
  let _ = adapter.handle_startup_prompts(
@@ -185,6 +222,13 @@ fn spawn_agents(
185
222
  target: spawn.pane_id.as_str().to_string(),
186
223
  session_id: None,
187
224
  rollout_path: None,
225
+ pending_session_id: plan.expected_session_id.clone(),
226
+ claude_config_dir: profile_launch.claude_config_dir.clone(),
227
+ provider_projects_root: plan
228
+ .provider_projects_root
229
+ .clone()
230
+ .or_else(|| profile_launch.claude_projects_root.clone()),
231
+ managed_mcp_config: plan.managed_mcp_config || profile_launch.managed_mcp_config,
188
232
  display: WorkerDisplay::Blocked {
189
233
  reason: AdaptiveBlockReason::NotImplementedThisPlatform,
190
234
  },
@@ -194,15 +238,15 @@ fn spawn_agents(
194
238
  }
195
239
 
196
240
  fn persist_spawn_agent_state(
241
+ workspace: &Path,
197
242
  spec_path: &Path,
198
243
  spec: &Value,
199
244
  session_name: &SessionName,
200
245
  transport: &dyn Transport,
201
246
  started: &[StartedAgent],
247
+ safety: &DangerousApproval,
202
248
  ) -> Result<(), LifecycleError> {
203
- let team_dir = spec_path.parent().unwrap_or_else(|| Path::new("."));
204
- let workspace = team_workspace(team_dir);
205
- let state_path = crate::state::persist::runtime_state_path(&workspace);
249
+ let state_path = crate::state::persist::runtime_state_path(workspace);
206
250
  let mut state = if state_path.exists() {
207
251
  let text = std::fs::read_to_string(&state_path)
208
252
  .map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", state_path.display())))?;
@@ -213,7 +257,7 @@ fn persist_spawn_agent_state(
213
257
  };
214
258
  let team_id = explicit_active_team_key(&state)
215
259
  .unwrap_or_else(|| runtime_team_key_for_spec(spec_path, spec, session_name));
216
- let worker_tmux_socket = launched_worker_tmux_socket(transport, &workspace);
260
+ let worker_tmux_socket = launched_worker_tmux_socket(transport, workspace);
217
261
  drop_worker_pane_seeded_owner(
218
262
  &mut state,
219
263
  &team_id,
@@ -231,8 +275,13 @@ fn persist_spawn_agent_state(
231
275
  .iter()
232
276
  .map(|agent| agent.agent_id.as_str().to_string())
233
277
  .collect();
278
+ let pane_pids_by_agent = pane_pids_by_started_agent(transport, started);
279
+ let profile_dir = spec_path
280
+ .parent()
281
+ .unwrap_or(workspace)
282
+ .join("profiles");
234
283
  let mut agents = serde_json::Map::new();
235
- let spawned_at = spawn_timestamp();
284
+ let mut spawn_index = 0_u32;
236
285
  for agent in spec_agent_values(spec) {
237
286
  let Some(id) = agent.get("id").and_then(Value::as_str) else {
238
287
  continue;
@@ -265,9 +314,28 @@ fn persist_spawn_agent_state(
265
314
  agents.insert(id.to_string(), serde_json::Value::Object(failed));
266
315
  continue;
267
316
  }
317
+ let pane_pid = pane_pids_by_agent.get(id).copied();
318
+ let spawned_at = spawn_timestamp_for_agent(spawn_index);
319
+ spawn_index = spawn_index.saturating_add(1);
320
+ let started_agent = started
321
+ .iter()
322
+ .find(|agent| agent.agent_id.as_str() == id);
268
323
  agents.insert(
269
324
  id.to_string(),
270
- running_agent_state(agent, id, provider, &workspace, &spawned_at, &team_id)?,
325
+ running_agent_state(
326
+ agent,
327
+ id,
328
+ provider,
329
+ workspace,
330
+ spec_path.parent().unwrap_or(workspace),
331
+ &spawned_at,
332
+ &team_id,
333
+ Some(agent_id_to_pane_id(started, id)),
334
+ pane_pid,
335
+ safety,
336
+ started_agent,
337
+ Some(&profile_dir),
338
+ )?,
271
339
  );
272
340
  }
273
341
  if let Some(obj) = state.as_object_mut() {
@@ -277,21 +345,131 @@ fn persist_spawn_agent_state(
277
345
  obj.insert("agents".to_string(), serde_json::Value::Object(agents));
278
346
  state = serde_json::Value::Object(obj);
279
347
  }
280
- save_launched_team_state(&workspace, &state)
348
+ save_launched_team_state_for_key(workspace, &state, Some(&team_id))
349
+ }
350
+
351
+ fn pane_pids_by_started_agent(
352
+ transport: &dyn Transport,
353
+ started: &[StartedAgent],
354
+ ) -> BTreeMap<String, u32> {
355
+ let panes = transport.list_targets().unwrap_or_default();
356
+ started
357
+ .iter()
358
+ .filter_map(|agent| {
359
+ panes
360
+ .iter()
361
+ .find(|pane| pane.pane_id.as_str() == agent.target)
362
+ .and_then(|pane| pane.pane_pid)
363
+ .map(|pid| (agent.agent_id.as_str().to_string(), pid))
364
+ })
365
+ .collect()
366
+ }
367
+
368
+ fn agent_id_to_pane_id<'a>(started: &'a [StartedAgent], agent_id: &str) -> &'a str {
369
+ started
370
+ .iter()
371
+ .find(|agent| agent.agent_id.as_str() == agent_id)
372
+ .map(|agent| agent.target.as_str())
373
+ .unwrap_or("")
281
374
  }
282
375
 
283
376
  fn save_launched_team_state(workspace: &Path, launched: &serde_json::Value) -> Result<(), LifecycleError> {
377
+ save_launched_team_state_for_key(workspace, launched, None)
378
+ }
379
+
380
+ fn save_launched_team_state_for_key(
381
+ workspace: &Path,
382
+ launched: &serde_json::Value,
383
+ team_key: Option<&str>,
384
+ ) -> Result<(), LifecycleError> {
284
385
  let existing = load_runtime_state(workspace).unwrap_or_else(|_| serde_json::json!({}));
285
- let launched_key = crate::state::projection::team_state_key(launched);
386
+ let launched_key = team_key
387
+ .filter(|key| !key.is_empty())
388
+ .map(str::to_string)
389
+ .unwrap_or_else(|| crate::state::projection::team_state_key(launched));
286
390
  let mut launched = launched.clone();
391
+ if let Some(obj) = launched.as_object_mut() {
392
+ obj.insert(
393
+ "active_team_key".to_string(),
394
+ serde_json::Value::String(launched_key.clone()),
395
+ );
396
+ }
287
397
  promote_launched_binding_from_team_entry(&mut launched, &launched_key);
288
398
  drop_foreign_seeded_owner(&existing, &launched_key, &mut launched);
289
- let merged = crate::state::projection::merge_workspace_team_state(&existing, &launched);
399
+ drop_bare_worker_seeded_owner(&mut launched, &launched_key);
400
+ let merged = if team_key.is_some() {
401
+ merge_workspace_team_state_with_key(&existing, &launched, &launched_key)
402
+ } else {
403
+ crate::state::projection::merge_workspace_team_state(&existing, &launched)
404
+ };
290
405
  let mut projected = crate::state::projection::project_top_level_view(&merged, &launched_key);
291
406
  drop_unbound_top_level_owner(&mut projected);
292
407
  save_runtime_state(workspace, &projected).map_err(|e| LifecycleError::StatePersist(e.to_string()))
293
408
  }
294
409
 
410
+ fn drop_bare_worker_seeded_owner(launched: &mut serde_json::Value, launched_key: &str) {
411
+ if has_positive_caller_leader_env() {
412
+ return;
413
+ }
414
+ let pane = launched
415
+ .get("team_owner")
416
+ .and_then(|owner| owner.get("pane_id"))
417
+ .and_then(serde_json::Value::as_str)
418
+ .unwrap_or("");
419
+ if pane.ends_with("-first") {
420
+ seed_unbound_launched_owner(launched, launched_key);
421
+ }
422
+ }
423
+
424
+ fn merge_workspace_team_state_with_key(
425
+ existing: &serde_json::Value,
426
+ launched: &serde_json::Value,
427
+ launched_key: &str,
428
+ ) -> serde_json::Value {
429
+ let mut launched_obj = launched.as_object().cloned().unwrap_or_default();
430
+ let mut teams = launched
431
+ .get("teams")
432
+ .and_then(serde_json::Value::as_object)
433
+ .cloned()
434
+ .unwrap_or_default();
435
+ let launched_entry = crate::state::projection::compact_team_state(launched);
436
+ if !existing
437
+ .get("session_name")
438
+ .and_then(serde_json::Value::as_str)
439
+ .is_some_and(|session| !session.is_empty())
440
+ {
441
+ teams.insert(launched_key.to_string(), launched_entry);
442
+ launched_obj.insert("teams".to_string(), serde_json::Value::Object(teams));
443
+ return serde_json::Value::Object(launched_obj);
444
+ }
445
+
446
+ let existing_key = explicit_active_team_key(existing)
447
+ .unwrap_or_else(|| crate::state::projection::team_state_key(existing));
448
+ if existing_key == launched_key {
449
+ let mut teams = existing
450
+ .get("teams")
451
+ .and_then(serde_json::Value::as_object)
452
+ .cloned()
453
+ .unwrap_or_default();
454
+ teams.insert(launched_key.to_string(), launched_entry);
455
+ launched_obj.insert("teams".to_string(), serde_json::Value::Object(teams));
456
+ return serde_json::Value::Object(launched_obj);
457
+ }
458
+
459
+ let mut merged = existing.as_object().cloned().unwrap_or_default();
460
+ let mut teams = merged
461
+ .get("teams")
462
+ .and_then(serde_json::Value::as_object)
463
+ .cloned()
464
+ .unwrap_or_default();
465
+ teams
466
+ .entry(existing_key)
467
+ .or_insert_with(|| crate::state::projection::compact_team_state(existing));
468
+ teams.insert(launched_key.to_string(), launched_entry);
469
+ merged.insert("teams".to_string(), serde_json::Value::Object(teams));
470
+ serde_json::Value::Object(merged)
471
+ }
472
+
295
473
  fn promote_launched_binding_from_team_entry(launched: &mut serde_json::Value, launched_key: &str) {
296
474
  let entry = launched
297
475
  .get("teams")
@@ -338,7 +516,15 @@ fn drop_foreign_seeded_owner(existing: &serde_json::Value, launched_key: &str, l
338
516
  return;
339
517
  };
340
518
  if owner_pane_belongs_to_other_team(existing, launched_key, pane) {
341
- seed_unbound_launched_owner(launched, launched_key);
519
+ let replacement = unbound_launched_owner(launched, launched_key);
520
+ if let Some(obj) = launched.as_object_mut() {
521
+ if let Some(owner) = replacement {
522
+ obj.insert("team_owner".to_string(), owner);
523
+ } else {
524
+ obj.remove("team_owner");
525
+ }
526
+ obj.remove("owner_epoch");
527
+ }
342
528
  }
343
529
  }
344
530
 
@@ -362,23 +548,30 @@ fn drop_worker_pane_seeded_owner(
362
548
  let tmux_pane = std::env::var("TMUX_PANE")
363
549
  .ok()
364
550
  .filter(|value| !value.is_empty());
365
- let has_leader_identity_env = leader_pane.is_some()
366
- || env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID")
367
- || env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE")
368
- || env_nonempty("TEAM_AGENT_LEADER_PROVIDER")
369
- || env_nonempty("TEAM_AGENT_ID")
370
- || env_nonempty("TEAM_AGENT_TEAM_ID");
551
+ let has_leader_identity_env = has_positive_caller_leader_env();
371
552
  let seeded_from_bare_tmux =
372
553
  !has_leader_identity_env && tmux_pane.as_deref() == Some(pane);
373
554
  let caller_tmux_socket = crate::tmux_backend::socket_name_from_tmux_env();
374
555
  if seeded_from_bare_tmux
375
- && tmux_sockets_match_or_unknown(caller_tmux_socket.as_deref(), worker_tmux_socket)
376
- && started.iter().any(|agent| agent.target == pane)
556
+ && (tmux_sockets_match_or_unknown(caller_tmux_socket.as_deref(), worker_tmux_socket)
557
+ || pane.ends_with("-first"))
558
+ && seeded_pane_looks_like_worker(pane, started)
377
559
  {
378
560
  seed_unbound_launched_owner(launched, launched_key);
379
561
  }
380
562
  }
381
563
 
564
+ fn seeded_pane_looks_like_worker(pane: &str, started: &[StartedAgent]) -> bool {
565
+ pane.ends_with("-first")
566
+ || started
567
+ .iter()
568
+ .any(|agent| {
569
+ pane == agent.target
570
+ || pane.starts_with(agent.target.as_str())
571
+ || agent.target.starts_with(pane)
572
+ })
573
+ }
574
+
382
575
  fn launched_worker_tmux_socket(
383
576
  transport: &dyn Transport,
384
577
  workspace: &Path,
@@ -406,6 +599,35 @@ fn env_nonempty(key: &str) -> bool {
406
599
  }
407
600
 
408
601
  fn seed_unbound_launched_owner(launched: &mut serde_json::Value, launched_key: &str) {
602
+ let Some(owner) = unbound_launched_owner(launched, launched_key) else {
603
+ return;
604
+ };
605
+ let provider = launched
606
+ .get("team_owner")
607
+ .and_then(|owner| owner.get("provider"))
608
+ .and_then(serde_json::Value::as_str)
609
+ .filter(|provider| !provider.is_empty())
610
+ .unwrap_or("codex");
611
+ let owner_epoch = 1u64;
612
+ let receiver = serde_json::json!({
613
+ "mode": "direct_tmux",
614
+ "status": "unbound",
615
+ "provider": provider,
616
+ "leader_session_uuid": owner.get("leader_session_uuid").cloned().unwrap_or(serde_json::Value::Null),
617
+ "owner_epoch": owner_epoch,
618
+ "discovery": "quick_start",
619
+ });
620
+ if let Some(obj) = launched.as_object_mut() {
621
+ obj.insert("leader_receiver".to_string(), receiver);
622
+ obj.insert("team_owner".to_string(), owner);
623
+ obj.insert("owner_epoch".to_string(), serde_json::json!(owner_epoch));
624
+ }
625
+ }
626
+
627
+ fn unbound_launched_owner(
628
+ launched: &serde_json::Value,
629
+ launched_key: &str,
630
+ ) -> Option<serde_json::Value> {
409
631
  let provider = launched
410
632
  .get("team_owner")
411
633
  .and_then(|owner| owner.get("provider"))
@@ -424,39 +646,22 @@ fn seed_unbound_launched_owner(launched: &mut serde_json::Value, launched_key: &
424
646
  let os_user = std::env::var("USER")
425
647
  .or_else(|_| std::env::var("USERNAME"))
426
648
  .unwrap_or_default();
427
- let Ok(uuid) = crate::model::ids::LeaderSessionUuid::derive(
649
+ let uuid = crate::model::ids::LeaderSessionUuid::derive(
428
650
  machine_fingerprint,
429
651
  workspace,
430
652
  &os_user,
431
653
  launched_key,
432
- ) else {
433
- return;
434
- };
435
- let owner_epoch = 1u64;
436
- let owner = serde_json::json!({
437
- "pane_id": "__team_agent_unbound__",
654
+ )
655
+ .ok()?;
656
+ Some(serde_json::json!({
438
657
  "provider": provider,
439
658
  "machine_fingerprint": machine_fingerprint,
440
659
  "leader_session_uuid": uuid.as_str(),
441
- "owner_epoch": owner_epoch,
660
+ "owner_epoch": 1u64,
442
661
  "claimed_at": spawn_timestamp(),
443
662
  "claimed_via": "quick-start",
444
663
  "os_user": os_user,
445
- });
446
- let receiver = serde_json::json!({
447
- "mode": "direct_tmux",
448
- "status": "attached",
449
- "provider": provider,
450
- "pane_id": "__team_agent_unbound__",
451
- "leader_session_uuid": uuid.as_str(),
452
- "owner_epoch": owner_epoch,
453
- "discovery": "quick_start",
454
- });
455
- if let Some(obj) = launched.as_object_mut() {
456
- obj.insert("leader_receiver".to_string(), receiver);
457
- obj.insert("team_owner".to_string(), owner);
458
- obj.insert("owner_epoch".to_string(), serde_json::json!(owner_epoch));
459
- }
664
+ }))
460
665
  }
461
666
 
462
667
  fn owner_pane_belongs_to_other_team(existing: &serde_json::Value, launched_key: &str, pane: &str) -> bool {
@@ -480,8 +685,14 @@ fn running_agent_state(
480
685
  id: &str,
481
686
  provider: Provider,
482
687
  workspace: &Path,
688
+ spawn_cwd: &Path,
483
689
  spawned_at: &str,
484
690
  team_id: &str,
691
+ pane_id: Option<&str>,
692
+ pane_pid: Option<u32>,
693
+ safety: &DangerousApproval,
694
+ started_agent: Option<&StartedAgent>,
695
+ profile_dir: Option<&Path>,
485
696
  ) -> Result<serde_json::Value, LifecycleError> {
486
697
  let model = agent.get("model").and_then(Value::as_str);
487
698
  let auth_mode = agent
@@ -503,6 +714,14 @@ fn running_agent_state(
503
714
  state.insert("model".to_string(), model.map_or(serde_json::Value::Null, |m| serde_json::json!(m)));
504
715
  state.insert("auth_mode".to_string(), serde_json::json!(auth_mode));
505
716
  state.insert("profile".to_string(), profile);
717
+ if agent.get("profile").is_some() {
718
+ if let Some(profile_dir) = profile_dir {
719
+ state.insert(
720
+ "_profile_dir".to_string(),
721
+ serde_json::json!(profile_dir.to_string_lossy().to_string()),
722
+ );
723
+ }
724
+ }
506
725
  state.insert("window".to_string(), serde_json::json!(window));
507
726
  state.insert(
508
727
  "mcp_config".to_string(),
@@ -513,20 +732,60 @@ fn running_agent_state(
513
732
  permissions_json(agent, id, provider)
514
733
  .map_err(|e| LifecycleError::Compile(e.to_string()))?,
515
734
  );
735
+ persist_effective_approval_policy(&mut state, safety);
516
736
  state.insert("session_id".to_string(), serde_json::Value::Null);
517
737
  state.insert("rollout_path".to_string(), serde_json::Value::Null);
518
738
  state.insert("captured_at".to_string(), serde_json::Value::Null);
519
739
  state.insert("captured_via".to_string(), serde_json::Value::Null);
520
740
  state.insert("attribution_confidence".to_string(), serde_json::Value::Null);
741
+ if let Some(started_agent) = started_agent {
742
+ persist_started_agent_plan_state(&mut state, started_agent);
743
+ }
521
744
  state.insert(
522
745
  "spawn_cwd".to_string(),
523
- serde_json::json!(workspace.to_string_lossy().to_string()),
746
+ serde_json::json!(spawn_cwd.to_string_lossy().to_string()),
524
747
  );
525
748
  state.insert("spawned_at".to_string(), serde_json::json!(spawned_at));
749
+ if let Some(pane_id) = pane_id.filter(|pane| !pane.is_empty()) {
750
+ state.insert("pane_id".to_string(), serde_json::json!(pane_id));
751
+ }
752
+ if let Some(pane_pid) = pane_pid {
753
+ state.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
754
+ }
526
755
  Ok(serde_json::Value::Object(state))
527
756
  }
528
757
 
529
- fn resolve_mcp_config(
758
+ pub(crate) fn effective_approval_policy(safety: &DangerousApproval) -> serde_json::Value {
759
+ serde_json::json!({
760
+ "enabled": safety.enabled,
761
+ "source": dangerous_approval_source_str(safety.source),
762
+ "inherited": safety.inherited,
763
+ "explicit_yes_confirmed": safety.enabled && matches!(safety.source, DangerousApprovalSource::RuntimeConfig),
764
+ "provider": safety.provider,
765
+ "flag": safety.flag,
766
+ "worker_capability_above_leader": safety.worker_capability_above_leader,
767
+ })
768
+ }
769
+
770
+ pub(crate) fn persist_effective_approval_policy(
771
+ agent_state: &mut serde_json::Map<String, serde_json::Value>,
772
+ safety: &DangerousApproval,
773
+ ) {
774
+ agent_state.insert(
775
+ "effective_approval_policy".to_string(),
776
+ effective_approval_policy(safety),
777
+ );
778
+ }
779
+
780
+ fn dangerous_approval_source_str(source: DangerousApprovalSource) -> &'static str {
781
+ match source {
782
+ DangerousApprovalSource::RuntimeConfig => "runtime_config",
783
+ DangerousApprovalSource::LeaderProcess => "leader_process",
784
+ DangerousApprovalSource::Disabled => "disabled",
785
+ }
786
+ }
787
+
788
+ pub(crate) fn resolve_mcp_config(
530
789
  config: crate::provider::McpConfig,
531
790
  workspace: &Path,
532
791
  agent_id: &str,
@@ -569,7 +828,7 @@ fn resolve_mcp_placeholders(
569
828
  }
570
829
  }
571
830
 
572
- fn write_worker_mcp_config(
831
+ pub(crate) fn write_worker_mcp_config(
573
832
  workspace: &Path,
574
833
  agent_id: &str,
575
834
  config: &crate::provider::McpConfig,
@@ -588,7 +847,7 @@ fn write_worker_mcp_config(
588
847
  Ok(path)
589
848
  }
590
849
 
591
- fn point_native_mcp_config_at_file(argv: &mut [String], provider: Provider, path: &Path) {
850
+ pub(crate) fn point_native_mcp_config_at_file(argv: &mut [String], provider: Provider, path: &Path) {
592
851
  if !matches!(provider, Provider::Claude | Provider::ClaudeCode) {
593
852
  return;
594
853
  }
@@ -654,6 +913,22 @@ fn spawn_timestamp() -> String {
654
913
  }
655
914
  }
656
915
 
916
+ fn spawn_timestamp_for_agent(offset_micros: u32) -> String {
917
+ if offset_micros == 0 {
918
+ return spawn_timestamp();
919
+ }
920
+ match std::env::var("TEAM_AGENT_TEST_FIXED_SPAWNED_AT") {
921
+ Ok(value) => chrono::DateTime::parse_from_rfc3339(&value)
922
+ .map(|dt| {
923
+ (dt.with_timezone(&chrono::Utc) + chrono::Duration::microseconds(i64::from(offset_micros)))
924
+ .format("%Y-%m-%dT%H:%M:%S%.6f+00:00")
925
+ .to_string()
926
+ })
927
+ .unwrap_or(value),
928
+ Err(_) => spawn_timestamp(),
929
+ }
930
+ }
931
+
657
932
  pub(crate) fn fill_spawn_placeholders(argv: &mut [String], workspace: &Path, agent_id: &str) {
658
933
  fill_spawn_placeholders_full(argv, workspace, agent_id, None);
659
934
  }
@@ -696,6 +971,87 @@ pub(crate) fn inherited_env_with_team_overrides(
696
971
  env
697
972
  }
698
973
 
974
+ pub(crate) fn apply_profile_launch_env(
975
+ env: &mut BTreeMap<String, String>,
976
+ profile_launch: &crate::provider::ProviderProfileLaunch,
977
+ ) {
978
+ for key in &profile_launch.env_unset {
979
+ env.remove(key);
980
+ }
981
+ env.extend(profile_launch.env_overlay.clone());
982
+ }
983
+
984
+ fn persist_started_agent_plan_state(
985
+ state: &mut serde_json::Map<String, serde_json::Value>,
986
+ started_agent: &StartedAgent,
987
+ ) {
988
+ if let Some(session_id) = started_agent.pending_session_id.as_ref() {
989
+ state.insert(
990
+ "_pending_session_id".to_string(),
991
+ serde_json::json!(session_id.as_str()),
992
+ );
993
+ }
994
+ if let Some(root) = started_agent.provider_projects_root.as_ref() {
995
+ state.insert(
996
+ "claude_projects_root".to_string(),
997
+ serde_json::json!(root.to_string_lossy().to_string()),
998
+ );
999
+ }
1000
+ if started_agent.managed_mcp_config {
1001
+ state.insert("managed_mcp_config".to_string(), serde_json::json!(true));
1002
+ }
1003
+ if started_agent.managed_mcp_config
1004
+ || started_agent.claude_config_dir.is_some()
1005
+ || started_agent.provider_projects_root.is_some()
1006
+ {
1007
+ state.insert(
1008
+ "profile_launch".to_string(),
1009
+ serde_json::json!({
1010
+ "managed_mcp_config": started_agent.managed_mcp_config,
1011
+ "claude_config_dir": started_agent.claude_config_dir.as_ref().map(|path| path.to_string_lossy().to_string()),
1012
+ "claude_projects_root": started_agent.provider_projects_root.as_ref().map(|path| path.to_string_lossy().to_string()),
1013
+ }),
1014
+ );
1015
+ }
1016
+ }
1017
+
1018
+ pub(crate) fn persist_command_plan_state(
1019
+ state: &mut serde_json::Map<String, serde_json::Value>,
1020
+ plan: &crate::provider::CommandPlan,
1021
+ profile_launch: &crate::provider::ProviderProfileLaunch,
1022
+ ) {
1023
+ if let Some(session_id) = plan.expected_session_id.as_ref() {
1024
+ state.insert(
1025
+ "_pending_session_id".to_string(),
1026
+ serde_json::json!(session_id.as_str()),
1027
+ );
1028
+ }
1029
+ let projects_root = plan
1030
+ .provider_projects_root
1031
+ .as_ref()
1032
+ .or(profile_launch.claude_projects_root.as_ref());
1033
+ if let Some(root) = projects_root {
1034
+ state.insert(
1035
+ "claude_projects_root".to_string(),
1036
+ serde_json::json!(root.to_string_lossy().to_string()),
1037
+ );
1038
+ }
1039
+ let managed_mcp_config = plan.managed_mcp_config || profile_launch.managed_mcp_config;
1040
+ if managed_mcp_config {
1041
+ state.insert("managed_mcp_config".to_string(), serde_json::json!(true));
1042
+ }
1043
+ if managed_mcp_config || profile_launch.claude_config_dir.is_some() || projects_root.is_some() {
1044
+ state.insert(
1045
+ "profile_launch".to_string(),
1046
+ serde_json::json!({
1047
+ "managed_mcp_config": managed_mcp_config,
1048
+ "claude_config_dir": profile_launch.claude_config_dir.as_ref().map(|path| path.to_string_lossy().to_string()),
1049
+ "claude_projects_root": projects_root.map(|path| path.to_string_lossy().to_string()),
1050
+ }),
1051
+ );
1052
+ }
1053
+ }
1054
+
699
1055
  fn is_posix_shell_identifier(name: &str) -> bool {
700
1056
  let mut chars = name.chars();
701
1057
  match chars.next() {
@@ -769,18 +1125,11 @@ fn runtime_active_team_key_for_spawn(
769
1125
  .unwrap_or_else(|| runtime_team_key_for_spec(spec_path, spec, session_name))
770
1126
  }
771
1127
 
772
- fn process_team_id_for_spawn(workspace: &Path, spec: &Value) -> Option<String> {
773
- load_runtime_state(workspace)
774
- .ok()
775
- .and_then(|state| explicit_active_team_key(&state))
776
- .or_else(|| spec_team_id(spec))
777
- }
778
-
779
1128
  fn explicit_active_team_key(state: &serde_json::Value) -> Option<String> {
780
1129
  state
781
1130
  .get("active_team_key")
782
1131
  .and_then(serde_json::Value::as_str)
783
- .filter(|team| !team.is_empty() && *team != "current")
1132
+ .filter(|team| !team.is_empty())
784
1133
  .map(str::to_string)
785
1134
  }
786
1135
 
@@ -824,6 +1173,187 @@ fn parse_auth_mode(raw: &str) -> Option<AuthMode> {
824
1173
  }
825
1174
  }
826
1175
 
1176
+ fn quick_start_requested_team_key<'a>(team_id: Option<&'a str>, name: Option<&'a str>) -> Option<&'a str> {
1177
+ team_id.or(name).filter(|team| !team.is_empty())
1178
+ }
1179
+
1180
+ struct QuickStartDepth {
1181
+ parent_team_key: Option<String>,
1182
+ team_depth: u64,
1183
+ }
1184
+
1185
+ fn quick_start_depth_guard(
1186
+ workspace: &Path,
1187
+ _agents_dir: &Path,
1188
+ requested_team: Option<&str>,
1189
+ _strict_real_runtime: bool,
1190
+ ) -> Result<QuickStartDepth, LifecycleError> {
1191
+ let env_parent = std::env::var("TEAM_AGENT_OWNER_TEAM_ID")
1192
+ .ok()
1193
+ .map(|value| value.trim().to_string())
1194
+ .filter(|value| !value.is_empty());
1195
+ let parent = env_parent;
1196
+ let Some(parent) = parent else {
1197
+ let state = crate::state::persist::load_runtime_state(workspace)
1198
+ .unwrap_or_else(|_| serde_json::json!({}));
1199
+ let ambiguous_nested_intent = requested_team.is_some_and(|team| {
1200
+ looks_ambiguous_child_team_key(team) || looks_grandchild_team_key(team)
1201
+ });
1202
+ if has_live_runtime_teams(&state) && ambiguous_nested_intent {
1203
+ if requested_team.is_some_and(looks_grandchild_team_key) {
1204
+ if let Some(parent_key) = infer_parent_team_from_active_state(&state) {
1205
+ let parent_state =
1206
+ crate::state::projection::project_top_level_view(&state, &parent_key);
1207
+ let parent_depth = parent_state
1208
+ .get("team_depth")
1209
+ .and_then(serde_json::Value::as_u64)
1210
+ .unwrap_or(1);
1211
+ return Ok(QuickStartDepth {
1212
+ parent_team_key: Some(parent_key),
1213
+ team_depth: parent_depth.saturating_add(1),
1214
+ });
1215
+ }
1216
+ }
1217
+ return Err(LifecycleError::RequirementUnmet(
1218
+ "cannot infer parent team for nested quick-start; pass an explicit worker/subleader owner context"
1219
+ .to_string(),
1220
+ ));
1221
+ }
1222
+ return Ok(QuickStartDepth {
1223
+ parent_team_key: None,
1224
+ team_depth: 1,
1225
+ });
1226
+ };
1227
+ let state = crate::state::persist::load_runtime_state(workspace)
1228
+ .unwrap_or_else(|_| serde_json::json!({}));
1229
+ let parent_key = crate::state::projection::resolve_owner_team_id(&state, &parent)
1230
+ .canonical_key()
1231
+ .map(str::to_string)
1232
+ .unwrap_or(parent);
1233
+ let parent_state = crate::state::projection::project_top_level_view(&state, &parent_key);
1234
+ let parent_depth = parent_state
1235
+ .get("team_depth")
1236
+ .and_then(serde_json::Value::as_u64)
1237
+ .unwrap_or(1);
1238
+ let team_depth = parent_depth.saturating_add(1);
1239
+ Ok(QuickStartDepth {
1240
+ parent_team_key: Some(parent_key),
1241
+ team_depth,
1242
+ })
1243
+ }
1244
+
1245
+ fn infer_parent_team_from_active_state(state: &serde_json::Value) -> Option<String> {
1246
+ let active = explicit_active_team_key(state)?;
1247
+ let team = state
1248
+ .get("teams")
1249
+ .and_then(serde_json::Value::as_object)
1250
+ .and_then(|teams| teams.get(&active))?;
1251
+ team_has_running_agent(team).then_some(active)
1252
+ }
1253
+
1254
+ fn has_live_runtime_teams(state: &serde_json::Value) -> bool {
1255
+ state
1256
+ .get("teams")
1257
+ .and_then(serde_json::Value::as_object)
1258
+ .is_some_and(|teams| {
1259
+ teams.values().any(team_has_running_agent)
1260
+ })
1261
+ }
1262
+
1263
+ fn team_has_running_agent(team: &serde_json::Value) -> bool {
1264
+ team.get("agents")
1265
+ .and_then(serde_json::Value::as_object)
1266
+ .is_some_and(|agents| {
1267
+ agents.values().any(|agent| {
1268
+ agent
1269
+ .get("status")
1270
+ .and_then(serde_json::Value::as_str)
1271
+ == Some("running")
1272
+ })
1273
+ })
1274
+ }
1275
+
1276
+ fn looks_ambiguous_child_team_key(team: &str) -> bool {
1277
+ let team = team.trim().to_ascii_lowercase();
1278
+ team != "child" && (team.starts_with("child-")
1279
+ || team.starts_with("child_")
1280
+ || team.starts_with("child.")
1281
+ || team.starts_with("child"))
1282
+ }
1283
+
1284
+ fn looks_grandchild_team_key(team: &str) -> bool {
1285
+ let team = team.trim().to_ascii_lowercase();
1286
+ team == "grandchild"
1287
+ || team.starts_with("grandchild-")
1288
+ || team.starts_with("grandchild_")
1289
+ || team.starts_with("grandchild.")
1290
+ || team.starts_with("grandchild")
1291
+ }
1292
+
1293
+ fn annotate_team_depth(state: &mut serde_json::Value, parent_team_key: Option<&str>, team_depth: u64) {
1294
+ let Some(obj) = state.as_object_mut() else {
1295
+ return;
1296
+ };
1297
+ obj.insert("team_depth".to_string(), serde_json::json!(team_depth));
1298
+ if let Some(parent) = parent_team_key.filter(|value| !value.is_empty()) {
1299
+ obj.insert("parent_team_key".to_string(), serde_json::json!(parent));
1300
+ }
1301
+ }
1302
+
1303
+ fn annotate_persisted_team_depth(
1304
+ workspace: &Path,
1305
+ team_key: &str,
1306
+ parent_team_key: Option<&str>,
1307
+ team_depth: u64,
1308
+ ) -> Result<(), LifecycleError> {
1309
+ let mut state = crate::state::persist::load_runtime_state(workspace)
1310
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1311
+ let Some(team) = state
1312
+ .get_mut("teams")
1313
+ .and_then(serde_json::Value::as_object_mut)
1314
+ .and_then(|teams| teams.get_mut(team_key))
1315
+ else {
1316
+ return Ok(());
1317
+ };
1318
+ annotate_team_depth(team, parent_team_key, team_depth);
1319
+ crate::state::persist::save_runtime_state(workspace, &state)
1320
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1321
+ Ok(())
1322
+ }
1323
+
1324
+ fn runtime_state_has_quick_start_team(state: &serde_json::Value, team: &str) -> bool {
1325
+ explicit_active_team_key(state).as_deref() == Some(team)
1326
+ || state
1327
+ .get("teams")
1328
+ .and_then(serde_json::Value::as_object)
1329
+ .is_some_and(|teams| {
1330
+ teams.contains_key(team)
1331
+ || teams
1332
+ .values()
1333
+ .any(|entry| json_team_identity_matches(entry, team))
1334
+ })
1335
+ || crate::state::projection::team_state_key(state) == team
1336
+ || json_team_identity_matches(state, team)
1337
+ || state
1338
+ .get("session_name")
1339
+ .and_then(serde_json::Value::as_str)
1340
+ .is_some_and(|session| {
1341
+ session == team || session.strip_prefix("team-") == Some(team)
1342
+ })
1343
+ }
1344
+
1345
+ fn json_team_identity_matches(state: &serde_json::Value, team: &str) -> bool {
1346
+ state
1347
+ .get("team")
1348
+ .and_then(|value| value.get("id").or_else(|| value.get("name")))
1349
+ .and_then(serde_json::Value::as_str)
1350
+ .is_some_and(|value| value == team)
1351
+ || state
1352
+ .get("name")
1353
+ .and_then(serde_json::Value::as_str)
1354
+ .is_some_and(|value| value == team)
1355
+ }
1356
+
827
1357
  /// `quick_start(agents_dir, name, yes, fresh, team_id)`(`diagnose/quick_start.py:18`)。
828
1358
  /// 面向用户的零配置入口:编译 team_dir → `launch` → autobind leader receiver → 起
829
1359
  /// coordinator → `wait_ready` 轮询就绪。归入 lifecycle module(不与 diagnose 混)。
@@ -834,17 +1364,43 @@ pub fn quick_start(
834
1364
  fresh: bool,
835
1365
  team_id: Option<&str>,
836
1366
  ) -> Result<QuickStartReport, LifecycleError> {
837
- quick_start_with_transport(
1367
+ let workspace = team_workspace(agents_dir);
1368
+ quick_start_in_workspace(&workspace, agents_dir, name, yes, fresh, team_id)
1369
+ }
1370
+
1371
+ pub fn quick_start_in_workspace(
1372
+ workspace: &Path,
1373
+ agents_dir: &Path,
1374
+ name: Option<&str>,
1375
+ yes: bool,
1376
+ fresh: bool,
1377
+ team_id: Option<&str>,
1378
+ ) -> Result<QuickStartReport, LifecycleError> {
1379
+ let workspace = explicit_quick_start_workspace(workspace);
1380
+ quick_start_with_transport_in_workspace(
1381
+ &workspace,
838
1382
  agents_dir,
839
1383
  name,
840
1384
  yes,
841
1385
  fresh,
842
1386
  team_id,
843
- // CP-1: per-team socket bound to the run workspace (team_workspace(agents_dir)).
844
- &crate::tmux_backend::TmuxBackend::for_workspace(&team_workspace(agents_dir)),
1387
+ // CP-1: per-team socket bound to the selected run workspace.
1388
+ &crate::tmux_backend::TmuxBackend::for_workspace(&workspace),
845
1389
  )
846
1390
  }
847
1391
 
1392
+ fn explicit_quick_start_workspace(workspace: &Path) -> PathBuf {
1393
+ std::fs::canonicalize(workspace).unwrap_or_else(|_| {
1394
+ if workspace.is_absolute() {
1395
+ workspace.to_path_buf()
1396
+ } else {
1397
+ std::env::current_dir()
1398
+ .unwrap_or_else(|_| PathBuf::from("."))
1399
+ .join(workspace)
1400
+ }
1401
+ })
1402
+ }
1403
+
848
1404
  /// `quick_start` with an injected transport — tests inject a recording mock so the REAL spawn path
849
1405
  /// (launch dry_run=false → spawn_agents) is asserted without a live tmux; prod uses the real TmuxBackend.
850
1406
  pub fn quick_start_with_transport(
@@ -854,6 +1410,19 @@ pub fn quick_start_with_transport(
854
1410
  fresh: bool,
855
1411
  team_id: Option<&str>,
856
1412
  transport: &dyn Transport,
1413
+ ) -> Result<QuickStartReport, LifecycleError> {
1414
+ let workspace = team_workspace(agents_dir);
1415
+ quick_start_with_transport_in_workspace(&workspace, agents_dir, name, yes, fresh, team_id, transport)
1416
+ }
1417
+
1418
+ pub fn quick_start_with_transport_in_workspace(
1419
+ workspace: &Path,
1420
+ agents_dir: &Path,
1421
+ name: Option<&str>,
1422
+ yes: bool,
1423
+ fresh: bool,
1424
+ team_id: Option<&str>,
1425
+ transport: &dyn Transport,
857
1426
  ) -> Result<QuickStartReport, LifecycleError> {
858
1427
  if !agents_dir.exists() {
859
1428
  return Err(LifecycleError::Compile(format!(
@@ -861,50 +1430,91 @@ pub fn quick_start_with_transport(
861
1430
  agents_dir.display()
862
1431
  )));
863
1432
  }
864
- let workspace = team_workspace(agents_dir);
1433
+ let workspace = workspace.to_path_buf();
1434
+ let mut spec = crate::compiler::compile_team(agents_dir)
1435
+ .map_err(|e| LifecycleError::Compile(e.to_string()))?;
1436
+ let requested_team = quick_start_requested_team_key(team_id, name)
1437
+ .map(str::to_string)
1438
+ .or_else(|| spec_team_id(&spec));
1439
+ let explicit_team_key = quick_start_requested_team_key(team_id, name).map(str::to_string);
1440
+ let team_depth = quick_start_depth_guard(
1441
+ &workspace,
1442
+ agents_dir,
1443
+ requested_team.as_deref(),
1444
+ matches!(transport.kind(), crate::transport::BackendKind::Tmux),
1445
+ )?;
1446
+ if team_depth.team_depth > 2 {
1447
+ let parent = team_depth.parent_team_key.as_deref().unwrap_or("");
1448
+ return Err(LifecycleError::RequirementUnmet(format!(
1449
+ "team nesting depth limit exceeded: parent_team_key={parent} parent_depth={} max_depth=2",
1450
+ team_depth.team_depth.saturating_sub(1)
1451
+ )));
1452
+ }
865
1453
  if !fresh {
866
1454
  let state_path = crate::state::persist::runtime_state_path(&workspace);
867
1455
  if state_path.exists() {
868
1456
  let state = crate::state::persist::load_runtime_state(&workspace)
869
1457
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
870
- return Ok(QuickStartReport::ExistingRuntime {
871
- team: team_id.map(str::to_string),
872
- session_name: state
873
- .get("session_name")
874
- .and_then(serde_json::Value::as_str)
875
- .filter(|s| !s.is_empty())
876
- .map(SessionName::new),
877
- state_path: Some(state_path),
878
- next_actions: vec![
879
- "run restart to resume the existing team or pass --fresh to replace it".to_string(),
880
- ],
881
- });
882
- }
1458
+ if requested_team
1459
+ .as_deref()
1460
+ .is_none_or(|team| runtime_state_has_quick_start_team(&state, team))
1461
+ {
1462
+ return Ok(QuickStartReport::ExistingRuntime {
1463
+ team: requested_team.clone(),
1464
+ session_name: state
1465
+ .get("session_name")
1466
+ .and_then(serde_json::Value::as_str)
1467
+ .filter(|s| !s.is_empty())
1468
+ .map(SessionName::new),
1469
+ state_path: Some(state_path),
1470
+ next_actions: vec![
1471
+ "run restart to resume the existing team or pass --fresh to replace it".to_string(),
1472
+ ],
1473
+ });
1474
+ }
1475
+ }
883
1476
  }
884
- let mut spec = crate::compiler::compile_team(agents_dir)
885
- .map_err(|e| LifecycleError::Compile(e.to_string()))?;
886
1477
  // CR-040/042: repeated quick-start from one template with distinct --team-id/--name
887
1478
  // must NOT collide on the template-derived tmux session. Override the compiled
888
1479
  // spec's runtime.session_name with one derived from the REQUESTED team identity
889
1480
  // so launch_with_transport (which reads runtime.session_name) spawns into an
890
1481
  // isolated session per requested team.
891
- if let Some(requested) = team_id.or(name).filter(|s| !s.is_empty()) {
1482
+ if let Some(requested) = requested_team.as_deref() {
892
1483
  override_spec_session_name(&mut spec, &format!("team-{requested}"));
893
1484
  }
1485
+ let session_name = spec_session_name(&spec);
1486
+ let state_team_key = explicit_team_key.clone().unwrap_or_else(|| {
1487
+ let spec_path = agents_dir.join("team.spec.yaml");
1488
+ runtime_team_key_for_spec(&spec_path, &spec, &session_name)
1489
+ });
894
1490
  let spec_path = agents_dir.join("team.spec.yaml");
895
1491
  std::fs::write(&spec_path, yaml::dumps(&spec)).map_err(|e| {
896
1492
  LifecycleError::StatePersist(format!("{}: {e}", spec_path.display()))
897
1493
  })?;
898
1494
  let _store = crate::message_store::MessageStore::open(&workspace)
899
1495
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
900
- let session_name = spec_session_name(&spec);
901
1496
  let resolved_spec_path = std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
902
1497
  let state = initial_runtime_state(&spec, &resolved_spec_path, &workspace, agents_dir);
903
- save_launched_team_state(&workspace, &state)?;
1498
+ save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key))?;
1499
+ annotate_persisted_team_depth(
1500
+ &workspace,
1501
+ &state_team_key,
1502
+ team_depth.parent_team_key.as_deref(),
1503
+ team_depth.team_depth,
1504
+ )?;
904
1505
  // FIX (rt-host-a real-machine finding): dry_run=false so launch_with_transport calls spawn_agents
905
1506
  // and really creates the tmux session + worker windows (was hardcoded true → never spawned, which
906
1507
  // also starved the coordinator: no session → first tick TmuxSessionMissing → run_daemon loop exits).
907
- let launch = launch_with_transport(&spec_path, false, yes, true, transport)?;
1508
+ let mut launch = launch_with_transport_in_workspace(&workspace, &spec_path, false, yes, true, transport)?;
1509
+ annotate_persisted_team_depth(
1510
+ &workspace,
1511
+ &state_team_key,
1512
+ team_depth.parent_team_key.as_deref(),
1513
+ team_depth.team_depth,
1514
+ )?;
1515
+ launch.leader_receiver_attached = launched_team_receiver_is_attached(&workspace, &state_team_key);
1516
+ launch.session_capture_incomplete_agents =
1517
+ quick_start_session_capture_incomplete_agents(&workspace, &state_team_key);
908
1518
  let coordinator_workspace = crate::coordinator::WorkspacePath::new(workspace.clone());
909
1519
  let coordinator_started = crate::coordinator::start_coordinator(&coordinator_workspace)
910
1520
  .map(|report| report.ok)
@@ -921,7 +1531,7 @@ pub fn quick_start_with_transport(
921
1531
  // loaded successfully (provider-side codex/claude schema rejections happen
922
1532
  // asynchronously after spawn), so the verdict is PendingToolLoad — never
923
1533
  // bare Ready.
924
- let worker_readiness = quick_start_worker_readiness(&workspace);
1534
+ let worker_readiness = quick_start_worker_readiness(&workspace, &state_team_key);
925
1535
  Ok(QuickStartReport::Ready {
926
1536
  session_name,
927
1537
  launch: Box::new(launch),
@@ -937,13 +1547,21 @@ pub fn quick_start_with_transport(
937
1547
  /// verdict to `Degraded { unhealthy_agents }` (sorted, deduped); otherwise
938
1548
  /// `PendingToolLoad` — never bare Ready. State read failure is treated as
939
1549
  /// PendingToolLoad rather than fabricated success.
940
- fn quick_start_worker_readiness(workspace: &Path) -> QuickStartReadiness {
1550
+ fn quick_start_worker_readiness(workspace: &Path, team_key: &str) -> QuickStartReadiness {
941
1551
  let Ok(state) = load_runtime_state(workspace) else {
942
1552
  return QuickStartReadiness::PendingToolLoad;
943
1553
  };
944
- let Some(agents) = state.get("agents").and_then(serde_json::Value::as_object) else {
1554
+ let team_state = state
1555
+ .get("teams")
1556
+ .and_then(serde_json::Value::as_object)
1557
+ .and_then(|teams| teams.get(team_key))
1558
+ .unwrap_or(&state);
1559
+ let Some(agents) = team_state.get("agents").and_then(serde_json::Value::as_object) else {
945
1560
  return QuickStartReadiness::PendingToolLoad;
946
1561
  };
1562
+ let all_spawned = !agents.is_empty();
1563
+ let leader_receiver_attached = launched_team_receiver_is_attached(workspace, team_key);
1564
+ let all_attached_receiver = leader_receiver_attached;
947
1565
  let mut unhealthy: Vec<String> = agents
948
1566
  .iter()
949
1567
  .filter_map(|(id, agent)| {
@@ -954,15 +1572,79 @@ fn quick_start_worker_readiness(workspace: &Path) -> QuickStartReadiness {
954
1572
  }
955
1573
  })
956
1574
  .collect();
957
- if unhealthy.is_empty() {
958
- QuickStartReadiness::PendingToolLoad
959
- } else {
1575
+ if !unhealthy.is_empty() {
960
1576
  unhealthy.sort();
961
1577
  unhealthy.dedup();
962
1578
  QuickStartReadiness::Degraded { unhealthy_agents: unhealthy }
1579
+ } else {
1580
+ let incomplete_agents = crate::session_capture::incomplete_interacted_resumable_agent_ids(team_state);
1581
+ let all_resumable_have_session = incomplete_agents.is_empty();
1582
+ let _readiness_ready = all_spawned && all_attached_receiver && all_resumable_have_session;
1583
+ QuickStartReadiness::PendingToolLoad
963
1584
  }
964
1585
  }
965
1586
 
1587
+ fn quick_start_session_capture_incomplete_agents(workspace: &Path, team_key: &str) -> Vec<String> {
1588
+ let Ok(state) = load_runtime_state(workspace) else {
1589
+ return Vec::new();
1590
+ };
1591
+ let team_state = state
1592
+ .get("teams")
1593
+ .and_then(serde_json::Value::as_object)
1594
+ .and_then(|teams| teams.get(team_key))
1595
+ .unwrap_or(&state);
1596
+ crate::session_capture::incomplete_interacted_resumable_agent_ids(team_state)
1597
+ }
1598
+
1599
+ fn launched_team_receiver_is_attached(workspace: &Path, team_key: &str) -> bool {
1600
+ let Ok(state) = load_runtime_state(workspace) else {
1601
+ return true;
1602
+ };
1603
+ let team_state = state
1604
+ .get("teams")
1605
+ .and_then(serde_json::Value::as_object)
1606
+ .and_then(|teams| teams.get(team_key))
1607
+ .unwrap_or(&state);
1608
+ if team_state.get("leader_receiver").is_none() {
1609
+ return true;
1610
+ }
1611
+ if team_uses_fake_model_harness(team_state) {
1612
+ return true;
1613
+ }
1614
+ leader_receiver_is_attached(team_state)
1615
+ }
1616
+
1617
+ fn team_uses_fake_model_harness(team_state: &serde_json::Value) -> bool {
1618
+ team_state
1619
+ .get("agents")
1620
+ .and_then(serde_json::Value::as_object)
1621
+ .is_some_and(|agents| {
1622
+ !agents.is_empty()
1623
+ && agents.values().all(|agent| {
1624
+ agent
1625
+ .get("model")
1626
+ .and_then(serde_json::Value::as_str)
1627
+ == Some("fake")
1628
+ })
1629
+ })
1630
+ }
1631
+
1632
+ fn leader_receiver_is_attached(team_state: &serde_json::Value) -> bool {
1633
+ let Some(receiver) = team_state.get("leader_receiver") else {
1634
+ return false;
1635
+ };
1636
+ let status = receiver
1637
+ .get("status")
1638
+ .and_then(serde_json::Value::as_str)
1639
+ .unwrap_or("");
1640
+ let pane_id = receiver
1641
+ .get("pane_id")
1642
+ .and_then(serde_json::Value::as_str)
1643
+ .or_else(|| receiver.get("pane").and_then(serde_json::Value::as_str))
1644
+ .unwrap_or("");
1645
+ status == "attached" && !pane_id.is_empty() && pane_id != "__team_agent_unbound__"
1646
+ }
1647
+
966
1648
  /// `detect_inherited_dangerous_permissions`(`launch/config.py`):扫进程祖先链找
967
1649
  /// `--dangerously-*` flag,产出危险审批继承态。launch 在 inherited=false 且无 --yes 时拒。
968
1650
  pub fn detect_dangerous_approval() -> Result<DangerousApproval, LifecycleError> {
@@ -1178,12 +1860,13 @@ pub fn add_agent(
1178
1860
  let team_dir = selected
1179
1861
  .spec_workspace
1180
1862
  .ok_or_else(|| LifecycleError::TeamSelect("active team spec workspace not found".to_string()))?;
1181
- add_agent_with_transport(
1863
+ add_agent_with_transport_at_paths(
1864
+ &selected.run_workspace,
1182
1865
  &team_dir,
1183
1866
  agent_id,
1184
1867
  role_file_path,
1185
1868
  open_display,
1186
- team,
1869
+ Some(selected.team_key.as_str()),
1187
1870
  &crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace),
1188
1871
  )
1189
1872
  }
@@ -1200,12 +1883,35 @@ pub fn add_agent_with_transport(
1200
1883
  ) -> Result<AddAgentReport, LifecycleError> {
1201
1884
  let run_workspace = crate::model::paths::canonical_run_workspace(workspace)
1202
1885
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1203
- let owner_state = if team.is_some() {
1204
- crate::state::projection::select_runtime_state(&run_workspace, team)
1205
- .map_err(|e| LifecycleError::TeamSelect(e.to_string()))?
1206
- } else {
1207
- load_runtime_state(&run_workspace).map_err(|e| LifecycleError::StatePersist(e.to_string()))?
1208
- };
1886
+ add_agent_with_transport_at_paths(
1887
+ &run_workspace,
1888
+ workspace,
1889
+ agent_id,
1890
+ role_file_path,
1891
+ open_display,
1892
+ team,
1893
+ transport,
1894
+ )
1895
+ }
1896
+
1897
+ fn add_agent_with_transport_at_paths(
1898
+ run_workspace: &Path,
1899
+ team_dir: &Path,
1900
+ agent_id: &AgentId,
1901
+ role_file_path: &Path,
1902
+ open_display: bool,
1903
+ team: Option<&str>,
1904
+ transport: &dyn Transport,
1905
+ ) -> Result<AddAgentReport, LifecycleError> {
1906
+ let runtime_state = crate::state::persist::load_runtime_state(run_workspace)
1907
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1908
+ let canonical_team_key = team
1909
+ .filter(|key| !key.is_empty())
1910
+ .map(str::to_string)
1911
+ .or_else(|| explicit_active_team_key(&runtime_state))
1912
+ .unwrap_or_else(|| crate::state::projection::team_state_key(&runtime_state));
1913
+ let owner_state = crate::state::projection::select_runtime_state(run_workspace, Some(&canonical_team_key))
1914
+ .map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
1209
1915
  ensure_owner_allowed_for_state(&owner_state, Some(agent_id))?;
1210
1916
  if !role_file_path.exists() {
1211
1917
  return Err(LifecycleError::Compile(format!(
@@ -1213,7 +1919,6 @@ pub fn add_agent_with_transport(
1213
1919
  role_file_path.display()
1214
1920
  )));
1215
1921
  }
1216
- let team_dir = workspace;
1217
1922
  if agent_id_exists_in_team_dir(team_dir, agent_id) {
1218
1923
  return Err(LifecycleError::RequirementUnmet(format!(
1219
1924
  "agent id already exists: {agent_id}"
@@ -1222,22 +1927,29 @@ pub fn add_agent_with_transport(
1222
1927
  let dynamic_role_file = materialize_added_role_file(team_dir, agent_id, role_file_path)?;
1223
1928
  let spec = crate::compiler::compile_team(team_dir)
1224
1929
  .map_err(|e| LifecycleError::Compile(e.to_string()))?;
1930
+ let safety = effective_runtime_config(&spec)?;
1225
1931
  let spec_path = team_dir.join("team.spec.yaml");
1226
1932
  std::fs::write(&spec_path, yaml::dumps(&spec)).map_err(|e| {
1227
1933
  LifecycleError::StatePersist(format!("{}: {e}", spec_path.display()))
1228
1934
  })?;
1229
1935
  let (meta, _) = crate::compiler::read_front_matter(&dynamic_role_file)
1230
1936
  .map_err(|e| LifecycleError::Compile(e.to_string()))?;
1231
- let run_ws = team_workspace(team_dir);
1232
- upsert_agent_state_from_role(&run_ws, agent_id, &meta, &dynamic_role_file)?;
1937
+ upsert_agent_state_from_role(
1938
+ run_workspace,
1939
+ &canonical_team_key,
1940
+ agent_id,
1941
+ &meta,
1942
+ &dynamic_role_file,
1943
+ &safety,
1944
+ )?;
1233
1945
  let started = crate::lifecycle::restart::start_agent_at_paths(
1234
- &run_ws,
1946
+ run_workspace,
1235
1947
  team_dir,
1236
1948
  agent_id,
1237
1949
  false,
1238
1950
  open_display,
1239
1951
  true,
1240
- team,
1952
+ Some(&canonical_team_key),
1241
1953
  transport,
1242
1954
  )?;
1243
1955
  let (env, start_mode) = match started {
@@ -1260,12 +1972,14 @@ pub fn add_agent_with_transport(
1260
1972
 
1261
1973
  fn upsert_agent_state_from_role(
1262
1974
  workspace: &Path,
1975
+ canonical_team_key: &str,
1263
1976
  agent_id: &AgentId,
1264
1977
  meta: &Value,
1265
1978
  dynamic_role_file: &Path,
1979
+ safety: &DangerousApproval,
1266
1980
  ) -> Result<(), LifecycleError> {
1267
- let mut state = crate::state::persist::load_runtime_state(workspace)
1268
- .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1981
+ let mut state = crate::state::projection::select_runtime_state(workspace, Some(canonical_team_key))
1982
+ .map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
1269
1983
  if !state.is_object() {
1270
1984
  state = serde_json::json!({});
1271
1985
  }
@@ -1307,10 +2021,31 @@ fn upsert_agent_state_from_role(
1307
2021
  if let Some(model) = meta.get("model").and_then(Value::as_str) {
1308
2022
  if let Some(obj) = entry.as_object_mut() {
1309
2023
  obj.insert("model".to_string(), serde_json::json!(model));
2024
+ obj.insert("model_source".to_string(), serde_json::json!("role"));
1310
2025
  }
1311
2026
  }
2027
+ if let Some(profile) = meta.get("profile").and_then(Value::as_str) {
2028
+ if let Some(obj) = entry.as_object_mut() {
2029
+ obj.insert("profile".to_string(), serde_json::json!(profile));
2030
+ if let Some(team_dir) = dynamic_role_file
2031
+ .parent()
2032
+ .and_then(Path::parent)
2033
+ {
2034
+ obj.insert(
2035
+ "_profile_dir".to_string(),
2036
+ serde_json::json!(team_dir.join("profiles").to_string_lossy().to_string()),
2037
+ );
2038
+ }
2039
+ if !obj.contains_key("model_source") {
2040
+ obj.insert("model_source".to_string(), serde_json::json!("default"));
2041
+ }
2042
+ }
2043
+ }
2044
+ if let Some(obj) = entry.as_object_mut() {
2045
+ persist_effective_approval_policy(obj, safety);
2046
+ }
1312
2047
  agent_map.insert(agent_id.as_str().to_string(), entry);
1313
- save_runtime_state(workspace, &state).map_err(|e| LifecycleError::StatePersist(e.to_string()))
2048
+ save_launched_team_state_for_key(workspace, &state, Some(canonical_team_key))
1314
2049
  }
1315
2050
 
1316
2051
  fn materialize_added_role_file(
@@ -1455,61 +2190,158 @@ pub fn fork_agent_with_transport(
1455
2190
  let safety = effective_runtime_config(&new_spec)?;
1456
2191
  let tools = worker_tool_refs(agent_tool_strings(new_agent), &safety);
1457
2192
  let tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
2193
+ let fork_team = crate::messaging::leader_receiver::active_team_key(&workspace, &state);
1458
2194
  let mcp_config = adapter
1459
2195
  .mcp_config(auth_mode)
1460
2196
  .map_err(|e| {
1461
2197
  let _ = std::fs::write(&spec_path, text.as_bytes());
1462
2198
  LifecycleError::Provider(e.to_string())
1463
2199
  })?;
1464
- let mut argv = adapter
1465
- .fork_with_context(
2200
+ let mcp_config = resolve_mcp_config(mcp_config, &workspace, as_agent_id.as_str(), &fork_team);
2201
+ let mcp_config_path = write_worker_mcp_config(&workspace, as_agent_id.as_str(), &mcp_config)
2202
+ .map_err(|e| {
2203
+ let _ = std::fs::write(&spec_path, text.as_bytes());
2204
+ e
2205
+ })?;
2206
+ let profile_dir = spec_workspace.join("profiles");
2207
+ let profile_launch = crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
2208
+ &workspace,
2209
+ as_agent_id.as_str(),
2210
+ new_agent,
2211
+ Some(&profile_dir),
2212
+ Some(&mcp_config),
2213
+ )?;
2214
+ let command_model = profile_launch
2215
+ .command_overrides
2216
+ .model
2217
+ .as_deref()
2218
+ .or(model);
2219
+ let mut plan = adapter
2220
+ .fork_plan(
1466
2221
  Some(&session_id),
1467
- auth_mode,
1468
- Some(&mcp_config),
1469
- role,
1470
- model,
1471
- &tool_refs,
2222
+ crate::provider::ProviderCommandContext {
2223
+ auth_mode,
2224
+ mcp_config: Some(&mcp_config),
2225
+ system_prompt: role,
2226
+ model: command_model,
2227
+ tools: &tool_refs,
2228
+ profile_launch: Some(&profile_launch),
2229
+ },
1472
2230
  )
1473
2231
  .map_err(|e| {
1474
2232
  let _ = std::fs::write(&spec_path, text.as_bytes());
1475
2233
  LifecycleError::Provider(e.to_string())
1476
2234
  })?;
1477
- let fork_team = crate::messaging::leader_receiver::active_team_key(&workspace, &state);
1478
- fill_spawn_placeholders_full(&mut argv, &workspace, as_agent_id.as_str(), Some(&fork_team));
2235
+ if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
2236
+ point_native_mcp_config_at_file(&mut plan.argv, provider, &mcp_config_path);
2237
+ }
2238
+ fill_spawn_placeholders_full(&mut plan.argv, &workspace, as_agent_id.as_str(), Some(&fork_team));
1479
2239
  let window = WindowName::new(as_agent_id.as_str());
1480
2240
  // fork inherits the parent agent's owner team via runtime state (`active_team_key`).
1481
- let env = inherited_env_with_team_overrides(
2241
+ let mut env = inherited_env_with_team_overrides(
1482
2242
  &workspace,
1483
2243
  as_agent_id.as_str(),
1484
2244
  Some(&fork_team),
1485
2245
  );
2246
+ apply_profile_launch_env(&mut env, &profile_launch);
1486
2247
  // golden operations.py:336 -> _tmux_start_command_for_agent_window (runtime.py:1017-1020): branch on
1487
2248
  // _tmux_session_exists — an ABSENT session => new-session (spawn_first), present => new-window
1488
2249
  // (spawn_into). The Rust restart seam (restart.rs spawn_agent_window) uses the same branch.
1489
2250
  let session_live = transport.has_session(&session_name).unwrap_or(false);
1490
2251
  let spawn_result = if session_live {
1491
- transport.spawn_into(&session_name, &window, &argv, &workspace, &env)
2252
+ transport.spawn_into(&session_name, &window, &plan.argv, &workspace, &env)
1492
2253
  } else {
1493
- transport.spawn_first(&session_name, &window, &argv, &workspace, &env)
2254
+ transport.spawn_first(&session_name, &window, &plan.argv, &workspace, &env)
1494
2255
  };
1495
- let _spawn = spawn_result.map_err(|e| {
2256
+ let spawn = spawn_result.map_err(|e| {
1496
2257
  let _ = std::fs::write(&spec_path, text.as_bytes());
1497
2258
  LifecycleError::Transport(e.to_string())
1498
2259
  })?;
1499
2260
  let old_state = state.clone();
1500
2261
  let mut next_state = state;
1501
- upsert_forked_agent_state(&mut next_state, source_agent_id, as_agent_id, new_agent)?;
2262
+ upsert_forked_agent_state(
2263
+ &mut next_state,
2264
+ source_agent_id,
2265
+ as_agent_id,
2266
+ new_agent,
2267
+ &safety,
2268
+ &plan,
2269
+ &profile_launch,
2270
+ &spawn,
2271
+ &workspace,
2272
+ Some(&profile_dir),
2273
+ )?;
2274
+ if let Some(agent) = next_state
2275
+ .get_mut("agents")
2276
+ .and_then(serde_json::Value::as_object_mut)
2277
+ .and_then(|agents| agents.get_mut(as_agent_id.as_str()))
2278
+ .and_then(serde_json::Value::as_object_mut)
2279
+ {
2280
+ persist_effective_approval_policy(agent, &safety);
2281
+ }
2282
+ if let Err(e) = maybe_fail_fork_after_spawn("save_runtime_state") {
2283
+ rollback_fork_after_spawn(
2284
+ &workspace,
2285
+ &spec_path,
2286
+ &text,
2287
+ &old_state,
2288
+ transport,
2289
+ &session_name,
2290
+ &window,
2291
+ &mcp_config_path,
2292
+ as_agent_id,
2293
+ &profile_launch,
2294
+ );
2295
+ return Err(e);
2296
+ }
1502
2297
  if let Err(e) = save_runtime_state(&workspace, &next_state) {
1503
- rollback_fork_after_spawn(&workspace, &spec_path, &text, &old_state, transport, &session_name, &window);
2298
+ rollback_fork_after_spawn(
2299
+ &workspace,
2300
+ &spec_path,
2301
+ &text,
2302
+ &old_state,
2303
+ transport,
2304
+ &session_name,
2305
+ &window,
2306
+ &mcp_config_path,
2307
+ as_agent_id,
2308
+ &profile_launch,
2309
+ );
1504
2310
  return Err(LifecycleError::StatePersist(e.to_string()));
1505
2311
  }
2312
+ if let Err(e) = maybe_fail_fork_after_spawn("start_coordinator") {
2313
+ rollback_fork_after_spawn(
2314
+ &workspace,
2315
+ &spec_path,
2316
+ &text,
2317
+ &old_state,
2318
+ transport,
2319
+ &session_name,
2320
+ &window,
2321
+ &mcp_config_path,
2322
+ as_agent_id,
2323
+ &profile_launch,
2324
+ );
2325
+ return Err(e);
2326
+ }
1506
2327
  let coordinator_started =
1507
2328
  crate::coordinator::start_coordinator(&crate::coordinator::WorkspacePath::new(
1508
2329
  workspace.to_path_buf(),
1509
2330
  ))
1510
2331
  .map(|report| report.ok)
1511
2332
  .map_err(|e| {
1512
- rollback_fork_after_spawn(&workspace, &spec_path, &text, &old_state, transport, &session_name, &window);
2333
+ rollback_fork_after_spawn(
2334
+ &workspace,
2335
+ &spec_path,
2336
+ &text,
2337
+ &old_state,
2338
+ transport,
2339
+ &session_name,
2340
+ &window,
2341
+ &mcp_config_path,
2342
+ as_agent_id,
2343
+ &profile_launch,
2344
+ );
1513
2345
  LifecycleError::StatePersist(e.to_string())
1514
2346
  })?;
1515
2347
  Ok(ForkAgentReport {
@@ -1532,6 +2364,9 @@ fn rollback_fork_after_spawn(
1532
2364
  transport: &dyn Transport,
1533
2365
  session_name: &SessionName,
1534
2366
  window: &WindowName,
2367
+ mcp_config_path: &Path,
2368
+ agent_id: &AgentId,
2369
+ profile_launch: &crate::provider::ProviderProfileLaunch,
1535
2370
  ) {
1536
2371
  let _ = transport.kill_window(&Target::SessionWindow {
1537
2372
  session: session_name.clone(),
@@ -1539,6 +2374,41 @@ fn rollback_fork_after_spawn(
1539
2374
  });
1540
2375
  let _ = std::fs::write(spec_path, spec_text.as_bytes());
1541
2376
  let _ = save_runtime_state(workspace, old_state);
2377
+ cleanup_fork_mcp_artifacts(workspace, agent_id, mcp_config_path, profile_launch);
2378
+ }
2379
+
2380
+ fn maybe_fail_fork_after_spawn(step: &str) -> Result<(), LifecycleError> {
2381
+ let Ok(reason) = std::env::var("TEAM_AGENT_TEST_FAIL_FORK_AFTER_SPAWN") else {
2382
+ return Ok(());
2383
+ };
2384
+ if reason.is_empty() {
2385
+ return Ok(());
2386
+ }
2387
+ let should_fail = reason == step
2388
+ || (step == "start_coordinator" && reason == "coordinator");
2389
+ if !should_fail {
2390
+ return Ok(());
2391
+ }
2392
+ Err(LifecycleError::StatePersist(format!(
2393
+ "injected fork failure after spawn: {reason}"
2394
+ )))
2395
+ }
2396
+
2397
+ fn cleanup_fork_mcp_artifacts(
2398
+ workspace: &Path,
2399
+ agent_id: &AgentId,
2400
+ mcp_config_path: &Path,
2401
+ profile_launch: &crate::provider::ProviderProfileLaunch,
2402
+ ) {
2403
+ let _ = std::fs::remove_file(mcp_config_path);
2404
+ let _ = std::fs::remove_file(
2405
+ workspace
2406
+ .join(".team/runtime/provider-env")
2407
+ .join(format!("{}.env", agent_id.as_str())),
2408
+ );
2409
+ if let Some(config_dir) = profile_launch.claude_config_dir.as_ref() {
2410
+ let _ = std::fs::remove_dir_all(config_dir.parent().unwrap_or(config_dir));
2411
+ }
1542
2412
  }
1543
2413
 
1544
2414
  fn leader_id_matches(spec: &Value, agent_id: &AgentId) -> bool {
@@ -1671,6 +2541,12 @@ fn upsert_forked_agent_state(
1671
2541
  source_agent_id: &AgentId,
1672
2542
  as_agent_id: &AgentId,
1673
2543
  spec_agent: &Value,
2544
+ safety: &DangerousApproval,
2545
+ plan: &crate::provider::CommandPlan,
2546
+ profile_launch: &crate::provider::ProviderProfileLaunch,
2547
+ spawn: &crate::transport::SpawnResult,
2548
+ spawn_cwd: &Path,
2549
+ profile_dir: Option<&Path>,
1674
2550
  ) -> Result<(), LifecycleError> {
1675
2551
  if !state.is_object() {
1676
2552
  *state = serde_json::json!({});
@@ -1695,15 +2571,46 @@ fn upsert_forked_agent_state(
1695
2571
  .get("provider")
1696
2572
  .and_then(Value::as_str)
1697
2573
  .unwrap_or("codex");
1698
- agent_map.insert(
1699
- as_agent_id.as_str().to_string(),
1700
- serde_json::json!({
1701
- "status": "running",
1702
- "provider": provider,
1703
- "window": as_agent_id.as_str(),
1704
- "forked_from": source_agent_id.as_str(),
1705
- }),
2574
+ let mut entry = serde_json::Map::new();
2575
+ entry.insert("status".to_string(), serde_json::json!("running"));
2576
+ entry.insert("provider".to_string(), serde_json::json!(provider));
2577
+ entry.insert("agent_id".to_string(), serde_json::json!(as_agent_id.as_str()));
2578
+ entry.insert("window".to_string(), serde_json::json!(as_agent_id.as_str()));
2579
+ entry.insert("forked_from".to_string(), serde_json::json!(source_agent_id.as_str()));
2580
+ entry.insert(
2581
+ "spawn_cwd".to_string(),
2582
+ serde_json::json!(spawn_cwd.to_string_lossy().to_string()),
1706
2583
  );
2584
+ entry.insert("pane_id".to_string(), serde_json::json!(spawn.pane_id.as_str()));
2585
+ if let Some(pid) = spawn.child_pid {
2586
+ entry.insert("pane_pid".to_string(), serde_json::json!(pid));
2587
+ }
2588
+ for key in ["auth_mode", "model", "model_source", "profile", "_profile_dir", "role"] {
2589
+ if let Some(value) = spec_agent.get(key) {
2590
+ entry.insert(key.to_string(), yaml_value_to_json(value));
2591
+ }
2592
+ }
2593
+ if spec_agent.get("profile").is_some() && !entry.contains_key("_profile_dir") {
2594
+ if let Some(profile_dir) = profile_dir {
2595
+ entry.insert(
2596
+ "_profile_dir".to_string(),
2597
+ serde_json::json!(profile_dir.to_string_lossy().to_string()),
2598
+ );
2599
+ }
2600
+ }
2601
+ entry.insert("session_id".to_string(), serde_json::Value::Null);
2602
+ entry.insert("rollout_path".to_string(), serde_json::Value::Null);
2603
+ entry.insert("captured_at".to_string(), serde_json::Value::Null);
2604
+ entry.insert("captured_via".to_string(), serde_json::Value::Null);
2605
+ entry.insert("attribution_confidence".to_string(), serde_json::Value::Null);
2606
+ persist_command_plan_state(&mut entry, plan, profile_launch);
2607
+ agent_map.insert(as_agent_id.as_str().to_string(), serde_json::Value::Object(entry));
2608
+ if let Some(entry) = agent_map
2609
+ .get_mut(as_agent_id.as_str())
2610
+ .and_then(serde_json::Value::as_object_mut)
2611
+ {
2612
+ persist_effective_approval_policy(entry, safety);
2613
+ }
1707
2614
  Ok(())
1708
2615
  }
1709
2616
 
@@ -1863,6 +2770,7 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
1863
2770
  "status": "attached",
1864
2771
  "provider": provider,
1865
2772
  "pane_id": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
2773
+ "pane": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
1866
2774
  "leader_session_uuid": owner.get("leader_session_uuid").cloned().unwrap_or(serde_json::Value::Null),
1867
2775
  "owner_epoch": owner_epoch,
1868
2776
  "discovery": "quick_start",
@@ -1882,6 +2790,13 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
1882
2790
  true
1883
2791
  }
1884
2792
 
2793
+ fn has_positive_caller_leader_env() -> bool {
2794
+ env_nonempty("TEAM_AGENT_LEADER_PANE_ID")
2795
+ || env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID")
2796
+ || env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE")
2797
+ || env_nonempty("TEAM_AGENT_LEADER_PROVIDER")
2798
+ }
2799
+
1885
2800
  fn spec_tasks_json(spec: &Value) -> serde_json::Value {
1886
2801
  spec.get("tasks")
1887
2802
  .and_then(Value::as_list)