@team-agent/installer 0.5.50 → 0.5.51

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 (84) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -3
  4. package/crates/team-agent/src/cli/emit.rs +2 -1
  5. package/crates/team-agent/src/cli/mod.rs +32 -14
  6. package/crates/team-agent/src/cli/named_address.rs +11 -48
  7. package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
  8. package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
  9. package/crates/team-agent/src/cli/send/persist.rs +154 -0
  10. package/crates/team-agent/src/cli/send/presentation.rs +333 -0
  11. package/crates/team-agent/src/cli/send/resolve.rs +361 -0
  12. package/crates/team-agent/src/cli/send.rs +44 -1169
  13. package/crates/team-agent/src/cli/spec.rs +1 -1
  14. package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
  15. package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
  16. package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
  17. package/crates/team-agent/src/cli/status_port/format.rs +145 -0
  18. package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
  19. package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
  20. package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
  21. package/crates/team-agent/src/cli/status_port/store.rs +412 -0
  22. package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
  23. package/crates/team-agent/src/cli/status_port.rs +47 -1548
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
  25. package/crates/team-agent/src/cli/types.rs +1 -0
  26. package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
  27. package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
  28. package/crates/team-agent/src/coordinator/tick.rs +37 -0
  29. package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
  30. package/crates/team-agent/src/db/message_store.rs +154 -44
  31. package/crates/team-agent/src/event_log.rs +73 -0
  32. package/crates/team-agent/src/leader/start.rs +28 -4
  33. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
  34. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
  35. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
  36. package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
  37. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
  38. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
  39. package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
  40. package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
  41. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
  42. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
  43. package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
  44. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
  45. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
  46. package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
  47. package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
  48. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
  49. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
  50. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
  51. package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
  52. package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
  53. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
  54. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
  55. package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
  56. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  57. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
  58. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
  59. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
  60. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
  61. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
  62. package/crates/team-agent/src/mcp_server/mod.rs +1 -1
  63. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
  64. package/crates/team-agent/src/mcp_server/tools.rs +14 -5
  65. package/crates/team-agent/src/messaging/activity.rs +4 -2
  66. package/crates/team-agent/src/messaging/address.rs +86 -0
  67. package/crates/team-agent/src/messaging/delivery.rs +165 -39
  68. package/crates/team-agent/src/messaging/helpers.rs +17 -13
  69. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
  70. package/crates/team-agent/src/messaging/mod.rs +10 -2
  71. package/crates/team-agent/src/messaging/persist.rs +309 -0
  72. package/crates/team-agent/src/messaging/results.rs +16 -24
  73. package/crates/team-agent/src/messaging/scheduler.rs +4 -2
  74. package/crates/team-agent/src/messaging/selftest.rs +19 -12
  75. package/crates/team-agent/src/messaging/send.rs +101 -49
  76. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
  77. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  78. package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
  79. package/crates/team-agent/src/messaging/watchers.rs +13 -3
  80. package/crates/team-agent/src/redaction.rs +72 -2
  81. package/crates/team-agent/src/state/persist.rs +2 -1
  82. package/crates/team-agent/src/state/repository/tests.rs +47 -0
  83. package/crates/team-agent/src/state/repository.rs +59 -16
  84. package/package.json +4 -4
@@ -0,0 +1,424 @@
1
+ use std::collections::{BTreeMap, BTreeSet};
2
+ use std::path::{Path, PathBuf};
3
+ use std::process::Command;
4
+
5
+ use crate::lifecycle::*;
6
+ use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
7
+ use crate::model::ids::AgentId;
8
+ use crate::model::permissions::{self, AgentPermissionInput};
9
+ use crate::model::yaml::{self, Value};
10
+ use crate::state::persist::load_runtime_state;
11
+ use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
12
+
13
+ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
14
+
15
+ use super::*;
16
+
17
+ /// `add_agent(workspace, agent_id, role_file_path, open_display, team)`
18
+ /// (`lifecycle/operations.py:143`)。动态 role doc 编译进 spec + 起 worker;失败**字节级回滚**
19
+ /// spec_yaml / workspace_state / **team_state.md** / role_file(Gap 15.11),每步发
20
+ /// `lifecycle.add_step_*` 事件(顺序被测试锁死)。
21
+ pub fn add_agent(
22
+ workspace: &Path,
23
+ agent_id: &AgentId,
24
+ role_file_path: &Path,
25
+ open_display: bool,
26
+ team: Option<&str>,
27
+ ) -> Result<AddAgentReport, LifecycleError> {
28
+ let selected = match crate::state::selector::resolve_active_team(
29
+ workspace,
30
+ team,
31
+ crate::state::selector::SelectorMode::RequireSpec,
32
+ ) {
33
+ Ok(selected) => selected,
34
+ 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);
42
+ let transport =
43
+ crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
44
+ &team_ws, team,
45
+ )
46
+ .unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(&team_ws));
47
+ return add_agent_with_transport(
48
+ workspace,
49
+ agent_id,
50
+ role_file_path,
51
+ open_display,
52
+ team,
53
+ &transport,
54
+ );
55
+ }
56
+ Err(error) => return Err(LifecycleError::TeamSelect(error.to_string())),
57
+ };
58
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
59
+ workspace: &selected.run_workspace,
60
+ operation: "add-agent",
61
+ team: Some(selected.team_key.as_str()),
62
+ agent_id: Some(agent_id),
63
+ })?;
64
+ // E5 §3:compile_team 要角色定义目录(team_dir),不是 spec 落点(spec_workspace=runtime)。
65
+ let team_dir = selected.team_dir;
66
+ // **0.3.24 add-agent socket drift fix**: route to the live team's persisted
67
+ // tmux endpoint (NOT the workspace-hash for_workspace socket). Without this,
68
+ // `add-agent` spawns into an orphan socket (e.g. `ta-<hash>/termclaud`) while
69
+ // the live team runs on its persisted default socket — the leader can't see
70
+ // the new window, state never registers, and the orphaned `claude` process
71
+ // floats forever (macmini repro: `demo-director` startup blocker).
72
+ let transport = crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
73
+ &selected.run_workspace,
74
+ Some(selected.team_key.as_str()),
75
+ )
76
+ .unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace));
77
+ add_agent_with_transport_at_paths(
78
+ &selected.run_workspace,
79
+ &team_dir,
80
+ agent_id,
81
+ role_file_path,
82
+ open_display,
83
+ Some(selected.team_key.as_str()),
84
+ &transport,
85
+ )
86
+ }
87
+
88
+ /// Reconcile a single existing/inconsistent seat, then reuse the normal add
89
+ /// path. The external role source is preserved by remove-agent ownership
90
+ /// checks, so this is a one-command force-recreate rather than a team restart.
91
+ pub fn add_agent_force(
92
+ workspace: &Path,
93
+ agent_id: &AgentId,
94
+ role_file_path: &Path,
95
+ open_display: bool,
96
+ team: Option<&str>,
97
+ force: bool,
98
+ ) -> Result<AddAgentReport, LifecycleError> {
99
+ if !force {
100
+ return add_agent(workspace, agent_id, role_file_path, open_display, team);
101
+ }
102
+ let selected = crate::state::selector::resolve_active_team(
103
+ workspace,
104
+ team,
105
+ crate::state::selector::SelectorMode::RequireSpec,
106
+ )
107
+ .map_err(|error| LifecycleError::TeamSelect(error.to_string()))?;
108
+ let canonical_team_key = selected.team_key.clone();
109
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
110
+ workspace: &selected.run_workspace,
111
+ operation: "add-agent-force",
112
+ team: Some(canonical_team_key.as_str()),
113
+ agent_id: Some(agent_id),
114
+ })?;
115
+ let transport = crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
116
+ &selected.run_workspace,
117
+ Some(canonical_team_key.as_str()),
118
+ )
119
+ .unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace));
120
+ force_recreate_with_transport_locked(
121
+ &selected.run_workspace,
122
+ &selected.team_dir,
123
+ agent_id,
124
+ role_file_path,
125
+ open_display,
126
+ Some(canonical_team_key.as_str()),
127
+ &transport,
128
+ )
129
+ }
130
+
131
+ /// `add_agent` with an injected transport — after the recompile+write, wires the new worker spawn
132
+ /// (via start_agent_with_transport) + start_coordinator (rt-host-a sweep: recompiled but never spawned).
133
+ pub fn add_agent_with_transport(
134
+ workspace: &Path,
135
+ agent_id: &AgentId,
136
+ role_file_path: &Path,
137
+ open_display: bool,
138
+ team: Option<&str>,
139
+ transport: &dyn Transport,
140
+ ) -> Result<AddAgentReport, LifecycleError> {
141
+ let run_workspace = crate::model::paths::canonical_run_workspace(workspace)
142
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
143
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
144
+ workspace: &run_workspace,
145
+ operation: "add-agent",
146
+ team,
147
+ agent_id: Some(agent_id),
148
+ })?;
149
+ add_agent_with_transport_at_paths(
150
+ &run_workspace,
151
+ workspace,
152
+ agent_id,
153
+ role_file_path,
154
+ open_display,
155
+ team,
156
+ transport,
157
+ )
158
+ }
159
+
160
+ pub fn add_agent_with_transport_force(
161
+ workspace: &Path,
162
+ agent_id: &AgentId,
163
+ role_file_path: &Path,
164
+ open_display: bool,
165
+ team: Option<&str>,
166
+ force: bool,
167
+ transport: &dyn Transport,
168
+ ) -> Result<AddAgentReport, LifecycleError> {
169
+ if !force {
170
+ return add_agent_with_transport(
171
+ workspace,
172
+ agent_id,
173
+ role_file_path,
174
+ open_display,
175
+ team,
176
+ transport,
177
+ );
178
+ }
179
+ let run_workspace = crate::model::paths::canonical_run_workspace(workspace)
180
+ .map_err(|error| LifecycleError::StatePersist(error.to_string()))?;
181
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
182
+ workspace: &run_workspace,
183
+ operation: "add-agent-force",
184
+ team,
185
+ agent_id: Some(agent_id),
186
+ })?;
187
+ force_recreate_with_transport_locked(
188
+ &run_workspace,
189
+ workspace,
190
+ agent_id,
191
+ role_file_path,
192
+ open_display,
193
+ team,
194
+ transport,
195
+ )
196
+ }
197
+
198
+ pub(super) fn force_recreate_with_transport_locked(
199
+ run_workspace: &Path,
200
+ team_dir: &Path,
201
+ agent_id: &AgentId,
202
+ role_file_path: &Path,
203
+ open_display: bool,
204
+ team: Option<&str>,
205
+ transport: &dyn Transport,
206
+ ) -> Result<AddAgentReport, LifecycleError> {
207
+ // Reject an unusable replacement source before consuming the old seat.
208
+ // Deeper compile/spawn failures remain covered by the transaction snapshot.
209
+ if !role_file_path.exists() {
210
+ return Err(LifecycleError::Compile(format!(
211
+ "role file not found: {}",
212
+ role_file_path.display()
213
+ )));
214
+ }
215
+ let snapshot = crate::lifecycle::restart::remove::ForceRecreateSnapshot::capture(
216
+ run_workspace,
217
+ agent_id,
218
+ team,
219
+ transport,
220
+ )?;
221
+ let remove = crate::lifecycle::restart::remove::remove_agent_with_transport_locked(
222
+ run_workspace,
223
+ agent_id,
224
+ true,
225
+ true,
226
+ team,
227
+ transport,
228
+ );
229
+ if let Err(error) = remove {
230
+ let restore_errors = snapshot.restore(team, transport);
231
+ return force_recreate_rollback_error(agent_id, error, restore_errors);
232
+ }
233
+ let operation = add_agent_with_transport_at_paths(
234
+ run_workspace,
235
+ team_dir,
236
+ agent_id,
237
+ role_file_path,
238
+ open_display,
239
+ team,
240
+ transport,
241
+ )
242
+ .and_then(|report| {
243
+ maybe_fail_force_recreate_after_spawn()?;
244
+ Ok(report)
245
+ })
246
+ .and_then(|report| {
247
+ snapshot.require_coherent(agent_id, team, transport)?;
248
+ Ok(report)
249
+ });
250
+ match operation {
251
+ Ok(report) => Ok(report),
252
+ Err(error) => {
253
+ let restore_errors = snapshot.restore_after_consumption(transport);
254
+ force_recreate_rollback_error(agent_id, error, restore_errors)
255
+ }
256
+ }
257
+ }
258
+
259
+ pub(super) fn force_recreate_rollback_error<T>(
260
+ agent_id: &AgentId,
261
+ error: LifecycleError,
262
+ restore_errors: Vec<String>,
263
+ ) -> Result<T, LifecycleError> {
264
+ if restore_errors.is_empty() {
265
+ Err(error)
266
+ } else {
267
+ Err(LifecycleError::StatePersist(format!(
268
+ "force-recreate failed for {agent_id}: {error}; rollback_errors={}",
269
+ restore_errors.join("|")
270
+ )))
271
+ }
272
+ }
273
+
274
+ pub(super) fn maybe_fail_force_recreate_after_spawn() -> Result<(), LifecycleError> {
275
+ let Ok(reason) = std::env::var("TEAM_AGENT_TEST_FAIL_FORCE_RECREATE_AFTER_SPAWN") else {
276
+ return Ok(());
277
+ };
278
+ if reason.is_empty() {
279
+ return Ok(());
280
+ }
281
+ Err(LifecycleError::StatePersist(format!(
282
+ "injected force-recreate failure after spawn: {reason}"
283
+ )))
284
+ }
285
+
286
+ pub(super) fn add_agent_with_transport_at_paths(
287
+ run_workspace: &Path,
288
+ team_dir: &Path,
289
+ agent_id: &AgentId,
290
+ role_file_path: &Path,
291
+ open_display: bool,
292
+ team: Option<&str>,
293
+ transport: &dyn Transport,
294
+ ) -> Result<AddAgentReport, LifecycleError> {
295
+ let runtime_state = crate::state::persist::load_runtime_state(run_workspace)
296
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
297
+ let canonical_team_key = team
298
+ .filter(|key| !key.is_empty())
299
+ .map(str::to_string)
300
+ .or_else(|| explicit_active_team_key(&runtime_state))
301
+ .unwrap_or_else(|| crate::state::projection::team_state_key(&runtime_state));
302
+ let owner_state =
303
+ crate::state::projection::select_runtime_state(run_workspace, Some(&canonical_team_key))
304
+ .map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
305
+ ensure_owner_allowed_for_state(&owner_state, Some(agent_id))?;
306
+ if !role_file_path.exists() {
307
+ return Err(LifecycleError::Compile(format!(
308
+ "role file not found: {}",
309
+ role_file_path.display()
310
+ )));
311
+ }
312
+ if runtime_agent_exists(&owner_state, agent_id) {
313
+ return Err(LifecycleError::RequirementUnmet(format!(
314
+ "agent id already exists: {agent_id}"
315
+ )));
316
+ }
317
+ // E5 Bug1:不再 copy role 文件进 <team_dir>/agents(自拷贝 O_TRUNC 截断反模式)。
318
+ // 就地读外部 role 文档编译,注入 base team spec 的 agents/routing。role 文件留在原处。
319
+ let mut spec = crate::compiler::compile_team(team_dir)
320
+ .map_err(|e| LifecycleError::Compile(e.to_string()))?;
321
+ override_spec_workspace(&mut spec, run_workspace);
322
+ let workspace_s = spec
323
+ .get("team")
324
+ .and_then(|team| team.get("workspace"))
325
+ .and_then(Value::as_str)
326
+ .unwrap_or_else(|| team_dir.to_str().unwrap_or_default())
327
+ .to_string();
328
+ let team_meta = crate::compiler::read_front_matter(&team_dir.join("TEAM.md"))
329
+ .map(|(meta, _)| meta)
330
+ .unwrap_or(Value::Null);
331
+ let compiled = crate::compiler::compile_role_agent(role_file_path, &team_meta, &workspace_s)
332
+ .map_err(|e| LifecycleError::Compile(e.to_string()))?;
333
+ if compiled.id != agent_id.as_str() {
334
+ return Err(LifecycleError::Compile(format!(
335
+ "role file declares name '{}' but add-agent id is '{}'",
336
+ compiled.id, agent_id
337
+ )));
338
+ }
339
+ inject_agent_into_spec(&mut spec, compiled.agent, &compiled.id)?;
340
+ let safety = effective_runtime_config(&spec)?;
341
+ // E5 spec 迁移:重编译的 spec 原子写到 .team/runtime/<team_key>/(不落用户目录 team_dir)。
342
+ let spec_path = crate::model::paths::runtime_spec_path(run_workspace, &canonical_team_key);
343
+ // E42 (0.3.24 P0): capture pre-write bytes for atomic rollback. If anything
344
+ // downstream of write_spec_atomic + upsert_agent_state_from_role + spawn
345
+ // fails, restore the prior bytes so the canonical spec / runtime state never
346
+ // get a half-written row that disagrees with what remove-agent can see.
347
+ let pre_spec_text = match std::fs::read_to_string(&spec_path) {
348
+ Ok(text) => Some(text),
349
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
350
+ Err(e) => return Err(LifecycleError::StatePersist(format!("read spec: {e}"))),
351
+ };
352
+ let pre_runtime_state = crate::state::persist::load_runtime_state(run_workspace).ok();
353
+ write_spec_atomic(&spec_path, &spec)?;
354
+ let (meta, _) = crate::compiler::read_front_matter(role_file_path)
355
+ .map_err(|e| LifecycleError::Compile(e.to_string()))?;
356
+ // upsert writes status="starting" (E42) — start_agent_at_paths::mark_agent_started
357
+ // promotes to "running" on Ok. If anything fails between here and the Ok
358
+ // return below, rollback restores the captured pre-bytes.
359
+ if let Err(error) = upsert_agent_state_from_role(
360
+ run_workspace,
361
+ &canonical_team_key,
362
+ agent_id,
363
+ &meta,
364
+ role_file_path,
365
+ &safety,
366
+ ) {
367
+ rollback_add_agent_atomic(
368
+ run_workspace,
369
+ &spec_path,
370
+ pre_spec_text.as_deref(),
371
+ pre_runtime_state.as_ref(),
372
+ agent_id,
373
+ "state_upsert_failed",
374
+ );
375
+ return Err(error);
376
+ }
377
+ let started = match crate::lifecycle::restart::start_agent_at_paths(
378
+ run_workspace,
379
+ team_dir,
380
+ agent_id,
381
+ false,
382
+ open_display,
383
+ true,
384
+ Some(&canonical_team_key),
385
+ transport,
386
+ ) {
387
+ Ok(started) => started,
388
+ Err(error) => {
389
+ rollback_add_agent_atomic(
390
+ run_workspace,
391
+ &spec_path,
392
+ pre_spec_text.as_deref(),
393
+ pre_runtime_state.as_ref(),
394
+ agent_id,
395
+ "start_agent_failed",
396
+ );
397
+ return Err(error);
398
+ }
399
+ };
400
+ let (env, start_mode) = match started {
401
+ StartAgentOutcome::Running {
402
+ env, start_mode, ..
403
+ } => (env, start_mode),
404
+ StartAgentOutcome::Noop { env, .. } => (env, StartMode::Noop),
405
+ StartAgentOutcome::Paused { .. } => {
406
+ rollback_add_agent_atomic(
407
+ run_workspace,
408
+ &spec_path,
409
+ pre_spec_text.as_deref(),
410
+ pre_runtime_state.as_ref(),
411
+ agent_id,
412
+ "added_agent_paused",
413
+ );
414
+ return Err(LifecycleError::RequirementUnmet(format!(
415
+ "added agent {agent_id} is paused"
416
+ )));
417
+ }
418
+ };
419
+ Ok(AddAgentReport {
420
+ env,
421
+ start_mode,
422
+ role_file: role_file_path.to_path_buf(),
423
+ })
424
+ }