@team-agent/installer 0.5.66 → 0.5.68

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 (164) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -0
  4. package/crates/team-agent/src/cli/adapters.rs +5 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +82 -0
  6. package/crates/team-agent/src/cli/emit.rs +3 -3
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +21 -7
  9. package/crates/team-agent/src/cli/leaders.rs +125 -1
  10. package/crates/team-agent/src/cli/mod.rs +20 -0
  11. package/crates/team-agent/src/cli/send/presentation.rs +7 -1
  12. package/crates/team-agent/src/cli/spec.rs +2 -0
  13. package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
  14. package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
  15. package/crates/team-agent/src/cli/tests/base.rs +26 -3
  16. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
  17. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  18. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  19. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  20. package/crates/team-agent/src/compiler.rs +59 -0
  21. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  22. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  23. package/crates/team-agent/src/coordinator/health.rs +173 -0
  24. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  25. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  26. package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
  27. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  29. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  30. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  31. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  32. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  33. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  34. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  35. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  36. package/crates/team-agent/src/coordinator/types.rs +49 -0
  37. package/crates/team-agent/src/db/message_store.rs +39 -5
  38. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  39. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  40. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  41. package/crates/team-agent/src/leader/registry.rs +65 -0
  42. package/crates/team-agent/src/leader/start.rs +920 -63
  43. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  44. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  45. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -12
  46. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +62 -0
  47. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +59 -0
  48. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  49. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  50. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
  51. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -456
  52. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  53. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  54. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  55. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  56. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  57. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
  58. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  59. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  60. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  61. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
  62. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  63. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  64. package/crates/team-agent/src/lifecycle/launch/spawn.rs +62 -2
  65. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +139 -0
  66. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
  67. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  68. package/crates/team-agent/src/lifecycle/launch.rs +99 -29
  69. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  70. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  71. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  72. package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
  73. package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
  74. package/crates/team-agent/src/lifecycle/restart/agent.rs +124 -0
  75. package/crates/team-agent/src/lifecycle/restart/common.rs +369 -11
  76. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  77. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  78. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
  79. package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
  80. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  81. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  82. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  83. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
  84. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  85. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
  86. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
  88. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
  89. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
  90. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  91. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
  92. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  93. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  94. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
  95. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  96. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  97. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
  98. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
  99. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
  100. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
  103. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +2 -0
  104. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
  105. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
  106. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  107. package/crates/team-agent/src/lifecycle/tests.rs +23 -13
  108. package/crates/team-agent/src/lifecycle/types.rs +61 -0
  109. package/crates/team-agent/src/lifecycle/worker_command_context.rs +85 -11
  110. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  111. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  112. package/crates/team-agent/src/messaging/delivery.rs +405 -40
  113. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  114. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  115. package/crates/team-agent/src/messaging/results.rs +4 -0
  116. package/crates/team-agent/src/messaging/send.rs +34 -0
  117. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  118. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  119. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  121. package/crates/team-agent/src/messaging/types.rs +4 -1
  122. package/crates/team-agent/src/model/enums.rs +14 -5
  123. package/crates/team-agent/src/model/permissions.rs +3 -0
  124. package/crates/team-agent/src/os_probe.rs +73 -2
  125. package/crates/team-agent/src/provider/adapter.rs +231 -4
  126. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
  127. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  128. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  129. package/crates/team-agent/src/provider/bypass_flags.rs +46 -2
  130. package/crates/team-agent/src/provider/classify.rs +4 -2
  131. package/crates/team-agent/src/provider/faults.rs +2 -1
  132. package/crates/team-agent/src/provider/mod.rs +11 -0
  133. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  134. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  135. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  136. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  137. package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
  138. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  139. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  140. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  141. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  142. package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
  143. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  144. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  145. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  146. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  147. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  148. package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
  149. package/crates/team-agent/src/provider/types.rs +4 -2
  150. package/crates/team-agent/src/provider/wire.rs +23 -1
  151. package/crates/team-agent/src/state/persist.rs +301 -8
  152. package/crates/team-agent/src/state/repository.rs +5 -0
  153. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  154. package/crates/team-agent/src/tmux_backend.rs +1064 -75
  155. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  156. package/crates/team-agent/src/transport.rs +248 -1
  157. package/npm/install.mjs +129 -73
  158. package/package.json +4 -4
  159. package/skills/team-agent/SKILL.md +33 -238
  160. package/skills/team-agent/command-coverage.json +31 -0
  161. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  162. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
  163. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  164. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
@@ -1,3 +1,24 @@
1
+ //! ---
2
+ //! purpose: coordinator 的共享数据面——协议常量、Pid/WorkspacePath newtype、健康与启停的穷尽状态 enum、metadata/schema/report 结构、以及跨子系统注入用的 trait 占位
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: as_str
6
+ //! what: 把 binary-identity relation 与 metadata mismatch reason 映射成稳定 wire 字符串
7
+ //! - name: as_path
8
+ //! what: 从 WorkspacePath newtype 取回底层路径
9
+ //! depends:
10
+ //! - crate::model::enums
11
+ //! - crate::model::ids
12
+ //! - crate::provider
13
+ //! - crate::message_store
14
+ //! - crate::event_log
15
+ //! boundary:
16
+ //! - 只定义形状,不含任何判定、I/O 或事件写入
17
+ //! - as_str 的返回值是对外稳定契约:已发布的字符串只可新增不可改写
18
+ //! - ProviderRegistry / MarkerStore 只是注入点,实现体不在本模块
19
+ //! maturity: wired
20
+ //! ---
21
+ //!
1
22
  //! coordinator 共享数据面:常量 / newtype / 穷尽 enum / metadata / schema health /
2
23
  //! report 结构 / abnormal-track 数据型 / WatchCursor / cross-dep 占位。
3
24
 
@@ -42,9 +63,19 @@ pub const ROTATION_MARKER: &str =
42
63
  pub struct Pid(pub u32);
43
64
 
44
65
  impl Pid {
66
+ /// ---
67
+ /// purpose: 把裸 OS pid 包成 newtype,杜绝与其他 int id 混传
68
+ /// params:
69
+ /// pid: OS 进程号,不做存活或有效性校验
70
+ /// returns: 承载该 pid 的 newtype
71
+ /// ---
45
72
  pub fn new(pid: u32) -> Self {
46
73
  Self(pid)
47
74
  }
75
+ /// ---
76
+ /// purpose: 取回底层裸 pid,供需要 OS 接口的调用方使用
77
+ /// returns: 构造时传入的 pid 原值
78
+ /// ---
48
79
  pub fn get(self) -> u32 {
49
80
  self.0
50
81
  }
@@ -62,9 +93,19 @@ impl std::fmt::Display for Pid {
62
93
  pub struct WorkspacePath(pub PathBuf);
63
94
 
64
95
  impl WorkspacePath {
96
+ /// ---
97
+ /// purpose: 把一个路径包成 workspace newtype,作为 tick/health/start/stop 的统一 key
98
+ /// params:
99
+ /// p: workspace 根路径。调用方负责先 resolve;本构造不做规范化也不校验存在性
100
+ /// returns: 承载该路径的 newtype
101
+ /// ---
65
102
  pub fn new(p: impl Into<PathBuf>) -> Self {
66
103
  Self(p.into())
67
104
  }
105
+ /// ---
106
+ /// purpose: 借出底层路径,供文件系统与子进程参数使用
107
+ /// returns: 构造时传入的路径原值的借用
108
+ /// ---
68
109
  pub fn as_path(&self) -> &Path {
69
110
  &self.0
70
111
  }
@@ -200,6 +241,10 @@ pub enum CoordinatorBinaryIdentityRelation {
200
241
  }
201
242
 
202
243
  impl CoordinatorBinaryIdentityRelation {
244
+ /// ---
245
+ /// purpose: 把「调用方二进制相对 daemon 二进制」的关系映射成稳定 wire 字符串
246
+ /// returns: 五个稳定串之一(same / caller_newer_than_daemon / daemon_newer_than_caller / same_version_path_mismatch / unknown);这些值进事件与 status 输出,已发布的不可改写
247
+ /// ---
203
248
  pub fn as_str(self) -> &'static str {
204
249
  match self {
205
250
  Self::Same => "same",
@@ -225,6 +270,10 @@ pub enum CoordinatorMetadataMismatchReason {
225
270
  }
226
271
 
227
272
  impl CoordinatorMetadataMismatchReason {
273
+ /// ---
274
+ /// purpose: 把 coordinator metadata 被拒的原因映射成稳定 wire 字符串
275
+ /// returns: 七个稳定串之一(metadata_missing / pid_mismatch / protocol_version_mismatch / message_store_schema_version_mismatch / binary_identity_missing / binary_version_mismatch / binary_path_mismatch);进 HealthReport 与事件,已发布的不可改写
276
+ /// ---
228
277
  pub fn as_str(self) -> &'static str {
229
278
  match self {
230
279
  Self::MetadataMissing => "metadata_missing",
@@ -548,19 +548,26 @@ impl MessageStore {
548
548
 
549
549
  /// `claim_for_delivery` (`core.py:190-205`): atomic single-winner claim. Flips an
550
550
  /// eligible row (status ∈ pending/accepted/queued_until_idle/queued_until_start/
551
- /// queued_stopped/queued_pane_missing) to `target_resolved`, `delivery_attempts +=
552
- /// 1`. Returns `true` iff THIS update matched exactly one row.
551
+ /// queued_stopped/queued_pane_missing, **or** `target_resolved` with a non-null
552
+ /// error the retry/error-bypass path) to `target_resolved`, clears `error`,
553
+ /// `delivery_attempts += 1`. Returns `true` iff THIS update matched exactly one
554
+ /// row. Clearing `error` is the CAS: a concurrent retry cannot also match
555
+ /// `error is not null`.
553
556
  pub fn claim_for_delivery(&self, message_id: &str) -> Result<bool, MessageStoreError> {
554
557
  let conn = crate::db::schema::open_db(&self.path)?;
555
558
  let rows = conn.execute(
556
559
  "update messages
557
560
  set status = 'target_resolved',
558
561
  delivery_attempts = delivery_attempts + 1,
562
+ error = null,
559
563
  updated_at = ?2
560
564
  where message_id = ?1
561
- and status in (
562
- 'pending', 'accepted', 'queued_until_idle', 'queued_until_start',
563
- 'queued_stopped', 'queued_pane_missing', ?3
565
+ and (
566
+ status in (
567
+ 'pending', 'accepted', 'queued_until_idle', 'queued_until_start',
568
+ 'queued_stopped', 'queued_pane_missing', ?3
569
+ )
570
+ or (status = 'target_resolved' and error is not null)
564
571
  )",
565
572
  params![
566
573
  message_id,
@@ -1138,6 +1145,33 @@ mod tests {
1138
1145
  assert_eq!(status_of(&read(&s), &mid), "failed");
1139
1146
  }
1140
1147
 
1148
+ #[test]
1149
+ fn claim_for_delivery_error_retry_is_atomic_single_winner() {
1150
+ let s = store();
1151
+ let mid = s
1152
+ .create_message(Some("t"), "s", "r", "c", None, true, None)
1153
+ .unwrap();
1154
+ s.mark(&mid, "target_resolved", Some("probe_failed"))
1155
+ .unwrap();
1156
+ assert_eq!(
1157
+ col_str(&read(&s), &mid, "error").as_deref(),
1158
+ Some("probe_failed")
1159
+ );
1160
+ assert!(
1161
+ s.claim_for_delivery(&mid).unwrap(),
1162
+ "target_resolved+error must enter the claim gate, not a non-CAS bypass"
1163
+ );
1164
+ let c = read(&s);
1165
+ assert_eq!(status_of(&c, &mid), "target_resolved");
1166
+ assert_eq!(col_str(&c, &mid, "error"), None);
1167
+ assert_eq!(col_i64(&c, &mid, "delivery_attempts"), 1);
1168
+ assert!(
1169
+ !s.claim_for_delivery(&mid).unwrap(),
1170
+ "cleared error must lose the second claim"
1171
+ );
1172
+ assert_eq!(col_i64(&read(&s), &mid, "delivery_attempts"), 1);
1173
+ }
1174
+
1141
1175
  #[test]
1142
1176
  fn claim_for_delivery_never_claims_stored_only_presentation() {
1143
1177
  let s = store();
@@ -72,7 +72,8 @@ const WORKER_IDENTITY_PREFIXES: &[&str] = &["CLAUDE_CODE_"];
72
72
  /// * All POSIX-valid shell identifier keys from `parent_env` EXCEPT
73
73
  /// those matching `STRIP_PREFIXES` / `STRIP_EXACT`.
74
74
  /// * Overlay: `TEAM_AGENT_WORKSPACE`, `TEAM_AGENT_ID`,
75
- /// `TEAM_AGENT_AGENT_ID`, `TEAM_AGENT_OWNER_TEAM_ID` (when present).
75
+ /// `TEAM_AGENT_AGENT_ID`, `TEAM_AGENT_OWNER_TEAM_ID` (when present),
76
+ /// `TEAM_AGENT_AUTH_MODE` (when present).
76
77
  ///
77
78
  /// POSIX-validity filter: shell variable names must match
78
79
  /// `[A-Za-z_][A-Za-z0-9_]*`. Cargo's integration-test runner exports keys
@@ -83,6 +84,7 @@ pub fn worker_spawn_env<I>(
83
84
  workspace: &Path,
84
85
  agent_id: &str,
85
86
  team_id: Option<&str>,
87
+ auth_mode: Option<&str>,
86
88
  ) -> BTreeMap<String, String>
87
89
  where
88
90
  I: IntoIterator<Item = (String, String)>,
@@ -106,6 +108,9 @@ where
106
108
  if let Some(tid) = team_id.filter(|s| !s.is_empty()) {
107
109
  env.insert("TEAM_AGENT_OWNER_TEAM_ID".to_string(), tid.to_string());
108
110
  }
111
+ if let Some(auth) = auth_mode.filter(|s| !s.is_empty()) {
112
+ env.insert("TEAM_AGENT_AUTH_MODE".to_string(), auth.to_string());
113
+ }
109
114
  env
110
115
  }
111
116
 
@@ -223,6 +228,7 @@ mod tests {
223
228
  Path::new("/ws"),
224
229
  "developer",
225
230
  Some("alpha"),
231
+ None,
226
232
  );
227
233
  for stripped in [
228
234
  "TEAM_AGENT_LEADER_PROVIDER",
@@ -248,6 +254,7 @@ mod tests {
248
254
  Path::new("/ws"),
249
255
  "developer",
250
256
  Some("alpha"),
257
+ Some("subscription"),
251
258
  );
252
259
  assert_eq!(
253
260
  env.get("TEAM_AGENT_ID").map(String::as_str),
@@ -265,11 +272,21 @@ mod tests {
265
272
  env.get("TEAM_AGENT_OWNER_TEAM_ID").map(String::as_str),
266
273
  Some("alpha")
267
274
  );
275
+ assert_eq!(
276
+ env.get("TEAM_AGENT_AUTH_MODE").map(String::as_str),
277
+ Some("subscription")
278
+ );
268
279
  }
269
280
 
270
281
  #[test]
271
282
  fn worker_spawn_env_inherits_parent_env_minus_strip_list() {
272
- let env = worker_spawn_env(make_parent_env(&[]), Path::new("/ws"), "developer", None);
283
+ let env = worker_spawn_env(
284
+ make_parent_env(&[]),
285
+ Path::new("/ws"),
286
+ "developer",
287
+ None,
288
+ None,
289
+ );
273
290
  // Inherit-then-strip: every parent key passes through EXCEPT the
274
291
  // strip list. PATH-like + provider creds + random user vars all
275
292
  // present.
@@ -292,7 +309,7 @@ mod tests {
292
309
  #[test]
293
310
  fn worker_spawn_env_drops_posix_invalid_keys() {
294
311
  let parent = make_parent_env(&[("CARGO_BIN_EXE_team-agent", "/bin/x")]);
295
- let env = worker_spawn_env(parent, Path::new("/ws"), "developer", None);
312
+ let env = worker_spawn_env(parent, Path::new("/ws"), "developer", None, None);
296
313
  assert!(!env.contains_key("CARGO_BIN_EXE_team-agent"));
297
314
  }
298
315
 
@@ -17,6 +17,8 @@ pub fn provider_window_name(provider: Provider) -> &'static str {
17
17
  Provider::Codex => "codex",
18
18
  Provider::Copilot => "copilot",
19
19
  Provider::GeminiCli => "gemini",
20
+ Provider::Grok => "grok",
21
+ Provider::CursorAgent => "agent",
20
22
  Provider::Fake => "fake",
21
23
  }
22
24
  }
@@ -17,6 +17,11 @@ const COMMAND_GRAMMAR_PROVIDER_COMMANDS: &[(&str, Provider)] = &[
17
17
  ("codex", Provider::Codex),
18
18
  ("copilot", Provider::Copilot),
19
19
  ("gemini", Provider::GeminiCli),
20
+ ("grok", Provider::Grok),
21
+ // NOTE: Cursor `agent` binary is NOT added here — command-text attribution
22
+ // is a substring contains() match in table order; `"agent"` would also
23
+ // match the framework's own `team-agent` binary and mis-attribute leader
24
+ // panes. CursorAgent panes need env/argv-based attribution (TODO 0.5.67).
20
25
  ("fake", Provider::Fake),
21
26
  ];
22
27
 
@@ -91,11 +96,37 @@ fn provider_from_pid_env(pid: u32) -> Option<Provider> {
91
96
 
92
97
  fn provider_from_command_text(text: &str) -> Option<Provider> {
93
98
  let lower = text.to_ascii_lowercase();
99
+ if cursor_agent_command_text(&lower) {
100
+ return Some(Provider::CursorAgent);
101
+ }
94
102
  COMMAND_GRAMMAR_PROVIDER_COMMANDS
95
103
  .iter()
96
104
  .find_map(|(needle, provider)| lower.contains(needle).then_some(*provider))
97
105
  }
98
106
 
107
+ /// Cursor 二进制名为 `agent`,不能做 substring contains(会误伤 `team-agent`)。
108
+ /// 认精确 token `cursor-agent` / `…/agent`,且必须带 `--sandbox` 或 `--trust`。
109
+ fn cursor_agent_command_text(lower: &str) -> bool {
110
+ let tokens: Vec<&str> = lower.split_whitespace().collect();
111
+ if tokens.iter().any(|tok| {
112
+ *tok == "team-agent"
113
+ || tok.ends_with("/team-agent")
114
+ || tok.ends_with("\\team-agent")
115
+ }) {
116
+ return false;
117
+ }
118
+ let is_cursor_bin = tokens.iter().any(|tok| {
119
+ *tok == "cursor-agent"
120
+ || *tok == "agent"
121
+ || tok.ends_with("/cursor-agent")
122
+ || tok.ends_with("/agent")
123
+ || tok.ends_with("\\cursor-agent")
124
+ || tok.ends_with("\\agent")
125
+ });
126
+ let has_cursor_flag = tokens.iter().any(|tok| *tok == "--sandbox" || *tok == "--trust");
127
+ is_cursor_bin && has_cursor_flag
128
+ }
129
+
99
130
  fn provider_from_env_text(text: &str) -> Option<Provider> {
100
131
  text.split(|ch: char| ch == '\0' || ch.is_ascii_whitespace())
101
132
  .find_map(|part| part.strip_prefix("TEAM_AGENT_LEADER_PROVIDER="))
@@ -180,6 +211,28 @@ mod tests {
180
211
  );
181
212
  }
182
213
 
214
+ #[test]
215
+ fn cursor_agent_basename_with_trust_is_attributed() {
216
+ assert_eq!(
217
+ attribute_command_provider("cursor-agent --trust --sandbox disabled --workspace /ws"),
218
+ Some(Provider::CursorAgent)
219
+ );
220
+ assert_eq!(
221
+ attribute_command_provider("agent --trust --sandbox disabled --workspace /ws"),
222
+ Some(Provider::CursorAgent)
223
+ );
224
+ }
225
+
226
+ #[test]
227
+ fn team_agent_binary_is_not_cursor() {
228
+ assert_ne!(
229
+ attribute_command_provider("team-agent --trust claude"),
230
+ Some(Provider::CursorAgent)
231
+ );
232
+ assert_eq!(attribute_command_provider("team-agent status"), None);
233
+ assert_eq!(attribute_command_provider("agent"), None);
234
+ }
235
+
183
236
  #[test]
184
237
  fn pane_env_provider_wins_over_command() {
185
238
  let mut pane = pane("codex", None);
@@ -243,6 +243,9 @@ pub fn register_binding_from_state_best_effort(
243
243
  );
244
244
  let event_log = crate::event_log::EventLog::new(workspace);
245
245
  let write_result = write_entry_best_effort(&entry);
246
+ if write_result.is_some() {
247
+ let _ = mark_displaced_same_pane_entries_unbound(&entry);
248
+ }
246
249
  let status = if let Some(path) = &write_result {
247
250
  let _ = event_log.write(
248
251
  EVENT_REGISTERED,
@@ -275,6 +278,68 @@ pub fn register_binding_from_state_best_effort(
275
278
  })
276
279
  }
277
280
 
281
+ /// Stored registry status after another workspace claimed the same pane.
282
+ pub const REGISTRY_STATUS_UNBOUND: &str = "unbound";
283
+
284
+ /// When workspace B claims a pane, every other attached registry row that
285
+ /// still names that pane is no longer deliverable. Mark those rows unbound.
286
+ /// Does not change pane-side authorization storage.
287
+ pub fn mark_displaced_same_pane_entries_unbound(winner: &LeaderRegistryEntry) -> Vec<PathBuf> {
288
+ let Some(winner_pane) = winner
289
+ .channel
290
+ .get("pane_id")
291
+ .and_then(Value::as_str)
292
+ .filter(|pane| !pane.is_empty())
293
+ else {
294
+ return Vec::new();
295
+ };
296
+ let winner_socket = winner
297
+ .channel
298
+ .get("tmux_socket")
299
+ .and_then(Value::as_str)
300
+ .filter(|socket| !socket.is_empty());
301
+ let mut displaced = Vec::new();
302
+ for (path, mut entry) in read_all_entries() {
303
+ if entry.workspace_hash == winner.workspace_hash && entry.team_key == winner.team_key {
304
+ continue;
305
+ }
306
+ if entry.status != "attached" {
307
+ continue;
308
+ }
309
+ let Some(pane) = entry
310
+ .channel
311
+ .get("pane_id")
312
+ .and_then(Value::as_str)
313
+ .filter(|pane| !pane.is_empty())
314
+ else {
315
+ continue;
316
+ };
317
+ if pane != winner_pane {
318
+ continue;
319
+ }
320
+ let Some(socket) = entry
321
+ .channel
322
+ .get("tmux_socket")
323
+ .and_then(Value::as_str)
324
+ .filter(|socket| !socket.is_empty())
325
+ else {
326
+ continue;
327
+ };
328
+ let Some(winner_socket) = winner_socket else {
329
+ continue;
330
+ };
331
+ if socket != winner_socket {
332
+ continue;
333
+ }
334
+ entry.status = REGISTRY_STATUS_UNBOUND.to_string();
335
+ entry.updated_at = chrono::Utc::now().to_rfc3339();
336
+ if write_entry_best_effort(&entry).is_some() {
337
+ displaced.push(path);
338
+ }
339
+ }
340
+ displaced
341
+ }
342
+
278
343
  fn entry_filename(entry: &LeaderRegistryEntry) -> String {
279
344
  format!("{}__{}.json", entry.workspace_hash, entry.team_key)
280
345
  }