@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
@@ -18,6 +18,15 @@ fn e23_fallback_pane_has_single_shared_noisy_surface() {
18
18
  .unwrap(),
19
19
  "primary-ok guard must run before any fallback audit/inject attempt"
20
20
  );
21
+ let already = leader_receiver
22
+ .split("fn message_already_delivered")
23
+ .nth(1)
24
+ .unwrap_or("");
25
+ assert!(
26
+ already.contains("submitted_pending_acceptance")
27
+ && already.contains("injected_awaiting_receipt"),
28
+ "fallback already-delivered set must include the live post-submit statuses, not only the dead string submitted"
29
+ );
21
30
  }
22
31
 
23
32
  #[test]
@@ -99,6 +99,72 @@ fn attached_leader_mailbox_is_rechecked_by_the_normal_delivery_tick() {
99
99
  assert!(case.events().contains("leader_receiver.mailbox_requeued"));
100
100
  }
101
101
 
102
+ #[test]
103
+ fn claimed_leader_delivery_revalidates_a_stale_unattached_snapshot() {
104
+ let workspace = tmp_ws("leader-inject-acceptance-stale-attach");
105
+ let rollout = workspace.join("leader.jsonl");
106
+ std::fs::write(&rollout, "").unwrap();
107
+ let current_state = serde_json::json!({
108
+ "active_team_key": "acceptance-team",
109
+ "session_name": "team-acceptance",
110
+ "leader_receiver": {
111
+ "pane_id": "%leader",
112
+ "status": "attached",
113
+ "provider": "claude",
114
+ "rollout_path": rollout,
115
+ },
116
+ "teams": {
117
+ "acceptance-team": {
118
+ "session_name": "team-acceptance",
119
+ }
120
+ }
121
+ });
122
+ crate::state::persist::save_runtime_state(&workspace, &current_state).unwrap();
123
+ let store = MessageStore::open(&workspace).unwrap();
124
+ let event_log = EventLog::new(&workspace);
125
+ let message_id = store
126
+ .create_message(
127
+ None,
128
+ "worker",
129
+ "leader",
130
+ "stale attach canary",
131
+ None,
132
+ false,
133
+ Some("acceptance-team"),
134
+ )
135
+ .unwrap();
136
+ let stale_state = serde_json::json!({
137
+ "active_team_key": "acceptance-team",
138
+ "teams": {
139
+ "acceptance-team": {
140
+ "session_name": "team-acceptance"
141
+ }
142
+ }
143
+ });
144
+ let transport = RecordingTransport::new("");
145
+
146
+ let outcome = deliver_pending_message(
147
+ &workspace,
148
+ &store,
149
+ &transport,
150
+ &message_id,
151
+ &event_log,
152
+ &stale_state,
153
+ )
154
+ .expect("delivery from stale coordinator snapshot");
155
+
156
+ assert_ne!(
157
+ outcome.reason,
158
+ Some(DeliveryRefusal::LeaderNotAttached),
159
+ "a claim made after attach must revalidate durable state before writing leader_not_attached"
160
+ );
161
+ assert_ne!(
162
+ outcome.message_status.0, "failed",
163
+ "a stale pre-attach snapshot must not strand the requeued row"
164
+ );
165
+ assert_eq!(transport.inject_count(), 1);
166
+ }
167
+
102
168
  #[test]
103
169
  fn leader_submit_without_receipt_source_cannot_park_forever_without_a_completion_writer() {
104
170
  let case = Case::new_without_rollout("missing-receipt-source");
@@ -296,6 +296,7 @@ impl Drop for EnvGuardMsg {
296
296
  }
297
297
 
298
298
  mod basic;
299
+ mod dup_inject;
299
300
  mod e23;
300
301
  mod leader_channel;
301
302
  mod leader_inject_acceptance;
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
5
5
 
6
6
  use crate::model::enums::Provider;
7
7
  use crate::model::ids::{LeaderSessionUuid, OwnerEpoch, TaskId, TeamKey};
8
- use crate::transport::PaneId;
8
+ use crate::transport::{PaneId, TurnVerification};
9
9
 
10
10
  use super::helpers::MessageStatusShadow;
11
11
 
@@ -308,6 +308,9 @@ pub struct DeliveryOutcome {
308
308
  pub reason: Option<DeliveryRefusal>,
309
309
  /// fallback / broadcast / fanout 等通道标注 (`channel` 字段)。
310
310
  pub channel: Option<String>,
311
+ /// 入箱 vs 开跑。None = 本路径没观测(JSON 必须落成 not_yet_observed,不得报开跑)。
312
+ /// Gap42:只是 metadata,不是投递闸门。
313
+ pub turn_verification: Option<TurnVerification>,
311
314
  }
312
315
 
313
316
  /// trust retry payload (card §45;`delivery.py:273` scheduled_events.payload_json 的 typed 视图)。
@@ -21,6 +21,12 @@ pub enum Provider {
21
21
  /// AGENTS.md + `COPILOT_CUSTOM_INSTRUCTIONS_DIRS` env(B2 灵魂件降级,§C2)。
22
22
  Copilot,
23
23
  GeminiCli,
24
+ /// Grok CLI (0.5.67 provider-adapter). Append system prompt via `--rules`,
25
+ /// bypass `--always-approve`, session `--session-id`.
26
+ Grok,
27
+ /// Cursor `agent` CLI (0.5.67 provider-adapter). Append system prompt via
28
+ /// workspace `.cursor/rules/*.mdc` (方案 1 变体), bypass `--force`.
29
+ CursorAgent,
24
30
  Fake,
25
31
  }
26
32
 
@@ -33,7 +39,8 @@ pub enum Provider {
33
39
  /// Provider support:
34
40
  /// - claude / claude_code: low|medium|high|xhigh|max → `--effort <level>`
35
41
  /// - codex: low|medium|high|xhigh (NOT max) → `-c model_reasoning_effort=<level>`
36
- /// - copilot / gemini_cli / fake: unsupported warning event, no flag
42
+ /// - grok: low|medium|high|xhigh (NOT max) `--effort <level>`
43
+ /// - copilot / gemini_cli / cursor_agent / fake: unsupported — warning event, no flag
37
44
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
38
45
  pub enum ProviderEffort {
39
46
  #[serde(rename = "low")]
@@ -78,13 +85,15 @@ impl ProviderEffort {
78
85
  }
79
86
 
80
87
  /// True when the given provider supports this effort level. `max` is
81
- /// Claude-only; other levels are supported by Claude and Codex, ignored
82
- /// by Copilot/Gemini/Fake (warning emitted at runtime).
88
+ /// Claude-only; other levels are supported by Claude, Codex, and Grok.
89
+ /// Copilot/Gemini/CursorAgent/Fake ignore the field (warning at runtime).
83
90
  pub fn is_supported_by(self, provider: Provider) -> bool {
84
91
  match provider {
85
92
  Provider::Claude | Provider::ClaudeCode => true,
86
- Provider::Codex => !self.is_claude_only(),
87
- Provider::Copilot | Provider::GeminiCli | Provider::Fake => false,
93
+ Provider::Codex | Provider::Grok => !self.is_claude_only(),
94
+ Provider::Copilot | Provider::GeminiCli | Provider::CursorAgent | Provider::Fake => {
95
+ false
96
+ }
88
97
  }
89
98
  }
90
99
  }
@@ -189,6 +189,9 @@ fn provider_enforcement(provider: Provider, tool: Tool) -> Enforcement {
189
189
  FsRead | FsWrite | FsList | ExecuteBash | GitDiff | ProviderBuiltin => Hard,
190
190
  },
191
191
  Provider::Fake => Hard,
192
+ // 0.5.67: grok/cursor_agent 无 deny-kind 概念 (同 codex 精神), 诚实
193
+ // prompt_only, 不替 provider 决。
194
+ Provider::Grok | Provider::CursorAgent => PromptOnly,
192
195
  // Copilot(C-2-1 cr verdict):execute_bash/fs_write/network/mcp_team = hard,
193
196
  // fs_read/fs_list/git_diff/provider_builtin = prompt_only(诚实:copilot 无
194
197
  // 对应 deny kind,framework 不替决,留给 provider prompt 控制;MUST-NOT-13)。
@@ -1,10 +1,31 @@
1
+ //! ---
2
+ //! purpose: 为运行时提供有界的进程输出读取与前台进程组探测
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: bounded_command_output_with_probe
6
+ //! what: 在固定超时内执行命令并读取 stdout,超时后终止子进程
7
+ //! - name: pane_foreground_and_root_pgrp
8
+ //! what: 读取 pane PID 的前台与根进程组 ID,无法判定时返回 None
9
+ //! depends:
10
+ //! - std
11
+ //! boundary:
12
+ //! - 只负责有界探测、临时 stdout 文件的独占创建与清理
13
+ //! - 不决定 shutdown 状态,不重试探测,不修改调用方行为
14
+ //! arch:
15
+ //! allowed_dependencies:
16
+ //! - std
17
+ //! maturity: wired
18
+ //! ---
19
+
1
20
  use std::cell::RefCell;
2
21
  use std::fs::OpenOptions;
3
22
  use std::io::{self, Read};
4
23
  use std::process::{Command, ExitStatus, Stdio};
24
+ use std::sync::atomic::{AtomicU64, Ordering};
5
25
  use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
6
26
 
7
27
  const DEFAULT_TIMEOUT: Duration = Duration::from_millis(900);
28
+ static NEXT_TEMP_OUTPUT_ID: AtomicU64 = AtomicU64::new(0);
8
29
 
9
30
  thread_local! {
10
31
  static PROBE_TIMEOUT: RefCell<Option<ProbeTimeout>> = const { RefCell::new(None) };
@@ -23,19 +44,31 @@ pub(crate) struct BoundedCommandOutput {
23
44
  pub(crate) stdout: Vec<u8>,
24
45
  }
25
46
 
47
+ /// ---
48
+ /// purpose: 清除当前线程记录的探测超时
49
+ /// ---
26
50
  pub(crate) fn clear_probe_timeout() {
27
51
  PROBE_TIMEOUT.with(|timeout| *timeout.borrow_mut() = None);
28
52
  }
29
53
 
54
+ /// ---
55
+ /// purpose: 判断当前线程是否记录了探测超时
56
+ /// ---
30
57
  pub(crate) fn probe_timed_out() -> bool {
31
58
  PROBE_TIMEOUT.with(|timeout| timeout.borrow().is_some())
32
59
  }
33
60
 
61
+ /// ---
62
+ /// purpose: 获取当前线程记录的探测超时详情
63
+ /// ---
34
64
  pub(crate) fn probe_timeout() -> Option<ProbeTimeout> {
35
65
  PROBE_TIMEOUT.with(|timeout| timeout.borrow().clone())
36
66
  }
37
67
 
38
68
  #[cfg(test)]
69
+ /// ---
70
+ /// purpose: 为单元测试设置探测超时详情
71
+ /// ---
39
72
  pub(crate) fn set_probe_timeout_for_test(probe: &'static str, pid: Option<u32>, timeout_ms: u64) {
40
73
  PROBE_TIMEOUT.with(|current| {
41
74
  *current.borrow_mut() = Some(ProbeTimeout {
@@ -46,6 +79,9 @@ pub(crate) fn set_probe_timeout_for_test(probe: &'static str, pid: Option<u32>,
46
79
  });
47
80
  }
48
81
 
82
+ /// ---
83
+ /// purpose: 执行一个带默认超时与探测标识的命令并读取 stdout
84
+ /// ---
49
85
  pub(crate) fn bounded_command_output_with_probe(
50
86
  command: &mut Command,
51
87
  probe: &'static str,
@@ -114,9 +150,14 @@ fn temp_output_path(kind: &str) -> std::path::PathBuf {
114
150
  .duration_since(UNIX_EPOCH)
115
151
  .map(|duration| duration.as_nanos())
116
152
  .unwrap_or(0);
153
+ temp_output_path_with_nanos(kind, nanos)
154
+ }
155
+
156
+ fn temp_output_path_with_nanos(kind: &str, nanos: u128) -> std::path::PathBuf {
157
+ let unique_id = NEXT_TEMP_OUTPUT_ID.fetch_add(1, Ordering::Relaxed);
117
158
  std::env::temp_dir().join(format!(
118
- "team-agent-os-probe-{}-{nanos}.{kind}",
119
- std::process::id()
159
+ "team-agent-os-probe-{}-{nanos}-{unique_id}.{kind}",
160
+ std::process::id(),
120
161
  ))
121
162
  }
122
163
 
@@ -146,6 +187,9 @@ fn read_and_remove(path: &std::path::Path) -> Vec<u8> {
146
187
  ///
147
188
  /// Bounded via [`run_bounded_command`] so a wedged ps cannot stall the
148
189
  /// coordinator tick.
190
+ /// ---
191
+ /// purpose: 读取 pane PID 的前台与根进程组 ID,无法判定时返回 None
192
+ /// ---
149
193
  pub(crate) fn pane_foreground_and_root_pgrp(pane_pid: u32) -> io::Result<Option<(u32, u32)>> {
150
194
  let mut command = Command::new("ps");
151
195
  command.args(["-o", "tpgid=,pgid=", "-p", &pane_pid.to_string()]);
@@ -206,4 +250,31 @@ mod fg_pgrp_tests {
206
250
  pane_foreground_and_root_pgrp(0xFFFF_FFFE).expect("must not error on missing pid");
207
251
  assert!(result.is_none(), "missing pid → None");
208
252
  }
253
+
254
+ #[test]
255
+ fn temp_output_create_new_requires_atomic_discriminator() {
256
+ let fixed_nanos = 0x5eed_u128;
257
+ let first_path = temp_output_path_with_nanos("stdout", fixed_nanos);
258
+ let second_path = temp_output_path_with_nanos("stdout", fixed_nanos);
259
+ let _ = std::fs::remove_file(&first_path);
260
+ let _ = std::fs::remove_file(&second_path);
261
+
262
+ let first = OpenOptions::new()
263
+ .create_new(true)
264
+ .read(true)
265
+ .write(true)
266
+ .open(&first_path)
267
+ .expect("first atomic path create");
268
+ let second = OpenOptions::new()
269
+ .create_new(true)
270
+ .read(true)
271
+ .write(true)
272
+ .open(&second_path)
273
+ .expect("second atomic path create");
274
+ assert_ne!(first_path, second_path, "atomic discriminator must advance");
275
+ drop(first);
276
+ drop(second);
277
+ let _ = std::fs::remove_file(first_path);
278
+ let _ = std::fs::remove_file(second_path);
279
+ }
209
280
  }
@@ -342,6 +342,22 @@ impl ProviderAdapter for BasicProviderAdapter {
342
342
  native_mcp_config: false,
343
343
  writes_global_settings: true,
344
344
  },
345
+ // Grok: --resume/--fork-session 齐全 → resume/fork true; 无原生
346
+ // --mcp-config flag → native_mcp_config false (MCP 走 .grok/config.toml)。
347
+ Provider::Grok => ProviderCaps {
348
+ resume: true,
349
+ fork: true,
350
+ native_mcp_config: false,
351
+ writes_global_settings: false,
352
+ },
353
+ // Cursor agent: --resume [chatId] 有 → resume true; 无 fork flag →
354
+ // fork false; 无 --mcp-config → native_mcp_config false。
355
+ Provider::CursorAgent => ProviderCaps {
356
+ resume: true,
357
+ fork: false,
358
+ native_mcp_config: false,
359
+ writes_global_settings: false,
360
+ },
345
361
  Provider::Fake => ProviderCaps {
346
362
  resume: false,
347
363
  fork: false,
@@ -389,6 +405,9 @@ impl ProviderAdapter for BasicProviderAdapter {
389
405
  // 标"weak / no auth-status command available";Compatible/Official 走 BYOK
390
406
  // 路径,有 COPILOT_PROVIDER_BASE_URL 时已脱离 GitHub 登录通道。
391
407
  Provider::Copilot => copilot_auth_hint(auth_mode),
408
+ // Cursor: 登录探测未接。U-17 前不得把未观测写成 Present(shim 对
409
+ // `cursor-agent status` 会假绿 Logged in)。缺键 ≠ 已知未登录,故不用 Missing。
410
+ Provider::CursorAgent => AuthHintStatus::Unknown,
392
411
  _ => match auth_mode {
393
412
  AuthMode::Subscription => AuthHintStatus::Present,
394
413
  AuthMode::OfficialApi | AuthMode::CompatibleApi => AuthHintStatus::MissingOrUnknown,
@@ -454,6 +473,24 @@ impl ProviderAdapter for BasicProviderAdapter {
454
473
  }
455
474
  Ok(argv)
456
475
  }
476
+ Provider::Grok => Ok(grok_launch_command(
477
+ self,
478
+ auth_mode,
479
+ mcp_config,
480
+ system_prompt,
481
+ model,
482
+ tools,
483
+ )?),
484
+ Provider::CursorAgent => Ok(cursor_agent_base_command(
485
+ self,
486
+ auth_mode,
487
+ mcp_config,
488
+ system_prompt,
489
+ model,
490
+ tools,
491
+ false,
492
+ None,
493
+ )?),
457
494
  Provider::Fake => Ok(fake_worker_command()),
458
495
  }
459
496
  }
@@ -543,6 +580,32 @@ impl ProviderAdapter for BasicProviderAdapter {
543
580
  // * `-C <workspace>`(双保险,即便 spawn cwd 漂移也能锚定)
544
581
  // mcp_config inline 形态由 build_command 写入,launch 路径会用
545
582
  // point_native_mcp_config_at_file 重写为 @<file> 形(§C1 note)。
583
+ Provider::Grok => {
584
+ // Align with Claude/Copilot: the UUID on `--session-id` is
585
+ // also the capture hint. `CommandPlan::argv_only` used to
586
+ // drop expected_session_id, so `_pending_session_id` was
587
+ // never written and restart could not resume.
588
+ let expected = next_session_token();
589
+ let mut argv = grok_base_command(
590
+ self,
591
+ ctx.auth_mode,
592
+ ctx.mcp_config,
593
+ ctx.system_prompt,
594
+ ctx.model,
595
+ ctx.tools,
596
+ ctx.profile_launch
597
+ .is_some_and(|profile| profile.managed_mcp_config),
598
+ ctx.effort,
599
+ )?;
600
+ argv.push("--session-id".to_string());
601
+ argv.push(expected.clone());
602
+ Ok(CommandPlan {
603
+ argv,
604
+ expected_session_id: Some(SessionId::new(expected)),
605
+ provider_projects_root: None,
606
+ managed_mcp_config: false,
607
+ })
608
+ }
546
609
  Provider::Copilot => {
547
610
  let expected = next_session_token();
548
611
  let mut argv = copilot_base_command(
@@ -561,6 +624,27 @@ impl ProviderAdapter for BasicProviderAdapter {
561
624
  managed_mcp_config: false,
562
625
  })
563
626
  }
627
+ Provider::CursorAgent => {
628
+ // U-01 create-chat 无登录超时;禁止发明 --session-id。
629
+ // pending 只能是 CLI chatId,本拍不造假 uuid。
630
+ let argv = cursor_agent_base_command(
631
+ self,
632
+ ctx.auth_mode,
633
+ ctx.mcp_config,
634
+ ctx.system_prompt,
635
+ ctx.model,
636
+ ctx.tools,
637
+ ctx.profile_launch
638
+ .is_some_and(|profile| profile.managed_mcp_config),
639
+ ctx.effort,
640
+ )?;
641
+ Ok(CommandPlan {
642
+ argv,
643
+ expected_session_id: None,
644
+ provider_projects_root: None,
645
+ managed_mcp_config: false,
646
+ })
647
+ }
564
648
  _ => self
565
649
  .build_command_with_tools(
566
650
  ctx.auth_mode,
@@ -725,6 +809,40 @@ impl ProviderAdapter for BasicProviderAdapter {
725
809
  argv.push(session_id.as_str().to_string());
726
810
  Ok(argv)
727
811
  }
812
+ // Grok resume: base + `--resume <id>` (grok -r/--resume 接受 id|title)。
813
+ Provider::Grok => {
814
+ let mut argv = grok_base_command(
815
+ self,
816
+ auth_mode,
817
+ mcp_config,
818
+ system_prompt,
819
+ model,
820
+ tools,
821
+ false,
822
+ None,
823
+ )?;
824
+ argv.push("--resume".to_string());
825
+ argv.push(session_id.as_str().to_string());
826
+ Ok(argv)
827
+ }
828
+ // Cursor resume: base + `--resume <chatId>`。
829
+ Provider::CursorAgent => {
830
+ let mut argv = cursor_agent_base_command(
831
+ self,
832
+ auth_mode,
833
+ mcp_config,
834
+ system_prompt,
835
+ model,
836
+ tools,
837
+ false,
838
+ None,
839
+ )?;
840
+ // Put --resume immediately after `agent` so the CLI treats it as
841
+ // the session mode rather than a trailing flag after --workspace.
842
+ argv.insert(1, "--resume".to_string());
843
+ argv.insert(2, session_id.as_str().to_string());
844
+ Ok(argv)
845
+ }
728
846
  Provider::GeminiCli | Provider::Fake => Err(ProviderError::ResumeUnavailable(format!(
729
847
  "{} resume requires session_id",
730
848
  provider_wire(self.provider)
@@ -801,6 +919,54 @@ impl ProviderAdapter for BasicProviderAdapter {
801
919
  argv.push(session_id.as_str().to_string());
802
920
  Ok(CommandPlan::argv_only(argv))
803
921
  }
922
+ Provider::Grok => {
923
+ let Some(session_id) = session_id else {
924
+ return Err(ProviderError::ResumeUnavailable(
925
+ "resume requires session_id".to_string(),
926
+ ));
927
+ };
928
+ let mut argv = grok_base_command(
929
+ self,
930
+ ctx.auth_mode,
931
+ ctx.mcp_config,
932
+ ctx.system_prompt,
933
+ ctx.model,
934
+ ctx.tools,
935
+ ctx.profile_launch
936
+ .is_some_and(|profile| profile.managed_mcp_config),
937
+ ctx.effort,
938
+ )?;
939
+ argv.push("--resume".to_string());
940
+ argv.push(session_id.as_str().to_string());
941
+ Ok(CommandPlan::argv_only(argv))
942
+ }
943
+ Provider::CursorAgent => {
944
+ let Some(session_id) = session_id else {
945
+ return Err(ProviderError::ResumeUnavailable(
946
+ "cursor_agent resume requires chatId; empty --resume is forbidden (picker)".to_string(),
947
+ ));
948
+ };
949
+ let raw = session_id.as_str();
950
+ if raw.is_empty() || raw.contains('/') || raw.contains('\\') || raw.contains('\0') {
951
+ return Err(ProviderError::ResumeUnavailable(
952
+ "cursor_agent resume rejected forged or empty chatId".to_string(),
953
+ ));
954
+ }
955
+ let mut argv = cursor_agent_base_command(
956
+ self,
957
+ ctx.auth_mode,
958
+ ctx.mcp_config,
959
+ ctx.system_prompt,
960
+ ctx.model,
961
+ ctx.tools,
962
+ ctx.profile_launch
963
+ .is_some_and(|profile| profile.managed_mcp_config),
964
+ ctx.effort,
965
+ )?;
966
+ argv.push("--resume".to_string());
967
+ argv.push(raw.to_string());
968
+ Ok(CommandPlan::argv_only(argv))
969
+ }
804
970
  _ => self
805
971
  .build_resume_command_with_context(
806
972
  session_id,
@@ -882,7 +1048,27 @@ impl ProviderAdapter for BasicProviderAdapter {
882
1048
  Provider::Copilot => Err(ProviderError::CapabilityUnsupported(
883
1049
  "copilot CLI 无 fork 旗标,session-store 不支持 branched continuation".to_string(),
884
1050
  )),
885
- Provider::GeminiCli | Provider::Fake => {
1051
+ // Grok fork: base + 新 `--session-id` + `--resume <src>` + `--fork-session`
1052
+ // (grok help: --fork-session 需配合 --resume/--continue)。
1053
+ Provider::Grok => {
1054
+ let mut argv = grok_base_command(
1055
+ self,
1056
+ auth_mode,
1057
+ mcp_config,
1058
+ system_prompt,
1059
+ model,
1060
+ tools,
1061
+ false,
1062
+ None,
1063
+ )?;
1064
+ argv.push("--session-id".to_string());
1065
+ argv.push(next_session_token());
1066
+ argv.push("--resume".to_string());
1067
+ argv.push(session_id.as_str().to_string());
1068
+ argv.push("--fork-session".to_string());
1069
+ Ok(argv)
1070
+ }
1071
+ Provider::CursorAgent | Provider::GeminiCli | Provider::Fake => {
886
1072
  Err(ProviderError::CapabilityUnsupported(format!(
887
1073
  "{} does not support native session fork",
888
1074
  provider_wire(self.provider)
@@ -1006,6 +1192,30 @@ impl ProviderAdapter for BasicProviderAdapter {
1006
1192
  managed_mcp_config: profile.managed_mcp_config,
1007
1193
  })
1008
1194
  }
1195
+ Provider::Grok => {
1196
+ let Some(session_id) = session_id else {
1197
+ return Err(ProviderError::ResumeUnavailable(
1198
+ "fork requires session_id".to_string(),
1199
+ ));
1200
+ };
1201
+ let mut argv = grok_base_command(
1202
+ self,
1203
+ ctx.auth_mode,
1204
+ ctx.mcp_config,
1205
+ ctx.system_prompt,
1206
+ ctx.model,
1207
+ ctx.tools,
1208
+ ctx.profile_launch
1209
+ .is_some_and(|profile| profile.managed_mcp_config),
1210
+ ctx.effort,
1211
+ )?;
1212
+ argv.push("--session-id".to_string());
1213
+ argv.push(next_session_token());
1214
+ argv.push("--resume".to_string());
1215
+ argv.push(session_id.as_str().to_string());
1216
+ argv.push("--fork-session".to_string());
1217
+ Ok(CommandPlan::argv_only(argv))
1218
+ }
1009
1219
  _ => self
1010
1220
  .fork_with_context(
1011
1221
  session_id,
@@ -1061,9 +1271,17 @@ impl ProviderAdapter for BasicProviderAdapter {
1061
1271
  r"working|processing",
1062
1272
  r"Error|panic",
1063
1273
  ),
1064
- Provider::GeminiCli | Provider::Fake => {
1274
+ // Grok: 暂无专有 status 正则, 沿用 generic。
1275
+ Provider::Grok | Provider::GeminiCli | Provider::Fake => {
1065
1276
  patterns(r">", r"working|processing", r"Error|Traceback")
1066
1277
  }
1278
+ // Cursor: U-15 未过前不得用 generic `>` 单独判已证实 idle。
1279
+ // 字面量 `cursor-idle-unset-until-u15` 不会出现在屏上。
1280
+ Provider::CursorAgent => patterns(
1281
+ r"cursor-idle-unset-until-u15",
1282
+ r"cursor-busy-unset-until-u15",
1283
+ r"Error|Traceback",
1284
+ ),
1067
1285
  }
1068
1286
  }
1069
1287
 
@@ -1164,7 +1382,9 @@ pub(crate) fn json_inline(value: &serde_json::Value) -> String {
1164
1382
  use super::adapters::claude::{claude_base_command, claude_launch_command};
1165
1383
  use super::adapters::codex::codex_base_command;
1166
1384
  use super::adapters::copilot::{copilot_base_command, copilot_base_command_resume};
1385
+ use super::adapters::cursor_agent::cursor_agent_base_command;
1167
1386
  use super::adapters::fake::fake_worker_command;
1387
+ use super::adapters::grok::{grok_base_command, grok_launch_command};
1168
1388
 
1169
1389
  /// Contract C / MUST-8: the per-worker Team Agent MCP server config used by Claude
1170
1390
  /// (`--mcp-config`) and Codex (`-c mcp_servers.*` injection). Placeholders
@@ -1249,9 +1469,16 @@ pub(crate) fn next_session_token() -> String {
1249
1469
  #[cfg(test)]
1250
1470
  mod fork_materialization_source_guard {
1251
1471
  #[test]
1252
- fn lifecycle_uses_provider_neutral_fork_materialization_dispatch() {
1472
+ fn lifecycle_uses_in_window_fork_not_session_file_materialization() {
1253
1473
  let source = include_str!("../lifecycle/launch/fork_agent.rs");
1254
- assert!(source.contains(".materialize_fork_backing("));
1474
+ assert!(
1475
+ source.contains("in_window_fork(") && source.contains("inject_clean_command"),
1476
+ "fork must stay on the shared in-window inject path"
1477
+ );
1478
+ assert!(
1479
+ !source.contains(".materialize_fork_backing("),
1480
+ "session-file fork materialization must stay gone"
1481
+ );
1255
1482
  assert!(!source.contains("materialize_codex_fork"));
1256
1483
  assert!(!source.contains("codex_fork"));
1257
1484
  }