@team-agent/installer 0.5.119 → 0.5.121

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 (73) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +46 -1
  4. package/README.zh.md +46 -1
  5. package/crates/team-agent/src/cli/adapters.rs +11 -0
  6. package/crates/team-agent/src/cli/arguments.rs +401 -0
  7. package/crates/team-agent/src/cli/emit.rs +241 -142
  8. package/crates/team-agent/src/cli/intent/runtime.rs +1175 -0
  9. package/crates/team-agent/src/cli/intent/runtime_tests.rs +698 -0
  10. package/crates/team-agent/src/cli/intent/tests.rs +654 -0
  11. package/crates/team-agent/src/cli/intent.rs +642 -0
  12. package/crates/team-agent/src/cli/mod.rs +53 -6
  13. package/crates/team-agent/src/cli/named_address.rs +10 -1
  14. package/crates/team-agent/src/cli/spec.rs +22 -10
  15. package/crates/team-agent/src/cli/status_port/brief.rs +117 -16
  16. package/crates/team-agent/src/cli/tests/base.rs +6 -1
  17. package/crates/team-agent/src/cli/tests/parser_safety.rs +383 -0
  18. package/crates/team-agent/src/cli/tests/run_delegation.rs +63 -19
  19. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +56 -0
  20. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +3 -2
  21. package/crates/team-agent/src/cli/tests/verb_profile.rs +3 -1
  22. package/crates/team-agent/src/contract_runtime/backend.rs +1 -1
  23. package/crates/team-agent/src/db/message_store.rs +65 -0
  24. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +8 -0
  25. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +111 -4
  26. package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +244 -82
  27. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +166 -77
  28. package/crates/team-agent/src/lifecycle/launch.rs +29 -0
  29. package/crates/team-agent/src/lifecycle/restart/common.rs +7 -1
  30. package/crates/team-agent/src/lifecycle/restart.rs +17 -0
  31. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -3
  32. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +5 -15
  33. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -3
  34. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +2 -3
  35. package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +10 -2
  36. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +8 -1
  37. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +3 -2
  38. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +6 -9
  39. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +7 -5
  40. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +64 -19
  41. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +5 -2
  42. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +34 -10
  43. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +2 -4
  44. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +3 -4
  45. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +5 -4
  46. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +2 -4
  47. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +78 -36
  48. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +8 -4
  49. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +6 -12
  50. package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +4 -0
  51. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1 -3
  52. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +7 -0
  53. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +47 -4
  54. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -3
  55. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -1
  56. package/crates/team-agent/src/lifecycle/types.rs +10 -0
  57. package/crates/team-agent/src/mcp_server/mod.rs +2 -0
  58. package/crates/team-agent/src/mcp_server/readiness.rs +432 -0
  59. package/crates/team-agent/src/mcp_server/readiness_tests.rs +242 -0
  60. package/crates/team-agent/src/mcp_server/wire.rs +96 -11
  61. package/crates/team-agent/src/messaging/delivery.rs +62 -0
  62. package/crates/team-agent/src/messaging/tests/dup_inject.rs +41 -1
  63. package/crates/team-agent/src/provider/adapter.rs +7 -0
  64. package/crates/team-agent/src/tmux_backend/tests.rs +37 -0
  65. package/crates/team-agent/src/tmux_backend.rs +8 -1
  66. package/package.json +4 -4
  67. package/skills/team-agent/SKILL.md +71 -54
  68. package/skills/team-agent/command-coverage.json +283 -13
  69. package/skills/team-agent/docs/reference/team-agent-operator.md +220 -0
  70. package/skills/team-agent/references/bug-as-artifact-flow.md +1 -1
  71. package/skills/team-agent/references/recovery-runbook.md +61 -212
  72. package/skills/team-agent/references/team-agent-operator.md +181 -48
  73. package/skills/team-agent/references/team-in-team.md +47 -80
@@ -262,10 +262,7 @@ fn cursor_mcp_enable_child_env_applies_direct_profile_unsets() {
262
262
  }
263
263
  command.env("NO_PROXY", "fixture");
264
264
  command.env("no_proxy", "fixture");
265
- crate::lifecycle::launch::apply_cursor_mcp_enable_profile_env(
266
- &mut command,
267
- Some(&launch),
268
- );
265
+ crate::lifecycle::launch::apply_cursor_mcp_enable_profile_env(&mut command, Some(&launch));
269
266
  let output = command.output().expect("env child should run");
270
267
  assert!(output.status.success());
271
268
  let text = String::from_utf8(output.stdout).unwrap();
@@ -280,7 +277,9 @@ fn cursor_mcp_enable_child_env_applies_direct_profile_unsets() {
280
277
  "GLOBAL_AGENT_HTTP_PROXY",
281
278
  ] {
282
279
  assert!(
283
- !text.lines().any(|line| line.starts_with(&format!("{key}="))),
280
+ !text
281
+ .lines()
282
+ .any(|line| line.starts_with(&format!("{key}="))),
284
283
  "direct MCP child must omit {key}"
285
284
  );
286
285
  }
@@ -291,8 +290,9 @@ fn cursor_mcp_enable_child_env_applies_direct_profile_unsets() {
291
290
  #[test]
292
291
  #[serial(env)]
293
292
  fn cursor_spawn_writes_identity_into_project_mcp_json() {
294
- let _hermetic = hermetic_guard::HermeticTestEnv::enter("cursor-mcp-spawn");
293
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("cursor-mcp-spawn");
295
294
  let ws = tmp_dir("cursor-mcp-spawn");
295
+ super::launch_spawn::seed_healthy_coordinator(&ws);
296
296
  let team = write_cursor_team(&ws, "cursortm", "cursor_writer");
297
297
  let selected = cursor_mcp_json_path(&ws, "cursor_writer").unwrap();
298
298
  assert!(
@@ -325,17 +325,49 @@ fn cursor_spawn_writes_identity_into_project_mcp_json() {
325
325
  let enable_cwd = cursor_mcp_enable_working_dir(&ws, Some(&project));
326
326
  assert_eq!(enable_cwd, physical_workspace_path(&project));
327
327
  let argv_workspace = spawn_argv_workspace_flag(&ws).expect("spawn argv --workspace");
328
- assert_eq!(argv_workspace, physical_workspace_path(&project).to_string_lossy());
328
+ assert_eq!(
329
+ argv_workspace,
330
+ physical_workspace_path(&project).to_string_lossy()
331
+ );
332
+ let state = crate::state::persist::load_runtime_state(&ws).unwrap();
333
+ let row = state.pointer("/agents/cursor_writer").unwrap();
334
+ assert_eq!(row["spawn_cwd"], project.to_string_lossy().as_ref());
335
+ assert_eq!(row["session_id"], serde_json::Value::Null);
336
+
337
+ // Measured native layout: the store bucket uses --workspace primary, not
338
+ // --add-dir. A decoy in the real project's bucket must never be selected.
339
+ let sid = "d298b261-23bb-42bb-bbeb-b2ec08d81ca3";
340
+ for (cwd, id) in [(&project, sid), (&ws, "foreign-project-session")] {
341
+ let bucket = format!("{:x}", md5::compute(cwd.to_string_lossy().as_bytes()));
342
+ let archive = _hermetic.home().join(".cursor/chats").join(bucket).join(id);
343
+ std::fs::create_dir_all(&archive).unwrap();
344
+ std::fs::write(archive.join("meta.json"), b"{}").unwrap();
345
+ }
346
+ let mut capture = serde_json::json!({"agents": {"cursor_writer": row.clone()}});
347
+ let report = crate::provider::session::capture::capture_missing_provider_sessions_once(
348
+ &mut capture,
349
+ &mut crate::provider::get_adapter,
350
+ false,
351
+ 0,
352
+ )
353
+ .unwrap();
354
+ assert_eq!(report.assigned, vec!["cursor_writer"]);
355
+ assert_eq!(capture["agents"]["cursor_writer"]["session_id"], sid);
356
+ assert_eq!(
357
+ capture["agents"]["cursor_writer"]["spawn_cwd"],
358
+ project.to_string_lossy().as_ref()
359
+ );
329
360
  }
330
361
 
331
362
  #[test]
332
363
  #[serial(env)]
333
364
  fn cursor_spawn_isolation_off_writes_workspace_mcp_json() {
334
- let _hermetic = hermetic_guard::HermeticTestEnv::enter("cursor-mcp-spawn-legacy");
365
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("cursor-mcp-spawn-legacy");
335
366
  let key = "TEAM_AGENT_CURSOR_MCP_ISOLATION";
336
367
  let prev = std::env::var(key).ok();
337
368
  std::env::set_var(key, "0");
338
369
  let ws = tmp_dir("cursor-mcp-spawn-legacy");
370
+ super::launch_spawn::seed_healthy_coordinator(&ws);
339
371
  let team = write_cursor_team(&ws, "cursortm", "cursor_writer");
340
372
  let result = quick_start_with_transport_in_workspace(
341
373
  &ws,
@@ -359,13 +391,14 @@ fn cursor_spawn_isolation_off_writes_workspace_mcp_json() {
359
391
  });
360
392
  assert_cursor_identity_overlay(&text, "cursor_writer", &ws);
361
393
  assert!(
362
- !cursor_mcp_json_path(&ws, "cursor_writer")
363
- .unwrap()
364
- .exists(),
394
+ !cursor_mcp_json_path(&ws, "cursor_writer").unwrap().exists(),
365
395
  "isolation-off must not materialize the per-seat project overlay"
366
396
  );
367
397
  let argv_workspace = spawn_argv_workspace_flag(&ws).expect("spawn argv --workspace");
368
- assert_eq!(argv_workspace, physical_workspace_path(&ws).to_string_lossy());
398
+ assert_eq!(
399
+ argv_workspace,
400
+ physical_workspace_path(&ws).to_string_lossy()
401
+ );
369
402
  }
370
403
 
371
404
  fn assert_cursor_identity_overlay(text: &str, agent_id: &str, workspace: &Path) {
@@ -439,7 +472,7 @@ fn sample_mcp_config(agent_id: &str, workspace: &str) -> McpConfig {
439
472
  McpConfig {
440
473
  raw: serde_json::json!({
441
474
  "team_orchestrator": {
442
- "command": "/bin/team-agent-test",
475
+ "command": std::env::current_exe().unwrap(),
443
476
  "args": ["mcp-server", "--workspace", workspace],
444
477
  "env": {
445
478
  "TEAM_AGENT_ID": agent_id,
@@ -456,12 +489,21 @@ fn sample_mcp_config(agent_id: &str, workspace: &str) -> McpConfig {
456
489
  #[serial(env)]
457
490
  fn isolated_cursor_prepare_materializes_per_seat_project_not_workspace_overlay() {
458
491
  let ws = tmp_dir("cursor-iso-clean");
459
- let project = prepare_cursor_seat_mcp(&ws, "seat-iso", &sample_mcp_config("seat-iso", &ws.to_string_lossy()))
460
- .expect("prepare isolated cursor")
461
- .expect("isolation defaults on");
492
+ let project = prepare_cursor_seat_mcp(
493
+ &ws,
494
+ "seat-iso",
495
+ &sample_mcp_config("seat-iso", &ws.to_string_lossy()),
496
+ )
497
+ .expect("prepare isolated cursor")
498
+ .expect("isolation defaults on");
462
499
  assert!(
463
- project.ends_with("provider-config/seat-iso/cursor"),
464
- "per-seat project must be materialized: {}",
500
+ !project.starts_with(&ws)
501
+ && project
502
+ .file_name()
503
+ .unwrap()
504
+ .to_string_lossy()
505
+ .starts_with("ta-cursor-"),
506
+ "per-seat native project must be outside workspace Git ancestry: {}",
465
507
  project.display()
466
508
  );
467
509
  assert!(project.join(".cursor").is_dir());
@@ -487,7 +529,10 @@ fn isolated_cursor_prepare_materializes_per_seat_project_not_workspace_overlay()
487
529
  .find(|pair| pair[0] == "--workspace")
488
530
  .map(|pair| pair[1].as_str())
489
531
  .unwrap();
490
- assert_eq!(workspace_flag, physical_workspace_path(&project).to_string_lossy());
532
+ assert_eq!(
533
+ workspace_flag,
534
+ physical_workspace_path(&project).to_string_lossy()
535
+ );
491
536
  assert_eq!(
492
537
  include_str!("../launch/spawn.rs").contains("prepare_cursor_seat_mcp("),
493
538
  true,
@@ -32,7 +32,9 @@ use team_agent::transport::test_support::OfflineTransport;
32
32
  #[test]
33
33
  #[serial(env)]
34
34
  fn cursor_role_missing_model_uses_provider_default() {
35
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("cursor-no-model");
35
36
  let ws = tmp_dir("cursor-no-model");
37
+ super::launch_spawn::seed_healthy_coordinator(&ws);
36
38
  let team = write_role_team(&ws, "cursortm", "cursor_writer", None, None);
37
39
  let transport = OfflineTransport::new();
38
40
  quick_start_with_transport_in_workspace(
@@ -54,8 +56,9 @@ fn cursor_role_missing_model_uses_provider_default() {
54
56
  #[test]
55
57
  #[serial(env)]
56
58
  fn cursor_role_explicit_model_reaches_argv() {
57
- let _hermetic = hermetic_guard::HermeticTestEnv::enter("cursor-with-model");
59
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("cursor-with-model");
58
60
  let ws = tmp_dir("cursor-with-model");
61
+ super::launch_spawn::seed_healthy_coordinator(&ws);
59
62
  let team = write_role_team(
60
63
  &ws,
61
64
  "cursortm",
@@ -116,7 +119,7 @@ fn cursor_role_with_effort_refuses_to_start() {
116
119
  #[test]
117
120
  #[serial(env)]
118
121
  fn cursor_second_seat_in_same_workspace_is_refused() {
119
- let _hermetic = hermetic_guard::HermeticTestEnv::enter("cursor-two");
122
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("cursor-two");
120
123
  let ws = tmp_dir("cursor-two");
121
124
  let team = ws.join("cursortm");
122
125
  std::fs::create_dir_all(team.join("agents")).unwrap();
@@ -26,21 +26,20 @@ use std::sync::atomic::{AtomicU64, Ordering};
26
26
 
27
27
  use crate::lifecycle::restart::classify_restart_plan_with_resume_validation;
28
28
  use crate::lifecycle::types::ResumeDecision;
29
- use crate::provider::session_scan::cursor::{
30
- cursor_session_archive_present, cursor_session_dir,
31
- };
29
+ use crate::model::ids::AgentId;
30
+ use crate::provider::session_scan::cursor::{cursor_session_archive_present, cursor_session_dir};
32
31
  use crate::state::persist::{load_runtime_state, save_runtime_state};
33
32
  use serial_test::serial;
34
- use crate::model::ids::AgentId;
35
33
  use team_agent::lifecycle::{
36
- fork_agent_with_transport, quick_start_with_transport_in_workspace, restart_with_transport,
34
+ cursor_mcp_project_dir, fork_agent_with_transport, quick_start_with_transport_in_workspace,
35
+ restart_with_transport,
37
36
  };
38
37
  use team_agent::transport::test_support::OfflineTransport;
39
38
 
40
39
  #[test]
41
40
  #[serial(env)]
42
41
  fn cursor_fresh_spawn_does_not_invent_session_id() {
43
- let _hermetic = hermetic_guard::HermeticTestEnv::enter("cursor-fresh");
42
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("cursor-fresh");
44
43
  let (ws, _home, _guard, team) = seed_cursor("pending");
45
44
  let transport = OfflineTransport::new();
46
45
  quick_start_with_transport_in_workspace(&ws, &team, None, true, Some("cursortm"), &transport)
@@ -80,17 +79,22 @@ fn cursor_fresh_spawn_does_not_invent_session_id() {
80
79
  #[test]
81
80
  #[serial(env)]
82
81
  fn cursor_restart_uses_resume_when_session_captured_and_archive_present() {
83
- let _hermetic = hermetic_guard::HermeticTestEnv::enter("cursor-resume");
82
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("cursor-resume");
84
83
  let (ws, home, _guard, team) = seed_cursor("resume");
85
84
  let first = OfflineTransport::new();
86
85
  quick_start_with_transport_in_workspace(&ws, &team, None, true, Some("cursortm"), &first)
87
86
  .expect("initial cursor start");
88
87
  let sid = "502896a1-72ba-4c53-9a86-b2da28780806";
88
+ let primary = cursor_mcp_project_dir(&ws, "cursor_writer").unwrap();
89
+ let primary_before = std::fs::canonicalize(&primary).unwrap();
89
90
  let dir = {
90
91
  let path = home
91
92
  .join(".cursor")
92
93
  .join("chats")
93
- .join("00a437742b92089861da7821a62f232a")
94
+ .join(format!(
95
+ "{:x}",
96
+ md5::compute(primary.to_string_lossy().as_bytes())
97
+ ))
94
98
  .join(sid);
95
99
  std::fs::create_dir_all(&path).unwrap();
96
100
  std::fs::write(path.join("store.db"), b"").unwrap();
@@ -100,7 +104,11 @@ fn cursor_restart_uses_resume_when_session_captured_and_archive_present() {
100
104
  assert_eq!(cursor_session_dir(sid).as_deref(), Some(dir.as_path()));
101
105
 
102
106
  let mut state = load_runtime_state(&ws).expect("load");
103
- patch_cursor_writer(&mut state, sid, &dir, &ws);
107
+ assert_eq!(
108
+ state["agents"]["cursor_writer"]["spawn_cwd"],
109
+ primary.to_string_lossy().as_ref()
110
+ );
111
+ patch_cursor_writer(&mut state, sid, &dir, &primary);
104
112
  save_runtime_state(&ws, &state).expect("save captured");
105
113
 
106
114
  seed_healthy_coordinator(&ws);
@@ -121,6 +129,21 @@ fn cursor_restart_uses_resume_when_session_captured_and_archive_present() {
121
129
  !argv.iter().any(|a| a == "--continue"),
122
130
  "resume argv must not use --continue; argv={argv:?}"
123
131
  );
132
+ let after = load_runtime_state(&ws).unwrap();
133
+ assert_eq!(
134
+ after["agents"]["cursor_writer"]["spawn_cwd"],
135
+ primary.to_string_lossy().as_ref()
136
+ );
137
+ assert_eq!(after["agents"]["cursor_writer"]["session_id"], sid);
138
+ assert_eq!(
139
+ cursor_mcp_project_dir(&ws, "cursor_writer").unwrap(),
140
+ primary_before
141
+ );
142
+ assert_eq!(argv_flag(&argv, "--workspace").as_deref(), primary.to_str());
143
+ assert!(argv_contains_adjacent(
144
+ &argv,
145
+ &["--add-dir", ws.to_str().unwrap()]
146
+ ));
124
147
  }
125
148
 
126
149
  #[test]
@@ -182,7 +205,7 @@ fn cursor_classify_resumes_when_archive_present_and_refuses_when_missing() {
182
205
  #[test]
183
206
  #[serial(env)]
184
207
  fn cursor_fork_agent_refuses_as_unsupported_not_unverified() {
185
- let _hermetic = hermetic_guard::HermeticTestEnv::enter("cursor-fork");
208
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("cursor-fork");
186
209
  let (ws, _home, _guard, team) = seed_cursor("fork");
187
210
  let transport = OfflineTransport::new();
188
211
  quick_start_with_transport_in_workspace(&ws, &team, None, true, Some("cursortm"), &transport)
@@ -280,6 +303,7 @@ fn seed_healthy_coordinator(workspace: &Path) {
280
303
 
281
304
  fn seed_cursor(tag: &str) -> (PathBuf, PathBuf, HomeGuard, PathBuf) {
282
305
  let ws = tmp_dir(tag);
306
+ super::launch_spawn::seed_healthy_coordinator(&ws);
283
307
  let home = tmp_dir(&format!("{tag}-home"));
284
308
  let guard = HomeGuard::set(&home);
285
309
  let team = write_cursor_role(&ws);
@@ -22,10 +22,7 @@ mod hermetic_guard;
22
22
  fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
23
23
 
24
24
  fn enter_hermetic(tag: &str) -> hermetic_guard::HermeticTestEnv {
25
- let env = hermetic_guard::HermeticTestEnv::enter(tag);
26
- env.scrub_tmux();
27
- env.assert_no_real_tmux();
28
- env
25
+ hermetic_guard::HermeticTestEnv::enter_with_leader(tag)
29
26
  }
30
27
 
31
28
  use std::path::{Path, PathBuf};
@@ -159,6 +156,7 @@ fn seed_healthy_coordinator(workspace: &Path) {
159
156
 
160
157
  fn seed_grok(tag: &str, effort: Option<&str>) -> (PathBuf, PathBuf, HomeGuard, PathBuf) {
161
158
  let ws = tmp_dir(tag);
159
+ super::launch_spawn::seed_healthy_coordinator(&ws);
162
160
  let home = tmp_dir(&format!("{tag}-home"));
163
161
  seed_grok_home(&home, Some(&ws));
164
162
  let guard = HomeGuard::set(&home);
@@ -15,10 +15,7 @@ mod hermetic_guard;
15
15
  fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
16
16
 
17
17
  fn enter_hermetic(tag: &str) -> hermetic_guard::HermeticTestEnv {
18
- let env = hermetic_guard::HermeticTestEnv::enter(tag);
19
- env.scrub_tmux();
20
- env.assert_no_real_tmux();
21
- env
18
+ hermetic_guard::HermeticTestEnv::enter_with_leader(tag)
22
19
  }
23
20
 
24
21
  use std::path::{Path, PathBuf};
@@ -218,6 +215,7 @@ fn two_grok_seats_coexist_when_toml_has_no_per_seat_keys() {
218
215
  fn leftover_per_seat_key_is_cleared_before_start() {
219
216
  let _hermetic = enter_hermetic("grok-overlay-leftover-seat");
220
217
  let ws = tmp_dir("grok-cwd-per-seat");
218
+ super::launch_spawn::seed_healthy_coordinator(&ws);
221
219
  let home = tmp_dir("grok-cwd-per-seat-home");
222
220
  seed_grok_home(&home, Some(&ws));
223
221
  let _guard = HomeGuard::set(&home);
@@ -345,6 +343,7 @@ fn grok_missing_login_refuses_to_start() {
345
343
  fn grok_spawn_writes_resolved_team_agent_into_project_grok_config() {
346
344
  let _hermetic = enter_hermetic("grok-overlay-spawn");
347
345
  let ws = tmp_dir("grok-mcp-overlay");
346
+ super::launch_spawn::seed_healthy_coordinator(&ws);
348
347
  let home = tmp_dir("grok-mcp-overlay-home");
349
348
  seed_grok_home(&home, Some(&ws));
350
349
  let _guard = HomeGuard::set(&home);
@@ -14,10 +14,7 @@ mod hermetic_guard;
14
14
  fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
15
15
 
16
16
  fn enter_hermetic(tag: &str) -> hermetic_guard::HermeticTestEnv {
17
- let env = hermetic_guard::HermeticTestEnv::enter(tag);
18
- env.scrub_tmux();
19
- env.assert_no_real_tmux();
20
- env
17
+ hermetic_guard::HermeticTestEnv::enter_with_leader(tag)
21
18
  }
22
19
 
23
20
  use std::path::{Path, PathBuf};
@@ -31,7 +28,9 @@ use team_agent::transport::test_support::OfflineTransport;
31
28
  #[test]
32
29
  #[serial(env)]
33
30
  fn grok_role_missing_model_uses_provider_default() {
31
+ let _hermetic = enter_hermetic("grok-no-model");
34
32
  let ws = tmp_dir("grok-no-model");
33
+ super::launch_spawn::seed_healthy_coordinator(&ws);
35
34
  let home = tmp_dir("grok-no-model-home");
36
35
  seed_grok_home(&home, Some(&ws));
37
36
  let _guard = HomeGuard::set(&home);
@@ -67,6 +66,7 @@ fn grok_role_missing_model_uses_provider_default() {
67
66
  fn grok_role_explicit_model_reaches_argv() {
68
67
  let _hermetic = enter_hermetic("grok-explicit-model");
69
68
  let ws = tmp_dir("grok-with-model");
69
+ super::launch_spawn::seed_healthy_coordinator(&ws);
70
70
  let home = tmp_dir("grok-with-model-home");
71
71
  seed_grok_home(&home, Some(&ws));
72
72
  let _guard = HomeGuard::set(&home);
@@ -103,6 +103,7 @@ fn grok_role_explicit_model_reaches_argv() {
103
103
  fn claude_role_missing_model_still_starts() {
104
104
  let _hermetic = enter_hermetic("grok-claude-missing-model");
105
105
  let ws = tmp_dir("claude-no-model");
106
+ super::launch_spawn::seed_healthy_coordinator(&ws);
106
107
  let team = write_role_team(
107
108
  &ws,
108
109
  "claudeteam",
@@ -22,10 +22,7 @@ mod hermetic_guard;
22
22
  fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
23
23
 
24
24
  fn enter_hermetic(tag: &str) -> hermetic_guard::HermeticTestEnv {
25
- let env = hermetic_guard::HermeticTestEnv::enter(tag);
26
- env.scrub_tmux();
27
- env.assert_no_real_tmux();
28
- env
25
+ hermetic_guard::HermeticTestEnv::enter_with_leader(tag)
29
26
  }
30
27
 
31
28
  use std::path::{Path, PathBuf};
@@ -212,6 +209,7 @@ fn seed_healthy_coordinator(workspace: &Path) {
212
209
 
213
210
  fn seed_grok(tag: &str) -> (PathBuf, PathBuf, HomeGuard, PathBuf) {
214
211
  let ws = tmp_dir(tag);
212
+ super::launch_spawn::seed_healthy_coordinator(&ws);
215
213
  let home = tmp_dir(&format!("{tag}-home"));
216
214
  seed_grok_home(&home, Some(&ws));
217
215
  let guard = HomeGuard::set(&home);
@@ -174,8 +174,9 @@ fn unowned_running_ws() -> PathBuf {
174
174
  #[test]
175
175
  #[serial(env)]
176
176
  fn quick_start_compiles_real_spec_to_team_spec_yaml() {
177
- let _hermetic = enter_hermetic("launch-spawn-compile-spec");
177
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("launch-spawn-compile-spec");
178
178
  let team = quick_start_team_dir(QS_VALID_ROLE);
179
+ seed_healthy_coordinator(&crate::model::paths::team_workspace(&team).unwrap());
179
180
  let transport = OfflineTransport::new();
180
181
  let result = quick_start_with_transport(&team, None, true, None, &transport);
181
182
 
@@ -209,9 +210,10 @@ fn quick_start_compiles_real_spec_to_team_spec_yaml() {
209
210
  #[test]
210
211
  #[serial(env)]
211
212
  fn quick_start_without_team_id_uses_compiled_team_name_as_canonical_key() {
212
- let _hermetic = enter_hermetic("launch-spawn-canonical-team");
213
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("launch-spawn-canonical-team");
213
214
  let team = quick_start_team_dir(QS_VALID_ROLE);
214
215
  let workspace = crate::model::paths::team_workspace(&team).unwrap();
216
+ seed_healthy_coordinator(&workspace);
215
217
  let transport = OfflineTransport::new();
216
218
 
217
219
  let result = quick_start_with_transport(&team, None, true, None, &transport);
@@ -231,8 +233,9 @@ fn quick_start_without_team_id_uses_compiled_team_name_as_canonical_key() {
231
233
  #[test]
232
234
  #[serial(env)]
233
235
  fn existing_runtime_producer_scopes_canonical_team() {
234
- let _hermetic = enter_hermetic("launch-spawn-existing-producer");
236
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("launch-spawn-existing-producer");
235
237
  let team = quick_start_team_dir(QS_VALID_ROLE);
238
+ seed_healthy_coordinator(&crate::model::paths::team_workspace(&team).unwrap());
236
239
  let transport = OfflineTransport::new();
237
240
  let first = quick_start_with_transport(&team, Some("from-name"), true, None, &transport)
238
241
  .expect("first quick-start");
@@ -342,8 +345,9 @@ fn existing_runtime_empty_top_level_agents_still_emits_bob() {
342
345
  #[test]
343
346
  #[serial(env)]
344
347
  fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_collect() {
345
- let _hermetic = enter_hermetic("launch-spawn-dot-team-workspace");
348
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("launch-spawn-dot-team-workspace");
346
349
  let workspace = temp_ws();
350
+ seed_healthy_coordinator(&workspace);
347
351
  let team = workspace.join(".team").join("current");
348
352
  std::fs::create_dir_all(team.join("agents")).unwrap();
349
353
  std::fs::write(team.join("TEAM.md"), QS_TEAM_MD).unwrap();
@@ -417,7 +421,7 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
417
421
  #[test]
418
422
  #[serial(env)]
419
423
  fn quick_start_default_workspace_compiled_spec_uses_project_root() {
420
- let _hermetic = enter_hermetic("launch-spawn-default-workspace");
424
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("launch-spawn-default-workspace");
421
425
  let workspace = temp_ws();
422
426
  std::fs::create_dir_all(workspace.join("agents")).unwrap();
423
427
  std::fs::write(workspace.join("TEAM.md"), QS_TEAM_MD).unwrap();
@@ -724,8 +728,9 @@ fn unowned_running_ws_all_paused() -> PathBuf {
724
728
  #[test]
725
729
  #[serial(env)]
726
730
  fn spine_quick_start_seeds_state_under_team_workspace_not_team_dir() {
727
- let _hermetic = enter_hermetic("launch-spawn-spine-workspace");
731
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("launch-spawn-spine-workspace");
728
732
  let team = quick_start_team_dir(QS_VALID_ROLE); // <base>/teamdir
733
+ seed_healthy_coordinator(&crate::model::paths::team_workspace(&team).unwrap());
729
734
  let transport = OfflineTransport::new();
730
735
  let _ = quick_start_with_transport(&team, None, true, None, &transport);
731
736
  let workspace = team.parent().unwrap(); // team_workspace(<base>/teamdir) = <base>
@@ -1302,21 +1307,32 @@ fn start_agent_session_generation_inherits_birth_or_stamps_recreated_session() {
1302
1307
  /// Seed a HEALTHY coordinator at `workspace` (this process's pid + matching metadata + db schema) so
1303
1308
  /// quick_start's internal start_coordinator returns AlreadyRunning — NO real daemon subprocess spawn.
1304
1309
  pub(super) fn seed_healthy_coordinator(workspace: &std::path::Path) {
1305
- let wp = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
1306
- std::fs::create_dir_all(crate::model::paths::runtime_dir(workspace)).unwrap();
1307
- let _ = crate::message_store::MessageStore::open(workspace).unwrap(); // create db schema (schema.ok)
1308
- let me = crate::coordinator::Pid::new(std::process::id());
1309
- crate::coordinator::write_coordinator_metadata(
1310
- &wp,
1311
- me,
1312
- crate::coordinator::MetadataSource::Boot,
1313
- )
1314
- .unwrap();
1315
- std::fs::write(
1316
- crate::coordinator::coordinator_pid_path(&wp),
1317
- me.to_string(),
1318
- )
1319
- .unwrap();
1310
+ hermetic_guard::seed_in_process_coordinator(workspace);
1311
+ }
1312
+
1313
+ #[test]
1314
+ #[cfg(unix)]
1315
+ #[serial(env)]
1316
+ fn issue337_owned_fixture_caller_admits_fresh_launch_without_guard_bypass() {
1317
+ let env = hermetic_guard::HermeticTestEnv::enter_with_leader("issue337-fixture-caller");
1318
+ let workspace = env.workspace("ws");
1319
+ let team = workspace.join("roles");
1320
+ std::fs::create_dir_all(team.join("agents")).unwrap();
1321
+ std::fs::write(team.join("TEAM.md"), QS_TEAM_MD).unwrap();
1322
+ std::fs::write(team.join("agents/implementer.md"), QS_VALID_ROLE).unwrap();
1323
+ seed_healthy_coordinator(&workspace);
1324
+ let transport = OfflineTransport::new();
1325
+ let report = quick_start_with_transport_in_workspace(
1326
+ &workspace, &team, None, true, Some("fixture-team"), &transport,
1327
+ ).unwrap();
1328
+ let QuickStartReport::Ready { launch, .. } = report else { panic!("owned caller must admit launch: {report:?}") };
1329
+ assert!(launch.leader_receiver_attached);
1330
+ assert_eq!(launch.started.len(), 1);
1331
+ assert_eq!(transport.spawn_records().len(), 1);
1332
+ let socket = std::env::var("TMUX").unwrap().split(',').next().unwrap().to_string();
1333
+ assert!(std::path::Path::new(&socket).exists());
1334
+ drop(env);
1335
+ assert!(!std::path::Path::new(&socket).exists());
1320
1336
  }
1321
1337
 
1322
1338
  fn seed_canonical_team_identity(workspace: &std::path::Path, team_key: &str) {
@@ -1333,7 +1349,7 @@ fn seed_canonical_team_identity(workspace: &std::path::Path, team_key: &str) {
1333
1349
  #[test]
1334
1350
  #[serial(env)]
1335
1351
  fn quick_start_with_transport_spawns_workers_not_dry_run() {
1336
- let _hermetic = enter_hermetic("launch-spawn-spawn-path");
1352
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("launch-spawn-spawn-path");
1337
1353
  let team = quick_start_team_dir(QS_VALID_ROLE); // one agent: implementer / provider codex
1338
1354
  let workspace = team.parent().expect("team_workspace(team_dir) = parent"); // where start_coordinator runs
1339
1355
  seed_healthy_coordinator(workspace);
@@ -1422,7 +1438,7 @@ fn adaptive_layout_plan_8_workers_is_3_3_2() {
1422
1438
  #[test]
1423
1439
  #[serial(env)]
1424
1440
  fn quick_start_default_adaptive_groups_workers_into_layout_panes() {
1425
- let _hermetic = enter_hermetic("launch-spawn-adaptive-layout");
1441
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("launch-spawn-adaptive-layout");
1426
1442
  let roles = (1..=4)
1427
1443
  .map(|i| {
1428
1444
  let id = format!("w{i}");
@@ -1507,7 +1523,7 @@ fn quick_start_tmux_backend_uses_workspace_private_endpoint() {
1507
1523
  #[test]
1508
1524
  #[serial(env)]
1509
1525
  fn quick_start_persists_selected_tmux_endpoint_and_attach_commands() {
1510
- let _hermetic = enter_hermetic("launch-spawn-endpoint-attach");
1526
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("launch-spawn-endpoint-attach");
1511
1527
  let team = quick_start_team_dir(QS_VALID_ROLE);
1512
1528
  let workspace = team.parent().expect("team_workspace(team_dir) = parent");
1513
1529
  seed_healthy_coordinator(workspace);
@@ -1568,23 +1584,27 @@ fn annotate_runtime_tmux_endpoint_persists_workspace_socket_as_full_path() {
1568
1584
  #[test]
1569
1585
  #[serial(env)]
1570
1586
  fn quick_start_preserves_managed_leader_topology_and_emits_leader_attach_command() {
1571
- let _hermetic = enter_hermetic("launch-spawn-managed-leader");
1572
- let team = quick_start_team_dir(QS_VALID_ROLE);
1573
- let workspace = team.parent().expect("team_workspace(team_dir) = parent");
1587
+ let hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("launch-spawn-managed-leader");
1588
+ let workspace = hermetic.root();
1589
+ let team = workspace.join("teamdir");
1590
+ std::fs::create_dir_all(team.join("agents")).unwrap();
1591
+ std::fs::write(team.join("TEAM.md"), QS_TEAM_MD).unwrap();
1592
+ std::fs::write(team.join("agents/implementer.md"), QS_VALID_ROLE).unwrap();
1574
1593
  seed_healthy_coordinator(workspace);
1594
+ let endpoint = std::env::var("TMUX").unwrap().split(',').next().unwrap().to_string();
1575
1595
  crate::state::persist::save_runtime_state(
1576
1596
  workspace,
1577
1597
  &json!({
1578
1598
  "active_team_key": "teamdir",
1579
- "session_name": "team-teamdir",
1599
+ "session_name": null,
1580
1600
  "is_external_leader": false,
1581
1601
  "leader_client": {"diagnostic_only": true, "attach_mode": "attach-session"},
1582
- "leader_receiver": {"mode": "direct_tmux", "status": "attached", "pane_id": "%42"},
1583
- "team_owner": {"pane_id": "%42", "owner_epoch": 1},
1602
+ "tmux_socket": endpoint,
1603
+ "tmux_endpoint": endpoint,
1584
1604
  "agents": {},
1585
1605
  "teams": {
1586
1606
  "teamdir": {
1587
- "session_name": "team-teamdir",
1607
+ "session_name": null,
1588
1608
  "is_external_leader": false,
1589
1609
  "leader_client": {"diagnostic_only": true, "attach_mode": "attach-session"},
1590
1610
  "agents": {}
@@ -1593,9 +1613,25 @@ fn quick_start_preserves_managed_leader_topology_and_emits_leader_attach_command
1593
1613
  }),
1594
1614
  )
1595
1615
  .expect("seed managed leader state");
1616
+ let attached = crate::cli::cmd_attach_leader(&crate::cli::AttachLeaderArgs {
1617
+ workspace: workspace.to_path_buf(), team: Some("teamdir".into()), pane: None,
1618
+ provider: crate::model::enums::Provider::Fake, confirm: false, json: true,
1619
+ }).expect("bind the real owned caller through the public lease port");
1620
+ assert_eq!(attached.exit, crate::cli::ExitCode::Ok, "{attached:?}");
1621
+ let mut state = crate::state::persist::load_runtime_state(workspace).unwrap();
1622
+ assert!(state["session_name"].is_null(), "leader-only bootstrap has no worker session");
1623
+ assert!(state["teams"]["teamdir"]["session_name"].is_null());
1624
+ assert!(state["teams"]["teamdir"]["agents"].as_object().unwrap().is_empty());
1625
+ // The owned fake caller models the already-managed launcher topology;
1626
+ // retain the real binding while setting the presentation fixture metadata.
1627
+ state["is_external_leader"] = json!(false);
1628
+ state["leader_client"] = json!({"diagnostic_only": true, "attach_mode": "attach-session"});
1629
+ state["teams"]["teamdir"]["is_external_leader"] = json!(false);
1630
+ state["teams"]["teamdir"]["leader_client"] = state["leader_client"].clone();
1631
+ crate::state::persist::save_runtime_state(workspace, &state).unwrap();
1596
1632
  let transport = OfflineTransport::new();
1597
1633
 
1598
- let report = quick_start_with_transport(&team, None, true, None, &transport)
1634
+ let report = quick_start_with_transport(&team, None, true, Some("teamdir"), &transport)
1599
1635
  .expect("quick_start_with_transport must reach Ready");
1600
1636
  let attach_commands = match report {
1601
1637
  QuickStartReport::Ready {
@@ -2872,8 +2908,9 @@ fn stop_agent_adaptive_kills_target_pane_not_shared_layout_window() {
2872
2908
  #[test]
2873
2909
  #[serial(env)]
2874
2910
  fn quick_start_seeds_tasks_key_from_compiled_spec() {
2875
- let _hermetic = enter_hermetic("launch-spawn-tasks-key");
2911
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("launch-spawn-tasks-key");
2876
2912
  let team = quick_start_team_dir(QS_VALID_ROLE);
2913
+ seed_healthy_coordinator(&crate::model::paths::team_workspace(&team).unwrap());
2877
2914
  let transport = OfflineTransport::new();
2878
2915
  let _ = quick_start_with_transport(&team, None, true, None, &transport);
2879
2916
  let workspace = team
@@ -2921,7 +2958,7 @@ fn quick_start_seeds_tasks_key_from_compiled_spec() {
2921
2958
  // suffix (active_team_key, teams).
2922
2959
  #[test]
2923
2960
  #[serial(env)]
2924
- fn quick_start_state_seeds_spec_path_workspace_leader() {
2961
+ fn quick_start_unbound_state_producer_seeds_spec_path_workspace_leader() {
2925
2962
  let _hermetic = enter_hermetic("launch-spawn-state-seeds");
2926
2963
  // Bug 2 owner team-scope: top-level owner triple is dropped when the seeded
2927
2964
  // pane is empty; an ambient TMUX_PANE (tests run inside the dev's tmux session)
@@ -2932,10 +2969,14 @@ fn quick_start_state_seeds_spec_path_workspace_leader() {
2932
2969
  let _tmux = EnvVarGuard::unset("TMUX");
2933
2970
  let team = quick_start_team_dir(QS_VALID_ROLE);
2934
2971
  let transport = OfflineTransport::new();
2935
- let _ = quick_start_with_transport(&team, None, true, None, &transport);
2936
2972
  let workspace = team
2937
2973
  .parent()
2938
2974
  .expect("team_workspace(<base>/teamdir) = <base>");
2975
+ // Test the real unbound state producer plus recording-only worker stage.
2976
+ // Public start correctly refuses this absent caller (separate regressions).
2977
+ crate::lifecycle::launch::materialize_unbound_state_for_test(
2978
+ workspace, &team, "quickteam", &transport,
2979
+ ).expect("materialize the unbound producer fixture without public start");
2939
2980
  // E5: spec_path in state now points to .team/runtime/<team_key>/, team_dir stays the user role dir.
2940
2981
  let spec_path = quick_start_runtime_spec_path(&team);
2941
2982
  let (raw, state) = raw_runtime_state(workspace);
@@ -3018,10 +3059,11 @@ fn quick_start_state_seeds_spec_path_workspace_leader() {
3018
3059
  #[test]
3019
3060
  #[serial(env)]
3020
3061
  fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
3021
- let _hermetic = enter_hermetic("launch-spawn-running-state");
3062
+ let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("launch-spawn-running-state");
3022
3063
  const FIXED_SPAWNED_AT: &str = "2026-06-04T00:00:00+00:00";
3023
3064
  let _clock_guard = EnvVarGuard::set("TEAM_AGENT_TEST_FIXED_SPAWNED_AT", FIXED_SPAWNED_AT);
3024
3065
  let team = quick_start_team_dir(QS_VALID_ROLE);
3066
+ seed_healthy_coordinator(&crate::model::paths::team_workspace(&team).unwrap());
3025
3067
  let transport = OfflineTransport::new();
3026
3068
  let _ = quick_start_with_transport(&team, None, true, None, &transport);
3027
3069
  let workspace = team