@team-agent/installer 0.4.11 → 0.5.0

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 (72) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -0
  4. package/crates/team-agent/src/cli/diagnose.rs +2 -15
  5. package/crates/team-agent/src/cli/emit.rs +62 -4
  6. package/crates/team-agent/src/compiler.rs +11 -20
  7. package/crates/team-agent/src/coordinator/orphan.rs +2 -2
  8. package/crates/team-agent/src/coordinator/runtime_detectors.rs +5 -4
  9. package/crates/team-agent/src/coordinator/steps/abnormal.rs +1776 -2
  10. package/crates/team-agent/src/coordinator/steps/mod.rs +19 -0
  11. package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +78 -7
  12. package/crates/team-agent/src/coordinator/tests/abnormal.rs +195 -0
  13. package/crates/team-agent/src/coordinator/tick.rs +31 -932
  14. package/crates/team-agent/src/diagnose/orphans.rs +66 -8
  15. package/crates/team-agent/src/layout/worker_window_helpers.rs +5 -7
  16. package/crates/team-agent/src/leader/provider_attribution.rs +7 -5
  17. package/crates/team-agent/src/leader/rediscover.rs +1 -11
  18. package/crates/team-agent/src/lifecycle/launch/plan.rs +19 -8
  19. package/crates/team-agent/src/lifecycle/launch.rs +135 -58
  20. package/crates/team-agent/src/lifecycle/lock.rs +302 -0
  21. package/crates/team-agent/src/lifecycle/mod.rs +1 -0
  22. package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -11
  23. package/crates/team-agent/src/lifecycle/restart/agent.rs +169 -113
  24. package/crates/team-agent/src/lifecycle/restart/common.rs +108 -17
  25. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -32
  26. package/crates/team-agent/src/lifecycle/restart/remove.rs +16 -2
  27. package/crates/team-agent/src/lifecycle/restart/selection.rs +2 -1
  28. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +6 -0
  29. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +3 -3
  30. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +321 -0
  31. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +35 -5
  32. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +802 -0
  33. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +648 -0
  34. package/crates/team-agent/src/lifecycle/tests.rs +3 -0
  35. package/crates/team-agent/src/lifecycle/types.rs +8 -0
  36. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +24 -9
  37. package/crates/team-agent/src/mcp_server/tools.rs +157 -68
  38. package/crates/team-agent/src/messaging/activity.rs +43 -18
  39. package/crates/team-agent/src/messaging/delivery.rs +161 -97
  40. package/crates/team-agent/src/messaging/helpers.rs +1 -0
  41. package/crates/team-agent/src/messaging/leader_receiver.rs +22 -1
  42. package/crates/team-agent/src/messaging/results.rs +34 -9
  43. package/crates/team-agent/src/messaging/scheduler.rs +52 -9
  44. package/crates/team-agent/src/model/spec.rs +10 -6
  45. package/crates/team-agent/src/provider/adapter.rs +10 -1038
  46. package/crates/team-agent/src/provider/adapters/claude.rs +3 -8
  47. package/crates/team-agent/src/provider/adapters/copilot.rs +2 -5
  48. package/crates/team-agent/src/provider/classify.rs +23 -48
  49. package/crates/team-agent/src/provider/command.rs +16 -7
  50. package/crates/team-agent/src/provider/faults.rs +93 -53
  51. package/crates/team-agent/src/provider/session/capture.rs +4 -15
  52. package/crates/team-agent/src/provider/session_scan/claude.rs +309 -0
  53. package/crates/team-agent/src/provider/session_scan/codex.rs +40 -0
  54. package/crates/team-agent/src/provider/session_scan/common.rs +350 -0
  55. package/crates/team-agent/src/provider/session_scan/copilot.rs +202 -0
  56. package/crates/team-agent/src/provider/session_scan.rs +65 -27
  57. package/crates/team-agent/src/provider/tests/faults.rs +80 -0
  58. package/crates/team-agent/src/provider/types.rs +33 -1
  59. package/crates/team-agent/src/provider/wire.rs +171 -1
  60. package/crates/team-agent/src/state/identity.rs +242 -57
  61. package/crates/team-agent/src/state/identity_keys.rs +9 -12
  62. package/crates/team-agent/src/state/mod.rs +5 -1
  63. package/crates/team-agent/src/state/owner_gate.rs +59 -15
  64. package/crates/team-agent/src/state/ownership.rs +12 -11
  65. package/crates/team-agent/src/state/paths.rs +13 -6
  66. package/crates/team-agent/src/state/persist.rs +671 -128
  67. package/crates/team-agent/src/state/projection.rs +240 -49
  68. package/crates/team-agent/src/state/selector.rs +11 -5
  69. package/crates/team-agent/src/tmux_backend/tests.rs +51 -2
  70. package/crates/team-agent/src/tmux_backend.rs +42 -5
  71. package/crates/team-agent/src/transport/test_support.rs +55 -6
  72. package/package.json +4 -4
@@ -503,20 +503,43 @@ fn is_provider_or_mcp_workspace_command(command: &str) -> bool {
503
503
  }
504
504
 
505
505
  fn parse_workspace_arg(command: &str) -> Option<PathBuf> {
506
- let mut parts = command.split_whitespace().peekable();
507
- while let Some(part) = parts.next() {
508
- if let Some(value) = part.strip_prefix("--workspace=") {
509
- if !value.is_empty() {
510
- return Some(PathBuf::from(value));
511
- }
506
+ for (start, _) in command.match_indices("--workspace") {
507
+ let boundary_before = start == 0
508
+ || command
509
+ .get(..start)
510
+ .and_then(|prefix| prefix.chars().next_back())
511
+ .is_some_and(char::is_whitespace);
512
+ if !boundary_before {
513
+ continue;
514
+ }
515
+ let after = &command[start + "--workspace".len()..];
516
+ if let Some(value) = after.strip_prefix('=') {
517
+ return parse_workspace_value(value);
512
518
  }
513
- if part == "--workspace" {
514
- return parts.peek().map(PathBuf::from);
519
+ if after.chars().next().is_some_and(char::is_whitespace) {
520
+ return parse_workspace_value(after);
515
521
  }
516
522
  }
517
523
  None
518
524
  }
519
525
 
526
+ fn parse_workspace_value(value: &str) -> Option<PathBuf> {
527
+ let value = value.trim_start();
528
+ if value.is_empty() {
529
+ return None;
530
+ }
531
+ let value = value
532
+ .split_once(" --")
533
+ .map(|(workspace, _)| workspace)
534
+ .unwrap_or(value)
535
+ .trim_end();
536
+ if value.is_empty() {
537
+ None
538
+ } else {
539
+ Some(PathBuf::from(value))
540
+ }
541
+ }
542
+
520
543
  fn classify_workspace_orphan(workspace: &Path, pid: Pid) -> Option<OrphanReason> {
521
544
  if !workspace.is_absolute() {
522
545
  return None;
@@ -656,3 +679,38 @@ fn reason_key(reason: &OrphanReason) -> &'static str {
656
679
  OrphanReason::PidNotRunning => "pid_not_running",
657
680
  }
658
681
  }
682
+
683
+ #[cfg(test)]
684
+ mod tests {
685
+ use super::*;
686
+
687
+ #[test]
688
+ fn parse_workspace_arg_keeps_trailing_path_with_spaces() {
689
+ let command = "/tmp/team-agent coordinator --workspace /Volumes/projects/My Agent";
690
+
691
+ assert_eq!(
692
+ parse_workspace_arg(command),
693
+ Some(PathBuf::from("/Volumes/projects/My Agent"))
694
+ );
695
+ }
696
+
697
+ #[test]
698
+ fn parse_workspace_arg_stops_at_next_flag() {
699
+ let command = "/tmp/team-agent coordinator --workspace /tmp/My Agent --once";
700
+
701
+ assert_eq!(
702
+ parse_workspace_arg(command),
703
+ Some(PathBuf::from("/tmp/My Agent"))
704
+ );
705
+ }
706
+
707
+ #[test]
708
+ fn parse_workspace_arg_supports_equals_form_with_spaces() {
709
+ let command = "/tmp/team-agent coordinator --workspace=/tmp/My Agent --once";
710
+
711
+ assert_eq!(
712
+ parse_workspace_arg(command),
713
+ Some(PathBuf::from("/tmp/My Agent"))
714
+ );
715
+ }
716
+ }
@@ -1,14 +1,12 @@
1
- //! 0.3.28 layout step 2 — small provider-wire helper for layout-layer window naming.
1
+ //! 0.3.28 layout step 2 — small window grammar helper for layout-layer naming.
2
2
  //!
3
- //! Mirrors `leader::helpers::provider_wire` so the layout module does not
4
- //! depend on the leader module's private helpers. Single source of truth
5
- //! is `provider_command_name` from `crate::provider::adapter` (the
6
- //! authoritative provider→string mapping).
3
+ //! Command/UI grammar, not provider identity parsing: leader window names
4
+ //! intentionally collapse `claude_code` to `claude` and use executable-style
5
+ //! labels such as `gemini`.
7
6
 
8
7
  use crate::model::enums::Provider;
9
8
 
10
- /// Window name used inside the leader session, derived from the provider's
11
- /// wire identifier (e.g. `claude`, `codex`, `copilot`, `fake`). Mirrors
9
+ /// Window name used inside the leader session. Mirrors
12
10
  /// Python's `leader_window_name = provider` (see
13
11
  /// `runtime/0.2.11/src/team_agent/leader/__init__.py:114-131`).
14
12
  pub fn provider_window_name(provider: Provider) -> &'static str {
@@ -8,7 +8,9 @@ use crate::transport::PaneInfo;
8
8
 
9
9
  use super::helpers::{parse_provider, provider_wire};
10
10
 
11
- const PROVIDER_COMMANDS: &[(&str, Provider)] = &[
11
+ // Command grammar, not provider identity parsing: these are process argv/current-command
12
+ // needles observed in tmux panes.
13
+ const COMMAND_GRAMMAR_PROVIDER_COMMANDS: &[(&str, Provider)] = &[
12
14
  ("claude", Provider::ClaudeCode),
13
15
  ("claude.exe", Provider::ClaudeCode),
14
16
  ("codex", Provider::Codex),
@@ -37,7 +39,7 @@ fn attribute_pane_provider_with_process(
37
39
  }
38
40
 
39
41
  pub(crate) fn attribute_command_provider(command: &str) -> Option<Provider> {
40
- provider_from_text(command)
42
+ provider_from_command_text(command)
41
43
  }
42
44
 
43
45
  pub(crate) fn command_matches_provider(provider: Provider, command: &str) -> bool {
@@ -58,7 +60,7 @@ fn provider_from_env(env: &BTreeMap<String, String>) -> Option<Provider> {
58
60
  fn provider_from_pid_argv(pid: u32) -> Option<Provider> {
59
61
  process_command_line(pid)
60
62
  .as_deref()
61
- .and_then(provider_from_text)
63
+ .and_then(provider_from_command_text)
62
64
  }
63
65
 
64
66
  fn provider_from_pid_env(pid: u32) -> Option<Provider> {
@@ -67,9 +69,9 @@ fn provider_from_pid_env(pid: u32) -> Option<Provider> {
67
69
  .and_then(provider_from_env_text)
68
70
  }
69
71
 
70
- fn provider_from_text(text: &str) -> Option<Provider> {
72
+ fn provider_from_command_text(text: &str) -> Option<Provider> {
71
73
  let lower = text.to_ascii_lowercase();
72
- PROVIDER_COMMANDS
74
+ COMMAND_GRAMMAR_PROVIDER_COMMANDS
73
75
  .iter()
74
76
  .find_map(|(needle, provider)| lower.contains(needle).then_some(*provider))
75
77
  }
@@ -7,6 +7,7 @@ use serde_json::{json, Value};
7
7
 
8
8
  use crate::model::ids::{LeaderSessionUuid, OwnerEpoch};
9
9
  use crate::provider::Provider;
10
+ use crate::provider::wire::provider_wire;
10
11
  use crate::transport::{PaneId, PaneInfo, SessionName, Transport, WindowName};
11
12
 
12
13
  use super::helpers::{get_path_str, now_ts, parse_provider, prefix, resolve_workspace_for_hash, sha1_hex_prefix};
@@ -902,17 +903,6 @@ fn uuid_prefix(uuid: Option<&LeaderSessionUuid>) -> String {
902
903
  uuid.map_or_else(String::new, |value| prefix(value.as_str(), 8).to_string())
903
904
  }
904
905
 
905
- fn provider_wire(provider: Provider) -> &'static str {
906
- match provider {
907
- Provider::Claude => "claude",
908
- Provider::ClaudeCode => "claude_code",
909
- Provider::Codex => "codex",
910
- Provider::Copilot => "copilot",
911
- Provider::GeminiCli => "gemini_cli",
912
- Provider::Fake => "fake",
913
- }
914
- }
915
-
916
906
  fn event_provider(identity: &OwnerIdentity) -> &'static str {
917
907
  provider_wire(identity.provider.unwrap_or(Provider::Codex))
918
908
  }
@@ -15,9 +15,8 @@ pub fn start_plan(
15
15
  plan_path.display()
16
16
  )));
17
17
  }
18
- let text = std::fs::read_to_string(plan_path).map_err(|e| {
19
- LifecycleError::InvalidPlan(format!("{}: {e}", plan_path.display()))
20
- })?;
18
+ let text = std::fs::read_to_string(plan_path)
19
+ .map_err(|e| LifecycleError::InvalidPlan(format!("{}: {e}", plan_path.display())))?;
21
20
  let plan = yaml::loads(&text).map_err(|e| LifecycleError::InvalidPlan(e.to_string()))?;
22
21
  let plan_id = plan_id_from_plan(plan_path, &plan)?;
23
22
  let state_path = plan_state_path(workspace, &plan_id);
@@ -66,7 +65,8 @@ pub fn handle_report_result(
66
65
  return Ok(PlanProgress::NoMatch);
67
66
  };
68
67
  for entry in entries {
69
- let entry = entry.map_err(|e| LifecycleError::InvalidPlan(format!("read plan dir: {e}")))?;
68
+ let entry =
69
+ entry.map_err(|e| LifecycleError::InvalidPlan(format!("read plan dir: {e}")))?;
70
70
  let path = entry.path();
71
71
  if path.extension().and_then(|s| s.to_str()) != Some("json") {
72
72
  continue;
@@ -112,10 +112,19 @@ fn plan_stages(plan: &Value) -> Result<Vec<PlanStage>, LifecycleError> {
112
112
  .transpose()?;
113
113
  stages.push(PlanStage {
114
114
  id,
115
- assignee: raw.get("assignee").and_then(Value::as_str).map(AgentId::new),
116
- prompt: raw.get("prompt").and_then(Value::as_str).map(str::to_string),
115
+ assignee: raw
116
+ .get("assignee")
117
+ .and_then(Value::as_str)
118
+ .map(AgentId::new),
119
+ prompt: raw
120
+ .get("prompt")
121
+ .and_then(Value::as_str)
122
+ .map(str::to_string),
117
123
  on_result: condition,
118
- status: raw.get("status").and_then(Value::as_str).map(str::to_string),
124
+ status: raw
125
+ .get("status")
126
+ .and_then(Value::as_str)
127
+ .map(str::to_string),
119
128
  });
120
129
  }
121
130
  Ok(stages)
@@ -222,6 +231,8 @@ fn json_scalar_to_string(value: &serde_json::Value) -> Option<String> {
222
231
  serde_json::Value::String(s) => Some(s.clone()),
223
232
  serde_json::Value::Bool(b) => Some(b.to_string()),
224
233
  serde_json::Value::Number(n) => Some(n.to_string()),
225
- serde_json::Value::Null | serde_json::Value::Array(_) | serde_json::Value::Object(_) => None,
234
+ serde_json::Value::Null | serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
235
+ None
236
+ }
226
237
  }
227
238
  }
@@ -11,6 +11,8 @@ use crate::model::yaml::{self, Value};
11
11
  use crate::state::persist::{load_runtime_state, save_runtime_state};
12
12
  use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
13
13
 
14
+ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
15
+
14
16
  use super::*;
15
17
 
16
18
  // ── lifecycle::launch —— 冷启 / quick-start / 危险审批探测 ──────────────────
@@ -124,7 +126,8 @@ pub fn launch_with_transport_in_workspace(
124
126
  // Step 10 once Steps 2–9 have eliminated structural co-location.
125
127
  if !dry_run {
126
128
  if let Ok(state_for_check) = crate::state::persist::load_runtime_state(workspace) {
127
- let violations = crate::layout::sessions::assert_topology_invariants(&state_for_check, &spec);
129
+ let violations =
130
+ crate::layout::sessions::assert_topology_invariants(&state_for_check, &spec);
128
131
  crate::layout::sessions::log_topology_violations(&violations);
129
132
  }
130
133
  }
@@ -247,9 +250,7 @@ fn spawn_agents(
247
250
  // unsupported warning event when the spec asked for an effort the
248
251
  // provider can't satisfy.
249
252
  let agent_effort = provider_effort_for_spawn(agent, provider);
250
- if let Some(event_value) =
251
- provider_effort_event_if_dropped(agent, provider, agent_id_raw)
252
- {
253
+ if let Some(event_value) = provider_effort_event_if_dropped(agent, provider, agent_id_raw) {
253
254
  let _ = crate::event_log::EventLog::new(workspace)
254
255
  .write("provider.effort_unsupported", event_value);
255
256
  }
@@ -279,7 +280,11 @@ fn spawn_agents(
279
280
  // 含 "model" 字面,失败信息透传给 leader。
280
281
  if matches!(provider, Provider::Copilot) && auth_mode == AuthMode::CompatibleApi {
281
282
  let has_model = model.is_some_and(|s| !s.is_empty())
282
- || profile_launch.command_overrides.model.as_deref().is_some_and(|s| !s.is_empty())
283
+ || profile_launch
284
+ .command_overrides
285
+ .model
286
+ .as_deref()
287
+ .is_some_and(|s| !s.is_empty())
283
288
  || profile_launch
284
289
  .env_overlay
285
290
  .get("COPILOT_MODEL")
@@ -307,9 +312,8 @@ fn spawn_agents(
307
312
  .join("logs")
308
313
  .join("copilot")
309
314
  .join(agent_id_raw);
310
- std::fs::create_dir_all(&log_dir).map_err(|e| {
311
- LifecycleError::StatePersist(format!("{}: {e}", log_dir.display()))
312
- })?;
315
+ std::fs::create_dir_all(&log_dir)
316
+ .map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", log_dir.display())))?;
313
317
  plan.argv.push("--log-dir".to_string());
314
318
  plan.argv.push(log_dir.to_string_lossy().to_string());
315
319
  plan.argv.push("--log-level".to_string());
@@ -606,9 +610,7 @@ pub(crate) fn adaptive_placement_for_agent(
606
610
  if !is_adaptive_layout_window(window) {
607
611
  continue;
608
612
  }
609
- let pane_id = agent
610
- .get("pane_id")
611
- .and_then(serde_json::Value::as_str);
613
+ let pane_id = agent.get("pane_id").and_then(serde_json::Value::as_str);
612
614
  let pane_live = pane_id.is_some_and(|pane| live_panes.contains(pane));
613
615
  // E43 Fix A: window_name match — when the agent's pane is live,
614
616
  // its live window_name MUST equal the claimed `window`; otherwise
@@ -728,7 +730,9 @@ pub(crate) fn adaptive_existing_placement_for_agent(
728
730
  .unwrap_or_default()
729
731
  .into_iter()
730
732
  .filter(|pane| {
731
- pane.window_name.as_ref().is_some_and(|name| name.as_str() == window)
733
+ pane.window_name
734
+ .as_ref()
735
+ .is_some_and(|name| name.as_str() == window)
732
736
  && agent
733
737
  .get("pane_id")
734
738
  .and_then(serde_json::Value::as_str)
@@ -1328,7 +1332,10 @@ fn warn_ignored_owner_team_id(workspace: &Path, team_dir: &Path, runtime_team_ke
1328
1332
  let Ok(Some(ignored)) = crate::compiler::ignored_owner_team_id_from_team_md(team_dir) else {
1329
1333
  return;
1330
1334
  };
1331
- eprintln!("Warning: ignored TEAM.md {}={}", ignored.field, ignored.value);
1335
+ eprintln!(
1336
+ "Warning: ignored TEAM.md {}={}",
1337
+ ignored.field, ignored.value
1338
+ );
1332
1339
  eprintln!("Reason: owner identity is the canonical runtime team key ({runtime_team_key}), not TEAM.md front matter");
1333
1340
  eprintln!("Action: remove {} from TEAM.md", ignored.field);
1334
1341
  if let Err(err) = crate::event_log::EventLog::new(workspace).write(
@@ -1382,7 +1389,9 @@ fn active_leader_pane_state_across_tmux_sockets(
1382
1389
  .map(|endpoint| crate::tmux_backend::TmuxBackend::for_tmux_endpoint(endpoint))
1383
1390
  .collect::<Vec<_>>();
1384
1391
  active_leader_pane_state_across_transports(
1385
- transports.iter().map(|transport| transport as &dyn Transport),
1392
+ transports
1393
+ .iter()
1394
+ .map(|transport| transport as &dyn Transport),
1386
1395
  pane,
1387
1396
  )
1388
1397
  }
@@ -2085,12 +2094,7 @@ pub(crate) fn inherited_env_with_team_overrides(
2085
2094
  // * Strips `COPILOT_DISABLE_TERMINAL_TITLE` (re-injected per-agent by
2086
2095
  // `apply_copilot_instructions_overlay` based on the WORKER's provider).
2087
2096
  // * Strips `TMUX` / `TMUX_PANE` (would attach worker to leader's pane).
2088
- crate::layout::worker_env::worker_spawn_env(
2089
- std::env::vars(),
2090
- workspace,
2091
- agent_id,
2092
- team_id,
2093
- )
2097
+ crate::layout::worker_env::worker_spawn_env(std::env::vars(), workspace, agent_id, team_id)
2094
2098
  }
2095
2099
 
2096
2100
  pub(crate) fn apply_mcp_auto_approval_env(
@@ -2112,15 +2116,30 @@ pub(crate) fn apply_mcp_auto_approval_env(
2112
2116
  && safety.inherited
2113
2117
  {
2114
2118
  env.insert("TEAM_AGENT_LEADER_BYPASS".to_string(), "1".to_string());
2115
- env.insert("TEAM_AGENT_LEADER_BYPASS_SOURCE".to_string(), "leader_process".to_string());
2119
+ env.insert(
2120
+ "TEAM_AGENT_LEADER_BYPASS_SOURCE".to_string(),
2121
+ "leader_process".to_string(),
2122
+ );
2116
2123
  if let Some(provider) = safety.provider.as_deref() {
2117
- env.insert("TEAM_AGENT_LEADER_BYPASS_PROVIDER".to_string(), provider.to_string());
2124
+ env.insert(
2125
+ "TEAM_AGENT_LEADER_BYPASS_PROVIDER".to_string(),
2126
+ provider.to_string(),
2127
+ );
2118
2128
  }
2119
2129
  if let Some(flag) = safety.flag.as_deref() {
2120
- env.insert("TEAM_AGENT_LEADER_BYPASS_FLAG".to_string(), flag.to_string());
2130
+ env.insert(
2131
+ "TEAM_AGENT_LEADER_BYPASS_FLAG".to_string(),
2132
+ flag.to_string(),
2133
+ );
2121
2134
  }
2122
- env.insert("TEAM_AGENT_MCP_AUTO_APPROVE".to_string(), "team_orchestrator".to_string());
2123
- env.insert("TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE".to_string(), "leader_bypass".to_string());
2135
+ env.insert(
2136
+ "TEAM_AGENT_MCP_AUTO_APPROVE".to_string(),
2137
+ "team_orchestrator".to_string(),
2138
+ );
2139
+ env.insert(
2140
+ "TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE".to_string(),
2141
+ "leader_bypass".to_string(),
2142
+ );
2124
2143
  } else {
2125
2144
  env.insert("TEAM_AGENT_LEADER_BYPASS".to_string(), "0".to_string());
2126
2145
  }
@@ -2163,7 +2182,10 @@ pub(crate) fn apply_copilot_instructions_overlay(
2163
2182
  // 漏关后果定级为【B5 leader 误杀同级 incident】,绝不允许 silent 跳过。
2164
2183
  // 主案:env `COPILOT_DISABLE_TERMINAL_TITLE=1`(help-config 原文 "Can also be
2165
2184
  // disabled via the COPILOT_DISABLE_TERMINAL_TITLE environment variable")。
2166
- env.insert("COPILOT_DISABLE_TERMINAL_TITLE".to_string(), "1".to_string());
2185
+ env.insert(
2186
+ "COPILOT_DISABLE_TERMINAL_TITLE".to_string(),
2187
+ "1".to_string(),
2188
+ );
2167
2189
  Ok(())
2168
2190
  }
2169
2191
 
@@ -2219,7 +2241,10 @@ fn apply_copilot_mcp_residual_disables(
2219
2241
  let stderr = String::from_utf8_lossy(&out.stderr).to_string();
2220
2242
  std::fs::write(
2221
2243
  &residual_path,
2222
- format!("copilot mcp list exit={:?} stderr={stderr}\n", out.status.code()),
2244
+ format!(
2245
+ "copilot mcp list exit={:?} stderr={stderr}\n",
2246
+ out.status.code()
2247
+ ),
2223
2248
  )
2224
2249
  .map_err(|e| {
2225
2250
  LifecycleError::StatePersist(format!("{}: {e}", residual_path.display()))
@@ -2578,7 +2603,10 @@ pub(crate) fn provider_effort_for_spawn_json(
2578
2603
  agent: &serde_json::Value,
2579
2604
  provider: Provider,
2580
2605
  ) -> Option<ProviderEffort> {
2581
- provider_effort_from_raw(agent.get("effort").and_then(serde_json::Value::as_str), provider)
2606
+ provider_effort_from_raw(
2607
+ agent.get("effort").and_then(serde_json::Value::as_str),
2608
+ provider,
2609
+ )
2582
2610
  }
2583
2611
 
2584
2612
  pub(crate) fn provider_effort_event_if_dropped_json(
@@ -2797,14 +2825,7 @@ pub fn quick_start_in_workspace(
2797
2825
  ) -> Result<QuickStartReport, LifecycleError> {
2798
2826
  let workspace = explicit_quick_start_workspace(workspace);
2799
2827
  let transport = quick_start_tmux_backend(&workspace);
2800
- quick_start_with_transport_in_workspace(
2801
- &workspace,
2802
- agents_dir,
2803
- name,
2804
- yes,
2805
- team_id,
2806
- &transport,
2807
- )
2828
+ quick_start_with_transport_in_workspace(&workspace, agents_dir, name, yes, team_id, &transport)
2808
2829
  }
2809
2830
 
2810
2831
  pub fn quick_start_in_workspace_with_display(
@@ -2873,7 +2894,9 @@ pub(crate) fn configure_adaptive_pane_title(
2873
2894
  "warning": message,
2874
2895
  }),
2875
2896
  ) {
2876
- eprintln!("Warning: adaptive_layout.pane_title_failed event write failed: {event_error}");
2897
+ eprintln!(
2898
+ "Warning: adaptive_layout.pane_title_failed event write failed: {event_error}"
2899
+ );
2877
2900
  }
2878
2901
  }
2879
2902
  }
@@ -2900,9 +2923,7 @@ pub fn quick_start_with_transport(
2900
2923
  transport: &dyn Transport,
2901
2924
  ) -> Result<QuickStartReport, LifecycleError> {
2902
2925
  let workspace = team_workspace(agents_dir);
2903
- quick_start_with_transport_in_workspace(
2904
- &workspace, agents_dir, name, yes, team_id, transport,
2905
- )
2926
+ quick_start_with_transport_in_workspace(&workspace, agents_dir, name, yes, team_id, transport)
2906
2927
  }
2907
2928
 
2908
2929
  pub fn quick_start_with_transport_in_workspace(
@@ -3090,7 +3111,10 @@ pub fn quick_start_with_transport_in_workspace_with_display(
3090
3111
  let attach_windows = load_runtime_state(&workspace)
3091
3112
  .ok()
3092
3113
  .map(|state| {
3093
- attach_window_names_with_managed_leader(&state, started_attach_window_names(&launch.started))
3114
+ attach_window_names_with_managed_leader(
3115
+ &state,
3116
+ started_attach_window_names(&launch.started),
3117
+ )
3094
3118
  })
3095
3119
  .unwrap_or_else(|| started_attach_window_names(&launch.started));
3096
3120
  let attach_commands = attach_commands_for_runtime_windows(
@@ -3110,10 +3134,13 @@ pub fn quick_start_with_transport_in_workspace_with_display(
3110
3134
  next_actions.push(
3111
3135
  "quick-start initialized this team; for all subsequent starts use \
3112
3136
  `team-agent restart` to resume context (quick-start refuses on \
3113
- already-initialised teams)".to_string(),
3137
+ already-initialised teams)"
3138
+ .to_string(),
3114
3139
  );
3115
3140
  if crate::tmux_backend::socket_probe_missing_for_workspace(&workspace) {
3116
- next_actions.push(crate::tmux_backend::socket_missing_hint_for_workspace(&workspace));
3141
+ next_actions.push(crate::tmux_backend::socket_missing_hint_for_workspace(
3142
+ &workspace,
3143
+ ));
3117
3144
  }
3118
3145
  next_actions.extend(attach_commands.iter().cloned());
3119
3146
  let display_backend = state
@@ -3137,7 +3164,11 @@ pub fn quick_start_with_transport_in_workspace_with_display(
3137
3164
  /// the persisted endpoint synchronized with the transport they actually used,
3138
3165
  /// closing the silent socket-drift gap** (single state-save path; no parallel
3139
3166
  /// "annotate after spawn" race with coordinator).
3140
- pub(crate) fn annotate_runtime_tmux_endpoint(state: &mut serde_json::Value, transport: &dyn Transport, workspace: &Path) {
3167
+ pub(crate) fn annotate_runtime_tmux_endpoint(
3168
+ state: &mut serde_json::Value,
3169
+ transport: &dyn Transport,
3170
+ workspace: &Path,
3171
+ ) {
3141
3172
  let Some(endpoint) = transport.tmux_endpoint() else {
3142
3173
  return;
3143
3174
  };
@@ -3153,7 +3184,10 @@ pub(crate) fn annotate_runtime_tmux_endpoint(state: &mut serde_json::Value, tran
3153
3184
  .unwrap_or_else(|| endpoint.clone())
3154
3185
  };
3155
3186
  if let Some(obj) = state.as_object_mut() {
3156
- obj.insert("tmux_endpoint".to_string(), serde_json::json!(endpoint_for_state));
3187
+ obj.insert(
3188
+ "tmux_endpoint".to_string(),
3189
+ serde_json::json!(endpoint_for_state),
3190
+ );
3157
3191
  obj.insert(
3158
3192
  "tmux_socket".to_string(),
3159
3193
  obj.get("tmux_endpoint")
@@ -3430,6 +3464,7 @@ fn detect_dangerous_approval_in_argv(argv_tokens: &[String]) -> Option<Dangerous
3430
3464
  }
3431
3465
 
3432
3466
  fn dangerous_leader_flags() -> &'static [(&'static str, &'static str)] {
3467
+ // Process argv command grammar, not persisted provider identity parsing.
3433
3468
  &[
3434
3469
  ("claude", "--dangerously-skip-permissions"),
3435
3470
  ("claude", "--dangerously-skip-permission"),
@@ -3445,6 +3480,7 @@ fn dangerous_leader_flags() -> &'static [(&'static str, &'static str)] {
3445
3480
  }
3446
3481
 
3447
3482
  fn binary_matches_provider(provider: &str, binary: Option<&str>) -> bool {
3483
+ // Binary-name command grammar, not provider identity parsing.
3448
3484
  match (provider, binary) {
3449
3485
  ("codex", Some("codex")) => true,
3450
3486
  ("claude", Some("claude" | "claude-code" | "claude_code")) => true,
@@ -3607,10 +3643,11 @@ pub fn add_agent(
3607
3643
  // team uses. Cold workspaces / first-agent paths safely fall back to
3608
3644
  // `TmuxBackend::for_workspace(team_workspace)` inside the resolver.
3609
3645
  let team_ws = team_workspace(workspace);
3610
- let transport = crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
3611
- &team_ws, team,
3612
- )
3613
- .unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(&team_ws));
3646
+ let transport =
3647
+ crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
3648
+ &team_ws, team,
3649
+ )
3650
+ .unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(&team_ws));
3614
3651
  return add_agent_with_transport(
3615
3652
  workspace,
3616
3653
  agent_id,
@@ -3622,6 +3659,12 @@ pub fn add_agent(
3622
3659
  }
3623
3660
  Err(error) => return Err(LifecycleError::TeamSelect(error.to_string())),
3624
3661
  };
3662
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
3663
+ workspace: &selected.run_workspace,
3664
+ operation: "add-agent",
3665
+ team: Some(selected.team_key.as_str()),
3666
+ agent_id: Some(agent_id),
3667
+ })?;
3625
3668
  // E5 §3:compile_team 要角色定义目录(team_dir),不是 spec 落点(spec_workspace=runtime)。
3626
3669
  let team_dir = selected.team_dir;
3627
3670
  // **0.3.24 add-agent socket drift fix**: route to the live team's persisted
@@ -3658,6 +3701,12 @@ pub fn add_agent_with_transport(
3658
3701
  ) -> Result<AddAgentReport, LifecycleError> {
3659
3702
  let run_workspace = crate::model::paths::canonical_run_workspace(workspace)
3660
3703
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
3704
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
3705
+ workspace: &run_workspace,
3706
+ operation: "add-agent",
3707
+ team,
3708
+ agent_id: Some(agent_id),
3709
+ })?;
3661
3710
  add_agent_with_transport_at_paths(
3662
3711
  &run_workspace,
3663
3712
  workspace,
@@ -3950,7 +3999,11 @@ fn inject_agent_into_spec(
3950
3999
  agents.push(agent);
3951
4000
  }
3952
4001
  }
3953
- _ => return Err(LifecycleError::Compile("spec.agents missing or not a list".to_string())),
4002
+ _ => {
4003
+ return Err(LifecycleError::Compile(
4004
+ "spec.agents missing or not a list".to_string(),
4005
+ ))
4006
+ }
3954
4007
  }
3955
4008
  // routing.rules 追加 route-<id>(与 compile_team 同形)。
3956
4009
  if let Some((_, Value::Map(routing))) = pairs.iter_mut().find(|(k, _)| k == "routing") {
@@ -4062,12 +4115,27 @@ pub fn fork_agent_with_transport(
4062
4115
  crate::state::selector::SelectorMode::RequireSpec,
4063
4116
  )
4064
4117
  .map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
4118
+ let lock_workspace = selected.run_workspace.clone();
4119
+ let lock_team_key = selected.team_key.clone();
4120
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
4121
+ workspace: &lock_workspace,
4122
+ operation: "fork-agent",
4123
+ team: Some(lock_team_key.as_str()),
4124
+ agent_id: Some(as_agent_id),
4125
+ })?;
4126
+ let selected = crate::state::selector::resolve_active_team(
4127
+ &lock_workspace,
4128
+ Some(lock_team_key.as_str()),
4129
+ crate::state::selector::SelectorMode::RequireSpec,
4130
+ )
4131
+ .map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
4065
4132
  // E5 §3:team_dir(角色定义+profiles)恒用户目录。spec 读用 selector 解析的 spec_path
4066
4133
  // (读序 B:runtime 优先、legacy 回落),写恒走 runtime_spec_path(canonical 落点)。
4067
4134
  let fork_team_dir = selected.team_dir.clone();
4068
- let read_spec_path = selected.spec_path.clone().ok_or_else(|| {
4069
- LifecycleError::TeamSelect("active team spec not found".to_string())
4070
- })?;
4135
+ let read_spec_path = selected
4136
+ .spec_path
4137
+ .clone()
4138
+ .ok_or_else(|| LifecycleError::TeamSelect("active team spec not found".to_string()))?;
4071
4139
  let workspace = selected.run_workspace;
4072
4140
  let state = selected.state;
4073
4141
  ensure_owner_allowed_for_state(&state, Some(source_agent_id))?;
@@ -4141,8 +4209,8 @@ pub fn fork_agent_with_transport(
4141
4209
  }
4142
4210
  let new_spec = append_forked_agent(&spec, source_agent, source_agent_id, as_agent_id, label)?;
4143
4211
  // validate 用角色定义目录的 team_workspace(校验 working_directory),非 spec 落点。
4144
- let validate_ws = crate::model::paths::team_workspace(&fork_team_dir)
4145
- .unwrap_or_else(|_| workspace.clone());
4212
+ let validate_ws =
4213
+ crate::model::paths::team_workspace(&fork_team_dir).unwrap_or_else(|_| workspace.clone());
4146
4214
  crate::model::spec::validate_spec(&new_spec, &validate_ws)
4147
4215
  .map_err(|e| LifecycleError::Compile(e.to_string()))?;
4148
4216
  write_spec_atomic(&spec_path, &new_spec)?;
@@ -4322,7 +4390,11 @@ pub fn fork_agent_with_transport(
4322
4390
  );
4323
4391
  return Err(e);
4324
4392
  }
4325
- if let Err(e) = save_runtime_state(&workspace, &next_state) {
4393
+ if let Err(e) = crate::state::persist::save_runtime_state_with_lifecycle_topology_authority(
4394
+ &workspace,
4395
+ &next_state,
4396
+ &[as_agent_id.as_str()],
4397
+ ) {
4326
4398
  rollback_fork_after_spawn(
4327
4399
  &workspace,
4328
4400
  &spec_path,
@@ -4400,7 +4472,11 @@ fn rollback_fork_after_spawn(
4400
4472
  window: window.clone(),
4401
4473
  });
4402
4474
  let _ = std::fs::write(spec_path, spec_text.as_bytes());
4403
- let _ = save_runtime_state(workspace, old_state);
4475
+ let _ = crate::state::persist::save_runtime_state_with_deleted_agents(
4476
+ workspace,
4477
+ old_state,
4478
+ &[agent_id.as_str()],
4479
+ );
4404
4480
  cleanup_fork_mcp_artifacts(workspace, agent_id, mcp_config_path, profile_launch);
4405
4481
  }
4406
4482
 
@@ -4948,7 +5024,8 @@ pub(crate) fn override_spec_workspace(spec: &mut Value, workspace: &Path) {
4948
5024
  if let Some((_, Value::List(agents))) = root.iter_mut().find(|(k, _)| k == "agents") {
4949
5025
  for agent in agents {
4950
5026
  if let Value::Map(fields) = agent {
4951
- if let Some((_, value)) = fields.iter_mut().find(|(k, _)| k == "working_directory") {
5027
+ if let Some((_, value)) = fields.iter_mut().find(|(k, _)| k == "working_directory")
5028
+ {
4952
5029
  *value = Value::Str(workspace_s.clone());
4953
5030
  }
4954
5031
  }