@team-agent/installer 0.5.66 → 0.5.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) 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 +156 -0
  6. package/crates/team-agent/src/cli/emit.rs +6 -5
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +22 -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 +3 -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 +121 -5
  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 +31 -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 +3 -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 +973 -75
  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 +398 -26
  46. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +80 -0
  47. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +60 -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 +548 -0
  51. package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +120 -0
  52. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +208 -456
  53. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  54. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  55. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  56. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  57. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  58. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +482 -1
  59. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  60. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +830 -0
  61. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  62. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  63. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +87 -3
  64. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  65. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  66. package/crates/team-agent/src/lifecycle/launch/spawn.rs +127 -25
  67. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +140 -0
  68. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
  69. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  70. package/crates/team-agent/src/lifecycle/launch.rs +107 -29
  71. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  72. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  73. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  74. package/crates/team-agent/src/lifecycle/profile_launch.rs +99 -0
  75. package/crates/team-agent/src/lifecycle/profile_smoke.rs +52 -1
  76. package/crates/team-agent/src/lifecycle/restart/agent.rs +214 -160
  77. package/crates/team-agent/src/lifecycle/restart/common.rs +513 -37
  78. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  79. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  80. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
  81. package/crates/team-agent/src/lifecycle/restart/remove.rs +233 -6
  82. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  83. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  84. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  85. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
  86. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +24 -3
  87. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  88. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
  89. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
  90. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
  91. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +464 -0
  92. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +247 -0
  93. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  94. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_g0.stdout.txt +222 -0
  95. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +787 -0
  96. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  97. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  98. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +485 -0
  99. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  100. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  101. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
  102. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
  103. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
  104. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
  105. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  106. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
  107. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
  108. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +97 -0
  109. package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +128 -0
  110. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +207 -0
  111. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +838 -0
  112. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +320 -0
  113. package/crates/team-agent/src/lifecycle/tests/pi_lifecycle_doctor_red.rs +98 -0
  114. package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +216 -0
  115. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -0
  116. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
  117. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
  118. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  119. package/crates/team-agent/src/lifecycle/tests.rs +30 -13
  120. package/crates/team-agent/src/lifecycle/types.rs +61 -0
  121. package/crates/team-agent/src/lifecycle/worker_command_context.rs +143 -12
  122. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  123. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  124. package/crates/team-agent/src/messaging/delivery.rs +430 -41
  125. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  126. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  127. package/crates/team-agent/src/messaging/results.rs +4 -0
  128. package/crates/team-agent/src/messaging/send.rs +34 -0
  129. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  130. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  131. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  132. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  133. package/crates/team-agent/src/messaging/types.rs +4 -1
  134. package/crates/team-agent/src/model/enums.rs +17 -6
  135. package/crates/team-agent/src/model/permissions.rs +3 -0
  136. package/crates/team-agent/src/os_probe.rs +73 -2
  137. package/crates/team-agent/src/provider/adapter.rs +259 -6
  138. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +89 -0
  139. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  140. package/crates/team-agent/src/provider/adapters/mod.rs +3 -0
  141. package/crates/team-agent/src/provider/adapters/pi.rs +152 -0
  142. package/crates/team-agent/src/provider/bypass_flags.rs +47 -2
  143. package/crates/team-agent/src/provider/classify.rs +4 -2
  144. package/crates/team-agent/src/provider/faults.rs +2 -1
  145. package/crates/team-agent/src/provider/mod.rs +11 -0
  146. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  147. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  148. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  149. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  150. package/crates/team-agent/src/provider/session/context_fork.rs +78 -3
  151. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  152. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  153. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  154. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  155. package/crates/team-agent/src/provider/session_scan/common.rs +205 -2
  156. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  157. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  158. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  159. package/crates/team-agent/src/provider/session_scan/pi.rs +172 -0
  160. package/crates/team-agent/src/provider/session_scan.rs +12 -0
  161. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  162. package/crates/team-agent/src/provider/tests/adapter.rs +384 -0
  163. package/crates/team-agent/src/provider/types.rs +4 -2
  164. package/crates/team-agent/src/provider/wire.rs +29 -1
  165. package/crates/team-agent/src/state/persist.rs +301 -8
  166. package/crates/team-agent/src/state/repository.rs +5 -0
  167. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  168. package/crates/team-agent/src/tmux_backend.rs +1116 -75
  169. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  170. package/crates/team-agent/src/transport.rs +248 -1
  171. package/npm/install.mjs +129 -73
  172. package/package.json +4 -4
  173. package/skills/team-agent/SKILL.md +32 -239
  174. package/skills/team-agent/command-coverage.json +31 -0
  175. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  176. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
  177. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  178. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
@@ -1,6 +1,32 @@
1
+ //! ---
2
+ //! purpose: 动态角色文档加一席,失败按快照回滚;force 变体先摘旧席再加
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: add_agent
6
+ //! what: 编译角色进 runtime spec 并起席,失败回滚 spec 与 state
7
+ //! - name: add_agent_force
8
+ //! what: 先快照并摘除同名旧席,再走正常加席,失败按快照恢复
9
+ //! - name: add_agent_with_transport_at_paths
10
+ //! what: 加席的实体实现,含 owner 门、重名拒绝、原子写 spec 与起席
11
+ //! depends:
12
+ //! - crate::lifecycle::lock
13
+ //! - crate::lifecycle::restart
14
+ //! - crate::lifecycle::restart::remove
15
+ //! - crate::compiler
16
+ //! - crate::state::selector
17
+ //! - crate::state::projection
18
+ //! - crate::state::persist
19
+ //! - crate::tmux_backend
20
+ //! boundary:
21
+ //! - 不拷贝外部角色文件进 team 目录,就地读取编译
22
+ //! - 起席一律走 restart 的 start_agent_at_paths,本文件不直接 spawn
23
+ //! - start path 按 exact pane receipt 回收已 spawn pane;本文件恢复 spec/state,并删除新 Pi 席位的 exact wrapper
24
+ //! maturity: wired
25
+ //! ---
1
26
  use std::collections::{BTreeMap, BTreeSet};
2
27
  use std::path::{Path, PathBuf};
3
28
  use std::process::Command;
29
+ use std::time::{SystemTime, UNIX_EPOCH};
4
30
 
5
31
  use crate::lifecycle::*;
6
32
  use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
@@ -14,6 +40,165 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
14
40
 
15
41
  use super::*;
16
42
 
43
+ struct AgentReservation {
44
+ workspace: PathBuf,
45
+ team_key: String,
46
+ agent_id: AgentId,
47
+ owner: String,
48
+ active: bool,
49
+ }
50
+
51
+ impl AgentReservation {
52
+ fn commit(mut self) {
53
+ self.active = false;
54
+ }
55
+ }
56
+
57
+ impl Drop for AgentReservation {
58
+ fn drop(&mut self) {
59
+ if !self.active {
60
+ return;
61
+ }
62
+ let request = LifecycleLockRequest {
63
+ workspace: &self.workspace,
64
+ operation: "agent-reservation-rollback",
65
+ team: Some(self.team_key.as_str()),
66
+ agent_id: Some(&self.agent_id),
67
+ };
68
+ let Ok(_lock) = acquire_agent_lifecycle_lock(request) else {
69
+ let _ = crate::event_log::EventLog::new(&self.workspace).write(
70
+ "lifecycle.clone_reservation_rollback_failed",
71
+ serde_json::json!({
72
+ "agent_id": self.agent_id.as_str(),
73
+ "reservation_owner": self.owner,
74
+ "reason": "lifecycle lock unavailable",
75
+ }),
76
+ );
77
+ return;
78
+ };
79
+ let result =
80
+ release_agent_reservation(&self.workspace, &self.team_key, &self.agent_id, &self.owner);
81
+ let _ = crate::event_log::EventLog::new(&self.workspace).write(
82
+ if result.is_ok() {
83
+ "lifecycle.clone_reservation_rolled_back"
84
+ } else {
85
+ "lifecycle.clone_reservation_rollback_failed"
86
+ },
87
+ serde_json::json!({
88
+ "agent_id": self.agent_id.as_str(),
89
+ "reservation_owner": self.owner,
90
+ "result": result.as_ref().err().map(ToString::to_string),
91
+ }),
92
+ );
93
+ }
94
+ }
95
+
96
+ fn reserve_agent_slot(
97
+ workspace: &Path,
98
+ team: Option<&str>,
99
+ agent_id: &AgentId,
100
+ ) -> Result<AgentReservation, LifecycleError> {
101
+ let state = crate::state::projection::select_runtime_state(workspace, team)
102
+ .map_err(|error| LifecycleError::TeamSelect(error.to_string()))?;
103
+ let team_key = team
104
+ .filter(|key| !key.is_empty())
105
+ .map(str::to_string)
106
+ .or_else(|| explicit_active_team_key(&state))
107
+ .unwrap_or_else(|| crate::state::projection::team_state_key(&state));
108
+ let owner = format!(
109
+ "{}-{}",
110
+ std::process::id(),
111
+ SystemTime::now()
112
+ .duration_since(UNIX_EPOCH)
113
+ .map_err(|error| LifecycleError::StatePersist(error.to_string()))?
114
+ .as_nanos()
115
+ );
116
+ let mut latest = crate::state::projection::select_runtime_state(workspace, Some(&team_key))
117
+ .map_err(|error| LifecycleError::TeamSelect(error.to_string()))?;
118
+ ensure_owner_allowed_for_state(&latest, Some(agent_id))?;
119
+ if runtime_agent_exists(&latest, agent_id) {
120
+ return Err(LifecycleError::RequirementUnmet(format!(
121
+ "agent id already exists: {agent_id}"
122
+ )));
123
+ }
124
+ latest
125
+ .as_object_mut()
126
+ .ok_or_else(|| LifecycleError::StatePersist("runtime state root is not an object".into()))?
127
+ .entry("agents")
128
+ .or_insert_with(|| serde_json::json!({}));
129
+ let agents = latest
130
+ .get_mut("agents")
131
+ .and_then(serde_json::Value::as_object_mut)
132
+ .ok_or_else(|| {
133
+ LifecycleError::StatePersist("runtime state agents is not an object".into())
134
+ })?;
135
+ agents.insert(
136
+ agent_id.as_str().to_string(),
137
+ serde_json::json!({
138
+ "agent_id": agent_id.as_str(),
139
+ "status": "reserved",
140
+ "reservation_owner": owner,
141
+ }),
142
+ );
143
+ save_launched_team_state_for_key(
144
+ workspace,
145
+ &latest,
146
+ Some(team_key.as_str()),
147
+ Some(agent_id.as_str()),
148
+ )?;
149
+ let _ = crate::event_log::EventLog::new(workspace).write(
150
+ "lifecycle.clone_reservation_acquired",
151
+ serde_json::json!({
152
+ "agent_id": agent_id.as_str(),
153
+ "reservation_owner": owner,
154
+ "team": team_key,
155
+ }),
156
+ );
157
+ Ok(AgentReservation {
158
+ workspace: workspace.to_path_buf(),
159
+ team_key,
160
+ agent_id: agent_id.clone(),
161
+ owner,
162
+ active: true,
163
+ })
164
+ }
165
+
166
+ fn release_agent_reservation(
167
+ workspace: &Path,
168
+ team_key: &str,
169
+ agent_id: &AgentId,
170
+ owner: &str,
171
+ ) -> Result<(), LifecycleError> {
172
+ let mut state = crate::state::projection::select_runtime_state(workspace, Some(team_key))
173
+ .map_err(|error| LifecycleError::TeamSelect(error.to_string()))?;
174
+ let matches = state
175
+ .get("agents")
176
+ .and_then(|agents| agents.get(agent_id.as_str()))
177
+ .and_then(|agent| agent.get("reservation_owner"))
178
+ .and_then(serde_json::Value::as_str)
179
+ == Some(owner);
180
+ if !matches {
181
+ return Ok(());
182
+ }
183
+ state
184
+ .get_mut("agents")
185
+ .and_then(serde_json::Value::as_object_mut)
186
+ .ok_or_else(|| {
187
+ LifecycleError::StatePersist("runtime state agents is not an object".into())
188
+ })?
189
+ .remove(agent_id.as_str());
190
+ save_launched_team_state_for_key(workspace, &state, Some(team_key), Some(agent_id.as_str()))
191
+ }
192
+
193
+ /// ---
194
+ /// purpose: 加一席的默认入口,解析活跃 team、拿生命周期锁、路由到该 team 实际使用的 tmux socket
195
+ /// params:
196
+ /// role_file_path: 外部角色文档路径,就地读取不拷贝
197
+ /// open_display: 是否为新席开显示
198
+ /// returns: 新席的环境与启动模式
199
+ /// errors: 选不到 team 返回 TeamSelect,角色文件缺失或编译失败返回 Compile,重名返回 RequirementUnmet
200
+ /// contract_id: lifecycle.add_agent.entry
201
+ /// ---
17
202
  /// `add_agent(workspace, agent_id, role_file_path, open_display, team)`
18
203
  /// (`lifecycle/operations.py:143`)。动态 role doc 编译进 spec + 起 worker;失败**字节级回滚**
19
204
  /// spec_yaml / workspace_state / **team_state.md** / role_file(Gap 15.11),每步发
@@ -32,18 +217,14 @@ pub fn add_agent(
32
217
  ) {
33
218
  Ok(selected) => selected,
34
219
  Err(_) if workspace.join("TEAM.md").exists() => {
35
- // **0.3.24 add-agent socket drift fix**: even on the TEAM.md fallback
36
- // path (no spec yet), prefer the state-aware resolver. It reads the
37
- // team's persisted `tmux_endpoint` (set at `team-agent launch` time)
38
- // and routes the new agent's spawn to the SAME tmux socket the live
39
- // team uses. Cold workspaces / first-agent paths safely fall back to
40
- // `TmuxBackend::for_workspace(team_workspace)` inside the resolver.
41
- let team_ws = team_workspace(workspace);
220
+ // The MCP server already passes the canonical run workspace here.
221
+ // Reapplying team_workspace() would strip the scratch root and bind
222
+ // the first dynamic worker to the parent workspace's tmux server.
42
223
  let transport =
43
224
  crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
44
- &team_ws, team,
225
+ workspace, team,
45
226
  )
46
- .unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(&team_ws));
227
+ .unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(workspace));
47
228
  return add_agent_with_transport(
48
229
  workspace,
49
230
  agent_id,
@@ -55,12 +236,18 @@ pub fn add_agent(
55
236
  }
56
237
  Err(error) => return Err(LifecycleError::TeamSelect(error.to_string())),
57
238
  };
58
- let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
239
+ let lifecycle_lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
59
240
  workspace: &selected.run_workspace,
60
241
  operation: "add-agent",
61
242
  team: Some(selected.team_key.as_str()),
62
243
  agent_id: Some(agent_id),
63
244
  })?;
245
+ let reservation = reserve_agent_slot(
246
+ &selected.run_workspace,
247
+ Some(selected.team_key.as_str()),
248
+ agent_id,
249
+ )?;
250
+ drop(lifecycle_lock);
64
251
  // E5 §3:compile_team 要角色定义目录(team_dir),不是 spec 落点(spec_workspace=runtime)。
65
252
  let team_dir = selected.team_dir;
66
253
  // **0.3.24 add-agent socket drift fix**: route to the live team's persisted
@@ -74,7 +261,7 @@ pub fn add_agent(
74
261
  Some(selected.team_key.as_str()),
75
262
  )
76
263
  .unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace));
77
- add_agent_with_transport_at_paths(
264
+ add_agent_with_transport_at_paths_reserved(
78
265
  &selected.run_workspace,
79
266
  &team_dir,
80
267
  agent_id,
@@ -82,9 +269,19 @@ pub fn add_agent(
82
269
  open_display,
83
270
  Some(selected.team_key.as_str()),
84
271
  &transport,
272
+ Some(reservation),
273
+ false,
85
274
  )
86
275
  }
87
276
 
277
+ /// ---
278
+ /// purpose: 强制重建一席,先快照旧席再摘除再加回
279
+ /// params:
280
+ /// force: 为假时直接退回普通 add_agent
281
+ /// returns: 新席的环境与启动模式
282
+ /// errors: 任一步失败都按快照恢复,恢复本身再出错时错误里附 rollback_errors
283
+ /// contract_id: lifecycle.add_agent.force_entry
284
+ /// ---
88
285
  /// Reconcile a single existing/inconsistent seat, then reuse the normal add
89
286
  /// path. The external role source is preserved by remove-agent ownership
90
287
  /// checks, so this is a one-command force-recreate rather than a team restart.
@@ -128,6 +325,12 @@ pub fn add_agent_force(
128
325
  )
129
326
  }
130
327
 
328
+ /// ---
329
+ /// purpose: 带注入 transport 的加席入口,归一 workspace 并拿锁后转实体实现
330
+ /// returns: 新席的环境与启动模式
331
+ /// errors: 归一 workspace 失败返回 StatePersist,其余透传
332
+ /// contract_id: lifecycle.add_agent.entry
333
+ /// ---
131
334
  /// `add_agent` with an injected transport — after the recompile+write, wires the new worker spawn
132
335
  /// (via start_agent_with_transport) + start_coordinator (rt-host-a sweep: recompiled but never spawned).
133
336
  pub(crate) fn add_agent_with_transport(
@@ -140,13 +343,15 @@ pub(crate) fn add_agent_with_transport(
140
343
  ) -> Result<AddAgentReport, LifecycleError> {
141
344
  let run_workspace = crate::model::paths::canonical_run_workspace(workspace)
142
345
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
143
- let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
346
+ let lifecycle_lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
144
347
  workspace: &run_workspace,
145
348
  operation: "add-agent",
146
349
  team,
147
350
  agent_id: Some(agent_id),
148
351
  })?;
149
- add_agent_with_transport_at_paths(
352
+ let reservation = reserve_agent_slot(&run_workspace, team, agent_id)?;
353
+ drop(lifecycle_lock);
354
+ add_agent_with_transport_at_paths_reserved(
150
355
  &run_workspace,
151
356
  workspace,
152
357
  agent_id,
@@ -154,9 +359,19 @@ pub(crate) fn add_agent_with_transport(
154
359
  open_display,
155
360
  team,
156
361
  transport,
362
+ Some(reservation),
363
+ false,
157
364
  )
158
365
  }
159
366
 
367
+ /// ---
368
+ /// purpose: 带注入 transport 的强制重建入口
369
+ /// params:
370
+ /// force: 为假时退回普通 add_agent_with_transport
371
+ /// returns: 新席的环境与启动模式
372
+ /// errors: 归一 workspace 失败返回 StatePersist,其余透传
373
+ /// contract_id: lifecycle.add_agent.force_entry
374
+ /// ---
160
375
  pub(crate) fn add_agent_with_transport_force(
161
376
  workspace: &Path,
162
377
  agent_id: &AgentId,
@@ -195,6 +410,11 @@ pub(crate) fn add_agent_with_transport_force(
195
410
  )
196
411
  }
197
412
 
413
+ /// ---
414
+ /// purpose: 已持锁状态下的强制重建,先校验替换源可用再消费旧席
415
+ /// returns: 新席报告;成功后还要过快照的一致性校验
416
+ /// errors: 角色文件不存在先行返回 Compile;摘除、加回或一致性校验失败时按快照恢复并返回错误
417
+ /// ---
198
418
  pub(super) fn force_recreate_with_transport_locked(
199
419
  run_workspace: &Path,
200
420
  team_dir: &Path,
@@ -230,7 +450,7 @@ pub(super) fn force_recreate_with_transport_locked(
230
450
  let restore_errors = snapshot.restore(team, transport);
231
451
  return force_recreate_rollback_error(agent_id, error, restore_errors);
232
452
  }
233
- let operation = add_agent_with_transport_at_paths(
453
+ let operation = add_agent_with_transport_at_paths_locked(
234
454
  run_workspace,
235
455
  team_dir,
236
456
  agent_id,
@@ -256,6 +476,10 @@ pub(super) fn force_recreate_with_transport_locked(
256
476
  }
257
477
  }
258
478
 
479
+ /// ---
480
+ /// purpose: 把原始错误与回滚过程中的错误合成一个对外错误
481
+ /// returns: 回滚干净时原样返回原始错误;否则包成 StatePersist 并附 rollback_errors
482
+ /// ---
259
483
  pub(super) fn force_recreate_rollback_error<T>(
260
484
  agent_id: &AgentId,
261
485
  error: LifecycleError,
@@ -271,6 +495,11 @@ pub(super) fn force_recreate_rollback_error<T>(
271
495
  }
272
496
  }
273
497
 
498
+ /// ---
499
+ /// purpose: 测试用注入点,按环境变量在 spawn 之后制造一次失败
500
+ /// returns: 环境变量未设或为空时直接成功
501
+ /// errors: 设了非空值时返回 StatePersist
502
+ /// ---
274
503
  pub(super) fn maybe_fail_force_recreate_after_spawn() -> Result<(), LifecycleError> {
275
504
  let Ok(reason) = std::env::var("TEAM_AGENT_TEST_FAIL_FORCE_RECREATE_AFTER_SPAWN") else {
276
505
  return Ok(());
@@ -283,6 +512,36 @@ pub(super) fn maybe_fail_force_recreate_after_spawn() -> Result<(), LifecycleErr
283
512
  )))
284
513
  }
285
514
 
515
+ fn rollback_added_pi_wrapper(
516
+ run_workspace: &Path,
517
+ team_key: &str,
518
+ agent_id: &AgentId,
519
+ role_meta: &Value,
520
+ ) -> Result<(), LifecycleError> {
521
+ if role_meta.get("provider").and_then(Value::as_str) != Some("pi") {
522
+ return Ok(());
523
+ }
524
+ let wrapper =
525
+ crate::lifecycle::launch::pi_mcp::pi_seat_paths(run_workspace, team_key, agent_id.as_str())
526
+ .wrapper;
527
+ match std::fs::remove_file(&wrapper) {
528
+ Ok(()) => Ok(()),
529
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
530
+ Err(error) => Err(LifecycleError::StatePersist(format!(
531
+ "failed to roll back added Pi wrapper {}: {error}",
532
+ wrapper.display()
533
+ ))),
534
+ }
535
+ }
536
+
537
+ /// ---
538
+ /// purpose: 加席的实体实现,含 owner 门、重名拒绝、重编译 spec 原子写、席位 state upsert 与起席
539
+ /// params:
540
+ /// run_workspace: 已归一的 run workspace
541
+ /// team_dir: 角色定义所在目录,编译 team 用它
542
+ /// returns: 新席的环境与启动模式
543
+ /// errors: owner 门不过或重名返回 RequirementUnmet,角色文件缺失或编译不一致返回 Compile,state 与 spawn 失败先回滚再透传原错
544
+ /// ---
286
545
  pub(super) fn add_agent_with_transport_at_paths(
287
546
  run_workspace: &Path,
288
547
  team_dir: &Path,
@@ -291,6 +550,54 @@ pub(super) fn add_agent_with_transport_at_paths(
291
550
  open_display: bool,
292
551
  team: Option<&str>,
293
552
  transport: &dyn Transport,
553
+ ) -> Result<AddAgentReport, LifecycleError> {
554
+ add_agent_with_transport_at_paths_reserved(
555
+ run_workspace,
556
+ team_dir,
557
+ agent_id,
558
+ role_file_path,
559
+ open_display,
560
+ team,
561
+ transport,
562
+ None,
563
+ false,
564
+ )
565
+ }
566
+
567
+ #[allow(clippy::too_many_arguments)]
568
+ fn add_agent_with_transport_at_paths_locked(
569
+ run_workspace: &Path,
570
+ team_dir: &Path,
571
+ agent_id: &AgentId,
572
+ role_file_path: &Path,
573
+ open_display: bool,
574
+ team: Option<&str>,
575
+ transport: &dyn Transport,
576
+ ) -> Result<AddAgentReport, LifecycleError> {
577
+ add_agent_with_transport_at_paths_reserved(
578
+ run_workspace,
579
+ team_dir,
580
+ agent_id,
581
+ role_file_path,
582
+ open_display,
583
+ team,
584
+ transport,
585
+ None,
586
+ true,
587
+ )
588
+ }
589
+
590
+ #[allow(clippy::too_many_arguments)]
591
+ fn add_agent_with_transport_at_paths_reserved(
592
+ run_workspace: &Path,
593
+ team_dir: &Path,
594
+ agent_id: &AgentId,
595
+ role_file_path: &Path,
596
+ open_display: bool,
597
+ team: Option<&str>,
598
+ transport: &dyn Transport,
599
+ reservation: Option<AgentReservation>,
600
+ lifecycle_lock_held: bool,
294
601
  ) -> Result<AddAgentReport, LifecycleError> {
295
602
  let runtime_state = crate::state::persist::load_runtime_state(run_workspace)
296
603
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
@@ -309,7 +616,15 @@ pub(super) fn add_agent_with_transport_at_paths(
309
616
  role_file_path.display()
310
617
  )));
311
618
  }
312
- if runtime_agent_exists(&owner_state, agent_id) {
619
+ let reservation_owned = reservation.as_ref().is_some_and(|reservation| {
620
+ owner_state
621
+ .get("agents")
622
+ .and_then(|agents| agents.get(agent_id.as_str()))
623
+ .and_then(|agent| agent.get("reservation_owner"))
624
+ .and_then(serde_json::Value::as_str)
625
+ == Some(reservation.owner.as_str())
626
+ });
627
+ if runtime_agent_exists(&owner_state, agent_id) && !reservation_owned {
313
628
  return Err(LifecycleError::RequirementUnmet(format!(
314
629
  "agent id already exists: {agent_id}"
315
630
  )));
@@ -336,20 +651,40 @@ pub(super) fn add_agent_with_transport_at_paths(
336
651
  compiled.id, agent_id
337
652
  )));
338
653
  }
339
- inject_agent_into_spec(&mut spec, compiled.agent, &compiled.id)?;
654
+ let compiled_agent = compiled.agent.clone();
655
+ inject_agent_into_spec(&mut spec, compiled_agent.clone(), &compiled.id)?;
340
656
  // E5 spec 迁移:重编译的 spec 原子写到 .team/runtime/<team_key>/(不落用户目录 team_dir)。
341
657
  let spec_path = crate::model::paths::runtime_spec_path(run_workspace, &canonical_team_key);
342
658
  // E42 (0.3.24 P0): capture pre-write bytes for atomic rollback. If anything
343
659
  // downstream of write_spec_atomic + upsert_agent_state_from_role + spawn
344
660
  // fails, restore the prior bytes so the canonical spec / runtime state never
345
661
  // get a half-written row that disagrees with what remove-agent can see.
662
+ let reservation_lock = if lifecycle_lock_held {
663
+ None
664
+ } else {
665
+ Some(acquire_agent_lifecycle_lock(LifecycleLockRequest {
666
+ workspace: run_workspace,
667
+ operation: "add-agent",
668
+ team,
669
+ agent_id: Some(agent_id),
670
+ })?)
671
+ };
346
672
  let pre_spec_text = match std::fs::read_to_string(&spec_path) {
347
673
  Ok(text) => Some(text),
348
674
  Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
349
675
  Err(e) => return Err(LifecycleError::StatePersist(format!("read spec: {e}"))),
350
676
  };
351
- let pre_runtime_state = crate::state::persist::load_runtime_state(run_workspace).ok();
352
- write_spec_atomic(&spec_path, &spec)?;
677
+ // Merge against the latest runtime spec while holding only the short
678
+ // filesystem critical section. Role compilation happened before this lock;
679
+ // peer reservations therefore remain visible instead of being overwritten
680
+ // by a stale base-spec snapshot.
681
+ let mut latest_spec = pre_spec_text
682
+ .as_deref()
683
+ .and_then(|text| crate::model::yaml::loads(text).ok())
684
+ .unwrap_or(spec);
685
+ inject_agent_into_spec(&mut latest_spec, compiled_agent, &compiled.id)?;
686
+ write_spec_atomic(&spec_path, &latest_spec)?;
687
+ drop(reservation_lock);
353
688
  let (meta, _) = crate::compiler::read_front_matter(role_file_path)
354
689
  .map_err(|e| LifecycleError::Compile(e.to_string()))?;
355
690
  // upsert writes status="starting" (E42) — start_agent_at_paths::mark_agent_started
@@ -366,7 +701,7 @@ pub(super) fn add_agent_with_transport_at_paths(
366
701
  run_workspace,
367
702
  &spec_path,
368
703
  pre_spec_text.as_deref(),
369
- pre_runtime_state.as_ref(),
704
+ None,
370
705
  agent_id,
371
706
  "state_upsert_failed",
372
707
  );
@@ -384,36 +719,73 @@ pub(super) fn add_agent_with_transport_at_paths(
384
719
  ) {
385
720
  Ok(started) => started,
386
721
  Err(error) => {
722
+ let wrapper_rollback =
723
+ rollback_added_pi_wrapper(run_workspace, &canonical_team_key, agent_id, &meta);
387
724
  rollback_add_agent_atomic(
388
725
  run_workspace,
389
726
  &spec_path,
390
727
  pre_spec_text.as_deref(),
391
- pre_runtime_state.as_ref(),
728
+ None,
392
729
  agent_id,
393
730
  "start_agent_failed",
394
731
  );
395
- return Err(error);
732
+ return match wrapper_rollback {
733
+ Ok(()) => Err(error),
734
+ Err(rollback_error) => Err(LifecycleError::StatePersist(format!(
735
+ "{error}; {rollback_error}"
736
+ ))),
737
+ };
396
738
  }
397
739
  };
398
740
  let (env, start_mode) = match started {
399
741
  StartAgentOutcome::Running {
400
742
  env, start_mode, ..
401
743
  } => (env, start_mode),
402
- StartAgentOutcome::Noop { env, .. } => (env, StartMode::Noop),
744
+ StartAgentOutcome::Noop { .. } => {
745
+ let wrapper_rollback =
746
+ rollback_added_pi_wrapper(run_workspace, &canonical_team_key, agent_id, &meta);
747
+ rollback_add_agent_atomic(
748
+ run_workspace,
749
+ &spec_path,
750
+ pre_spec_text.as_deref(),
751
+ None,
752
+ agent_id,
753
+ "added_agent_noop",
754
+ );
755
+ let error = LifecycleError::RequirementUnmet(format!(
756
+ "newly added agent {agent_id} returned start_agent.noop"
757
+ ));
758
+ return match wrapper_rollback {
759
+ Ok(()) => Err(error),
760
+ Err(rollback_error) => Err(LifecycleError::StatePersist(format!(
761
+ "{error}; {rollback_error}"
762
+ ))),
763
+ };
764
+ }
403
765
  StartAgentOutcome::Paused { .. } => {
766
+ let wrapper_rollback =
767
+ rollback_added_pi_wrapper(run_workspace, &canonical_team_key, agent_id, &meta);
404
768
  rollback_add_agent_atomic(
405
769
  run_workspace,
406
770
  &spec_path,
407
771
  pre_spec_text.as_deref(),
408
- pre_runtime_state.as_ref(),
772
+ None,
409
773
  agent_id,
410
774
  "added_agent_paused",
411
775
  );
412
- return Err(LifecycleError::RequirementUnmet(format!(
413
- "added agent {agent_id} is paused"
414
- )));
776
+ let error =
777
+ LifecycleError::RequirementUnmet(format!("added agent {agent_id} is paused"));
778
+ return match wrapper_rollback {
779
+ Ok(()) => Err(error),
780
+ Err(rollback_error) => Err(LifecycleError::StatePersist(format!(
781
+ "{error}; {rollback_error}"
782
+ ))),
783
+ };
415
784
  }
416
785
  };
786
+ if let Some(reservation) = reservation {
787
+ reservation.commit();
788
+ }
417
789
  Ok(AddAgentReport {
418
790
  env,
419
791
  start_mode,