@team-agent/installer 0.5.65 → 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 (208) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +6 -0
  4. package/crates/team-agent/src/cli/adapters.rs +6 -1
  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/compile.rs +1 -1
  17. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
  18. package/crates/team-agent/src/cli/tests/mod.rs +2 -2
  19. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
  20. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  21. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  22. package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
  23. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  24. package/crates/team-agent/src/compiler/tests.rs +23 -20
  25. package/crates/team-agent/src/compiler.rs +84 -5
  26. package/crates/team-agent/src/conpty/backend.rs +16 -0
  27. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  28. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  29. package/crates/team-agent/src/coordinator/health.rs +173 -0
  30. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  31. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  32. package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
  33. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  34. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  35. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  36. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  37. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  38. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  39. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  40. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  41. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  42. package/crates/team-agent/src/coordinator/types.rs +49 -0
  43. package/crates/team-agent/src/db/message_store.rs +39 -5
  44. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  45. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  46. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  47. package/crates/team-agent/src/leader/registry.rs +65 -0
  48. package/crates/team-agent/src/leader/start.rs +1080 -58
  49. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1 -1
  50. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  51. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  52. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -14
  53. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +69 -2
  54. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +196 -2
  55. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  56. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  57. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
  58. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -457
  59. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  60. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  61. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  62. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  63. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  64. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
  65. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  66. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  67. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  68. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
  69. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  70. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  71. package/crates/team-agent/src/lifecycle/launch/spawn.rs +101 -4
  72. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +244 -48
  73. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -2
  74. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  75. package/crates/team-agent/src/lifecycle/launch.rs +209 -43
  76. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  77. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  78. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  79. package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
  80. package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
  81. package/crates/team-agent/src/lifecycle/restart/agent.rs +213 -3
  82. package/crates/team-agent/src/lifecycle/restart/common.rs +373 -18
  83. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  84. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  85. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -15
  86. package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
  87. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  88. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  89. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  90. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -1
  91. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +3 -4
  92. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +1 -1
  93. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -2
  94. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +4 -4
  95. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  96. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +159 -174
  97. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +2 -2
  98. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +15 -13
  99. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -18
  100. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +1 -1
  101. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
  102. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
  103. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  104. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +1 -1
  105. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +1 -1
  106. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
  107. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  108. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  109. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
  110. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  111. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  112. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -0
  113. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +3 -3
  114. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +65 -40
  115. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +20 -32
  116. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +17 -9
  117. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  118. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +24 -23
  119. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +98 -62
  120. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +2 -2
  121. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +1 -1
  122. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +1 -1
  123. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -1
  124. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -2
  125. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +1 -1
  126. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +1 -1
  127. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +41 -8
  128. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +3 -3
  129. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1 -1
  130. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +1 -1
  131. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -1
  132. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1 -1
  133. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1 -1
  134. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +1 -1
  135. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +60 -2
  136. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -1
  137. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -2
  138. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  139. package/crates/team-agent/src/lifecycle/tests.rs +23 -15
  140. package/crates/team-agent/src/lifecycle/types.rs +61 -2
  141. package/crates/team-agent/src/lifecycle/worker_command_context.rs +200 -33
  142. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  143. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  144. package/crates/team-agent/src/messaging/delivery.rs +405 -40
  145. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  146. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  147. package/crates/team-agent/src/messaging/results.rs +4 -0
  148. package/crates/team-agent/src/messaging/send.rs +34 -0
  149. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  150. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  151. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  152. package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
  153. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  154. package/crates/team-agent/src/messaging/types.rs +4 -1
  155. package/crates/team-agent/src/model/enums.rs +14 -5
  156. package/crates/team-agent/src/model/permissions.rs +3 -0
  157. package/crates/team-agent/src/model/spec.rs +129 -6
  158. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +3 -1
  159. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +3 -1
  160. package/crates/team-agent/src/model/testdata/team.spec.yaml +3 -1
  161. package/crates/team-agent/src/os_probe.rs +73 -2
  162. package/crates/team-agent/src/provider/adapter.rs +231 -4
  163. package/crates/team-agent/src/provider/adapters/claude.rs +5 -2
  164. package/crates/team-agent/src/provider/adapters/codex.rs +5 -2
  165. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -2
  166. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
  167. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  168. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  169. package/crates/team-agent/src/provider/bypass_flags.rs +124 -0
  170. package/crates/team-agent/src/provider/classify.rs +4 -2
  171. package/crates/team-agent/src/provider/faults.rs +2 -1
  172. package/crates/team-agent/src/provider/mod.rs +13 -0
  173. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  174. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  175. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  176. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  177. package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
  178. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  179. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  180. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  181. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  182. package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
  183. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  184. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  185. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  186. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  187. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  188. package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
  189. package/crates/team-agent/src/provider/types.rs +4 -2
  190. package/crates/team-agent/src/provider/wire.rs +23 -1
  191. package/crates/team-agent/src/state/persist.rs +301 -8
  192. package/crates/team-agent/src/state/repository.rs +5 -0
  193. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  194. package/crates/team-agent/src/tmux_backend.rs +1083 -75
  195. package/crates/team-agent/src/transport/test_support.rs +20 -0
  196. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  197. package/crates/team-agent/src/transport.rs +265 -1
  198. package/npm/install.mjs +129 -73
  199. package/package.json +4 -4
  200. package/skills/team-agent/SKILL.md +33 -238
  201. package/skills/team-agent/command-coverage.json +31 -0
  202. package/crates/team-agent/src/lifecycle/launch/approval.rs +0 -134
  203. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  204. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -483
  205. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  206. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
  207. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +0 -1187
  208. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +0 -327
@@ -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
+ //! - 回滚只恢复 spec 字节与 runtime state,不回收已 spawn 的 pane
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),每步发
@@ -55,12 +240,18 @@ pub fn add_agent(
55
240
  }
56
241
  Err(error) => return Err(LifecycleError::TeamSelect(error.to_string())),
57
242
  };
58
- let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
243
+ let lifecycle_lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
59
244
  workspace: &selected.run_workspace,
60
245
  operation: "add-agent",
61
246
  team: Some(selected.team_key.as_str()),
62
247
  agent_id: Some(agent_id),
63
248
  })?;
249
+ let reservation = reserve_agent_slot(
250
+ &selected.run_workspace,
251
+ Some(selected.team_key.as_str()),
252
+ agent_id,
253
+ )?;
254
+ drop(lifecycle_lock);
64
255
  // E5 §3:compile_team 要角色定义目录(team_dir),不是 spec 落点(spec_workspace=runtime)。
65
256
  let team_dir = selected.team_dir;
66
257
  // **0.3.24 add-agent socket drift fix**: route to the live team's persisted
@@ -74,7 +265,7 @@ pub fn add_agent(
74
265
  Some(selected.team_key.as_str()),
75
266
  )
76
267
  .unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace));
77
- add_agent_with_transport_at_paths(
268
+ add_agent_with_transport_at_paths_reserved(
78
269
  &selected.run_workspace,
79
270
  &team_dir,
80
271
  agent_id,
@@ -82,9 +273,19 @@ pub fn add_agent(
82
273
  open_display,
83
274
  Some(selected.team_key.as_str()),
84
275
  &transport,
276
+ Some(reservation),
277
+ false,
85
278
  )
86
279
  }
87
280
 
281
+ /// ---
282
+ /// purpose: 强制重建一席,先快照旧席再摘除再加回
283
+ /// params:
284
+ /// force: 为假时直接退回普通 add_agent
285
+ /// returns: 新席的环境与启动模式
286
+ /// errors: 任一步失败都按快照恢复,恢复本身再出错时错误里附 rollback_errors
287
+ /// contract_id: lifecycle.add_agent.force_entry
288
+ /// ---
88
289
  /// Reconcile a single existing/inconsistent seat, then reuse the normal add
89
290
  /// path. The external role source is preserved by remove-agent ownership
90
291
  /// checks, so this is a one-command force-recreate rather than a team restart.
@@ -128,6 +329,12 @@ pub fn add_agent_force(
128
329
  )
129
330
  }
130
331
 
332
+ /// ---
333
+ /// purpose: 带注入 transport 的加席入口,归一 workspace 并拿锁后转实体实现
334
+ /// returns: 新席的环境与启动模式
335
+ /// errors: 归一 workspace 失败返回 StatePersist,其余透传
336
+ /// contract_id: lifecycle.add_agent.entry
337
+ /// ---
131
338
  /// `add_agent` with an injected transport — after the recompile+write, wires the new worker spawn
132
339
  /// (via start_agent_with_transport) + start_coordinator (rt-host-a sweep: recompiled but never spawned).
133
340
  pub(crate) fn add_agent_with_transport(
@@ -140,13 +347,15 @@ pub(crate) fn add_agent_with_transport(
140
347
  ) -> Result<AddAgentReport, LifecycleError> {
141
348
  let run_workspace = crate::model::paths::canonical_run_workspace(workspace)
142
349
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
143
- let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
350
+ let lifecycle_lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
144
351
  workspace: &run_workspace,
145
352
  operation: "add-agent",
146
353
  team,
147
354
  agent_id: Some(agent_id),
148
355
  })?;
149
- add_agent_with_transport_at_paths(
356
+ let reservation = reserve_agent_slot(&run_workspace, team, agent_id)?;
357
+ drop(lifecycle_lock);
358
+ add_agent_with_transport_at_paths_reserved(
150
359
  &run_workspace,
151
360
  workspace,
152
361
  agent_id,
@@ -154,9 +363,19 @@ pub(crate) fn add_agent_with_transport(
154
363
  open_display,
155
364
  team,
156
365
  transport,
366
+ Some(reservation),
367
+ false,
157
368
  )
158
369
  }
159
370
 
371
+ /// ---
372
+ /// purpose: 带注入 transport 的强制重建入口
373
+ /// params:
374
+ /// force: 为假时退回普通 add_agent_with_transport
375
+ /// returns: 新席的环境与启动模式
376
+ /// errors: 归一 workspace 失败返回 StatePersist,其余透传
377
+ /// contract_id: lifecycle.add_agent.force_entry
378
+ /// ---
160
379
  pub(crate) fn add_agent_with_transport_force(
161
380
  workspace: &Path,
162
381
  agent_id: &AgentId,
@@ -195,6 +414,11 @@ pub(crate) fn add_agent_with_transport_force(
195
414
  )
196
415
  }
197
416
 
417
+ /// ---
418
+ /// purpose: 已持锁状态下的强制重建,先校验替换源可用再消费旧席
419
+ /// returns: 新席报告;成功后还要过快照的一致性校验
420
+ /// errors: 角色文件不存在先行返回 Compile;摘除、加回或一致性校验失败时按快照恢复并返回错误
421
+ /// ---
198
422
  pub(super) fn force_recreate_with_transport_locked(
199
423
  run_workspace: &Path,
200
424
  team_dir: &Path,
@@ -230,7 +454,7 @@ pub(super) fn force_recreate_with_transport_locked(
230
454
  let restore_errors = snapshot.restore(team, transport);
231
455
  return force_recreate_rollback_error(agent_id, error, restore_errors);
232
456
  }
233
- let operation = add_agent_with_transport_at_paths(
457
+ let operation = add_agent_with_transport_at_paths_locked(
234
458
  run_workspace,
235
459
  team_dir,
236
460
  agent_id,
@@ -256,6 +480,10 @@ pub(super) fn force_recreate_with_transport_locked(
256
480
  }
257
481
  }
258
482
 
483
+ /// ---
484
+ /// purpose: 把原始错误与回滚过程中的错误合成一个对外错误
485
+ /// returns: 回滚干净时原样返回原始错误;否则包成 StatePersist 并附 rollback_errors
486
+ /// ---
259
487
  pub(super) fn force_recreate_rollback_error<T>(
260
488
  agent_id: &AgentId,
261
489
  error: LifecycleError,
@@ -271,6 +499,11 @@ pub(super) fn force_recreate_rollback_error<T>(
271
499
  }
272
500
  }
273
501
 
502
+ /// ---
503
+ /// purpose: 测试用注入点,按环境变量在 spawn 之后制造一次失败
504
+ /// returns: 环境变量未设或为空时直接成功
505
+ /// errors: 设了非空值时返回 StatePersist
506
+ /// ---
274
507
  pub(super) fn maybe_fail_force_recreate_after_spawn() -> Result<(), LifecycleError> {
275
508
  let Ok(reason) = std::env::var("TEAM_AGENT_TEST_FAIL_FORCE_RECREATE_AFTER_SPAWN") else {
276
509
  return Ok(());
@@ -283,6 +516,14 @@ pub(super) fn maybe_fail_force_recreate_after_spawn() -> Result<(), LifecycleErr
283
516
  )))
284
517
  }
285
518
 
519
+ /// ---
520
+ /// purpose: 加席的实体实现,含 owner 门、重名拒绝、重编译 spec 原子写、席位 state upsert 与起席
521
+ /// params:
522
+ /// run_workspace: 已归一的 run workspace
523
+ /// team_dir: 角色定义所在目录,编译 team 用它
524
+ /// returns: 新席的环境与启动模式
525
+ /// errors: owner 门不过或重名返回 RequirementUnmet,角色文件缺失或编译不一致返回 Compile,state 与 spawn 失败先回滚再透传原错
526
+ /// ---
286
527
  pub(super) fn add_agent_with_transport_at_paths(
287
528
  run_workspace: &Path,
288
529
  team_dir: &Path,
@@ -291,6 +532,54 @@ pub(super) fn add_agent_with_transport_at_paths(
291
532
  open_display: bool,
292
533
  team: Option<&str>,
293
534
  transport: &dyn Transport,
535
+ ) -> Result<AddAgentReport, LifecycleError> {
536
+ add_agent_with_transport_at_paths_reserved(
537
+ run_workspace,
538
+ team_dir,
539
+ agent_id,
540
+ role_file_path,
541
+ open_display,
542
+ team,
543
+ transport,
544
+ None,
545
+ false,
546
+ )
547
+ }
548
+
549
+ #[allow(clippy::too_many_arguments)]
550
+ fn add_agent_with_transport_at_paths_locked(
551
+ run_workspace: &Path,
552
+ team_dir: &Path,
553
+ agent_id: &AgentId,
554
+ role_file_path: &Path,
555
+ open_display: bool,
556
+ team: Option<&str>,
557
+ transport: &dyn Transport,
558
+ ) -> Result<AddAgentReport, LifecycleError> {
559
+ add_agent_with_transport_at_paths_reserved(
560
+ run_workspace,
561
+ team_dir,
562
+ agent_id,
563
+ role_file_path,
564
+ open_display,
565
+ team,
566
+ transport,
567
+ None,
568
+ true,
569
+ )
570
+ }
571
+
572
+ #[allow(clippy::too_many_arguments)]
573
+ fn add_agent_with_transport_at_paths_reserved(
574
+ run_workspace: &Path,
575
+ team_dir: &Path,
576
+ agent_id: &AgentId,
577
+ role_file_path: &Path,
578
+ open_display: bool,
579
+ team: Option<&str>,
580
+ transport: &dyn Transport,
581
+ reservation: Option<AgentReservation>,
582
+ lifecycle_lock_held: bool,
294
583
  ) -> Result<AddAgentReport, LifecycleError> {
295
584
  let runtime_state = crate::state::persist::load_runtime_state(run_workspace)
296
585
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
@@ -309,7 +598,15 @@ pub(super) fn add_agent_with_transport_at_paths(
309
598
  role_file_path.display()
310
599
  )));
311
600
  }
312
- if runtime_agent_exists(&owner_state, agent_id) {
601
+ let reservation_owned = reservation.as_ref().is_some_and(|reservation| {
602
+ owner_state
603
+ .get("agents")
604
+ .and_then(|agents| agents.get(agent_id.as_str()))
605
+ .and_then(|agent| agent.get("reservation_owner"))
606
+ .and_then(serde_json::Value::as_str)
607
+ == Some(reservation.owner.as_str())
608
+ });
609
+ if runtime_agent_exists(&owner_state, agent_id) && !reservation_owned {
313
610
  return Err(LifecycleError::RequirementUnmet(format!(
314
611
  "agent id already exists: {agent_id}"
315
612
  )));
@@ -336,21 +633,40 @@ pub(super) fn add_agent_with_transport_at_paths(
336
633
  compiled.id, agent_id
337
634
  )));
338
635
  }
339
- inject_agent_into_spec(&mut spec, compiled.agent, &compiled.id)?;
340
- let safety = effective_runtime_config(&spec)?;
636
+ let compiled_agent = compiled.agent.clone();
637
+ inject_agent_into_spec(&mut spec, compiled_agent.clone(), &compiled.id)?;
341
638
  // E5 spec 迁移:重编译的 spec 原子写到 .team/runtime/<team_key>/(不落用户目录 team_dir)。
342
639
  let spec_path = crate::model::paths::runtime_spec_path(run_workspace, &canonical_team_key);
343
640
  // E42 (0.3.24 P0): capture pre-write bytes for atomic rollback. If anything
344
641
  // downstream of write_spec_atomic + upsert_agent_state_from_role + spawn
345
642
  // fails, restore the prior bytes so the canonical spec / runtime state never
346
643
  // get a half-written row that disagrees with what remove-agent can see.
644
+ let reservation_lock = if lifecycle_lock_held {
645
+ None
646
+ } else {
647
+ Some(acquire_agent_lifecycle_lock(LifecycleLockRequest {
648
+ workspace: run_workspace,
649
+ operation: "add-agent",
650
+ team,
651
+ agent_id: Some(agent_id),
652
+ })?)
653
+ };
347
654
  let pre_spec_text = match std::fs::read_to_string(&spec_path) {
348
655
  Ok(text) => Some(text),
349
656
  Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
350
657
  Err(e) => return Err(LifecycleError::StatePersist(format!("read spec: {e}"))),
351
658
  };
352
- let pre_runtime_state = crate::state::persist::load_runtime_state(run_workspace).ok();
353
- write_spec_atomic(&spec_path, &spec)?;
659
+ // Merge against the latest runtime spec while holding only the short
660
+ // filesystem critical section. Role compilation happened before this lock;
661
+ // peer reservations therefore remain visible instead of being overwritten
662
+ // by a stale base-spec snapshot.
663
+ let mut latest_spec = pre_spec_text
664
+ .as_deref()
665
+ .and_then(|text| crate::model::yaml::loads(text).ok())
666
+ .unwrap_or(spec);
667
+ inject_agent_into_spec(&mut latest_spec, compiled_agent, &compiled.id)?;
668
+ write_spec_atomic(&spec_path, &latest_spec)?;
669
+ drop(reservation_lock);
354
670
  let (meta, _) = crate::compiler::read_front_matter(role_file_path)
355
671
  .map_err(|e| LifecycleError::Compile(e.to_string()))?;
356
672
  // upsert writes status="starting" (E42) — start_agent_at_paths::mark_agent_started
@@ -362,13 +678,12 @@ pub(super) fn add_agent_with_transport_at_paths(
362
678
  agent_id,
363
679
  &meta,
364
680
  role_file_path,
365
- &safety,
366
681
  ) {
367
682
  rollback_add_agent_atomic(
368
683
  run_workspace,
369
684
  &spec_path,
370
685
  pre_spec_text.as_deref(),
371
- pre_runtime_state.as_ref(),
686
+ None,
372
687
  agent_id,
373
688
  "state_upsert_failed",
374
689
  );
@@ -390,7 +705,7 @@ pub(super) fn add_agent_with_transport_at_paths(
390
705
  run_workspace,
391
706
  &spec_path,
392
707
  pre_spec_text.as_deref(),
393
- pre_runtime_state.as_ref(),
708
+ None,
394
709
  agent_id,
395
710
  "start_agent_failed",
396
711
  );
@@ -407,7 +722,7 @@ pub(super) fn add_agent_with_transport_at_paths(
407
722
  run_workspace,
408
723
  &spec_path,
409
724
  pre_spec_text.as_deref(),
410
- pre_runtime_state.as_ref(),
725
+ None,
411
726
  agent_id,
412
727
  "added_agent_paused",
413
728
  );
@@ -416,6 +731,9 @@ pub(super) fn add_agent_with_transport_at_paths(
416
731
  )));
417
732
  }
418
733
  };
734
+ if let Some(reservation) = reservation {
735
+ reservation.commit();
736
+ }
419
737
  Ok(AddAgentReport {
420
738
  env,
421
739
  start_mode,
@@ -1,3 +1,26 @@
1
+ //! ---
2
+ //! purpose: 加席过程中的 state 写入、spec 注入与失败回滚
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: rollback_add_agent_atomic
6
+ //! what: 尽力恢复 spec 字节与 runtime state,并写一条回滚事件
7
+ //! - name: upsert_agent_state_from_role
8
+ //! what: 由角色文档 front matter 写出该席位的 state 行
9
+ //! - name: inject_agent_into_spec
10
+ //! what: 把编译出的 agent 注入 spec 的 agents 与 routing 规则
11
+ //! - name: runtime_agent_exists
12
+ //! what: 判断 state 里是否已有同 id 席位
13
+ //! depends:
14
+ //! - crate::state::repository
15
+ //! - crate::state::persist
16
+ //! - crate::lifecycle::restart::remove
17
+ //! - crate::event_log::EventLog
18
+ //! boundary:
19
+ //! - 回滚是尽力而为,回滚自身的错误被吞掉,只在事件里如实记录成败
20
+ //! - 注入 spec 不落盘,落盘由调用方原子写
21
+ //! - 已存在同 id 的条目不重复注入
22
+ //! maturity: wired
23
+ //! ---
1
24
  use std::collections::{BTreeMap, BTreeSet};
2
25
  use std::path::{Path, PathBuf};
3
26
  use std::process::Command;
@@ -14,6 +37,14 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
14
37
 
15
38
  use super::*;
16
39
 
40
+ /// ---
41
+ /// purpose: 加席失败后恢复 spec 与 runtime state,并给新席立墓碑防止被合并回来
42
+ /// params:
43
+ /// pre_spec_text: 写之前的 spec 字节;None 表示原本没有该文件,回滚即删除
44
+ /// pre_runtime_state: 写之前的 runtime state;None 时改为从当前 state 里摘掉该席位
45
+ /// reason: 写进回滚事件的原因串
46
+ /// returns: 无返回值;回滚成败如实写进 add_agent.rollback 事件
47
+ /// ---
17
48
  /// E42 (0.3.24 P0, double-spec deadlock): best-effort atomic rollback for a
18
49
  /// failed add-agent. Restores the canonical spec to its pre-write bytes (or
19
50
  /// removes the file if it didn't exist), and restores runtime state to its
@@ -110,13 +141,20 @@ pub(super) fn rollback_add_agent_atomic(
110
141
  );
111
142
  }
112
143
 
144
+ /// ---
145
+ /// purpose: 由角色文档的 front matter 写出该席位的 state 行并落盘
146
+ /// params:
147
+ /// meta: 角色文档 front matter
148
+ /// dynamic_role_file: 角色文件路径,记进 state 供 restart 重建 spec 用
149
+ /// returns: 成功返回空值,席位状态记为 starting
150
+ /// errors: 读或写 runtime state 失败时返回 StatePersist
151
+ /// ---
113
152
  pub(super) fn upsert_agent_state_from_role(
114
153
  workspace: &Path,
115
154
  canonical_team_key: &str,
116
155
  agent_id: &AgentId,
117
156
  meta: &Value,
118
157
  dynamic_role_file: &Path,
119
- safety: &DangerousApproval,
120
158
  ) -> Result<(), LifecycleError> {
121
159
  let mut state =
122
160
  crate::state::projection::select_runtime_state(workspace, Some(canonical_team_key))
@@ -195,7 +233,13 @@ pub(super) fn upsert_agent_state_from_role(
195
233
  }
196
234
  }
197
235
  if let Some(obj) = entry.as_object_mut() {
198
- persist_effective_approval_policy(obj, safety);
236
+ // 0.5.66 bypass 单源:policy 从 role 文档字段派生,不再用全队 `DangerousApproval`。
237
+ let meta_provider = meta
238
+ .get("provider")
239
+ .and_then(Value::as_str)
240
+ .and_then(crate::provider::wire::parse_provider)
241
+ .unwrap_or(Provider::Codex);
242
+ persist_effective_approval_policy_from_yaml_agent(obj, meta, meta_provider);
199
243
  }
200
244
  agent_map.insert(agent_id.as_str().to_string(), entry);
201
245
  crate::lifecycle::restart::remove::clear_agent_retirement_in_state(&mut state, agent_id);
@@ -207,6 +251,10 @@ pub(super) fn upsert_agent_state_from_role(
207
251
  )
208
252
  }
209
253
 
254
+ /// ---
255
+ /// purpose: 判断角色文件是框架托管的还是外部的
256
+ /// returns: 路径落在托管目录下为 managed,否则为 external
257
+ /// ---
210
258
  pub(super) fn role_source_ownership(workspace: &Path, role_file: &Path) -> &'static str {
211
259
  let managed_root = workspace.join(".team").join("dynamic-role-files");
212
260
  match (
@@ -218,6 +266,13 @@ pub(super) fn role_source_ownership(workspace: &Path, role_file: &Path) -> &'sta
218
266
  }
219
267
  }
220
268
 
269
+ /// ---
270
+ /// purpose: 把编译出的 agent 注入 spec 的 agents 列表并补一条同形路由规则
271
+ /// params:
272
+ /// spec: 就地改写;已有同 id 的 agent 或同目标的路由规则时不重复追加
273
+ /// returns: 成功返回空值,不落盘
274
+ /// errors: spec 不是 map 或 agents 缺失时返回 Compile
275
+ /// ---
221
276
  /// E5 Bug1:把 add-agent 就地编译出的 agent 条目注入 base team spec(`agents` 列表 +
222
277
  /// `routing.rules` 加 `route-<id>`),复刻 [`compile_team`] 的路由规则形态。不落任何文件。
223
278
  ///
@@ -274,6 +329,10 @@ pub(crate) fn inject_agent_into_spec(
274
329
  Ok(())
275
330
  }
276
331
 
332
+ /// ---
333
+ /// purpose: 判断 state 的 agents 表里是否已有该 id
334
+ /// returns: 存在则 true
335
+ /// ---
277
336
  pub(super) fn runtime_agent_exists(state: &serde_json::Value, agent_id: &AgentId) -> bool {
278
337
  state
279
338
  .get("agents")
@@ -281,6 +340,10 @@ pub(super) fn runtime_agent_exists(state: &serde_json::Value, agent_id: &AgentId
281
340
  .is_some_and(|agents| agents.contains_key(agent_id.as_str()))
282
341
  }
283
342
 
343
+ /// ---
344
+ /// purpose: 取 YAML agent 节点的 id
345
+ /// returns: id 是字符串时返回它,否则 None
346
+ /// ---
284
347
  pub(super) fn yaml_agent_id(agent: &Value) -> Option<&str> {
285
348
  let Value::Map(pairs) = agent else {
286
349
  return None;
@@ -294,6 +357,10 @@ pub(super) fn yaml_agent_id(agent: &Value) -> Option<&str> {
294
357
  })
295
358
  }
296
359
 
360
+ /// ---
361
+ /// purpose: 取 YAML 路由规则的 assign_to
362
+ /// returns: 该字段是字符串时返回它,否则 None
363
+ /// ---
297
364
  pub(super) fn yaml_route_assigns_to(rule: &Value) -> Option<&str> {
298
365
  let Value::Map(pairs) = rule else {
299
366
  return None;