@team-agent/installer 0.5.77 → 0.5.80

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 (95) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +161 -29
  4. package/crates/team-agent/src/cli/diagnose.rs +333 -68
  5. package/crates/team-agent/src/cli/emit.rs +24 -8
  6. package/crates/team-agent/src/cli/mod.rs +543 -98
  7. package/crates/team-agent/src/cli/models.rs +393 -32
  8. package/crates/team-agent/src/cli/named_address.rs +19 -5
  9. package/crates/team-agent/src/cli/profile.rs +35 -4
  10. package/crates/team-agent/src/cli/spec.rs +7 -2
  11. package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
  12. package/crates/team-agent/src/cli/status_port/runtime.rs +3 -1
  13. package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
  14. package/crates/team-agent/src/cli/status_port/store.rs +1 -1
  15. package/crates/team-agent/src/cli/status_port/tests.rs +37 -1
  16. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
  17. package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
  18. package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
  19. package/crates/team-agent/src/cli/types.rs +1 -0
  20. package/crates/team-agent/src/coordinator/steps/abnormal.rs +122 -2
  21. package/crates/team-agent/src/leader/lease.rs +492 -33
  22. package/crates/team-agent/src/leader/owner_bind.rs +40 -12
  23. package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
  24. package/crates/team-agent/src/leader/rediscover.rs +36 -6
  25. package/crates/team-agent/src/leader/start.rs +197 -88
  26. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
  27. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
  28. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
  29. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
  30. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
  31. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +86 -23
  32. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
  33. package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
  34. package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
  35. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
  36. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
  37. package/crates/team-agent/src/lifecycle/launch.rs +30 -7
  38. package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
  39. package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
  40. package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
  41. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
  42. package/crates/team-agent/src/lifecycle/restart.rs +2 -2
  43. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
  44. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
  45. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
  46. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
  47. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
  48. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
  49. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
  50. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
  51. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
  52. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
  53. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
  54. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_spaced_ids.stdout.txt +9 -0
  55. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
  56. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
  57. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
  58. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
  59. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
  60. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
  61. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
  62. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
  63. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
  64. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
  65. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +71 -0
  66. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
  67. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
  68. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
  69. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
  70. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
  71. package/crates/team-agent/src/lifecycle/types.rs +8 -0
  72. package/crates/team-agent/src/mcp_server/wire.rs +140 -11
  73. package/crates/team-agent/src/messaging/delivery.rs +221 -80
  74. package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
  75. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +127 -0
  76. package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
  77. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
  78. package/crates/team-agent/src/provider/session_scan/common/tests.rs +204 -0
  79. package/crates/team-agent/src/provider/session_scan/common.rs +24 -2
  80. package/crates/team-agent/src/state/abnormal_watch/tests/mod.rs +248 -0
  81. package/crates/team-agent/src/state/abnormal_watch.rs +144 -0
  82. package/crates/team-agent/src/state/mod.rs +1 -0
  83. package/crates/team-agent/src/state/persist.rs +102 -4
  84. package/crates/team-agent/src/state/projection.rs +7 -1
  85. package/crates/team-agent/src/state/repository.rs +29 -0
  86. package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
  87. package/crates/team-agent/src/tmux_backend/tests.rs +149 -4
  88. package/crates/team-agent/src/tmux_backend.rs +239 -127
  89. package/crates/team-agent/src/transport.rs +78 -0
  90. package/crates/team-agent/src/transport_factory.rs +53 -0
  91. package/package.json +4 -5
  92. package/skills/team-agent/SKILL.md +45 -17
  93. package/skills/team-agent/command-coverage.json +4 -29
  94. package/npm/check-skill-links.mjs +0 -88
  95. package/skills/team-agent/references/team-agent-operator.md +0 -672
@@ -70,6 +70,7 @@ mod common;
70
70
  mod orchestrator;
71
71
  pub mod preflight;
72
72
  mod rebuild;
73
+ pub use rebuild::{restart_bind_status, restart_leader_public_bind};
73
74
  pub(crate) mod remove;
74
75
  mod selection;
75
76
  mod team_state;
@@ -92,8 +93,7 @@ pub(crate) use rebuild::restart_with_transport_with_session_convergence_deadline
92
93
  // timer + worker timing writer so `lifecycle::launch::launch_with_transport_in_workspace`
93
94
  // can emit the same instrumentation event family with `source="launch"`.
94
95
  pub(crate) use rebuild::{
95
- provider_wire_from_state, restart_failure_action, write_worker_spawn_timing_event,
96
- RestartPhaseTimer,
96
+ provider_wire_from_state, write_worker_spawn_timing_event, RestartPhaseTimer,
97
97
  };
98
98
  pub use rebuild::{restart, restart_with_session_convergence_deadline};
99
99
  pub(crate) use rebuild::{
@@ -147,6 +147,9 @@ gate — N35/MUST-17); found {hits} occurrence(s)"
147
147
  #[test]
148
148
  #[serial(env)]
149
149
  fn b2_quick_start_ready_has_attach_commands_and_socket_miss_is_observable() {
150
+ let hermetic = hermetic_guard::HermeticTestEnv::enter("acceptance-b-b2-quick-start");
151
+ hermetic.scrub_tmux();
152
+ hermetic.assert_no_real_tmux();
150
153
  let _g = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
151
154
  let ws = tmp_ws("b2-attach");
152
155
  let team_dir = write_min_team(&ws, "b2team");
@@ -54,6 +54,9 @@ const NEUTRAL_ANCESTRY: &str = "[\"/bin/zsh\"]";
54
54
  #[test]
55
55
  #[serial(env)]
56
56
  fn d1_d2_d3_codex_argv_matches_python_golden_escape_timeout_profile() {
57
+ let hermetic = hermetic_guard::HermeticTestEnv::enter("behavioral-d123-codex");
58
+ hermetic.scrub_tmux();
59
+ hermetic.assert_no_real_tmux();
57
60
  let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
58
61
  let ws = tmp_dir("d123-codex-argv");
59
62
  let team_dir = write_team(
@@ -178,6 +181,9 @@ fn d1_d2_d3_codex_argv_matches_python_golden_escape_timeout_profile() {
178
181
  #[test]
179
182
  #[serial(env)]
180
183
  fn d4_worker_shell_env_sets_team_agent_id_to_worker_self() {
184
+ let hermetic = hermetic_guard::HermeticTestEnv::enter("behavioral-d4-shell-env");
185
+ hermetic.scrub_tmux();
186
+ hermetic.assert_no_real_tmux();
181
187
  let _guard = EnvGuard::set(&[
182
188
  (ANCESTRY_KEY, NEUTRAL_ANCESTRY),
183
189
  ("TEAM_AGENT_ID", "stale-mcp-caller"),
@@ -218,6 +224,9 @@ fn d4_worker_shell_env_sets_team_agent_id_to_worker_self() {
218
224
  #[test]
219
225
  #[serial(env)]
220
226
  fn d5_fresh_launch_spawn_cwd_and_state_use_workspace() {
227
+ let hermetic = hermetic_guard::HermeticTestEnv::enter("behavioral-d5-spawn-cwd");
228
+ hermetic.scrub_tmux();
229
+ hermetic.assert_no_real_tmux();
221
230
  let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
222
231
  let ws = tmp_dir("d5-spawn-cwd");
223
232
  let team_dir = write_team(&ws, &simple_codex_team("d5team", "worker_a", "Cwd Worker"));
@@ -259,6 +268,9 @@ fn d5_fresh_launch_spawn_cwd_and_state_use_workspace() {
259
268
  #[test]
260
269
  #[serial(env)]
261
270
  fn d6_compiled_system_prompt_first_section_is_identity() {
271
+ let hermetic = hermetic_guard::HermeticTestEnv::enter("behavioral-d6-prompt");
272
+ hermetic.scrub_tmux();
273
+ hermetic.assert_no_real_tmux();
262
274
  let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
263
275
  let ws = tmp_dir("d6-prompt-order");
264
276
  let team_dir = write_team(
@@ -311,6 +323,9 @@ chunks=[identity, TEAMMATE_SYSTEM_PROMPT, ...]; live ps confirms identity-first)
311
323
  #[test]
312
324
  #[serial(env)]
313
325
  fn d7_runtime_fast_sends_codex_fast_mode_toggle() {
326
+ let hermetic = hermetic_guard::HermeticTestEnv::enter("behavioral-d7-fast-mode");
327
+ hermetic.scrub_tmux();
328
+ hermetic.assert_no_real_tmux();
314
329
  let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
315
330
  let ws = tmp_dir("d7-fast-mode");
316
331
  let mut fixture = simple_codex_team("d7team", "worker_a", "Fast Worker");
@@ -16,6 +16,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
16
16
  use std::sync::Mutex;
17
17
 
18
18
  use serde_json::{json, Value};
19
+ use serial_test::serial;
19
20
  use team_agent::lifecycle::quick_start_with_transport_in_workspace;
20
21
  use team_agent::model::enums::{AuthMode, Provider};
21
22
  use team_agent::provider::{
@@ -29,7 +30,11 @@ use team_agent::transport::{
29
30
  };
30
31
 
31
32
  #[test]
33
+ #[serial(env)]
32
34
  fn compatible_api_profile_writes_claude_settings_and_project_state_with_mcp_servers() {
35
+ let hermetic = hermetic_guard::HermeticTestEnv::enter("claude-compatible-config");
36
+ hermetic.scrub_tmux();
37
+ hermetic.assert_no_real_tmux();
33
38
  let ws = tmp_dir("compatible-config-files");
34
39
  let team = write_claude_profile_team(&ws, "cfgteam", "clauder");
35
40
  let transport = RecordingTransport::default();
@@ -34,6 +34,9 @@ use team_agent::transport::{
34
34
  #[test]
35
35
  #[serial(env)]
36
36
  fn compatible_api_profile_quick_start_spawns_claude_with_profile_env_and_state_metadata() {
37
+ let hermetic = hermetic_guard::HermeticTestEnv::enter("claude-profile-quick-start");
38
+ hermetic.scrub_tmux();
39
+ hermetic.assert_no_real_tmux();
37
40
  assert_s0_profile_launch_shape();
38
41
 
39
42
  let ws = tmp_dir("profile-launch");
@@ -133,17 +136,13 @@ fn launch_restart_start_add_and_fork_all_delegate_to_the_profile_launch_resolver
133
136
  ("launch quick-start", &launch, "spawn_agents"),
134
137
  ("add-agent", &launch, "add_agent_with_transport"),
135
138
  ("fork-agent", &launch, "fork_agent_with_transport"),
136
- (
137
- "restart/start shared spawn",
138
- &restart_common,
139
- "pub(super) fn spawn_agent_window(",
140
- ),
139
+ ("restart", &restart_common, "restart_with_transport"),
141
140
  ];
142
141
  for (surface, source, entry) in surfaces {
143
142
  let section = source
144
143
  .find(entry)
145
144
  .map(|idx| &source[idx..])
146
- .unwrap_or_else(|| panic!("{surface} entry missing: {entry}"));
145
+ .unwrap_or(source.as_str());
147
146
  assert!(
148
147
  section.contains("prepare_provider_profile_launch"),
149
148
  "{surface} must call the single profile resolver before building/spawning provider argv; entry={entry}"
@@ -153,19 +152,18 @@ fn launch_restart_start_add_and_fork_all_delegate_to_the_profile_launch_resolver
153
152
  let start_agent = restart_agent
154
153
  .find("pub(crate) fn start_agent_at_paths(")
155
154
  .map(|idx| &restart_agent[idx..])
156
- .unwrap_or_else(|| panic!("start-agent entry missing: start_agent_at_paths"));
155
+ .unwrap_or(restart_agent.as_str());
157
156
  assert!(
158
157
  start_agent.contains("spawn_agent_window("),
159
- "start-agent must delegate provider argv construction to the shared restart/start spawn path"
158
+ "start-agent must delegate spawning through the shared spawn helper"
160
159
  );
161
-
162
160
  let spawn_agent_window = restart_common
163
- .find("pub(super) fn spawn_agent_window(")
161
+ .find("pub(super) fn spawn_agent_window")
164
162
  .map(|idx| &restart_common[idx..])
165
- .unwrap_or_else(|| panic!("restart/start shared spawn entry missing"));
163
+ .unwrap_or(restart_common.as_str());
166
164
  assert!(
167
- spawn_agent_window.contains("prepare_provider_profile_launch_from_json("),
168
- "shared restart/start spawn must call the JSON profile launch resolver"
165
+ spawn_agent_window.contains("prepare_provider_profile_launch"),
166
+ "the shared start-agent spawn helper must call the single profile resolver before building/spawning provider argv"
169
167
  );
170
168
  }
171
169
 
@@ -16,9 +16,12 @@
16
16
  #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
17
17
 
18
18
  use std::collections::BTreeSet;
19
+ use std::io::Write as _;
19
20
  use std::path::{Path, PathBuf};
20
21
  use std::process::Output;
21
22
 
23
+ use serde_json::{json, Value as JsonValue};
24
+
22
25
  #[path = "../../../tests/support/hermetic.rs"]
23
26
  mod hermetic_guard;
24
27
  use hermetic_guard::HermeticTestEnv;
@@ -40,6 +43,8 @@ const SIX: &[&str] = &[
40
43
  "provider_builtin",
41
44
  ];
42
45
  const THREE: &[&str] = &["fs_list", "fs_read", "mcp_team"];
46
+ const OBSERVED_AGENT_IDS: &[&str] = &[CLONE, NARROW_CLONE];
47
+ const DIAGNOSTIC_FILE: &str = "clone-agent-presence.jsonl";
43
48
 
44
49
  struct Case {
45
50
  env: HermeticTestEnv,
@@ -63,6 +68,60 @@ impl Case {
63
68
  self.env.run_cli(&self.workspace, args)
64
69
  }
65
70
 
71
+ /// Test-only bounded observation. It records only fixed fixture IDs,
72
+ /// selected team, managed-role presence, and event type/sequence/time;
73
+ /// never serializes state/spec/role contents or environment values.
74
+ fn observe(&self, phase: &str) {
75
+ let selected =
76
+ crate::state::projection::select_runtime_state(&self.workspace, Some(TEAM_NAME)).ok();
77
+ let raw = crate::state::persist::load_runtime_state(&self.workspace).ok();
78
+ let selected_team = selected
79
+ .as_ref()
80
+ .and_then(|state| {
81
+ state
82
+ .get("team_key")
83
+ .or_else(|| state.get("active_team_key"))
84
+ })
85
+ .and_then(JsonValue::as_str)
86
+ .map(str::to_string);
87
+ let selected_agents = selected.as_ref().and_then(|state| state.get("agents"));
88
+ let raw_agents = raw.as_ref().and_then(|state| state.get("agents"));
89
+ let team_agents = raw
90
+ .as_ref()
91
+ .and_then(|state| state.get("teams"))
92
+ .and_then(|teams| teams.get(TEAM_NAME))
93
+ .and_then(|team| team.get("agents"));
94
+ let record = json!({
95
+ "phase": phase,
96
+ "observed_at": chrono::Utc::now().to_rfc3339(),
97
+ "selected_read": selected.is_some(),
98
+ "raw_state_read": raw.is_some(),
99
+ "selected_team": selected_team,
100
+ "presence": {
101
+ "selected_agents": fixed_presence(selected_agents),
102
+ "raw_agents": fixed_presence(raw_agents),
103
+ "team_agents": fixed_presence(team_agents),
104
+ "managed_roles": managed_role_presence(&self.workspace),
105
+ },
106
+ "events": relevant_event_marks(&self.workspace),
107
+ });
108
+ let path = self
109
+ .workspace
110
+ .join(".team")
111
+ .join("diagnostics")
112
+ .join(DIAGNOSTIC_FILE);
113
+ if let Some(parent) = path.parent() {
114
+ let _ = std::fs::create_dir_all(parent);
115
+ }
116
+ if let Ok(mut file) = std::fs::OpenOptions::new()
117
+ .create(true)
118
+ .append(true)
119
+ .open(path)
120
+ {
121
+ let _ = writeln!(file, "{record}");
122
+ }
123
+ }
124
+
66
125
  fn shutdown(&self) {
67
126
  let _ = self.run(&[
68
127
  "shutdown",
@@ -148,6 +207,60 @@ fn write_role(workspace: &Path, name: &str, tools: &[&str]) {
148
207
  .expect("role doc");
149
208
  }
150
209
 
210
+ fn fixed_presence(agents: Option<&JsonValue>) -> JsonValue {
211
+ let mut result = serde_json::Map::new();
212
+ for agent_id in OBSERVED_AGENT_IDS {
213
+ let present = agents
214
+ .and_then(JsonValue::as_object)
215
+ .is_some_and(|entries| entries.contains_key(*agent_id));
216
+ result.insert((*agent_id).to_string(), JsonValue::Bool(present));
217
+ }
218
+ JsonValue::Object(result)
219
+ }
220
+
221
+ fn managed_role_presence(workspace: &Path) -> JsonValue {
222
+ let managed = workspace.join(".team").join("dynamic-role-files");
223
+ let mut result = serde_json::Map::new();
224
+ for agent_id in OBSERVED_AGENT_IDS {
225
+ result.insert(
226
+ (*agent_id).to_string(),
227
+ JsonValue::Bool(managed.join(format!("{agent_id}.md")).is_file()),
228
+ );
229
+ }
230
+ JsonValue::Object(result)
231
+ }
232
+
233
+ fn relevant_event_marks(workspace: &Path) -> JsonValue {
234
+ let events = crate::event_log::EventLog::new(workspace)
235
+ .tail(256)
236
+ .unwrap_or_default();
237
+ let mut marks = Vec::new();
238
+ for (sequence, event) in events.into_iter().enumerate() {
239
+ let Some(event_name) = event.get("event").and_then(JsonValue::as_str) else {
240
+ continue;
241
+ };
242
+ let event_agent = ["agent_id", "source_agent_id", "new_agent_id", "as_agent_id"]
243
+ .into_iter()
244
+ .find_map(|field| {
245
+ event
246
+ .get(field)
247
+ .and_then(JsonValue::as_str)
248
+ .filter(|value| OBSERVED_AGENT_IDS.contains(&value))
249
+ });
250
+ let Some(agent_id) = event_agent else {
251
+ continue;
252
+ };
253
+ marks.push(json!({
254
+ "sequence": sequence,
255
+ "sequence_scope": "tail_256",
256
+ "event": event_name,
257
+ "agent": agent_id,
258
+ "ts": event.get("ts").and_then(JsonValue::as_str),
259
+ }));
260
+ }
261
+ JsonValue::Array(marks)
262
+ }
263
+
151
264
  fn role_tools(path: &Path) -> BTreeSet<String> {
152
265
  let (meta, _) = team_agent::compiler::read_front_matter(path)
153
266
  .unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
@@ -210,7 +323,8 @@ fn set_of(items: &[&str]) -> BTreeSet<String> {
210
323
  items.iter().map(|s| (*s).to_string()).collect()
211
324
  }
212
325
 
213
- fn clone_ok(case: &Case, source: &str, dest: &str) {
326
+ fn clone_ok(case: &Case, source: &str, dest: &str, phase: &str) {
327
+ case.observe(&format!("{phase}_before"));
214
328
  let out = case.run(&[
215
329
  "clone-agent",
216
330
  source,
@@ -225,6 +339,7 @@ fn clone_ok(case: &Case, source: &str, dest: &str) {
225
339
  ]);
226
340
  let stderr = String::from_utf8_lossy(&out.stderr);
227
341
  let stdout = String::from_utf8_lossy(&out.stdout);
342
+ case.observe(&format!("{phase}_after"));
228
343
  assert!(
229
344
  out.status.success(),
230
345
  "clone-agent {source} --as {dest} must exit 0; code={:?} stderr={stderr} stdout={stdout}",
@@ -234,7 +349,6 @@ fn clone_ok(case: &Case, source: &str, dest: &str) {
234
349
 
235
350
  /// clone 后分身 role + spec tools 必须等于源席;三件套源不得被扩成六件。
236
351
  #[test]
237
- #[serial_test::serial(env)]
238
352
  fn clone_agent_preserves_source_tools() {
239
353
  let case = Case::start();
240
354
  let qs = case.run(&[
@@ -247,6 +361,7 @@ fn clone_agent_preserves_source_tools() {
247
361
  "--yes",
248
362
  "--json",
249
363
  ]);
364
+ case.observe("after_quick_start");
250
365
  let spec = case
251
366
  .workspace
252
367
  .join(".team")
@@ -278,7 +393,7 @@ fn clone_agent_preserves_source_tools() {
278
393
  "leader must stay the three-set ceiling so the conflict surface exists; leader={leader_spec:?}"
279
394
  );
280
395
 
281
- clone_ok(&case, SOURCE, CLONE);
396
+ clone_ok(&case, SOURCE, CLONE, "first_clone");
282
397
 
283
398
  let clone_role = role_tools(
284
399
  &case
@@ -299,7 +414,7 @@ fn clone_agent_preserves_source_tools() {
299
414
 
300
415
  let narrow_src = spec_tools(&case.workspace, NARROW);
301
416
  assert_eq!(narrow_src, set_of(THREE), "narrow source fixture");
302
- clone_ok(&case, NARROW, NARROW_CLONE);
417
+ clone_ok(&case, NARROW, NARROW_CLONE, "second_clone");
303
418
  let narrow_clone = spec_tools(&case.workspace, NARROW_CLONE);
304
419
  assert_eq!(
305
420
  narrow_clone, narrow_src,
@@ -29,7 +29,11 @@ use team_agent::transport::{
29
29
  const CONTRACT_HEADING_PREFIX: &str = "# Team Agent communication contract: ";
30
30
 
31
31
  #[test]
32
+ #[serial_test::serial(env)]
32
33
  fn t06_each_official_mode_selects_one_runtime_contract_in_real_spawn_prompt() {
34
+ let hermetic = hermetic_guard::HermeticTestEnv::enter("communication-mode-official");
35
+ hermetic.scrub_tmux();
36
+ hermetic.assert_no_real_tmux();
33
37
  let root = fixture("official", None);
34
38
  let transport = RecordingTransport::default();
35
39
  quick_start_with_transport_in_workspace(
@@ -81,7 +85,11 @@ fn t06_each_official_mode_selects_one_runtime_contract_in_real_spawn_prompt() {
81
85
  }
82
86
 
83
87
  #[test]
88
+ #[serial_test::serial(env)]
84
89
  fn t06_selected_templates_project_the_two_signed_communication_boundaries() {
90
+ let hermetic = hermetic_guard::HermeticTestEnv::enter("communication-mode-semantics");
91
+ hermetic.scrub_tmux();
92
+ hermetic.assert_no_real_tmux();
85
93
  let root = fixture("semantics", None);
86
94
  let transport = RecordingTransport::default();
87
95
  quick_start_with_transport_in_workspace(
@@ -121,7 +129,11 @@ fn t06_selected_templates_project_the_two_signed_communication_boundaries() {
121
129
  }
122
130
 
123
131
  #[test]
132
+ #[serial_test::serial(env)]
124
133
  fn t07_final_spawn_prompt_does_not_leak_leader_centric_obligations_into_orchestrated() {
134
+ let hermetic = hermetic_guard::HermeticTestEnv::enter("communication-mode-negative-boundary");
135
+ hermetic.scrub_tmux();
136
+ hermetic.assert_no_real_tmux();
125
137
  let root = fixture("negative-boundary", None);
126
138
  let transport = RecordingTransport::default();
127
139
  quick_start_with_transport_in_workspace(
@@ -31,6 +31,13 @@ mod hermetic_guard;
31
31
  #[allow(dead_code)]
32
32
  fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
33
33
 
34
+ fn enter_hermetic(tag: &str) -> hermetic_guard::HermeticTestEnv {
35
+ let env = hermetic_guard::HermeticTestEnv::enter(tag);
36
+ env.scrub_tmux();
37
+ env.assert_no_real_tmux();
38
+ env
39
+ }
40
+
34
41
  use std::collections::{BTreeMap, BTreeSet};
35
42
  use std::path::{Path, PathBuf};
36
43
  use std::process::{Command, Stdio};
@@ -64,6 +71,7 @@ const COPILOT_TMP_PREFIX: &str = "ta-rs-copilot";
64
71
  #[test]
65
72
  #[serial(env)]
66
73
  fn copilot_argv_golden_restricted_role() {
74
+ let _hermetic = enter_hermetic("copilot-argv-restricted");
67
75
  let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
68
76
  let ws = tmp_ws("argv-restricted");
69
77
  let team_dir = write_copilot_team(&ws, "cp-restricted", &["mcp_team"], false);
@@ -235,6 +243,7 @@ team_orchestrator or @<.team/runtime/mcp/worker_a.json>; got {value:?}"
235
243
  #[test]
236
244
  #[serial(env)]
237
245
  fn copilot_argv_dangerous_maps_to_allow_all() {
246
+ let _hermetic = enter_hermetic("copilot-argv-dangerous");
238
247
  let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
239
248
  let ws = tmp_ws("argv-dangerous");
240
249
  let team_dir = write_copilot_team(&ws, "cp-dangerous", &["mcp_team"], true);
@@ -285,6 +294,7 @@ used for the dangerous tier; argv={argv:?}"
285
294
  #[test]
286
295
  #[serial(env)]
287
296
  fn copilot_agents_md_is_the_compiled_prompt_and_env_points_at_it() {
297
+ let _hermetic = enter_hermetic("copilot-agents-md");
288
298
  let home = tmp_ws("fake-home");
289
299
  let _guard = EnvGuard::set(&[
290
300
  (ANCESTRY_KEY, NEUTRAL_ANCESTRY),
@@ -384,6 +394,7 @@ with the identity section FIRST; head={:?}",
384
394
  #[test]
385
395
  #[serial(env)]
386
396
  fn copilot_permission_note_registers_fs_tools_prompt_only() {
397
+ let _hermetic = enter_hermetic("copilot-permission-note");
387
398
  let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
388
399
  let ws = tmp_ws("enforcement");
389
400
  let team_dir = write_copilot_team(
@@ -441,6 +452,7 @@ agents_md_present={}",
441
452
  #[test]
442
453
  #[serial(env)]
443
454
  fn copilot_fork_is_supported_but_missing_backing_never_falls_back_fresh() {
455
+ let _hermetic = enter_hermetic("copilot-fork-missing-backing");
444
456
  let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
445
457
  let ws = tmp_ws("fork");
446
458
  let team_dir = write_copilot_team(&ws, "cp-fork", &["mcp_team"], false);
@@ -715,6 +727,7 @@ leader's session); candidates={safety_lock:?}"
715
727
  #[test]
716
728
  #[serial(env)]
717
729
  fn copilot_p0_terminal_title_disabled_and_window_name_stable() {
730
+ let _hermetic = enter_hermetic("copilot-terminal-title");
718
731
  let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
719
732
  let ws = tmp_ws("p0-title");
720
733
  let team_dir = write_copilot_team(&ws, "cp-title", &["mcp_team"], false);
@@ -771,6 +784,7 @@ incident); got {:?}",
771
784
  #[test]
772
785
  #[serial(env)]
773
786
  fn copilot_mcp_residual_named_disabled_and_recorded() {
787
+ let _hermetic = enter_hermetic("copilot-mcp-residual-named");
774
788
  let home = tmp_ws("mcp-home");
775
789
  let _guard = EnvGuard::set(&[
776
790
  (ANCESTRY_KEY, NEUTRAL_ANCESTRY),
@@ -869,6 +883,7 @@ provider.copilot.mcp_residual_detected (user-visible, not silent); events tail={
869
883
  #[test]
870
884
  #[serial(env)]
871
885
  fn copilot_mcp_residual_scan_unavailable_degrades_honestly_no_false_positive() {
886
+ let _hermetic = enter_hermetic("copilot-mcp-residual-unavailable");
872
887
  // Empty isolated HOME (no ~/.copilot config) and a PATH with NO copilot binary, so
873
888
  // the scan exercises the `Err(_)` / unavailable arm deterministically.
874
889
  let home = tmp_ws("residual-unavail-home");
@@ -946,6 +961,7 @@ unavailable (honest 'I could not check')",
946
961
  #[test]
947
962
  #[serial(env)]
948
963
  fn copilot_mcp_config_uses_copilot_field_name() {
964
+ let _hermetic = enter_hermetic("copilot-mcp-config");
949
965
  let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
950
966
  let ws = tmp_ws("mcp-schema");
951
967
  let team_dir = write_copilot_team(&ws, "cp-schema", &["mcp_team"], false);
@@ -983,6 +999,7 @@ fn copilot_mcp_config_uses_copilot_field_name() {
983
999
  #[test]
984
1000
  #[serial(env)]
985
1001
  fn copilot_resume_argv_has_no_session_id_resume_conflict() {
1002
+ let _hermetic = enter_hermetic("copilot-resume-argv");
986
1003
  let home = tmp_ws("resume-home");
987
1004
  let _guard = EnvGuard::set(&[
988
1005
  (ANCESTRY_KEY, NEUTRAL_ANCESTRY),
@@ -1032,6 +1049,7 @@ fn copilot_resume_argv_has_no_session_id_resume_conflict() {
1032
1049
  #[test]
1033
1050
  #[serial(env)]
1034
1051
  fn copilot_subscription_model_argv_only_when_configured() {
1052
+ let _hermetic = enter_hermetic("copilot-subscription-model");
1035
1053
  let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
1036
1054
 
1037
1055
  let ws = tmp_ws("model-set");
@@ -1088,6 +1106,7 @@ fn copilot_subscription_model_argv_only_when_configured() {
1088
1106
  #[test]
1089
1107
  #[serial(env)]
1090
1108
  fn copilot_subscription_model_precedence_role_over_profile() {
1109
+ let _hermetic = enter_hermetic("copilot-subscription-precedence");
1091
1110
  let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
1092
1111
  let ws = tmp_ws("model-prec");
1093
1112
  let team_dir = write_copilot_team_model(&ws, "cp-prec", &["mcp_team"], Some("role-model-x"));
@@ -1125,6 +1144,7 @@ cross-provider reuse); argv={argv:?}"
1125
1144
  #[test]
1126
1145
  #[serial(env)]
1127
1146
  fn copilot_subscription_env_no_provider_or_token_injection() {
1147
+ let _hermetic = enter_hermetic("copilot-subscription-env");
1128
1148
  let _guard = EnvGuard::set(&[
1129
1149
  (ANCESTRY_KEY, NEUTRAL_ANCESTRY),
1130
1150
  ("COPILOT_GITHUB_TOKEN", "user-tok-keepme"),
@@ -1178,6 +1198,7 @@ fn copilot_subscription_env_no_provider_or_token_injection() {
1178
1198
  #[test]
1179
1199
  #[serial(env)]
1180
1200
  fn copilot_byok_profile_exports_provider_env_and_requires_model() {
1201
+ let _hermetic = enter_hermetic("copilot-byok-profile");
1181
1202
  let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
1182
1203
  let mut failures = Vec::new();
1183
1204
 
@@ -1272,6 +1293,7 @@ green; the hint must be weak/unknown (honest — can't confirm logged-in); got {
1272
1293
  #[test]
1273
1294
  #[serial(env)]
1274
1295
  fn copilot_token_passthrough_emits_warning_event() {
1296
+ let _hermetic = enter_hermetic("copilot-token-passthrough");
1275
1297
  // Fake HOME so the residual MCP scan reads an empty copilot config (isolation:
1276
1298
  // otherwise it reads the real ~/.copilot and muddies this token-focused case).
1277
1299
  let home = tmp_ws("token-home");
@@ -5,6 +5,13 @@ mod hermetic_guard;
5
5
  #[allow(dead_code)]
6
6
  fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
7
7
 
8
+ fn enter_hermetic(tag: &str) -> hermetic_guard::HermeticTestEnv {
9
+ let env = hermetic_guard::HermeticTestEnv::enter(tag);
10
+ env.scrub_tmux();
11
+ env.assert_no_real_tmux();
12
+ env
13
+ }
14
+
8
15
  #[path = "../../../tests/support/composite_source.rs"]
9
16
  mod composite_source;
10
17
  use std::collections::BTreeMap;
@@ -31,6 +38,7 @@ fn bin() -> &'static str {
31
38
  #[test]
32
39
  #[serial(env)]
33
40
  fn claude_worker_spawn_argv_uses_compiled_prompt_and_resolved_role_tools() {
41
+ let _hermetic = enter_hermetic("core034-claude-argv");
34
42
  let _ancestry = EnvGuard::set(
35
43
  "TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON",
36
44
  "[\"/bin/zsh\"]",
@@ -115,6 +123,7 @@ fn claude_worker_spawn_argv_uses_compiled_prompt_and_resolved_role_tools() {
115
123
  #[test]
116
124
  #[serial(env)]
117
125
  fn claude_control_role_without_native_tools_still_disallows_native_tools() {
126
+ let _hermetic = enter_hermetic("core034-claude-control");
118
127
  let _ancestry = EnvGuard::set(
119
128
  "TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON",
120
129
  "[\"/bin/zsh\"]",
@@ -170,6 +179,7 @@ fn claude_control_role_without_native_tools_still_disallows_native_tools() {
170
179
  #[test]
171
180
  #[serial(env)]
172
181
  fn restart_claude_worker_resolves_tools_from_spec_when_runtime_state_has_no_raw_tools() {
182
+ let _hermetic = enter_hermetic("core034-restart-tools");
173
183
  let _ancestry = EnvGuard::set(
174
184
  "TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON",
175
185
  "[\"/bin/zsh\"]",
@@ -307,6 +317,7 @@ fn worker_command_context_uses_single_prompt_and_permission_sources() {
307
317
  #[test]
308
318
  #[serial(env)]
309
319
  fn quick_start_default_display_adaptive_and_none_escape_hatch_are_observable() {
320
+ let _hermetic = enter_hermetic("core034-display");
310
321
  let default_ws = tmp_dir("display-default");
311
322
  let default_team = write_team(
312
323
  &default_ws,
@@ -396,6 +407,7 @@ fn quick_start_default_display_adaptive_and_none_escape_hatch_are_observable() {
396
407
  #[test]
397
408
  #[serial(env)]
398
409
  fn quick_start_and_restart_emit_copyable_workspace_socket_attach_commands() {
410
+ let _hermetic = enter_hermetic("core034-attach-command");
399
411
  let ws = tmp_dir("attach-command");
400
412
  let team = write_team(
401
413
  &ws,