@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
@@ -1,3 +1,22 @@
1
+ //! ---
2
+ //! purpose: 单个席位在 runtime state 里的行的构造,含 MCP 配置落盘与审批策略投影
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: running_agent_state
6
+ //! what: 构造 running 席位的 state 行,同时写出该席位的 MCP 配置文件
7
+ //! - name: effective_approval_policy
8
+ //! what: 把 bypass 审批结论投影成 state 里的 JSON
9
+ //! - name: effective_approval_policy_from_agent
10
+ //! what: 由席位行自己的 dangerously_skip_permissions 算审批策略
11
+ //! depends:
12
+ //! - crate::provider
13
+ //! - crate::provider::bypass_flags
14
+ //! - crate::lifecycle::launch::mcp_config
15
+ //! boundary:
16
+ //! - 只构造与落 MCP 配置,不 spawn 也不改窗口
17
+ //! - 审批结论只认席位自己的声明,不做全队继承
18
+ //! maturity: wired
19
+ //! ---
1
20
  use std::collections::{BTreeMap, BTreeSet};
2
21
  use std::path::{Path, PathBuf};
3
22
  use std::process::Command;
@@ -14,6 +33,15 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
14
33
 
15
34
  use super::*;
16
35
 
36
+ /// ---
37
+ /// purpose: 构造某席位 running 状态的 state 行
38
+ /// params:
39
+ /// spawn_cwd: 该席位进程的工作目录
40
+ /// started_agent: 本次 spawn 结果,用于取布局窗口
41
+ /// pane_id: 该席位的 pane,空串时不写入
42
+ /// returns: 含状态、provider、模型、auth_mode、窗口、MCP 配置路径、spawn 时间与 pane 信息的行
43
+ /// errors: 取 provider MCP 配置失败返回 Provider,写 MCP 配置文件失败返回 StatePersist
44
+ /// ---
17
45
  pub(super) fn running_agent_state(
18
46
  agent: &Value,
19
47
  id: &str,
@@ -141,6 +169,10 @@ pub(super) fn running_agent_state(
141
169
  Ok(serde_json::Value::Object(state))
142
170
  }
143
171
 
172
+ /// ---
173
+ /// purpose: 把 bypass 审批结论投影成落盘用的 JSON
174
+ /// returns: 含 enabled、来源、是否继承与显式确认位;来源为 runtime_config 且启用时视为已显式确认
175
+ /// ---
144
176
  pub(crate) fn effective_approval_policy(safety: &DangerousApproval) -> serde_json::Value {
145
177
  serde_json::json!({
146
178
  "enabled": safety.enabled,
@@ -153,6 +185,11 @@ pub(crate) fn effective_approval_policy(safety: &DangerousApproval) -> serde_jso
153
185
  })
154
186
  }
155
187
 
188
+ /// ---
189
+ /// purpose: 把审批策略写进席位 state 行
190
+ /// params:
191
+ /// agent_state: 就地写入 effective_approval_policy 字段
192
+ /// ---
156
193
  pub(crate) fn persist_effective_approval_policy(
157
194
  agent_state: &mut serde_json::Map<String, serde_json::Value>,
158
195
  safety: &DangerousApproval,
@@ -163,6 +200,10 @@ pub(crate) fn persist_effective_approval_policy(
163
200
  );
164
201
  }
165
202
 
203
+ /// ---
204
+ /// purpose: 给出审批来源的稳定 wire 字符串
205
+ /// returns: runtime_config、leader_process 或 disabled
206
+ /// ---
166
207
  pub(super) fn dangerous_approval_source_str(source: DangerousApprovalSource) -> &'static str {
167
208
  match source {
168
209
  DangerousApprovalSource::RuntimeConfig => "runtime_config",
@@ -171,6 +212,11 @@ pub(super) fn dangerous_approval_source_str(source: DangerousApprovalSource) ->
171
212
  }
172
213
  }
173
214
 
215
+ /// ---
216
+ /// purpose: 由 JSON 席位行自己的 bypass 声明算出审批策略
217
+ /// returns: 未声明时全为关闭;声明为真时记 runtime_config 来源并带该 provider 的 bypass argv
218
+ /// contract_id: lifecycle.agent_state.approval_policy_from_agent
219
+ /// ---
174
220
  /// 0.5.66 bypass 单源:从 agent 行 + provider 构造 approval policy JSON
175
221
  /// (restart/fork/add 落盘用;取代全队 `DangerousApproval` 派生)。
176
222
  pub(crate) fn effective_approval_policy_from_agent(
@@ -211,10 +257,17 @@ fn provider_wire_str(provider: Provider) -> &'static str {
211
257
  Provider::Codex => "codex",
212
258
  Provider::Copilot => "copilot",
213
259
  Provider::GeminiCli => "gemini_cli",
260
+ Provider::Grok => "grok",
261
+ Provider::CursorAgent => "cursor_agent",
214
262
  Provider::Fake => "fake",
215
263
  }
216
264
  }
217
265
 
266
+ /// ---
267
+ /// purpose: 由席位行自身算出审批策略并写回同一行
268
+ /// params:
269
+ /// agent_state: 既是来源也是写入目标
270
+ /// ---
218
271
  /// 0.5.66 bypass 单源:从 agent 行(state Map)+ provider 落盘 approval policy。
219
272
  /// 目标与来源是同一 agent 行。
220
273
  pub(crate) fn persist_effective_approval_policy_from_agent(
@@ -228,6 +281,12 @@ pub(crate) fn persist_effective_approval_policy_from_agent(
228
281
  );
229
282
  }
230
283
 
284
+ /// ---
285
+ /// purpose: 由 YAML spec 里的 agent 声明算出审批策略并写进 state 行
286
+ /// params:
287
+ /// agent: YAML 侧的 agent 节点,提供 bypass 声明
288
+ /// agent_state: 写入目标
289
+ /// ---
231
290
  /// 0.5.66 bypass 单源:yaml spec agent 版(launch/state_projection 用)。
232
291
  pub(crate) fn persist_effective_approval_policy_from_yaml_agent(
233
292
  agent_state: &mut serde_json::Map<String, serde_json::Value>,
@@ -1,3 +1,14 @@
1
+ //! ---
2
+ //! purpose: 零上下文重读角色文件再起一席,不继承源会话
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: clone_agent
6
+ //! what: 用源席最新角色文件 add-agent,保留源工具集
7
+ //! boundary:
8
+ //! - 不走 fork 的窗口内命令
9
+ //! - 不把 clone 与 fork 合成一条路径
10
+ //! maturity: wired
11
+ //! ---
1
12
  use crate::lifecycle::*;
2
13
  use crate::model::ids::AgentId;
3
14
  use crate::provider::SessionId;
@@ -5,6 +16,15 @@ use std::path::Path;
5
16
 
6
17
  use super::*;
7
18
 
19
+ /// ---
20
+ /// purpose: 以源席最新角色文件另起一席,不继承源会话
21
+ /// params:
22
+ /// source_agent_id: 源席位,只借它的角色文件与工具集
23
+ /// as_agent_id: 新席位名
24
+ /// label: 新席位的角色标签
25
+ /// returns: clone 报告
26
+ /// errors: 选不到 team 返回 TeamSelect,源席不存在返回 RequirementUnmet,角色物化与加席错误透传
27
+ /// ---
8
28
  pub fn clone_agent(
9
29
  workspace: &Path,
10
30
  source_agent_id: &AgentId,
@@ -40,16 +60,10 @@ pub fn clone_agent(
40
60
  as_agent_id,
41
61
  label,
42
62
  )?;
43
- let spec_path = selected
44
- .spec_path
45
- .as_ref()
46
- .ok_or_else(|| LifecycleError::TeamSelect("active team spec not found".to_string()))?;
47
- let spec = crate::model::yaml::loads(
48
- &std::fs::read_to_string(spec_path)
49
- .map_err(|error| LifecycleError::Compile(error.to_string()))?,
50
- )
51
- .map_err(|error| LifecycleError::Compile(error.to_string()))?;
52
- clamp_materialized_role_to_leader(materialized.path(), &spec)?;
63
+ // The materialized role carries the source role's DECLARED tools verbatim —
64
+ // a clone must preserve the source seat's full tools set (add-agent does
65
+ // the same; no leader-ceiling clamp). See role_source.rs for the removed
66
+ // clamp_materialized_role_to_leader.
53
67
  let added = add_agent(
54
68
  &selected.run_workspace,
55
69
  as_agent_id,
@@ -63,6 +77,15 @@ pub fn clone_agent(
63
77
  as_agent_id,
64
78
  source_session_id.as_ref(),
65
79
  );
80
+ if let Some((session_id, backing_path)) = verified.as_ref() {
81
+ persist_clone_session(
82
+ &selected.run_workspace,
83
+ selected.team_key.as_str(),
84
+ as_agent_id,
85
+ session_id,
86
+ backing_path,
87
+ )?;
88
+ }
66
89
  let (session_id, backing_path, backing_state) = match verified {
67
90
  Some((session_id, backing_path)) => (
68
91
  Some(session_id),
@@ -94,13 +117,80 @@ fn read_agent_session(
94
117
  .and_then(|agents| agents.get(agent_id.as_str()))?;
95
118
  let session = agent
96
119
  .get("session_id")
120
+ .or_else(|| agent.get("_pending_session_id"))
97
121
  .and_then(serde_json::Value::as_str)
98
122
  .filter(|value| !value.is_empty())?;
99
123
  let backing = agent
100
124
  .get("rollout_path")
101
125
  .and_then(serde_json::Value::as_str)
102
126
  .filter(|value| !value.is_empty())
103
- .map(std::path::PathBuf::from)?;
127
+ .map(std::path::PathBuf::from)
128
+ .or_else(|| find_claude_backing(agent, session))?;
104
129
  let distinct = source_session_id.is_none_or(|source| source.as_str() != session);
105
130
  (distinct && backing.is_file()).then(|| (SessionId::new(session), backing))
106
131
  }
132
+
133
+ fn find_claude_backing(agent: &serde_json::Value, session: &str) -> Option<std::path::PathBuf> {
134
+ let root = agent
135
+ .get("claude_projects_root")
136
+ .and_then(serde_json::Value::as_str)
137
+ .map(std::path::PathBuf::from)
138
+ .or_else(|| {
139
+ std::env::var_os("HOME")
140
+ .map(|home| std::path::PathBuf::from(home).join(".claude/projects"))
141
+ })?;
142
+ let target = format!("{session}.jsonl");
143
+ let mut pending = vec![root];
144
+ while let Some(dir) = pending.pop() {
145
+ let entries = std::fs::read_dir(&dir).ok()?;
146
+ for entry in entries.flatten() {
147
+ let path = entry.path();
148
+ if path.is_dir() {
149
+ pending.push(path);
150
+ } else if path.file_name().and_then(|name| name.to_str()) == Some(target.as_str()) {
151
+ return Some(path);
152
+ }
153
+ }
154
+ }
155
+ None
156
+ }
157
+
158
+ fn persist_clone_session(
159
+ workspace: &Path,
160
+ team_key: &str,
161
+ agent_id: &AgentId,
162
+ session_id: &SessionId,
163
+ backing_path: &Path,
164
+ ) -> Result<(), LifecycleError> {
165
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
166
+ workspace,
167
+ operation: "clone-session-bind",
168
+ team: Some(team_key),
169
+ agent_id: Some(agent_id),
170
+ })?;
171
+ let mut state = crate::state::projection::select_runtime_state(workspace, Some(team_key))
172
+ .map_err(|error| LifecycleError::TeamSelect(error.to_string()))?;
173
+ let Some(agent) = state
174
+ .get_mut("agents")
175
+ .and_then(serde_json::Value::as_object_mut)
176
+ .and_then(|agents| agents.get_mut(agent_id.as_str()))
177
+ .and_then(serde_json::Value::as_object_mut)
178
+ else {
179
+ return Ok(());
180
+ };
181
+ agent.insert(
182
+ "session_id".to_string(),
183
+ serde_json::json!(session_id.as_str()),
184
+ );
185
+ agent.insert(
186
+ "rollout_path".to_string(),
187
+ serde_json::json!(backing_path.to_string_lossy().to_string()),
188
+ );
189
+ agent.insert("captured_via".to_string(), serde_json::json!("clone-agent"));
190
+ crate::lifecycle::launch::save_launched_team_state_for_key(
191
+ workspace,
192
+ &state,
193
+ Some(team_key),
194
+ Some(agent_id.as_str()),
195
+ )
196
+ }
@@ -0,0 +1,229 @@
1
+ //! ---
2
+ //! purpose: cursor 候选 A——`agent create-chat` 解析首行 chatId 后按记录 PID 杀
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: apply_cursor_create_chat_plan
6
+ //! what: 无 pending 时把 CLI chatId 写入 expected_session_id 并 `--resume` 该 id
7
+ //! - name: create_cursor_chat_id
8
+ //! what: 跑 create-chat,读第一行 UUID,只杀本调用记录的 PID
9
+ //! requires:
10
+ //! - name: cursor-agent-cli
11
+ //! what: PATH 上的 `agent`;不存在或首行非法则返回 None(降级候选 B)
12
+ //! boundary:
13
+ //! - 不发明 --session-id;不 pkill/killall;不 wait 子进程自行退出
14
+ //! - 不读 chats 正文、不打印 proxy 值
15
+ //! - 已有 expected_session_id 或 argv 已含 --resume 则不动
16
+ //! - 2026-08-21 活体:create-chat 后 `--resume` 的存档在 pane stop 后被清;
17
+ //! TUI 自建会话能留盘。fresh 路径不调用本函数(走 scan 唯一 hex)。
18
+ //! maturity: wired
19
+ //! ---
20
+
21
+ use std::io::{BufRead, BufReader};
22
+ use std::path::Path;
23
+ use std::process::{Command, Stdio};
24
+ use std::sync::mpsc;
25
+ use std::thread;
26
+ use std::time::Duration;
27
+
28
+ use crate::provider::types::{CommandPlan, SessionId};
29
+
30
+ const FIRST_LINE_TIMEOUT: Duration = Duration::from_secs(5);
31
+ const TERM_GRACE: Duration = Duration::from_millis(200);
32
+
33
+ /// ---
34
+ /// purpose: 冷启/无 session 的 cursor 计划绑定 create-chat 得到的 chatId
35
+ /// params:
36
+ /// plan: build_command_plan 产出;workspace: 物理 cwd
37
+ /// returns: 成功则 expected_session_id 与 argv `--resume <id>` 齐;失败则计划不变
38
+ /// contract:
39
+ /// provides:
40
+ /// - name: apply_cursor_create_chat_plan
41
+ /// what: 只在无 pending 且 argv 无 --resume 时写入 CLI chatId
42
+ /// boundary:
43
+ /// - 库测试(cfg(test))不调真实 agent,避免订阅/挂起;具名测试打 create_cursor_chat_id
44
+ /// - 不在 launch/restart fresh 调用:活体证实 `--resume` 空聊存档停 pane 后消失
45
+ /// ---
46
+ pub(crate) fn apply_cursor_create_chat_plan(plan: &mut CommandPlan, workspace: &Path) {
47
+ if cfg!(test) {
48
+ return;
49
+ }
50
+ if plan.expected_session_id.is_some() {
51
+ return;
52
+ }
53
+ if plan.argv.iter().any(|flag| flag == "--resume") {
54
+ return;
55
+ }
56
+ let Some(chat_id) = create_cursor_chat_id(workspace) else {
57
+ return;
58
+ };
59
+ plan.expected_session_id = Some(SessionId::new(chat_id.clone()));
60
+ plan.argv.push("--resume".to_string());
61
+ plan.argv.push(chat_id);
62
+ }
63
+
64
+ /// ---
65
+ /// purpose: 跑 `agent create-chat`,解析首行 UUID,按记录 PID 终止挂起进程
66
+ /// params:
67
+ /// workspace: create-chat 的 cwd(物理 workspace)
68
+ /// returns: 合法 UUID;命令缺失/超时/非法首行则 None
69
+ /// contract:
70
+ /// provides:
71
+ /// - name: create_cursor_chat_id
72
+ /// what: 读一行就停;不 wait 退出;只杀本 Child.id()
73
+ /// boundary:
74
+ /// - 禁 pkill/killall;pid 0/1 不杀
75
+ /// ---
76
+ pub(crate) fn create_cursor_chat_id(workspace: &Path) -> Option<String> {
77
+ create_cursor_chat_id_with_command("agent", workspace)
78
+ }
79
+
80
+ fn create_cursor_chat_id_with_command(program: &str, workspace: &Path) -> Option<String> {
81
+ let mut child = Command::new(program)
82
+ .arg("create-chat")
83
+ .current_dir(workspace)
84
+ .stdin(Stdio::null())
85
+ .stdout(Stdio::piped())
86
+ .stderr(Stdio::null())
87
+ .spawn()
88
+ .ok()?;
89
+ let pid = child.id();
90
+ let stdout = child.stdout.take()?;
91
+ let (tx, rx) = mpsc::channel();
92
+ thread::spawn(move || {
93
+ let mut reader = BufReader::new(stdout);
94
+ let mut line = String::new();
95
+ let result = reader.read_line(&mut line).ok().and_then(|_| parse_chat_id_line(&line));
96
+ let _ = tx.send(result);
97
+ });
98
+ let parsed = match rx.recv_timeout(FIRST_LINE_TIMEOUT) {
99
+ Ok(value) => value,
100
+ Err(_) => None,
101
+ };
102
+ kill_recorded_pid(pid);
103
+ let _ = child.wait();
104
+ parsed
105
+ }
106
+
107
+ fn parse_chat_id_line(line: &str) -> Option<String> {
108
+ let token = line
109
+ .trim()
110
+ .split_whitespace()
111
+ .next()
112
+ .unwrap_or("")
113
+ .trim_matches(['"', '\'']);
114
+ is_chat_uuid(token).then(|| token.to_string())
115
+ }
116
+
117
+ fn is_chat_uuid(value: &str) -> bool {
118
+ let bytes = value.as_bytes();
119
+ if bytes.len() != 36 {
120
+ return false;
121
+ }
122
+ for (i, b) in bytes.iter().enumerate() {
123
+ if i == 8 || i == 13 || i == 18 || i == 23 {
124
+ if *b != b'-' {
125
+ return false;
126
+ }
127
+ } else if !b.is_ascii_hexdigit() {
128
+ return false;
129
+ }
130
+ }
131
+ true
132
+ }
133
+
134
+ fn kill_recorded_pid(pid: u32) {
135
+ if pid == 0 || pid == 1 {
136
+ return;
137
+ }
138
+ #[cfg(unix)]
139
+ {
140
+ let raw = pid as i32;
141
+ unsafe {
142
+ libc::kill(raw, libc::SIGTERM);
143
+ }
144
+ thread::sleep(TERM_GRACE);
145
+ unsafe {
146
+ libc::kill(raw, libc::SIGKILL);
147
+ }
148
+ }
149
+ #[cfg(not(unix))]
150
+ {
151
+ let _ = pid;
152
+ }
153
+ }
154
+
155
+ #[cfg(test)]
156
+ mod tests {
157
+ #![allow(clippy::unwrap_used)]
158
+
159
+ use super::*;
160
+ use std::os::unix::fs::PermissionsExt;
161
+ use std::sync::atomic::{AtomicU64, Ordering};
162
+
163
+ fn tmp_root(tag: &str) -> std::path::PathBuf {
164
+ static CTR: AtomicU64 = AtomicU64::new(0);
165
+ let dir = std::env::temp_dir().join(format!(
166
+ "ta-d117-create-chat-{}-{}-{}",
167
+ tag,
168
+ std::process::id(),
169
+ CTR.fetch_add(1, Ordering::Relaxed)
170
+ ));
171
+ let _ = std::fs::remove_dir_all(&dir);
172
+ std::fs::create_dir_all(&dir).unwrap();
173
+ dir
174
+ }
175
+
176
+ fn write_stub(dir: &Path, body: &str) -> std::path::PathBuf {
177
+ let path = dir.join("agent");
178
+ std::fs::write(&path, body).unwrap();
179
+ let mut perm = std::fs::metadata(&path).unwrap().permissions();
180
+ perm.set_mode(0o755);
181
+ std::fs::set_permissions(&path, perm).unwrap();
182
+ path
183
+ }
184
+
185
+ #[test]
186
+ fn create_chat_reads_first_line_and_kills_recorded_pid() {
187
+ let base = tmp_root("ok");
188
+ let ws = base.join("ws");
189
+ std::fs::create_dir_all(&ws).unwrap();
190
+ let bin = write_stub(
191
+ &base,
192
+ "#!/bin/sh\necho '681c8837-d564-44fa-98bf-4ff5a66592e2'\nkill -STOP $$\n",
193
+ );
194
+ let got = create_cursor_chat_id_with_command(bin.to_str().unwrap(), &ws);
195
+ assert_eq!(
196
+ got.as_deref(),
197
+ Some("681c8837-d564-44fa-98bf-4ff5a66592e2")
198
+ );
199
+ let _ = std::fs::remove_dir_all(&base);
200
+ }
201
+
202
+ #[test]
203
+ fn create_chat_rejects_non_uuid_first_line() {
204
+ let base = tmp_root("bad");
205
+ let ws = base.join("ws");
206
+ std::fs::create_dir_all(&ws).unwrap();
207
+ let bin = write_stub(&base, "#!/bin/sh\necho not-a-uuid\nkill -STOP $$\n");
208
+ let got = create_cursor_chat_id_with_command(bin.to_str().unwrap(), &ws);
209
+ assert!(got.is_none());
210
+ let _ = std::fs::remove_dir_all(&base);
211
+ }
212
+
213
+ #[test]
214
+ fn apply_is_noop_when_resume_already_present() {
215
+ let mut plan = CommandPlan {
216
+ argv: vec![
217
+ "agent".into(),
218
+ "--resume".into(),
219
+ "already-bound".into(),
220
+ ],
221
+ expected_session_id: None,
222
+ provider_projects_root: None,
223
+ managed_mcp_config: false,
224
+ };
225
+ apply_cursor_create_chat_plan(&mut plan, Path::new("/tmp"));
226
+ assert!(plan.expected_session_id.is_none());
227
+ assert_eq!(plan.argv.iter().filter(|a| *a == "--resume").count(), 1);
228
+ }
229
+ }