@team-agent/installer 0.5.100 → 0.5.102

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 (63) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/diagnose.rs +29 -7
  4. package/crates/team-agent/src/cli/mod.rs +24 -8
  5. package/crates/team-agent/src/cli/tests/main_preserved.rs +0 -74
  6. package/crates/team-agent/src/compiler/tests.rs +20 -23
  7. package/crates/team-agent/src/compiler.rs +52 -65
  8. package/crates/team-agent/src/coordinator/tests/s3_bounded.rs +16 -48
  9. package/crates/team-agent/src/leader/start.rs +1 -14
  10. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +211 -16
  11. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +1 -0
  12. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +15 -5
  13. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +89 -6
  14. package/crates/team-agent/src/lifecycle/launch/role_source.rs +168 -32
  15. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
  16. package/crates/team-agent/src/lifecycle/launch.rs +11 -3
  17. package/crates/team-agent/src/lifecycle/profile_launch.rs +23 -8
  18. package/crates/team-agent/src/lifecycle/restart/agent.rs +2 -2
  19. package/crates/team-agent/src/lifecycle/restart/avatar.rs +1806 -0
  20. package/crates/team-agent/src/lifecycle/restart/common.rs +115 -2
  21. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +8 -2
  22. package/crates/team-agent/src/lifecycle/restart/selection.rs +14 -2
  23. package/crates/team-agent/src/lifecycle/restart.rs +11 -4
  24. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +0 -28
  25. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +11 -9
  26. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +8 -401
  27. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +2 -2
  28. package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1398 -0
  29. package/crates/team-agent/src/lifecycle/tests.rs +1 -0
  30. package/crates/team-agent/src/lifecycle/types.rs +10 -2
  31. package/crates/team-agent/src/lifecycle/worker_command_context.rs +22 -8
  32. package/crates/team-agent/src/mcp_server/helpers.rs +0 -16
  33. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +2 -7
  34. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -127
  35. package/crates/team-agent/src/mcp_server/mod.rs +7 -12
  36. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -132
  37. package/crates/team-agent/src/mcp_server/tests/send.rs +1 -93
  38. package/crates/team-agent/src/mcp_server/tests/tools.rs +0 -61
  39. package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -51
  40. package/crates/team-agent/src/mcp_server/tests.rs +4 -200
  41. package/crates/team-agent/src/mcp_server/tools.rs +5 -396
  42. package/crates/team-agent/src/mcp_server/types.rs +3 -36
  43. package/crates/team-agent/src/mcp_server/wire.rs +2 -223
  44. package/crates/team-agent/src/messaging/leader_receiver.rs +1 -1
  45. package/crates/team-agent/src/messaging/mod.rs +2 -2
  46. package/crates/team-agent/src/messaging/scheduler.rs +2 -205
  47. package/crates/team-agent/src/messaging/tests/basic.rs +0 -31
  48. package/crates/team-agent/src/messaging/tests/main_preserved.rs +0 -73
  49. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -26
  50. package/crates/team-agent/src/messaging/types.rs +0 -20
  51. package/crates/team-agent/src/model/spec.rs +15 -18
  52. package/crates/team-agent/src/provider/types.rs +2 -0
  53. package/crates/team-agent/src/state/persist.rs +58 -2
  54. package/crates/team-agent/src/state/projection.rs +5 -1
  55. package/crates/team-agent/src/state/repository/tests.rs +1 -16
  56. package/crates/team-agent/src/state/repository.rs +126 -37
  57. package/crates/team-agent/src/transport/test_support.rs +23 -3
  58. package/package.json +4 -4
  59. package/skills/team-agent/SKILL.md +18 -47
  60. package/skills/team-agent/command-coverage.json +7 -0
  61. package/skills/team-agent/references/team-agent-operator.md +28 -118
  62. package/skills/team-agent/references/team-in-team.md +4 -19
  63. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +0 -484
@@ -9,55 +9,118 @@
9
9
  //! - 不读 provider session 落盘
10
10
  //! maturity: wired
11
11
  //! ---
12
+ use std::fs::OpenOptions;
13
+ use std::io::Write;
14
+ use std::os::unix::fs::MetadataExt;
12
15
  use std::path::{Path, PathBuf};
16
+ use std::time::{SystemTime, UNIX_EPOCH};
13
17
 
14
18
  use crate::lifecycle::LifecycleError;
15
19
  use crate::model::ids::AgentId;
16
20
  use crate::model::yaml::{self, Value};
17
21
 
18
- fn set_yaml_map_value(
19
- value: &mut Value,
20
- key: &str,
21
- next: Value,
22
- ) -> Result<(), LifecycleError> {
22
+ fn set_yaml_map_value(value: &mut Value, key: &str, next: Value) -> Result<(), LifecycleError> {
23
23
  let Value::Map(pairs) = value else {
24
24
  return Err(LifecycleError::Compile(
25
25
  "agent entry is not a map".to_string(),
26
26
  ));
27
27
  };
28
- if let Some((_, existing)) = pairs.iter_mut().find(|(k, _)| k == key) {
29
- *existing = next;
30
- } else {
28
+ let mut found = false;
29
+ for (_, existing) in pairs
30
+ .iter_mut()
31
+ .filter(|(existing_key, _)| existing_key.as_str() == key)
32
+ {
33
+ *existing = next.clone();
34
+ found = true;
35
+ }
36
+ if !found {
31
37
  pairs.push((key.to_string(), next));
32
38
  }
33
39
  Ok(())
34
40
  }
35
41
 
36
- pub(super) struct MaterializedRole {
42
+ fn dump_role_frontmatter(meta: &Value) -> String {
43
+ let Value::Map(pairs) = meta else {
44
+ return yaml::dumps(meta);
45
+ };
46
+ let mut rendered = String::new();
47
+ for (key, value) in pairs {
48
+ if value.as_str().is_some_and(is_plain_frontmatter_scalar) {
49
+ if let Some(value) = value.as_str() {
50
+ rendered.push_str(&format!("{key}: {value}\n"));
51
+ }
52
+ } else {
53
+ rendered.push_str(&yaml::dumps(&Value::Map(vec![(
54
+ key.clone(),
55
+ value.clone(),
56
+ )])));
57
+ }
58
+ }
59
+ rendered
60
+ }
61
+
62
+ fn is_plain_frontmatter_scalar(value: &str) -> bool {
63
+ !value.is_empty()
64
+ && value.trim() == value
65
+ && value
66
+ .chars()
67
+ .any(|character| character.is_ascii_alphabetic() || character == '_')
68
+ && value.chars().all(|character| {
69
+ character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.' | ' ' | '/')
70
+ })
71
+ && !matches!(
72
+ value.to_ascii_lowercase().as_str(),
73
+ "true" | "false" | "null" | "yes" | "no" | "on" | "off"
74
+ )
75
+ }
76
+
77
+ fn strip_label_quotes(mut label: &str) -> &str {
78
+ loop {
79
+ let bytes = label.as_bytes();
80
+ if bytes.len() >= 2
81
+ && matches!(bytes[0], b'\'' | b'"')
82
+ && bytes[0] == bytes[bytes.len() - 1]
83
+ {
84
+ label = &label[1..label.len() - 1];
85
+ } else {
86
+ return label;
87
+ }
88
+ }
89
+ }
90
+
91
+ pub(crate) struct MaterializedRole {
37
92
  path: PathBuf,
93
+ device: u64,
94
+ inode: u64,
38
95
  keep: bool,
39
96
  }
40
97
 
41
98
  impl MaterializedRole {
42
- /// ---
43
- /// purpose: 取物化出来的角色文件路径
44
- /// returns: 落盘路径
45
- /// ---
46
- pub(super) fn path(&self) -> &Path {
99
+ /// ---
100
+ /// purpose: 取物化出来的角色文件路径
101
+ /// returns: 落盘路径
102
+ /// ---
103
+ pub(crate) fn path(&self) -> &Path {
47
104
  &self.path
48
105
  }
49
106
 
50
- /// ---
51
- /// purpose: 标记该文件由调用方接管,Drop 时不再删除
52
- /// ---
53
- pub(super) fn keep(&mut self) {
107
+ /// ---
108
+ /// purpose: 标记该文件由调用方接管,Drop 时不再删除
109
+ /// ---
110
+ pub(crate) fn keep(&mut self) {
54
111
  self.keep = true;
55
112
  }
56
113
  }
57
114
 
58
115
  impl Drop for MaterializedRole {
59
116
  fn drop(&mut self) {
60
- if !self.keep {
117
+ if !self.keep
118
+ && std::fs::symlink_metadata(&self.path).is_ok_and(|metadata| {
119
+ metadata.file_type().is_file()
120
+ && metadata.dev() == self.device
121
+ && metadata.ino() == self.inode
122
+ })
123
+ {
61
124
  let _ = std::fs::remove_file(&self.path);
62
125
  }
63
126
  }
@@ -69,16 +132,18 @@ impl Drop for MaterializedRole {
69
132
  /// state: 用于找源席的 dynamic_role_file,找不到时退到 team 目录下的同名 md
70
133
  /// as_agent_id: 新席位名,写进 front matter 的 name
71
134
  /// label: 非空时覆盖 front matter 的 role
135
+ /// agent_label: 非空时写入 front matter 的 label
72
136
  /// returns: 物化结果,未调用 keep 时 Drop 会删掉该文件
73
137
  /// errors: 源文件缺失、未声明 name 或声明与源席不符时返回 Compile;目标已存在返回 RequirementUnmet;建目录或写盘失败返回 StatePersist
74
138
  /// ---
75
- pub(super) fn materialize_latest_role(
139
+ pub(crate) fn materialize_latest_role(
76
140
  run_workspace: &Path,
77
141
  team_dir: &Path,
78
142
  state: &serde_json::Value,
79
143
  source_agent_id: &AgentId,
80
144
  as_agent_id: &AgentId,
81
145
  label: Option<&str>,
146
+ agent_label: Option<&str>,
82
147
  ) -> Result<MaterializedRole, LifecycleError> {
83
148
  let source_path = resolve_role_source(run_workspace, team_dir, state, source_agent_id)?;
84
149
  let (mut meta, body) = crate::compiler::read_front_matter(&source_path)
@@ -104,32 +169,103 @@ pub(super) fn materialize_latest_role(
104
169
  "name",
105
170
  Value::Str(as_agent_id.as_str().to_string()),
106
171
  )?;
107
- if let Some(label) = label.filter(|value| !value.is_empty()) {
172
+ if let Some(label) = label
173
+ .map(strip_label_quotes)
174
+ .filter(|value| !value.is_empty())
175
+ {
108
176
  set_yaml_map_value(&mut meta, "role", Value::Str(label.to_string()))?;
109
177
  }
178
+ if let Some(agent_label) = agent_label
179
+ .map(strip_label_quotes)
180
+ .filter(|value| !value.is_empty())
181
+ {
182
+ set_yaml_map_value(&mut meta, "label", Value::Str(agent_label.to_string()))?;
183
+ }
184
+ if let Some(role) = meta.get("role").and_then(Value::as_str).map(str::to_string) {
185
+ let role_without_quotes = strip_label_quotes(&role);
186
+ if !role_without_quotes.is_empty() && role_without_quotes != role {
187
+ set_yaml_map_value(
188
+ &mut meta,
189
+ "role",
190
+ Value::Str(role_without_quotes.to_string()),
191
+ )?;
192
+ }
193
+ }
110
194
 
111
195
  let managed_dir = run_workspace.join(".team").join("dynamic-role-files");
112
196
  std::fs::create_dir_all(&managed_dir)
113
197
  .map_err(|error| LifecycleError::StatePersist(error.to_string()))?;
114
198
  let path = managed_dir.join(format!("{}.md", as_agent_id.as_str()));
115
- if path.exists() {
116
- return Err(LifecycleError::RequirementUnmet(format!(
117
- "managed role file already exists: {}",
118
- path.display()
119
- )));
199
+ match std::fs::symlink_metadata(&path) {
200
+ Ok(_) => {
201
+ return Err(LifecycleError::RequirementUnmet(format!(
202
+ "managed role file already exists: {}",
203
+ path.display()
204
+ )))
205
+ }
206
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
207
+ Err(error) => return Err(LifecycleError::StatePersist(error.to_string())),
120
208
  }
121
- let rendered = format!("---\n{}---\n\n{}", yaml::dumps(&meta), body);
122
- let temp = path.with_extension(format!("md.tmp-{}", std::process::id()));
123
- std::fs::write(&temp, rendered.as_bytes())
209
+ let rendered = format!("---\n{}---\n\n{}", dump_role_frontmatter(&meta), body);
210
+ let nonce = SystemTime::now()
211
+ .duration_since(UNIX_EPOCH)
212
+ .map_err(|error| LifecycleError::StatePersist(error.to_string()))?
213
+ .as_nanos();
214
+ let temp = managed_dir.join(format!(
215
+ ".{}.md.tmp-{}-{nonce}",
216
+ as_agent_id.as_str(),
217
+ std::process::id()
218
+ ));
219
+ let mut file = OpenOptions::new()
220
+ .write(true)
221
+ .create_new(true)
222
+ .open(&temp)
124
223
  .map_err(|error| LifecycleError::StatePersist(error.to_string()))?;
125
- if let Err(error) = std::fs::rename(&temp, &path) {
224
+ if let Err(error) = file
225
+ .write_all(rendered.as_bytes())
226
+ .and_then(|_| file.sync_all())
227
+ {
228
+ drop(file);
229
+ let _ = std::fs::remove_file(&temp);
230
+ return Err(LifecycleError::StatePersist(error.to_string()));
231
+ }
232
+ let metadata = match file.metadata() {
233
+ Ok(metadata) => metadata,
234
+ Err(error) => {
235
+ drop(file);
236
+ let _ = std::fs::remove_file(&temp);
237
+ return Err(LifecycleError::StatePersist(error.to_string()));
238
+ }
239
+ };
240
+ let (device, inode) = (metadata.dev(), metadata.ino());
241
+ drop(file);
242
+ if let Err(error) = std::fs::hard_link(&temp, &path) {
126
243
  let _ = std::fs::remove_file(&temp);
244
+ if error.kind() == std::io::ErrorKind::AlreadyExists {
245
+ return Err(LifecycleError::RequirementUnmet(format!(
246
+ "managed role file already exists: {}",
247
+ path.display()
248
+ )));
249
+ }
250
+ return Err(LifecycleError::StatePersist(error.to_string()));
251
+ }
252
+ if let Err(error) = std::fs::remove_file(&temp) {
253
+ if std::fs::symlink_metadata(&path).is_ok_and(|metadata| {
254
+ metadata.file_type().is_file() && metadata.dev() == device && metadata.ino() == inode
255
+ }) {
256
+ let _ = std::fs::remove_file(&path);
257
+ }
127
258
  return Err(LifecycleError::StatePersist(error.to_string()));
128
259
  }
129
- Ok(MaterializedRole { path, keep: false })
260
+ Ok(MaterializedRole {
261
+ path,
262
+ device,
263
+ inode,
264
+ keep: false,
265
+ })
130
266
  }
131
267
 
132
- fn resolve_role_source(
268
+ pub(crate) fn resolve_role_source(
133
269
  run_workspace: &Path,
134
270
  team_dir: &Path,
135
271
  state: &serde_json::Value,
@@ -167,7 +167,7 @@ pub(super) fn spawn_agents(
167
167
  model: command_model,
168
168
  effort: agent_effort,
169
169
  system_prompt: &system_prompt,
170
- team_mcp_tools: &["send_message", "report_result"],
170
+ team_mcp_tools: &["send_message", "report_result", "get_team_status"],
171
171
  mcp_config: &mcp_config,
172
172
  session_scope: crate::lifecycle::launch::pi_mcp::PiSessionScope::Isolated,
173
173
  },
@@ -23,6 +23,15 @@ use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
23
23
 
24
24
  use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
25
25
 
26
+ #[derive(Clone, Debug)]
27
+ pub(crate) struct ForkCaptureSeed {
28
+ pub source_agent_id: AgentId,
29
+ pub captured: crate::provider::CapturedSession,
30
+ pub captured_at: String,
31
+ pub pi_sessions_root: PathBuf,
32
+ pub profile_dir: PathBuf,
33
+ }
34
+
26
35
  use super::*;
27
36
 
28
37
  // ── lifecycle::launch —— 冷启 / quick-start / 危险审批探测 ──────────────────
@@ -395,8 +404,7 @@ pub(super) use add_agent::*;
395
404
  pub use add_agent::{add_agent, add_agent_force};
396
405
  pub(crate) use add_agent::{add_agent_with_transport, add_agent_with_transport_force};
397
406
 
398
- mod add_agent_state;
399
- pub(crate) use add_agent_state::inject_agent_into_spec;
407
+ pub(crate) mod add_agent_state;
400
408
  pub(super) use add_agent_state::*;
401
409
 
402
410
  mod fork_agent;
@@ -406,7 +414,7 @@ pub use fork_agent::{fork_agent_with_transport, in_window_fork, in_window_fork_c
406
414
  mod fork_entry;
407
415
  pub use fork_entry::fork_agent;
408
416
 
409
- mod role_source;
417
+ pub(crate) mod role_source;
410
418
  pub(super) use role_source::*;
411
419
 
412
420
  mod clone_agent;
@@ -217,7 +217,13 @@ fn prepare_profile_launch(
217
217
  let Some(profile) = agent.profile.as_deref() else {
218
218
  return Ok(ProviderProfileLaunch::default());
219
219
  };
220
- let loaded = load_profile(workspace, profile, agent.profile_dir.as_deref())?;
220
+ let Some(path) = find_profile_file(workspace, profile, agent.profile_dir.as_deref()) else {
221
+ if agent.provider == Provider::Pi && agent.auth_mode == AuthMode::Subscription {
222
+ return Ok(ProviderProfileLaunch::default());
223
+ }
224
+ return Err(profile_not_found(profile));
225
+ };
226
+ let loaded = read_profile_file(path)?;
221
227
  validate_profile(&agent, profile, &loaded)?;
222
228
 
223
229
  let mut env_overlay = provider_env_exports(agent.provider, agent.auth_mode, &loaded.values);
@@ -290,9 +296,14 @@ pub(crate) fn load_profile(
290
296
  name: &str,
291
297
  profile_dir: Option<&Path>,
292
298
  ) -> Result<ProfileValues, LifecycleError> {
299
+ let path =
300
+ find_profile_file(workspace, name, profile_dir).ok_or_else(|| profile_not_found(name))?;
301
+ read_profile_file(path)
302
+ }
303
+
304
+ fn find_profile_file(workspace: &Path, name: &str, profile_dir: Option<&Path>) -> Option<PathBuf> {
293
305
  let dirs = profile_lookup_dirs(workspace, profile_dir);
294
- let path = dirs
295
- .iter()
306
+ dirs.iter()
296
307
  .map(|directory| directory.join(format!("{name}.env")))
297
308
  .find(|path| path.exists())
298
309
  .or_else(|| {
@@ -300,11 +311,15 @@ pub(crate) fn load_profile(
300
311
  .map(|directory| directory.join(format!("{name}.example.env")))
301
312
  .find(|path| path.exists())
302
313
  })
303
- .ok_or_else(|| {
304
- LifecycleError::RequirementUnmet(format!(
305
- "profile {name} not found; run team-agent profile init {name} --auth-mode subscription"
306
- ))
307
- })?;
314
+ }
315
+
316
+ fn profile_not_found(name: &str) -> LifecycleError {
317
+ LifecycleError::RequirementUnmet(format!(
318
+ "profile {name} not found; run team-agent profile init {name} --auth-mode subscription"
319
+ ))
320
+ }
321
+
322
+ fn read_profile_file(path: PathBuf) -> Result<ProfileValues, LifecycleError> {
308
323
  let text = std::fs::read_to_string(&path)
309
324
  .map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", path.display())))?;
310
325
  Ok(ProfileValues {
@@ -546,7 +546,7 @@ fn replay_worker_target_missing_messages(
546
546
  Ok(())
547
547
  }
548
548
 
549
- fn verify_spawned_pane_matches_target(
549
+ pub(super) fn verify_spawned_pane_matches_target(
550
550
  transport: &dyn crate::transport::Transport,
551
551
  pane: &crate::transport::PaneId,
552
552
  session: &crate::transport::SessionName,
@@ -1244,7 +1244,7 @@ pub(super) fn resolve_team_scoped_state_or_refuse(
1244
1244
  })
1245
1245
  }
1246
1246
 
1247
- fn mark_agent_started(
1247
+ pub(super) fn mark_agent_started(
1248
1248
  state: &mut serde_json::Value,
1249
1249
  agent_id: &AgentId,
1250
1250
  window: &str,