@team-agent/installer 0.5.66 → 0.5.69

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 (178) 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 +156 -0
  6. package/crates/team-agent/src/cli/emit.rs +6 -5
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +22 -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 +3 -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 +121 -5
  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 +31 -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 +3 -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 +973 -75
  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 +398 -26
  46. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +80 -0
  47. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +60 -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 +548 -0
  51. package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +120 -0
  52. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +208 -456
  53. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  54. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  55. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  56. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  57. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  58. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +482 -1
  59. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  60. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +830 -0
  61. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  62. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  63. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +87 -3
  64. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  65. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  66. package/crates/team-agent/src/lifecycle/launch/spawn.rs +127 -25
  67. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +140 -0
  68. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
  69. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  70. package/crates/team-agent/src/lifecycle/launch.rs +107 -29
  71. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  72. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  73. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  74. package/crates/team-agent/src/lifecycle/profile_launch.rs +99 -0
  75. package/crates/team-agent/src/lifecycle/profile_smoke.rs +52 -1
  76. package/crates/team-agent/src/lifecycle/restart/agent.rs +214 -160
  77. package/crates/team-agent/src/lifecycle/restart/common.rs +513 -37
  78. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  79. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  80. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
  81. package/crates/team-agent/src/lifecycle/restart/remove.rs +233 -6
  82. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  83. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  84. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  85. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
  86. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +24 -3
  87. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  88. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
  89. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
  90. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
  91. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +464 -0
  92. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +247 -0
  93. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  94. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_g0.stdout.txt +222 -0
  95. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +787 -0
  96. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  97. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  98. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +485 -0
  99. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  100. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  101. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
  102. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
  103. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
  104. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
  105. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  106. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
  107. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
  108. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +97 -0
  109. package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +128 -0
  110. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +207 -0
  111. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +838 -0
  112. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +320 -0
  113. package/crates/team-agent/src/lifecycle/tests/pi_lifecycle_doctor_red.rs +98 -0
  114. package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +216 -0
  115. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -0
  116. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
  117. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
  118. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  119. package/crates/team-agent/src/lifecycle/tests.rs +30 -13
  120. package/crates/team-agent/src/lifecycle/types.rs +61 -0
  121. package/crates/team-agent/src/lifecycle/worker_command_context.rs +143 -12
  122. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  123. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  124. package/crates/team-agent/src/messaging/delivery.rs +430 -41
  125. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  126. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  127. package/crates/team-agent/src/messaging/results.rs +4 -0
  128. package/crates/team-agent/src/messaging/send.rs +34 -0
  129. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  130. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  131. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  132. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  133. package/crates/team-agent/src/messaging/types.rs +4 -1
  134. package/crates/team-agent/src/model/enums.rs +17 -6
  135. package/crates/team-agent/src/model/permissions.rs +3 -0
  136. package/crates/team-agent/src/os_probe.rs +73 -2
  137. package/crates/team-agent/src/provider/adapter.rs +259 -6
  138. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +89 -0
  139. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  140. package/crates/team-agent/src/provider/adapters/mod.rs +3 -0
  141. package/crates/team-agent/src/provider/adapters/pi.rs +152 -0
  142. package/crates/team-agent/src/provider/bypass_flags.rs +47 -2
  143. package/crates/team-agent/src/provider/classify.rs +4 -2
  144. package/crates/team-agent/src/provider/faults.rs +2 -1
  145. package/crates/team-agent/src/provider/mod.rs +11 -0
  146. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  147. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  148. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  149. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  150. package/crates/team-agent/src/provider/session/context_fork.rs +78 -3
  151. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  152. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  153. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  154. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  155. package/crates/team-agent/src/provider/session_scan/common.rs +205 -2
  156. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  157. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  158. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  159. package/crates/team-agent/src/provider/session_scan/pi.rs +172 -0
  160. package/crates/team-agent/src/provider/session_scan.rs +12 -0
  161. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  162. package/crates/team-agent/src/provider/tests/adapter.rs +384 -0
  163. package/crates/team-agent/src/provider/types.rs +4 -2
  164. package/crates/team-agent/src/provider/wire.rs +29 -1
  165. package/crates/team-agent/src/state/persist.rs +301 -8
  166. package/crates/team-agent/src/state/repository.rs +5 -0
  167. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  168. package/crates/team-agent/src/tmux_backend.rs +1116 -75
  169. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  170. package/crates/team-agent/src/transport.rs +248 -1
  171. package/npm/install.mjs +129 -73
  172. package/package.json +4 -4
  173. package/skills/team-agent/SKILL.md +32 -239
  174. package/skills/team-agent/command-coverage.json +31 -0
  175. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  176. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
  177. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  178. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
@@ -1,3 +1,26 @@
1
+ //! ---
2
+ //! purpose: 加席过程中的 state 写入、spec 注入与失败回滚
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: rollback_add_agent_atomic
6
+ //! what: 尽力恢复 spec 字节与 runtime state,并写一条回滚事件
7
+ //! - name: upsert_agent_state_from_role
8
+ //! what: 由角色文档 front matter 写出该席位的 state 行
9
+ //! - name: inject_agent_into_spec
10
+ //! what: 把编译出的 agent 注入 spec 的 agents 与 routing 规则
11
+ //! - name: runtime_agent_exists
12
+ //! what: 判断 state 里是否已有同 id 席位
13
+ //! depends:
14
+ //! - crate::state::repository
15
+ //! - crate::state::persist
16
+ //! - crate::lifecycle::restart::remove
17
+ //! - crate::event_log::EventLog
18
+ //! boundary:
19
+ //! - 回滚是尽力而为,回滚自身的错误被吞掉,只在事件里如实记录成败
20
+ //! - 注入 spec 不落盘,落盘由调用方原子写
21
+ //! - 已存在同 id 的条目不重复注入
22
+ //! maturity: wired
23
+ //! ---
1
24
  use std::collections::{BTreeMap, BTreeSet};
2
25
  use std::path::{Path, PathBuf};
3
26
  use std::process::Command;
@@ -14,6 +37,14 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
14
37
 
15
38
  use super::*;
16
39
 
40
+ /// ---
41
+ /// purpose: 加席失败后恢复 spec 与 runtime state,并给新席立墓碑防止被合并回来
42
+ /// params:
43
+ /// pre_spec_text: 写之前的 spec 字节;None 表示原本没有该文件,回滚即删除
44
+ /// pre_runtime_state: 写之前的 runtime state;None 时改为从当前 state 里摘掉该席位
45
+ /// reason: 写进回滚事件的原因串
46
+ /// returns: 无返回值;回滚成败如实写进 add_agent.rollback 事件
47
+ /// ---
17
48
  /// E42 (0.3.24 P0, double-spec deadlock): best-effort atomic rollback for a
18
49
  /// failed add-agent. Restores the canonical spec to its pre-write bytes (or
19
50
  /// removes the file if it didn't exist), and restores runtime state to its
@@ -110,6 +141,14 @@ pub(super) fn rollback_add_agent_atomic(
110
141
  );
111
142
  }
112
143
 
144
+ /// ---
145
+ /// purpose: 由角色文档的 front matter 写出该席位的 state 行并落盘
146
+ /// params:
147
+ /// meta: 角色文档 front matter
148
+ /// dynamic_role_file: 角色文件路径,记进 state 供 restart 重建 spec 用
149
+ /// returns: 成功返回空值,席位状态记为 starting
150
+ /// errors: 读或写 runtime state 失败时返回 StatePersist
151
+ /// ---
113
152
  pub(super) fn upsert_agent_state_from_role(
114
153
  workspace: &Path,
115
154
  canonical_team_key: &str,
@@ -128,6 +167,24 @@ pub(super) fn upsert_agent_state_from_role(
128
167
  "runtime state root is not an object".to_string(),
129
168
  ));
130
169
  };
170
+ if !root
171
+ .get("session_name")
172
+ .and_then(serde_json::Value::as_str)
173
+ .is_some_and(|session| !session.is_empty())
174
+ {
175
+ if let Some(owner_session) = root
176
+ .get("leader_receiver")
177
+ .and_then(|receiver| receiver.get("session_name"))
178
+ .and_then(serde_json::Value::as_str)
179
+ .filter(|session| !session.is_empty())
180
+ .map(str::to_string)
181
+ {
182
+ root.insert(
183
+ "session_name".to_string(),
184
+ serde_json::Value::String(owner_session),
185
+ );
186
+ }
187
+ }
131
188
  let agents = root
132
189
  .entry("agents".to_string())
133
190
  .or_insert_with(|| serde_json::json!({}));
@@ -212,6 +269,10 @@ pub(super) fn upsert_agent_state_from_role(
212
269
  )
213
270
  }
214
271
 
272
+ /// ---
273
+ /// purpose: 判断角色文件是框架托管的还是外部的
274
+ /// returns: 路径落在托管目录下为 managed,否则为 external
275
+ /// ---
215
276
  pub(super) fn role_source_ownership(workspace: &Path, role_file: &Path) -> &'static str {
216
277
  let managed_root = workspace.join(".team").join("dynamic-role-files");
217
278
  match (
@@ -223,6 +284,13 @@ pub(super) fn role_source_ownership(workspace: &Path, role_file: &Path) -> &'sta
223
284
  }
224
285
  }
225
286
 
287
+ /// ---
288
+ /// purpose: 把编译出的 agent 注入 spec 的 agents 列表并补一条同形路由规则
289
+ /// params:
290
+ /// spec: 就地改写;已有同 id 的 agent 或同目标的路由规则时不重复追加
291
+ /// returns: 成功返回空值,不落盘
292
+ /// errors: spec 不是 map 或 agents 缺失时返回 Compile
293
+ /// ---
226
294
  /// E5 Bug1:把 add-agent 就地编译出的 agent 条目注入 base team spec(`agents` 列表 +
227
295
  /// `routing.rules` 加 `route-<id>`),复刻 [`compile_team`] 的路由规则形态。不落任何文件。
228
296
  ///
@@ -279,6 +347,10 @@ pub(crate) fn inject_agent_into_spec(
279
347
  Ok(())
280
348
  }
281
349
 
350
+ /// ---
351
+ /// purpose: 判断 state 的 agents 表里是否已有该 id
352
+ /// returns: 存在则 true
353
+ /// ---
282
354
  pub(super) fn runtime_agent_exists(state: &serde_json::Value, agent_id: &AgentId) -> bool {
283
355
  state
284
356
  .get("agents")
@@ -286,6 +358,10 @@ pub(super) fn runtime_agent_exists(state: &serde_json::Value, agent_id: &AgentId
286
358
  .is_some_and(|agents| agents.contains_key(agent_id.as_str()))
287
359
  }
288
360
 
361
+ /// ---
362
+ /// purpose: 取 YAML agent 节点的 id
363
+ /// returns: id 是字符串时返回它,否则 None
364
+ /// ---
289
365
  pub(super) fn yaml_agent_id(agent: &Value) -> Option<&str> {
290
366
  let Value::Map(pairs) = agent else {
291
367
  return None;
@@ -299,6 +375,10 @@ pub(super) fn yaml_agent_id(agent: &Value) -> Option<&str> {
299
375
  })
300
376
  }
301
377
 
378
+ /// ---
379
+ /// purpose: 取 YAML 路由规则的 assign_to
380
+ /// returns: 该字段是字符串时返回它,否则 None
381
+ /// ---
302
382
  pub(super) fn yaml_route_assigns_to(rule: &Value) -> Option<&str> {
303
383
  let Value::Map(pairs) = rule else {
304
384
  return None;
@@ -1,3 +1,22 @@
1
+ //! ---
2
+ //! purpose: 单个席位在 runtime state 里的行的构造,含 MCP 配置落盘与审批策略投影
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: running_agent_state
6
+ //! what: 构造 running 席位的 state 行,同时写出该席位的 MCP 配置文件
7
+ //! - name: effective_approval_policy
8
+ //! what: 把 bypass 审批结论投影成 state 里的 JSON
9
+ //! - name: effective_approval_policy_from_agent
10
+ //! what: 由席位行自己的 dangerously_skip_permissions 算审批策略
11
+ //! depends:
12
+ //! - crate::provider
13
+ //! - crate::provider::bypass_flags
14
+ //! - crate::lifecycle::launch::mcp_config
15
+ //! boundary:
16
+ //! - 只构造与落 MCP 配置,不 spawn 也不改窗口
17
+ //! - 审批结论只认席位自己的声明,不做全队继承
18
+ //! maturity: wired
19
+ //! ---
1
20
  use std::collections::{BTreeMap, BTreeSet};
2
21
  use std::path::{Path, PathBuf};
3
22
  use std::process::Command;
@@ -14,6 +33,15 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
14
33
 
15
34
  use super::*;
16
35
 
36
+ /// ---
37
+ /// purpose: 构造某席位 running 状态的 state 行
38
+ /// params:
39
+ /// spawn_cwd: 该席位进程的工作目录
40
+ /// started_agent: 本次 spawn 结果,用于取布局窗口
41
+ /// pane_id: 该席位的 pane,空串时不写入
42
+ /// returns: 含状态、provider、模型、auth_mode、窗口、MCP 配置路径、spawn 时间与 pane 信息的行
43
+ /// errors: 取 provider MCP 配置失败返回 Provider,写 MCP 配置文件失败返回 StatePersist
44
+ /// ---
17
45
  pub(super) fn running_agent_state(
18
46
  agent: &Value,
19
47
  id: &str,
@@ -141,6 +169,10 @@ pub(super) fn running_agent_state(
141
169
  Ok(serde_json::Value::Object(state))
142
170
  }
143
171
 
172
+ /// ---
173
+ /// purpose: 把 bypass 审批结论投影成落盘用的 JSON
174
+ /// returns: 含 enabled、来源、是否继承与显式确认位;来源为 runtime_config 且启用时视为已显式确认
175
+ /// ---
144
176
  pub(crate) fn effective_approval_policy(safety: &DangerousApproval) -> serde_json::Value {
145
177
  serde_json::json!({
146
178
  "enabled": safety.enabled,
@@ -153,6 +185,11 @@ pub(crate) fn effective_approval_policy(safety: &DangerousApproval) -> serde_jso
153
185
  })
154
186
  }
155
187
 
188
+ /// ---
189
+ /// purpose: 把审批策略写进席位 state 行
190
+ /// params:
191
+ /// agent_state: 就地写入 effective_approval_policy 字段
192
+ /// ---
156
193
  pub(crate) fn persist_effective_approval_policy(
157
194
  agent_state: &mut serde_json::Map<String, serde_json::Value>,
158
195
  safety: &DangerousApproval,
@@ -163,6 +200,10 @@ pub(crate) fn persist_effective_approval_policy(
163
200
  );
164
201
  }
165
202
 
203
+ /// ---
204
+ /// purpose: 给出审批来源的稳定 wire 字符串
205
+ /// returns: runtime_config、leader_process 或 disabled
206
+ /// ---
166
207
  pub(super) fn dangerous_approval_source_str(source: DangerousApprovalSource) -> &'static str {
167
208
  match source {
168
209
  DangerousApprovalSource::RuntimeConfig => "runtime_config",
@@ -171,6 +212,11 @@ pub(super) fn dangerous_approval_source_str(source: DangerousApprovalSource) ->
171
212
  }
172
213
  }
173
214
 
215
+ /// ---
216
+ /// purpose: 由 JSON 席位行自己的 bypass 声明算出审批策略
217
+ /// returns: 未声明时全为关闭;声明为真时记 runtime_config 来源并带该 provider 的 bypass argv
218
+ /// contract_id: lifecycle.agent_state.approval_policy_from_agent
219
+ /// ---
174
220
  /// 0.5.66 bypass 单源:从 agent 行 + provider 构造 approval policy JSON
175
221
  /// (restart/fork/add 落盘用;取代全队 `DangerousApproval` 派生)。
176
222
  pub(crate) fn effective_approval_policy_from_agent(
@@ -211,10 +257,18 @@ fn provider_wire_str(provider: Provider) -> &'static str {
211
257
  Provider::Codex => "codex",
212
258
  Provider::Copilot => "copilot",
213
259
  Provider::GeminiCli => "gemini_cli",
260
+ Provider::Grok => "grok",
261
+ Provider::CursorAgent => "cursor_agent",
262
+ Provider::Pi => "pi",
214
263
  Provider::Fake => "fake",
215
264
  }
216
265
  }
217
266
 
267
+ /// ---
268
+ /// purpose: 由席位行自身算出审批策略并写回同一行
269
+ /// params:
270
+ /// agent_state: 既是来源也是写入目标
271
+ /// ---
218
272
  /// 0.5.66 bypass 单源:从 agent 行(state Map)+ provider 落盘 approval policy。
219
273
  /// 目标与来源是同一 agent 行。
220
274
  pub(crate) fn persist_effective_approval_policy_from_agent(
@@ -228,6 +282,12 @@ pub(crate) fn persist_effective_approval_policy_from_agent(
228
282
  );
229
283
  }
230
284
 
285
+ /// ---
286
+ /// purpose: 由 YAML spec 里的 agent 声明算出审批策略并写进 state 行
287
+ /// params:
288
+ /// agent: YAML 侧的 agent 节点,提供 bypass 声明
289
+ /// agent_state: 写入目标
290
+ /// ---
231
291
  /// 0.5.66 bypass 单源:yaml spec agent 版(launch/state_projection 用)。
232
292
  pub(crate) fn persist_effective_approval_policy_from_yaml_agent(
233
293
  agent_state: &mut serde_json::Map<String, serde_json::Value>,
@@ -1,3 +1,14 @@
1
+ //! ---
2
+ //! purpose: 零上下文重读角色文件再起一席,不继承源会话
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: clone_agent
6
+ //! what: 用源席最新角色文件 add-agent,保留源工具集
7
+ //! boundary:
8
+ //! - 不走 fork 的窗口内命令
9
+ //! - 不把 clone 与 fork 合成一条路径
10
+ //! maturity: wired
11
+ //! ---
1
12
  use crate::lifecycle::*;
2
13
  use crate::model::ids::AgentId;
3
14
  use crate::provider::SessionId;
@@ -5,6 +16,15 @@ use std::path::Path;
5
16
 
6
17
  use super::*;
7
18
 
19
+ /// ---
20
+ /// purpose: 以源席最新角色文件另起一席,不继承源会话
21
+ /// params:
22
+ /// source_agent_id: 源席位,只借它的角色文件与工具集
23
+ /// as_agent_id: 新席位名
24
+ /// label: 新席位的角色标签
25
+ /// returns: clone 报告
26
+ /// errors: 选不到 team 返回 TeamSelect,源席不存在返回 RequirementUnmet,角色物化与加席错误透传
27
+ /// ---
8
28
  pub fn clone_agent(
9
29
  workspace: &Path,
10
30
  source_agent_id: &AgentId,
@@ -40,16 +60,10 @@ pub fn clone_agent(
40
60
  as_agent_id,
41
61
  label,
42
62
  )?;
43
- let spec_path = selected
44
- .spec_path
45
- .as_ref()
46
- .ok_or_else(|| LifecycleError::TeamSelect("active team spec not found".to_string()))?;
47
- let spec = crate::model::yaml::loads(
48
- &std::fs::read_to_string(spec_path)
49
- .map_err(|error| LifecycleError::Compile(error.to_string()))?,
50
- )
51
- .map_err(|error| LifecycleError::Compile(error.to_string()))?;
52
- clamp_materialized_role_to_leader(materialized.path(), &spec)?;
63
+ // The materialized role carries the source role's DECLARED tools verbatim —
64
+ // a clone must preserve the source seat's full tools set (add-agent does
65
+ // the same; no leader-ceiling clamp). See role_source.rs for the removed
66
+ // clamp_materialized_role_to_leader.
53
67
  let added = add_agent(
54
68
  &selected.run_workspace,
55
69
  as_agent_id,
@@ -63,6 +77,15 @@ pub fn clone_agent(
63
77
  as_agent_id,
64
78
  source_session_id.as_ref(),
65
79
  );
80
+ if let Some((session_id, backing_path)) = verified.as_ref() {
81
+ persist_clone_session(
82
+ &selected.run_workspace,
83
+ selected.team_key.as_str(),
84
+ as_agent_id,
85
+ session_id,
86
+ backing_path,
87
+ )?;
88
+ }
66
89
  let (session_id, backing_path, backing_state) = match verified {
67
90
  Some((session_id, backing_path)) => (
68
91
  Some(session_id),
@@ -94,13 +117,80 @@ fn read_agent_session(
94
117
  .and_then(|agents| agents.get(agent_id.as_str()))?;
95
118
  let session = agent
96
119
  .get("session_id")
120
+ .or_else(|| agent.get("_pending_session_id"))
97
121
  .and_then(serde_json::Value::as_str)
98
122
  .filter(|value| !value.is_empty())?;
99
123
  let backing = agent
100
124
  .get("rollout_path")
101
125
  .and_then(serde_json::Value::as_str)
102
126
  .filter(|value| !value.is_empty())
103
- .map(std::path::PathBuf::from)?;
127
+ .map(std::path::PathBuf::from)
128
+ .or_else(|| find_claude_backing(agent, session))?;
104
129
  let distinct = source_session_id.is_none_or(|source| source.as_str() != session);
105
130
  (distinct && backing.is_file()).then(|| (SessionId::new(session), backing))
106
131
  }
132
+
133
+ fn find_claude_backing(agent: &serde_json::Value, session: &str) -> Option<std::path::PathBuf> {
134
+ let root = agent
135
+ .get("claude_projects_root")
136
+ .and_then(serde_json::Value::as_str)
137
+ .map(std::path::PathBuf::from)
138
+ .or_else(|| {
139
+ std::env::var_os("HOME")
140
+ .map(|home| std::path::PathBuf::from(home).join(".claude/projects"))
141
+ })?;
142
+ let target = format!("{session}.jsonl");
143
+ let mut pending = vec![root];
144
+ while let Some(dir) = pending.pop() {
145
+ let entries = std::fs::read_dir(&dir).ok()?;
146
+ for entry in entries.flatten() {
147
+ let path = entry.path();
148
+ if path.is_dir() {
149
+ pending.push(path);
150
+ } else if path.file_name().and_then(|name| name.to_str()) == Some(target.as_str()) {
151
+ return Some(path);
152
+ }
153
+ }
154
+ }
155
+ None
156
+ }
157
+
158
+ fn persist_clone_session(
159
+ workspace: &Path,
160
+ team_key: &str,
161
+ agent_id: &AgentId,
162
+ session_id: &SessionId,
163
+ backing_path: &Path,
164
+ ) -> Result<(), LifecycleError> {
165
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
166
+ workspace,
167
+ operation: "clone-session-bind",
168
+ team: Some(team_key),
169
+ agent_id: Some(agent_id),
170
+ })?;
171
+ let mut state = crate::state::projection::select_runtime_state(workspace, Some(team_key))
172
+ .map_err(|error| LifecycleError::TeamSelect(error.to_string()))?;
173
+ let Some(agent) = state
174
+ .get_mut("agents")
175
+ .and_then(serde_json::Value::as_object_mut)
176
+ .and_then(|agents| agents.get_mut(agent_id.as_str()))
177
+ .and_then(serde_json::Value::as_object_mut)
178
+ else {
179
+ return Ok(());
180
+ };
181
+ agent.insert(
182
+ "session_id".to_string(),
183
+ serde_json::json!(session_id.as_str()),
184
+ );
185
+ agent.insert(
186
+ "rollout_path".to_string(),
187
+ serde_json::json!(backing_path.to_string_lossy().to_string()),
188
+ );
189
+ agent.insert("captured_via".to_string(), serde_json::json!("clone-agent"));
190
+ crate::lifecycle::launch::save_launched_team_state_for_key(
191
+ workspace,
192
+ &state,
193
+ Some(team_key),
194
+ Some(agent_id.as_str()),
195
+ )
196
+ }
@@ -0,0 +1,229 @@
1
+ //! ---
2
+ //! purpose: cursor 候选 A——`agent create-chat` 解析首行 chatId 后按记录 PID 杀
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: apply_cursor_create_chat_plan
6
+ //! what: 无 pending 时把 CLI chatId 写入 expected_session_id 并 `--resume` 该 id
7
+ //! - name: create_cursor_chat_id
8
+ //! what: 跑 create-chat,读第一行 UUID,只杀本调用记录的 PID
9
+ //! requires:
10
+ //! - name: cursor-agent-cli
11
+ //! what: PATH 上的 `agent`;不存在或首行非法则返回 None(降级候选 B)
12
+ //! boundary:
13
+ //! - 不发明 --session-id;不 pkill/killall;不 wait 子进程自行退出
14
+ //! - 不读 chats 正文、不打印 proxy 值
15
+ //! - 已有 expected_session_id 或 argv 已含 --resume 则不动
16
+ //! - 2026-08-21 活体:create-chat 后 `--resume` 的存档在 pane stop 后被清;
17
+ //! TUI 自建会话能留盘。fresh 路径不调用本函数(走 scan 唯一 hex)。
18
+ //! maturity: wired
19
+ //! ---
20
+
21
+ use std::io::{BufRead, BufReader};
22
+ use std::path::Path;
23
+ use std::process::{Command, Stdio};
24
+ use std::sync::mpsc;
25
+ use std::thread;
26
+ use std::time::Duration;
27
+
28
+ use crate::provider::types::{CommandPlan, SessionId};
29
+
30
+ const FIRST_LINE_TIMEOUT: Duration = Duration::from_secs(5);
31
+ const TERM_GRACE: Duration = Duration::from_millis(200);
32
+
33
+ /// ---
34
+ /// purpose: 冷启/无 session 的 cursor 计划绑定 create-chat 得到的 chatId
35
+ /// params:
36
+ /// plan: build_command_plan 产出;workspace: 物理 cwd
37
+ /// returns: 成功则 expected_session_id 与 argv `--resume <id>` 齐;失败则计划不变
38
+ /// contract:
39
+ /// provides:
40
+ /// - name: apply_cursor_create_chat_plan
41
+ /// what: 只在无 pending 且 argv 无 --resume 时写入 CLI chatId
42
+ /// boundary:
43
+ /// - 库测试(cfg(test))不调真实 agent,避免订阅/挂起;具名测试打 create_cursor_chat_id
44
+ /// - 不在 launch/restart fresh 调用:活体证实 `--resume` 空聊存档停 pane 后消失
45
+ /// ---
46
+ pub(crate) fn apply_cursor_create_chat_plan(plan: &mut CommandPlan, workspace: &Path) {
47
+ if cfg!(test) {
48
+ return;
49
+ }
50
+ if plan.expected_session_id.is_some() {
51
+ return;
52
+ }
53
+ if plan.argv.iter().any(|flag| flag == "--resume") {
54
+ return;
55
+ }
56
+ let Some(chat_id) = create_cursor_chat_id(workspace) else {
57
+ return;
58
+ };
59
+ plan.expected_session_id = Some(SessionId::new(chat_id.clone()));
60
+ plan.argv.push("--resume".to_string());
61
+ plan.argv.push(chat_id);
62
+ }
63
+
64
+ /// ---
65
+ /// purpose: 跑 `agent create-chat`,解析首行 UUID,按记录 PID 终止挂起进程
66
+ /// params:
67
+ /// workspace: create-chat 的 cwd(物理 workspace)
68
+ /// returns: 合法 UUID;命令缺失/超时/非法首行则 None
69
+ /// contract:
70
+ /// provides:
71
+ /// - name: create_cursor_chat_id
72
+ /// what: 读一行就停;不 wait 退出;只杀本 Child.id()
73
+ /// boundary:
74
+ /// - 禁 pkill/killall;pid 0/1 不杀
75
+ /// ---
76
+ pub(crate) fn create_cursor_chat_id(workspace: &Path) -> Option<String> {
77
+ create_cursor_chat_id_with_command("agent", workspace)
78
+ }
79
+
80
+ fn create_cursor_chat_id_with_command(program: &str, workspace: &Path) -> Option<String> {
81
+ let mut child = Command::new(program)
82
+ .arg("create-chat")
83
+ .current_dir(workspace)
84
+ .stdin(Stdio::null())
85
+ .stdout(Stdio::piped())
86
+ .stderr(Stdio::null())
87
+ .spawn()
88
+ .ok()?;
89
+ let pid = child.id();
90
+ let stdout = child.stdout.take()?;
91
+ let (tx, rx) = mpsc::channel();
92
+ thread::spawn(move || {
93
+ let mut reader = BufReader::new(stdout);
94
+ let mut line = String::new();
95
+ let result = reader.read_line(&mut line).ok().and_then(|_| parse_chat_id_line(&line));
96
+ let _ = tx.send(result);
97
+ });
98
+ let parsed = match rx.recv_timeout(FIRST_LINE_TIMEOUT) {
99
+ Ok(value) => value,
100
+ Err(_) => None,
101
+ };
102
+ kill_recorded_pid(pid);
103
+ let _ = child.wait();
104
+ parsed
105
+ }
106
+
107
+ fn parse_chat_id_line(line: &str) -> Option<String> {
108
+ let token = line
109
+ .trim()
110
+ .split_whitespace()
111
+ .next()
112
+ .unwrap_or("")
113
+ .trim_matches(['"', '\'']);
114
+ is_chat_uuid(token).then(|| token.to_string())
115
+ }
116
+
117
+ fn is_chat_uuid(value: &str) -> bool {
118
+ let bytes = value.as_bytes();
119
+ if bytes.len() != 36 {
120
+ return false;
121
+ }
122
+ for (i, b) in bytes.iter().enumerate() {
123
+ if i == 8 || i == 13 || i == 18 || i == 23 {
124
+ if *b != b'-' {
125
+ return false;
126
+ }
127
+ } else if !b.is_ascii_hexdigit() {
128
+ return false;
129
+ }
130
+ }
131
+ true
132
+ }
133
+
134
+ fn kill_recorded_pid(pid: u32) {
135
+ if pid == 0 || pid == 1 {
136
+ return;
137
+ }
138
+ #[cfg(unix)]
139
+ {
140
+ let raw = pid as i32;
141
+ unsafe {
142
+ libc::kill(raw, libc::SIGTERM);
143
+ }
144
+ thread::sleep(TERM_GRACE);
145
+ unsafe {
146
+ libc::kill(raw, libc::SIGKILL);
147
+ }
148
+ }
149
+ #[cfg(not(unix))]
150
+ {
151
+ let _ = pid;
152
+ }
153
+ }
154
+
155
+ #[cfg(test)]
156
+ mod tests {
157
+ #![allow(clippy::unwrap_used)]
158
+
159
+ use super::*;
160
+ use std::os::unix::fs::PermissionsExt;
161
+ use std::sync::atomic::{AtomicU64, Ordering};
162
+
163
+ fn tmp_root(tag: &str) -> std::path::PathBuf {
164
+ static CTR: AtomicU64 = AtomicU64::new(0);
165
+ let dir = std::env::temp_dir().join(format!(
166
+ "ta-d117-create-chat-{}-{}-{}",
167
+ tag,
168
+ std::process::id(),
169
+ CTR.fetch_add(1, Ordering::Relaxed)
170
+ ));
171
+ let _ = std::fs::remove_dir_all(&dir);
172
+ std::fs::create_dir_all(&dir).unwrap();
173
+ dir
174
+ }
175
+
176
+ fn write_stub(dir: &Path, body: &str) -> std::path::PathBuf {
177
+ let path = dir.join("agent");
178
+ std::fs::write(&path, body).unwrap();
179
+ let mut perm = std::fs::metadata(&path).unwrap().permissions();
180
+ perm.set_mode(0o755);
181
+ std::fs::set_permissions(&path, perm).unwrap();
182
+ path
183
+ }
184
+
185
+ #[test]
186
+ fn create_chat_reads_first_line_and_kills_recorded_pid() {
187
+ let base = tmp_root("ok");
188
+ let ws = base.join("ws");
189
+ std::fs::create_dir_all(&ws).unwrap();
190
+ let bin = write_stub(
191
+ &base,
192
+ "#!/bin/sh\necho '681c8837-d564-44fa-98bf-4ff5a66592e2'\nkill -STOP $$\n",
193
+ );
194
+ let got = create_cursor_chat_id_with_command(bin.to_str().unwrap(), &ws);
195
+ assert_eq!(
196
+ got.as_deref(),
197
+ Some("681c8837-d564-44fa-98bf-4ff5a66592e2")
198
+ );
199
+ let _ = std::fs::remove_dir_all(&base);
200
+ }
201
+
202
+ #[test]
203
+ fn create_chat_rejects_non_uuid_first_line() {
204
+ let base = tmp_root("bad");
205
+ let ws = base.join("ws");
206
+ std::fs::create_dir_all(&ws).unwrap();
207
+ let bin = write_stub(&base, "#!/bin/sh\necho not-a-uuid\nkill -STOP $$\n");
208
+ let got = create_cursor_chat_id_with_command(bin.to_str().unwrap(), &ws);
209
+ assert!(got.is_none());
210
+ let _ = std::fs::remove_dir_all(&base);
211
+ }
212
+
213
+ #[test]
214
+ fn apply_is_noop_when_resume_already_present() {
215
+ let mut plan = CommandPlan {
216
+ argv: vec![
217
+ "agent".into(),
218
+ "--resume".into(),
219
+ "already-bound".into(),
220
+ ],
221
+ expected_session_id: None,
222
+ provider_projects_root: None,
223
+ managed_mcp_config: false,
224
+ };
225
+ apply_cursor_create_chat_plan(&mut plan, Path::new("/tmp"));
226
+ assert!(plan.expected_session_id.is_none());
227
+ assert_eq!(plan.argv.iter().filter(|a| *a == "--resume").count(), 1);
228
+ }
229
+ }