@team-agent/installer 0.3.2 → 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.
- package/Cargo.lock +34 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +196 -19
- package/crates/team-agent/src/cli/diagnose.rs +144 -10
- package/crates/team-agent/src/cli/emit.rs +286 -52
- package/crates/team-agent/src/cli/leader.rs +37 -8
- package/crates/team-agent/src/cli/mod.rs +799 -316
- package/crates/team-agent/src/cli/status_port.rs +25 -2
- package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
- package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +57 -3
- package/crates/team-agent/src/cli/types.rs +17 -0
- package/crates/team-agent/src/compiler.rs +15 -5
- package/crates/team-agent/src/coordinator/health.rs +89 -20
- package/crates/team-agent/src/coordinator/mod.rs +4 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
- package/crates/team-agent/src/coordinator/tick.rs +222 -69
- package/crates/team-agent/src/coordinator/types.rs +15 -3
- package/crates/team-agent/src/db/schema.rs +37 -2
- package/crates/team-agent/src/diagnose/comms.rs +226 -0
- package/crates/team-agent/src/diagnose/mod.rs +45 -0
- package/crates/team-agent/src/diagnose/orphans.rs +658 -0
- package/crates/team-agent/src/fake_worker.rs +146 -3
- package/crates/team-agent/src/leader/start.rs +121 -23
- package/crates/team-agent/src/leader/types.rs +44 -1
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +645 -47
- package/crates/team-agent/src/lifecycle/launch.rs +818 -116
- package/crates/team-agent/src/lifecycle/mod.rs +2 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
- package/crates/team-agent/src/lifecycle/restart/common.rs +177 -83
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +443 -9
- package/crates/team-agent/src/lifecycle/restart/remove.rs +22 -6
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
- package/crates/team-agent/src/lifecycle/restart.rs +4 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
- package/crates/team-agent/src/lifecycle/types.rs +19 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
- package/crates/team-agent/src/mcp_server/mod.rs +3 -74
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
- package/crates/team-agent/src/mcp_server/tools.rs +312 -111
- package/crates/team-agent/src/mcp_server/types.rs +6 -4
- package/crates/team-agent/src/mcp_server/wire.rs +19 -7
- package/crates/team-agent/src/message_store.rs +21 -4
- package/crates/team-agent/src/messaging/delivery.rs +87 -37
- package/crates/team-agent/src/messaging/mod.rs +9 -6
- package/crates/team-agent/src/messaging/results.rs +153 -16
- package/crates/team-agent/src/messaging/selftest.rs +199 -12
- package/crates/team-agent/src/messaging/send.rs +35 -3
- package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
- package/crates/team-agent/src/messaging/types.rs +11 -3
- package/crates/team-agent/src/os_probe.rs +119 -0
- package/crates/team-agent/src/packaging/migrate.rs +10 -2
- package/crates/team-agent/src/packaging/tests.rs +23 -0
- package/crates/team-agent/src/provider/adapter.rs +483 -67
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
- package/crates/team-agent/src/provider/classify.rs +51 -4
- package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
- package/crates/team-agent/src/provider/types.rs +47 -0
- package/crates/team-agent/src/session_capture.rs +616 -0
- package/crates/team-agent/src/state/persist.rs +57 -0
- package/crates/team-agent/src/state/projection.rs +32 -23
- package/crates/team-agent/src/state/selector.rs +5 -2
- package/crates/team-agent/src/tmux_backend.rs +97 -60
- package/crates/team-agent/src/transport/test_support.rs +9 -0
- package/crates/team-agent/src/transport/tests/wire.rs +4 -0
- package/crates/team-agent/src/transport.rs +13 -2
- package/package.json +4 -4
|
@@ -102,7 +102,7 @@ pub fn launch_with_transport_in_workspace(
|
|
|
102
102
|
Vec::new()
|
|
103
103
|
} else {
|
|
104
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)?;
|
|
105
|
+
persist_spawn_agent_state(workspace, spec_path, &spec, &session_name, transport, &started, &safety)?;
|
|
106
106
|
started
|
|
107
107
|
};
|
|
108
108
|
Ok(LaunchReport {
|
|
@@ -113,6 +113,7 @@ pub fn launch_with_transport_in_workspace(
|
|
|
113
113
|
permissions,
|
|
114
114
|
safety,
|
|
115
115
|
leader_receiver_attached: false,
|
|
116
|
+
session_capture_incomplete_agents: Vec::new(),
|
|
116
117
|
})
|
|
117
118
|
}
|
|
118
119
|
|
|
@@ -161,32 +162,49 @@ fn spawn_agents(
|
|
|
161
162
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
162
163
|
let mcp_config = resolve_mcp_config(mcp_config, workspace, agent_id_raw, &mcp_team_id);
|
|
163
164
|
let mcp_config_path = write_worker_mcp_config(workspace, agent_id_raw, &mcp_config)?;
|
|
164
|
-
let
|
|
165
|
-
|
|
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 {
|
|
166
180
|
auth_mode,
|
|
167
|
-
Some(&mcp_config),
|
|
168
|
-
role,
|
|
169
|
-
model,
|
|
170
|
-
&tool_refs,
|
|
171
|
-
|
|
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
|
+
})
|
|
172
187
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
173
|
-
|
|
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
|
+
}
|
|
174
191
|
fill_spawn_placeholders_full(
|
|
175
|
-
&mut argv,
|
|
192
|
+
&mut plan.argv,
|
|
176
193
|
workspace,
|
|
177
194
|
agent_id_raw,
|
|
178
195
|
Some(&mcp_team_id),
|
|
179
196
|
);
|
|
180
197
|
let window = WindowName::new(agent_id_raw);
|
|
181
|
-
let env = inherited_env_with_team_overrides(
|
|
198
|
+
let mut env = inherited_env_with_team_overrides(
|
|
182
199
|
workspace,
|
|
183
200
|
agent_id_raw,
|
|
184
201
|
Some(&mcp_team_id),
|
|
185
202
|
);
|
|
203
|
+
apply_profile_launch_env(&mut env, &profile_launch);
|
|
186
204
|
let spawn = if started.is_empty() {
|
|
187
|
-
transport.spawn_first(session_name, &window, &argv, team_dir, &env)
|
|
205
|
+
transport.spawn_first(session_name, &window, &plan.argv, team_dir, &env)
|
|
188
206
|
} else {
|
|
189
|
-
transport.spawn_into(session_name, &window, &argv, team_dir, &env)
|
|
207
|
+
transport.spawn_into(session_name, &window, &plan.argv, team_dir, &env)
|
|
190
208
|
}
|
|
191
209
|
.map_err(|e| LifecycleError::Transport(e.to_string()))?;
|
|
192
210
|
let _ = adapter.handle_startup_prompts(
|
|
@@ -204,6 +222,13 @@ fn spawn_agents(
|
|
|
204
222
|
target: spawn.pane_id.as_str().to_string(),
|
|
205
223
|
session_id: None,
|
|
206
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,
|
|
207
232
|
display: WorkerDisplay::Blocked {
|
|
208
233
|
reason: AdaptiveBlockReason::NotImplementedThisPlatform,
|
|
209
234
|
},
|
|
@@ -219,6 +244,7 @@ fn persist_spawn_agent_state(
|
|
|
219
244
|
session_name: &SessionName,
|
|
220
245
|
transport: &dyn Transport,
|
|
221
246
|
started: &[StartedAgent],
|
|
247
|
+
safety: &DangerousApproval,
|
|
222
248
|
) -> Result<(), LifecycleError> {
|
|
223
249
|
let state_path = crate::state::persist::runtime_state_path(workspace);
|
|
224
250
|
let mut state = if state_path.exists() {
|
|
@@ -250,8 +276,12 @@ fn persist_spawn_agent_state(
|
|
|
250
276
|
.map(|agent| agent.agent_id.as_str().to_string())
|
|
251
277
|
.collect();
|
|
252
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");
|
|
253
283
|
let mut agents = serde_json::Map::new();
|
|
254
|
-
let
|
|
284
|
+
let mut spawn_index = 0_u32;
|
|
255
285
|
for agent in spec_agent_values(spec) {
|
|
256
286
|
let Some(id) = agent.get("id").and_then(Value::as_str) else {
|
|
257
287
|
continue;
|
|
@@ -285,9 +315,27 @@ fn persist_spawn_agent_state(
|
|
|
285
315
|
continue;
|
|
286
316
|
}
|
|
287
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);
|
|
288
323
|
agents.insert(
|
|
289
324
|
id.to_string(),
|
|
290
|
-
running_agent_state(
|
|
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
|
+
)?,
|
|
291
339
|
);
|
|
292
340
|
}
|
|
293
341
|
if let Some(obj) = state.as_object_mut() {
|
|
@@ -317,6 +365,14 @@ fn pane_pids_by_started_agent(
|
|
|
317
365
|
.collect()
|
|
318
366
|
}
|
|
319
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("")
|
|
374
|
+
}
|
|
375
|
+
|
|
320
376
|
fn save_launched_team_state(workspace: &Path, launched: &serde_json::Value) -> Result<(), LifecycleError> {
|
|
321
377
|
save_launched_team_state_for_key(workspace, launched, None)
|
|
322
378
|
}
|
|
@@ -340,6 +396,7 @@ fn save_launched_team_state_for_key(
|
|
|
340
396
|
}
|
|
341
397
|
promote_launched_binding_from_team_entry(&mut launched, &launched_key);
|
|
342
398
|
drop_foreign_seeded_owner(&existing, &launched_key, &mut launched);
|
|
399
|
+
drop_bare_worker_seeded_owner(&mut launched, &launched_key);
|
|
343
400
|
let merged = if team_key.is_some() {
|
|
344
401
|
merge_workspace_team_state_with_key(&existing, &launched, &launched_key)
|
|
345
402
|
} else {
|
|
@@ -350,6 +407,20 @@ fn save_launched_team_state_for_key(
|
|
|
350
407
|
save_runtime_state(workspace, &projected).map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
351
408
|
}
|
|
352
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
|
+
|
|
353
424
|
fn merge_workspace_team_state_with_key(
|
|
354
425
|
existing: &serde_json::Value,
|
|
355
426
|
launched: &serde_json::Value,
|
|
@@ -445,7 +516,15 @@ fn drop_foreign_seeded_owner(existing: &serde_json::Value, launched_key: &str, l
|
|
|
445
516
|
return;
|
|
446
517
|
};
|
|
447
518
|
if owner_pane_belongs_to_other_team(existing, launched_key, pane) {
|
|
448
|
-
|
|
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
|
+
}
|
|
449
528
|
}
|
|
450
529
|
}
|
|
451
530
|
|
|
@@ -469,23 +548,30 @@ fn drop_worker_pane_seeded_owner(
|
|
|
469
548
|
let tmux_pane = std::env::var("TMUX_PANE")
|
|
470
549
|
.ok()
|
|
471
550
|
.filter(|value| !value.is_empty());
|
|
472
|
-
let has_leader_identity_env =
|
|
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");
|
|
551
|
+
let has_leader_identity_env = has_positive_caller_leader_env();
|
|
478
552
|
let seeded_from_bare_tmux =
|
|
479
553
|
!has_leader_identity_env && tmux_pane.as_deref() == Some(pane);
|
|
480
554
|
let caller_tmux_socket = crate::tmux_backend::socket_name_from_tmux_env();
|
|
481
555
|
if seeded_from_bare_tmux
|
|
482
|
-
&& tmux_sockets_match_or_unknown(caller_tmux_socket.as_deref(), worker_tmux_socket)
|
|
483
|
-
|
|
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)
|
|
484
559
|
{
|
|
485
560
|
seed_unbound_launched_owner(launched, launched_key);
|
|
486
561
|
}
|
|
487
562
|
}
|
|
488
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
|
+
|
|
489
575
|
fn launched_worker_tmux_socket(
|
|
490
576
|
transport: &dyn Transport,
|
|
491
577
|
workspace: &Path,
|
|
@@ -513,6 +599,35 @@ fn env_nonempty(key: &str) -> bool {
|
|
|
513
599
|
}
|
|
514
600
|
|
|
515
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> {
|
|
516
631
|
let provider = launched
|
|
517
632
|
.get("team_owner")
|
|
518
633
|
.and_then(|owner| owner.get("provider"))
|
|
@@ -531,40 +646,22 @@ fn seed_unbound_launched_owner(launched: &mut serde_json::Value, launched_key: &
|
|
|
531
646
|
let os_user = std::env::var("USER")
|
|
532
647
|
.or_else(|_| std::env::var("USERNAME"))
|
|
533
648
|
.unwrap_or_default();
|
|
534
|
-
let
|
|
649
|
+
let uuid = crate::model::ids::LeaderSessionUuid::derive(
|
|
535
650
|
machine_fingerprint,
|
|
536
651
|
workspace,
|
|
537
652
|
&os_user,
|
|
538
653
|
launched_key,
|
|
539
|
-
)
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
let owner_epoch = 1u64;
|
|
543
|
-
let owner = serde_json::json!({
|
|
544
|
-
"pane_id": "__team_agent_unbound__",
|
|
654
|
+
)
|
|
655
|
+
.ok()?;
|
|
656
|
+
Some(serde_json::json!({
|
|
545
657
|
"provider": provider,
|
|
546
658
|
"machine_fingerprint": machine_fingerprint,
|
|
547
659
|
"leader_session_uuid": uuid.as_str(),
|
|
548
|
-
"owner_epoch":
|
|
660
|
+
"owner_epoch": 1u64,
|
|
549
661
|
"claimed_at": spawn_timestamp(),
|
|
550
662
|
"claimed_via": "quick-start",
|
|
551
663
|
"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
|
-
}
|
|
664
|
+
}))
|
|
568
665
|
}
|
|
569
666
|
|
|
570
667
|
fn owner_pane_belongs_to_other_team(existing: &serde_json::Value, launched_key: &str, pane: &str) -> bool {
|
|
@@ -588,9 +685,14 @@ fn running_agent_state(
|
|
|
588
685
|
id: &str,
|
|
589
686
|
provider: Provider,
|
|
590
687
|
workspace: &Path,
|
|
688
|
+
spawn_cwd: &Path,
|
|
591
689
|
spawned_at: &str,
|
|
592
690
|
team_id: &str,
|
|
691
|
+
pane_id: Option<&str>,
|
|
593
692
|
pane_pid: Option<u32>,
|
|
693
|
+
safety: &DangerousApproval,
|
|
694
|
+
started_agent: Option<&StartedAgent>,
|
|
695
|
+
profile_dir: Option<&Path>,
|
|
594
696
|
) -> Result<serde_json::Value, LifecycleError> {
|
|
595
697
|
let model = agent.get("model").and_then(Value::as_str);
|
|
596
698
|
let auth_mode = agent
|
|
@@ -612,6 +714,14 @@ fn running_agent_state(
|
|
|
612
714
|
state.insert("model".to_string(), model.map_or(serde_json::Value::Null, |m| serde_json::json!(m)));
|
|
613
715
|
state.insert("auth_mode".to_string(), serde_json::json!(auth_mode));
|
|
614
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
|
+
}
|
|
615
725
|
state.insert("window".to_string(), serde_json::json!(window));
|
|
616
726
|
state.insert(
|
|
617
727
|
"mcp_config".to_string(),
|
|
@@ -622,23 +732,60 @@ fn running_agent_state(
|
|
|
622
732
|
permissions_json(agent, id, provider)
|
|
623
733
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?,
|
|
624
734
|
);
|
|
735
|
+
persist_effective_approval_policy(&mut state, safety);
|
|
625
736
|
state.insert("session_id".to_string(), serde_json::Value::Null);
|
|
626
737
|
state.insert("rollout_path".to_string(), serde_json::Value::Null);
|
|
627
738
|
state.insert("captured_at".to_string(), serde_json::Value::Null);
|
|
628
739
|
state.insert("captured_via".to_string(), serde_json::Value::Null);
|
|
629
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
|
+
}
|
|
630
744
|
state.insert(
|
|
631
745
|
"spawn_cwd".to_string(),
|
|
632
|
-
serde_json::json!(
|
|
746
|
+
serde_json::json!(spawn_cwd.to_string_lossy().to_string()),
|
|
633
747
|
);
|
|
634
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
|
+
}
|
|
635
752
|
if let Some(pane_pid) = pane_pid {
|
|
636
753
|
state.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
|
|
637
754
|
}
|
|
638
755
|
Ok(serde_json::Value::Object(state))
|
|
639
756
|
}
|
|
640
757
|
|
|
641
|
-
fn
|
|
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(
|
|
642
789
|
config: crate::provider::McpConfig,
|
|
643
790
|
workspace: &Path,
|
|
644
791
|
agent_id: &str,
|
|
@@ -681,7 +828,7 @@ fn resolve_mcp_placeholders(
|
|
|
681
828
|
}
|
|
682
829
|
}
|
|
683
830
|
|
|
684
|
-
fn write_worker_mcp_config(
|
|
831
|
+
pub(crate) fn write_worker_mcp_config(
|
|
685
832
|
workspace: &Path,
|
|
686
833
|
agent_id: &str,
|
|
687
834
|
config: &crate::provider::McpConfig,
|
|
@@ -700,7 +847,7 @@ fn write_worker_mcp_config(
|
|
|
700
847
|
Ok(path)
|
|
701
848
|
}
|
|
702
849
|
|
|
703
|
-
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) {
|
|
704
851
|
if !matches!(provider, Provider::Claude | Provider::ClaudeCode) {
|
|
705
852
|
return;
|
|
706
853
|
}
|
|
@@ -766,6 +913,22 @@ fn spawn_timestamp() -> String {
|
|
|
766
913
|
}
|
|
767
914
|
}
|
|
768
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
|
+
|
|
769
932
|
pub(crate) fn fill_spawn_placeholders(argv: &mut [String], workspace: &Path, agent_id: &str) {
|
|
770
933
|
fill_spawn_placeholders_full(argv, workspace, agent_id, None);
|
|
771
934
|
}
|
|
@@ -808,6 +971,87 @@ pub(crate) fn inherited_env_with_team_overrides(
|
|
|
808
971
|
env
|
|
809
972
|
}
|
|
810
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
|
+
|
|
811
1055
|
fn is_posix_shell_identifier(name: &str) -> bool {
|
|
812
1056
|
let mut chars = name.chars();
|
|
813
1057
|
match chars.next() {
|
|
@@ -885,7 +1129,7 @@ fn explicit_active_team_key(state: &serde_json::Value) -> Option<String> {
|
|
|
885
1129
|
state
|
|
886
1130
|
.get("active_team_key")
|
|
887
1131
|
.and_then(serde_json::Value::as_str)
|
|
888
|
-
.filter(|team| !team.is_empty()
|
|
1132
|
+
.filter(|team| !team.is_empty())
|
|
889
1133
|
.map(str::to_string)
|
|
890
1134
|
}
|
|
891
1135
|
|
|
@@ -933,6 +1177,150 @@ fn quick_start_requested_team_key<'a>(team_id: Option<&'a str>, name: Option<&'a
|
|
|
933
1177
|
team_id.or(name).filter(|team| !team.is_empty())
|
|
934
1178
|
}
|
|
935
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
|
+
|
|
936
1324
|
fn runtime_state_has_quick_start_team(state: &serde_json::Value, team: &str) -> bool {
|
|
937
1325
|
explicit_active_team_key(state).as_deref() == Some(team)
|
|
938
1326
|
|| state
|
|
@@ -988,8 +1376,7 @@ pub fn quick_start_in_workspace(
|
|
|
988
1376
|
fresh: bool,
|
|
989
1377
|
team_id: Option<&str>,
|
|
990
1378
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
991
|
-
let workspace =
|
|
992
|
-
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1379
|
+
let workspace = explicit_quick_start_workspace(workspace);
|
|
993
1380
|
quick_start_with_transport_in_workspace(
|
|
994
1381
|
&workspace,
|
|
995
1382
|
agents_dir,
|
|
@@ -1002,6 +1389,18 @@ pub fn quick_start_in_workspace(
|
|
|
1002
1389
|
)
|
|
1003
1390
|
}
|
|
1004
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
|
+
|
|
1005
1404
|
/// `quick_start` with an injected transport — tests inject a recording mock so the REAL spawn path
|
|
1006
1405
|
/// (launch dry_run=false → spawn_agents) is asserted without a live tmux; prod uses the real TmuxBackend.
|
|
1007
1406
|
pub fn quick_start_with_transport(
|
|
@@ -1038,6 +1437,19 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1038
1437
|
.map(str::to_string)
|
|
1039
1438
|
.or_else(|| spec_team_id(&spec));
|
|
1040
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
|
+
}
|
|
1041
1453
|
if !fresh {
|
|
1042
1454
|
let state_path = crate::state::persist::runtime_state_path(&workspace);
|
|
1043
1455
|
if state_path.exists() {
|
|
@@ -1058,9 +1470,9 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1058
1470
|
next_actions: vec![
|
|
1059
1471
|
"run restart to resume the existing team or pass --fresh to replace it".to_string(),
|
|
1060
1472
|
],
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1064
1476
|
}
|
|
1065
1477
|
// CR-040/042: repeated quick-start from one template with distinct --team-id/--name
|
|
1066
1478
|
// must NOT collide on the template-derived tmux session. Override the compiled
|
|
@@ -1070,22 +1482,39 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1070
1482
|
if let Some(requested) = requested_team.as_deref() {
|
|
1071
1483
|
override_spec_session_name(&mut spec, &format!("team-{requested}"));
|
|
1072
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
|
+
});
|
|
1073
1490
|
let spec_path = agents_dir.join("team.spec.yaml");
|
|
1074
1491
|
std::fs::write(&spec_path, yaml::dumps(&spec)).map_err(|e| {
|
|
1075
1492
|
LifecycleError::StatePersist(format!("{}: {e}", spec_path.display()))
|
|
1076
1493
|
})?;
|
|
1077
1494
|
let _store = crate::message_store::MessageStore::open(&workspace)
|
|
1078
1495
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1079
|
-
let session_name = spec_session_name(&spec);
|
|
1080
1496
|
let resolved_spec_path = std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
|
|
1081
1497
|
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
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
|
+
)?;
|
|
1085
1505
|
// FIX (rt-host-a real-machine finding): dry_run=false so launch_with_transport calls spawn_agents
|
|
1086
1506
|
// and really creates the tmux session + worker windows (was hardcoded true → never spawned, which
|
|
1087
1507
|
// 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)?;
|
|
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);
|
|
1089
1518
|
let coordinator_workspace = crate::coordinator::WorkspacePath::new(workspace.clone());
|
|
1090
1519
|
let coordinator_started = crate::coordinator::start_coordinator(&coordinator_workspace)
|
|
1091
1520
|
.map(|report| report.ok)
|
|
@@ -1102,7 +1531,7 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1102
1531
|
// loaded successfully (provider-side codex/claude schema rejections happen
|
|
1103
1532
|
// asynchronously after spawn), so the verdict is PendingToolLoad — never
|
|
1104
1533
|
// bare Ready.
|
|
1105
|
-
let worker_readiness = quick_start_worker_readiness(&workspace);
|
|
1534
|
+
let worker_readiness = quick_start_worker_readiness(&workspace, &state_team_key);
|
|
1106
1535
|
Ok(QuickStartReport::Ready {
|
|
1107
1536
|
session_name,
|
|
1108
1537
|
launch: Box::new(launch),
|
|
@@ -1118,13 +1547,21 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1118
1547
|
/// verdict to `Degraded { unhealthy_agents }` (sorted, deduped); otherwise
|
|
1119
1548
|
/// `PendingToolLoad` — never bare Ready. State read failure is treated as
|
|
1120
1549
|
/// PendingToolLoad rather than fabricated success.
|
|
1121
|
-
fn quick_start_worker_readiness(workspace: &Path) -> QuickStartReadiness {
|
|
1550
|
+
fn quick_start_worker_readiness(workspace: &Path, team_key: &str) -> QuickStartReadiness {
|
|
1122
1551
|
let Ok(state) = load_runtime_state(workspace) else {
|
|
1123
1552
|
return QuickStartReadiness::PendingToolLoad;
|
|
1124
1553
|
};
|
|
1125
|
-
let
|
|
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 {
|
|
1126
1560
|
return QuickStartReadiness::PendingToolLoad;
|
|
1127
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;
|
|
1128
1565
|
let mut unhealthy: Vec<String> = agents
|
|
1129
1566
|
.iter()
|
|
1130
1567
|
.filter_map(|(id, agent)| {
|
|
@@ -1135,15 +1572,79 @@ fn quick_start_worker_readiness(workspace: &Path) -> QuickStartReadiness {
|
|
|
1135
1572
|
}
|
|
1136
1573
|
})
|
|
1137
1574
|
.collect();
|
|
1138
|
-
if unhealthy.is_empty() {
|
|
1139
|
-
QuickStartReadiness::PendingToolLoad
|
|
1140
|
-
} else {
|
|
1575
|
+
if !unhealthy.is_empty() {
|
|
1141
1576
|
unhealthy.sort();
|
|
1142
1577
|
unhealthy.dedup();
|
|
1143
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
|
|
1144
1584
|
}
|
|
1145
1585
|
}
|
|
1146
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
|
+
|
|
1147
1648
|
/// `detect_inherited_dangerous_permissions`(`launch/config.py`):扫进程祖先链找
|
|
1148
1649
|
/// `--dangerously-*` flag,产出危险审批继承态。launch 在 inherited=false 且无 --yes 时拒。
|
|
1149
1650
|
pub fn detect_dangerous_approval() -> Result<DangerousApproval, LifecycleError> {
|
|
@@ -1365,7 +1866,7 @@ pub fn add_agent(
|
|
|
1365
1866
|
agent_id,
|
|
1366
1867
|
role_file_path,
|
|
1367
1868
|
open_display,
|
|
1368
|
-
|
|
1869
|
+
Some(selected.team_key.as_str()),
|
|
1369
1870
|
&crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace),
|
|
1370
1871
|
)
|
|
1371
1872
|
}
|
|
@@ -1402,12 +1903,15 @@ fn add_agent_with_transport_at_paths(
|
|
|
1402
1903
|
team: Option<&str>,
|
|
1403
1904
|
transport: &dyn Transport,
|
|
1404
1905
|
) -> Result<AddAgentReport, LifecycleError> {
|
|
1405
|
-
let
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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()))?;
|
|
1411
1915
|
ensure_owner_allowed_for_state(&owner_state, Some(agent_id))?;
|
|
1412
1916
|
if !role_file_path.exists() {
|
|
1413
1917
|
return Err(LifecycleError::Compile(format!(
|
|
@@ -1423,13 +1927,21 @@ fn add_agent_with_transport_at_paths(
|
|
|
1423
1927
|
let dynamic_role_file = materialize_added_role_file(team_dir, agent_id, role_file_path)?;
|
|
1424
1928
|
let spec = crate::compiler::compile_team(team_dir)
|
|
1425
1929
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
1930
|
+
let safety = effective_runtime_config(&spec)?;
|
|
1426
1931
|
let spec_path = team_dir.join("team.spec.yaml");
|
|
1427
1932
|
std::fs::write(&spec_path, yaml::dumps(&spec)).map_err(|e| {
|
|
1428
1933
|
LifecycleError::StatePersist(format!("{}: {e}", spec_path.display()))
|
|
1429
1934
|
})?;
|
|
1430
1935
|
let (meta, _) = crate::compiler::read_front_matter(&dynamic_role_file)
|
|
1431
1936
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
1432
|
-
upsert_agent_state_from_role(
|
|
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
|
+
)?;
|
|
1433
1945
|
let started = crate::lifecycle::restart::start_agent_at_paths(
|
|
1434
1946
|
run_workspace,
|
|
1435
1947
|
team_dir,
|
|
@@ -1437,7 +1949,7 @@ fn add_agent_with_transport_at_paths(
|
|
|
1437
1949
|
false,
|
|
1438
1950
|
open_display,
|
|
1439
1951
|
true,
|
|
1440
|
-
|
|
1952
|
+
Some(&canonical_team_key),
|
|
1441
1953
|
transport,
|
|
1442
1954
|
)?;
|
|
1443
1955
|
let (env, start_mode) = match started {
|
|
@@ -1460,18 +1972,14 @@ fn add_agent_with_transport_at_paths(
|
|
|
1460
1972
|
|
|
1461
1973
|
fn upsert_agent_state_from_role(
|
|
1462
1974
|
workspace: &Path,
|
|
1463
|
-
|
|
1975
|
+
canonical_team_key: &str,
|
|
1464
1976
|
agent_id: &AgentId,
|
|
1465
1977
|
meta: &Value,
|
|
1466
1978
|
dynamic_role_file: &Path,
|
|
1979
|
+
safety: &DangerousApproval,
|
|
1467
1980
|
) -> Result<(), LifecycleError> {
|
|
1468
|
-
let mut state =
|
|
1469
|
-
|
|
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
|
-
};
|
|
1981
|
+
let mut state = crate::state::projection::select_runtime_state(workspace, Some(canonical_team_key))
|
|
1982
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
1475
1983
|
if !state.is_object() {
|
|
1476
1984
|
state = serde_json::json!({});
|
|
1477
1985
|
}
|
|
@@ -1513,16 +2021,31 @@ fn upsert_agent_state_from_role(
|
|
|
1513
2021
|
if let Some(model) = meta.get("model").and_then(Value::as_str) {
|
|
1514
2022
|
if let Some(obj) = entry.as_object_mut() {
|
|
1515
2023
|
obj.insert("model".to_string(), serde_json::json!(model));
|
|
2024
|
+
obj.insert("model_source".to_string(), serde_json::json!("role"));
|
|
1516
2025
|
}
|
|
1517
2026
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
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
|
+
}
|
|
1525
2043
|
}
|
|
2044
|
+
if let Some(obj) = entry.as_object_mut() {
|
|
2045
|
+
persist_effective_approval_policy(obj, safety);
|
|
2046
|
+
}
|
|
2047
|
+
agent_map.insert(agent_id.as_str().to_string(), entry);
|
|
2048
|
+
save_launched_team_state_for_key(workspace, &state, Some(canonical_team_key))
|
|
1526
2049
|
}
|
|
1527
2050
|
|
|
1528
2051
|
fn materialize_added_role_file(
|
|
@@ -1667,61 +2190,158 @@ pub fn fork_agent_with_transport(
|
|
|
1667
2190
|
let safety = effective_runtime_config(&new_spec)?;
|
|
1668
2191
|
let tools = worker_tool_refs(agent_tool_strings(new_agent), &safety);
|
|
1669
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);
|
|
1670
2194
|
let mcp_config = adapter
|
|
1671
2195
|
.mcp_config(auth_mode)
|
|
1672
2196
|
.map_err(|e| {
|
|
1673
2197
|
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
1674
2198
|
LifecycleError::Provider(e.to_string())
|
|
1675
2199
|
})?;
|
|
1676
|
-
let
|
|
1677
|
-
|
|
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(
|
|
1678
2221
|
Some(&session_id),
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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
|
+
},
|
|
1684
2230
|
)
|
|
1685
2231
|
.map_err(|e| {
|
|
1686
2232
|
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
1687
2233
|
LifecycleError::Provider(e.to_string())
|
|
1688
2234
|
})?;
|
|
1689
|
-
|
|
1690
|
-
|
|
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));
|
|
1691
2239
|
let window = WindowName::new(as_agent_id.as_str());
|
|
1692
2240
|
// fork inherits the parent agent's owner team via runtime state (`active_team_key`).
|
|
1693
|
-
let env = inherited_env_with_team_overrides(
|
|
2241
|
+
let mut env = inherited_env_with_team_overrides(
|
|
1694
2242
|
&workspace,
|
|
1695
2243
|
as_agent_id.as_str(),
|
|
1696
2244
|
Some(&fork_team),
|
|
1697
2245
|
);
|
|
2246
|
+
apply_profile_launch_env(&mut env, &profile_launch);
|
|
1698
2247
|
// golden operations.py:336 -> _tmux_start_command_for_agent_window (runtime.py:1017-1020): branch on
|
|
1699
2248
|
// _tmux_session_exists — an ABSENT session => new-session (spawn_first), present => new-window
|
|
1700
2249
|
// (spawn_into). The Rust restart seam (restart.rs spawn_agent_window) uses the same branch.
|
|
1701
2250
|
let session_live = transport.has_session(&session_name).unwrap_or(false);
|
|
1702
2251
|
let spawn_result = if session_live {
|
|
1703
|
-
transport.spawn_into(&session_name, &window, &argv, &workspace, &env)
|
|
2252
|
+
transport.spawn_into(&session_name, &window, &plan.argv, &workspace, &env)
|
|
1704
2253
|
} else {
|
|
1705
|
-
transport.spawn_first(&session_name, &window, &argv, &workspace, &env)
|
|
2254
|
+
transport.spawn_first(&session_name, &window, &plan.argv, &workspace, &env)
|
|
1706
2255
|
};
|
|
1707
|
-
let
|
|
2256
|
+
let spawn = spawn_result.map_err(|e| {
|
|
1708
2257
|
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
1709
2258
|
LifecycleError::Transport(e.to_string())
|
|
1710
2259
|
})?;
|
|
1711
2260
|
let old_state = state.clone();
|
|
1712
2261
|
let mut next_state = state;
|
|
1713
|
-
upsert_forked_agent_state(
|
|
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
|
+
}
|
|
1714
2297
|
if let Err(e) = save_runtime_state(&workspace, &next_state) {
|
|
1715
|
-
rollback_fork_after_spawn(
|
|
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
|
+
);
|
|
1716
2310
|
return Err(LifecycleError::StatePersist(e.to_string()));
|
|
1717
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
|
+
}
|
|
1718
2327
|
let coordinator_started =
|
|
1719
2328
|
crate::coordinator::start_coordinator(&crate::coordinator::WorkspacePath::new(
|
|
1720
2329
|
workspace.to_path_buf(),
|
|
1721
2330
|
))
|
|
1722
2331
|
.map(|report| report.ok)
|
|
1723
2332
|
.map_err(|e| {
|
|
1724
|
-
rollback_fork_after_spawn(
|
|
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
|
+
);
|
|
1725
2345
|
LifecycleError::StatePersist(e.to_string())
|
|
1726
2346
|
})?;
|
|
1727
2347
|
Ok(ForkAgentReport {
|
|
@@ -1744,6 +2364,9 @@ fn rollback_fork_after_spawn(
|
|
|
1744
2364
|
transport: &dyn Transport,
|
|
1745
2365
|
session_name: &SessionName,
|
|
1746
2366
|
window: &WindowName,
|
|
2367
|
+
mcp_config_path: &Path,
|
|
2368
|
+
agent_id: &AgentId,
|
|
2369
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
1747
2370
|
) {
|
|
1748
2371
|
let _ = transport.kill_window(&Target::SessionWindow {
|
|
1749
2372
|
session: session_name.clone(),
|
|
@@ -1751,6 +2374,41 @@ fn rollback_fork_after_spawn(
|
|
|
1751
2374
|
});
|
|
1752
2375
|
let _ = std::fs::write(spec_path, spec_text.as_bytes());
|
|
1753
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
|
+
}
|
|
1754
2412
|
}
|
|
1755
2413
|
|
|
1756
2414
|
fn leader_id_matches(spec: &Value, agent_id: &AgentId) -> bool {
|
|
@@ -1883,6 +2541,12 @@ fn upsert_forked_agent_state(
|
|
|
1883
2541
|
source_agent_id: &AgentId,
|
|
1884
2542
|
as_agent_id: &AgentId,
|
|
1885
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>,
|
|
1886
2550
|
) -> Result<(), LifecycleError> {
|
|
1887
2551
|
if !state.is_object() {
|
|
1888
2552
|
*state = serde_json::json!({});
|
|
@@ -1907,15 +2571,46 @@ fn upsert_forked_agent_state(
|
|
|
1907
2571
|
.get("provider")
|
|
1908
2572
|
.and_then(Value::as_str)
|
|
1909
2573
|
.unwrap_or("codex");
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
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()),
|
|
1918
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
|
+
}
|
|
1919
2614
|
Ok(())
|
|
1920
2615
|
}
|
|
1921
2616
|
|
|
@@ -2095,6 +2790,13 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
|
|
|
2095
2790
|
true
|
|
2096
2791
|
}
|
|
2097
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
|
+
|
|
2098
2800
|
fn spec_tasks_json(spec: &Value) -> serde_json::Value {
|
|
2099
2801
|
spec.get("tasks")
|
|
2100
2802
|
.and_then(Value::as_list)
|