@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
@@ -116,12 +116,357 @@ pub(super) fn quick_start_session_capture_incomplete_agents(
116
116
  /// ---
117
117
  /// Host registry is the deliverability authority. Workspace `state.json`
118
118
  /// is only a copy. Detection failure is unbound, never attached.
119
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
120
+ pub enum LeaderBindingClass {
121
+ Attached,
122
+ IndexMissing,
123
+ Unknown,
124
+ Unbound,
125
+ }
126
+
127
+ impl LeaderBindingClass {
128
+ pub fn issue_id(self) -> &'static str {
129
+ match self {
130
+ Self::Attached => "leader_receiver_attached",
131
+ Self::IndexMissing => "leader_registry_index_missing",
132
+ Self::Unknown => "leader_binding_unknown",
133
+ Self::Unbound => "leader_receiver_unbound",
134
+ }
135
+ }
136
+
137
+ pub fn repair(self) -> &'static str {
138
+ match self {
139
+ Self::Attached => "",
140
+ Self::IndexMissing => {
141
+ "publish the leader registry index for the selected team; do not claim-leader"
142
+ }
143
+ Self::Unknown => {
144
+ "inspect the selected-team registry file and live leader channel; do not claim-leader"
145
+ }
146
+ Self::Unbound => "team-agent claim-leader --confirm --json",
147
+ }
148
+ }
149
+ }
150
+
151
+ pub fn selected_team_leader_receiver<'a>(
152
+ state: &'a serde_json::Value,
153
+ team_key: &str,
154
+ ) -> Option<&'a serde_json::Value> {
155
+ if let Some(teams) = state.get("teams").and_then(serde_json::Value::as_object) {
156
+ if let Some(team) = teams.get(team_key) {
157
+ return team.get("leader_receiver");
158
+ }
159
+ }
160
+ state.get("leader_receiver")
161
+ }
162
+
163
+ fn owner_record<'a>(state: &'a serde_json::Value, team_key: &str) -> Option<&'a serde_json::Value> {
164
+ let owner = if let Some(team) = state
165
+ .get("teams")
166
+ .and_then(serde_json::Value::as_object)
167
+ .and_then(|teams| teams.get(team_key))
168
+ {
169
+ team.get("team_owner")
170
+ } else {
171
+ state.get("team_owner")
172
+ }?;
173
+ owner.as_object().map(|_| owner)
174
+ }
175
+
176
+ fn canonical_identity_aligned(
177
+ workspace: &Path,
178
+ team_key: &str,
179
+ state: &serde_json::Value,
180
+ receiver: &serde_json::Value,
181
+ entry: &crate::leader::registry::LeaderRegistryEntry,
182
+ ) -> bool {
183
+ let Some(owner) = owner_record(state, team_key) else {
184
+ return false;
185
+ };
186
+ let owner_pane = owner.get("pane_id").and_then(serde_json::Value::as_str);
187
+ let receiver_pane = receiver.get("pane_id").and_then(serde_json::Value::as_str);
188
+ let owner_epoch = owner.get("owner_epoch").and_then(serde_json::Value::as_u64);
189
+ let receiver_epoch = receiver
190
+ .get("owner_epoch")
191
+ .and_then(serde_json::Value::as_u64);
192
+ let channel_pane = entry
193
+ .channel
194
+ .get("pane_id")
195
+ .and_then(serde_json::Value::as_str)
196
+ .or_else(|| entry.channel.get("pane").and_then(serde_json::Value::as_str));
197
+ entry.team_key == team_key
198
+ && entry.status == "attached"
199
+ && receiver.get("status").and_then(serde_json::Value::as_str) == Some("attached")
200
+ && owner_pane == receiver_pane
201
+ && owner_pane.is_some()
202
+ && owner_epoch == receiver_epoch
203
+ && owner_epoch == Some(entry.owner_epoch)
204
+ && channel_pane.is_none_or(|pane| Some(pane) == owner_pane)
205
+ && crate::leader::registry::workspace_hash(workspace) == entry.workspace_hash
206
+ && {
207
+ let receiver_socket = receiver
208
+ .get("tmux_socket")
209
+ .and_then(serde_json::Value::as_str);
210
+ let channel_socket = entry
211
+ .channel
212
+ .get("tmux_socket")
213
+ .and_then(serde_json::Value::as_str);
214
+ channel_socket.is_none_or(|socket| Some(socket) == receiver_socket)
215
+ }
216
+ }
217
+
218
+ pub fn classify_leader_binding(workspace: &Path, team_key: &str) -> LeaderBindingClass {
219
+ let state = match load_runtime_state(workspace) {
220
+ Ok(state) => state,
221
+ Err(_) => return LeaderBindingClass::Unknown,
222
+ };
223
+ let receiver = selected_team_leader_receiver(&state, team_key);
224
+ let transport: Box<dyn crate::transport::Transport> = match receiver
225
+ .and_then(|value| value.get("tmux_socket"))
226
+ .and_then(serde_json::Value::as_str)
227
+ .filter(|endpoint| !endpoint.is_empty() && *endpoint != "default")
228
+ {
229
+ Some(endpoint) => crate::transport_factory::leader_endpoint_transport(endpoint),
230
+ None => return classify_leader_binding_at(workspace, team_key, &state, None),
231
+ };
232
+ classify_leader_binding_at(workspace, team_key, &state, Some(transport.as_ref()))
233
+ }
234
+
235
+ pub(crate) fn classify_leader_binding_at(
236
+ workspace: &Path,
237
+ team_key: &str,
238
+ state: &serde_json::Value,
239
+ transport: Option<&dyn crate::transport::Transport>,
240
+ ) -> LeaderBindingClass {
241
+ let registry = registry_deliverability(workspace, team_key);
242
+ let receiver = selected_team_leader_receiver(state, team_key);
243
+ let receiver_attached = receiver
244
+ .and_then(|value| value.get("status"))
245
+ .and_then(serde_json::Value::as_str)
246
+ == Some("attached");
247
+ let has_owner = owner_record(state, team_key).is_some();
248
+ match registry {
249
+ RegistryDeliverability::Undecidable => LeaderBindingClass::Unknown,
250
+ RegistryDeliverability::Attached => {
251
+ let Some(receiver) = receiver else {
252
+ return LeaderBindingClass::Unknown;
253
+ };
254
+ let dir = crate::leader::registry::registry_dir();
255
+ let entry = dir.and_then(|dir| {
256
+ let path = dir.join(format!(
257
+ "{}__{team_key}.json",
258
+ crate::leader::registry::workspace_hash(workspace)
259
+ ));
260
+ std::fs::read_to_string(path)
261
+ .ok()
262
+ .and_then(|text| {
263
+ serde_json::from_str::<crate::leader::registry::LeaderRegistryEntry>(&text)
264
+ .ok()
265
+ })
266
+ });
267
+ let Some(entry) = entry else {
268
+ return LeaderBindingClass::Unknown;
269
+ };
270
+ if !canonical_identity_aligned(workspace, team_key, state, receiver, &entry) {
271
+ return LeaderBindingClass::Unknown;
272
+ }
273
+ let Some(transport) = transport else {
274
+ return LeaderBindingClass::Unknown;
275
+ };
276
+ match crate::messaging::leader_channel::resolve_live_leader_channel(
277
+ workspace, receiver, transport,
278
+ ) {
279
+ crate::messaging::leader_channel::LeaderChannelResolution::Live(_) => {
280
+ LeaderBindingClass::Attached
281
+ }
282
+ _ => LeaderBindingClass::Unknown,
283
+ }
284
+ }
285
+ RegistryDeliverability::Unbound if has_owner || receiver_attached => {
286
+ LeaderBindingClass::IndexMissing
287
+ }
288
+ RegistryDeliverability::Unbound => LeaderBindingClass::Unbound,
289
+ }
290
+ }
291
+
119
292
  pub fn launched_team_receiver_is_attached(workspace: &Path, team_key: &str) -> bool {
120
- match registry_deliverability(workspace, team_key) {
121
- RegistryDeliverability::Attached => {}
122
- RegistryDeliverability::Unbound | RegistryDeliverability::Undecidable => return false,
293
+ classify_leader_binding(workspace, team_key) == LeaderBindingClass::Attached
294
+ }
295
+
296
+ #[cfg(test)]
297
+ mod claim_rework_class_tests {
298
+ use super::*;
299
+ use crate::transport::test_support::OfflineTransport;
300
+ use crate::transport::{PaneId, PaneInfo, SessionName};
301
+ use serde_json::json;
302
+
303
+ fn isolated() -> (std::path::PathBuf, std::path::PathBuf, Option<std::ffi::OsString>) {
304
+ let root = std::env::temp_dir().join(format!(
305
+ "ta-class-{}-{}",
306
+ std::process::id(),
307
+ std::time::SystemTime::now()
308
+ .duration_since(std::time::UNIX_EPOCH)
309
+ .unwrap()
310
+ .as_nanos()
311
+ ));
312
+ let home = root.join("home");
313
+ let workspace = root.join("ws");
314
+ std::fs::create_dir_all(&home).unwrap();
315
+ std::fs::create_dir_all(&workspace).unwrap();
316
+ let previous = std::env::var_os("HOME");
317
+ std::env::set_var("HOME", &home);
318
+ (root, workspace, previous)
319
+ }
320
+
321
+ fn restore_home(previous: Option<std::ffi::OsString>, root: &Path) {
322
+ match previous {
323
+ Some(home) => std::env::set_var("HOME", home),
324
+ None => std::env::remove_var("HOME"),
325
+ }
326
+ let _ = std::fs::remove_dir_all(root);
327
+ }
328
+
329
+ #[test]
330
+ #[serial_test::serial(env)]
331
+ fn classify_bad_registry_json_is_unknown() {
332
+ let (root, workspace, previous) = isolated();
333
+ crate::state::persist::save_runtime_state(
334
+ &workspace,
335
+ &json!({"teams": {"alpha": {"team_owner": {"pane_id": "%1", "owner_epoch": 1}}}}),
336
+ )
337
+ .unwrap();
338
+ let dir = crate::leader::registry::registry_dir().unwrap();
339
+ std::fs::create_dir_all(&dir).unwrap();
340
+ let path = dir.join(format!(
341
+ "{}__alpha.json",
342
+ crate::leader::registry::workspace_hash(&workspace)
343
+ ));
344
+ std::fs::write(&path, "{not json").unwrap();
345
+ let class = classify_leader_binding(&workspace, "alpha");
346
+ restore_home(previous, &root);
347
+ assert_eq!(class, LeaderBindingClass::Unknown);
348
+ }
349
+
350
+ #[test]
351
+ #[serial_test::serial(env)]
352
+ fn classify_null_owner_is_not_index_missing() {
353
+ let (root, workspace, previous) = isolated();
354
+ crate::state::persist::save_runtime_state(
355
+ &workspace,
356
+ &json!({"teams": {"alpha": {"team_owner": null}}}),
357
+ )
358
+ .unwrap();
359
+ let class = classify_leader_binding(&workspace, "alpha");
360
+ restore_home(previous, &root);
361
+ assert_eq!(class, LeaderBindingClass::Unbound);
362
+ }
363
+
364
+ fn live_pane(workspace: &Path, pane: &str) -> PaneInfo {
365
+ PaneInfo {
366
+ pane_id: PaneId::new(pane),
367
+ session: SessionName::new("s"),
368
+ window_index: None,
369
+ window_name: None,
370
+ pane_index: None,
371
+ tty: None,
372
+ current_command: Some("codex".to_string()),
373
+ current_path: Some(workspace.to_path_buf()),
374
+ active: true,
375
+ pane_pid: None,
376
+ leader_env: Default::default(),
377
+ }
378
+ }
379
+
380
+ #[test]
381
+ #[serial_test::serial(env)]
382
+ fn classify_attached_requires_identity_and_live() {
383
+ let (root, workspace, previous) = isolated();
384
+ let endpoint = "/tmp/ta-class-live.sock";
385
+ let state = json!({
386
+ "teams": {
387
+ "alpha": {
388
+ "team_owner": {"pane_id": "%1", "owner_epoch": 3, "provider": "codex"},
389
+ "leader_receiver": {
390
+ "status": "attached",
391
+ "pane_id": "%1",
392
+ "owner_epoch": 3,
393
+ "tmux_socket": endpoint
394
+ }
395
+ }
396
+ }
397
+ });
398
+ crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
399
+ let outcome = crate::leader::registry::register_binding_from_state_best_effort(
400
+ &workspace,
401
+ Some("alpha"),
402
+ "classify-attached",
403
+ );
404
+ assert_eq!(
405
+ outcome.as_ref().map(|value| value.status),
406
+ Some("registered")
407
+ );
408
+ let transport = OfflineTransport::default()
409
+ .with_tmux_endpoint(endpoint)
410
+ .with_targets(vec![live_pane(&workspace, "%1")]);
411
+ let class = crate::transport_factory::with_leader_endpoint_transport(
412
+ endpoint,
413
+ transport,
414
+ || classify_leader_binding(&workspace, "alpha"),
415
+ );
416
+ restore_home(previous, &root);
417
+ assert_eq!(class, LeaderBindingClass::Attached);
418
+ }
419
+
420
+ #[test]
421
+ #[serial_test::serial(env)]
422
+ fn classify_registry_socket_mismatch_is_unknown() {
423
+ let (root, workspace, previous) = isolated();
424
+ let endpoint = "/tmp/ta-class-live.sock";
425
+ let state = json!({
426
+ "teams": {
427
+ "alpha": {
428
+ "team_owner": {"pane_id": "%1", "owner_epoch": 3, "provider": "codex"},
429
+ "leader_receiver": {
430
+ "status": "attached",
431
+ "pane_id": "%1",
432
+ "owner_epoch": 3,
433
+ "tmux_socket": endpoint
434
+ }
435
+ }
436
+ }
437
+ });
438
+ crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
439
+ let mut entry = crate::leader::registry::build_entry(
440
+ &workspace,
441
+ "alpha",
442
+ "direct_tmux",
443
+ json!({
444
+ "pane_id": "%1",
445
+ "tmux_socket": "/tmp/other-leader.sock"
446
+ }),
447
+ 3,
448
+ "mismatch",
449
+ "2026-01-01T00:00:00Z".to_string(),
450
+ );
451
+ entry.status = "attached".to_string();
452
+ let dir = crate::leader::registry::registry_dir().unwrap();
453
+ std::fs::create_dir_all(&dir).unwrap();
454
+ let path = dir.join(format!(
455
+ "{}__alpha.json",
456
+ crate::leader::registry::workspace_hash(&workspace)
457
+ ));
458
+ std::fs::write(&path, serde_json::to_vec(&entry).unwrap()).unwrap();
459
+ let transport = OfflineTransport::default()
460
+ .with_tmux_endpoint(endpoint)
461
+ .with_targets(vec![live_pane(&workspace, "%1")]);
462
+ let class = crate::transport_factory::with_leader_endpoint_transport(
463
+ endpoint,
464
+ transport,
465
+ || classify_leader_binding(&workspace, "alpha"),
466
+ );
467
+ restore_home(previous, &root);
468
+ assert_eq!(class, LeaderBindingClass::Unknown);
123
469
  }
124
- load_runtime_state(workspace).is_ok()
125
470
  }
126
471
 
127
472
  enum RegistryDeliverability {
@@ -322,18 +322,18 @@ pub(super) fn spawn_agents(
322
322
  if matches!(provider, Provider::CursorAgent) {
323
323
  refuse_second_cursor_occupant(workspace, agent_id_raw, Some(spec))?;
324
324
  apply_cursor_agent_rules_overlay(workspace, agent_id_raw, system_prompt.as_str())?;
325
- apply_cursor_mcp_overlay(workspace, &mcp_config)?;
326
- let project = if crate::lifecycle::launch::cursor_mcp_isolation_enabled() {
327
- Some(crate::lifecycle::launch::cursor_mcp_project_dir(
328
- workspace,
329
- agent_id_raw,
330
- )?)
331
- } else {
332
- None
333
- };
334
- enable_cursor_workspace_mcp(workspace, project.as_deref())?;
325
+ let project = crate::lifecycle::launch::prepare_cursor_seat_mcp(
326
+ workspace,
327
+ agent_id_raw,
328
+ &mcp_config,
329
+ )?;
330
+ enable_cursor_workspace_mcp_with_profile(
331
+ workspace,
332
+ project.as_deref(),
333
+ Some(&profile_launch),
334
+ )?;
335
335
  apply_cursor_spawn_workspace_pointers(&mut plan.argv, workspace, agent_id_raw)?;
336
- let proxy = apply_cursor_subscription_proxy_env(&mut env);
336
+ let proxy = apply_cursor_subscription_proxy_env_with_profile(&mut env, &profile_launch);
337
337
  let event_log = crate::event_log::EventLog::new(workspace);
338
338
  let _ = event_log.write(
339
339
  "provider.cursor.proxy_presence",
@@ -203,7 +203,7 @@ pub(super) fn seed_launched_owner_from_env(
203
203
  /// lookup_pane_provider: 由 pane 反查 provider 的函数,便于测试替换
204
204
  /// returns: true 表示已写入 owner 记录
205
205
  /// ---
206
- pub(super) fn seed_launched_owner_from_caller_with_provider_lookup(
206
+ pub fn seed_launched_owner_from_caller_with_provider_lookup(
207
207
  state: &mut serde_json::Value,
208
208
  caller: crate::state::owner_gate::CallerIdentity,
209
209
  lookup_pane_provider: impl Fn(&PaneId) -> Option<Provider>,
@@ -227,12 +227,12 @@ pub(super) fn seed_launched_owner_from_caller_with_provider_lookup(
227
227
  });
228
228
  let mut receiver = serde_json::json!({
229
229
  "mode": "direct_tmux",
230
- "status": "attached",
230
+ "status": "pending",
231
231
  "pane_id": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
232
232
  "pane": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
233
233
  "leader_session_uuid": owner.get("leader_session_uuid").cloned().unwrap_or(serde_json::Value::Null),
234
234
  "owner_epoch": owner_epoch,
235
- "discovery": "quick_start",
235
+ "discovery": "quick_start_seed",
236
236
  });
237
237
  if let Some(provider) = provider.as_ref() {
238
238
  if let Some(owner) = owner.as_object_mut() {
@@ -452,6 +452,38 @@ pub fn worker_session_name_pub(spec: &Value) -> SessionName {
452
452
  spec_session_name(spec)
453
453
  }
454
454
 
455
+ #[cfg(test)]
456
+ mod claim_rework_seed_tests {
457
+ use super::*;
458
+ use crate::state::owner_gate::CallerIdentity;
459
+
460
+ #[test]
461
+ fn seed_writes_pending_receiver_not_attached() {
462
+ let mut state = serde_json::json!({
463
+ "session_name": "team-seed",
464
+ "workspace": "/tmp/claim-rework-seed"
465
+ });
466
+ let caller = CallerIdentity {
467
+ pane_id: "%1".to_string(),
468
+ provider: "codex".to_string(),
469
+ machine_fingerprint: "fp".to_string(),
470
+ leader_session_uuid: "uuid".to_string(),
471
+ leader_session_uuid_source: "env".to_string(),
472
+ };
473
+ assert!(seed_launched_owner_from_caller_with_provider_lookup(
474
+ &mut state,
475
+ caller,
476
+ |_| None
477
+ ));
478
+ let status = state
479
+ .pointer("/teams/team-seed/leader_receiver/status")
480
+ .or_else(|| state.pointer("/leader_receiver/status"))
481
+ .and_then(serde_json::Value::as_str);
482
+ assert_eq!(status, Some("pending"));
483
+ assert_ne!(status, Some("attached"));
484
+ }
485
+ }
486
+
455
487
  /// ---
456
488
  /// purpose: 取出 spec 里全部 agent id
457
489
  /// returns: 按 spec 顺序的 agent id,无 id 字段的条目被跳过
@@ -505,6 +505,22 @@ pub fn apply_cursor_subscription_proxy_env(
505
505
  }
506
506
  }
507
507
 
508
+ pub(crate) fn apply_cursor_subscription_proxy_env_with_profile(
509
+ env: &mut BTreeMap<String, String>,
510
+ profile_launch: &crate::provider::ProviderProfileLaunch,
511
+ ) -> CursorProxyPresence {
512
+ if crate::lifecycle::profile_launch::cursor_subscription_direct_mode(profile_launch) {
513
+ for key in &profile_launch.env_unset {
514
+ env.remove(key);
515
+ }
516
+ return CursorProxyPresence {
517
+ https_proxy: false,
518
+ no_proxy: env.keys().any(|key| key.eq_ignore_ascii_case("NO_PROXY")),
519
+ };
520
+ }
521
+ apply_cursor_subscription_proxy_env(env)
522
+ }
523
+
508
524
  /// ---
509
525
  /// purpose: 把 profile 推出的环境清除与覆盖套用到 spawn 环境上
510
526
  /// params:
@@ -210,6 +210,8 @@ pub fn launch_with_transport_in_workspace(
210
210
  routes,
211
211
  permissions,
212
212
  leader_receiver_attached: false,
213
+ leader_bind_stage: None,
214
+ leader_bind_reason: None,
213
215
  session_capture_incomplete_agents: Vec::new(),
214
216
  })
215
217
  }
@@ -309,6 +311,20 @@ pub(crate) use mcp_config::{
309
311
 
310
312
  pub(crate) mod pi_mcp;
311
313
 
314
+ pub(crate) fn run_pi_catalog_preflight(
315
+ agents_dir: &std::path::Path,
316
+ discover: &mut dyn FnMut(&str) -> Result<Vec<String>, ()>,
317
+ ) -> Result<(), crate::lifecycle::LifecycleError> {
318
+ crate::compiler::preflight_pi_models_in_team_with(agents_dir, discover).map_err(|error| {
319
+ crate::lifecycle::LifecycleError::PiModelPreflight {
320
+ requested: error.requested,
321
+ candidates: error.candidates,
322
+ action: error.action,
323
+ not_ready: error.not_ready,
324
+ }
325
+ })
326
+ }
327
+
312
328
  mod cursor_mcp_iso;
313
329
  pub use cursor_mcp_iso::{
314
330
  cursor_mcp_isolation_enabled, cursor_mcp_json_path, cursor_mcp_project_dir,
@@ -317,9 +333,11 @@ pub use cursor_mcp_iso::{
317
333
  mod cursor_mcp;
318
334
  pub use cursor_mcp::{
319
335
  apply_cursor_mcp_overlay, apply_cursor_spawn_workspace_pointers,
320
- apply_cursor_workspace_physical_path, cursor_mcp_enable_argv, enable_cursor_workspace_mcp,
321
- physical_workspace_path, refuse_second_cursor_occupant,
336
+ apply_cursor_workspace_physical_path, cursor_mcp_enable_argv, cursor_mcp_enable_working_dir,
337
+ enable_cursor_workspace_mcp, enable_cursor_workspace_mcp_with_profile, physical_workspace_path,
338
+ prepare_cursor_seat_mcp, refuse_second_cursor_occupant,
322
339
  };
340
+ pub(crate) use cursor_mcp::apply_cursor_mcp_enable_profile_env;
323
341
 
324
342
  mod cursor_create_chat;
325
343
 
@@ -327,7 +345,8 @@ mod worker_env;
327
345
  pub(super) use worker_env::*;
328
346
  pub(crate) use worker_env::{
329
347
  apply_copilot_instructions_overlay, apply_cursor_agent_rules_overlay,
330
- apply_mcp_auto_approval_env, apply_profile_launch_env, auth_mode_env_value,
348
+ apply_cursor_subscription_proxy_env_with_profile, apply_mcp_auto_approval_env,
349
+ apply_profile_launch_env, auth_mode_env_value,
331
350
  fill_spawn_placeholders, fill_spawn_placeholders_full, inherited_env_with_team_overrides,
332
351
  persist_command_plan_state, spawn_timestamp,
333
352
  };
@@ -358,7 +377,10 @@ pub(crate) use quick_start_transport::{
358
377
  };
359
378
 
360
379
  pub mod readiness;
361
- pub use readiness::launched_team_receiver_is_attached;
380
+ pub use readiness::{
381
+ classify_leader_binding, launched_team_receiver_is_attached, selected_team_leader_receiver,
382
+ LeaderBindingClass,
383
+ };
362
384
  pub(super) use readiness::*;
363
385
 
364
386
  mod add_agent;
@@ -388,14 +410,15 @@ pub(super) use ownership::*;
388
410
  pub(crate) use ownership::{ensure_owner_allowed, ensure_owner_allowed_for_state, state_path};
389
411
 
390
412
  pub mod spec_state;
391
- pub use spec_state::worker_session_name_pub;
413
+ pub use spec_state::{
414
+ seed_launched_owner_from_caller_with_provider_lookup, worker_session_name_pub,
415
+ };
392
416
  pub(crate) use spec_state::{
393
417
  effective_runtime_config_for_worker_spawn, effective_runtime_config_for_worker_spawn_json,
394
418
  };
395
419
  use spec_state::{
396
420
  env_nonempty, has_positive_caller_leader_env, initial_runtime_state,
397
- override_spec_display_backend, override_spec_runtime_str,
398
- seed_launched_owner_from_caller_with_provider_lookup, seed_launched_owner_from_env,
421
+ override_spec_display_backend, override_spec_runtime_str, seed_launched_owner_from_env,
399
422
  spec_agent_values, spec_agents, spec_default_assignee, spec_routes, spec_session_name,
400
423
  spec_tasks_json, team_workspace, yaml_value_to_json,
401
424
  };
@@ -46,6 +46,26 @@ const COMPATIBLE_NETWORK_ENV_KEYS: &[&str] = &[
46
46
  "REQUESTS_CA_BUNDLE",
47
47
  ];
48
48
 
49
+ // Cursor subscription direct mode removes only proxy URL inputs. NO_PROXY is
50
+ // deliberately retained: it is a bypass list, not a proxy endpoint, and the
51
+ // observed catalog failure came from the GLOBAL_AGENT proxy URL settings.
52
+ const CURSOR_DIRECT_PROXY_ENV_KEYS: &[&str] = &[
53
+ "HTTPS_PROXY",
54
+ "HTTP_PROXY",
55
+ "ALL_PROXY",
56
+ "https_proxy",
57
+ "http_proxy",
58
+ "all_proxy",
59
+ "GLOBAL_AGENT_HTTPS_PROXY",
60
+ "GLOBAL_AGENT_HTTP_PROXY",
61
+ ];
62
+
63
+ pub(crate) fn cursor_subscription_direct_mode(profile_launch: &ProviderProfileLaunch) -> bool {
64
+ CURSOR_DIRECT_PROXY_ENV_KEYS
65
+ .iter()
66
+ .all(|key| profile_launch.env_unset.contains(*key))
67
+ }
68
+
49
69
  #[derive(Debug, Clone)]
50
70
  pub(crate) struct ProfileValues {
51
71
  pub(crate) path: PathBuf,
@@ -234,6 +254,15 @@ fn prepare_profile_launch(
234
254
  env_overlay.remove(*key);
235
255
  }
236
256
  }
257
+ if agent.provider == Provider::CursorAgent
258
+ && agent.auth_mode == AuthMode::Subscription
259
+ && profile_proxy_mode(&loaded.values) == "direct"
260
+ {
261
+ for key in CURSOR_DIRECT_PROXY_ENV_KEYS {
262
+ env_unset.insert((*key).to_string());
263
+ env_overlay.remove(*key);
264
+ }
265
+ }
237
266
 
238
267
  let command_overrides = provider_command_overrides(&agent, &loaded.values);
239
268
  write_runtime_env_file(workspace, &agent.id, &env_overlay, &env_unset)?;
@@ -353,6 +382,12 @@ fn validate_profile(
353
382
  profile: &str,
354
383
  loaded: &ProfileValues,
355
384
  ) -> Result<(), LifecycleError> {
385
+ let proxy_mode = profile_proxy_mode(&loaded.values);
386
+ if !matches!(proxy_mode.as_str(), "direct" | "inherit") {
387
+ return Err(LifecycleError::RequirementUnmet(format!(
388
+ "profile {profile} has invalid proxy mode {proxy_mode}; expected direct|inherit"
389
+ )));
390
+ }
356
391
  if loaded
357
392
  .values
358
393
  .get("AUTH_MODE")
@@ -481,9 +516,8 @@ fn provider_env_exports(
481
516
  }
482
517
  }
483
518
  }
484
- // grok/cursor_agent 一期无 profile exports(subscription 已登录态)。
485
- // Cursor 订阅席的代理透传不在这里:profile 只服务 compatible-api。
486
- // 订阅路径见 apply_cursor_subscription_proxy_env(只记有无,不记值)。
519
+ // grok/cursor_agent 一期无 credential/profile exports(subscription 已登录态)。
520
+ // Cursor 订阅席默认继承代理;PROXY_MODE=direct 的局部清除在上方处理。
487
521
  Provider::Grok | Provider::CursorAgent | Provider::Pi => {}
488
522
  Provider::Fake => {}
489
523
  }