@team-agent/installer 0.5.112 → 0.5.113

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 (44) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +100 -44
  4. package/crates/team-agent/src/cli/emit.rs +127 -98
  5. package/crates/team-agent/src/cli/leaders.rs +32 -32
  6. package/crates/team-agent/src/cli/mod.rs +6 -6
  7. package/crates/team-agent/src/cli/named_address.rs +17 -16
  8. package/crates/team-agent/src/cli/route.rs +4 -4
  9. package/crates/team-agent/src/cli/send/presentation.rs +19 -18
  10. package/crates/team-agent/src/cli/send/resolve.rs +11 -11
  11. package/crates/team-agent/src/cli/send.rs +4 -4
  12. package/crates/team-agent/src/cli/spec.rs +29 -29
  13. package/crates/team-agent/src/cli/status.rs +7 -7
  14. package/crates/team-agent/src/cli/tests/base.rs +6 -6
  15. package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
  16. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
  17. package/crates/team-agent/src/cli/tests/named_address.rs +3 -3
  18. package/crates/team-agent/src/cli/tests/status_send.rs +7 -7
  19. package/crates/team-agent/src/cli/triage.rs +53 -2
  20. package/crates/team-agent/src/cli/types.rs +8 -8
  21. package/crates/team-agent/src/compiler/tests.rs +12 -0
  22. package/crates/team-agent/src/compiler.rs +0 -9
  23. package/crates/team-agent/src/leader/start.rs +5 -0
  24. package/crates/team-agent/src/leader/tests/identity.rs +21 -0
  25. package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
  26. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +325 -14
  27. package/crates/team-agent/src/lifecycle/launch.rs +2 -2
  28. package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
  29. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
  30. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  31. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -2
  32. package/crates/team-agent/src/provider/adapter.rs +8 -59
  33. package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
  34. package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
  35. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
  36. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
  37. package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
  38. package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
  39. package/crates/team-agent/src/provider/command_helpers.rs +45 -0
  40. package/crates/team-agent/src/provider/mod.rs +1 -0
  41. package/crates/team-agent/src/provider/model_catalog.rs +21 -28
  42. package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
  43. package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
  44. package/package.json +4 -4
@@ -41,6 +41,71 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
41
41
  // `pane_verify_ms`, `startup_prompt_handler_ms`, `tmux_start_mode`) so
42
42
  // bounded-concurrency spawn can be justified with real numbers.
43
43
 
44
+ /// Stable wire labels for launch/restart timing events, not readiness states.
45
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
46
+ pub(crate) enum LifecyclePhase {
47
+ ResolveContext,
48
+ CompileSpec,
49
+ PlanClassification,
50
+ Teardown,
51
+ SpawnAll,
52
+ SaveState,
53
+ CoordinatorStart,
54
+ ReadinessWait,
55
+ Completed,
56
+ }
57
+
58
+ impl LifecyclePhase {
59
+ pub(crate) fn as_str(self) -> &'static str {
60
+ match self {
61
+ Self::ResolveContext => "resolve_context",
62
+ Self::CompileSpec => "compile_spec",
63
+ Self::PlanClassification => "plan_classification",
64
+ Self::Teardown => "teardown",
65
+ Self::SpawnAll => "spawn_all",
66
+ Self::SaveState => "save_state",
67
+ Self::CoordinatorStart => "coordinator_start",
68
+ Self::ReadinessWait => "readiness_wait",
69
+ Self::Completed => "completed",
70
+ }
71
+ }
72
+
73
+ /// Pure event formatting shared by launch/restart; no clock, I/O or readiness policy.
74
+ pub(crate) fn event_fields(self, elapsed_ms: u64) -> serde_json::Value {
75
+ serde_json::json!({
76
+ "phase": self.as_str(),
77
+ "elapsed_ms": elapsed_ms,
78
+ })
79
+ }
80
+ }
81
+
82
+ #[test]
83
+ fn lifecycle_phase_wire_labels_remain_stable() {
84
+ for (phase, wire) in [
85
+ (LifecyclePhase::ResolveContext, "resolve_context"),
86
+ (LifecyclePhase::CompileSpec, "compile_spec"),
87
+ (LifecyclePhase::PlanClassification, "plan_classification"),
88
+ (LifecyclePhase::Teardown, "teardown"),
89
+ (LifecyclePhase::SpawnAll, "spawn_all"),
90
+ (LifecyclePhase::SaveState, "save_state"),
91
+ (LifecyclePhase::CoordinatorStart, "coordinator_start"),
92
+ (LifecyclePhase::ReadinessWait, "readiness_wait"),
93
+ (LifecyclePhase::Completed, "completed"),
94
+ ] {
95
+ assert_eq!(phase.as_str(), wire);
96
+ for elapsed_ms in [0, 123, u64::MAX] {
97
+ assert_eq!(phase.event_fields(elapsed_ms), serde_json::json!({
98
+ "phase": wire,
99
+ "elapsed_ms": elapsed_ms,
100
+ }));
101
+ assert_eq!(
102
+ serde_json::to_string(&phase.event_fields(elapsed_ms)).expect("phase JSON"),
103
+ format!("{{\"phase\":\"{wire}\",\"elapsed_ms\":{elapsed_ms}}}"),
104
+ );
105
+ }
106
+ }
107
+ }
108
+
44
109
  pub(crate) struct RestartPhaseTimer {
45
110
  started_at: std::time::Instant,
46
111
  }
@@ -71,15 +136,9 @@ impl RestartPhaseTimer {
71
136
  /// phase: 阶段名
72
137
  /// returns: 写事件失败被吞掉,不影响主流程
73
138
  /// ---
74
- pub(crate) fn emit(&self, workspace: &Path, kind: &'static str, phase: &'static str) {
139
+ pub(crate) fn emit(&self, workspace: &Path, kind: &'static str, phase: LifecyclePhase) {
75
140
  let event_log = crate::event_log::EventLog::new(workspace);
76
- let _ = event_log.write(
77
- kind,
78
- serde_json::json!({
79
- "phase": phase,
80
- "elapsed_ms": self.elapsed_ms(),
81
- }),
82
- );
141
+ let _ = event_log.write(kind, phase.event_fields(self.elapsed_ms()));
83
142
  }
84
143
  }
85
144
 
@@ -305,7 +364,7 @@ fn restart_with_selected_team_and_transport(
305
364
  // resolved a context; downstream phases are emitted with monotonic
306
365
  // `elapsed_ms` for at-a-glance latency triage.
307
366
  let phase_timer = RestartPhaseTimer::start();
308
- phase_timer.emit(&selected.run_workspace, "restart.phase", "resolve_context");
367
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::ResolveContext);
309
368
  let lifecycle_lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
310
369
  workspace: &selected.run_workspace,
311
370
  operation: "restart",
@@ -345,7 +404,7 @@ fn restart_with_selected_team_and_transport(
345
404
  // 写 runtime spec。角色缺(TEAM.md/agents 不在)→ 显式拒(列缺哪些),旧 spec 原地保留不删不用。
346
405
  let spec =
347
406
  rebuild_runtime_spec_from_roles(&selected.run_workspace, &selected.team_key, &state)?;
348
- phase_timer.emit(&selected.run_workspace, "restart.phase", "compile_spec");
407
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::CompileSpec);
349
408
  // 重建后 spec_workspace 恒为 runtime spec 的父目录(.team/runtime/<team_key>/)。
350
409
  let runtime_spec =
351
410
  crate::model::paths::runtime_spec_path(&selected.run_workspace, &selected.team_key);
@@ -441,7 +500,7 @@ fn restart_with_selected_team_and_transport(
441
500
  phase_timer.emit(
442
501
  &selected.run_workspace,
443
502
  "restart.phase",
444
- "plan_classification",
503
+ LifecyclePhase::PlanClassification,
445
504
  );
446
505
  write_restart_resume_decision_events(
447
506
  &selected.run_workspace,
@@ -558,8 +617,8 @@ fn restart_with_selected_team_and_transport(
558
617
  &topology_authority_agent_ids,
559
618
  )?;
560
619
  }
561
- phase_timer.emit(&selected.run_workspace, "restart.phase", "teardown");
562
- phase_timer.emit(&selected.run_workspace, "restart.phase", "spawn_all");
620
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::Teardown);
621
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::SpawnAll);
563
622
  // 0.5.40 Slice 3 (tmux-server-death-locate §7 Slice 3): when the
564
623
  // pre-spawn teardown was DEFERRED (live worker session with
565
624
  // authoritative running agents), snapshot the pre-spawn agent rows
@@ -582,8 +641,8 @@ fn restart_with_selected_team_and_transport(
582
641
  // tmux session is created deterministically; the remaining independent
583
642
  // decisions run their `transport.spawn_into` in parallel via a thread
584
643
  // scope with concurrency capped at `min(4, workers-1)`. Every spawn
585
- // result is collected in-memory; `verify_spawned_agent_live` +
586
- // `mark_agent_respawned` are then applied in ORIGINAL PLAN ORDER so
644
+ // result is collected in-memory; `mark_agent_respawned` is then
645
+ // applied in ORIGINAL PLAN ORDER so
587
646
  // persisted `spawn_epoch` / `spawned_at` / `pane_id` / `window` stay
588
647
  // deterministic regardless of thread completion order. Failure
589
648
  // aggregation stays equivalent to serial: per-agent errors go into
@@ -837,17 +896,14 @@ fn restart_with_selected_team_and_transport(
837
896
  agent.get("dangerously_skip_permissions"),
838
897
  Some(serde_json::Value::Bool(true))
839
898
  );
840
- if let Err(error) = verify_spawned_agent_live(&decision.agent_id, &spawn, transport)
841
- .and_then(|_| {
842
- mark_agent_respawned(
843
- &mut state,
844
- &decision.agent_id,
845
- decision.restart_mode,
846
- &spawn,
847
- transport,
848
- launched_bypass,
849
- )
850
- })
899
+ if let Err(error) = mark_agent_respawned(
900
+ &mut state,
901
+ &decision.agent_id,
902
+ decision.restart_mode,
903
+ &spawn,
904
+ transport,
905
+ launched_bypass,
906
+ )
851
907
  {
852
908
  let error = error.to_string();
853
909
  mark_agent_restart_failed(&mut state, decision, &error);
@@ -1026,7 +1082,7 @@ fn restart_with_selected_team_and_transport(
1026
1082
  &capture_backfill_skip_agent_ids,
1027
1083
  &topology_authority_agent_ids,
1028
1084
  )?;
1029
- phase_timer.emit(&selected.run_workspace, "restart.phase", "save_state");
1085
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::SaveState);
1030
1086
  if fatal_resume_failure {
1031
1087
  let attach_commands = Vec::new();
1032
1088
  let next_actions = restart_failure_next_actions(&failed_agents);
@@ -1304,10 +1360,10 @@ fn restart_with_selected_team_and_transport(
1304
1360
  phase_timer.emit(
1305
1361
  &selected.run_workspace,
1306
1362
  "restart.phase",
1307
- "coordinator_start",
1363
+ LifecyclePhase::CoordinatorStart,
1308
1364
  );
1309
1365
  let coordinator_started = coordinator.ok;
1310
- phase_timer.emit(&selected.run_workspace, "restart.phase", "readiness_wait");
1366
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::ReadinessWait);
1311
1367
  wait_restart_readiness_or_timeout(
1312
1368
  &selected.run_workspace,
1313
1369
  &state,
@@ -1331,7 +1387,7 @@ fn restart_with_selected_team_and_transport(
1331
1387
  let mut next_actions = Vec::new();
1332
1388
  if !failed_agents.is_empty() {
1333
1389
  next_actions.extend(restart_failure_next_actions(&failed_agents));
1334
- phase_timer.emit(&selected.run_workspace, "restart.phase", "completed");
1390
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::Completed);
1335
1391
  write_restart_completed_event(
1336
1392
  &selected.run_workspace,
1337
1393
  &successful_agents,
@@ -1360,7 +1416,7 @@ fn restart_with_selected_team_and_transport(
1360
1416
  leader_bind_reason,
1361
1417
  });
1362
1418
  }
1363
- phase_timer.emit(&selected.run_workspace, "restart.phase", "completed");
1419
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::Completed);
1364
1420
  write_restart_completed_event(
1365
1421
  &selected.run_workspace,
1366
1422
  &successful_agents,
@@ -1852,14 +1908,6 @@ fn yes_no(value: bool) -> &'static str {
1852
1908
  }
1853
1909
  }
1854
1910
 
1855
- fn verify_spawned_agent_live(
1856
- _agent_id: &AgentId,
1857
- _spawn: &SpawnedAgentWindow,
1858
- _transport: &dyn crate::transport::Transport,
1859
- ) -> Result<(), LifecycleError> {
1860
- Ok(())
1861
- }
1862
-
1863
1911
  /// 0.3.30 Bug 1: restart success path auto-attach.
1864
1912
  /// When `TMUX_PANE` is present in the caller's env, treat the restart as if
1865
1913
  /// the user had also run `attach-leader` from that pane. Mirrors quick-start's
@@ -3088,8 +3136,8 @@ fn run_bounded_parallel_worker_spawns(
3088
3136
  outcomes
3089
3137
  }
3090
3138
 
3091
- /// 0.5.38 Step 2: serial post-spawn stage. Runs verify_spawned_agent_live +
3092
- /// mark_agent_respawned in plan order, emits worker.spawn_timing, and
3139
+ /// 0.5.38 Step 2: serial post-spawn stage. Runs mark_agent_respawned
3140
+ /// in plan order, emits worker.spawn_timing, and
3093
3141
  /// updates the successful/failed agent lists exactly like the pre-0.5.38
3094
3142
  /// serial loop. Keeping this serial guarantees deterministic persisted
3095
3143
  /// state (`spawn_epoch`, `spawned_at`, etc.) regardless of parallel
@@ -3111,17 +3159,14 @@ fn apply_marked_respawn(
3111
3159
  launched_bypass: bool,
3112
3160
  ) {
3113
3161
  let verify_start = std::time::Instant::now();
3114
- if let Err(error) =
3115
- verify_spawned_agent_live(&decision.agent_id, spawn, transport).and_then(|_| {
3116
- mark_agent_respawned(
3117
- state,
3118
- &decision.agent_id,
3119
- decision.restart_mode,
3120
- spawn,
3121
- transport,
3122
- launched_bypass,
3123
- )
3124
- })
3162
+ if let Err(error) = mark_agent_respawned(
3163
+ state,
3164
+ &decision.agent_id,
3165
+ decision.restart_mode,
3166
+ spawn,
3167
+ transport,
3168
+ launched_bypass,
3169
+ )
3125
3170
  {
3126
3171
  let error = error.to_string();
3127
3172
  mark_agent_restart_failed(state, decision, &error);
@@ -100,7 +100,7 @@ pub(crate) use rebuild::restart_with_transport_with_session_convergence_deadline
100
100
  // timer + worker timing writer so `lifecycle::launch::launch_with_transport_in_workspace`
101
101
  // can emit the same instrumentation event family with `source="launch"`.
102
102
  pub(crate) use rebuild::{
103
- provider_wire_from_state, write_worker_spawn_timing_event, RestartPhaseTimer,
103
+ provider_wire_from_state, write_worker_spawn_timing_event, LifecyclePhase, RestartPhaseTimer,
104
104
  };
105
105
  pub use rebuild::{restart, restart_with_session_convergence_deadline};
106
106
  pub(crate) use rebuild::{
@@ -165,14 +165,15 @@ fn purge_agent_help_and_dispatch_are_consistent() {
165
165
  output_text(&case.run(["purge-agent", "ghost", "--workspace", case.ws(), "--json"]));
166
166
  if help.contains("purge-agent") {
167
167
  assert!(
168
- !command.contains("invalid choice") && !command.contains("unknown subcommand") && !command.contains("Commands:"),
168
+ !command.contains("invalid choice") && !command.contains("unknown subcommand")
169
+ && !command.contains("Unknown command:") && !command.contains("Commands:"),
169
170
  "RED6: help exposes purge-agent, so dispatch must be real or the command must be removed from help; help={help} command={command}"
170
171
  );
171
172
  } else {
172
173
  assert!(
173
174
  command.contains("unknown subcommand")
174
175
  || command.contains("invalid choice")
175
- || command.contains("没有这个操作"),
176
+ || command.contains("Unknown command:"),
176
177
  "RED6: if purge-agent is not implemented, help and dispatch must agree it is absent; help={help} command={command}"
177
178
  );
178
179
  }
@@ -300,6 +300,10 @@ pub fn get_adapter(p: Provider) -> Box<dyn ProviderAdapter> {
300
300
  Box::new(BasicProviderAdapter { provider: p })
301
301
  }
302
302
 
303
+ #[cfg(test)]
304
+ #[path = "tests/command_helpers.rs"]
305
+ mod command_helpers_tests;
306
+
303
307
  #[derive(Debug, Clone, Copy)]
304
308
  pub(crate) struct BasicProviderAdapter {
305
309
  provider: Provider,
@@ -480,22 +484,13 @@ impl ProviderAdapter for BasicProviderAdapter {
480
484
  Ok(argv)
481
485
  }
482
486
  Provider::Grok => Ok(grok_launch_command(
483
- self,
484
- auth_mode,
485
- mcp_config,
486
487
  system_prompt,
487
488
  model,
488
489
  dangerously_skip_permissions,
489
490
  )?),
490
491
  Provider::CursorAgent => Ok(cursor_agent_base_command(
491
- self,
492
- auth_mode,
493
- mcp_config,
494
- system_prompt,
495
492
  model,
496
493
  dangerously_skip_permissions,
497
- false,
498
- None,
499
494
  )?),
500
495
  Provider::Pi => Err(ProviderError::Command(
501
496
  "Pi commands require the shared lifecycle materializer".to_string(),
@@ -596,14 +591,9 @@ impl ProviderAdapter for BasicProviderAdapter {
596
591
  // never written and restart could not resume.
597
592
  let expected = next_session_token();
598
593
  let mut argv = grok_base_command(
599
- self,
600
- ctx.auth_mode,
601
- ctx.mcp_config,
602
594
  ctx.system_prompt,
603
595
  ctx.model,
604
596
  ctx.dangerously_skip_permissions,
605
- ctx.profile_launch
606
- .is_some_and(|profile| profile.managed_mcp_config),
607
597
  ctx.effort,
608
598
  )?;
609
599
  argv.push("--session-id".to_string());
@@ -637,15 +627,8 @@ impl ProviderAdapter for BasicProviderAdapter {
637
627
  // U-01 create-chat 无登录超时;禁止发明 --session-id。
638
628
  // pending 只能是 CLI chatId,本拍不造假 uuid。
639
629
  let argv = cursor_agent_base_command(
640
- self,
641
- ctx.auth_mode,
642
- ctx.mcp_config,
643
- ctx.system_prompt,
644
630
  ctx.model,
645
631
  ctx.dangerously_skip_permissions,
646
- ctx.profile_launch
647
- .is_some_and(|profile| profile.managed_mcp_config),
648
- ctx.effort,
649
632
  )?;
650
633
  Ok(CommandPlan {
651
634
  argv,
@@ -816,7 +799,7 @@ impl ProviderAdapter for BasicProviderAdapter {
816
799
  // copilot --resume 接受 id|name)。
817
800
  Provider::Copilot => {
818
801
  let mut argv =
819
- copilot_base_command_resume(auth_mode, mcp_config, system_prompt, model, dangerously_skip_permissions);
802
+ copilot_base_command(auth_mode, mcp_config, system_prompt, model, dangerously_skip_permissions);
820
803
  argv.push("--resume".to_string());
821
804
  argv.push(session_id.as_str().to_string());
822
805
  Ok(argv)
@@ -824,13 +807,9 @@ impl ProviderAdapter for BasicProviderAdapter {
824
807
  // Grok resume: base + `--resume <id>` (grok -r/--resume 接受 id|title)。
825
808
  Provider::Grok => {
826
809
  let mut argv = grok_base_command(
827
- self,
828
- auth_mode,
829
- mcp_config,
830
810
  system_prompt,
831
811
  model,
832
812
  dangerously_skip_permissions,
833
- false,
834
813
  None,
835
814
  )?;
836
815
  argv.push("--resume".to_string());
@@ -839,16 +818,7 @@ impl ProviderAdapter for BasicProviderAdapter {
839
818
  }
840
819
  // Cursor resume: base + `--resume <chatId>`。
841
820
  Provider::CursorAgent => {
842
- let mut argv = cursor_agent_base_command(
843
- self,
844
- auth_mode,
845
- mcp_config,
846
- system_prompt,
847
- model,
848
- dangerously_skip_permissions,
849
- false,
850
- None,
851
- )?;
821
+ let mut argv = cursor_agent_base_command(model, dangerously_skip_permissions)?;
852
822
  // Put --resume immediately after `agent` so the CLI treats it as
853
823
  // the session mode rather than a trailing flag after --workspace.
854
824
  argv.insert(1, "--resume".to_string());
@@ -941,14 +911,9 @@ impl ProviderAdapter for BasicProviderAdapter {
941
911
  ));
942
912
  };
943
913
  let mut argv = grok_base_command(
944
- self,
945
- ctx.auth_mode,
946
- ctx.mcp_config,
947
914
  ctx.system_prompt,
948
915
  ctx.model,
949
916
  ctx.dangerously_skip_permissions,
950
- ctx.profile_launch
951
- .is_some_and(|profile| profile.managed_mcp_config),
952
917
  ctx.effort,
953
918
  )?;
954
919
  argv.push("--resume".to_string());
@@ -968,15 +933,8 @@ impl ProviderAdapter for BasicProviderAdapter {
968
933
  ));
969
934
  }
970
935
  let mut argv = cursor_agent_base_command(
971
- self,
972
- ctx.auth_mode,
973
- ctx.mcp_config,
974
- ctx.system_prompt,
975
936
  ctx.model,
976
937
  ctx.dangerously_skip_permissions,
977
- ctx.profile_launch
978
- .is_some_and(|profile| profile.managed_mcp_config),
979
- ctx.effort,
980
938
  )?;
981
939
  argv.push("--resume".to_string());
982
940
  argv.push(raw.to_string());
@@ -1067,13 +1025,9 @@ impl ProviderAdapter for BasicProviderAdapter {
1067
1025
  // (grok help: --fork-session 需配合 --resume/--continue)。
1068
1026
  Provider::Grok => {
1069
1027
  let mut argv = grok_base_command(
1070
- self,
1071
- auth_mode,
1072
- mcp_config,
1073
1028
  system_prompt,
1074
1029
  model,
1075
1030
  dangerously_skip_permissions,
1076
- false,
1077
1031
  None,
1078
1032
  )?;
1079
1033
  argv.push("--session-id".to_string());
@@ -1191,7 +1145,7 @@ impl ProviderAdapter for BasicProviderAdapter {
1191
1145
  .ok_or_else(|| {
1192
1146
  ProviderError::Command("copilot fork root was not materialized".to_string())
1193
1147
  })?;
1194
- let mut argv = copilot_base_command_resume(
1148
+ let mut argv = copilot_base_command(
1195
1149
  ctx.auth_mode,
1196
1150
  ctx.mcp_config,
1197
1151
  ctx.system_prompt,
@@ -1214,14 +1168,9 @@ impl ProviderAdapter for BasicProviderAdapter {
1214
1168
  ));
1215
1169
  };
1216
1170
  let mut argv = grok_base_command(
1217
- self,
1218
- ctx.auth_mode,
1219
- ctx.mcp_config,
1220
1171
  ctx.system_prompt,
1221
1172
  ctx.model,
1222
1173
  ctx.dangerously_skip_permissions,
1223
- ctx.profile_launch
1224
- .is_some_and(|profile| profile.managed_mcp_config),
1225
1174
  ctx.effort,
1226
1175
  )?;
1227
1176
  argv.push("--session-id".to_string());
@@ -1405,7 +1354,7 @@ pub(crate) fn json_inline(value: &serde_json::Value) -> String {
1405
1354
  // the extracted base-command functions, not directly by this file.
1406
1355
  use super::adapters::claude::{claude_base_command, claude_launch_command};
1407
1356
  use super::adapters::codex::codex_base_command;
1408
- use super::adapters::copilot::{copilot_base_command, copilot_base_command_resume};
1357
+ use super::adapters::copilot::copilot_base_command;
1409
1358
  use super::adapters::cursor_agent::cursor_agent_base_command;
1410
1359
  use super::adapters::fake::fake_worker_command;
1411
1360
  use super::adapters::grok::{grok_base_command, grok_launch_command};
@@ -11,6 +11,7 @@
11
11
  use crate::model::enums::{AuthMode, Provider};
12
12
  use crate::provider::adapter::{next_session_token, prompt_needs_native_mcp, BasicProviderAdapter};
13
13
  use crate::provider::{McpConfig, ProviderAdapter, ProviderError};
14
+ use crate::provider::command_helpers::{append_opt_pair, append_pair};
14
15
 
15
16
  pub(crate) fn claude_launch_command(
16
17
  adapter: &BasicProviderAdapter,
@@ -30,8 +31,7 @@ pub(crate) fn claude_launch_command(
30
31
  false,
31
32
  None,
32
33
  )?;
33
- argv.push("--session-id".to_string());
34
- argv.push(next_session_token());
34
+ append_pair(&mut argv, "--session-id", next_session_token());
35
35
  Ok(argv)
36
36
  }
37
37
 
@@ -54,21 +54,14 @@ pub(crate) fn claude_base_command(
54
54
  .expect("claude provider must define a bypass flag");
55
55
  argv.push(flag.to_string());
56
56
  } else {
57
- argv.push("--permission-mode".to_string());
58
- argv.push("default".to_string());
59
- }
60
- if let Some(model) = model {
61
- argv.push("--model".to_string());
62
- argv.push(model.to_string());
57
+ append_pair(&mut argv, "--permission-mode", "default");
63
58
  }
59
+ append_opt_pair(&mut argv, "--model", model);
64
60
  if let Some(effort) = effort {
65
61
  argv.push("--effort".to_string());
66
62
  argv.push(effort.as_str().to_string());
67
63
  }
68
- if let Some(prompt) = system_prompt {
69
- argv.push("--append-system-prompt".to_string());
70
- argv.push(prompt.to_string());
71
- }
64
+ append_opt_pair(&mut argv, "--append-system-prompt", system_prompt);
72
65
  if !managed_mcp_config
73
66
  && (mcp_config.is_some()
74
67
  || auth_mode == AuthMode::CompatibleApi
@@ -79,8 +72,7 @@ pub(crate) fn claude_base_command(
79
72
  } else {
80
73
  serde_json::json!({"mcpServers": adapter.mcp_config(auth_mode)?.raw})
81
74
  };
82
- argv.push("--mcp-config".to_string());
83
- argv.push(raw.to_string());
75
+ append_pair(&mut argv, "--mcp-config", raw.to_string());
84
76
  }
85
77
  Ok(argv)
86
78
  }
@@ -8,6 +8,7 @@
8
8
 
9
9
  use crate::model::enums::AuthMode;
10
10
  use crate::provider::adapter::json_inline;
11
+ use crate::provider::command_helpers::{append_opt_pair, append_pair};
11
12
  use crate::provider::{McpConfig, ProviderCommandOverrides};
12
13
 
13
14
  pub(crate) fn codex_base_command(
@@ -36,24 +37,17 @@ pub(crate) fn codex_base_command(
36
37
  "--disable".to_string(),
37
38
  "apps".to_string(),
38
39
  ]);
39
- if let Some(profile) = overrides.and_then(|o| o.codex_profile.as_deref()) {
40
- argv.push("--profile".to_string());
41
- argv.push(profile.to_string());
42
- }
40
+ append_opt_pair(&mut argv, "--profile", overrides.and_then(|o| o.codex_profile.as_deref()));
43
41
  if dangerously_skip_permissions {
44
42
  // 0.5.66 bypass 单源:flag 由 provider_bypass_flag 表供给。
45
43
  let flag = crate::provider::bypass_flags::provider_bypass_flag(crate::model::enums::Provider::Codex)
46
44
  .expect("codex provider must define a bypass flag");
47
45
  argv.push(flag.to_string());
48
46
  }
49
- if let Some(model) = model {
50
- argv.push("--model".to_string());
51
- argv.push(model.to_string());
52
- }
47
+ append_opt_pair(&mut argv, "--model", model);
53
48
  if let Some(overrides) = overrides {
54
49
  for config in &overrides.codex_config {
55
- argv.push("-c".to_string());
56
- argv.push(config.clone());
50
+ append_pair(&mut argv, "-c", config.clone());
57
51
  }
58
52
  }
59
53
  if let Some(effort) = effort {
@@ -66,8 +60,7 @@ pub(crate) fn codex_base_command(
66
60
  .replace('\\', "\\\\")
67
61
  .replace('"', "\\\"")
68
62
  .replace('\n', "\\n");
69
- argv.push("-c".to_string());
70
- argv.push(format!("developer_instructions=\"{escaped}\""));
63
+ append_pair(&mut argv, "-c", format!("developer_instructions=\"{escaped}\""));
71
64
  }
72
65
  if let Some(config) = mcp_config {
73
66
  append_codex_mcp_overrides(&mut argv, &config.raw);
@@ -91,8 +84,7 @@ pub(crate) fn append_codex_mcp_overrides(argv: &mut Vec<String>, raw: &serde_jso
91
84
  if key == "env" {
92
85
  if let Some(env) = value.as_object() {
93
86
  for (env_key, env_value) in env {
94
- argv.push("-c".to_string());
95
- argv.push(format!(
87
+ append_pair(argv, "-c", format!(
96
88
  "mcp_servers.{name}.env.{env_key}={}",
97
89
  json_inline(env_value)
98
90
  ));
@@ -100,12 +92,10 @@ pub(crate) fn append_codex_mcp_overrides(argv: &mut Vec<String>, raw: &serde_jso
100
92
  }
101
93
  continue;
102
94
  }
103
- argv.push("-c".to_string());
104
- argv.push(format!("mcp_servers.{name}.{key}={}", json_inline(value)));
95
+ append_pair(argv, "-c", format!("mcp_servers.{name}.{key}={}", json_inline(value)));
105
96
  }
106
97
  // Every MCP server gets a 600s tool timeout so long-running
107
98
  // team_orchestrator calls (report_result etc.) survive the codex default.
108
- argv.push("-c".to_string());
109
- argv.push(format!("mcp_servers.{name}.tool_timeout_sec=600.0"));
99
+ append_pair(argv, "-c", format!("mcp_servers.{name}.tool_timeout_sec=600.0"));
110
100
  }
111
101
  }
@@ -3,12 +3,13 @@
3
3
  //!
4
4
  //! Extracted from `provider/adapter.rs` (0.4.x decoupling step 2). Pure
5
5
  //! extraction — byte-identical to the original inline forms. Scope kept
6
- //! small: base command + resume + MCP type→transport
6
+ //! small: base command + MCP type→transport
7
7
  //! translation. Auth hint (`copilot_auth_hint`) stays in `adapter.rs`;
8
8
  //! session-store scanning lives under `provider/session_scan/copilot.rs`.
9
9
 
10
10
  use crate::model::enums::AuthMode;
11
11
  use crate::provider::McpConfig;
12
+ use crate::provider::command_helpers::{append_opt_pair, append_pair};
12
13
 
13
14
  pub(crate) fn copilot_base_command(
14
15
  auth_mode: AuthMode,
@@ -35,18 +36,13 @@ pub(crate) fn copilot_base_command(
35
36
  argv.push(flag.to_string());
36
37
  }
37
38
  // mcp_team ∈ canonical → approval-free (whole-server pattern).
38
- argv.push("--allow-tool".to_string());
39
- argv.push("team_orchestrator".to_string());
40
- if let Some(model) = model {
41
- argv.push("--model".to_string());
42
- argv.push(model.to_string());
43
- }
39
+ append_pair(&mut argv, "--allow-tool", "team_orchestrator");
40
+ append_opt_pair(&mut argv, "--model", model);
44
41
  if let Some(config) = mcp_config {
45
42
  // Copilot mcp config schema field is `transport` (stdio|http|sse),
46
43
  // not the canonical `type`. McpConfig.raw is canonical; only copilot
47
44
  // translates type→transport for --additional-mcp-config.
48
- argv.push("--additional-mcp-config".to_string());
49
- argv.push(copilot_translate_mcp_config(&config.raw).to_string());
45
+ append_pair(&mut argv, "--additional-mcp-config", copilot_translate_mcp_config(&config.raw).to_string());
50
46
  }
51
47
  argv
52
48
  }
@@ -77,16 +73,3 @@ pub(crate) fn copilot_translate_mcp_config(raw: &serde_json::Value) -> serde_jso
77
73
  }
78
74
  serde_json::Value::Object(translated)
79
75
  }
80
-
81
- /// Resume path = base + `--resume <sid>` (drop --session-id); kept separate
82
- /// to avoid the plan accidentally emitting --session-id and --resume in the
83
- /// same frame.
84
- pub(crate) fn copilot_base_command_resume(
85
- auth_mode: AuthMode,
86
- mcp_config: Option<&McpConfig>,
87
- system_prompt: Option<&str>,
88
- model: Option<&str>,
89
- dangerously_skip_permissions: bool,
90
- ) -> Vec<String> {
91
- copilot_base_command(auth_mode, mcp_config, system_prompt, model, dangerously_skip_permissions)
92
- }