@team-agent/installer 0.5.111 → 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 (73) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +93 -779
  4. package/crates/team-agent/src/cli/diagnose.rs +2 -424
  5. package/crates/team-agent/src/cli/emit.rs +158 -373
  6. package/crates/team-agent/src/cli/leaders.rs +32 -32
  7. package/crates/team-agent/src/cli/mod.rs +8 -15
  8. package/crates/team-agent/src/cli/named_address.rs +17 -16
  9. package/crates/team-agent/src/cli/route.rs +4 -4
  10. package/crates/team-agent/src/cli/send/presentation.rs +19 -18
  11. package/crates/team-agent/src/cli/send/resolve.rs +11 -11
  12. package/crates/team-agent/src/cli/send.rs +4 -4
  13. package/crates/team-agent/src/cli/spec.rs +29 -32
  14. package/crates/team-agent/src/cli/status.rs +7 -7
  15. package/crates/team-agent/src/cli/tests/base.rs +6 -6
  16. package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
  21. package/crates/team-agent/src/cli/tests/mod.rs +0 -2
  22. package/crates/team-agent/src/cli/tests/named_address.rs +3 -3
  23. package/crates/team-agent/src/cli/tests/status_send.rs +7 -7
  24. package/crates/team-agent/src/cli/triage.rs +53 -2
  25. package/crates/team-agent/src/cli/types.rs +12 -116
  26. package/crates/team-agent/src/compiler/tests.rs +12 -0
  27. package/crates/team-agent/src/compiler.rs +0 -9
  28. package/crates/team-agent/src/coordinator/health.rs +3 -283
  29. package/crates/team-agent/src/coordinator/mod.rs +0 -5
  30. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
  31. package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
  32. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
  33. package/crates/team-agent/src/coordinator/types.rs +1 -21
  34. package/crates/team-agent/src/db/message_store.rs +0 -42
  35. package/crates/team-agent/src/db/migration.rs +49 -9
  36. package/crates/team-agent/src/db/schema.rs +27 -20
  37. package/crates/team-agent/src/leader/owner_bind.rs +0 -24
  38. package/crates/team-agent/src/leader/start.rs +5 -0
  39. package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
  40. package/crates/team-agent/src/leader/tests/identity.rs +21 -32
  41. package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
  42. package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
  43. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +325 -14
  44. package/crates/team-agent/src/lifecycle/launch.rs +2 -2
  45. package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
  46. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
  47. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  48. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
  49. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -2
  50. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
  51. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
  52. package/crates/team-agent/src/messaging/mod.rs +0 -2
  53. package/crates/team-agent/src/messaging/results.rs +0 -58
  54. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
  55. package/crates/team-agent/src/provider/adapter.rs +8 -59
  56. package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
  57. package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
  58. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
  59. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
  60. package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
  61. package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
  62. package/crates/team-agent/src/provider/command_helpers.rs +45 -0
  63. package/crates/team-agent/src/provider/mod.rs +1 -0
  64. package/crates/team-agent/src/provider/model_catalog.rs +21 -28
  65. package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
  66. package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
  67. package/crates/team-agent/src/state/repository.rs +1 -5
  68. package/package.json +4 -4
  69. package/templates/team_state.md +1 -1
  70. package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
  71. package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
  72. package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
  73. package/crates/team-agent/src/messaging/peers.rs +0 -34
@@ -1090,52 +1090,6 @@ fn collect_rejects_message_scoped_result_without_matching_recipient() {
1090
1090
  );
1091
1091
  }
1092
1092
 
1093
- #[test]
1094
- fn allow_peer_talk_records_bidirectional_allowlist_and_event() {
1095
- let ws = tmp_ws("allowpeer");
1096
- let out = allow_peer_talk(&ws, "alice", "bob").unwrap();
1097
- assert_eq!(out.get("ok").and_then(|v| v.as_bool()), Some(true));
1098
- assert_eq!(out.get("a").and_then(|v| v.as_str()), Some("alice"));
1099
- assert_eq!(out.get("b").and_then(|v| v.as_str()), Some("bob"));
1100
- assert_eq!(
1101
- out.get("status").and_then(|v| v.as_str()),
1102
- Some("compat_noop")
1103
- );
1104
- assert_eq!(
1105
- out.get("reason").and_then(|v| v.as_str()),
1106
- Some("team_scoped_peer_messages_enabled")
1107
- );
1108
-
1109
- let store = store_for(&ws);
1110
- let conn = seed_conn(&store);
1111
- let rows = conn
1112
- .prepare("select a, b from peer_allowlist order by a, b")
1113
- .unwrap()
1114
- .query_map([], |row| {
1115
- Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
1116
- })
1117
- .unwrap()
1118
- .collect::<Result<Vec<_>, _>>()
1119
- .unwrap();
1120
- assert_eq!(
1121
- rows,
1122
- vec![
1123
- ("alice".to_string(), "bob".to_string()),
1124
- ("bob".to_string(), "alice".to_string()),
1125
- ]
1126
- );
1127
-
1128
- let events = EventLog::new(&ws).tail(10).unwrap();
1129
- let event = events
1130
- .iter()
1131
- .find(|event| {
1132
- event.get("event").and_then(|v| v.as_str()) == Some("communication.peer_allowed")
1133
- })
1134
- .expect("communication.peer_allowed event");
1135
- assert_eq!(event.get("a").and_then(|v| v.as_str()), Some("alice"));
1136
- assert_eq!(event.get("b").and_then(|v| v.as_str()), Some("bob"));
1137
- }
1138
-
1139
1093
  // ════════════════════════════════════════════════════════════════════════
1140
1094
  // GROUP R — run_comms_selftest: §84 / MUST-NOT-13 zero-provider-SDK gate.
1141
1095
  // diagnose/comms.py:21-47. The whole point: assert zero provider client calls.
@@ -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
- }
@@ -3,7 +3,7 @@
3
3
  //! contract:
4
4
  //! provides:
5
5
  //! - name: cursor_agent_base_command
6
- //! what: 只组已实测文档化 flag;mcp_config 不入 argv,由 launch overlay 写盘
6
+ //! what: 只组已实测文档化 flag;MCP 由 launch overlay 写盘
7
7
  //! boundary:
8
8
  //! - 不调未文档化 flag(--system-prompt / --allowed-tools)
9
9
  //! - 不写 grok 那种 cwd 独占闸
@@ -22,60 +22,23 @@
22
22
  //! 随时可能消失。可以在注释里记录,代码里不许调。
23
23
  //! `--allowed-tools` 正反行为未验证,不写「支持工具白名单」。
24
24
 
25
- use crate::model::enums::AuthMode;
26
- use crate::provider::adapter::BasicProviderAdapter;
27
- use crate::provider::{McpConfig, ProviderError};
28
-
29
- pub(crate) fn cursor_agent_launch_command(
30
- adapter: &BasicProviderAdapter,
31
- auth_mode: AuthMode,
32
- mcp_config: Option<&McpConfig>,
33
- system_prompt: Option<&str>,
34
- model: Option<&str>,
35
- dangerously_skip_permissions: bool,
36
- ) -> Result<Vec<String>, ProviderError> {
37
- cursor_agent_base_command(
38
- adapter,
39
- auth_mode,
40
- mcp_config,
41
- system_prompt,
42
- model,
43
- dangerously_skip_permissions,
44
- false,
45
- None,
46
- )
47
- }
25
+ use crate::provider::ProviderError;
26
+ use crate::provider::command_helpers::{append_opt_pair, append_pair};
48
27
 
49
28
  pub(crate) fn cursor_agent_base_command(
50
- adapter: &BasicProviderAdapter,
51
- auth_mode: AuthMode,
52
- mcp_config: Option<&McpConfig>,
53
- system_prompt: Option<&str>,
54
29
  model: Option<&str>,
55
30
  dangerously_skip_permissions: bool,
56
- managed_mcp_config: bool,
57
- effort: Option<crate::model::enums::ProviderEffort>,
58
31
  ) -> Result<Vec<String>, ProviderError> {
59
32
  let mut argv = vec!["agent".to_string()];
60
33
  if dangerously_skip_permissions {
61
34
  // Explicit bypass also opts into Cursor's trusted, unsandboxed launch.
62
35
  argv.push("--trust".to_string());
63
- argv.push("--sandbox".to_string());
64
- argv.push("disabled".to_string());
36
+ append_pair(&mut argv, "--sandbox", "disabled");
65
37
  argv.push("--force".to_string());
66
38
  }
67
- if let Some(model) = model {
68
- argv.push("--model".to_string());
69
- argv.push(model.to_string());
70
- }
71
- // Cursor `--effort` flag 不存在;框架在调用方丢掉 effort。绝不发明 flag。
72
- let _ = effort;
73
- // system_prompt 不入 argv(help 无 --rules / --append-system-prompt)。
74
- // launch 写 `<workspace>/.cursor/rules/team-agent-role-<agent_id>.mdc`。
75
- // mcp_config 也不入 argv(无 --mcp-config)。launch 写 `.cursor/mcp.json`
76
- // 并 `agent mcp enable team_orchestrator`。这里收下以免静默丢弃。
77
- let _ = (adapter, auth_mode, mcp_config, managed_mcp_config, system_prompt);
78
- argv.push("--workspace".to_string());
79
- argv.push("{workspace}".to_string());
39
+ append_opt_pair(&mut argv, "--model", model);
40
+ // Cursor has no `--effort`, system-prompt, or MCP config flag.
41
+ // Launch materializes role rules and the `.cursor/mcp.json` overlay.
42
+ append_pair(&mut argv, "--workspace", "{workspace}");
80
43
  Ok(argv)
81
44
  }
@@ -30,62 +30,38 @@
30
30
  //! → `native_mcp_config: false`; MCP reaches the worker via launch-path
31
31
  //! `<cwd>/.grok/config.toml` (`apply_grok_mcp_overlay`).
32
32
 
33
- use crate::model::enums::AuthMode;
34
- use crate::provider::adapter::{next_session_token, BasicProviderAdapter};
35
- use crate::provider::{McpConfig, ProviderError};
33
+ use crate::provider::adapter::next_session_token;
34
+ use crate::provider::ProviderError;
35
+ use crate::provider::command_helpers::{append_opt_pair, append_pair};
36
36
 
37
37
  pub(crate) fn grok_launch_command(
38
- adapter: &BasicProviderAdapter,
39
- auth_mode: AuthMode,
40
- mcp_config: Option<&McpConfig>,
41
38
  system_prompt: Option<&str>,
42
39
  model: Option<&str>,
43
40
  dangerously_skip_permissions: bool,
44
41
  ) -> Result<Vec<String>, ProviderError> {
45
- let mut argv = grok_base_command(
46
- adapter,
47
- auth_mode,
48
- mcp_config,
49
- system_prompt,
50
- model,
51
- dangerously_skip_permissions,
52
- false,
53
- None,
54
- )?;
55
- argv.push("--session-id".to_string());
56
- argv.push(next_session_token());
42
+ let mut argv = grok_base_command(system_prompt, model, dangerously_skip_permissions, None)?;
43
+ append_pair(&mut argv, "--session-id", next_session_token());
57
44
  Ok(argv)
58
45
  }
59
46
 
60
47
  pub(crate) fn grok_base_command(
61
- adapter: &BasicProviderAdapter,
62
- auth_mode: AuthMode,
63
- mcp_config: Option<&McpConfig>,
64
48
  system_prompt: Option<&str>,
65
49
  model: Option<&str>,
66
50
  dangerously_skip_permissions: bool,
67
- managed_mcp_config: bool,
68
51
  effort: Option<crate::model::enums::ProviderEffort>,
69
52
  ) -> Result<Vec<String>, ProviderError> {
70
53
  let mut argv = vec!["grok".to_string()];
71
54
  if dangerously_skip_permissions {
72
55
  argv.push("--always-approve".to_string());
73
56
  }
74
- if let Some(model) = model.map(str::trim).filter(|value| !value.is_empty()) {
75
- argv.push("--model".to_string());
76
- argv.push(model.to_string());
77
- }
57
+ append_opt_pair(&mut argv, "--model", model.map(str::trim).filter(|value| !value.is_empty()));
78
58
  if let Some(effort) = effort {
79
59
  // Grok accepts `--effort` as alias for `--reasoning-effort`.
80
60
  argv.push("--effort".to_string());
81
61
  argv.push(effort.as_str().to_string());
82
62
  }
83
- if let Some(prompt) = system_prompt {
84
- argv.push("--rules".to_string());
85
- argv.push(prompt.to_string());
86
- }
63
+ append_opt_pair(&mut argv, "--rules", system_prompt);
87
64
  // Grok CLI has no `--mcp-config` flag — the claude inline-MCP block is
88
65
  // intentionally absent. Launch writes `<cwd>/.grok/config.toml`.
89
- let _ = (adapter, auth_mode, mcp_config, managed_mcp_config);
90
66
  Ok(argv)
91
67
  }
@@ -14,6 +14,7 @@ use std::path::Path;
14
14
 
15
15
  use crate::model::enums::ProviderEffort;
16
16
  use crate::provider::ProviderError;
17
+ use crate::provider::command_helpers::{append_opt_pair, append_pair};
17
18
 
18
19
  #[derive(Debug, Clone, Copy)]
19
20
  pub(crate) enum PiSessionSelector<'a> {
@@ -60,38 +61,27 @@ pub(crate) fn build_pi_command_argv(
60
61
  "-e".to_string(),
61
62
  request.extension.to_string_lossy().into_owned(),
62
63
  ];
63
- if let Some(model) = request.model {
64
- argv.push("--model".to_string());
65
- argv.push(model.to_string());
66
- }
64
+ append_opt_pair(&mut argv, "--model", request.model);
67
65
  if let Some(effort) = request.effort {
68
66
  argv.push("--thinking".to_string());
69
67
  argv.push(effort.as_str().to_string());
70
68
  }
71
- argv.extend([
72
- "--append-system-prompt".to_string(),
73
- request.system_prompt.to_string(),
74
- ]);
69
+ append_pair(&mut argv, "--append-system-prompt", request.system_prompt);
75
70
  if let Some(session_dir) = request.session_dir {
76
- argv.extend([
77
- "--session-dir".to_string(),
78
- session_dir.to_string_lossy().into_owned(),
79
- ]);
71
+ append_pair(&mut argv, "--session-dir", session_dir.to_string_lossy().into_owned());
80
72
  }
81
73
  // The shared lifecycle materializer always revalidates resume root/path/header.
82
74
  // Keep the pure argv builder usable before a fixture root is materialized, while
83
75
  // refusing a missing exact file once the recorded path's parent exists.
84
76
  match request.session {
85
77
  PiSessionSelector::Fresh { session_id } if !session_id.trim().is_empty() => {
86
- argv.push("--session-id".to_string());
87
- argv.push(session_id.to_string());
78
+ append_pair(&mut argv, "--session-id", session_id);
88
79
  }
89
80
  PiSessionSelector::Resume { path }
90
81
  if !path.as_os_str().is_empty()
91
82
  && (!path.parent().is_some_and(Path::is_dir) || path.is_file()) =>
92
83
  {
93
- argv.push("--session".to_string());
94
- argv.push(path.to_string_lossy().into_owned());
84
+ append_pair(&mut argv, "--session", path.to_string_lossy().into_owned());
95
85
  }
96
86
  PiSessionSelector::Resume { path } if !path.as_os_str().is_empty() => {
97
87
  return Err(ProviderError::ResumeUnavailable(format!(
@@ -105,7 +95,6 @@ pub(crate) fn build_pi_command_argv(
105
95
  ));
106
96
  }
107
97
  }
108
- argv.push("--name".to_string());
109
- argv.push(request.agent_id.to_string());
98
+ append_pair(&mut argv, "--name", request.agent_id);
110
99
  Ok(argv)
111
100
  }
@@ -0,0 +1,45 @@
1
+ //! Pure append operations. Callers own ordering, filtering, escaping and env-unset policy.
2
+ use std::collections::BTreeMap;
3
+
4
+ pub(crate) fn append_pair(argv: &mut Vec<String>, flag: &str, value: impl Into<String>) {
5
+ argv.push(flag.to_string());
6
+ argv.push(value.into());
7
+ }
8
+
9
+ pub(crate) fn append_opt_pair(argv: &mut Vec<String>, flag: &str, value: Option<&str>) {
10
+ if let Some(value) = value {
11
+ append_pair(argv, flag, value);
12
+ }
13
+ }
14
+
15
+ pub(crate) fn append_env_pair(env: &mut BTreeMap<String, String>, key: &str, value: Option<&str>) {
16
+ if let Some(value) = value {
17
+ env.insert(key.to_string(), value.to_string());
18
+ }
19
+ }
20
+
21
+ #[cfg(test)]
22
+ mod tests {
23
+ use super::*;
24
+
25
+ #[test]
26
+ fn append_preserves_order_and_raw_values() {
27
+ let mut argv = vec!["cli".to_string()];
28
+ append_opt_pair(&mut argv, "--absent", None);
29
+ append_opt_pair(&mut argv, "--empty", Some(""));
30
+ append_pair(&mut argv, "--raw", " \"'\\\n ".to_string());
31
+ append_pair(&mut argv, "--repeat", "first");
32
+ append_pair(&mut argv, "--repeat", "last");
33
+ assert_eq!(argv, ["cli", "--empty", "", "--raw", " \"'\\\n ", "--repeat", "first", "--repeat", "last"]);
34
+ }
35
+
36
+ #[test]
37
+ fn env_append_preserves_absence_empty_values_and_last_write() {
38
+ let mut env = BTreeMap::from([("Z".to_string(), "old".to_string())]);
39
+ append_env_pair(&mut env, "Z", None);
40
+ assert_eq!(env["Z"], "old");
41
+ append_env_pair(&mut env, "A", Some(""));
42
+ append_env_pair(&mut env, "Z", Some(" \"'\\\n "));
43
+ assert_eq!(env.into_iter().collect::<Vec<_>>(), vec![("A".to_string(), "".to_string()), ("Z".to_string(), " \"'\\\n ".to_string())]);
44
+ }
45
+ }