@team-agent/installer 0.5.68 → 0.5.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/diagnose.rs +74 -0
  4. package/crates/team-agent/src/cli/emit.rs +5 -4
  5. package/crates/team-agent/src/cli/leader.rs +1 -0
  6. package/crates/team-agent/src/cli/spec.rs +1 -0
  7. package/crates/team-agent/src/compiler.rs +62 -5
  8. package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
  9. package/crates/team-agent/src/layout/worker_window_helpers.rs +1 -0
  10. package/crates/team-agent/src/leader/start.rs +53 -12
  11. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +67 -15
  12. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +18 -0
  13. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +1 -0
  14. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +260 -44
  15. package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +120 -0
  16. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +5 -1
  17. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +10 -59
  18. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +830 -0
  19. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +9 -3
  20. package/crates/team-agent/src/lifecycle/launch/spawn.rs +70 -28
  21. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +1 -0
  22. package/crates/team-agent/src/lifecycle/launch.rs +10 -2
  23. package/crates/team-agent/src/lifecycle/profile_launch.rs +3 -2
  24. package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -0
  25. package/crates/team-agent/src/lifecycle/restart/agent.rs +90 -160
  26. package/crates/team-agent/src/lifecycle/restart/common.rs +146 -28
  27. package/crates/team-agent/src/lifecycle/restart/remove.rs +37 -1
  28. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +24 -3
  29. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +195 -18
  30. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +30 -12
  31. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_g0.stdout.txt +222 -0
  32. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +5 -2
  33. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +35 -48
  34. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
  35. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +97 -0
  36. package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +128 -0
  37. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +207 -0
  38. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +838 -0
  39. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +320 -0
  40. package/crates/team-agent/src/lifecycle/tests/pi_lifecycle_doctor_red.rs +98 -0
  41. package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +216 -0
  42. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1 -0
  43. package/crates/team-agent/src/lifecycle/tests.rs +7 -0
  44. package/crates/team-agent/src/lifecycle/worker_command_context.rs +61 -4
  45. package/crates/team-agent/src/messaging/delivery.rs +36 -12
  46. package/crates/team-agent/src/model/enums.rs +3 -1
  47. package/crates/team-agent/src/model/permissions.rs +1 -1
  48. package/crates/team-agent/src/provider/adapter.rs +29 -3
  49. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +5 -2
  50. package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
  51. package/crates/team-agent/src/provider/adapters/pi.rs +152 -0
  52. package/crates/team-agent/src/provider/bypass_flags.rs +1 -0
  53. package/crates/team-agent/src/provider/classify.rs +2 -2
  54. package/crates/team-agent/src/provider/faults.rs +1 -1
  55. package/crates/team-agent/src/provider/session/context_fork.rs +2 -1
  56. package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
  57. package/crates/team-agent/src/provider/session_scan/pi.rs +172 -0
  58. package/crates/team-agent/src/provider/session_scan.rs +4 -0
  59. package/crates/team-agent/src/provider/tests/adapter.rs +80 -0
  60. package/crates/team-agent/src/provider/wire.rs +7 -1
  61. package/crates/team-agent/src/tmux_backend.rs +52 -0
  62. package/package.json +4 -4
  63. package/skills/team-agent/SKILL.md +2 -4
@@ -183,7 +183,11 @@ pub(crate) fn annotate_runtime_tmux_endpoint(
183
183
  transport: &dyn Transport,
184
184
  workspace: &Path,
185
185
  ) {
186
- let Some(endpoint) = transport.tmux_endpoint() else {
186
+ let transport_endpoint = transport.tmux_endpoint();
187
+ let endpoint = crate::tmux_backend::owning_tmux_endpoint_from_state(state)
188
+ .map(|(endpoint, _source)| endpoint.to_string())
189
+ .or_else(|| transport_endpoint.clone());
190
+ let Some(endpoint) = endpoint else {
187
191
  return;
188
192
  };
189
193
  let endpoint_for_state = if Path::new(&endpoint).is_absolute() || endpoint == "default" {
@@ -208,8 +212,10 @@ pub(crate) fn annotate_runtime_tmux_endpoint(
208
212
  .cloned()
209
213
  .unwrap_or(serde_json::Value::Null),
210
214
  );
211
- if let Some(source) = selected_tmux_socket_source(transport, workspace) {
212
- obj.insert("tmux_socket_source".to_string(), serde_json::json!(source));
215
+ if transport_endpoint.as_deref() == Some(endpoint.as_str()) {
216
+ if let Some(source) = selected_tmux_socket_source(transport, workspace) {
217
+ obj.insert("tmux_socket_source".to_string(), serde_json::json!(source));
218
+ }
213
219
  }
214
220
  }
215
221
  }
@@ -135,12 +135,16 @@ pub(super) fn spawn_agents(
135
135
  .mcp_config(auth_mode)
136
136
  .map_err(|e| LifecycleError::Provider(e.to_string()))?;
137
137
  let mcp_config = resolve_mcp_config(mcp_config, workspace, agent_id_raw, &mcp_team_id);
138
- let mcp_config_path = write_worker_mcp_config_for_provider(
139
- workspace,
140
- agent_id_raw,
141
- &mcp_config,
142
- Some(provider),
143
- )?;
138
+ let mcp_config_path = if provider == Provider::Pi {
139
+ None
140
+ } else {
141
+ Some(write_worker_mcp_config_for_provider(
142
+ workspace,
143
+ agent_id_raw,
144
+ &mcp_config,
145
+ Some(provider),
146
+ )?)
147
+ };
144
148
  let profile_dir = team_dir.join("profiles");
145
149
  let profile_launch =
146
150
  crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
@@ -159,24 +163,54 @@ pub(super) fn spawn_agents(
159
163
  let _ = crate::event_log::EventLog::new(workspace)
160
164
  .write("provider.effort_unsupported", event_value);
161
165
  }
162
- let mut plan = adapter
163
- .build_command_plan(crate::provider::ProviderCommandContext {
164
- auth_mode,
165
- mcp_config: Some(&mcp_config),
166
- system_prompt: Some(system_prompt.as_str()),
167
- model: command_model,
168
- tools: &resolved_tool_refs,
169
- profile_launch: Some(&profile_launch),
170
- // Layer 1 self-healing (architect probe 2026-06-22): expose
171
- // agent_id as a display-name hint so Claude / Copilot
172
- // adapters can pass `--name <agent_id>`. Codex has no
173
- // equivalent flag and ignores the hint.
174
- agent_id_hint: Some(agent_id_raw),
175
- effort: agent_effort,
176
- })
177
- .map_err(|e| LifecycleError::Provider(e.to_string()))?;
166
+ let command_context = crate::provider::ProviderCommandContext {
167
+ auth_mode,
168
+ mcp_config: Some(&mcp_config),
169
+ system_prompt: Some(system_prompt.as_str()),
170
+ model: command_model,
171
+ tools: &resolved_tool_refs,
172
+ profile_launch: Some(&profile_launch),
173
+ // Layer 1 self-healing (architect probe 2026-06-22): expose
174
+ // agent_id as a display-name hint so Claude / Copilot
175
+ // adapters can pass `--name <agent_id>`. Codex has no
176
+ // equivalent flag and ignores the hint.
177
+ agent_id_hint: Some(agent_id_raw),
178
+ effort: agent_effort,
179
+ };
180
+ let mut plan = if provider == Provider::Pi {
181
+ let model = command_model.ok_or_else(|| {
182
+ LifecycleError::RequirementUnmet(
183
+ "Pi launch requires an explicit qualified model".to_string(),
184
+ )
185
+ })?;
186
+ let effort = agent_effort.ok_or_else(|| {
187
+ LifecycleError::RequirementUnmet(
188
+ "Pi launch requires an explicit thinking effort".to_string(),
189
+ )
190
+ })?;
191
+ crate::lifecycle::launch::pi_mcp::materialize_pi_plan(
192
+ crate::lifecycle::launch::pi_mcp::PiMaterializeRequest {
193
+ workspace,
194
+ team_id: &mcp_team_id,
195
+ agent_id: agent_id_raw,
196
+ model,
197
+ effort,
198
+ system_prompt: &system_prompt,
199
+ tool_categories: &resolved_tool_refs,
200
+ team_mcp_tools: &["send_message", "report_result"],
201
+ mcp_config: &mcp_config,
202
+ },
203
+ )
204
+ .map_err(|e| LifecycleError::Provider(e.to_string()))?
205
+ } else {
206
+ adapter
207
+ .build_command_plan(command_context)
208
+ .map_err(|e| LifecycleError::Provider(e.to_string()))?
209
+ };
178
210
  if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
179
- point_native_mcp_config_at_file(&mut plan.argv, provider, &mcp_config_path);
211
+ if let Some(mcp_config_path) = mcp_config_path.as_ref() {
212
+ point_native_mcp_config_at_file(&mut plan.argv, provider, mcp_config_path);
213
+ }
180
214
  }
181
215
  // C-A-4 cr verdict v2 — Copilot BYOK(compatible_api)硬性校验:
182
216
  // "A model is required for BYOK"(help-providers 原文)。检查 agent
@@ -287,15 +321,23 @@ pub(super) fn spawn_agents(
287
321
  );
288
322
  }
289
323
  }
290
- // Cursor: role 经 .cursor/rules;MCP 身份必须写进 mcp.json env
291
- // (不继承父进程 TEAM_AGENT_*)。--workspace 钉物理路径。
292
- // mcp.json last-writer:同 workspace 第二 CursorAgent 拒绝。
324
+ // Cursor: role 经 .cursor/rules。MCP 身份默认写 provider-config/<id>/cursor
325
+ // (不继承父进程 TEAM_AGENT_*)。--workspace 指该工程根,--add-dir 真 workspace。
326
+ // 隔离关闭时仍拒绝第二席(共用 mcp.json last-writer)。不改 HOME。
293
327
  if matches!(provider, Provider::CursorAgent) {
294
328
  refuse_second_cursor_occupant(workspace, agent_id_raw, Some(spec))?;
295
329
  apply_cursor_agent_rules_overlay(workspace, agent_id_raw, system_prompt.as_str())?;
296
330
  apply_cursor_mcp_overlay(workspace, &mcp_config)?;
297
- enable_cursor_workspace_mcp(workspace)?;
298
- apply_cursor_workspace_physical_path(&mut plan.argv, workspace);
331
+ let project = if crate::lifecycle::launch::cursor_mcp_isolation_enabled() {
332
+ Some(crate::lifecycle::launch::cursor_mcp_project_dir(
333
+ workspace,
334
+ agent_id_raw,
335
+ )?)
336
+ } else {
337
+ None
338
+ };
339
+ enable_cursor_workspace_mcp(workspace, project.as_deref())?;
340
+ apply_cursor_spawn_workspace_pointers(&mut plan.argv, workspace, agent_id_raw)?;
299
341
  let proxy = apply_cursor_subscription_proxy_env(&mut env);
300
342
  let event_log = crate::event_log::EventLog::new(workspace);
301
343
  let _ = event_log.write(
@@ -599,6 +599,7 @@ pub(crate) fn provider_display_str(provider: Provider) -> &'static str {
599
599
  Provider::GeminiCli => "gemini_cli",
600
600
  Provider::Grok => "grok",
601
601
  Provider::CursorAgent => "cursor_agent",
602
+ Provider::Pi => "pi",
602
603
  Provider::Fake => "fake",
603
604
  }
604
605
  }
@@ -307,10 +307,18 @@ pub(crate) use mcp_config::{
307
307
  write_worker_mcp_config_for_provider,
308
308
  };
309
309
 
310
+ pub(crate) mod pi_mcp;
311
+
312
+ mod cursor_mcp_iso;
313
+ pub use cursor_mcp_iso::{
314
+ cursor_mcp_isolation_enabled, cursor_mcp_json_path, cursor_mcp_project_dir,
315
+ materialize_cursor_mcp_project,
316
+ };
310
317
  mod cursor_mcp;
311
318
  pub use cursor_mcp::{
312
- apply_cursor_mcp_overlay, apply_cursor_workspace_physical_path, cursor_mcp_enable_argv,
313
- enable_cursor_workspace_mcp, physical_workspace_path, refuse_second_cursor_occupant,
319
+ apply_cursor_mcp_overlay, apply_cursor_spawn_workspace_pointers,
320
+ apply_cursor_workspace_physical_path, cursor_mcp_enable_argv, enable_cursor_workspace_mcp,
321
+ physical_workspace_path, refuse_second_cursor_occupant,
314
322
  };
315
323
 
316
324
  mod cursor_create_chat;
@@ -484,7 +484,7 @@ fn provider_env_exports(
484
484
  // grok/cursor_agent 一期无 profile exports(subscription 已登录态)。
485
485
  // Cursor 订阅席的代理透传不在这里:profile 只服务 compatible-api。
486
486
  // 订阅路径见 apply_cursor_subscription_proxy_env(只记有无,不记值)。
487
- Provider::Grok | Provider::CursorAgent => {}
487
+ Provider::Grok | Provider::CursorAgent | Provider::Pi => {}
488
488
  Provider::Fake => {}
489
489
  }
490
490
  exports
@@ -567,6 +567,7 @@ pub(crate) fn provider_env_unsets(provider: Provider, auth_mode: AuthMode) -> BT
567
567
  // 禁止在这里 unset 代理。
568
568
  Provider::Grok => {}
569
569
  Provider::CursorAgent => {}
570
+ Provider::Pi => {}
570
571
  Provider::Fake => {}
571
572
  }
572
573
  unsets
@@ -992,7 +993,7 @@ fn required_profile_keys(provider: Provider, auth_mode: AuthMode) -> &'static [&
992
993
  // 二期 BYOK 立项时填(向后兼容字段保留)。
993
994
  Provider::Copilot => &[],
994
995
  // 0.5.67: grok/cursor_agent 一期无 BYOK required key 集合 (同 copilot 精神)。
995
- Provider::Grok | Provider::CursorAgent => &[],
996
+ Provider::Grok | Provider::CursorAgent | Provider::Pi => &[],
996
997
  Provider::Fake => &[],
997
998
  }
998
999
  }
@@ -236,6 +236,7 @@ impl SmokeTarget {
236
236
  Provider::Copilot
237
237
  | Provider::Grok
238
238
  | Provider::CursorAgent
239
+ | Provider::Pi
239
240
  | Provider::GeminiCli
240
241
  | Provider::Fake => {
241
242
  Err("unsupported_provider_smoke_skipped")
@@ -399,85 +399,97 @@ pub(crate) fn start_agent_at_paths(
399
399
  return Err(error);
400
400
  }
401
401
  let actual_spawn_window = spawn.spawn.window.as_str().to_string();
402
- mark_agent_started(
403
- &mut state,
404
- agent_id,
405
- &actual_spawn_window,
406
- &spawn,
407
- transport,
408
- &safety,
409
- start_mode,
410
- )?;
411
- // 0.5.66 bypass 单源 §2.7:启动即带 bypass → 审计留痕。
412
- if safety.enabled {
413
- let state_agent = state
414
- .get("agents")
415
- .and_then(|v| v.get(agent_id.as_str()));
416
- let _ = crate::event_log::EventLog::new(workspace).write(
417
- "worker.spawn_dangerously_skip_permissions",
418
- serde_json::json!({
419
- "agent_id": agent_id.as_str(),
420
- "role": state_agent
421
- .and_then(|a| a.get("role"))
422
- .and_then(serde_json::Value::as_str)
423
- .unwrap_or_default(),
424
- "provider": state_agent
425
- .and_then(|a| a.get("provider"))
426
- .and_then(serde_json::Value::as_str)
427
- .unwrap_or_default(),
428
- "trigger": "start-agent",
429
- "spec_source_path": crate::lifecycle::launch::agent_id_spec_source_path(
430
- workspace, agent_id.as_str(),
431
- ),
432
- }),
402
+ let post_spawn = (|| -> Result<bool, LifecycleError> {
403
+ mark_agent_started(
404
+ &mut state,
405
+ agent_id,
406
+ &actual_spawn_window,
407
+ &spawn,
408
+ transport,
409
+ &safety,
410
+ start_mode,
411
+ )?;
412
+ // 0.5.66 bypass 单源 §2.7:启动即带 bypass → 审计留痕。
413
+ if safety.enabled {
414
+ let state_agent = state.get("agents").and_then(|v| v.get(agent_id.as_str()));
415
+ let _ = crate::event_log::EventLog::new(workspace).write(
416
+ "worker.spawn_dangerously_skip_permissions",
417
+ serde_json::json!({
418
+ "agent_id": agent_id.as_str(),
419
+ "role": state_agent
420
+ .and_then(|a| a.get("role"))
421
+ .and_then(serde_json::Value::as_str)
422
+ .unwrap_or_default(),
423
+ "provider": state_agent
424
+ .and_then(|a| a.get("provider"))
425
+ .and_then(serde_json::Value::as_str)
426
+ .unwrap_or_default(),
427
+ "trigger": "start-agent",
428
+ "spec_source_path": crate::lifecycle::launch::agent_id_spec_source_path(
429
+ workspace, agent_id.as_str(),
430
+ ),
431
+ }),
432
+ );
433
+ }
434
+ // **0.3.24 add-agent socket drift fix**: keep `state.tmux_endpoint` /
435
+ // `state.tmux_socket` synchronized with the transport actually used for the
436
+ // spawn. Without this, add-agent / fork-agent could spawn to a socket that
437
+ // never gets persisted, and the next coordinator tick would re-resolve to
438
+ // the workspace-hash socket and lose the new pane. Annotation runs inside
439
+ // the same `save_restart_projected_state` window — no parallel "annotate
440
+ // after spawn" race with coordinator and no double source of truth.
441
+ crate::lifecycle::launch::annotate_runtime_tmux_endpoint(&mut state, transport, workspace);
442
+ let team_key = restart_projection_team_key(&state, team);
443
+ // 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
444
+ // clear the matching `agent_health` observation on the new spawn cohort
445
+ // so five-line status summary and status --json do not surface a stale
446
+ // WORKING row from the pre-shutdown process. Best-effort: DB failure
447
+ // must not fail the start-agent path.
448
+ let _ = crate::db::agent_health_capture::clear_agent_health_observation(
449
+ workspace, &team_key, agent_id,
433
450
  );
434
- }
435
- // **0.3.24 add-agent socket drift fix**: keep `state.tmux_endpoint` /
436
- // `state.tmux_socket` synchronized with the transport actually used for the
437
- // spawn. Without this, add-agent / fork-agent could spawn to a socket that
438
- // never gets persisted, and the next coordinator tick would re-resolve to
439
- // the workspace-hash socket and lose the new pane. Annotation runs inside
440
- // the same `save_restart_projected_state` window — no parallel "annotate
441
- // after spawn" race with coordinator and no double source of truth.
442
- crate::lifecycle::launch::annotate_runtime_tmux_endpoint(&mut state, transport, workspace);
443
- let team_key = restart_projection_team_key(&state, team);
444
- // 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
445
- // clear the matching `agent_health` observation on the new spawn cohort
446
- // so five-line status summary and status --json do not surface a stale
447
- // WORKING row from the pre-shutdown process. Best-effort: DB failure
448
- // must not fail the start-agent path.
449
- let _ = crate::db::agent_health_capture::clear_agent_health_observation(
450
- workspace, &team_key, agent_id,
451
- );
452
- let skip_capture_backfill = if matches!(
453
- start_mode,
454
- StartMode::Fresh | StartMode::FreshAfterMissingRollout
455
- ) {
456
- vec![agent_id.as_str()]
457
- } else {
458
- Vec::new()
451
+ let skip_capture_backfill = if matches!(
452
+ start_mode,
453
+ StartMode::Fresh | StartMode::FreshAfterMissingRollout
454
+ ) {
455
+ vec![agent_id.as_str()]
456
+ } else {
457
+ Vec::new()
458
+ };
459
+ save_restart_projected_state_with_capture_backfill_skip(
460
+ workspace,
461
+ &mut state,
462
+ &team_key,
463
+ &skip_capture_backfill,
464
+ &[agent_id.as_str()],
465
+ )?;
466
+ write_start_agent_start_event(
467
+ workspace,
468
+ agent_id,
469
+ &agent,
470
+ provider,
471
+ &spawn.plan,
472
+ start_mode,
473
+ &session_name,
474
+ &actual_spawn_window,
475
+ spawn_session_id,
476
+ tmux_start_mode_for_spawn(&spawn, into_existing_session),
477
+ )?;
478
+ replay_worker_target_missing_messages(workspace, agent_id, &team_key, &state, transport)?;
479
+ Ok(start_coordinator_for_workspace(workspace, Some(&team_key))?.ok)
480
+ })();
481
+ let coordinator_started = match post_spawn {
482
+ Ok(started) => started,
483
+ Err(error) => {
484
+ if let Err(rollback_error) = transport.kill_pane(&spawn.spawn.pane_id) {
485
+ return Err(LifecycleError::RequirementUnmet(format!(
486
+ "{error}; failed to roll back spawned pane {}: {rollback_error}",
487
+ spawn.spawn.pane_id.as_str()
488
+ )));
489
+ }
490
+ return Err(error);
491
+ }
459
492
  };
460
- save_restart_projected_state_with_capture_backfill_skip(
461
- workspace,
462
- &mut state,
463
- &team_key,
464
- &skip_capture_backfill,
465
- &[agent_id.as_str()],
466
- )?;
467
- write_start_agent_start_event(
468
- workspace,
469
- agent_id,
470
- &agent,
471
- provider,
472
- start_mode,
473
- &session_name,
474
- &actual_spawn_window,
475
- spawn_session_id,
476
- tmux_start_mode_for_spawn(&spawn, into_existing_session),
477
- )?;
478
- replay_worker_target_missing_messages(workspace, agent_id, &team_key, &state, transport)?;
479
- let coordinator = start_coordinator_for_workspace(workspace, Some(&team_key))?;
480
- let coordinator_started = coordinator.ok;
481
493
  Ok(StartAgentOutcome::Running {
482
494
  env: AgentActionEnvelope {
483
495
  agent_id: agent_id.clone(),
@@ -1772,95 +1784,13 @@ fn write_start_agent_start_event(
1772
1784
  agent_id: &AgentId,
1773
1785
  agent: &serde_json::Value,
1774
1786
  provider: crate::provider::Provider,
1787
+ plan: &crate::provider::CommandPlan,
1775
1788
  start_mode: StartMode,
1776
1789
  session_name: &SessionName,
1777
1790
  window: &str,
1778
1791
  session_id: Option<&SessionId>,
1779
1792
  tmux_start_mode: &'static str,
1780
1793
  ) -> Result<(), LifecycleError> {
1781
- let auth_mode = agent_auth_mode(agent);
1782
- let model = agent.get("model").and_then(|v| v.as_str());
1783
- let adapter = crate::provider::get_adapter(provider);
1784
- // Contract C / F6.4: event log must record the same context-aware argv that the
1785
- // actual spawn used — so the role/tools/MCP context appears in `start_agent.agent_start`.
1786
- let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
1787
- agent,
1788
- Some(agent_id.as_str()),
1789
- provider,
1790
- )?;
1791
- let system_prompt =
1792
- crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
1793
- let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
1794
- &command_agent,
1795
- provider,
1796
- )?;
1797
- let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
1798
- let mcp_config = adapter
1799
- .mcp_config(auth_mode)
1800
- .map_err(|e| LifecycleError::Provider(e.to_string()))?;
1801
- let team_id = agent.get("owner_team_id").and_then(|v| v.as_str());
1802
- let mcp_config = crate::lifecycle::launch::resolve_mcp_config(
1803
- mcp_config,
1804
- workspace,
1805
- agent_id.as_str(),
1806
- team_id.unwrap_or(""),
1807
- );
1808
- let mcp_config_path = crate::lifecycle::launch::write_worker_mcp_config(
1809
- workspace,
1810
- agent_id.as_str(),
1811
- &mcp_config,
1812
- )?;
1813
- let profile_launch =
1814
- crate::lifecycle::profile_launch::prepare_provider_profile_launch_from_json(
1815
- workspace,
1816
- agent_id.as_str(),
1817
- agent,
1818
- Some(&mcp_config),
1819
- )?;
1820
- let command_model = profile_launch.command_overrides.model.as_deref().or(model);
1821
- // 0.4.x provider effort MVP: start_agent path preserves effort from the
1822
- // persisted agent JSON.
1823
- let start_agent_effort =
1824
- crate::lifecycle::launch::provider_effort_for_spawn_json(&agent, provider);
1825
- if let Some(event_value) = crate::lifecycle::launch::provider_effort_event_if_dropped_json(
1826
- &agent,
1827
- provider,
1828
- agent_id.as_str(),
1829
- ) {
1830
- let _ = crate::event_log::EventLog::new(workspace)
1831
- .write("provider.effort_unsupported", event_value);
1832
- }
1833
- let context = crate::provider::ProviderCommandContext {
1834
- auth_mode,
1835
- mcp_config: Some(&mcp_config),
1836
- system_prompt: Some(system_prompt.as_str()),
1837
- model: command_model,
1838
- tools: &resolved_tool_refs,
1839
- profile_launch: Some(&profile_launch),
1840
- agent_id_hint: Some(agent_id.as_str()),
1841
- effort: start_agent_effort,
1842
- };
1843
- let mut plan = match session_id {
1844
- Some(session_id) => adapter
1845
- .build_resume_command_plan(Some(session_id), context)
1846
- .map_err(|e| LifecycleError::Provider(e.to_string()))?,
1847
- None => adapter
1848
- .build_command_plan(context)
1849
- .map_err(|e| LifecycleError::Provider(e.to_string()))?,
1850
- };
1851
- if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
1852
- crate::lifecycle::launch::point_native_mcp_config_at_file(
1853
- &mut plan.argv,
1854
- provider,
1855
- &mcp_config_path,
1856
- );
1857
- }
1858
- crate::lifecycle::launch::fill_spawn_placeholders_full(
1859
- &mut plan.argv,
1860
- workspace,
1861
- agent_id.as_str(),
1862
- team_id,
1863
- );
1864
1794
  crate::event_log::EventLog::new(workspace)
1865
1795
  .write(
1866
1796
  "start_agent.agent_start",