@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
@@ -0,0 +1,698 @@
1
+ #![allow(clippy::unwrap_used, clippy::panic)]
2
+ use super::super::tests::Sandbox;
3
+ use super::*;
4
+
5
+ #[cfg(unix)]
6
+ #[path = "../../../tests/support/hermetic.rs"]
7
+ mod hermetic_guard;
8
+
9
+ fn fixture(ids: &[&str]) -> (Sandbox, Intent) {
10
+ let root = Sandbox::new();
11
+ std::fs::create_dir(root.path().join("agents")).unwrap();
12
+ std::fs::write(
13
+ root.path().join("TEAM.md"),
14
+ "---\nname: fixture\nprovider: pi\n---\nFixture\n",
15
+ )
16
+ .unwrap();
17
+ for id in ids {
18
+ write_role(root.path(), id);
19
+ }
20
+ let mut intent = super::super::tests::intent(Verb::Start);
21
+ intent.workspace = root.path().to_owned();
22
+ intent.subject = Subject::Team { source: None };
23
+ intent.team = Some("fixture".into());
24
+ (root, intent)
25
+ }
26
+ fn write_role(root: &Path, id: &str) {
27
+ std::fs::write(root.join("agents").join(format!("{id}.md")),format!("---\nname: {id}\nprovider: pi\nmodel: openai-codex/gpt-6-luna\neffort: max\ndangerously_skip_permissions: true\n---\nFixture\n")).unwrap();
28
+ }
29
+ fn install(root: &Path, ids: &[(&str, &str, bool)]) {
30
+ let agents:serde_json::Map<String,Value>=ids.iter().map(|(id,status,explicit)|((*id).into(),json!({"provider":"pi","status":status,"explicit_stop":explicit,"pane_pid":std::process::id(),"pane_id":format!("%{id}"),"window":id}))).collect();
31
+ let entry = json!({"team_key":"fixture","team_dir":root,"status":"alive","session_name":"team-fixture","agents":agents,"tasks":{}});
32
+ let mut raw = entry.clone();
33
+ raw["active_team_key"] = json!("fixture");
34
+ raw["teams"] = json!({"fixture":entry});
35
+ let state = crate::state::persist::runtime_state_path(root);
36
+ std::fs::create_dir_all(state.parent().unwrap()).unwrap();
37
+ std::fs::write(state, serde_json::to_vec(&raw).unwrap()).unwrap();
38
+ let spec = crate::model::paths::runtime_spec_path(root, "fixture");
39
+ std::fs::create_dir_all(spec.parent().unwrap()).unwrap();
40
+ std::fs::write(
41
+ spec,
42
+ yaml::dumps(&crate::compiler::compile_team(root).unwrap()),
43
+ )
44
+ .unwrap();
45
+ }
46
+ fn actions(plan: &Plan) -> Vec<(&str, Option<&str>)> {
47
+ plan.steps.iter().map(|s| (s.name(), s.agent())).collect()
48
+ }
49
+ #[test]
50
+ #[serial_test::serial(env)]
51
+ fn absent_source_is_one_original_create_not_restart_or_probe() {
52
+ let (root, intent) = fixture(&["one", "two"]);
53
+ let plan = plan_with_observer(intent, |_, _, _| panic!("fresh team probed")).unwrap();
54
+ assert_eq!(actions(&plan), [("quick_start", None)]);
55
+ assert!(!crate::state::persist::runtime_state_path(root.path()).exists());
56
+ }
57
+ #[test]
58
+ #[serial_test::serial(env)]
59
+ fn stopped_team_starts_both_and_mixed_start_leaves_live_peer_untouched() {
60
+ let (root, intent) = fixture(&["one", "two"]);
61
+ install(
62
+ root.path(),
63
+ &[("one", "stopped", true), ("two", "stopped", false)],
64
+ );
65
+ let plan =
66
+ plan_with_observer(intent.clone(), |_, _, _| Ok(Observation::StoppedVerified)).unwrap();
67
+ assert_eq!(
68
+ actions(&plan),
69
+ [("start_agent", Some("one")), ("start_agent", Some("two"))]
70
+ );
71
+ assert_eq!(plan.scope.team.as_deref(), Some("fixture"));
72
+ install(
73
+ root.path(),
74
+ &[("one", "running", false), ("two", "stopped", false)],
75
+ );
76
+ let plan = plan_with_observer(intent, |_, id, _| {
77
+ Ok(if id == "one" {
78
+ Observation::Live
79
+ } else {
80
+ Observation::StoppedVerified
81
+ })
82
+ })
83
+ .unwrap();
84
+ assert_eq!(actions(&plan), [("start_agent", Some("two"))]);
85
+ assert_eq!(plan.skipped[0]["reason"], "already_running");
86
+ }
87
+ #[test]
88
+ #[serial_test::serial(env)]
89
+ fn verified_all_live_is_noop_but_known_drift_and_unknown_refuse() {
90
+ let (root, intent) = fixture(&["one"]);
91
+ install(root.path(), &[("one", "running", false)]);
92
+ assert!(
93
+ plan_with_observer(intent.clone(), |_, _, _| Ok(Observation::Live))
94
+ .unwrap()
95
+ .steps
96
+ .is_empty()
97
+ );
98
+ assert!(plan_with_observer(intent.clone(), |_, _, _| Ok(Observation::Unknown)).is_err());
99
+ let mut patched = intent.clone();
100
+ patched.subject = Subject::Agent(AgentId::new("one"));
101
+ patched.patch.model = Some("new-model".into());
102
+ assert!(plan_with_observer(patched, |_, _, _| Ok(Observation::Live)).is_err());
103
+ let state = crate::state::persist::runtime_state_path(root.path());
104
+ let mut raw: Value = serde_json::from_slice(&std::fs::read(&state).unwrap()).unwrap();
105
+ raw["teams"]["fixture"]["agents"]["one"]["as_launched_dangerously_skip_permissions"] =
106
+ json!(false);
107
+ std::fs::write(state, serde_json::to_vec(&raw).unwrap()).unwrap();
108
+ assert!(plan_with_observer(intent, |_, _, _| Ok(Observation::Live)).is_err());
109
+ }
110
+ #[test]
111
+ #[serial_test::serial(env)]
112
+ fn new_restart_preserves_held_seats_and_shutdown_branch_starts_only_eligible() {
113
+ let (root, mut intent) = fixture(&["one", "two"]);
114
+ install(
115
+ root.path(),
116
+ &[("one", "stopped", true), ("two", "stopped", false)],
117
+ );
118
+ intent.verb = Verb::Restart;
119
+ intent.session = SessionPolicy::Auto;
120
+ let state = crate::state::persist::runtime_state_path(root.path());
121
+ let mut raw: Value = serde_json::from_slice(&std::fs::read(&state).unwrap()).unwrap();
122
+ raw["teams"]["fixture"]["status"] = json!("shutdown");
123
+ std::fs::write(&state, serde_json::to_vec(&raw).unwrap()).unwrap();
124
+ let plan = plan_with_observer(intent.clone(), |_, id, _| {
125
+ assert_ne!(id, "one", "held seat was probed");
126
+ Ok(Observation::StoppedVerified)
127
+ })
128
+ .unwrap();
129
+ assert_eq!(actions(&plan), [("start_agent", Some("two"))]);
130
+ assert_eq!(plan.skipped[0]["reason"], "explicit_stop");
131
+ raw["teams"]["fixture"]["agents"]["two"]["status"] = json!("running");
132
+ std::fs::write(state, serde_json::to_vec(&raw).unwrap()).unwrap();
133
+ let plan = plan_with_observer(intent, |_, _, _| Ok(Observation::Live)).unwrap();
134
+ assert_eq!(actions(&plan), [("restart_team", None)]);
135
+ let Step::RestartTeam(args) = &plan.steps[0] else {
136
+ panic!()
137
+ };
138
+ assert!(args.allow_fresh);
139
+ }
140
+ #[test]
141
+ #[serial_test::serial(env)]
142
+ fn all_paused_no_eligible_and_single_restart_never_unpauses() {
143
+ let (root, mut intent) = fixture(&["one"]);
144
+ install(root.path(), &[("one", "paused", false)]);
145
+ let blocked = plan_with_observer(intent.clone(), |_, _, _| panic!("paused probe")).unwrap();
146
+ assert!(blocked.no_eligible.is_some());
147
+ assert!(blocked.steps.is_empty());
148
+ assert_eq!(blocked.skipped[0]["reason"], "paused");
149
+ intent.verb = Verb::Restart;
150
+ intent.subject = Subject::Agent(AgentId::new("one"));
151
+ assert!(plan_with_observer(intent, |_, _, _| panic!("paused probe")).is_err());
152
+ }
153
+ #[test]
154
+ #[serial_test::serial(env)]
155
+ fn no_eligible_restart_preserves_scoped_held_and_stopped_skip_reasons() {
156
+ for explicit in [false, true] {
157
+ let (root, mut intent) = fixture(&["one"]);
158
+ install(root.path(), &[("one", "stopped", explicit)]);
159
+ intent.verb = Verb::Restart;
160
+ let blocked =
161
+ plan_with_observer(intent, |_, _, _| Ok(Observation::StoppedVerified)).unwrap();
162
+ assert!(blocked.no_eligible.is_some());
163
+ assert!(blocked.steps.is_empty());
164
+ assert_eq!(blocked.scope.team.as_deref(), Some("fixture"));
165
+ assert_eq!(blocked.skipped[0]["agent"], "one");
166
+ assert_eq!(
167
+ blocked.skipped[0]["reason"],
168
+ if explicit {
169
+ "explicit_stop"
170
+ } else {
171
+ "stopped_preserved_by_original_restart"
172
+ }
173
+ );
174
+ }
175
+ }
176
+
177
+ #[test]
178
+ #[serial_test::serial(env)]
179
+ fn role_only_add_and_absent_creation_need_explicit_decisions() {
180
+ let (root, mut intent) = fixture(&["one"]);
181
+ install(root.path(), &[("one", "stopped", false)]);
182
+ write_role(root.path(), "two");
183
+ intent.subject = Subject::Agent(AgentId::new("two"));
184
+ let plan = plan_with_observer(intent.clone(), |_, _, _| panic!("unregistered probe")).unwrap();
185
+ assert_eq!(actions(&plan), [("add_agent", Some("two"))]);
186
+ let Step::AddAgent(args) = &plan.steps[0] else {
187
+ panic!()
188
+ };
189
+ assert_eq!(
190
+ Path::new(&args.role_file),
191
+ root.path().join("agents/two.md")
192
+ );
193
+ intent.subject = Subject::Agent(AgentId::new("typo"));
194
+ assert!(plan_with_observer(intent.clone(), |_, _, _| panic!()).is_err());
195
+ intent.patch.provider = Some("pi".into());
196
+ intent.patch.bypass = Some(true);
197
+ assert!(matches!(
198
+ plan_with_observer(intent, |_, _, _| panic!())
199
+ .unwrap()
200
+ .steps[0],
201
+ Step::AddAgent(_)
202
+ ));
203
+ }
204
+ #[test]
205
+ #[serial_test::serial(env)]
206
+ fn spec_only_state_only_and_missing_spec_do_not_fallback_to_creation() {
207
+ let (root, mut intent) = fixture(&["one", "two"]);
208
+ install(root.path(), &[("one", "stopped", false)]);
209
+ assert!(
210
+ plan_with_observer(intent.clone(), |_, _, _| Ok(Observation::StoppedVerified)).is_err()
211
+ );
212
+ let spec = crate::model::paths::runtime_spec_path(root.path(), "fixture");
213
+ std::fs::remove_file(&spec).unwrap();
214
+ assert!(
215
+ plan_with_observer(intent.clone(), |_, _, _| Ok(Observation::StoppedVerified)).is_err()
216
+ );
217
+ intent.verb = Verb::Restart;
218
+ let blocked = plan_with_observer(intent, |_, _, _| Ok(Observation::StoppedVerified)).unwrap();
219
+ assert!(blocked.no_eligible.is_some());
220
+ assert!(blocked.steps.is_empty());
221
+ assert_eq!(blocked.skipped[0]["agent"], "one");
222
+ }
223
+ #[test]
224
+ #[serial_test::serial(env)]
225
+ fn runtime_files_and_source_are_unchanged_by_dry_planning() {
226
+ let (root, intent) = fixture(&["one"]);
227
+ install(root.path(), &[("one", "stopped", true)]);
228
+ let state = crate::state::persist::runtime_state_path(root.path());
229
+ let before = std::fs::read(&state).unwrap();
230
+ let plan = plan_with_observer(intent, |_, _, _| Ok(Observation::StoppedVerified)).unwrap();
231
+ assert!(!plan.steps.is_empty());
232
+ assert_eq!(std::fs::read(state).unwrap(), before);
233
+ }
234
+ #[test]
235
+ fn discovery_never_prefers_current_over_two_distinct_sources() {
236
+ let root = Sandbox::new();
237
+ for name in ["one", "current"] {
238
+ let path = root.path().join(".team").join(name);
239
+ std::fs::create_dir_all(path.join("agents")).unwrap();
240
+ std::fs::write(path.join("TEAM.md"), "fixture").unwrap();
241
+ }
242
+ assert!(discover(root.path(), None, None).is_err());
243
+ }
244
+ #[test]
245
+ #[serial_test::serial(env)]
246
+ fn explicit_source_and_relative_role_file_are_resolved_from_caller_not_workspace() {
247
+ let (root, _) = fixture(&["one"]);
248
+ let workspace = root.path().join("other");
249
+ std::fs::create_dir(&workspace).unwrap();
250
+ let args = vec![
251
+ "--from".into(),
252
+ ".".into(),
253
+ "--workspace".into(),
254
+ workspace.to_string_lossy().into_owned(),
255
+ "--dry-run".into(),
256
+ "--json".into(),
257
+ ];
258
+ let result = super::super::run("start", &args, root.path())
259
+ .unwrap()
260
+ .unwrap();
261
+ let CmdOutput::Json(value) = result.output else {
262
+ panic!()
263
+ };
264
+ assert_eq!(
265
+ value["scope"]["workspace"],
266
+ workspace.to_string_lossy().as_ref()
267
+ );
268
+ assert!(!workspace.join(".team").exists());
269
+ }
270
+
271
+ #[test]
272
+ #[serial_test::serial(env)]
273
+ fn genuine_bootstrap_uses_original_predicate_but_history_is_not_fresh() {
274
+ let (root, intent) = fixture(&["one"]);
275
+ let state = crate::state::persist::runtime_state_path(root.path());
276
+ std::fs::create_dir_all(state.parent().unwrap()).unwrap();
277
+ let bootstrap = json!({"team_key":"fixture","agents":{},"tasks":[],"session_name":null,"team_owner":{"pane_id":"%0"},"leader_receiver":{"pane_id":"%0","status":"attached"}});
278
+ std::fs::write(&state, serde_json::to_vec(&bootstrap).unwrap()).unwrap();
279
+ assert_eq!(
280
+ actions(&plan_with_observer(intent.clone(), |_, _, _| panic!()).unwrap()),
281
+ [("quick_start", None)]
282
+ );
283
+ let mut history = bootstrap;
284
+ history["tasks"] = json!({"old":{}});
285
+ std::fs::write(&state, serde_json::to_vec(&history).unwrap()).unwrap();
286
+ assert!(plan_with_observer(intent, |_, _, _| panic!()).is_err());
287
+ }
288
+
289
+ #[test]
290
+ #[serial_test::serial(env)]
291
+ fn preserve_rejects_known_missing_backing_before_stop_auto_uses_existing_decision() {
292
+ let (root, _) = fixture(&["one"]);
293
+ install(root.path(), &[("one", "running", false)]);
294
+ let selected =
295
+ resolve_active_team_readonly(root.path(), Some("fixture"), SelectorMode::RuntimeOnly)
296
+ .unwrap();
297
+ let row = json!({"provider":"pi","session_id":"missing-session","rollout_path":root.path().join("missing.jsonl"),"spawn_cwd":root.path()});
298
+ assert!(preflight_start(&selected, "one", &row, SessionPolicy::Preserve).is_err());
299
+ assert!(preflight_start(&selected, "one", &row, SessionPolicy::Auto).is_ok());
300
+ assert!(
301
+ preflight_start(
302
+ &selected,
303
+ "one",
304
+ &json!({"provider":"pi"}),
305
+ SessionPolicy::Preserve
306
+ )
307
+ .is_ok(),
308
+ "first fresh has no captured context to discard"
309
+ );
310
+ for policy in [SessionPolicy::Auto, SessionPolicy::Preserve] {
311
+ assert!(
312
+ preflight_start(&selected, "one", &json!({"provider":"cursor"}), policy).is_err(),
313
+ "unadmitted native resume cannot fall back to legacy/fresh"
314
+ );
315
+ }
316
+ }
317
+
318
+ #[cfg(unix)]
319
+ fn cursor_scope_fixture() -> (
320
+ hermetic_guard::HermeticTestEnv,
321
+ Sandbox,
322
+ SelectedTeam,
323
+ PathBuf,
324
+ Value,
325
+ ) {
326
+ let env = hermetic_guard::HermeticTestEnv::enter("cursor-intent-scope");
327
+ let (root, _) = fixture(&["one"]);
328
+ install(root.path(), &[("one", "stopped", true)]);
329
+ let selected =
330
+ resolve_active_team_readonly(root.path(), Some("fixture"), SelectorMode::RuntimeOnly)
331
+ .unwrap();
332
+ let config = crate::provider::McpConfig {
333
+ raw: json!({"team_orchestrator": {
334
+ "command": std::env::current_exe().unwrap(),
335
+ "args": ["mcp-server", "--workspace", root.path()],
336
+ "env": {"TEAM_AGENT_WORKSPACE": root.path(), "TEAM_AGENT_ID": "one", "TEAM_AGENT_OWNER_TEAM_ID": "fixture", "TEAM_AGENT_AUTH_MODE": "subscription"}
337
+ }}),
338
+ };
339
+ let primary = crate::lifecycle::launch::prepare_cursor_seat_mcp(root.path(), "one", &config)
340
+ .unwrap()
341
+ .unwrap();
342
+ let sid = "af3ce55e-2ee9-4bc5-8dae-b628e7447286";
343
+ let archive = env
344
+ .home()
345
+ .join(".cursor/chats")
346
+ .join(format!(
347
+ "{:x}",
348
+ md5::compute(primary.to_string_lossy().as_bytes())
349
+ ))
350
+ .join(sid);
351
+ std::fs::create_dir_all(&archive).unwrap();
352
+ std::fs::write(archive.join("meta.json"), b"{}").unwrap();
353
+ let row = json!({"provider":"cursor_agent","status":"stopped","explicit_stop":true,"session_id":sid,"rollout_path":archive,"spawn_cwd":primary});
354
+ (env, root, selected, primary, row)
355
+ }
356
+
357
+ #[test]
358
+ #[cfg(unix)]
359
+ #[serial_test::serial(env)]
360
+ fn issue342_cursor_owned_primary_scope_is_readonly_and_exact() {
361
+ let (_env, root, selected, primary, row) = cursor_scope_fixture();
362
+ let state = crate::state::persist::runtime_state_path(root.path());
363
+ let record = root
364
+ .path()
365
+ .join(".team/runtime/provider-config/one/cursor-project.json");
366
+ let paths = [state, record, primary.join(".cursor/mcp.json")];
367
+ let before: Vec<_> = paths.iter().map(|p| std::fs::read(p).unwrap()).collect();
368
+ assert!(!primary.starts_with(root.path()));
369
+ for policy in [SessionPolicy::Preserve, SessionPolicy::Auto] {
370
+ assert!(preflight_start(&selected, "one", &row, policy).is_ok());
371
+ }
372
+ let after: Vec<_> = paths.iter().map(|p| std::fs::read(p).unwrap()).collect();
373
+ assert_eq!(
374
+ before, after,
375
+ "scope preflight must not write state, record or MCP config"
376
+ );
377
+
378
+ let mut foreign_provider = row.clone();
379
+ foreign_provider["provider"] = json!("pi");
380
+ assert!(preflight_start(&selected, "one", &foreign_provider, SessionPolicy::Auto).is_err());
381
+ let mut other_team = selected.clone();
382
+ other_team.team_key = "foreign-team".into();
383
+ assert!(preflight_start(&other_team, "one", &row, SessionPolicy::Preserve).is_err());
384
+ assert!(preflight_start(&selected, "two", &row, SessionPolicy::Preserve).is_err());
385
+ let outside = Sandbox::new();
386
+ let mut other_workspace = selected.clone();
387
+ other_workspace.run_workspace = outside.path().to_owned();
388
+ assert!(preflight_start(&other_workspace, "one", &row, SessionPolicy::Preserve).is_err());
389
+ assert!(!outside.path().join(".team").exists());
390
+ }
391
+
392
+ #[test]
393
+ #[cfg(unix)]
394
+ #[serial_test::serial(env)]
395
+ fn issue342_cursor_scope_refuses_unrecorded_alias_and_replaced_incarnation() {
396
+ use std::os::unix::fs::{symlink, DirBuilderExt};
397
+ let (_env, root, selected, primary, row) = cursor_scope_fixture();
398
+ let record = root
399
+ .path()
400
+ .join(".team/runtime/provider-config/one/cursor-project.json");
401
+ let bytes = std::fs::read(&record).unwrap();
402
+ let retained = record.with_extension("retained");
403
+ std::fs::rename(&record, &retained).unwrap();
404
+ assert!(preflight_start(&selected, "one", &row, SessionPolicy::Preserve).is_err());
405
+ assert!(
406
+ !record.exists(),
407
+ "a same-named external directory is not authority to create a record"
408
+ );
409
+ std::fs::rename(retained, &record).unwrap();
410
+ let mut forged = row.clone();
411
+ let arbitrary = Sandbox::new();
412
+ forged["spawn_cwd"] = json!(arbitrary.path());
413
+ assert!(preflight_start(&selected, "one", &forged, SessionPolicy::Auto).is_err());
414
+ let alias = root.path().join("primary-alias");
415
+ symlink(&primary, &alias).unwrap();
416
+ forged["spawn_cwd"] = json!(alias);
417
+ assert!(preflight_start(&selected, "one", &forged, SessionPolicy::Preserve).is_err());
418
+ let old = primary.with_extension("retained");
419
+ std::fs::rename(&primary, old).unwrap();
420
+ std::fs::DirBuilder::new()
421
+ .mode(0o700)
422
+ .create(&primary)
423
+ .unwrap();
424
+ assert!(preflight_start(&selected, "one", &row, SessionPolicy::Auto).is_err());
425
+ assert_eq!(std::fs::read(record).unwrap(), bytes);
426
+ }
427
+
428
+ #[test]
429
+ #[serial_test::serial(env)]
430
+ fn changed_role_and_socket_refuse_in_the_real_readonly_recheck() {
431
+ let (root, intent) = fixture(&["one"]);
432
+ install(root.path(), &[("one", "stopped", false)]);
433
+ let plan = plan_with_observer(intent, |_, _, _| Ok(Observation::StoppedVerified)).unwrap();
434
+ let path = root.path().join("agents/one.md");
435
+ let original = std::fs::read(&path).unwrap();
436
+ std::fs::write(&path, b"changed role").unwrap();
437
+ assert!(recheck(&plan, &plan.steps[0], false).is_err());
438
+ std::fs::write(path, original).unwrap();
439
+ let state = crate::state::persist::runtime_state_path(root.path());
440
+ let mut raw: Value = serde_json::from_slice(&std::fs::read(&state).unwrap()).unwrap();
441
+ raw["teams"]["fixture"]["tmux_socket"] = json!("/different/endpoint");
442
+ std::fs::write(state, serde_json::to_vec(&raw).unwrap()).unwrap();
443
+ assert!(recheck(&plan, &plan.steps[0], false).is_err());
444
+ }
445
+
446
+ #[test]
447
+ #[serial_test::serial(env)]
448
+ fn creation_rejects_an_added_role_before_the_original_create_handler() {
449
+ let (root, intent) = fixture(&["one"]);
450
+ let plan = plan_with_observer(intent, |_, _, _| panic!()).unwrap();
451
+ write_role(root.path(), "two");
452
+ assert!(recheck(&plan, &plan.steps[0], false).is_err());
453
+ assert!(!crate::state::persist::runtime_state_path(root.path()).exists());
454
+ }
455
+
456
+ #[test]
457
+ #[serial_test::serial(env)]
458
+ fn stopped_text_without_physical_identity_is_unknown_and_standalone_source_keeps_its_workspace() {
459
+ let (root, _) = fixture(&["one"]);
460
+ install(root.path(), &[("one", "stopped", false)]);
461
+ let selected =
462
+ resolve_active_team_readonly(root.path(), Some("fixture"), SelectorMode::RuntimeOnly)
463
+ .unwrap();
464
+ assert_eq!(
465
+ observe(
466
+ &selected,
467
+ "one",
468
+ &json!({"status":"stopped","provider":"pi"})
469
+ )
470
+ .unwrap(),
471
+ Observation::Unknown
472
+ );
473
+ assert_eq!(
474
+ observe(
475
+ &selected,
476
+ "one",
477
+ &json!({"status":"stopped","provider":"pi","pane_pid":std::process::id()})
478
+ )
479
+ .unwrap(),
480
+ Observation::Unknown
481
+ );
482
+ let (root, _) = fixture(&["one"]);
483
+ let values = vec![
484
+ "--from".into(),
485
+ root.path().to_string_lossy().into_owned(),
486
+ "--dry-run".into(),
487
+ "--json".into(),
488
+ ];
489
+ let result = super::super::run("start", &values, root.path())
490
+ .unwrap()
491
+ .unwrap();
492
+ let CmdOutput::Json(value) = result.output else {
493
+ panic!()
494
+ };
495
+ assert_eq!(
496
+ value["scope"]["workspace"],
497
+ root.path().to_string_lossy().as_ref()
498
+ );
499
+ }
500
+
501
+ #[test]
502
+ fn declared_provider_aliases_do_not_create_false_drift_or_absorb_native_routes() {
503
+ assert!(!drift(
504
+ &json!({"provider":"agent"}),
505
+ &json!({"provider":"cursor_agent"})
506
+ ));
507
+ assert!(same_provider("claude-code", "claude_code"));
508
+ assert!(!same_provider("cursor", "cursor_agent"));
509
+ assert!(!same_provider("grok_build", "grok"));
510
+ }
511
+
512
+ #[test]
513
+ #[cfg(unix)]
514
+ fn native_noop_compares_all_five_authoritative_launch_settings() {
515
+ use crate::contract_runtime::{config::RoleConfig, prompt::Prompt};
516
+ use team_agent_contract::contract::types::{Digest, ProviderId, SeatId};
517
+ let old = RoleConfig {
518
+ id: SeatId::new("one").unwrap(),
519
+ provider: ProviderId::new("cursor").unwrap(),
520
+ source: PathBuf::from("/fixture/one.md"),
521
+ role: "one".into(),
522
+ model: "original".into(),
523
+ effort: None,
524
+ bypass: true,
525
+ paused: false,
526
+ body: "body".into(),
527
+ prompt: Prompt {
528
+ text: String::new(),
529
+ sha256: Digest([0; 32]),
530
+ sources: vec![],
531
+ },
532
+ };
533
+ assert!(!native_settings_changed(&old, &old.clone()));
534
+ for field in ["provider", "model", "effort", "bypass", "paused"] {
535
+ let mut current = old.clone();
536
+ match field {
537
+ "provider" => current.provider = ProviderId::new("kiro").unwrap(),
538
+ "model" => current.model = "changed".into(),
539
+ "effort" => current.effort = Some("high".into()),
540
+ "bypass" => current.bypass = false,
541
+ _ => current.paused = true,
542
+ }
543
+ assert!(
544
+ native_settings_changed(&old, &current),
545
+ "unexamined launch drift: {field}"
546
+ );
547
+ }
548
+ }
549
+
550
+ #[test]
551
+ fn original_supervised_shell_needs_both_current_native_frame_and_live_child() {
552
+ for shell in ["sh", "bash", "zsh", "fish"] {
553
+ assert!(surface_liveness(shell, true, true));
554
+ assert!(
555
+ !surface_liveness(shell, true, false),
556
+ "stale native screen without child"
557
+ );
558
+ assert!(
559
+ !surface_liveness(shell, false, true),
560
+ "inert tail sleep is not a provider"
561
+ );
562
+ assert!(!surface_liveness(shell, false, false));
563
+ }
564
+ assert!(!surface_liveness("", true, true));
565
+ assert!(surface_liveness("node", true, false));
566
+ assert!(!surface_liveness("node", false, true));
567
+ }
568
+
569
+ #[test]
570
+ fn unchanged_observation_port_rejects_native_screen_followed_by_exit_tail() {
571
+ use crate::model::enums::Provider;
572
+ use crate::transport::native_input::inspect_input;
573
+ let native = "────────────────────────\n\n────────────────────────\ngpt-6-luna · max · 0%/272k · 0% · $0.00\n";
574
+ assert!(inspect_input(Provider::Pi, native, 24).is_some());
575
+ for tail in ["[team-agent worker] pi exited with 1\n", "[team-agent] Provider exited; this pane no longer accepts input. Restart from another pane with the appropriate team-agent start command.\n", "$ "] {
576
+ assert!(inspect_input(Provider::Pi, &format!("{native}{tail}"), 24).is_none());
577
+ }
578
+ }
579
+
580
+ #[test]
581
+ #[cfg(unix)]
582
+ fn native_kernel_exit_can_recover_ready_but_mismatched_liveness_never_noops() {
583
+ use team_agent_contract::{
584
+ host::process::ProcessState as P, orchestration::store::SeatStatus as S,
585
+ };
586
+ for (status, process, root, expected) in [
587
+ (S::Ready, P::Alive, "running", Observation::Live),
588
+ (S::Ready, P::Exited, "running", Observation::StoppedVerified),
589
+ (
590
+ S::Stopped,
591
+ P::Exited,
592
+ "stopped",
593
+ Observation::StoppedVerified,
594
+ ),
595
+ (S::Ready, P::Alive, "stopped", Observation::Unknown),
596
+ (S::Stopped, P::Alive, "running", Observation::Unknown),
597
+ (S::Stopped, P::Exited, "running", Observation::Unknown),
598
+ (S::Ready, P::Replaced, "running", Observation::Unknown),
599
+ (
600
+ S::Ready,
601
+ P::Unknown(team_agent_contract::host::HostError::new(
602
+ "fixture",
603
+ team_agent_contract::host::HostErrorKind::Unknown,
604
+ )),
605
+ "running",
606
+ Observation::Unknown,
607
+ ),
608
+ ] {
609
+ assert_eq!(native_observation(status, process, Some(root)), expected);
610
+ }
611
+ }
612
+
613
+ #[test]
614
+ #[serial_test::serial(env)]
615
+ fn preserve_context_preflight_and_corrupt_metadata_refuse_before_stop() {
616
+ let (root, _) = fixture(&["one"]);
617
+ install(root.path(), &[("one", "running", false)]);
618
+ let selected =
619
+ resolve_active_team_readonly(root.path(), Some("fixture"), SelectorMode::RuntimeOnly)
620
+ .unwrap();
621
+ for signal in [
622
+ json!({"first_send_at":"2026-10-19T00:00:00Z"}),
623
+ json!({"task_prompt_delivered":true}),
624
+ json!({"last_result_at":"2026-10-19T00:00:00Z"}),
625
+ ] {
626
+ let mut row = json!({"provider":"pi"});
627
+ row.as_object_mut()
628
+ .unwrap()
629
+ .extend(signal.as_object().unwrap().clone());
630
+ assert!(preflight_start(&selected, "one", &row, SessionPolicy::Preserve).is_err());
631
+ assert!(preflight_start(&selected, "one", &row, SessionPolicy::Auto).is_ok());
632
+ }
633
+ for field in [
634
+ "session_id",
635
+ "rollout_path",
636
+ "last_result_at",
637
+ "spawn_cwd",
638
+ "first_send_at",
639
+ "task_prompt_delivered",
640
+ ] {
641
+ let mut row = json!({"provider":"pi"});
642
+ row[field] = json!([]);
643
+ for policy in [SessionPolicy::Auto, SessionPolicy::Preserve] {
644
+ assert!(
645
+ preflight_start(&selected, "one", &row, policy).is_err(),
646
+ "corrupt {field}"
647
+ );
648
+ }
649
+ }
650
+ let outside = Sandbox::new();
651
+ assert!(preflight_start(
652
+ &selected,
653
+ "one",
654
+ &json!({"provider":"pi", "spawn_cwd":outside.path()}),
655
+ SessionPolicy::Auto
656
+ )
657
+ .is_err());
658
+ assert!(preflight_start(
659
+ &selected,
660
+ "one",
661
+ &json!({"provider":"pi", "session_id":"captured"}),
662
+ SessionPolicy::Auto
663
+ )
664
+ .is_err());
665
+ }
666
+
667
+ #[test]
668
+ #[serial_test::serial(env)]
669
+ fn pinned_child_scope_never_inherits_parent_runtime_and_spec_only_never_creates() {
670
+ let (parent, _) = fixture(&["one"]);
671
+ install(parent.path(), &[("one", "running", false)]);
672
+ let child = parent.path().join("child");
673
+ std::fs::create_dir(&child).unwrap();
674
+ std::fs::create_dir(child.join("agents")).unwrap();
675
+ std::fs::write(
676
+ child.join("TEAM.md"),
677
+ "---\nname: child\nprovider: pi\n---\n",
678
+ )
679
+ .unwrap();
680
+ write_role(&child, "two");
681
+ let mut intent = super::super::tests::intent(Verb::Start);
682
+ intent.workspace = child.clone();
683
+ intent.team = None;
684
+ intent.subject = Subject::Team {
685
+ source: Some(child.clone()),
686
+ };
687
+ let plan = plan_with_observer(intent.clone(), |_, _, _| {
688
+ panic!("parent must not be selected")
689
+ })
690
+ .unwrap();
691
+ assert_eq!(plan.scope.workspace, child);
692
+ assert_eq!(actions(&plan), [("quick_start", None)]);
693
+ let path = crate::model::paths::runtime_spec_path(&child, "orphan");
694
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
695
+ std::fs::write(path, "agents: []\n").unwrap();
696
+ assert!(plan_with_observer(intent, |_, _, _| panic!()).is_err());
697
+ assert!(!crate::state::persist::runtime_state_path(&child).exists());
698
+ }