@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
@@ -8,6 +8,77 @@ use crate::model::enums::ProviderEffort;
8
8
  use crate::provider::adapters::pi::{build_pi_command_argv, PiCommandRequest, PiSessionSelector};
9
9
 
10
10
  const CATALOG: &[u8] = include_bytes!("fixtures/pi_list_models_g0.stdout.txt");
11
+ // Public pi --list-models rows captured for issue #186, preserving column widths.
12
+ const SPACED_IDS_CATALOG: &[u8] = include_bytes!("fixtures/pi_list_models_spaced_ids.stdout.txt");
13
+
14
+ #[test]
15
+ fn pi_catalog_preserves_spaced_ids_and_qualified_slug_variants() {
16
+ let models = parse_pi_list_models_table(SPACED_IDS_CATALOG).expect("public catalog rows");
17
+ assert_eq!(
18
+ models,
19
+ vec![
20
+ "cursor/claude-opus-4-7@1m:fast",
21
+ "cursor/claude-opus-4-7@1m:slow",
22
+ "openai-codex/gpt-5.6-luna",
23
+ "team-agent/Claude Opus 4.6 (Thinking)",
24
+ "team-agent/Claude Sonnet 4.6 (Thinking)",
25
+ "team-agent/Gemini 3.8 Flash",
26
+ "team-agent/gpt-5.6-luna",
27
+ "team-agent/qwen3.8-27b",
28
+ ]
29
+ );
30
+ for exact in &models {
31
+ assert_eq!(select_exact_pi_model(&models, exact).unwrap(), *exact);
32
+ assert!(argv_has_pair(&command_for_model(exact), "--model", exact));
33
+ }
34
+ }
35
+
36
+ #[test]
37
+ fn pi_catalog_still_rejects_duplicate_complete_spaced_ids() {
38
+ let text = std::str::from_utf8(SPACED_IDS_CATALOG).unwrap();
39
+ let repeated = text
40
+ .lines()
41
+ .find(|line| line.contains("Claude Opus 4.6 (Thinking)"))
42
+ .unwrap();
43
+ let duplicate = format!("{text}{repeated}\n");
44
+ let error = parse_pi_list_models_table(duplicate.as_bytes()).unwrap_err();
45
+ assert!(error
46
+ .to_string()
47
+ .contains("duplicate exact id \"team-agent/Claude Opus 4.6 (Thinking)\""));
48
+ let exact = "team-agent/Claude Opus 4.6 (Thinking)";
49
+ assert!(select_exact_pi_model(&[exact.into(), exact.into()], exact).is_err());
50
+ }
51
+
52
+ #[test]
53
+ fn pi_catalog_preserves_internal_whitespace_with_compact_and_full_headers() {
54
+ for catalog in [
55
+ "provider model\r\nteam-agent\tClaude Opus 4.6 (Thinking) \r\n",
56
+ "provider model context max-out thinking images\r\nteam-agent\tClaude Opus 4.6 (Thinking)\t200K 32.8K\tyes yes \r\n",
57
+ ] {
58
+ assert_eq!(
59
+ parse_pi_list_models_table(catalog.as_bytes()).unwrap(),
60
+ vec!["team-agent/Claude Opus 4.6 (Thinking)"]
61
+ );
62
+ }
63
+ }
64
+
65
+ #[test]
66
+ fn pi_catalog_rejects_rows_without_model_or_required_trailing_fields() {
67
+ for row in [
68
+ "team-agent",
69
+ "team-agent 200K 32.8K yes yes",
70
+ "team-agent gpt-5.6-luna 200K 32.8K yes",
71
+ ] {
72
+ let catalog = format!("provider model context max-out thinking images\n{row}\n");
73
+ assert!(
74
+ parse_pi_list_models_table(catalog.as_bytes())
75
+ .unwrap_err()
76
+ .to_string()
77
+ .contains("row 2 is malformed"),
78
+ "missing model or capability fields must refuse: {row}"
79
+ );
80
+ }
81
+ }
11
82
 
12
83
  fn argv_has_pair(argv: &[String], flag: &str, value: &str) -> bool {
13
84
  argv.windows(2)
@@ -428,6 +428,84 @@ fn pi_leader_and_teammate_body(hermetic: &HermeticTestEnv) {
428
428
  "dynamic add must not create a second tmux session"
429
429
  );
430
430
 
431
+ let pi_provider_root = hermetic.workspace("pi-add-provider");
432
+ let pi_bin = pi_provider_root.join("bin");
433
+ let pi_package = pi_provider_root.join("pi-mcp-adapter");
434
+ std::fs::create_dir_all(&pi_bin).expect("create Pi test bin");
435
+ std::fs::create_dir_all(&pi_package).expect("create Pi test adapter");
436
+ let pi_real = pi_provider_root.join("pi-cli");
437
+ std::fs::write(
438
+ &pi_real,
439
+ format!(
440
+ "#!/bin/sh\ncase \"$1\" in\n --version) printf '0.84.4\\n' ;;\n --list-models) printf 'provider model\\nteam-agent qwen3.8-27b\\n' ;;\n list) printf 'npm:pi-mcp-adapter\\n{}\\n' ;;\n *) exit 64 ;;\nesac\n",
441
+ pi_package.display()
442
+ ),
443
+ )
444
+ .expect("write protocol-capable Pi test executable");
445
+ std::fs::set_permissions(
446
+ &pi_real,
447
+ std::os::unix::fs::PermissionsExt::from_mode(0o755),
448
+ )
449
+ .expect("make Pi test executable");
450
+ std::os::unix::fs::symlink(&pi_real, pi_bin.join("pi")).expect("link Pi test executable");
451
+ std::fs::write(
452
+ pi_package.join("package.json"),
453
+ br#"{"name":"pi-mcp-adapter","version":"2.30.0","pi":{"extensions":["./index.ts"]}}"#,
454
+ )
455
+ .expect("write Pi test adapter package");
456
+ std::fs::write(
457
+ pi_package.join("index.ts"),
458
+ b"export const createMcpAdapter = () => {};\n",
459
+ )
460
+ .expect("write Pi test adapter entry");
461
+ let _pi_path = EnvVarGuard::set("PATH", pi_bin.as_os_str());
462
+ let pi_success_role = "---\nname: mate\nrole: Pi Dynamic Worker\nprovider: pi\nmodel: team-agent/qwen3.8-27b\nauth_mode: subscription\neffort: max\ntools:\n - mcp_team\ndangerously_skip_permissions: true\n---\n\ndynamic pi\n";
463
+ let (pi_success_team, pi_success_role_path) =
464
+ dynamic_add_fixture(&dynamic_root, "dynamic-pi-success", pi_success_role);
465
+ let pi_success_transport = OfflineTransport::new().with_session_present(true);
466
+ crate::lifecycle::add_agent_with_transport(
467
+ &pi_success_team,
468
+ &AgentId::new("mate"),
469
+ &pi_success_role_path,
470
+ false,
471
+ None,
472
+ &pi_success_transport,
473
+ )
474
+ .expect("Pi dynamic add must commit after spawning its materialized plan");
475
+ let pi_spawn = pi_success_transport.spawn_records();
476
+ assert_eq!(pi_spawn.len(), 1, "Pi dynamic add must spawn exactly once");
477
+ let pi_state = crate::state::persist::load_runtime_state(&pi_success_team)
478
+ .expect("load successful Pi add state");
479
+ assert_eq!(
480
+ pi_state
481
+ .pointer("/agents/mate/status")
482
+ .and_then(serde_json::Value::as_str),
483
+ Some("running"),
484
+ "successful Pi add must commit the running seat"
485
+ );
486
+ let pi_events = crate::event_log::EventLog::new(&pi_success_team)
487
+ .tail(50)
488
+ .expect("read successful Pi add events");
489
+ let pi_start_event = pi_events
490
+ .iter()
491
+ .find(|event| {
492
+ event.get("event").and_then(serde_json::Value::as_str)
493
+ == Some("start_agent.agent_start")
494
+ })
495
+ .expect("successful Pi add must emit start event");
496
+ let pi_event_command = pi_start_event
497
+ .get("command")
498
+ .and_then(serde_json::Value::as_array)
499
+ .expect("successful Pi start event command")
500
+ .iter()
501
+ .map(|value| value.as_str().expect("Pi argv string").to_string())
502
+ .collect::<Vec<_>>();
503
+ assert_eq!(
504
+ pi_event_command, pi_spawn[0].1,
505
+ "Pi start event must record the exact materialized plan that was spawned"
506
+ );
507
+ drop(_pi_path);
508
+
431
509
  let (rollback_team, rollback_role) =
432
510
  dynamic_add_fixture(&dynamic_root, "dynamic-rollback", fake_role);
433
511
  let events_path = rollback_team.join(".team/logs/events.jsonl");
@@ -35,6 +35,9 @@ use team_agent::transport::{
35
35
  #[test]
36
36
  #[serial(env)]
37
37
  fn quick_start_team_id_persists_requested_team_key_for_lifecycle_selection() {
38
+ let hermetic = hermetic_guard::HermeticTestEnv::enter("team-in-team-requested-key");
39
+ hermetic.scrub_tmux();
40
+ hermetic.assert_no_real_tmux();
38
41
  let _env = EnvGuard::unset();
39
42
  let root = tmp_dir("team-id-key");
40
43
  seed_healthy_coordinator(&root);
@@ -131,18 +131,46 @@ fn r5_restart_auto_attach_registers_isolated_live_leader_entry() {
131
131
  case.seed_restartable_workspace();
132
132
  seed_healthy_coordinator(&case.workspace);
133
133
 
134
- let report = restart_with_transport_with_readiness_deadline(
135
- &case.workspace,
136
- true,
137
- Some(TEAM_KEY),
138
- &RestartAutoAttachTransport::new(),
139
- Some(1_000),
134
+ let live = crate::transport::test_support::OfflineTransport::default()
135
+ .with_tmux_endpoint(TMUX_SOCKET)
136
+ .with_targets(vec![PaneInfo {
137
+ pane_id: PaneId::new(CALLER_PANE),
138
+ session: SessionName::new("team-agent-leader-0515"),
139
+ window_index: Some(0),
140
+ window_name: Some(WindowName::new("leader")),
141
+ pane_index: Some(0),
142
+ tty: None,
143
+ current_command: Some("codex".to_string()),
144
+ current_path: Some(case.workspace.clone()),
145
+ active: true,
146
+ pane_pid: None,
147
+ leader_env: Default::default(),
148
+ }]);
149
+ let report = crate::transport_factory::with_leader_endpoint_transport(
150
+ TMUX_SOCKET,
151
+ live,
152
+ || {
153
+ restart_with_transport_with_readiness_deadline(
154
+ &case.workspace,
155
+ true,
156
+ Some(TEAM_KEY),
157
+ &RestartAutoAttachTransport::new(),
158
+ Some(1_000),
159
+ )
160
+ },
140
161
  )
141
162
  .expect("R5 setup: restart should complete against fake transport");
142
- assert!(
143
- matches!(report, RestartReport::Restarted { .. }),
144
- "R5 setup: restart must succeed before checking registry side effects; report={report:?}"
145
- );
163
+ match &report {
164
+ RestartReport::Restarted {
165
+ leader_bind_ok, ..
166
+ } => {
167
+ assert!(
168
+ *leader_bind_ok,
169
+ "R5: public restart bind must be ok; report={report:?}"
170
+ );
171
+ }
172
+ other => panic!("R5 setup: restart must succeed before checking registry side effects; report={other:?}"),
173
+ }
146
174
  let state = load_runtime_state(&case.workspace).expect("read state after restart");
147
175
  assert_eq!(
148
176
  canonical_leader_receiver(&state)
@@ -527,6 +555,19 @@ impl RestartAutoAttachCase {
527
555
  "session_name": "team-ctxteam",
528
556
  "tmux_endpoint": TMUX_SOCKET,
529
557
  "tmux_socket": TMUX_SOCKET,
558
+ "team_owner": {
559
+ "pane_id": CALLER_PANE,
560
+ "owner_epoch": 1,
561
+ "provider": "codex"
562
+ },
563
+ "leader_receiver": {
564
+ "mode": "direct_tmux",
565
+ "status": "attached",
566
+ "pane_id": CALLER_PANE,
567
+ "owner_epoch": 1,
568
+ "provider": "codex",
569
+ "tmux_socket": TMUX_SOCKET
570
+ },
530
571
  "agents": { WORKER: worker }
531
572
  }
532
573
  }
@@ -103,13 +103,37 @@ fn worker_mcp_owner_team_scope_must_match_runtime_team_key_not_spec_name() {
103
103
  fn quick_start_preserves_external_leader_receiver_when_worker_pane_id_collides_across_tmux_sockets()
104
104
  {
105
105
  let hermetic = HermeticTestEnv::enter("rs031-external-pane-collision");
106
- let _tmux = hermetic.with_env("TMUX", "/tmp/default-tmux-socket,123,0");
106
+ let caller_endpoint = "/tmp/default-tmux-socket";
107
+ let _tmux = hermetic.with_env("TMUX", &format!("{caller_endpoint},123,0"));
107
108
  let _pane = hermetic.with_env("TMUX_PANE", "%0");
109
+ let _provider = hermetic.with_env("TEAM_AGENT_LEADER_PROVIDER", "codex");
108
110
  let team = codex_only_team_dir(&hermetic, "external-pane-collision");
109
- let transport = RecordingTransport::new().with_windows(vec![WindowName::new("codexer")]);
111
+ let workspace = team.parent().expect("teamdir has parent workspace");
112
+ let caller = PaneInfo {
113
+ pane_id: PaneId::new("%0"),
114
+ session: SessionName::new("leader"),
115
+ window_index: Some(0),
116
+ window_name: Some(WindowName::new("leader")),
117
+ pane_index: Some(0),
118
+ tty: None,
119
+ current_command: Some("codex".to_string()),
120
+ current_path: Some(workspace.to_path_buf()),
121
+ active: true,
122
+ pane_pid: None,
123
+ leader_env: Default::default(),
124
+ };
125
+ let transport = RecordingTransport::new()
126
+ .with_windows(vec![WindowName::new("codexer")])
127
+ .with_tmux_endpoint(caller_endpoint)
128
+ .with_targets(vec![caller.clone()]);
129
+ let live = crate::transport::test_support::OfflineTransport::default()
130
+ .with_tmux_endpoint(caller_endpoint)
131
+ .with_targets(vec![caller]);
110
132
  let launch = ready_launch(
111
- quick_start_with_transport(&team, None, true, None, &transport)
112
- .expect("quick-start should reach worker spawn path"),
133
+ crate::transport_factory::with_leader_endpoint_transport(caller_endpoint, live, || {
134
+ quick_start_with_transport(&team, None, true, None, &transport)
135
+ })
136
+ .expect("quick-start should reach worker spawn path"),
113
137
  );
114
138
  let worker_pane = launch
115
139
  .started
@@ -122,7 +146,6 @@ fn quick_start_preserves_external_leader_receiver_when_worker_pane_id_collides_a
122
146
  "fixture precondition: first worker in the product -L socket receives bare pane id %0"
123
147
  );
124
148
 
125
- let workspace = team.parent().expect("teamdir has parent workspace");
126
149
  let state = load_runtime_state(workspace).expect("runtime state written by launch");
127
150
  let top_receiver_pane = state
128
151
  .get("leader_receiver")
@@ -765,6 +788,7 @@ struct RecordingState {
765
788
  spawn_argvs: Vec<Vec<String>>,
766
789
  liveness: BTreeMap<String, PaneLiveness>,
767
790
  targets: Vec<PaneInfo>,
791
+ tmux_endpoint: Option<String>,
768
792
  }
769
793
 
770
794
  impl RecordingTransport {
@@ -789,6 +813,11 @@ impl RecordingTransport {
789
813
  self
790
814
  }
791
815
 
816
+ fn with_tmux_endpoint(self, endpoint: impl Into<String>) -> Self {
817
+ self.with_state(|state| state.tmux_endpoint = Some(endpoint.into()));
818
+ self
819
+ }
820
+
792
821
  fn inject_targets(&self) -> Vec<Target> {
793
822
  self.with_state(|state| state.inject_targets.clone())
794
823
  }
@@ -812,6 +841,10 @@ impl Transport for RecordingTransport {
812
841
  BackendKind::Tmux
813
842
  }
814
843
 
844
+ fn tmux_endpoint(&self) -> Option<String> {
845
+ self.with_state(|state| state.tmux_endpoint.clone())
846
+ }
847
+
815
848
  fn spawn_first(
816
849
  &self,
817
850
  session: &SessionName,
@@ -875,8 +908,18 @@ impl Transport for RecordingTransport {
875
908
  })
876
909
  }
877
910
 
878
- fn query(&self, _target: &Target, _field: PaneField) -> Result<Option<String>, TransportError> {
879
- Ok(None)
911
+ fn query(&self, _target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
912
+ match field {
913
+ PaneField::PaneCurrentCommand => Ok(Some("codex".to_string())),
914
+ PaneField::PaneCurrentPath => Ok(self.with_state(|state| {
915
+ state
916
+ .targets
917
+ .first()
918
+ .and_then(|target| target.current_path.as_ref())
919
+ .map(|path| path.to_string_lossy().into_owned())
920
+ })),
921
+ _ => Ok(None),
922
+ }
880
923
  }
881
924
 
882
925
  fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
@@ -95,6 +95,103 @@ fn worker_spawn_inherits_parent_process_env_for_proxy_and_ca() {
95
95
  );
96
96
  }
97
97
 
98
+ #[test]
99
+ #[serial(env)]
100
+ fn cursor_subscription_proxy_profile_reaches_fresh_and_restart_transport_final_env() {
101
+ let _guard = EnvGuard::set([
102
+ ("HTTPS_PROXY", "https://upper-https:1"),
103
+ ("HTTP_PROXY", "http://upper-http:1"),
104
+ ("ALL_PROXY", "socks5://upper-all:1"),
105
+ ("https_proxy", "https://lower-https:1"),
106
+ ("http_proxy", "http://lower-http:1"),
107
+ ("all_proxy", "socks5://lower-all:1"),
108
+ ("NO_PROXY", "localhost,127.0.0.1"),
109
+ ("no_proxy", "localhost,127.0.0.1"),
110
+ ("GLOBAL_AGENT_HTTPS_PROXY", "https://global-https:1"),
111
+ ("GLOBAL_AGENT_HTTP_PROXY", "http://global-http:1"),
112
+ ]);
113
+
114
+ let launch_direct = compiled_cursor_profile_team("cursor-direct-launch", "direct");
115
+ let launch_transport = RecordingTransport::new().with_session_present(false);
116
+ launch_with_transport(
117
+ &launch_direct.join("team.spec.yaml"),
118
+ false,
119
+ true,
120
+ true,
121
+ &launch_transport,
122
+ )
123
+ .expect("direct Cursor launch fixture should spawn");
124
+ assert_cursor_proxy_spawn(&launch_transport.single_spawn(), true);
125
+
126
+ let restart_direct = compiled_cursor_profile_team("cursor-direct-restart", "direct");
127
+ seed_restart_state_with_profile(&restart_direct, "worker_a", "cursor_proxy");
128
+ let restart_transport = RecordingTransport::new().with_session_present(false);
129
+ restart_with_transport(&restart_direct, true, None, &restart_transport)
130
+ .expect("direct Cursor restart fixture should spawn");
131
+ assert_cursor_proxy_spawn(&restart_transport.single_spawn(), true);
132
+
133
+ let launch_inherit = compiled_cursor_profile_team("cursor-inherit-launch", "inherit");
134
+ let inherit_transport = RecordingTransport::new().with_session_present(false);
135
+ launch_with_transport(
136
+ &launch_inherit.join("team.spec.yaml"),
137
+ false,
138
+ true,
139
+ true,
140
+ &inherit_transport,
141
+ )
142
+ .expect("inherit Cursor launch fixture should spawn");
143
+ assert_cursor_proxy_spawn(&inherit_transport.single_spawn(), false);
144
+
145
+ let restart_inherit = compiled_cursor_profile_team("cursor-inherit-restart", "inherit");
146
+ seed_restart_state_with_profile(&restart_inherit, "worker_a", "cursor_proxy");
147
+ let restart_inherit_transport = RecordingTransport::new().with_session_present(false);
148
+ restart_with_transport(
149
+ &restart_inherit,
150
+ true,
151
+ None,
152
+ &restart_inherit_transport,
153
+ )
154
+ .expect("inherit Cursor restart fixture should spawn");
155
+ assert_cursor_proxy_spawn(&restart_inherit_transport.single_spawn(), false);
156
+ }
157
+
158
+ fn assert_cursor_proxy_spawn(spawn: &RecordedSpawn, direct: bool) {
159
+ let keys = [
160
+ "HTTPS_PROXY",
161
+ "HTTP_PROXY",
162
+ "ALL_PROXY",
163
+ "https_proxy",
164
+ "http_proxy",
165
+ "all_proxy",
166
+ "GLOBAL_AGENT_HTTPS_PROXY",
167
+ "GLOBAL_AGENT_HTTP_PROXY",
168
+ ];
169
+ for key in keys {
170
+ if direct {
171
+ assert!(!spawn.env.contains_key(key), "direct env must omit {key}");
172
+ assert!(
173
+ spawn.env_unset.iter().any(|candidate| candidate == key),
174
+ "direct env_unset must contain {key}; got {:?}",
175
+ spawn.env_unset
176
+ );
177
+ } else {
178
+ assert!(spawn.env.contains_key(key), "inherit env must preserve {key}");
179
+ assert!(
180
+ spawn.env_unset.iter().all(|candidate| candidate != key),
181
+ "inherit env_unset must not contain {key}; got {:?}",
182
+ spawn.env_unset
183
+ );
184
+ }
185
+ }
186
+ for key in ["NO_PROXY", "no_proxy"] {
187
+ assert!(spawn.env.contains_key(key), "NO_PROXY bypass key must remain: {key}");
188
+ assert!(
189
+ spawn.env_unset.iter().all(|candidate| candidate != key),
190
+ "NO_PROXY bypass key must not be unset: {key}"
191
+ );
192
+ }
193
+ }
194
+
98
195
  #[test]
99
196
  fn tmux_control_filter_uses_env_keys_not_substring_in_inherited_values() {
100
197
  let mut env = BTreeMap::from([
@@ -410,6 +507,39 @@ fn compiled_team_dir(label: &str, agents: &[(&str, &str)]) -> PathBuf {
410
507
  compiled_team_dir_for_provider(label, agents, "codex")
411
508
  }
412
509
 
510
+ fn compiled_cursor_profile_team(label: &str, proxy_mode: &str) -> PathBuf {
511
+ let team = tmp_dir(label).join("teamdir");
512
+ std::fs::create_dir_all(team.join("agents")).unwrap();
513
+ std::fs::write(
514
+ team.join("TEAM.md"),
515
+ "---\nname: envteam\nobjective: Cursor proxy environment contract.\nprovider: cursor_agent\n---\n\nTeam.\n",
516
+ )
517
+ .unwrap();
518
+ std::fs::write(
519
+ team.join("agents").join("worker_a.md"),
520
+ "---\nname: worker_a\nrole: Cursor Worker\nprovider: cursor_agent\nmodel: sonnet-4-thinking\nauth_mode: subscription\nprofile: cursor_proxy\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nCursor worker.\n",
521
+ )
522
+ .unwrap();
523
+ for directory in [
524
+ team.join("profiles"),
525
+ team.join(".team/current/profiles"),
526
+ ] {
527
+ std::fs::create_dir_all(&directory).unwrap();
528
+ std::fs::write(
529
+ directory.join("cursor_proxy.env"),
530
+ format!("AUTH_MODE=subscription\nPROFILE_NAME=cursor_proxy\nPROXY_MODE={proxy_mode}\n"),
531
+ )
532
+ .unwrap();
533
+ }
534
+ let spec = team_agent::compiler::compile_team(&team).unwrap();
535
+ std::fs::write(
536
+ team.join("team.spec.yaml"),
537
+ team_agent::model::yaml::dumps(&spec),
538
+ )
539
+ .unwrap();
540
+ team
541
+ }
542
+
413
543
  fn compiled_team_dir_for_provider(label: &str, agents: &[(&str, &str)], provider: &str) -> PathBuf {
414
544
  let team = tmp_dir(label).join("teamdir");
415
545
  std::fs::create_dir_all(team.join("agents")).unwrap();
@@ -450,6 +580,13 @@ fn seed_restart_state(team: &Path, agent_id: &str) {
450
580
  seed_restart_state_as(team, agent_id, "codex");
451
581
  }
452
582
 
583
+ fn seed_restart_state_with_profile(team: &Path, agent_id: &str, profile: &str) {
584
+ seed_restart_state_as(team, agent_id, "cursor_agent");
585
+ let mut state = team_agent::state::persist::load_runtime_state(team).unwrap();
586
+ state["agents"][agent_id]["profile"] = json!(profile);
587
+ team_agent::state::persist::save_runtime_state(team, &state).unwrap();
588
+ }
589
+
453
590
  fn seed_restart_state_as(team: &Path, agent_id: &str, provider: &str) {
454
591
  team_agent::state::persist::save_runtime_state(
455
592
  team,
@@ -544,6 +544,8 @@ pub struct LaunchReport {
544
544
  pub permissions: Vec<PermissionSummary>,
545
545
  /// leader receiver(attach 成功时;经 step10 leader::attach_leader_to_state)。
546
546
  pub leader_receiver_attached: bool,
547
+ pub leader_bind_stage: Option<String>,
548
+ pub leader_bind_reason: Option<String>,
547
549
  pub session_capture_incomplete_agents: Vec<String>,
548
550
  }
549
551
 
@@ -619,6 +621,7 @@ pub enum QuickStartReport {
619
621
  /// [`QuickStartReadiness::PendingToolLoad`] (or `Degraded` if any agent
620
622
  /// failed to spawn) so the CLI surface cannot lie about availability.
621
623
  worker_readiness: QuickStartReadiness,
624
+ team: String,
622
625
  },
623
626
  /// 已有 runtime state → quick-start 拒绝并引导用 restart。
624
627
  ExistingRuntime {
@@ -627,6 +630,7 @@ pub enum QuickStartReport {
627
630
  state_path: Option<PathBuf>,
628
631
  next_actions: Vec<String>,
629
632
  attach_commands: Vec<String>,
633
+ agent_ids: Vec<String>,
630
634
  },
631
635
  /// preflight 阻塞(`quick_start.py:59`)。
632
636
  PreflightBlocked {
@@ -799,6 +803,8 @@ pub enum RestartReport {
799
803
  attach_commands: Vec<String>,
800
804
  /// Debt returned by this restart's best-effort auto-attach, if any.
801
805
  attach_window_failures: Option<serde_json::Value>,
806
+ leader_bind_ok: bool,
807
+ leader_bind_reason: Option<String>,
802
808
  },
803
809
  /// At least one worker failed during live spawn, but other workers were isolated
804
810
  /// and restarted. The CLI reports `status=partial` and exits non-zero.
@@ -815,6 +821,8 @@ pub enum RestartReport {
815
821
  attach_commands: Vec<String>,
816
822
  /// Debt returned by this restart's best-effort auto-attach, if any.
817
823
  attach_window_failures: Option<serde_json::Value>,
824
+ leader_bind_ok: bool,
825
+ leader_bind_reason: Option<String>,
818
826
  },
819
827
  /// All workers failed during live spawn. No worker is reported as restarted.
820
828
  Failed {