@team-agent/installer 0.5.65 → 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 (208) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +6 -0
  4. package/crates/team-agent/src/cli/adapters.rs +6 -1
  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/compile.rs +1 -1
  17. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
  18. package/crates/team-agent/src/cli/tests/mod.rs +2 -2
  19. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
  20. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  21. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  22. package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
  23. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  24. package/crates/team-agent/src/compiler/tests.rs +23 -20
  25. package/crates/team-agent/src/compiler.rs +84 -5
  26. package/crates/team-agent/src/conpty/backend.rs +16 -0
  27. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  28. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  29. package/crates/team-agent/src/coordinator/health.rs +173 -0
  30. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  31. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  32. package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
  33. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  34. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  35. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  36. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  37. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  38. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  39. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  40. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  41. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  42. package/crates/team-agent/src/coordinator/types.rs +49 -0
  43. package/crates/team-agent/src/db/message_store.rs +39 -5
  44. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  45. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  46. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  47. package/crates/team-agent/src/leader/registry.rs +65 -0
  48. package/crates/team-agent/src/leader/start.rs +1080 -58
  49. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1 -1
  50. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  51. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  52. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -14
  53. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +69 -2
  54. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +196 -2
  55. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  56. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  57. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
  58. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -457
  59. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  60. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  61. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  62. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  63. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  64. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
  65. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  66. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  67. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  68. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
  69. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  70. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  71. package/crates/team-agent/src/lifecycle/launch/spawn.rs +101 -4
  72. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +244 -48
  73. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -2
  74. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  75. package/crates/team-agent/src/lifecycle/launch.rs +209 -43
  76. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  77. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  78. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  79. package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
  80. package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
  81. package/crates/team-agent/src/lifecycle/restart/agent.rs +213 -3
  82. package/crates/team-agent/src/lifecycle/restart/common.rs +373 -18
  83. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  84. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  85. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -15
  86. package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
  87. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  88. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  89. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  90. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -1
  91. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +3 -4
  92. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +1 -1
  93. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -2
  94. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +4 -4
  95. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  96. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +159 -174
  97. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +2 -2
  98. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +15 -13
  99. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -18
  100. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +1 -1
  101. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
  102. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
  103. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  104. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +1 -1
  105. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +1 -1
  106. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
  107. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  108. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  109. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
  110. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  111. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  112. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -0
  113. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +3 -3
  114. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +65 -40
  115. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +20 -32
  116. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +17 -9
  117. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  118. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +24 -23
  119. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +98 -62
  120. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +2 -2
  121. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +1 -1
  122. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +1 -1
  123. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -1
  124. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -2
  125. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +1 -1
  126. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +1 -1
  127. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +41 -8
  128. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +3 -3
  129. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1 -1
  130. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +1 -1
  131. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -1
  132. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1 -1
  133. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1 -1
  134. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +1 -1
  135. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +60 -2
  136. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -1
  137. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -2
  138. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  139. package/crates/team-agent/src/lifecycle/tests.rs +23 -15
  140. package/crates/team-agent/src/lifecycle/types.rs +61 -2
  141. package/crates/team-agent/src/lifecycle/worker_command_context.rs +200 -33
  142. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  143. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  144. package/crates/team-agent/src/messaging/delivery.rs +405 -40
  145. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  146. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  147. package/crates/team-agent/src/messaging/results.rs +4 -0
  148. package/crates/team-agent/src/messaging/send.rs +34 -0
  149. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  150. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  151. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  152. package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
  153. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  154. package/crates/team-agent/src/messaging/types.rs +4 -1
  155. package/crates/team-agent/src/model/enums.rs +14 -5
  156. package/crates/team-agent/src/model/permissions.rs +3 -0
  157. package/crates/team-agent/src/model/spec.rs +129 -6
  158. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +3 -1
  159. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +3 -1
  160. package/crates/team-agent/src/model/testdata/team.spec.yaml +3 -1
  161. package/crates/team-agent/src/os_probe.rs +73 -2
  162. package/crates/team-agent/src/provider/adapter.rs +231 -4
  163. package/crates/team-agent/src/provider/adapters/claude.rs +5 -2
  164. package/crates/team-agent/src/provider/adapters/codex.rs +5 -2
  165. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -2
  166. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
  167. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  168. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  169. package/crates/team-agent/src/provider/bypass_flags.rs +124 -0
  170. package/crates/team-agent/src/provider/classify.rs +4 -2
  171. package/crates/team-agent/src/provider/faults.rs +2 -1
  172. package/crates/team-agent/src/provider/mod.rs +13 -0
  173. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  174. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  175. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  176. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  177. package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
  178. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  179. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  180. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  181. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  182. package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
  183. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  184. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  185. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  186. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  187. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  188. package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
  189. package/crates/team-agent/src/provider/types.rs +4 -2
  190. package/crates/team-agent/src/provider/wire.rs +23 -1
  191. package/crates/team-agent/src/state/persist.rs +301 -8
  192. package/crates/team-agent/src/state/repository.rs +5 -0
  193. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  194. package/crates/team-agent/src/tmux_backend.rs +1083 -75
  195. package/crates/team-agent/src/transport/test_support.rs +20 -0
  196. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  197. package/crates/team-agent/src/transport.rs +265 -1
  198. package/npm/install.mjs +129 -73
  199. package/package.json +4 -4
  200. package/skills/team-agent/SKILL.md +33 -238
  201. package/skills/team-agent/command-coverage.json +31 -0
  202. package/crates/team-agent/src/lifecycle/launch/approval.rs +0 -134
  203. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  204. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -483
  205. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  206. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
  207. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +0 -1187
  208. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +0 -327
@@ -1,3 +1,13 @@
1
+ //! ---
2
+ //! purpose: coordinator tick session_gate 步骤组的占位命名空间——实现仍在 tick.rs
3
+ //! contract:
4
+ //! provides: []
5
+ //! depends: []
6
+ //! boundary:
7
+ //! - 本文件当前不含任何 item:provider session 捕获与就绪门步骤未来迁入处
8
+ //! - 迁移落地前不要在此新增逻辑,否则步骤顺序会分裂成两处
9
+ //! maturity: signature_only
10
+ //! ---
1
11
  //!
2
12
  //! unit-11 (Stage 4) — coordinator tick `session_gate` step group.
3
13
  //!
@@ -1,3 +1,37 @@
1
+ //! ---
2
+ //! purpose: coordinator 的核心——Coordinator 本体与单次 tick 的固定顺序编排,外加心跳 sidecar 与 worker 运行态解析
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: tick
6
+ //! what: 跑一轮固定顺序的编排:会话门 → 捕获缺失 session → 启动/审批提示 → 健康对账 → 投递与到期事件 → 只读探测 → 原子持久化 → 回收结果
7
+ //! - name: write_coordinator_heartbeat
8
+ //! what: 写 runtime/coordinator_tick.json 心跳 sidecar(阶段、状态、boot id、迭代计数)
9
+ //! - name: read_coordinator_heartbeat
10
+ //! what: 只读取出最近一次心跳,供 status/diagnose 消费
11
+ //! - name: resolve_worker_runtime_state_with_fg_pgrp
12
+ //! what: 由 agent JSON 与活动分类结果解析 worker 运行态,缺信号时判 Unknown 而非 Idle
13
+ //! depends:
14
+ //! - super::health
15
+ //! - super::types
16
+ //! - super::steps
17
+ //! - super::steps::abnormal
18
+ //! - super::runtime_observation
19
+ //! - crate::state
20
+ //! - crate::message_store
21
+ //! - crate::messaging
22
+ //! - crate::leader
23
+ //! - crate::provider
24
+ //! - crate::transport
25
+ //! - crate::event_log
26
+ //! - crate::os_probe
27
+ //! boundary:
28
+ //! - 不实现投递、到期事件与结果回收的本体,那些在 crate::messaging,本模块只按序调用
29
+ //! - 无 pending obligation 时不注入任何探索性 prompt;无既定义务时唯一会投递的是 should_ping 成立时的一条中性提示。例外:屏上出现审批提示且策略允许时,runtime_approval 会经 transport.send_keys 注入应答按键(不是 prompt,是对已观测提示的应答)
30
+ //! - 监测步骤失败降级并继续,TickReport.ok=true 不代表每一步都成功——各步失败只在事件里
31
+ //! - 持久化失败走 degraded 报告而不是 panic,也不是 Err
32
+ //! - 不直接依赖 provider client crate,provider 一律经注入的 ProviderAdapter
33
+ //! maturity: wired
34
+ //! ---
1
35
  //!
2
36
  //! Coordinator core:daemon lifecycle 宿主 + 单次 tick 编排(19 步固定顺序)+ health/start/stop。
3
37
 
@@ -166,6 +200,14 @@ pub struct Coordinator {
166
200
 
167
201
  impl Coordinator {
168
202
  /// 构造(注入 provider registry + transport)。spawn 出的 daemon 在 `run` 前装配它。
203
+ /// ---
204
+ /// purpose: 装配一个 Coordinator——注入 provider registry 与 transport,两者都是 trait object 以便替换与 mock
205
+ /// params:
206
+ /// workspace: 本实例服务的 workspace 根
207
+ /// provider_registry: provider adapter 解析器;provider 一律经它取,绝不直连 provider client crate
208
+ /// transport: 会话/pane 控制面,用于存活探测与注入
209
+ /// returns: 未绑定 team_key、无 save 钩子、无顺序探针的实例
210
+ /// ---
169
211
  pub fn new(
170
212
  workspace: WorkspacePath,
171
213
  provider_registry: Box<dyn ProviderRegistry>,
@@ -181,6 +223,12 @@ impl Coordinator {
181
223
  }
182
224
  }
183
225
 
226
+ /// ---
227
+ /// purpose: 绑定 daemon 侧选定的 team_key,让 tick 的状态投影用它而不是可能过期的根 active_team_key
228
+ /// params:
229
+ /// team_key: 目标 team;None 或空串都视为未绑定,tick 会回落到 state 推导
230
+ /// returns: 绑定后的自身
231
+ /// ---
184
232
  pub(crate) fn with_team_key(mut self, team_key: Option<String>) -> Self {
185
233
  self.daemon_team_key = team_key.filter(|key| !key.is_empty());
186
234
  self
@@ -190,6 +238,17 @@ impl Coordinator {
190
238
  /// transport + mock provider registry + 可选 save 注入钩 + ORDER 探针。**纯 test-support
191
239
  /// 脚手架**(真实 impl,非 `unimplemented!()`):它只装配字段,不执行任何 daemon 逻辑;
192
240
  /// tick/health/start/stop 仍是 `unimplemented!()` 生产体,因此调它们的契约仍 RED。
241
+ /// ---
242
+ /// purpose: 测试装配口——直接构出实例并注入 save 钩子与步骤顺序探针
243
+ /// params:
244
+ /// workspace: workspace 根
245
+ /// provider_registry: 可 mock 的 provider adapter 解析器
246
+ /// transport: 可 mock 的传输控制面
247
+ /// save_hook: 替换真实持久化,用于制造持久化失败
248
+ /// order_recorder: 记录 tick 内各步骤名,用于断言固定顺序
249
+ /// returns: 只装配字段的实例,本函数自身不执行任何 daemon 逻辑
250
+ /// cfg: test
251
+ /// ---
193
252
  #[cfg(test)]
194
253
  pub(crate) fn for_test(
195
254
  workspace: WorkspacePath,
@@ -226,6 +285,11 @@ impl Coordinator {
226
285
  /// `state::save_runtime_state`,bug-084 测试注入失败);在每个 step8-11 原子调用点
227
286
  /// `if let Some(rec) = &self.order_recorder { rec.lock()...push(STEP_NAME) }`(tick
228
287
  /// 副作用 ORDER 测试断言固定序列)。生产两者均 `None`,零开销。
288
+ /// ---
289
+ /// purpose: 跑一轮 tick——按固定顺序把各步骤串起来,只投递既定义务,绝不凭空注入
290
+ /// returns: TickReport。传输会话不在了 → stop=true 且 reason=TmuxSessionMissing,主循环据此退出;持久化失败 → ok=false、reason=PersistenceDegraded、persisted=Some(false),但仍是 Ok;正常 → ok=true 并带本轮投递、到期事件、回收结果等清单。注意 ok=true 只说明主干走完,监测类步骤失败已被降级吞掉,真相只在事件日志里
291
+ /// errors: 加载状态、打开 message store、传输探测或事件写入这些主干操作失败时返回 TickError,由主循环 catch 后退避
292
+ /// ---
229
293
  pub fn tick(&self) -> Result<TickReport, TickError> {
230
294
  self.record_step(TickStepGroup::SessionGate, "load_state");
231
295
  let raw_state = crate::state::persist::load_runtime_state(self.workspace.as_path())?;
@@ -271,17 +335,12 @@ impl Coordinator {
271
335
  }
272
336
 
273
337
  self.record_step(TickStepGroup::SessionGate, "capture_missing");
274
- let pending_context_fork_audits =
275
- match self.capture_missing_sessions(&mut state, &event_log) {
276
- Ok(audits) => audits,
277
- Err(error) => {
278
- let _ = event_log.write(
279
- "coordinator.tick.capture_missing_failed",
280
- serde_json::json!({"error": error.to_string()}),
281
- );
282
- Vec::new()
283
- }
284
- };
338
+ if let Err(error) = self.capture_missing_sessions(&mut state, &event_log) {
339
+ let _ = event_log.write(
340
+ "coordinator.tick.capture_missing_failed",
341
+ serde_json::json!({"error": error.to_string()}),
342
+ );
343
+ }
285
344
 
286
345
  // Slice 1 energy gate: one pane snapshot per tick feeds probe eligibility,
287
346
  // health sync, and abnormal-exit detection. Missing panes are filtered
@@ -461,15 +520,6 @@ impl Coordinator {
461
520
  collections,
462
521
  ));
463
522
  }
464
- for context_fork in &pending_context_fork_audits {
465
- context_fork.write_audit(&event_log).map_err(|error| {
466
- eprintln!(
467
- "[coordinator] context_fork audit publish failed after state commit: {error}"
468
- );
469
- TickError::EventLog(error)
470
- })?;
471
- }
472
-
473
523
  self.record_step(TickStepGroup::Delivery, "collect_results");
474
524
  collections.results =
475
525
  collect_results(crate::messaging::collect_results_and_notify_watchers(
@@ -489,7 +539,7 @@ impl Coordinator {
489
539
  &self,
490
540
  state: &mut Value,
491
541
  event_log: &EventLog,
492
- ) -> Result<Vec<crate::lifecycle::launch::ContextForkFinalized>, TickError> {
542
+ ) -> Result<(), TickError> {
493
543
  let report = crate::session_capture::capture_missing_provider_sessions_once(
494
544
  state,
495
545
  &mut |provider| self.provider_registry.adapter_for(provider),
@@ -653,7 +703,7 @@ impl Coordinator {
653
703
  }),
654
704
  )?;
655
705
  }
656
- Ok(report.context_forks)
706
+ Ok(())
657
707
  }
658
708
 
659
709
  fn sync_agent_health(
@@ -1371,6 +1421,11 @@ impl Coordinator {
1371
1421
 
1372
1422
  /// `coordinator_health`(`lifecycle.py:26`)。pid + meta + schema 三合一健康。
1373
1423
  /// doctor / start 前置调它。`ok = running ∧ metadata_ok ∧ schema_ok`。
1424
+ /// ---
1425
+ /// purpose: 取本 workspace 的 coordinator 健康报告
1426
+ /// returns: 与 health::coordinator_health 完全一致的报告
1427
+ /// errors: 当前实现恒为 Ok;返回类型保留 Result 以便未来健康判定引入可失败路径
1428
+ /// ---
1374
1429
  pub fn health(&self) -> Result<HealthReport, TickError> {
1375
1430
  Ok(super::health::coordinator_health(&self.workspace))
1376
1431
  }
@@ -1379,11 +1434,23 @@ impl Coordinator {
1379
1434
  /// schema 不兼容拒启给 hint;否则 spawn 自身二进制子命令(`team-agent coordinator --workspace ..`,
1380
1435
  /// Python 是 `python -m team_agent.coordinator`,`lifecycle.py:108`)。
1381
1436
  /// **schema 兼容门**:三元任一不匹配 → restart_incompatible,**不可静默继续**(card §89)。
1437
+ /// ---
1438
+ /// purpose: 幂等启动本 workspace 的 daemon 子进程
1439
+ /// returns: 与 health::start_coordinator 完全一致的报告;「拒启」体现为 ok=false 而非 Err
1440
+ /// errors: 建目录、开日志、spawn 或写 pid/metadata 失败时返回 StartError
1441
+ /// ---
1382
1442
  pub fn start(&self) -> Result<StartReport, StartError> {
1383
1443
  super::health::start_coordinator(&self.workspace)
1384
1444
  }
1385
1445
 
1386
1446
  /// `stop_coordinator`(`lifecycle.py:229`)。SIGTERM + 清 pid/meta。pid 非整数 → 清文件返回。
1447
+ /// ---
1448
+ /// purpose: 清掉本 workspace 的 coordinator pid/meta 文件
1449
+ /// returns: pid 文件不存在 → Missing;文件在且内容可解析 → Stopped 并带该 pid;文件在但内容非法 → InvalidPidRemoved
1450
+ /// errors: 删文件失败时返回 StopError
1451
+ /// ---
1452
+ /// 注意:本方法只删文件,**不向 daemon 发任何信号**——与 health::stop_coordinator
1453
+ /// 的「真终止进程」语义不同。调它之后 daemon 仍会继续 tick,而健康探测会报 Missing。
1387
1454
  pub fn stop(&self) -> Result<StopReport, StopError> {
1388
1455
  let pid_path = coordinator_pid_path(&self.workspace);
1389
1456
  if !pid_path.exists() {
@@ -1412,6 +1479,10 @@ impl Coordinator {
1412
1479
 
1413
1480
  /// `message_store_schema_health`(`lifecycle.py:197`)。DB 列兼容门:区分 pre-init 必需列缺失
1414
1481
  /// (拒启)vs migratable 列缺失(可迁移)。`doctor --fix-schema` 用其 action hint。
1482
+ /// ---
1483
+ /// purpose: 查本队 message store 的 schema 兼容门
1484
+ /// returns: 与 health::message_store_schema_health 一致;打不开库时 ok=false 并带原文与修复 hint
1485
+ /// ---
1415
1486
  pub fn schema_health(&self) -> SchemaHealth {
1416
1487
  // A-8: the gate must inspect the REAL team.db (Python lifecycle.py:197+
1417
1488
  // message_store_schema_health); a hardcoded ok:true left the card §89
@@ -1525,6 +1596,21 @@ fn increment_coordinator_tick_iteration_count(workspace: &WorkspacePath) {
1525
1596
  );
1526
1597
  }
1527
1598
 
1599
+ /// ---
1600
+ /// purpose: 写心跳 sidecar runtime/coordinator_tick.json,让外部不必进程内也能看到 daemon 走到哪一步
1601
+ /// params:
1602
+ /// workspace: workspace 根
1603
+ /// pid: 当前 daemon pid
1604
+ /// boot_id: daemon 世代 id;None 时沿用文件里已有值,都没有则用 coord_unknown_<pid>
1605
+ /// last_phase: 本次写入代表的阶段名;值为 tick_running 时刷新 last_tick_started_at,否则刷新 last_tick_finished_at
1606
+ /// last_tick_status: 上一轮 tick 的状态串
1607
+ /// last_error: 上一轮的错误串
1608
+ /// increment_count: 是否把迭代计数加一
1609
+ /// returns: 写成功返回 ();先写临时文件再 rename,读方不会看到半份
1610
+ /// errors: 建目录、写临时文件或 rename 失败时返回 io::Error
1611
+ /// ---
1612
+ /// ⚠ 字段 `host_boot_time` 写的是**本次写入时刻的墙钟**,不是主机启动时间——
1613
+ /// 每次心跳都会变。主机启动身份在 `host_boot_id`,按名消费 host_boot_time 会拿到假事实。
1528
1614
  pub(crate) fn write_coordinator_heartbeat(
1529
1615
  workspace: &WorkspacePath,
1530
1616
  pid: Pid,
@@ -1597,6 +1683,10 @@ fn coordinator_heartbeat_path(workspace: &WorkspacePath) -> PathBuf {
1597
1683
  /// falls back to a formatted timestamp. Returns None when the platform
1598
1684
  /// signal cannot be read — the caller MUST NOT guess; downstream
1599
1685
  /// treats a missing/mismatched host boot separately from a matched one.
1686
+ /// ---
1687
+ /// purpose: 探测当前主机的启动身份,用来识别「所有 pane/pid/会话绑定都早于本次开机」
1688
+ /// returns: Linux 读 boot_id 得 linux-<uuid>;macOS 读 kern.boottime 得 macos-<...>;测试可用 TEAM_AGENT_TEST_HOST_BOOT_ID 覆盖。平台信号读不到时为 None——调用方必须把「读不到」与「不匹配」分开处理,不许猜
1689
+ /// ---
1600
1690
  pub(crate) fn probe_host_boot_id() -> Option<String> {
1601
1691
  if let Ok(override_id) = std::env::var("TEAM_AGENT_TEST_HOST_BOOT_ID") {
1602
1692
  if !override_id.is_empty() {
@@ -1634,6 +1724,12 @@ pub(crate) fn probe_host_boot_id() -> Option<String> {
1634
1724
  /// error (missing file, malformed JSON) returns None — callers treat
1635
1725
  /// that as "unknown, fall back to existing facts" per locate §8 risk
1636
1726
  /// note, they must never guess a stale/fresh verdict from absence.
1727
+ /// ---
1728
+ /// purpose: 只读取出最近一次写入的心跳 sidecar
1729
+ /// params:
1730
+ /// workspace: workspace 根
1731
+ /// returns: 解析成功才有值;文件缺失或 JSON 损坏一律 None。调用方须把 None 当作「未知」回落到既有事实,不得据此断定新鲜或陈旧
1732
+ /// ---
1637
1733
  pub fn read_coordinator_heartbeat(workspace: &WorkspacePath) -> Option<Value> {
1638
1734
  let path = coordinator_heartbeat_path(workspace);
1639
1735
  let text = std::fs::read_to_string(path).ok()?;
@@ -2529,6 +2625,13 @@ fn write_activity(
2529
2625
  /// conflict (or probe unavailable + activity idle).
2530
2626
  /// 5. Unknown — missing pane_pid, probe error, or no decisive
2531
2627
  /// signal. Iron law: never silently Idle.
2628
+ /// ---
2629
+ /// purpose: 由 agent 状态与活动分类结果解析出 worker 的运行态
2630
+ /// params:
2631
+ /// agent: agent 状态对象;读 pane_pid、awaiting_human_confirm 与审批/信任标志
2632
+ /// activity: JSONL 活动分类结果;None 表示本轮没有分类信号
2633
+ /// returns: 按优先级判定——先 Blocked(等人确认/等信任提示),再用前台进程组探测判 Busy,再由活动分类给 Busy/ProbablyIdle;任何一步缺信号都落到 Unknown。铁律是绝不在信息不足时静默判 Idle
2634
+ /// ---
2532
2635
  pub(crate) fn resolve_worker_runtime_state_with_fg_pgrp(
2533
2636
  agent: &Value,
2534
2637
  activity: Option<&crate::messaging::AgentActivity>,
@@ -2612,6 +2715,14 @@ fn agent_health_status_wire(status: crate::messaging::ActivityStatus) -> &'stati
2612
2715
  /// 0.4.x Phase 1: read the worker_state wire string that `write_activity`
2613
2716
  /// persisted alongside the legacy activity. Returns None when no worker_state
2614
2717
  /// has been written yet (older state row pre-upgrade).
2718
+ /// ---
2719
+ /// purpose: 读回 write_activity 与活动一起持久化的 worker_state 线字符串
2720
+ /// params:
2721
+ /// agent: agent 状态对象
2722
+ /// returns: 已写过 worker_state 时解析出运行态;升级前的旧状态行没有该字段,返回 None
2723
+ /// ---
2724
+ /// 现状:本仓内没有任何调用点(仅定义),与其他 pub(crate) 项不同——按机器事实记录,
2725
+ /// 不据此推断它「已接线」。
2615
2726
  pub(crate) fn agent_worker_state(agent: &Value) -> Option<crate::messaging::WorkerRuntimeState> {
2616
2727
  agent
2617
2728
  .get("worker_state")
@@ -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);