@team-agent/installer 0.5.66 → 0.5.68

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 (164) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -0
  4. package/crates/team-agent/src/cli/adapters.rs +5 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +82 -0
  6. package/crates/team-agent/src/cli/emit.rs +3 -3
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +21 -7
  9. package/crates/team-agent/src/cli/leaders.rs +125 -1
  10. package/crates/team-agent/src/cli/mod.rs +20 -0
  11. package/crates/team-agent/src/cli/send/presentation.rs +7 -1
  12. package/crates/team-agent/src/cli/spec.rs +2 -0
  13. package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
  14. package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
  15. package/crates/team-agent/src/cli/tests/base.rs +26 -3
  16. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
  17. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  18. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  19. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  20. package/crates/team-agent/src/compiler.rs +59 -0
  21. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  22. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  23. package/crates/team-agent/src/coordinator/health.rs +173 -0
  24. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  25. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  26. package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
  27. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  29. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  30. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  31. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  32. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  33. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  34. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  35. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  36. package/crates/team-agent/src/coordinator/types.rs +49 -0
  37. package/crates/team-agent/src/db/message_store.rs +39 -5
  38. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  39. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  40. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  41. package/crates/team-agent/src/leader/registry.rs +65 -0
  42. package/crates/team-agent/src/leader/start.rs +920 -63
  43. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  44. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  45. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -12
  46. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +62 -0
  47. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +59 -0
  48. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  49. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  50. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
  51. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -456
  52. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  53. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  54. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  55. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  56. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  57. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
  58. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  59. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  60. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  61. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
  62. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  63. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  64. package/crates/team-agent/src/lifecycle/launch/spawn.rs +62 -2
  65. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +139 -0
  66. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
  67. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  68. package/crates/team-agent/src/lifecycle/launch.rs +99 -29
  69. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  70. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  71. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  72. package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
  73. package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
  74. package/crates/team-agent/src/lifecycle/restart/agent.rs +124 -0
  75. package/crates/team-agent/src/lifecycle/restart/common.rs +369 -11
  76. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  77. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  78. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
  79. package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
  80. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  81. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  82. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  83. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
  84. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  85. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
  86. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
  88. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
  89. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
  90. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  91. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
  92. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  93. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  94. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
  95. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  96. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  97. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
  98. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
  99. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
  100. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
  103. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +2 -0
  104. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
  105. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
  106. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  107. package/crates/team-agent/src/lifecycle/tests.rs +23 -13
  108. package/crates/team-agent/src/lifecycle/types.rs +61 -0
  109. package/crates/team-agent/src/lifecycle/worker_command_context.rs +85 -11
  110. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  111. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  112. package/crates/team-agent/src/messaging/delivery.rs +405 -40
  113. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  114. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  115. package/crates/team-agent/src/messaging/results.rs +4 -0
  116. package/crates/team-agent/src/messaging/send.rs +34 -0
  117. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  118. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  119. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  121. package/crates/team-agent/src/messaging/types.rs +4 -1
  122. package/crates/team-agent/src/model/enums.rs +14 -5
  123. package/crates/team-agent/src/model/permissions.rs +3 -0
  124. package/crates/team-agent/src/os_probe.rs +73 -2
  125. package/crates/team-agent/src/provider/adapter.rs +231 -4
  126. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
  127. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  128. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  129. package/crates/team-agent/src/provider/bypass_flags.rs +46 -2
  130. package/crates/team-agent/src/provider/classify.rs +4 -2
  131. package/crates/team-agent/src/provider/faults.rs +2 -1
  132. package/crates/team-agent/src/provider/mod.rs +11 -0
  133. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  134. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  135. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  136. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  137. package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
  138. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  139. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  140. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  141. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  142. package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
  143. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  144. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  145. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  146. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  147. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  148. package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
  149. package/crates/team-agent/src/provider/types.rs +4 -2
  150. package/crates/team-agent/src/provider/wire.rs +23 -1
  151. package/crates/team-agent/src/state/persist.rs +301 -8
  152. package/crates/team-agent/src/state/repository.rs +5 -0
  153. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  154. package/crates/team-agent/src/tmux_backend.rs +1064 -75
  155. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  156. package/crates/team-agent/src/transport.rs +248 -1
  157. package/npm/install.mjs +129 -73
  158. package/package.json +4 -4
  159. package/skills/team-agent/SKILL.md +33 -238
  160. package/skills/team-agent/command-coverage.json +31 -0
  161. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  162. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
  163. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  164. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
@@ -0,0 +1,332 @@
1
+ //! ---
2
+ //! purpose: 把 team_orchestrator 身份写进 cursor 实际会读的 mcp.json
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: apply_cursor_mcp_overlay
6
+ //! what: 写 <workspace>/.cursor/mcp.json,env 必须带 TEAM_AGENT_ID
7
+ //! - name: cursor_mcp_enable_argv
8
+ //! what: 组 `agent mcp enable team_orchestrator`(不认 --workspace)
9
+ //! - name: enable_cursor_workspace_mcp
10
+ //! what: 在物理工作目录跑 enable;测试隔离下跳过以免写 ~/.cursor
11
+ //! - name: physical_workspace_path
12
+ //! what: pwd -P 等价路径;mcp enable 按 getcwd 分片
13
+ //! - name: refuse_second_cursor_occupant
14
+ //! what: 同一物理 workspace 第二 CursorAgent 拒绝(mcp.json last-writer)
15
+ //! boundary:
16
+ //! - 同 workspace 第二 CursorAgent 拒绝;U-07 过前不装「已支持多席」
17
+ //! - 不把身份从 json env 删掉(cursor 不继承父进程 TEAM_AGENT_*)
18
+ //! - 不写 ~/.cursor/mcp.json 全局文件
19
+ //! - 不调 --approve-mcps(未验证能代替 enable)
20
+ //! maturity: wired
21
+ //! ---
22
+
23
+ use std::collections::BTreeMap;
24
+ use std::path::{Path, PathBuf};
25
+ use std::process::Command;
26
+
27
+ use crate::lifecycle::LifecycleError;
28
+ use crate::model::yaml::Value as YamlValue;
29
+ use crate::provider::wire::command_name;
30
+ use crate::provider::Provider;
31
+
32
+ /// Keys that must appear in mcp.json env. Cursor strips parent env down to
33
+ /// HOME/PATH/TERM/… — TEAM_AGENT_ID only survives if it is in this table.
34
+ const REQUIRED_IDENTITY_KEYS: &[&str] = &[
35
+ "TEAM_AGENT_WORKSPACE",
36
+ "TEAM_AGENT_ID",
37
+ "TEAM_AGENT_OWNER_TEAM_ID",
38
+ "TEAM_AGENT_AUTH_MODE",
39
+ ];
40
+
41
+ /// ---
42
+ /// purpose: 给出 workspace 的物理路径
43
+ /// returns: 能 canonicalize 就用它,否则原样返回
44
+ /// ---
45
+ pub fn physical_workspace_path(workspace: &Path) -> PathBuf {
46
+ std::fs::canonicalize(workspace).unwrap_or_else(|_| workspace.to_path_buf())
47
+ }
48
+
49
+ fn status_is_live(status: &str) -> bool {
50
+ !matches!(
51
+ status,
52
+ "stopped" | "stopping" | "removed" | "spawn_failed" | "failed"
53
+ )
54
+ }
55
+
56
+ fn agent_is_cursor(provider: Option<&str>) -> bool {
57
+ matches!(
58
+ provider.and_then(crate::lifecycle::profile_launch::parse_provider),
59
+ Some(Provider::CursorAgent)
60
+ )
61
+ }
62
+
63
+ /// ---
64
+ /// purpose: 同一物理 workspace 拒绝第二 CursorAgent
65
+ /// params:
66
+ /// workspace: 物理工作目录
67
+ /// incoming_id: 正要起的席位 id
68
+ /// spec: 可选 yaml spec,用来在 state 尚未写入时看见第二席
69
+ /// returns: 已有其它 CursorAgent 则 RequirementUnmet,文案含 TEAM_AGENT_ID 与 mcp.json
70
+ /// contract:
71
+ /// provides:
72
+ /// - name: refuse_second_cursor_occupant
73
+ /// what: mcp.json last-writer 闸;U-07 过前不装多席
74
+ /// boundary:
75
+ /// - 不改 grok 独占实现
76
+ /// ---
77
+ pub fn refuse_second_cursor_occupant(
78
+ workspace: &Path,
79
+ incoming_id: &str,
80
+ spec: Option<&YamlValue>,
81
+ ) -> Result<(), LifecycleError> {
82
+ let mut others = Vec::new();
83
+ if let Some(spec) = spec {
84
+ if let Some(agents) = spec.get("agents").and_then(YamlValue::as_list) {
85
+ for agent in agents {
86
+ let Some(id) = agent.get("id").and_then(YamlValue::as_str) else {
87
+ continue;
88
+ };
89
+ if id == incoming_id {
90
+ continue;
91
+ }
92
+ if agent_is_paused_yaml(agent) {
93
+ continue;
94
+ }
95
+ if agent_is_cursor(agent.get("provider").and_then(YamlValue::as_str)) {
96
+ others.push(id.to_string());
97
+ }
98
+ }
99
+ }
100
+ }
101
+ if let Ok(state) = crate::state::persist::load_runtime_state(workspace) {
102
+ if let Some(agents) = state.get("agents").and_then(serde_json::Value::as_object) {
103
+ for (id, agent) in agents {
104
+ if id == incoming_id {
105
+ continue;
106
+ }
107
+ let provider = agent.get("provider").and_then(serde_json::Value::as_str);
108
+ if !agent_is_cursor(provider) {
109
+ continue;
110
+ }
111
+ let status = agent
112
+ .get("status")
113
+ .and_then(serde_json::Value::as_str)
114
+ .unwrap_or("running");
115
+ if status_is_live(status) {
116
+ others.push(id.clone());
117
+ }
118
+ }
119
+ }
120
+ }
121
+ others.sort();
122
+ others.dedup();
123
+ if others.is_empty() {
124
+ return Ok(());
125
+ }
126
+ Err(LifecycleError::RequirementUnmet(format!(
127
+ "error: cursor_agent seat already occupies this workspace\n\
128
+ incoming: {incoming_id}\n\
129
+ occupants: {}\n\
130
+ reason: <workspace>/.cursor/mcp.json is directory-scoped; a second seat overwrites TEAM_AGENT_ID (last-writer)\n\
131
+ workspace: {}\n\
132
+ action: do not add another CursorAgent in this workspace until per-seat MCP identity is isolated",
133
+ others.join(", "),
134
+ workspace.display(),
135
+ )))
136
+ }
137
+
138
+ fn agent_is_paused_yaml(agent: &YamlValue) -> bool {
139
+ matches!(agent.get("paused"), Some(YamlValue::Bool(true)))
140
+ }
141
+
142
+ /// ---
143
+ /// purpose: 写 workspace 下 cursor 会读的 mcp.json,env 里必须带席位身份键
144
+ /// params:
145
+ /// mcp_config: 已解析的 MCP 配置,须含 team_orchestrator 与 command
146
+ /// returns: 写出的文件路径
147
+ /// errors: 缺条目或缺 command 时返回 StatePersist,写盘失败也返回 StatePersist
148
+ /// ---
149
+ pub fn apply_cursor_mcp_overlay(
150
+ workspace: &Path,
151
+ mcp_config: &crate::provider::McpConfig,
152
+ ) -> Result<PathBuf, LifecycleError> {
153
+ let server = mcp_config
154
+ .raw
155
+ .get("team_orchestrator")
156
+ .or_else(|| mcp_config.raw.get("team-agent"))
157
+ .ok_or_else(|| {
158
+ LifecycleError::StatePersist(
159
+ "cursor MCP overlay requires team_orchestrator in resolved mcp config".to_string(),
160
+ )
161
+ })?;
162
+ let command = server
163
+ .get("command")
164
+ .and_then(serde_json::Value::as_str)
165
+ .filter(|value| !value.is_empty())
166
+ .ok_or_else(|| {
167
+ LifecycleError::StatePersist("cursor MCP overlay missing command".to_string())
168
+ })?;
169
+ let args = server
170
+ .get("args")
171
+ .and_then(serde_json::Value::as_array)
172
+ .map(|items| {
173
+ items
174
+ .iter()
175
+ .filter_map(serde_json::Value::as_str)
176
+ .map(str::to_string)
177
+ .collect::<Vec<_>>()
178
+ })
179
+ .unwrap_or_default();
180
+ let env = server
181
+ .get("env")
182
+ .and_then(serde_json::Value::as_object)
183
+ .map(|map| {
184
+ map.iter()
185
+ .filter_map(|(key, value)| {
186
+ value.as_str().map(|text| (key.clone(), text.to_string()))
187
+ })
188
+ .collect::<BTreeMap<_, _>>()
189
+ })
190
+ .unwrap_or_default();
191
+ for key in REQUIRED_IDENTITY_KEYS {
192
+ match env.get(*key).map(String::as_str) {
193
+ Some(value) if !value.trim().is_empty() && !value.contains('{') => {}
194
+ _ => {
195
+ return Err(LifecycleError::StatePersist(format!(
196
+ "cursor MCP overlay missing resolved {key} in json env \
197
+ (cursor does not inherit parent TEAM_AGENT_* into the MCP child)"
198
+ )));
199
+ }
200
+ }
201
+ }
202
+
203
+ let dir = workspace.join(".cursor");
204
+ std::fs::create_dir_all(&dir)
205
+ .map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", dir.display())))?;
206
+ let path = dir.join("mcp.json");
207
+ let mut root = read_existing_mcp_json(&path);
208
+ let servers = root
209
+ .as_object_mut()
210
+ .ok_or_else(|| {
211
+ LifecycleError::StatePersist(format!(
212
+ "{}: cursor mcp.json root must be an object",
213
+ path.display()
214
+ ))
215
+ })?
216
+ .entry("mcpServers")
217
+ .or_insert_with(|| serde_json::json!({}));
218
+ let servers = servers.as_object_mut().ok_or_else(|| {
219
+ LifecycleError::StatePersist(format!("{}: mcpServers must be an object", path.display()))
220
+ })?;
221
+ // 0.5.67 overlay 曾误用 team-agent 作 inbound key。cursor 按 server 名
222
+ // 给工具加命名空间,旧名会变成另一套工具。写新表时摘掉旧名。
223
+ servers.remove("team-agent");
224
+ let mut env_json = serde_json::Map::new();
225
+ for (key, value) in &env {
226
+ env_json.insert(key.clone(), serde_json::Value::String(value.clone()));
227
+ }
228
+ servers.insert(
229
+ "team_orchestrator".to_string(),
230
+ serde_json::json!({
231
+ "command": command,
232
+ "args": args,
233
+ "env": env_json,
234
+ }),
235
+ );
236
+ let body = serde_json::to_string_pretty(&root)
237
+ .map_err(|e| LifecycleError::StatePersist(format!("serialize cursor mcp.json: {e}")))?;
238
+ let tmp = dir.join("mcp.json.tmp");
239
+ std::fs::write(&tmp, body.as_bytes())
240
+ .map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", tmp.display())))?;
241
+ std::fs::rename(&tmp, &path)
242
+ .map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", path.display())))?;
243
+ Ok(path)
244
+ }
245
+
246
+ /// ---
247
+ /// purpose: 组出启用 team_orchestrator 的 cursor 命令行
248
+ /// returns: 不带 workspace 参数的 argv,该命令按进程工作目录分片
249
+ /// ---
250
+ pub fn cursor_mcp_enable_argv() -> Vec<String> {
251
+ vec![
252
+ command_name(Provider::CursorAgent).to_string(),
253
+ "mcp".to_string(),
254
+ "enable".to_string(),
255
+ "team_orchestrator".to_string(),
256
+ ]
257
+ }
258
+
259
+ /// ---
260
+ /// purpose: 在物理工作目录下执行 cursor 的 MCP 启用命令
261
+ /// returns: 测试隔离环境或显式跳过标志下直接成功,避免写用户全局配置
262
+ /// errors: 命令跑不起来或退出码非零时返回 RequirementUnmet,错误里只记输出长度不记内容
263
+ /// ---
264
+ pub fn enable_cursor_workspace_mcp(workspace: &Path) -> Result<(), LifecycleError> {
265
+ if skip_cursor_mcp_enable() {
266
+ return Ok(());
267
+ }
268
+ let physical = physical_workspace_path(workspace);
269
+ let argv = cursor_mcp_enable_argv();
270
+ let output = Command::new(&argv[0])
271
+ .args(&argv[1..])
272
+ .current_dir(&physical)
273
+ .output()
274
+ .map_err(|e| {
275
+ LifecycleError::RequirementUnmet(format!(
276
+ "error: cannot run `{} mcp enable team_orchestrator`\n\
277
+ reason: {e}\n\
278
+ action: install cursor-agent on PATH (same binary as `agent`) and retry",
279
+ argv[0]
280
+ ))
281
+ })?;
282
+ if output.status.success() {
283
+ return Ok(());
284
+ }
285
+ let stderr = String::from_utf8_lossy(&output.stderr);
286
+ let stdout = String::from_utf8_lossy(&output.stdout);
287
+ Err(LifecycleError::RequirementUnmet(format!(
288
+ "error: `{} mcp enable team_orchestrator` failed (exit {:?})\n\
289
+ reason: without enable, cursor keeps MCP as not loaded (needs approval)\n\
290
+ cwd: {}\n\
291
+ stdout_len: {}\n\
292
+ stderr_len: {}\n\
293
+ action: from that directory run `{} mcp enable team_orchestrator`",
294
+ argv[0],
295
+ output.status.code(),
296
+ physical.display(),
297
+ stdout.len(),
298
+ stderr.len(),
299
+ argv[0]
300
+ )))
301
+ }
302
+
303
+ /// ---
304
+ /// purpose: 把 argv 里 workspace 参数的值换成物理路径
305
+ /// params:
306
+ /// argv: 就地改写;没有该参数时什么都不做
307
+ /// ---
308
+ pub fn apply_cursor_workspace_physical_path(argv: &mut [String], workspace: &Path) {
309
+ let physical = physical_workspace_path(workspace);
310
+ let Some(index) = argv.iter().position(|arg| arg == "--workspace") else {
311
+ return;
312
+ };
313
+ if let Some(value) = argv.get_mut(index.saturating_add(1)) {
314
+ *value = physical.to_string_lossy().into_owned();
315
+ }
316
+ }
317
+
318
+ fn skip_cursor_mcp_enable() -> bool {
319
+ cfg!(test)
320
+ || std::env::var_os("TEAM_AGENT_TEST_TMP").is_some()
321
+ || matches!(
322
+ std::env::var("TEAM_AGENT_SKIP_CURSOR_MCP_ENABLE").as_deref(),
323
+ Ok("1") | Ok("true") | Ok("yes")
324
+ )
325
+ }
326
+
327
+ fn read_existing_mcp_json(path: &Path) -> serde_json::Value {
328
+ let Ok(text) = std::fs::read_to_string(path) else {
329
+ return serde_json::json!({"mcpServers": {}});
330
+ };
331
+ serde_json::from_str(&text).unwrap_or_else(|_| serde_json::json!({"mcpServers": {}}))
332
+ }