@team-agent/installer 0.5.110 → 0.5.112
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 +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +231 -812
- package/crates/team-agent/src/cli/diagnose.rs +2 -424
- package/crates/team-agent/src/cli/emit.rs +493 -451
- package/crates/team-agent/src/cli/leader.rs +4 -9
- package/crates/team-agent/src/cli/leaders.rs +90 -29
- package/crates/team-agent/src/cli/mod.rs +348 -169
- package/crates/team-agent/src/cli/models.rs +80 -24
- package/crates/team-agent/src/cli/named_address.rs +114 -25
- package/crates/team-agent/src/cli/route.rs +10 -5
- package/crates/team-agent/src/cli/send/presentation.rs +60 -41
- package/crates/team-agent/src/cli/send/resolve.rs +25 -18
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +53 -109
- package/crates/team-agent/src/cli/tests/base.rs +189 -82
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -2
- package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
- package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
- package/crates/team-agent/src/cli/tests/mod.rs +0 -2
- package/crates/team-agent/src/cli/tests/named_address.rs +19 -22
- package/crates/team-agent/src/cli/tests/status_send.rs +15 -10
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +2 -2
- package/crates/team-agent/src/cli/types.rs +26 -121
- package/crates/team-agent/src/coordinator/health.rs +3 -283
- package/crates/team-agent/src/coordinator/mod.rs +0 -5
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
- package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
- package/crates/team-agent/src/coordinator/types.rs +1 -21
- package/crates/team-agent/src/db/message_store.rs +0 -42
- package/crates/team-agent/src/db/migration.rs +49 -9
- package/crates/team-agent/src/db/schema.rs +27 -20
- package/crates/team-agent/src/leader/owner_bind.rs +0 -24
- package/crates/team-agent/src/leader/registry.rs +3 -3
- package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
- package/crates/team-agent/src/leader/tests/identity.rs +0 -32
- package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +4 -9
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -1
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
- package/crates/team-agent/src/messaging/mod.rs +0 -2
- package/crates/team-agent/src/messaging/results.rs +0 -58
- package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
- package/crates/team-agent/src/state/repository.rs +1 -5
- package/package.json +4 -4
- package/templates/team_state.md +1 -1
- package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
- package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
- package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
- package/crates/team-agent/src/messaging/peers.rs +0 -34
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
//! `cmd_doctor`(gate/comms/fix-schema/cleanup-orphans 分派)。
|
|
5
5
|
|
|
6
6
|
use super::*;
|
|
7
|
-
use crate::transport::Transport;
|
|
8
7
|
|
|
9
8
|
/// `cmd_quick_start`(`commands.py:18`)。`--json` 或 `!ok` → 整 dict;否则 `result["summary"]`。
|
|
10
9
|
pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
|
|
@@ -16,6 +15,9 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
|
|
|
16
15
|
args.yes,
|
|
17
16
|
args.backend.as_deref(),
|
|
18
17
|
)?;
|
|
18
|
+
if let Some(result) = quick_start_config_guidance(&mut value, args) {
|
|
19
|
+
return Ok(result);
|
|
20
|
+
}
|
|
19
21
|
append_send_guidance(&mut value, &args.workspace, args.team_id.as_deref());
|
|
20
22
|
let readiness = value.get("readiness").and_then(Value::as_object);
|
|
21
23
|
let all_resumable_have_session = readiness
|
|
@@ -46,6 +48,29 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
|
|
|
46
48
|
if args.json && status.as_deref() == Some("pending_tool_load") {
|
|
47
49
|
result.exit = ExitCode::Ok;
|
|
48
50
|
}
|
|
51
|
+
if !args.json {
|
|
52
|
+
if let CmdOutput::Json(value) = &result.output {
|
|
53
|
+
let explanation = if value.get("ok").and_then(Value::as_bool) == Some(false) {
|
|
54
|
+
format!(
|
|
55
|
+
"启动未完成:{}",
|
|
56
|
+
value
|
|
57
|
+
.get("error")
|
|
58
|
+
.and_then(Value::as_str)
|
|
59
|
+
.unwrap_or("队伍尚未准备好接收任务")
|
|
60
|
+
)
|
|
61
|
+
} else {
|
|
62
|
+
"队伍尚未准备好接收任务;请等待工具完成启动。".to_string()
|
|
63
|
+
};
|
|
64
|
+
let mut doctor = format!(
|
|
65
|
+
"team-agent doctor --workspace {}",
|
|
66
|
+
shell_quote(&args.workspace.to_string_lossy())
|
|
67
|
+
);
|
|
68
|
+
if let Some(team) = args.team_id.as_deref() {
|
|
69
|
+
doctor.push_str(&format!(" --team {}", shell_quote(team)));
|
|
70
|
+
}
|
|
71
|
+
result.output = CmdOutput::Human(format!("{explanation}\n下一步:运行 {doctor},按体检提示处理;用 status 查看状态,不反复起队或重发任务。"));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
49
74
|
Ok(result)
|
|
50
75
|
} else {
|
|
51
76
|
// E13:happy 人类路径必须带 attach_commands(json 路径 cli/mod.rs:1775 已有)。
|
|
@@ -53,13 +78,99 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
|
|
|
53
78
|
}
|
|
54
79
|
}
|
|
55
80
|
|
|
81
|
+
// Project only an actual compiler rejection; never reject a valid lifecycle fallback.
|
|
82
|
+
fn quick_start_config_guidance(value: &mut Value, args: &QuickStartArgs) -> Option<CmdResult> {
|
|
83
|
+
if value.get("ok").and_then(Value::as_bool) != Some(false) {
|
|
84
|
+
return None;
|
|
85
|
+
}
|
|
86
|
+
let error = value.get("error")?.as_str()?;
|
|
87
|
+
let compile = error.strip_prefix("spec compile failed: ")?;
|
|
88
|
+
let detail = compile
|
|
89
|
+
.strip_prefix("validation error: ")
|
|
90
|
+
.unwrap_or(compile);
|
|
91
|
+
let team_path = args.agents_dir.join("TEAM.md");
|
|
92
|
+
let agents_path = args.agents_dir.join("agents");
|
|
93
|
+
let missing_team = detail == format!("{}: missing TEAM.md", team_path.display())
|
|
94
|
+
&& team_path.try_exists().ok() == Some(false);
|
|
95
|
+
let missing_roles = detail == format!("{}: missing agents directory", agents_path.display())
|
|
96
|
+
|| detail == format!("{}: no role docs found", agents_path.display());
|
|
97
|
+
let missing_field = detail.split_once(": ").is_some_and(|(path, reason)| {
|
|
98
|
+
Path::new(path).parent() == Some(agents_path.as_path())
|
|
99
|
+
&& (reason.contains("missing required field") || reason.contains("is required"))
|
|
100
|
+
});
|
|
101
|
+
if !missing_team && !(team_path.is_file() && (missing_roles || missing_field)) {
|
|
102
|
+
return None;
|
|
103
|
+
}
|
|
104
|
+
let role_path = agents_path.join("worker.md");
|
|
105
|
+
let mut retry = format!(
|
|
106
|
+
"team-agent quick-start {} --workspace {}",
|
|
107
|
+
shell_quote(&args.agents_dir.to_string_lossy()),
|
|
108
|
+
shell_quote(&args.workspace.to_string_lossy())
|
|
109
|
+
);
|
|
110
|
+
if let Some(team) = args.team_id.as_deref() {
|
|
111
|
+
retry.push_str(&format!(" --team {}", shell_quote(team)));
|
|
112
|
+
}
|
|
113
|
+
if let Some(name) = args.name.as_deref() {
|
|
114
|
+
retry.push_str(&format!(" --name {}", shell_quote(name)));
|
|
115
|
+
}
|
|
116
|
+
if let Some(backend) = args.backend.as_deref() {
|
|
117
|
+
retry.push_str(&format!(" --backend {}", shell_quote(backend)));
|
|
118
|
+
}
|
|
119
|
+
if args.yes {
|
|
120
|
+
retry.push_str(" --yes");
|
|
121
|
+
}
|
|
122
|
+
if args.detail {
|
|
123
|
+
retry.push_str(" --detail");
|
|
124
|
+
}
|
|
125
|
+
if args.json {
|
|
126
|
+
retry.push_str(" --json");
|
|
127
|
+
}
|
|
128
|
+
let explanation = if missing_team {
|
|
129
|
+
format!("这里还没有队伍配置:{}。没有启动任何队友。请自行创建下面两个文件;本命令没有写入这些文件。", team_path.display())
|
|
130
|
+
} else {
|
|
131
|
+
format!("队友配置尚不完整:{detail}。请补齐角色目录或必需字段;下面仅给安全示例,不覆盖已有文件。")
|
|
132
|
+
};
|
|
133
|
+
let next = format!("确认 Pi 已安装并登录;运行 team-agent models --provider pi 核对模型。\n运行 team-agent pi,在该主控的命令行/工具上下文再次执行:\n{retry}");
|
|
134
|
+
let mut templates =
|
|
135
|
+
vec![serde_json::json!({"path": role_path, "content": super::emit::WORKER_TEMPLATE})];
|
|
136
|
+
let mut human = explanation.clone();
|
|
137
|
+
if missing_team {
|
|
138
|
+
templates.insert(
|
|
139
|
+
0,
|
|
140
|
+
serde_json::json!({"path": team_path, "content": super::emit::TEAM_TEMPLATE}),
|
|
141
|
+
);
|
|
142
|
+
human.push_str(&format!(
|
|
143
|
+
"\n\n{}:\n{}",
|
|
144
|
+
team_path.display(),
|
|
145
|
+
super::emit::TEAM_TEMPLATE
|
|
146
|
+
));
|
|
147
|
+
}
|
|
148
|
+
human.push_str(&format!(
|
|
149
|
+
"\n\n{}:\n{}\n下一步:\n{next}",
|
|
150
|
+
role_path.display(),
|
|
151
|
+
super::emit::WORKER_TEMPLATE
|
|
152
|
+
));
|
|
153
|
+
value["action"] = serde_json::json!(explanation);
|
|
154
|
+
value["next_actions"] =
|
|
155
|
+
serde_json::json!(["team-agent models --provider pi", "team-agent pi", retry]);
|
|
156
|
+
value["templates"] = serde_json::json!(templates);
|
|
157
|
+
if args.json {
|
|
158
|
+
Some(CmdResult::from_json(value.clone(), true))
|
|
159
|
+
} else {
|
|
160
|
+
let mut result = CmdResult::human(&human);
|
|
161
|
+
result.exit = ExitCode::Error;
|
|
162
|
+
Some(result)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
56
166
|
/// E13:quick-start "team 起了" 人类输出 = summary + attach 块。所有成功出口共用(别每分支手拷)。
|
|
57
167
|
/// attach_commands 缺/空 → 只 summary(向后兼容)。
|
|
58
168
|
fn quickstart_human(value: &Value) -> String {
|
|
59
|
-
let summary = value
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
169
|
+
let summary = if value.get("summary").and_then(Value::as_str) == Some("existing runtime") {
|
|
170
|
+
"发现已有队伍;请查看返回的连接方式,不会另建同名队伍。"
|
|
171
|
+
} else {
|
|
172
|
+
"队伍已启动;下面是连接方式和派发任务的命令。"
|
|
173
|
+
};
|
|
63
174
|
let attach: Vec<&str> = value
|
|
64
175
|
.get("attach_commands")
|
|
65
176
|
.and_then(Value::as_array)
|
|
@@ -72,14 +183,14 @@ fn quickstart_human(value: &Value) -> String {
|
|
|
72
183
|
.unwrap_or_default();
|
|
73
184
|
let mut out = String::from(summary);
|
|
74
185
|
if !attach.is_empty() {
|
|
75
|
-
out.push_str("\n\
|
|
186
|
+
out.push_str("\n\n连接队伍:");
|
|
76
187
|
for cmd in attach {
|
|
77
188
|
out.push_str("\n ");
|
|
78
189
|
out.push_str(cmd);
|
|
79
190
|
}
|
|
80
191
|
}
|
|
81
192
|
if !sends.is_empty() {
|
|
82
|
-
out.push_str("\n\
|
|
193
|
+
out.push_str("\n\n派发任务(请替换任务内容):");
|
|
83
194
|
for cmd in sends {
|
|
84
195
|
out.push_str("\n ");
|
|
85
196
|
out.push_str(cmd);
|
|
@@ -146,7 +257,7 @@ pub(crate) fn send_command(agent: &str, workspace: &Path, team: Option<&str>) ->
|
|
|
146
257
|
let mut command = format!(
|
|
147
258
|
"team-agent send {} {} --workspace {}",
|
|
148
259
|
shell_quote(agent),
|
|
149
|
-
shell_quote("
|
|
260
|
+
shell_quote("请完成任务并把答案回复给 leader。"),
|
|
150
261
|
shell_quote(workspace)
|
|
151
262
|
);
|
|
152
263
|
if let Some(team) = team {
|
|
@@ -156,7 +267,7 @@ pub(crate) fn send_command(agent: &str, workspace: &Path, team: Option<&str>) ->
|
|
|
156
267
|
Some(command)
|
|
157
268
|
}
|
|
158
269
|
|
|
159
|
-
fn shell_quote(value: &str) -> String {
|
|
270
|
+
pub(crate) fn shell_quote(value: &str) -> String {
|
|
160
271
|
if value.bytes().all(|byte| {
|
|
161
272
|
byte.is_ascii_alphanumeric() || matches!(byte, b'/' | b'.' | b'_' | b'-' | b':')
|
|
162
273
|
}) {
|
|
@@ -238,701 +349,11 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
|
|
|
238
349
|
)))
|
|
239
350
|
}
|
|
240
351
|
|
|
241
|
-
/// `cmd_watch`(`commands.py:103`)。委派 `coordinator::run_watch`;KeyboardInterrupt/正常 → `SystemExit(0)`。
|
|
242
|
-
/// 返回 [`CmdResult::none`](不经 emit)。
|
|
243
|
-
pub fn cmd_watch(args: &WatchArgs) -> Result<CmdResult, CliError> {
|
|
244
|
-
let workspace = crate::coordinator::WorkspacePath::new(args.workspace.clone());
|
|
245
|
-
#[cfg(not(test))]
|
|
246
|
-
{
|
|
247
|
-
let mut output = |line: &str| {
|
|
248
|
-
println!("{line}");
|
|
249
|
-
let _ = std::io::Write::flush(&mut std::io::stdout());
|
|
250
|
-
};
|
|
251
|
-
crate::coordinator::run_watch(&workspace, args.team.as_deref(), 1.0, &mut output)
|
|
252
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
253
|
-
Ok(CmdResult::none())
|
|
254
|
-
}
|
|
255
|
-
#[cfg(test)]
|
|
256
|
-
{
|
|
257
|
-
let store = crate::message_store::MessageStore::open(&args.workspace)
|
|
258
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
259
|
-
let mut cursor = crate::coordinator::WatchCursor::default();
|
|
260
|
-
let lines = crate::coordinator::collect_watch_lines(
|
|
261
|
-
&workspace,
|
|
262
|
-
&mut cursor,
|
|
263
|
-
&store,
|
|
264
|
-
args.team.as_deref(),
|
|
265
|
-
)
|
|
266
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
267
|
-
Ok(CmdResult::human(lines.join("\n")))
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/// `cmd_sessions`(`parser.py:230`)。
|
|
272
|
-
pub fn cmd_sessions(args: &SessionsArgs) -> Result<CmdResult, CliError> {
|
|
273
|
-
let selected = crate::state::selector::resolve_active_team(
|
|
274
|
-
&args.workspace,
|
|
275
|
-
args.team.as_deref(),
|
|
276
|
-
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
277
|
-
)
|
|
278
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
279
|
-
let spec = match selected.spec_path.as_ref() {
|
|
280
|
-
Some(path) => load_team_spec_at(path)?,
|
|
281
|
-
None => load_team_spec_optional(&selected.run_workspace, &selected.state)?,
|
|
282
|
-
};
|
|
283
|
-
Ok(CmdResult::from_json(
|
|
284
|
-
json!({
|
|
285
|
-
"ok": true,
|
|
286
|
-
"sessions": sessions_overview(&selected.state, spec.as_ref()),
|
|
287
|
-
"workspace": args.workspace.to_string_lossy().to_string(),
|
|
288
|
-
}),
|
|
289
|
-
args.json,
|
|
290
|
-
))
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
pub fn cmd_results(args: &ResultsArgs) -> Result<CmdResult, CliError> {
|
|
294
|
-
let selected = match crate::state::selector::resolve_active_team(
|
|
295
|
-
&args.workspace,
|
|
296
|
-
args.team.as_deref(),
|
|
297
|
-
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
298
|
-
) {
|
|
299
|
-
Ok(selected) => selected,
|
|
300
|
-
Err(error) => {
|
|
301
|
-
return Ok(CmdResult::from_json(
|
|
302
|
-
json!({
|
|
303
|
-
"ok": false,
|
|
304
|
-
"error": error.to_string(),
|
|
305
|
-
"workspace": args.workspace.to_string_lossy().to_string(),
|
|
306
|
-
}),
|
|
307
|
-
args.json,
|
|
308
|
-
));
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
let results = messaging::results::results_for_case(
|
|
312
|
-
&selected.run_workspace,
|
|
313
|
-
&args.case_id,
|
|
314
|
-
Some(selected.team_key.as_str()),
|
|
315
|
-
args.team.as_deref(),
|
|
316
|
-
)?;
|
|
317
|
-
let value = json!({
|
|
318
|
-
"ok": true,
|
|
319
|
-
"case_id": args.case_id,
|
|
320
|
-
"results": results,
|
|
321
|
-
"workspace": selected.run_workspace.to_string_lossy().to_string(),
|
|
322
|
-
});
|
|
323
|
-
Ok(if args.json {
|
|
324
|
-
CmdResult::from_ordered_json(value)
|
|
325
|
-
} else {
|
|
326
|
-
CmdResult::from_json(value, false)
|
|
327
|
-
})
|
|
328
|
-
}
|
|
329
|
-
|
|
330
352
|
pub fn cmd_wait(args: &WaitArgs) -> Result<CmdResult, CliError> {
|
|
331
353
|
let result = messaging::wait_for_result(&args.workspace, &args.task_id)?;
|
|
332
354
|
Ok(CmdResult::from_json(result.to_json(), args.json))
|
|
333
355
|
}
|
|
334
356
|
|
|
335
|
-
/// `cmd_allow_peer_talk`(`parser.py allow-peer-talk`).
|
|
336
|
-
pub fn cmd_allow_peer_talk(args: &AllowPeerTalkArgs) -> Result<CmdResult, CliError> {
|
|
337
|
-
if args.team.is_some() {
|
|
338
|
-
return Err(CliError::Usage(
|
|
339
|
-
"allow-peer-talk --team is not supported yet because peer allowlist storage is not team-scoped".to_string(),
|
|
340
|
-
));
|
|
341
|
-
}
|
|
342
|
-
let value = messaging::allow_peer_talk(&args.workspace, &args.a, &args.b)?;
|
|
343
|
-
Ok(CmdResult::from_json(value, args.json))
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/// `cmd_preflight`(`parser.py:160`)。
|
|
347
|
-
pub fn cmd_preflight(args: &PreflightArgs) -> Result<CmdResult, CliError> {
|
|
348
|
-
let report = build_preflight_report(&args.team)?;
|
|
349
|
-
Ok(CmdResult::from_json(report, args.json))
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/// `cmd_wait_ready`(`parser.py:171`)。
|
|
353
|
-
pub fn cmd_wait_ready(args: &WaitReadyArgs) -> Result<CmdResult, CliError> {
|
|
354
|
-
let report = build_wait_ready_report(&args.workspace, args.timeout, args.team.as_deref())?;
|
|
355
|
-
Ok(CmdResult::from_json(report, args.json))
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/// `cmd_e2e`(`parser.py:449`)。
|
|
359
|
-
pub fn cmd_e2e(args: &E2eArgs) -> Result<CmdResult, CliError> {
|
|
360
|
-
let mut providers = serde_json::Map::new();
|
|
361
|
-
for provider in &args.providers {
|
|
362
|
-
let result = if provider == "fake" {
|
|
363
|
-
run_fake_e2e(&args.workspace)?
|
|
364
|
-
} else {
|
|
365
|
-
skipped_provider_e2e(provider, args.real)
|
|
366
|
-
};
|
|
367
|
-
providers.insert(provider.clone(), result);
|
|
368
|
-
}
|
|
369
|
-
let ok = providers
|
|
370
|
-
.values()
|
|
371
|
-
.all(|value| value.get("ok").and_then(Value::as_bool) == Some(true));
|
|
372
|
-
Ok(CmdResult::from_json(
|
|
373
|
-
json!({
|
|
374
|
-
"workspace": args.workspace.to_string_lossy().to_string(),
|
|
375
|
-
"providers": Value::Object(providers),
|
|
376
|
-
"ok": ok,
|
|
377
|
-
}),
|
|
378
|
-
args.json,
|
|
379
|
-
))
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/// `cmd_peek`(`commands.py:118`)。
|
|
383
|
-
pub fn cmd_peek(args: &PeekArgs) -> Result<CmdResult, CliError> {
|
|
384
|
-
if !args.allow_raw_screen {
|
|
385
|
-
return Err(CliError::Usage(
|
|
386
|
-
"peek requires --allow-raw-screen".to_string(),
|
|
387
|
-
));
|
|
388
|
-
}
|
|
389
|
-
let state = crate::state::persist::load_runtime_state(&args.workspace)?;
|
|
390
|
-
let Some(agent_state) = state
|
|
391
|
-
.get("agents")
|
|
392
|
-
.and_then(|agents| agents.get(&args.agent))
|
|
393
|
-
else {
|
|
394
|
-
return Ok(CmdResult::from_json(
|
|
395
|
-
json!({
|
|
396
|
-
"ok": false,
|
|
397
|
-
"agent_id": args.agent,
|
|
398
|
-
"error": format!("unknown agent id: {}", args.agent),
|
|
399
|
-
}),
|
|
400
|
-
args.json,
|
|
401
|
-
));
|
|
402
|
-
};
|
|
403
|
-
let Some((session, window, target)) = agent_pane_id(&state, &args.agent, agent_state) else {
|
|
404
|
-
return Ok(peek_unavailable(&args.agent, args.json));
|
|
405
|
-
};
|
|
406
|
-
// 0.5.x Phase 1d Batch 3: factory-routed peek backend so conpty
|
|
407
|
-
// teams peek from the shim scrollback rather than a workspace
|
|
408
|
-
// tmux capture that would always be empty. Tmux teams keep
|
|
409
|
-
// byte-equivalent behavior.
|
|
410
|
-
let backend: Box<dyn crate::transport::Transport> =
|
|
411
|
-
match crate::transport_factory::resolve_read_only_transport(
|
|
412
|
-
&args.workspace,
|
|
413
|
-
Some(&state),
|
|
414
|
-
crate::transport_factory::TransportPurpose::Status,
|
|
415
|
-
) {
|
|
416
|
-
Ok(r) => r.backend,
|
|
417
|
-
Err(_) => Box::new(crate::tmux_backend::TmuxBackend::for_workspace(
|
|
418
|
-
&args.workspace,
|
|
419
|
-
)),
|
|
420
|
-
};
|
|
421
|
-
let windows = backend
|
|
422
|
-
.list_windows(&crate::transport::SessionName::new(session.clone()))
|
|
423
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
424
|
-
if !windows.iter().any(|w| w.as_str() == window) {
|
|
425
|
-
return Ok(peek_unavailable(&args.agent, args.json));
|
|
426
|
-
}
|
|
427
|
-
let range = args
|
|
428
|
-
.head
|
|
429
|
-
.map(|head| crate::transport::CaptureRange::Head(head as u32))
|
|
430
|
-
.unwrap_or_else(|| crate::transport::CaptureRange::Tail(args.tail as u32));
|
|
431
|
-
let capture = backend
|
|
432
|
-
.capture(
|
|
433
|
-
&crate::transport::Target::Pane(crate::transport::PaneId::new(target.clone())),
|
|
434
|
-
range,
|
|
435
|
-
)
|
|
436
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
437
|
-
let matches = args.search.as_ref().map(|needle| {
|
|
438
|
-
capture
|
|
439
|
-
.text
|
|
440
|
-
.lines()
|
|
441
|
-
.filter(|line| line.contains(needle))
|
|
442
|
-
.map(str::to_string)
|
|
443
|
-
.collect::<Vec<_>>()
|
|
444
|
-
});
|
|
445
|
-
Ok(CmdResult::from_json(
|
|
446
|
-
json!({
|
|
447
|
-
"ok": true,
|
|
448
|
-
"agent_id": args.agent,
|
|
449
|
-
"workspace": args.workspace.to_string_lossy().to_string(),
|
|
450
|
-
"tail": args.tail,
|
|
451
|
-
"head": args.head,
|
|
452
|
-
"search": args.search,
|
|
453
|
-
"matches": matches,
|
|
454
|
-
"pane_id": target,
|
|
455
|
-
"text": capture.text,
|
|
456
|
-
}),
|
|
457
|
-
args.json,
|
|
458
|
-
))
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
fn peek_unavailable(agent: &str, json: bool) -> CmdResult {
|
|
462
|
-
CmdResult::from_json(
|
|
463
|
-
json!({
|
|
464
|
-
"ok": false,
|
|
465
|
-
"agent_id": agent,
|
|
466
|
-
"error": format!("agent terminal is not available: {agent}"),
|
|
467
|
-
}),
|
|
468
|
-
json,
|
|
469
|
-
)
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
fn agent_pane_id(
|
|
473
|
-
state: &Value,
|
|
474
|
-
agent: &str,
|
|
475
|
-
agent_state: &Value,
|
|
476
|
-
) -> Option<(String, String, String)> {
|
|
477
|
-
let session = state
|
|
478
|
-
.get("session_name")
|
|
479
|
-
.and_then(Value::as_str)
|
|
480
|
-
.filter(|s| !s.is_empty())?;
|
|
481
|
-
let window = ["window", "window_name"]
|
|
482
|
-
.iter()
|
|
483
|
-
.find_map(|key| {
|
|
484
|
-
agent_state
|
|
485
|
-
.get(*key)
|
|
486
|
-
.and_then(Value::as_str)
|
|
487
|
-
.filter(|s| !s.is_empty())
|
|
488
|
-
})
|
|
489
|
-
.unwrap_or(agent);
|
|
490
|
-
Some((
|
|
491
|
-
session.to_string(),
|
|
492
|
-
window.to_string(),
|
|
493
|
-
format!("{session}:{window}"),
|
|
494
|
-
))
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
fn run_fake_e2e(workspace: &Path) -> Result<Value, CliError> {
|
|
498
|
-
std::fs::create_dir_all(workspace)?;
|
|
499
|
-
let spec_path = workspace.join("team.spec.yaml");
|
|
500
|
-
std::fs::write(&spec_path, fake_spec_yaml(workspace))?;
|
|
501
|
-
let launch = match crate::lifecycle::launch(&spec_path, true, true, true) {
|
|
502
|
-
Ok(report) => json!({
|
|
503
|
-
"ok": true,
|
|
504
|
-
"dry_run": report.dry_run,
|
|
505
|
-
"session_name": report.session_name.as_str(),
|
|
506
|
-
}),
|
|
507
|
-
Err(error) => json!({
|
|
508
|
-
"ok": false,
|
|
509
|
-
"error": error.to_string(),
|
|
510
|
-
}),
|
|
511
|
-
};
|
|
512
|
-
seed_fake_e2e_state(workspace)?;
|
|
513
|
-
let send = messaging::send_message(
|
|
514
|
-
workspace,
|
|
515
|
-
&MessageTarget::Single("fake_impl".to_string()),
|
|
516
|
-
"implement fake task",
|
|
517
|
-
&SendOptions {
|
|
518
|
-
task_id: Some(TaskId::new("task_impl")),
|
|
519
|
-
route_task_id: false,
|
|
520
|
-
sender: messaging::TrustedSender::leader(),
|
|
521
|
-
requires_ack: true,
|
|
522
|
-
..SendOptions::default()
|
|
523
|
-
},
|
|
524
|
-
)?;
|
|
525
|
-
let send_value = json!({
|
|
526
|
-
"ok": send.ok,
|
|
527
|
-
"status": send.status,
|
|
528
|
-
"message_status": send.message_status.0,
|
|
529
|
-
"message_id": send.message_id,
|
|
530
|
-
"reason": send.reason,
|
|
531
|
-
});
|
|
532
|
-
if send.ok {
|
|
533
|
-
if let Some(message_id) = send.message_id.as_deref() {
|
|
534
|
-
crate::message_store::MessageStore::open(workspace)
|
|
535
|
-
.map_err(crate::messaging::MessagingError::from)?
|
|
536
|
-
.mark(message_id, "delivered", None)
|
|
537
|
-
.map_err(crate::messaging::MessagingError::from)?;
|
|
538
|
-
}
|
|
539
|
-
let report = messaging::report_result(
|
|
540
|
-
workspace,
|
|
541
|
-
&json!({
|
|
542
|
-
"schema_version": "result_envelope_v1",
|
|
543
|
-
"task_id": "task_impl",
|
|
544
|
-
"agent_id": "fake_impl",
|
|
545
|
-
"status": "success",
|
|
546
|
-
"summary": "fake result finalized",
|
|
547
|
-
"changes": [],
|
|
548
|
-
"tests": [],
|
|
549
|
-
"risks": [],
|
|
550
|
-
"artifacts": [],
|
|
551
|
-
"next_actions": [],
|
|
552
|
-
}),
|
|
553
|
-
)?;
|
|
554
|
-
let auto_finalized = report
|
|
555
|
-
.get("finalization_status")
|
|
556
|
-
.and_then(Value::as_str)
|
|
557
|
-
== Some("auto_finalized");
|
|
558
|
-
let shutdown = fake_shutdown(workspace)?;
|
|
559
|
-
let ok = launch.get("ok").and_then(Value::as_bool) == Some(true)
|
|
560
|
-
&& send.ok
|
|
561
|
-
&& auto_finalized
|
|
562
|
-
&& shutdown.get("ok").and_then(Value::as_bool) == Some(true);
|
|
563
|
-
return Ok(json!({
|
|
564
|
-
"ok": ok,
|
|
565
|
-
"launch": launch,
|
|
566
|
-
"send": send_value,
|
|
567
|
-
"report": report,
|
|
568
|
-
"shutdown": shutdown,
|
|
569
|
-
}));
|
|
570
|
-
}
|
|
571
|
-
let shutdown = fake_shutdown(workspace)?;
|
|
572
|
-
Ok(json!({
|
|
573
|
-
"ok": false,
|
|
574
|
-
"launch": launch,
|
|
575
|
-
"send": send_value,
|
|
576
|
-
"report": null,
|
|
577
|
-
"shutdown": shutdown,
|
|
578
|
-
}))
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
fn skipped_provider_e2e(provider: &str, real: bool) -> Value {
|
|
582
|
-
let command = provider_command(provider);
|
|
583
|
-
if !command_on_path(command) {
|
|
584
|
-
return json!({
|
|
585
|
-
"ok": false,
|
|
586
|
-
"skipped": true,
|
|
587
|
-
"reason": format!("{command} not installed"),
|
|
588
|
-
"version": null,
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
let version = command_version(command);
|
|
592
|
-
if !real {
|
|
593
|
-
return json!({
|
|
594
|
-
"ok": false,
|
|
595
|
-
"skipped": true,
|
|
596
|
-
"reason": "real provider launch disabled; rerun with --real on an authenticated machine",
|
|
597
|
-
"version": version,
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
json!({
|
|
601
|
-
"ok": false,
|
|
602
|
-
"skipped": true,
|
|
603
|
-
"reason": "real provider e2e is not available in this build",
|
|
604
|
-
"version": version,
|
|
605
|
-
})
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
fn command_on_path(command: &str) -> bool {
|
|
609
|
-
std::env::var_os("PATH").is_some_and(|paths| {
|
|
610
|
-
std::env::split_paths(&paths).any(|dir| {
|
|
611
|
-
let candidate = dir.join(command);
|
|
612
|
-
candidate.is_file()
|
|
613
|
-
})
|
|
614
|
-
})
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
fn command_version(command: &str) -> Value {
|
|
618
|
-
match std::process::Command::new(command)
|
|
619
|
-
.arg("--version")
|
|
620
|
-
.output()
|
|
621
|
-
{
|
|
622
|
-
Ok(output) if output.status.success() => {
|
|
623
|
-
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
624
|
-
if text.is_empty() {
|
|
625
|
-
Value::Null
|
|
626
|
-
} else {
|
|
627
|
-
Value::String(text)
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
_ => Value::Null,
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
fn provider_command(provider: &str) -> &str {
|
|
635
|
-
// E2E provider command grammar, not persisted provider identity parsing.
|
|
636
|
-
match provider {
|
|
637
|
-
"claude" | "claude_code" => "claude",
|
|
638
|
-
"gemini" | "gemini_cli" => "gemini",
|
|
639
|
-
other => other,
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
fn seed_fake_e2e_state(workspace: &Path) -> Result<(), CliError> {
|
|
644
|
-
crate::state::repository::StateRepository::new(workspace).save(
|
|
645
|
-
crate::state::repository::StateWriteIntent::FakeE2eSeed,
|
|
646
|
-
&json!({
|
|
647
|
-
"leader": {"id": "leader"},
|
|
648
|
-
"session_name": "team-agent-fake-e2e",
|
|
649
|
-
"agents": {
|
|
650
|
-
"fake_impl": {
|
|
651
|
-
"provider": "fake",
|
|
652
|
-
"status": "running",
|
|
653
|
-
"window": "fake_impl",
|
|
654
|
-
"pane_id": "%fake_impl",
|
|
655
|
-
"spawn_cwd": workspace.to_string_lossy().to_string(),
|
|
656
|
-
}
|
|
657
|
-
},
|
|
658
|
-
"tasks": [{
|
|
659
|
-
"id": "task_impl",
|
|
660
|
-
"title": "Fake implementation",
|
|
661
|
-
"type": "implementation",
|
|
662
|
-
"assignee": "fake_impl",
|
|
663
|
-
"status": "pending",
|
|
664
|
-
}]
|
|
665
|
-
}),
|
|
666
|
-
)?;
|
|
667
|
-
Ok(())
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
fn fake_shutdown(workspace: &Path) -> Result<Value, CliError> {
|
|
671
|
-
let mut state = crate::state::persist::load_runtime_state(workspace)?;
|
|
672
|
-
if let Some(agents) = state.get_mut("agents").and_then(Value::as_object_mut) {
|
|
673
|
-
for agent in agents.values_mut() {
|
|
674
|
-
if let Some(obj) = agent.as_object_mut() {
|
|
675
|
-
obj.insert("status".to_string(), Value::String("stopped".to_string()));
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
crate::state::repository::StateRepository::new(workspace).save(
|
|
680
|
-
crate::state::repository::StateWriteIntent::FakeE2eSeed,
|
|
681
|
-
&state,
|
|
682
|
-
)?;
|
|
683
|
-
Ok(json!({
|
|
684
|
-
"ok": true,
|
|
685
|
-
"session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
|
|
686
|
-
"session_killed": false,
|
|
687
|
-
"coordinator": {"status": "missing", "pid": null},
|
|
688
|
-
}))
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
fn fake_spec_yaml(workspace: &Path) -> String {
|
|
692
|
-
let ws = workspace.to_string_lossy();
|
|
693
|
-
format!(
|
|
694
|
-
r#"version: 1
|
|
695
|
-
team:
|
|
696
|
-
name: "fake-e2e"
|
|
697
|
-
mode: "supervisor_worker"
|
|
698
|
-
objective: "Exercise fake provider orchestration."
|
|
699
|
-
workspace: "{ws}"
|
|
700
|
-
leader:
|
|
701
|
-
id: "leader"
|
|
702
|
-
role: "leader"
|
|
703
|
-
provider: "fake"
|
|
704
|
-
model: null
|
|
705
|
-
tools:
|
|
706
|
-
- "fs_read"
|
|
707
|
-
- "fs_list"
|
|
708
|
-
- "mcp_team"
|
|
709
|
-
context_policy:
|
|
710
|
-
keep_user_thread: true
|
|
711
|
-
receive_worker_outputs: "structured_only"
|
|
712
|
-
max_worker_result_tokens: 2000
|
|
713
|
-
agents:
|
|
714
|
-
- id: "fake_impl"
|
|
715
|
-
role: "implementation_engineer"
|
|
716
|
-
provider: "fake"
|
|
717
|
-
model: null
|
|
718
|
-
working_directory: "{ws}"
|
|
719
|
-
system_prompt:
|
|
720
|
-
inline: "Handle fake implementation tasks."
|
|
721
|
-
file: null
|
|
722
|
-
tools:
|
|
723
|
-
- "fs_read"
|
|
724
|
-
- "fs_write"
|
|
725
|
-
- "fs_list"
|
|
726
|
-
- "execute_bash"
|
|
727
|
-
- "git_diff"
|
|
728
|
-
- "mcp_team"
|
|
729
|
-
- "provider_builtin"
|
|
730
|
-
permission_mode: "restricted"
|
|
731
|
-
preferred_for:
|
|
732
|
-
- "implementation"
|
|
733
|
-
avoid_for: []
|
|
734
|
-
output_contract:
|
|
735
|
-
format: "result_envelope_v1"
|
|
736
|
-
required_fields:
|
|
737
|
-
- "task_id"
|
|
738
|
-
- "status"
|
|
739
|
-
- "summary"
|
|
740
|
-
- "artifacts"
|
|
741
|
-
routing:
|
|
742
|
-
default_assignee: "leader"
|
|
743
|
-
rules:
|
|
744
|
-
- id: "implementation-to-fake"
|
|
745
|
-
match:
|
|
746
|
-
type:
|
|
747
|
-
- "implementation"
|
|
748
|
-
assign_to: "fake_impl"
|
|
749
|
-
priority: 10
|
|
750
|
-
communication:
|
|
751
|
-
protocol: "mcp_inbox"
|
|
752
|
-
topology: "leader_centered"
|
|
753
|
-
worker_to_worker: true
|
|
754
|
-
ack_timeout_sec: 2
|
|
755
|
-
result_format: "result_envelope_v1"
|
|
756
|
-
message_store:
|
|
757
|
-
sqlite: ".team/runtime/team.db"
|
|
758
|
-
mirror_files: ".team/messages"
|
|
759
|
-
runtime:
|
|
760
|
-
backend: "tmux"
|
|
761
|
-
session_name: "team-agent-fake-e2e"
|
|
762
|
-
auto_launch: true
|
|
763
|
-
require_user_approval_before_launch: false
|
|
764
|
-
max_active_agents: 1
|
|
765
|
-
startup_order:
|
|
766
|
-
- "fake_impl"
|
|
767
|
-
context:
|
|
768
|
-
state_file: "team_state.md"
|
|
769
|
-
artifact_dir: ".team/artifacts"
|
|
770
|
-
log_dir: ".team/logs"
|
|
771
|
-
summarization:
|
|
772
|
-
worker_full_logs: "retain_outside_leader_context"
|
|
773
|
-
state_update: "after_each_result"
|
|
774
|
-
tasks:
|
|
775
|
-
- id: "task_impl"
|
|
776
|
-
title: "Fake implementation"
|
|
777
|
-
type: "implementation"
|
|
778
|
-
assignee: "fake_impl"
|
|
779
|
-
deps: []
|
|
780
|
-
acceptance:
|
|
781
|
-
- "fake result collected"
|
|
782
|
-
status: "pending"
|
|
783
|
-
requires_tools:
|
|
784
|
-
- "fs_write"
|
|
785
|
-
- "execute_bash"
|
|
786
|
-
files:
|
|
787
|
-
- "src/example.py"
|
|
788
|
-
risk: "low"
|
|
789
|
-
"#
|
|
790
|
-
)
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
fn sessions_overview(state: &Value, spec: Option<&crate::model::yaml::Value>) -> Value {
|
|
794
|
-
let mut rows = Vec::new();
|
|
795
|
-
if let Some(agents) = spec
|
|
796
|
-
.and_then(|v| v.get("agents"))
|
|
797
|
-
.and_then(crate::model::yaml::Value::as_list)
|
|
798
|
-
{
|
|
799
|
-
for agent in agents {
|
|
800
|
-
let Some(agent_id) = agent.get("id").and_then(crate::model::yaml::Value::as_str) else {
|
|
801
|
-
continue;
|
|
802
|
-
};
|
|
803
|
-
let agent_state = state
|
|
804
|
-
.get("agents")
|
|
805
|
-
.and_then(|agents| agents.get(agent_id))
|
|
806
|
-
.unwrap_or(&Value::Null);
|
|
807
|
-
rows.push(session_row(state, agent, agent_id, agent_state));
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
Value::Array(rows)
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
fn session_row(
|
|
814
|
-
state: &Value,
|
|
815
|
-
spec_agent: &crate::model::yaml::Value,
|
|
816
|
-
agent_id: &str,
|
|
817
|
-
agent_state: &Value,
|
|
818
|
-
) -> Value {
|
|
819
|
-
json!({
|
|
820
|
-
"agent_id": agent_id,
|
|
821
|
-
"provider": yaml_str(spec_agent, "provider").map(Value::String).unwrap_or(Value::Null),
|
|
822
|
-
"model": yaml_str(spec_agent, "model").map(Value::String).unwrap_or(Value::Null),
|
|
823
|
-
"profile": yaml_str(spec_agent, "profile").map(Value::String).unwrap_or(Value::Null),
|
|
824
|
-
"session_id": field_or_null(agent_state, &["session_id"]),
|
|
825
|
-
"resume_id": field_or_null(agent_state, &["resume_id"]),
|
|
826
|
-
"rollout_path": field_or_null(agent_state, &["rollout_path"]),
|
|
827
|
-
"captured_at": field_or_null(agent_state, &["captured_at"]),
|
|
828
|
-
"captured_via": field_or_null(agent_state, &["captured_via"]),
|
|
829
|
-
"attribution_confidence": field_or_null(agent_state, &["attribution_confidence"]),
|
|
830
|
-
"spawn_cwd": field_or_null(agent_state, &["spawn_cwd", "working_directory"]),
|
|
831
|
-
"context_usage": field_or_null(agent_state, &["context_usage"]),
|
|
832
|
-
"status": field_or_default(agent_state, "status", "unknown"),
|
|
833
|
-
"last_task": last_task_for_agent(state, agent_id),
|
|
834
|
-
"handoff_path": field_or_null(agent_state, &["handoff_path"]),
|
|
835
|
-
"display_target": field_or_null(agent_state, &["display"]),
|
|
836
|
-
"terminal_target": terminal_target(state, agent_id, agent_state),
|
|
837
|
-
})
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
fn yaml_str(value: &crate::model::yaml::Value, key: &str) -> Option<String> {
|
|
841
|
-
value
|
|
842
|
-
.get(key)
|
|
843
|
-
.and_then(crate::model::yaml::Value::as_str)
|
|
844
|
-
.map(ToString::to_string)
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
fn field_or_null(value: &Value, keys: &[&str]) -> Value {
|
|
848
|
-
keys.iter()
|
|
849
|
-
.find_map(|key| value.get(*key).cloned())
|
|
850
|
-
.unwrap_or(Value::Null)
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
fn field_or_default(value: &Value, key: &str, default: &str) -> Value {
|
|
854
|
-
value
|
|
855
|
-
.get(key)
|
|
856
|
-
.cloned()
|
|
857
|
-
.unwrap_or_else(|| Value::String(default.to_string()))
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
fn last_task_for_agent(state: &Value, agent_id: &str) -> Value {
|
|
861
|
-
state
|
|
862
|
-
.get("tasks")
|
|
863
|
-
.and_then(Value::as_array)
|
|
864
|
-
.and_then(|tasks| {
|
|
865
|
-
tasks
|
|
866
|
-
.iter()
|
|
867
|
-
.rev()
|
|
868
|
-
.find(|task| task.get("assignee").and_then(Value::as_str) == Some(agent_id))
|
|
869
|
-
})
|
|
870
|
-
.and_then(|task| {
|
|
871
|
-
task.get("id")
|
|
872
|
-
.and_then(Value::as_str)
|
|
873
|
-
.map(ToString::to_string)
|
|
874
|
-
})
|
|
875
|
-
.map(Value::String)
|
|
876
|
-
.unwrap_or(Value::Null)
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
fn terminal_target(state: &Value, agent_id: &str, agent_state: &Value) -> Value {
|
|
880
|
-
json!({
|
|
881
|
-
"session": agent_state
|
|
882
|
-
.get("session_name")
|
|
883
|
-
.cloned()
|
|
884
|
-
.or_else(|| state.get("session_name").cloned())
|
|
885
|
-
.unwrap_or(Value::Null),
|
|
886
|
-
"window": window_target(agent_state, agent_id),
|
|
887
|
-
"pane": field_or_null(agent_state, &["pane_id"]),
|
|
888
|
-
})
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
fn window_target(agent_state: &Value, agent_id: &str) -> Value {
|
|
892
|
-
let window = field_or_null(agent_state, &["window", "window_name"]);
|
|
893
|
-
if window.is_null() {
|
|
894
|
-
Value::String(agent_id.to_string())
|
|
895
|
-
} else {
|
|
896
|
-
window
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
fn load_team_spec_optional(
|
|
901
|
-
workspace: &Path,
|
|
902
|
-
state: &Value,
|
|
903
|
-
) -> Result<Option<crate::model::yaml::Value>, CliError> {
|
|
904
|
-
let spec_path = state
|
|
905
|
-
.get("spec_path")
|
|
906
|
-
.and_then(Value::as_str)
|
|
907
|
-
.filter(|path| !path.is_empty())
|
|
908
|
-
.map(PathBuf::from)
|
|
909
|
-
.or_else(|| {
|
|
910
|
-
state
|
|
911
|
-
.get("team_dir")
|
|
912
|
-
.and_then(Value::as_str)
|
|
913
|
-
.filter(|path| !path.is_empty())
|
|
914
|
-
.map(|path| PathBuf::from(path).join("team.spec.yaml"))
|
|
915
|
-
})
|
|
916
|
-
.unwrap_or_else(|| workspace.join("team.spec.yaml"));
|
|
917
|
-
if !spec_path.exists() {
|
|
918
|
-
return Ok(None);
|
|
919
|
-
}
|
|
920
|
-
let text = std::fs::read_to_string(&spec_path)?;
|
|
921
|
-
crate::model::yaml::loads(&text)
|
|
922
|
-
.map(Some)
|
|
923
|
-
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
fn load_team_spec_at(spec_path: &Path) -> Result<Option<crate::model::yaml::Value>, CliError> {
|
|
927
|
-
if !spec_path.exists() {
|
|
928
|
-
return Ok(None);
|
|
929
|
-
}
|
|
930
|
-
let text = std::fs::read_to_string(spec_path)?;
|
|
931
|
-
crate::model::yaml::loads(&text)
|
|
932
|
-
.map(Some)
|
|
933
|
-
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
934
|
-
}
|
|
935
|
-
|
|
936
357
|
/// `cmd_approvals`(`commands.py:112`)。
|
|
937
358
|
pub fn cmd_approvals(args: &ApprovalsArgs) -> Result<CmdResult, CliError> {
|
|
938
359
|
let selected = crate::state::selector::resolve_active_team(
|
|
@@ -1015,27 +436,24 @@ fn format_inbox_human(agent: &str, value: &Value) -> String {
|
|
|
1015
436
|
.and_then(Value::as_str)
|
|
1016
437
|
.unwrap_or("-"),
|
|
1017
438
|
);
|
|
1018
|
-
let sender =
|
|
1019
|
-
message.get("sender").and_then(Value::as_str).unwrap_or("-")
|
|
1020
|
-
);
|
|
439
|
+
let sender =
|
|
440
|
+
clean_inbox_field(message.get("sender").and_then(Value::as_str).unwrap_or("-"));
|
|
1021
441
|
let recipient = clean_inbox_field(
|
|
1022
442
|
message
|
|
1023
443
|
.get("recipient")
|
|
1024
444
|
.and_then(Value::as_str)
|
|
1025
445
|
.unwrap_or("-"),
|
|
1026
446
|
);
|
|
1027
|
-
let status =
|
|
1028
|
-
message.get("status").and_then(Value::as_str).unwrap_or("-")
|
|
1029
|
-
);
|
|
447
|
+
let status =
|
|
448
|
+
clean_inbox_field(message.get("status").and_then(Value::as_str).unwrap_or("-"));
|
|
1030
449
|
let time = clean_inbox_field(
|
|
1031
450
|
message
|
|
1032
451
|
.get("created_at")
|
|
1033
452
|
.and_then(Value::as_str)
|
|
1034
453
|
.unwrap_or("-"),
|
|
1035
454
|
);
|
|
1036
|
-
let summary =
|
|
1037
|
-
message.get("summary").and_then(Value::as_str).unwrap_or("")
|
|
1038
|
-
);
|
|
455
|
+
let summary =
|
|
456
|
+
clean_inbox_field(message.get("summary").and_then(Value::as_str).unwrap_or(""));
|
|
1039
457
|
truncate_inbox_line(&format!(
|
|
1040
458
|
"[{id}] [{sender} -> {recipient}] [{status}] [{time}] [{summary}]"
|
|
1041
459
|
))
|
|
@@ -1054,22 +472,13 @@ pub fn cmd_takeover(args: &TakeoverArgs) -> Result<CmdResult, CliError> {
|
|
|
1054
472
|
|
|
1055
473
|
/// `cmd_claim_leader`(`commands.py:156`)。
|
|
1056
474
|
pub fn cmd_claim_leader(args: &ClaimLeaderArgs) -> Result<CmdResult, CliError> {
|
|
1057
|
-
let mut value =
|
|
1058
|
-
leader_port::claim_leader(&args.workspace, args.team.as_deref(), args.confirm)?;
|
|
475
|
+
let mut value = leader_port::claim_leader(&args.workspace, args.team.as_deref(), args.confirm)?;
|
|
1059
476
|
if !args.detail {
|
|
1060
477
|
leader_port::compact_lease_value(&mut value);
|
|
1061
478
|
}
|
|
1062
479
|
Ok(CmdResult::from_json(value, args.json))
|
|
1063
480
|
}
|
|
1064
481
|
|
|
1065
|
-
/// `cmd_identity`(`commands.py:160`)。
|
|
1066
|
-
pub fn cmd_identity(args: &IdentityArgs) -> Result<CmdResult, CliError> {
|
|
1067
|
-
Ok(CmdResult::from_json(
|
|
1068
|
-
leader_port::leader_identity(&args.workspace, args.team.as_deref())?,
|
|
1069
|
-
args.json,
|
|
1070
|
-
))
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
482
|
/// `cmd_shutdown`(`commands.py:340`)。
|
|
1074
483
|
pub fn cmd_shutdown(args: &ShutdownArgs) -> Result<CmdResult, CliError> {
|
|
1075
484
|
Ok(CmdResult::from_json(
|
|
@@ -1104,10 +513,7 @@ pub fn cmd_start_agent(args: &StartAgentArgs) -> Result<CmdResult, CliError> {
|
|
|
1104
513
|
)?;
|
|
1105
514
|
if value.get("agent_ids").is_none() {
|
|
1106
515
|
if let Some(object) = value.as_object_mut() {
|
|
1107
|
-
object.insert(
|
|
1108
|
-
"agent_ids".to_string(),
|
|
1109
|
-
json!([args.agent.as_str()]),
|
|
1110
|
-
);
|
|
516
|
+
object.insert("agent_ids".to_string(), json!([args.agent.as_str()]));
|
|
1111
517
|
}
|
|
1112
518
|
}
|
|
1113
519
|
append_send_guidance(&mut value, &args.workspace, args.team.as_deref());
|
|
@@ -1147,10 +553,7 @@ pub fn cmd_add_agent(args: &AddAgentArgs) -> Result<CmdResult, CliError> {
|
|
|
1147
553
|
)?;
|
|
1148
554
|
if value.get("agent_ids").is_none() {
|
|
1149
555
|
if let Some(object) = value.as_object_mut() {
|
|
1150
|
-
object.insert(
|
|
1151
|
-
"agent_ids".to_string(),
|
|
1152
|
-
json!([args.agent.as_str()]),
|
|
1153
|
-
);
|
|
556
|
+
object.insert("agent_ids".to_string(), json!([args.agent.as_str()]));
|
|
1154
557
|
}
|
|
1155
558
|
}
|
|
1156
559
|
append_send_guidance(&mut value, &args.workspace, args.team.as_deref());
|
|
@@ -1222,10 +625,8 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
|
|
|
1222
625
|
}
|
|
1223
626
|
|
|
1224
627
|
let explicit_comms = args.comms || matches!(args.gate, Some(DoctorGate::Comms));
|
|
1225
|
-
let default_report =
|
|
1226
|
-
&& args.gate.is_none()
|
|
1227
|
-
&& !args.cleanup_orphans
|
|
1228
|
-
&& !args.fix_schema;
|
|
628
|
+
let default_report =
|
|
629
|
+
!explicit_comms && args.gate.is_none() && !args.cleanup_orphans && !args.fix_schema;
|
|
1229
630
|
let mut value = if explicit_comms {
|
|
1230
631
|
crate::diagnose::comms::doctor_comms_json(
|
|
1231
632
|
&args.workspace,
|
|
@@ -1282,22 +683,28 @@ fn unified_default_doctor_report(args: &DoctorArgs, mut value: Value) -> Value {
|
|
|
1282
683
|
args.team.as_deref(),
|
|
1283
684
|
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
1284
685
|
);
|
|
1285
|
-
let run_workspace = selected
|
|
1286
|
-
.
|
|
1287
|
-
|
|
686
|
+
let run_workspace = selected
|
|
687
|
+
.as_ref()
|
|
688
|
+
.map(|selected| selected.run_workspace.clone())
|
|
689
|
+
.unwrap_or_else(|_| {
|
|
690
|
+
crate::model::paths::canonical_run_workspace(&args.workspace)
|
|
691
|
+
.unwrap_or_else(|_| args.workspace.clone())
|
|
692
|
+
});
|
|
1288
693
|
let runtime_present = selected.as_ref().is_ok_and(|selected| {
|
|
1289
694
|
crate::cli::diagnose::workspace_has_existing_team_runtime(
|
|
1290
695
|
&selected.run_workspace,
|
|
1291
696
|
&selected.team_key,
|
|
1292
697
|
)
|
|
1293
698
|
});
|
|
1294
|
-
let db_present = crate::model::paths::runtime_dir(&run_workspace)
|
|
699
|
+
let db_present = crate::model::paths::runtime_dir(&run_workspace)
|
|
700
|
+
.join("team.db")
|
|
701
|
+
.exists();
|
|
1295
702
|
// One read-only observation supplies both issue classification and details.
|
|
1296
703
|
// A physical database must be checked even before state.json is created.
|
|
1297
704
|
let health = (runtime_present || db_present).then(|| {
|
|
1298
|
-
crate::coordinator::coordinator_health_read_only(
|
|
1299
|
-
|
|
1300
|
-
)
|
|
705
|
+
crate::coordinator::coordinator_health_read_only(&crate::coordinator::WorkspacePath::new(
|
|
706
|
+
run_workspace.clone(),
|
|
707
|
+
))
|
|
1301
708
|
});
|
|
1302
709
|
match selected {
|
|
1303
710
|
Ok(selected) => {
|
|
@@ -1358,10 +765,17 @@ fn unified_default_doctor_report(args: &DoctorArgs, mut value: Value) -> Value {
|
|
|
1358
765
|
}
|
|
1359
766
|
if let Some(object) = value.as_object_mut() {
|
|
1360
767
|
object.insert("runtime".to_string(), runtime);
|
|
1361
|
-
object.insert(
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
768
|
+
object.insert(
|
|
769
|
+
"event_log".to_string(),
|
|
770
|
+
Value::String(
|
|
771
|
+
run_workspace
|
|
772
|
+
.join(".team")
|
|
773
|
+
.join("logs")
|
|
774
|
+
.join("events.jsonl")
|
|
775
|
+
.to_string_lossy()
|
|
776
|
+
.to_string(),
|
|
777
|
+
),
|
|
778
|
+
);
|
|
1365
779
|
}
|
|
1366
780
|
finalize_doctor_report(&mut value, true);
|
|
1367
781
|
value
|
|
@@ -1385,7 +799,10 @@ fn merge_report_array(report: &mut Value, key: &str, incoming: &Value) {
|
|
|
1385
799
|
.expect("report array normalized before merge");
|
|
1386
800
|
for item in items {
|
|
1387
801
|
let identity = report_item_identity(item);
|
|
1388
|
-
if !target
|
|
802
|
+
if !target
|
|
803
|
+
.iter()
|
|
804
|
+
.any(|existing| report_item_identity(existing) == identity)
|
|
805
|
+
{
|
|
1389
806
|
target.push(item.clone());
|
|
1390
807
|
}
|
|
1391
808
|
}
|
|
@@ -1450,11 +867,17 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
|
|
|
1450
867
|
.and_then(|value| value.as_array().cloned())
|
|
1451
868
|
.unwrap_or_default();
|
|
1452
869
|
let mut add = |issue: Value, repair: Option<Value>| {
|
|
1453
|
-
if !issues
|
|
870
|
+
if !issues
|
|
871
|
+
.iter()
|
|
872
|
+
.any(|existing| report_item_identity(existing) == report_item_identity(&issue))
|
|
873
|
+
{
|
|
1454
874
|
issues.push(issue);
|
|
1455
875
|
}
|
|
1456
876
|
if let Some(repair) = repair {
|
|
1457
|
-
if !repairs
|
|
877
|
+
if !repairs
|
|
878
|
+
.iter()
|
|
879
|
+
.any(|existing| report_item_identity(existing) == report_item_identity(&repair))
|
|
880
|
+
{
|
|
1458
881
|
repairs.push(repair);
|
|
1459
882
|
}
|
|
1460
883
|
}
|
|
@@ -1467,10 +890,10 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
|
|
|
1467
890
|
.or_else(|| object.get("reason").and_then(Value::as_str))
|
|
1468
891
|
.unwrap_or("doctor_failed");
|
|
1469
892
|
let issue = if status == "failed" {
|
|
1470
|
-
object
|
|
1471
|
-
.
|
|
1472
|
-
|
|
1473
|
-
|
|
893
|
+
object.get("gate").and_then(Value::as_str).map_or_else(
|
|
894
|
+
|| "doctor_failed".to_string(),
|
|
895
|
+
|gate| format!("{gate}_failed"),
|
|
896
|
+
)
|
|
1474
897
|
} else if status.ends_with("_failed") || status == "refused" {
|
|
1475
898
|
status.to_string()
|
|
1476
899
|
} else {
|
|
@@ -1481,7 +904,10 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
|
|
|
1481
904
|
.or_else(|| object.get("next_action"))
|
|
1482
905
|
.or_else(|| object.get("reason"))
|
|
1483
906
|
.cloned();
|
|
1484
|
-
add(
|
|
907
|
+
add(
|
|
908
|
+
Value::String(issue),
|
|
909
|
+
repair.map(|value| json!({"action": value})),
|
|
910
|
+
);
|
|
1485
911
|
}
|
|
1486
912
|
if object
|
|
1487
913
|
.get("workspace")
|
|
@@ -1512,9 +938,9 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
|
|
|
1512
938
|
{
|
|
1513
939
|
add(
|
|
1514
940
|
json!("grok_slot_mismatch"),
|
|
1515
|
-
grok.get("reason")
|
|
1516
|
-
|
|
1517
|
-
|
|
941
|
+
grok.get("reason")
|
|
942
|
+
.cloned()
|
|
943
|
+
.map(|reason| json!({"issue": "grok_slot_mismatch", "action": reason})),
|
|
1518
944
|
);
|
|
1519
945
|
}
|
|
1520
946
|
}
|
|
@@ -1559,7 +985,9 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
|
|
|
1559
985
|
if runtime_status == "unresolved" {
|
|
1560
986
|
add(
|
|
1561
987
|
json!("runtime_selection_failed"),
|
|
1562
|
-
Some(
|
|
988
|
+
Some(
|
|
989
|
+
json!({"issue": "runtime_selection_failed", "action": "select an existing Team runtime"}),
|
|
990
|
+
),
|
|
1563
991
|
);
|
|
1564
992
|
}
|
|
1565
993
|
if runtime_status == "present"
|
|
@@ -1589,7 +1017,7 @@ mod tests {
|
|
|
1589
1017
|
#![allow(clippy::unwrap_used)]
|
|
1590
1018
|
|
|
1591
1019
|
use super::{
|
|
1592
|
-
|
|
1020
|
+
append_send_guidance, format_inbox_human, quickstart_human, send_command,
|
|
1593
1021
|
split_shell_argv,
|
|
1594
1022
|
};
|
|
1595
1023
|
use serde_json::{json, Value};
|
|
@@ -1610,7 +1038,10 @@ mod tests {
|
|
|
1610
1038
|
let output = format_inbox_human("worker", &value);
|
|
1611
1039
|
for line in output.lines() {
|
|
1612
1040
|
assert!(line.len() <= 160, "line is {} bytes: {line:?}", line.len());
|
|
1613
|
-
assert!(
|
|
1041
|
+
assert!(
|
|
1042
|
+
!line.chars().any(char::is_control),
|
|
1043
|
+
"controls leaked: {line:?}"
|
|
1044
|
+
);
|
|
1614
1045
|
}
|
|
1615
1046
|
}
|
|
1616
1047
|
|
|
@@ -1625,9 +1056,12 @@ mod tests {
|
|
|
1625
1056
|
],
|
|
1626
1057
|
});
|
|
1627
1058
|
let out = quickstart_human(&value);
|
|
1628
|
-
assert!(out.contains("team started"), "must keep summary; got {out}");
|
|
1629
1059
|
assert!(
|
|
1630
|
-
out.contains("
|
|
1060
|
+
out.contains("队伍已启动"),
|
|
1061
|
+
"must show startup summary; got {out}"
|
|
1062
|
+
);
|
|
1063
|
+
assert!(
|
|
1064
|
+
out.contains("连接队伍:"),
|
|
1631
1065
|
"must render attach block; got {out}"
|
|
1632
1066
|
);
|
|
1633
1067
|
assert!(
|
|
@@ -1643,14 +1077,20 @@ mod tests {
|
|
|
1643
1077
|
#[test]
|
|
1644
1078
|
fn reminders_only_recommend_supported_result_commands() {
|
|
1645
1079
|
let reminder = crate::cli::QUICK_START_REMINDER;
|
|
1646
|
-
assert!(reminder.contains("
|
|
1080
|
+
assert!(reminder.contains("team-agent status"));
|
|
1081
|
+
assert!(reminder.contains("team-agent inbox"));
|
|
1647
1082
|
for text in [
|
|
1648
1083
|
reminder,
|
|
1649
1084
|
crate::cli::STATUS_REMINDER,
|
|
1650
1085
|
crate::cli::SEND_REMINDER,
|
|
1651
1086
|
&quickstart_human(&json!({"summary": "team started"})),
|
|
1652
1087
|
] {
|
|
1653
|
-
|
|
1088
|
+
for hidden in ["collect", "team-agent results", "team-agent wait"] {
|
|
1089
|
+
assert!(
|
|
1090
|
+
!text.contains(hidden),
|
|
1091
|
+
"private/removed CLI command leaked: {text}"
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1654
1094
|
}
|
|
1655
1095
|
}
|
|
1656
1096
|
|
|
@@ -1659,13 +1099,19 @@ mod tests {
|
|
|
1659
1099
|
let value = json!({"summary": "quick-start complete"});
|
|
1660
1100
|
assert_eq!(
|
|
1661
1101
|
quickstart_human(&value),
|
|
1662
|
-
format!(
|
|
1102
|
+
format!(
|
|
1103
|
+
"队伍已启动;下面是连接方式和派发任务的命令。\n{}",
|
|
1104
|
+
crate::cli::QUICK_START_REMINDER
|
|
1105
|
+
)
|
|
1663
1106
|
);
|
|
1664
|
-
//
|
|
1107
|
+
// 空数组也只显示人类摘要与下一步,不凭空编造连接方式。
|
|
1665
1108
|
let value2 = json!({"summary": "s", "attach_commands": []});
|
|
1666
1109
|
assert_eq!(
|
|
1667
1110
|
quickstart_human(&value2),
|
|
1668
|
-
format!(
|
|
1111
|
+
format!(
|
|
1112
|
+
"队伍已启动;下面是连接方式和派发任务的命令。\n{}",
|
|
1113
|
+
crate::cli::QUICK_START_REMINDER
|
|
1114
|
+
)
|
|
1669
1115
|
);
|
|
1670
1116
|
}
|
|
1671
1117
|
|
|
@@ -1684,7 +1130,7 @@ mod tests {
|
|
|
1684
1130
|
"team-agent",
|
|
1685
1131
|
"send",
|
|
1686
1132
|
"worker name; echo unsafe",
|
|
1687
|
-
"
|
|
1133
|
+
"请完成任务并把答案回复给 leader。",
|
|
1688
1134
|
"--workspace",
|
|
1689
1135
|
"/tmp/my workspace",
|
|
1690
1136
|
"--team",
|
|
@@ -1695,11 +1141,15 @@ mod tests {
|
|
|
1695
1141
|
|
|
1696
1142
|
#[test]
|
|
1697
1143
|
fn send_guidance_is_copyable_and_preserves_explicit_scope() {
|
|
1698
|
-
let command = send_command(
|
|
1699
|
-
|
|
1144
|
+
let command = send_command(
|
|
1145
|
+
"worker name; echo unsafe",
|
|
1146
|
+
Path::new("/tmp/my workspace"),
|
|
1147
|
+
Some("team-a"),
|
|
1148
|
+
)
|
|
1149
|
+
.unwrap();
|
|
1700
1150
|
assert_eq!(
|
|
1701
1151
|
command,
|
|
1702
|
-
"team-agent send 'worker name; echo unsafe'
|
|
1152
|
+
"team-agent send 'worker name; echo unsafe' '请完成任务并把答案回复给 leader。' --workspace '/tmp/my workspace' --team team-a"
|
|
1703
1153
|
);
|
|
1704
1154
|
}
|
|
1705
1155
|
|
|
@@ -1720,7 +1170,10 @@ mod tests {
|
|
|
1720
1170
|
fn quick_start_guidance_lists_every_known_agent_without_guessing() {
|
|
1721
1171
|
let mut value = json!({"ok": true, "agent_ids": ["sol", "luna"]});
|
|
1722
1172
|
append_send_guidance(&mut value, Path::new("/tmp/ws"), Some("team"));
|
|
1723
|
-
let commands = value
|
|
1173
|
+
let commands = value
|
|
1174
|
+
.get("send_commands")
|
|
1175
|
+
.and_then(|v| v.as_array())
|
|
1176
|
+
.unwrap();
|
|
1724
1177
|
assert_eq!(commands.len(), 2);
|
|
1725
1178
|
assert!(commands[0].as_str().unwrap().contains("send sol"));
|
|
1726
1179
|
assert!(commands[1].as_str().unwrap().contains("send luna"));
|
|
@@ -1747,53 +1200,19 @@ mod tests {
|
|
|
1747
1200
|
.unwrap();
|
|
1748
1201
|
assert!(command.contains("send worker"));
|
|
1749
1202
|
assert!(command.contains("--team team-a"));
|
|
1750
|
-
let parts: Vec<&str> = command.split_whitespace().collect();
|
|
1751
|
-
assert_eq!(parts[3], "MESSAGE");
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
#[test]
|
|
1755
|
-
fn agent_pane_id_resolves_session_window_even_with_recorded_pane_id() {
|
|
1756
|
-
let state = json!({
|
|
1757
|
-
"session_name": "team-x",
|
|
1758
|
-
"agents": {
|
|
1759
|
-
"w1": {"pane_id": "%7", "window": "w1"}
|
|
1760
|
-
}
|
|
1761
|
-
});
|
|
1762
|
-
let agent = state
|
|
1763
|
-
.get("agents")
|
|
1764
|
-
.and_then(|agents| agents.get("w1"))
|
|
1765
|
-
.unwrap();
|
|
1766
|
-
|
|
1767
1203
|
assert_eq!(
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
"team-
|
|
1771
|
-
"
|
|
1772
|
-
"
|
|
1773
|
-
|
|
1204
|
+
split_shell_argv(command),
|
|
1205
|
+
[
|
|
1206
|
+
"team-agent",
|
|
1207
|
+
"send",
|
|
1208
|
+
"worker",
|
|
1209
|
+
"请完成任务并把答案回复给 leader。",
|
|
1210
|
+
"--workspace",
|
|
1211
|
+
"/tmp/ws",
|
|
1212
|
+
"--team",
|
|
1213
|
+
"team-a",
|
|
1214
|
+
]
|
|
1774
1215
|
);
|
|
1775
1216
|
}
|
|
1776
1217
|
|
|
1777
|
-
#[test]
|
|
1778
|
-
fn agent_pane_id_falls_back_to_session_window_target() {
|
|
1779
|
-
let state = json!({
|
|
1780
|
-
"session_name": "team-x",
|
|
1781
|
-
"agents": {
|
|
1782
|
-
"w1": {"window": "worker-one"}
|
|
1783
|
-
}
|
|
1784
|
-
});
|
|
1785
|
-
let agent = state
|
|
1786
|
-
.get("agents")
|
|
1787
|
-
.and_then(|agents| agents.get("w1"))
|
|
1788
|
-
.unwrap();
|
|
1789
|
-
|
|
1790
|
-
assert_eq!(
|
|
1791
|
-
agent_pane_id(&state, "w1", agent).unwrap(),
|
|
1792
|
-
(
|
|
1793
|
-
"team-x".to_string(),
|
|
1794
|
-
"worker-one".to_string(),
|
|
1795
|
-
"team-x:worker-one".to_string()
|
|
1796
|
-
)
|
|
1797
|
-
);
|
|
1798
|
-
}
|
|
1799
1218
|
}
|