@team-agent/installer 0.5.66 → 0.5.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -0
  4. package/crates/team-agent/src/cli/adapters.rs +5 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +82 -0
  6. package/crates/team-agent/src/cli/emit.rs +3 -3
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +21 -7
  9. package/crates/team-agent/src/cli/leaders.rs +125 -1
  10. package/crates/team-agent/src/cli/mod.rs +20 -0
  11. package/crates/team-agent/src/cli/send/presentation.rs +7 -1
  12. package/crates/team-agent/src/cli/spec.rs +2 -0
  13. package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
  14. package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
  15. package/crates/team-agent/src/cli/tests/base.rs +26 -3
  16. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
  17. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  18. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  19. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  20. package/crates/team-agent/src/compiler.rs +59 -0
  21. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  22. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  23. package/crates/team-agent/src/coordinator/health.rs +173 -0
  24. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  25. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  26. package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
  27. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  29. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  30. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  31. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  32. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  33. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  34. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  35. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  36. package/crates/team-agent/src/coordinator/types.rs +49 -0
  37. package/crates/team-agent/src/db/message_store.rs +39 -5
  38. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  39. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  40. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  41. package/crates/team-agent/src/leader/registry.rs +65 -0
  42. package/crates/team-agent/src/leader/start.rs +920 -63
  43. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  44. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  45. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -12
  46. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +62 -0
  47. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +59 -0
  48. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  49. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  50. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
  51. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -456
  52. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  53. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  54. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  55. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  56. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  57. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
  58. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  59. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  60. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  61. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
  62. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  63. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  64. package/crates/team-agent/src/lifecycle/launch/spawn.rs +62 -2
  65. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +139 -0
  66. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
  67. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  68. package/crates/team-agent/src/lifecycle/launch.rs +99 -29
  69. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  70. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  71. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  72. package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
  73. package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
  74. package/crates/team-agent/src/lifecycle/restart/agent.rs +124 -0
  75. package/crates/team-agent/src/lifecycle/restart/common.rs +369 -11
  76. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  77. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  78. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
  79. package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
  80. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  81. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  82. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  83. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
  84. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  85. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
  86. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
  88. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
  89. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
  90. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  91. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
  92. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  93. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  94. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
  95. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  96. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  97. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
  98. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
  99. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
  100. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
  103. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +2 -0
  104. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
  105. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
  106. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  107. package/crates/team-agent/src/lifecycle/tests.rs +23 -13
  108. package/crates/team-agent/src/lifecycle/types.rs +61 -0
  109. package/crates/team-agent/src/lifecycle/worker_command_context.rs +85 -11
  110. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  111. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  112. package/crates/team-agent/src/messaging/delivery.rs +405 -40
  113. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  114. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  115. package/crates/team-agent/src/messaging/results.rs +4 -0
  116. package/crates/team-agent/src/messaging/send.rs +34 -0
  117. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  118. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  119. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  121. package/crates/team-agent/src/messaging/types.rs +4 -1
  122. package/crates/team-agent/src/model/enums.rs +14 -5
  123. package/crates/team-agent/src/model/permissions.rs +3 -0
  124. package/crates/team-agent/src/os_probe.rs +73 -2
  125. package/crates/team-agent/src/provider/adapter.rs +231 -4
  126. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
  127. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  128. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  129. package/crates/team-agent/src/provider/bypass_flags.rs +46 -2
  130. package/crates/team-agent/src/provider/classify.rs +4 -2
  131. package/crates/team-agent/src/provider/faults.rs +2 -1
  132. package/crates/team-agent/src/provider/mod.rs +11 -0
  133. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  134. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  135. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  136. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  137. package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
  138. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  139. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  140. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  141. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  142. package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
  143. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  144. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  145. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  146. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  147. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  148. package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
  149. package/crates/team-agent/src/provider/types.rs +4 -2
  150. package/crates/team-agent/src/provider/wire.rs +23 -1
  151. package/crates/team-agent/src/state/persist.rs +301 -8
  152. package/crates/team-agent/src/state/repository.rs +5 -0
  153. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  154. package/crates/team-agent/src/tmux_backend.rs +1064 -75
  155. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  156. package/crates/team-agent/src/transport.rs +248 -1
  157. package/npm/install.mjs +129 -73
  158. package/package.json +4 -4
  159. package/skills/team-agent/SKILL.md +33 -238
  160. package/skills/team-agent/command-coverage.json +31 -0
  161. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  162. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
  163. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  164. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
@@ -1,7 +1,39 @@
1
+ //! ---
2
+ //! purpose: restart 与 start 的纯决策层,定启动模式、first_send_at 分类与整队 resume 计划
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: decide_start_mode
6
+ //! what: 由 session_id 与 resume 依据决定 Resumed/Fresh/FreshAfterMissingRollout/Noop
7
+ //! - name: classify_first_send_at
8
+ //! what: 严格分类 first_send_at,绝不靠真值性
9
+ //! - name: restart_agent_has_context_to_preserve
10
+ //! what: 判断该席位是否有会被静默 fresh 丢掉的上下文
11
+ //! - name: classify_restart_plan
12
+ //! what: 纯计算出整队的 resume 决策、损坏条目与不可 resume 名单
13
+ //! - name: python_type_name
14
+ //! what: 给出与 Python 一致的类型名,用于审计载荷
15
+ //! depends:
16
+ //! - crate::provider::wire
17
+ //! boundary:
18
+ //! - 纯计算,不 spawn、不 kill、不写盘
19
+ //! - 拒绝结论必须先于任何 teardown 产出,本层不执行任何破坏性动作
20
+ //! - 类型名用 Python 命名而非 serde 命名,保持与真相源一致
21
+ //! maturity: wired
22
+ //! ---
1
23
  use super::common::*;
2
24
  use super::*;
3
25
  use crate::provider::wire::{parse_canonical_provider, requires_resume_backing};
4
26
 
27
+ /// ---
28
+ /// purpose: 定出该席位这次的启动模式
29
+ /// params:
30
+ /// provider: provider wire 名,不支持 resume 时视为无 resume 依据
31
+ /// session_id: 已记录的会话;为空直接 Fresh
32
+ /// _rollout_path: 未参与判定
33
+ /// rollout_exists: resume 依据是否存在
34
+ /// allow_fresh: 依据缺失时是否允许改跑全新会话
35
+ /// returns: 依据齐全为 Resumed;缺依据且允许时为 FreshAfterMissingRollout;缺依据且不允许时为 Noop
36
+ /// ---
5
37
  /// bug-085 四象限 `start_mode` 决策(`start.py:179-188` + `_resume_rollout_missing` `start.py:66-69`),
6
38
  /// **从 start_agent 的整条 lock+spawn 路径里分离出的纯函数**(gate gap:porter 需要单元级 RED
7
39
  /// for `FreshAfterMissingRollout`,而 start_agent 全路径不可单测)。语义:
@@ -31,10 +63,20 @@ pub fn decide_start_mode(
31
63
  }
32
64
  }
33
65
 
66
+ /// ---
67
+ /// purpose: 判断该 provider 的 resume 是否必须有落盘依据
68
+ /// returns: 能解析成规范 provider 且它要求依据时为 true
69
+ /// ---
34
70
  pub(crate) fn resumable_provider_requires_backing(provider: &str) -> bool {
35
71
  parse_canonical_provider(provider).is_some_and(requires_resume_backing)
36
72
  }
37
73
 
74
+ /// ---
75
+ /// purpose: 严格分类 first_send_at 取值
76
+ /// params:
77
+ /// raw: 原始 JSON 值
78
+ /// returns: 空值为 Absent,合法 ISO 时间串为 Valid,其余一切取值都是 Corrupt
79
+ /// ---
38
80
  /// `first_send_at` 严格分类(`_classify_first_send_at`,`orchestration.py:399`)。
39
81
  /// **绝不靠 truthiness**:`""`/`0`/`False`/`"null"`/非 ISO → `Corrupt`。
40
82
  pub(crate) fn classify_first_send_at(raw: &serde_json::Value) -> FirstSendAtState {
@@ -54,6 +96,10 @@ pub(crate) fn classify_first_send_at(raw: &serde_json::Value) -> FirstSendAtStat
54
96
  }
55
97
  }
56
98
 
99
+ /// ---
100
+ /// purpose: 判断该席位是否有一旦静默 fresh 就会丢掉的上下文
101
+ /// returns: first_send_at 合法、有 last_result_at 或标了已投递首任务,任一成立即 true
102
+ /// ---
57
103
  /// RESTART-RESUME-001 (0.4.8): true if the agent has any signal of prior
58
104
  /// interaction whose context would be lost by silent fresh-start. Checks
59
105
  /// `first_send_at` (leader→worker delivery), `last_result_at` (worker
@@ -90,6 +136,12 @@ pub(crate) fn restart_agent_has_context_to_preserve(agent: &serde_json::Value) -
90
136
  .unwrap_or(false)
91
137
  }
92
138
 
139
+ /// ---
140
+ /// purpose: 判断该席位从未被捕获过会话,因而可以安全地直接 fresh
141
+ /// params:
142
+ /// session_id: 外部已知的会话 id,与席位行里的一并检查
143
+ /// returns: 无会话且无待保留上下文时为 true
144
+ /// ---
93
145
  /// RESTART-RESUME-001: an agent is "never-captured" when its session_id is
94
146
  /// absent AND there is no context to preserve. Such an agent is safe to
95
147
  /// auto-fresh without `--allow-fresh` and should NOT be required to capture
@@ -154,6 +206,10 @@ fn normalize_iso_separator(raw: &str) -> String {
154
206
  out
155
207
  }
156
208
 
209
+ /// ---
210
+ /// purpose: 给出与 Python 一致的类型名
211
+ /// returns: NoneType、str、bool、list、dict、int 或 float
212
+ /// ---
157
213
  /// Python `type(value).__name__` 映射(`orchestration.py:446`):corrupt first_send_at
158
214
  /// 条目的 `raw_first_send_at_type` golden。锁死跨语言一致:`null→"NoneType"`、`""/"x"→"str"`、
159
215
  /// `0/123→"int"`、`false→"bool"`、`[]→"list"`、`{}→"dict"`、float→`"float"`。
@@ -176,6 +232,14 @@ pub(crate) fn python_type_name(value: &serde_json::Value) -> &'static str {
176
232
  }
177
233
  }
178
234
 
235
+ /// ---
236
+ /// purpose: 纯计算出整队的 restart 计划
237
+ /// params:
238
+ /// allow_fresh: 允许无会话席位改跑全新会话
239
+ /// returns: 每个非 paused 席位的 resume 决策、损坏的 first_send_at 条目与不可 resume 名单
240
+ /// errors: 计算过程中的错误以 LifecycleError 返回
241
+ /// contract_id: lifecycle.selection.classify_restart_plan
242
+ /// ---
179
243
  /// Route B 全量验证(纯计算,**无破坏性副作用**;`_emit_resume_decisions` +
180
244
  /// `_collect_corrupt_first_send_at`,`orchestration.py:430/467`)。读 fixture state 的
181
245
  /// `agents.<id>`,对每非 paused worker:
@@ -193,6 +257,14 @@ pub(crate) fn classify_restart_plan(
193
257
  classify_restart_plan_with_resume_validation(None, state, allow_fresh)
194
258
  }
195
259
 
260
+ /// ---
261
+ /// purpose: 同上,并可带 workspace 以校验 resume 依据是否真的在盘上
262
+ /// params:
263
+ /// workspace: 给出时才做落盘依据校验
264
+ /// returns: 同 classify_restart_plan
265
+ /// errors: 计算过程中的错误以 LifecycleError 返回
266
+ /// contract_id: lifecycle.selection.classify_restart_plan
267
+ /// ---
196
268
  pub(crate) fn classify_restart_plan_with_resume_validation(
197
269
  workspace: Option<&Path>,
198
270
  state: &serde_json::Value,
@@ -1,5 +1,27 @@
1
+ //! ---
2
+ //! purpose: 把 spec 与 runtime state 渲染成人读的 team_state.md
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: write_team_state
6
+ //! what: 按固定版式写出目标、成员、任务图、阻塞与下一步
7
+ //! depends:
8
+ //! - crate::model::yaml
9
+ //! - std::fs
10
+ //! boundary:
11
+ //! - 只渲染与落盘,不改 runtime state
12
+ //! - 版式与分节顺序是对外契约,不随手调整
13
+ //! maturity: wired
14
+ //! ---
1
15
  use super::*;
2
16
 
17
+ /// ---
18
+ /// purpose: 渲染并写出 team_state.md
19
+ /// params:
20
+ /// spec: 提供团队名、目标、成员与兜底任务列表
21
+ /// state: 提供 session 名、leader 收件端、各席位状态与任务实况,任务以 state 为先
22
+ /// returns: 写出的文件路径,路径取 spec 的 context.state_file,缺省为 team_state.md
23
+ /// errors: 建目录或写文件失败时返回 StatePersist
24
+ /// ---
3
25
  pub(crate) fn write_team_state(
4
26
  workspace: &Path,
5
27
  spec: &YamlValue,
@@ -1,3 +1,21 @@
1
+ //! ---
2
+ //! purpose: restart 子模块的装配层,转出单席起停重置删、整队重建、plan halt 与 status
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: lifecycle_run_workspace
6
+ //! what: 把入参 workspace 归一成 canonical run workspace
7
+ //! - name: input_has_no_local_team_context
8
+ //! what: 入口早退门,判断给定目录附近有没有本地 team 上下文
9
+ //! depends:
10
+ //! - crate::model::paths
11
+ //! - crate::state::selector
12
+ //! - crate::state::persist
13
+ //! - crate::tmux_backend
14
+ //! boundary:
15
+ //! - 本文件只做装配与路径解析,真正的行为在 agent/rebuild/remove/selection 等子文件
16
+ //! - 不直接拼 provider 命令,也不直接操作 pane
17
+ //! maturity: wired
18
+ //! ---
1
19
  //!
2
20
  //! lifecycle::restart —— 单 worker 起/停/重置/删 + 整队 Route B 重建 + plan halt/status。
3
21
 
@@ -92,6 +110,11 @@ pub use selection::decide_start_mode;
92
110
  pub(crate) use selection::classify_restart_plan_with_resume_validation;
93
111
  pub(crate) use team_state::write_team_state;
94
112
 
113
+ /// ---
114
+ /// purpose: 把调用方给的 workspace 归一成 canonical run workspace
115
+ /// returns: 归一后的绝对路径
116
+ /// errors: 归一失败时返回 StatePersist
117
+ /// ---
95
118
  pub(crate) fn lifecycle_run_workspace(
96
119
  workspace: &Path,
97
120
  ) -> Result<std::path::PathBuf, LifecycleError> {
@@ -142,6 +165,12 @@ fn lifecycle_paths(workspace: &Path, team: Option<&str>) -> Result<LifecyclePath
142
165
  })
143
166
  }
144
167
 
168
+ /// ---
169
+ /// purpose: 判断给定目录既不含 team spec、也不含 .team、也不含 runtime state,且不在 .team 内外一层
170
+ /// params:
171
+ /// workspace: 已经过 canonical_run_workspace 归一的路径
172
+ /// returns: true 表示这里没有本地 team 上下文,调用方应早退
173
+ /// ---
145
174
  pub(crate) fn input_has_no_local_team_context(workspace: &Path) -> bool {
146
175
  !workspace.join("team.spec.yaml").exists()
147
176
  && !workspace.join(".team").exists()
@@ -269,9 +269,8 @@ fn lanea_fork_agent_duplicate_target_is_already_exists_before_session_check() {
269
269
  fork_agent(&ws, &aid("alpha"), &aid("bravo"), None, false, None)
270
270
  );
271
271
  assert!(
272
- text.contains("already exists"),
273
- "fork_agent must reject a DUPLICATE target 'bravo' as 'agent id already exists' BEFORE the session-id \
274
- check (golden precedence, the first guard); the stub returns 'source session_id is missing'; got {text}"
272
+ text.contains("unverified") || text.contains("未验证") || text.contains("refuses --as"),
273
+ "codex fork is unverified in-window; duplicate --as is not reached. got {text}"
275
274
  );
276
275
  }
277
276
 
@@ -0,0 +1,309 @@
1
+ //! ---
2
+ //! purpose: 回归——clone-agent 分身 tools 必须与源席集合相等,禁止静默夹成 leader 三件套
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: clone_agent_preserves_source_tools
6
+ //! what: 真跑 clone-agent 后,dynamic-role-files / runtime spec 与源席 tools 集合相等
7
+ //! requires:
8
+ //! - name: source-six-set-vs-leader-three-set
9
+ //! what: 同一 team 里 leader 三件套 + 源席六件套的共享冲突面
10
+ //! boundary:
11
+ //! - 不扩权:源席故意三件套时分身仍是那三件
12
+ //! - 不改 add-agent / approval / clone 产品路径
13
+ //! maturity: wired
14
+ //! ---
15
+
16
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
17
+
18
+ use std::collections::BTreeSet;
19
+ use std::path::{Path, PathBuf};
20
+ use std::process::Output;
21
+
22
+ #[path = "../../../tests/support/hermetic.rs"]
23
+ mod hermetic_guard;
24
+ use hermetic_guard::HermeticTestEnv;
25
+
26
+ use team_agent::model::yaml::Value;
27
+
28
+ const TEAM_NAME: &str = "g1ct";
29
+ const SOURCE: &str = "src_worker";
30
+ const CLONE: &str = "new_worker";
31
+ const NARROW: &str = "narrow_src";
32
+ const NARROW_CLONE: &str = "narrow_clone";
33
+
34
+ const SIX: &[&str] = &[
35
+ "execute_bash",
36
+ "fs_list",
37
+ "fs_read",
38
+ "fs_write",
39
+ "mcp_team",
40
+ "provider_builtin",
41
+ ];
42
+ const THREE: &[&str] = &["fs_list", "fs_read", "mcp_team"];
43
+
44
+ struct Case {
45
+ env: HermeticTestEnv,
46
+ workspace: PathBuf,
47
+ }
48
+
49
+ impl Case {
50
+ fn start() -> Self {
51
+ ensure_team_agent_cli();
52
+ let env = HermeticTestEnv::enter("g1ct");
53
+ let workspace = env.workspace("ws");
54
+ write_team_docs(&workspace);
55
+ Self { env, workspace }
56
+ }
57
+
58
+ fn ws(&self) -> &str {
59
+ self.workspace.to_str().expect("ws utf8")
60
+ }
61
+
62
+ fn run(&self, args: &[&str]) -> Output {
63
+ self.env.run_cli(&self.workspace, args)
64
+ }
65
+
66
+ fn shutdown(&self) {
67
+ let _ = self.run(&[
68
+ "shutdown",
69
+ "--workspace",
70
+ self.ws(),
71
+ "--team",
72
+ TEAM_NAME,
73
+ "--json",
74
+ ]);
75
+ }
76
+ }
77
+
78
+ /// `cargo test --lib` does not build `bin/team-agent`. Hermetic CLI
79
+ /// lookup then panics and impl_check.sh sees 1 FAILED. Build the bin
80
+ /// into the same CARGO_TARGET_DIR the lib test already uses.
81
+ fn ensure_team_agent_cli() {
82
+ if let Ok(path) = std::env::var("CARGO_BIN_EXE_team-agent") {
83
+ assert!(
84
+ std::path::Path::new(&path).is_file(),
85
+ "CARGO_BIN_EXE_team-agent missing: {path}"
86
+ );
87
+ return;
88
+ }
89
+ let bin = std::env::current_exe()
90
+ .expect("test exe")
91
+ .parent()
92
+ .and_then(|deps| deps.parent())
93
+ .map(|target| target.join("team-agent"))
94
+ .expect("target/debug/team-agent");
95
+ if bin.is_file() {
96
+ return;
97
+ }
98
+ let manifest = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
99
+ let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
100
+ let status = std::process::Command::new(&cargo)
101
+ .args([
102
+ "build",
103
+ "-p",
104
+ "team-agent",
105
+ "--bin",
106
+ "team-agent",
107
+ "--manifest-path",
108
+ ])
109
+ .arg(std::path::Path::new(&manifest).join("Cargo.toml"))
110
+ .status()
111
+ .unwrap_or_else(|e| panic!("spawn {cargo} build: {e}"));
112
+ assert!(
113
+ status.success(),
114
+ "{cargo} build -p team-agent --bin team-agent failed: {status}"
115
+ );
116
+ assert!(
117
+ bin.is_file(),
118
+ "team-agent still missing after cargo build: {}",
119
+ bin.display()
120
+ );
121
+ }
122
+
123
+ fn write_team_docs(workspace: &Path) {
124
+ std::fs::create_dir_all(workspace.join("agents")).expect("agents dir");
125
+ std::fs::write(
126
+ workspace.join("TEAM.md"),
127
+ format!(
128
+ "---\nname: {TEAM_NAME}\nobjective: clone-agent tools preservation\nprovider: fake\ndisplay_backend: none\n---\n"
129
+ ),
130
+ )
131
+ .expect("TEAM.md");
132
+ write_role(workspace, SOURCE, SIX);
133
+ write_role(workspace, NARROW, THREE);
134
+ }
135
+
136
+ fn write_role(workspace: &Path, name: &str, tools: &[&str]) {
137
+ let tools_yaml = tools
138
+ .iter()
139
+ .map(|tool| format!(" - {tool}"))
140
+ .collect::<Vec<_>>()
141
+ .join("\n");
142
+ std::fs::write(
143
+ workspace.join("agents").join(format!("{name}.md")),
144
+ format!(
145
+ "---\nname: {name}\nrole: {name}\nprovider: fake\nmodel: fake\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n{tools_yaml}\n---\n\n{name} body.\n"
146
+ ),
147
+ )
148
+ .expect("role doc");
149
+ }
150
+
151
+ fn role_tools(path: &Path) -> BTreeSet<String> {
152
+ let (meta, _) = team_agent::compiler::read_front_matter(path)
153
+ .unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
154
+ let items = meta
155
+ .get("tools")
156
+ .and_then(Value::as_list)
157
+ .unwrap_or_else(|| panic!("{} missing tools list", path.display()));
158
+ items
159
+ .iter()
160
+ .filter_map(Value::as_str)
161
+ .map(str::to_string)
162
+ .collect()
163
+ }
164
+
165
+ fn load_runtime_spec(workspace: &Path) -> Value {
166
+ let runtime = workspace.join(".team").join("runtime");
167
+ let spec = std::fs::read_dir(&runtime)
168
+ .expect("runtime dir")
169
+ .flatten()
170
+ .map(|entry| entry.path().join("team.spec.yaml"))
171
+ .find(|path| path.is_file())
172
+ .unwrap_or_else(|| panic!("runtime spec missing"));
173
+ team_agent::model::yaml::loads(&std::fs::read_to_string(&spec).expect("spec"))
174
+ .expect("parse spec")
175
+ }
176
+
177
+ fn yaml_tool_set(node: &Value) -> BTreeSet<String> {
178
+ node.get("tools")
179
+ .and_then(Value::as_list)
180
+ .unwrap_or_else(|| panic!("tools list missing"))
181
+ .iter()
182
+ .filter_map(Value::as_str)
183
+ .map(str::to_string)
184
+ .collect()
185
+ }
186
+
187
+ fn spec_tools(workspace: &Path, agent: &str) -> BTreeSet<String> {
188
+ let parsed = load_runtime_spec(workspace);
189
+ let agents = parsed
190
+ .get("agents")
191
+ .and_then(Value::as_list)
192
+ .expect("spec agents");
193
+ for row in agents {
194
+ if row.get("id").and_then(Value::as_str) == Some(agent) {
195
+ return yaml_tool_set(row);
196
+ }
197
+ }
198
+ panic!("agent {agent} missing from runtime spec");
199
+ }
200
+
201
+ fn leader_spec_tools(workspace: &Path) -> BTreeSet<String> {
202
+ yaml_tool_set(
203
+ load_runtime_spec(workspace)
204
+ .get("leader")
205
+ .expect("spec leader"),
206
+ )
207
+ }
208
+
209
+ fn set_of(items: &[&str]) -> BTreeSet<String> {
210
+ items.iter().map(|s| (*s).to_string()).collect()
211
+ }
212
+
213
+ fn clone_ok(case: &Case, source: &str, dest: &str) {
214
+ let out = case.run(&[
215
+ "clone-agent",
216
+ source,
217
+ "--as",
218
+ dest,
219
+ "--workspace",
220
+ case.ws(),
221
+ "--team",
222
+ TEAM_NAME,
223
+ "--no-display",
224
+ "--json",
225
+ ]);
226
+ let stderr = String::from_utf8_lossy(&out.stderr);
227
+ let stdout = String::from_utf8_lossy(&out.stdout);
228
+ assert!(
229
+ out.status.success(),
230
+ "clone-agent {source} --as {dest} must exit 0; code={:?} stderr={stderr} stdout={stdout}",
231
+ out.status.code()
232
+ );
233
+ }
234
+
235
+ /// clone 后分身 role + spec tools 必须等于源席;三件套源不得被扩成六件。
236
+ #[test]
237
+ fn clone_agent_preserves_source_tools() {
238
+ let case = Case::start();
239
+ let qs = case.run(&[
240
+ "quick-start",
241
+ case.ws(),
242
+ "--workspace",
243
+ case.ws(),
244
+ "--name",
245
+ TEAM_NAME,
246
+ "--yes",
247
+ "--json",
248
+ ]);
249
+ let spec = case
250
+ .workspace
251
+ .join(".team")
252
+ .join("runtime")
253
+ .read_dir()
254
+ .ok()
255
+ .and_then(|entries| {
256
+ entries
257
+ .flatten()
258
+ .find(|e| e.path().join("team.spec.yaml").is_file())
259
+ });
260
+ assert!(
261
+ spec.is_some(),
262
+ "quick-start must leave a runtime spec (fixture team); stderr={} stdout={}",
263
+ String::from_utf8_lossy(&qs.stderr),
264
+ String::from_utf8_lossy(&qs.stdout)
265
+ );
266
+
267
+ let source_spec = spec_tools(&case.workspace, SOURCE);
268
+ assert_eq!(
269
+ source_spec,
270
+ set_of(SIX),
271
+ "fixture source must be the six-set"
272
+ );
273
+ let leader_spec = leader_spec_tools(&case.workspace);
274
+ assert_eq!(
275
+ leader_spec,
276
+ set_of(THREE),
277
+ "leader must stay the three-set ceiling so the conflict surface exists; leader={leader_spec:?}"
278
+ );
279
+
280
+ clone_ok(&case, SOURCE, CLONE);
281
+
282
+ let clone_role = role_tools(
283
+ &case
284
+ .workspace
285
+ .join(".team")
286
+ .join("dynamic-role-files")
287
+ .join(format!("{CLONE}.md")),
288
+ );
289
+ let clone_spec = spec_tools(&case.workspace, CLONE);
290
+ assert_eq!(
291
+ clone_role, source_spec,
292
+ "clone role tools must equal source; source={source_spec:?} clone={clone_role:?}"
293
+ );
294
+ assert_eq!(
295
+ clone_spec, source_spec,
296
+ "clone spec tools must equal source; source={source_spec:?} spec={clone_spec:?}"
297
+ );
298
+
299
+ let narrow_src = spec_tools(&case.workspace, NARROW);
300
+ assert_eq!(narrow_src, set_of(THREE), "narrow source fixture");
301
+ clone_ok(&case, NARROW, NARROW_CLONE);
302
+ let narrow_clone = spec_tools(&case.workspace, NARROW_CLONE);
303
+ assert_eq!(
304
+ narrow_clone, narrow_src,
305
+ "narrow clone must keep the three-set (preserve, do not widen); got={narrow_clone:?}"
306
+ );
307
+
308
+ case.shutdown();
309
+ }