@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.
- package/Cargo.lock +8 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -0
- package/crates/team-agent/src/cli/adapters.rs +5 -0
- package/crates/team-agent/src/cli/diagnose.rs +82 -0
- package/crates/team-agent/src/cli/emit.rs +3 -3
- package/crates/team-agent/src/cli/grok_slot.rs +299 -0
- package/crates/team-agent/src/cli/leader.rs +21 -7
- package/crates/team-agent/src/cli/leaders.rs +125 -1
- package/crates/team-agent/src/cli/mod.rs +20 -0
- package/crates/team-agent/src/cli/send/presentation.rs +7 -1
- package/crates/team-agent/src/cli/spec.rs +2 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
- package/crates/team-agent/src/cli/tests/base.rs +26 -3
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
- package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
- package/crates/team-agent/src/communication_mode/mod.rs +6 -4
- package/crates/team-agent/src/compiler.rs +59 -0
- package/crates/team-agent/src/coordinator/backoff.rs +55 -0
- package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
- package/crates/team-agent/src/coordinator/health.rs +173 -0
- package/crates/team-agent/src/coordinator/mod.rs +31 -0
- package/crates/team-agent/src/coordinator/orphan.rs +31 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
- package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
- package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
- package/crates/team-agent/src/coordinator/tick.rs +133 -22
- package/crates/team-agent/src/coordinator/types.rs +49 -0
- package/crates/team-agent/src/db/message_store.rs +39 -5
- package/crates/team-agent/src/layout/worker_env.rs +20 -3
- package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
- package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
- package/crates/team-agent/src/leader/registry.rs +65 -0
- package/crates/team-agent/src/leader/start.rs +920 -63
- package/crates/team-agent/src/lifecycle/display.rs +56 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -12
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +62 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +59 -0
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
- package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -456
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
- package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +62 -2
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +139 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
- package/crates/team-agent/src/lifecycle/launch.rs +99 -29
- package/crates/team-agent/src/lifecycle/lock.rs +42 -1
- package/crates/team-agent/src/lifecycle/mod.rs +11 -0
- package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +124 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +369 -11
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
- package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
- package/crates/team-agent/src/lifecycle/restart.rs +29 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
- package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
- package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +2 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
- package/crates/team-agent/src/lifecycle/tests.rs +23 -13
- package/crates/team-agent/src/lifecycle/types.rs +61 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +85 -11
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
- package/crates/team-agent/src/mcp_server/tests.rs +196 -2
- package/crates/team-agent/src/messaging/delivery.rs +405 -40
- package/crates/team-agent/src/messaging/helpers.rs +2 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
- package/crates/team-agent/src/messaging/results.rs +4 -0
- package/crates/team-agent/src/messaging/send.rs +34 -0
- package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
- package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/types.rs +4 -1
- package/crates/team-agent/src/model/enums.rs +14 -5
- package/crates/team-agent/src/model/permissions.rs +3 -0
- package/crates/team-agent/src/os_probe.rs +73 -2
- package/crates/team-agent/src/provider/adapter.rs +231 -4
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
- package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
- package/crates/team-agent/src/provider/bypass_flags.rs +46 -2
- package/crates/team-agent/src/provider/classify.rs +4 -2
- package/crates/team-agent/src/provider/faults.rs +2 -1
- package/crates/team-agent/src/provider/mod.rs +11 -0
- package/crates/team-agent/src/provider/session/capture.rs +158 -20
- package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
- package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
- package/crates/team-agent/src/provider/session/mod.rs +18 -0
- package/crates/team-agent/src/provider/session/resume.rs +57 -0
- package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
- package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
- package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
- package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
- package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
- package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
- package/crates/team-agent/src/provider/session_scan.rs +8 -0
- package/crates/team-agent/src/provider/submit_now.rs +94 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
- package/crates/team-agent/src/provider/types.rs +4 -2
- package/crates/team-agent/src/provider/wire.rs +23 -1
- package/crates/team-agent/src/state/persist.rs +301 -8
- package/crates/team-agent/src/state/repository.rs +5 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
- package/crates/team-agent/src/tmux_backend.rs +1064 -75
- package/crates/team-agent/src/transport/tests/wire.rs +28 -2
- package/crates/team-agent/src/transport.rs +248 -1
- package/npm/install.mjs +129 -73
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +33 -238
- package/skills/team-agent/command-coverage.json +31 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
- package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
- 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("
|
|
273
|
-
"
|
|
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
|
+
}
|