@team-agent/installer 0.5.111 → 0.5.113
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 +93 -779
- package/crates/team-agent/src/cli/diagnose.rs +2 -424
- package/crates/team-agent/src/cli/emit.rs +158 -373
- package/crates/team-agent/src/cli/leaders.rs +32 -32
- package/crates/team-agent/src/cli/mod.rs +8 -15
- package/crates/team-agent/src/cli/named_address.rs +17 -16
- package/crates/team-agent/src/cli/route.rs +4 -4
- package/crates/team-agent/src/cli/send/presentation.rs +19 -18
- package/crates/team-agent/src/cli/send/resolve.rs +11 -11
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +29 -32
- package/crates/team-agent/src/cli/status.rs +7 -7
- package/crates/team-agent/src/cli/tests/base.rs +6 -6
- package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
- 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 +3 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +7 -7
- package/crates/team-agent/src/cli/triage.rs +53 -2
- package/crates/team-agent/src/cli/types.rs +12 -116
- package/crates/team-agent/src/compiler/tests.rs +12 -0
- package/crates/team-agent/src/compiler.rs +0 -9
- 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/start.rs +5 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
- package/crates/team-agent/src/leader/tests/identity.rs +21 -32
- package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
- package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +325 -14
- package/crates/team-agent/src/lifecycle/launch.rs +2 -2
- package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- 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 -2
- 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/provider/adapter.rs +8 -59
- package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
- package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
- package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
- package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
- package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
- package/crates/team-agent/src/provider/command_helpers.rs +45 -0
- package/crates/team-agent/src/provider/mod.rs +1 -0
- package/crates/team-agent/src/provider/model_catalog.rs +21 -28
- package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
- package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
- 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> {
|
|
@@ -20,6 +19,10 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
|
|
|
20
19
|
return Ok(result);
|
|
21
20
|
}
|
|
22
21
|
append_send_guidance(&mut value, &args.workspace, args.team_id.as_deref());
|
|
22
|
+
finish_quick_start(value, args)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn finish_quick_start(mut value: Value, args: &QuickStartArgs) -> Result<CmdResult, CliError> {
|
|
23
26
|
let readiness = value.get("readiness").and_then(Value::as_object);
|
|
24
27
|
let all_resumable_have_session = readiness
|
|
25
28
|
.and_then(|readiness| readiness.get("all_resumable_have_session"))
|
|
@@ -37,7 +40,7 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
|
|
|
37
40
|
.get("status")
|
|
38
41
|
.and_then(Value::as_str)
|
|
39
42
|
.map(str::to_string);
|
|
40
|
-
if !args.detail {
|
|
43
|
+
if !args.detail && value.get("ok").and_then(Value::as_bool) != Some(false) {
|
|
41
44
|
lifecycle_port::compact_quick_start_value(&mut value);
|
|
42
45
|
}
|
|
43
46
|
if args.json
|
|
@@ -46,30 +49,18 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
|
|
|
46
49
|
|| !readiness_ready
|
|
47
50
|
{
|
|
48
51
|
let mut result = CmdResult::from_json(value, args.json);
|
|
49
|
-
if
|
|
52
|
+
if status.as_deref() == Some("pending_tool_load") {
|
|
50
53
|
result.exit = ExitCode::Ok;
|
|
51
54
|
}
|
|
52
55
|
if !args.json {
|
|
53
56
|
if let CmdOutput::Json(value) = &result.output {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
62
|
-
} else {
|
|
63
|
-
"队伍尚未准备好接收任务;请等待工具完成启动。".to_string()
|
|
64
|
-
};
|
|
65
|
-
let mut doctor = format!(
|
|
66
|
-
"team-agent doctor --workspace {}",
|
|
67
|
-
shell_quote(&args.workspace.to_string_lossy())
|
|
68
|
-
);
|
|
69
|
-
if let Some(team) = args.team_id.as_deref() {
|
|
70
|
-
doctor.push_str(&format!(" --team {}", shell_quote(team)));
|
|
71
|
-
}
|
|
72
|
-
result.output = CmdOutput::Human(format!("{explanation}\n下一步:运行 {doctor},按体检提示处理;用 status 查看状态,不反复起队或重发任务。"));
|
|
57
|
+
// Typed refusals often have no `error`. Preserve the complete
|
|
58
|
+
// report instead of replacing its reason/actions with a guess.
|
|
59
|
+
let safe = crate::redaction::redact_external_value(value);
|
|
60
|
+
result.output = CmdOutput::Human(format!(
|
|
61
|
+
"quick-start report:\n{}",
|
|
62
|
+
serde_json::to_string_pretty(&safe)?
|
|
63
|
+
));
|
|
73
64
|
}
|
|
74
65
|
}
|
|
75
66
|
Ok(result)
|
|
@@ -127,11 +118,11 @@ fn quick_start_config_guidance(value: &mut Value, args: &QuickStartArgs) -> Opti
|
|
|
127
118
|
retry.push_str(" --json");
|
|
128
119
|
}
|
|
129
120
|
let explanation = if missing_team {
|
|
130
|
-
format!("
|
|
121
|
+
format!("Team configuration is missing: {}. No workers were started. Create the two files below; this command did not write them. Error: {error}", team_path.display())
|
|
131
122
|
} else {
|
|
132
|
-
format!("
|
|
123
|
+
format!("Worker configuration is incomplete: {detail}. Add the role directory or required fields. The examples below do not overwrite existing files. Error: {error}")
|
|
133
124
|
};
|
|
134
|
-
let next = format!("
|
|
125
|
+
let next = format!("Check that Pi is installed and signed in; verify the model with team-agent models --provider pi.\nRun team-agent pi, then execute this command in that leader's tool context:\n{retry}");
|
|
135
126
|
let mut templates =
|
|
136
127
|
vec![serde_json::json!({"path": role_path, "content": super::emit::WORKER_TEMPLATE})];
|
|
137
128
|
let mut human = explanation.clone();
|
|
@@ -141,13 +132,13 @@ fn quick_start_config_guidance(value: &mut Value, args: &QuickStartArgs) -> Opti
|
|
|
141
132
|
serde_json::json!({"path": team_path, "content": super::emit::TEAM_TEMPLATE}),
|
|
142
133
|
);
|
|
143
134
|
human.push_str(&format!(
|
|
144
|
-
"\n\n{}
|
|
135
|
+
"\n\n{}:\n{}",
|
|
145
136
|
team_path.display(),
|
|
146
137
|
super::emit::TEAM_TEMPLATE
|
|
147
138
|
));
|
|
148
139
|
}
|
|
149
140
|
human.push_str(&format!(
|
|
150
|
-
"\n\n{}
|
|
141
|
+
"\n\n{}:\n{}\nNext steps:\n{next}",
|
|
151
142
|
role_path.display(),
|
|
152
143
|
super::emit::WORKER_TEMPLATE
|
|
153
144
|
));
|
|
@@ -167,11 +158,7 @@ fn quick_start_config_guidance(value: &mut Value, args: &QuickStartArgs) -> Opti
|
|
|
167
158
|
/// E13:quick-start "team 起了" 人类输出 = summary + attach 块。所有成功出口共用(别每分支手拷)。
|
|
168
159
|
/// attach_commands 缺/空 → 只 summary(向后兼容)。
|
|
169
160
|
fn quickstart_human(value: &Value) -> String {
|
|
170
|
-
let summary =
|
|
171
|
-
"发现已有队伍;请查看返回的连接方式,不会另建同名队伍。"
|
|
172
|
-
} else {
|
|
173
|
-
"队伍已启动;下面是连接方式和派发任务的命令。"
|
|
174
|
-
};
|
|
161
|
+
let summary = value.get("summary").and_then(Value::as_str).unwrap_or("Team started.");
|
|
175
162
|
let attach: Vec<&str> = value
|
|
176
163
|
.get("attach_commands")
|
|
177
164
|
.and_then(Value::as_array)
|
|
@@ -184,14 +171,14 @@ fn quickstart_human(value: &Value) -> String {
|
|
|
184
171
|
.unwrap_or_default();
|
|
185
172
|
let mut out = String::from(summary);
|
|
186
173
|
if !attach.is_empty() {
|
|
187
|
-
out.push_str("\n\
|
|
174
|
+
out.push_str("\n\nConnect to the team:");
|
|
188
175
|
for cmd in attach {
|
|
189
176
|
out.push_str("\n ");
|
|
190
177
|
out.push_str(cmd);
|
|
191
178
|
}
|
|
192
179
|
}
|
|
193
180
|
if !sends.is_empty() {
|
|
194
|
-
out.push_str("\n\
|
|
181
|
+
out.push_str("\n\nSend a task (replace the example message):");
|
|
195
182
|
for cmd in sends {
|
|
196
183
|
out.push_str("\n ");
|
|
197
184
|
out.push_str(cmd);
|
|
@@ -258,7 +245,7 @@ pub(crate) fn send_command(agent: &str, workspace: &Path, team: Option<&str>) ->
|
|
|
258
245
|
let mut command = format!(
|
|
259
246
|
"team-agent send {} {} --workspace {}",
|
|
260
247
|
shell_quote(agent),
|
|
261
|
-
shell_quote("
|
|
248
|
+
shell_quote("Complete the task and reply to the leader."),
|
|
262
249
|
shell_quote(workspace)
|
|
263
250
|
);
|
|
264
251
|
if let Some(team) = team {
|
|
@@ -350,699 +337,11 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
|
|
|
350
337
|
)))
|
|
351
338
|
}
|
|
352
339
|
|
|
353
|
-
/// `cmd_watch`(`commands.py:103`)。委派 `coordinator::run_watch`;KeyboardInterrupt/正常 → `SystemExit(0)`。
|
|
354
|
-
/// 返回 [`CmdResult::none`](不经 emit)。
|
|
355
|
-
pub fn cmd_watch(args: &WatchArgs) -> Result<CmdResult, CliError> {
|
|
356
|
-
let workspace = crate::coordinator::WorkspacePath::new(args.workspace.clone());
|
|
357
|
-
#[cfg(not(test))]
|
|
358
|
-
{
|
|
359
|
-
let mut output = |line: &str| {
|
|
360
|
-
println!("{line}");
|
|
361
|
-
let _ = std::io::Write::flush(&mut std::io::stdout());
|
|
362
|
-
};
|
|
363
|
-
crate::coordinator::run_watch(&workspace, args.team.as_deref(), 1.0, &mut output)
|
|
364
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
365
|
-
Ok(CmdResult::none())
|
|
366
|
-
}
|
|
367
|
-
#[cfg(test)]
|
|
368
|
-
{
|
|
369
|
-
let store = crate::message_store::MessageStore::open(&args.workspace)
|
|
370
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
371
|
-
let mut cursor = crate::coordinator::WatchCursor::default();
|
|
372
|
-
let lines = crate::coordinator::collect_watch_lines(
|
|
373
|
-
&workspace,
|
|
374
|
-
&mut cursor,
|
|
375
|
-
&store,
|
|
376
|
-
args.team.as_deref(),
|
|
377
|
-
)
|
|
378
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
379
|
-
Ok(CmdResult::human(lines.join("\n")))
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/// `cmd_sessions`(`parser.py:230`)。
|
|
384
|
-
pub fn cmd_sessions(args: &SessionsArgs) -> Result<CmdResult, CliError> {
|
|
385
|
-
let selected = crate::state::selector::resolve_active_team(
|
|
386
|
-
&args.workspace,
|
|
387
|
-
args.team.as_deref(),
|
|
388
|
-
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
389
|
-
)
|
|
390
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
391
|
-
let spec = match selected.spec_path.as_ref() {
|
|
392
|
-
Some(path) => load_team_spec_at(path)?,
|
|
393
|
-
None => load_team_spec_optional(&selected.run_workspace, &selected.state)?,
|
|
394
|
-
};
|
|
395
|
-
Ok(CmdResult::from_json(
|
|
396
|
-
json!({
|
|
397
|
-
"ok": true,
|
|
398
|
-
"sessions": sessions_overview(&selected.state, spec.as_ref()),
|
|
399
|
-
"workspace": args.workspace.to_string_lossy().to_string(),
|
|
400
|
-
}),
|
|
401
|
-
args.json,
|
|
402
|
-
))
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
pub fn cmd_results(args: &ResultsArgs) -> Result<CmdResult, CliError> {
|
|
406
|
-
let selected = match crate::state::selector::resolve_active_team(
|
|
407
|
-
&args.workspace,
|
|
408
|
-
args.team.as_deref(),
|
|
409
|
-
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
410
|
-
) {
|
|
411
|
-
Ok(selected) => selected,
|
|
412
|
-
Err(error) => {
|
|
413
|
-
return Ok(CmdResult::from_json(
|
|
414
|
-
json!({
|
|
415
|
-
"ok": false,
|
|
416
|
-
"error": error.to_string(),
|
|
417
|
-
"workspace": args.workspace.to_string_lossy().to_string(),
|
|
418
|
-
}),
|
|
419
|
-
args.json,
|
|
420
|
-
));
|
|
421
|
-
}
|
|
422
|
-
};
|
|
423
|
-
let results = messaging::results::results_for_case(
|
|
424
|
-
&selected.run_workspace,
|
|
425
|
-
&args.case_id,
|
|
426
|
-
Some(selected.team_key.as_str()),
|
|
427
|
-
args.team.as_deref(),
|
|
428
|
-
)?;
|
|
429
|
-
let value = json!({
|
|
430
|
-
"ok": true,
|
|
431
|
-
"case_id": args.case_id,
|
|
432
|
-
"results": results,
|
|
433
|
-
"workspace": selected.run_workspace.to_string_lossy().to_string(),
|
|
434
|
-
});
|
|
435
|
-
Ok(if args.json {
|
|
436
|
-
CmdResult::from_ordered_json(value)
|
|
437
|
-
} else {
|
|
438
|
-
CmdResult::from_json(value, false)
|
|
439
|
-
})
|
|
440
|
-
}
|
|
441
|
-
|
|
442
340
|
pub fn cmd_wait(args: &WaitArgs) -> Result<CmdResult, CliError> {
|
|
443
341
|
let result = messaging::wait_for_result(&args.workspace, &args.task_id)?;
|
|
444
342
|
Ok(CmdResult::from_json(result.to_json(), args.json))
|
|
445
343
|
}
|
|
446
344
|
|
|
447
|
-
/// `cmd_allow_peer_talk`(`parser.py allow-peer-talk`).
|
|
448
|
-
pub fn cmd_allow_peer_talk(args: &AllowPeerTalkArgs) -> Result<CmdResult, CliError> {
|
|
449
|
-
if args.team.is_some() {
|
|
450
|
-
return Err(CliError::Usage(
|
|
451
|
-
"allow-peer-talk --team is not supported yet because peer allowlist storage is not team-scoped".to_string(),
|
|
452
|
-
));
|
|
453
|
-
}
|
|
454
|
-
let value = messaging::allow_peer_talk(&args.workspace, &args.a, &args.b)?;
|
|
455
|
-
Ok(CmdResult::from_json(value, args.json))
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/// `cmd_preflight`(`parser.py:160`)。
|
|
459
|
-
pub fn cmd_preflight(args: &PreflightArgs) -> Result<CmdResult, CliError> {
|
|
460
|
-
let report = build_preflight_report(&args.team)?;
|
|
461
|
-
Ok(CmdResult::from_json(report, args.json))
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/// `cmd_wait_ready`(`parser.py:171`)。
|
|
465
|
-
pub fn cmd_wait_ready(args: &WaitReadyArgs) -> Result<CmdResult, CliError> {
|
|
466
|
-
let report = build_wait_ready_report(&args.workspace, args.timeout, args.team.as_deref())?;
|
|
467
|
-
Ok(CmdResult::from_json(report, args.json))
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/// `cmd_e2e`(`parser.py:449`)。
|
|
471
|
-
pub fn cmd_e2e(args: &E2eArgs) -> Result<CmdResult, CliError> {
|
|
472
|
-
let mut providers = serde_json::Map::new();
|
|
473
|
-
for provider in &args.providers {
|
|
474
|
-
let result = if provider == "fake" {
|
|
475
|
-
run_fake_e2e(&args.workspace)?
|
|
476
|
-
} else {
|
|
477
|
-
skipped_provider_e2e(provider, args.real)
|
|
478
|
-
};
|
|
479
|
-
providers.insert(provider.clone(), result);
|
|
480
|
-
}
|
|
481
|
-
let ok = providers
|
|
482
|
-
.values()
|
|
483
|
-
.all(|value| value.get("ok").and_then(Value::as_bool) == Some(true));
|
|
484
|
-
Ok(CmdResult::from_json(
|
|
485
|
-
json!({
|
|
486
|
-
"workspace": args.workspace.to_string_lossy().to_string(),
|
|
487
|
-
"providers": Value::Object(providers),
|
|
488
|
-
"ok": ok,
|
|
489
|
-
}),
|
|
490
|
-
args.json,
|
|
491
|
-
))
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/// `cmd_peek`(`commands.py:118`)。
|
|
495
|
-
pub fn cmd_peek(args: &PeekArgs) -> Result<CmdResult, CliError> {
|
|
496
|
-
if !args.allow_raw_screen {
|
|
497
|
-
return Err(CliError::Usage(
|
|
498
|
-
"peek requires --allow-raw-screen".to_string(),
|
|
499
|
-
));
|
|
500
|
-
}
|
|
501
|
-
let state = crate::state::persist::load_runtime_state(&args.workspace)?;
|
|
502
|
-
let Some(agent_state) = state
|
|
503
|
-
.get("agents")
|
|
504
|
-
.and_then(|agents| agents.get(&args.agent))
|
|
505
|
-
else {
|
|
506
|
-
return Ok(CmdResult::from_json(
|
|
507
|
-
json!({
|
|
508
|
-
"ok": false,
|
|
509
|
-
"agent_id": args.agent,
|
|
510
|
-
"error": format!("unknown agent id: {}", args.agent),
|
|
511
|
-
}),
|
|
512
|
-
args.json,
|
|
513
|
-
));
|
|
514
|
-
};
|
|
515
|
-
let Some((session, window, target)) = agent_pane_id(&state, &args.agent, agent_state) else {
|
|
516
|
-
return Ok(peek_unavailable(&args.agent, args.json));
|
|
517
|
-
};
|
|
518
|
-
// 0.5.x Phase 1d Batch 3: factory-routed peek backend so conpty
|
|
519
|
-
// teams peek from the shim scrollback rather than a workspace
|
|
520
|
-
// tmux capture that would always be empty. Tmux teams keep
|
|
521
|
-
// byte-equivalent behavior.
|
|
522
|
-
let backend: Box<dyn crate::transport::Transport> =
|
|
523
|
-
match crate::transport_factory::resolve_read_only_transport(
|
|
524
|
-
&args.workspace,
|
|
525
|
-
Some(&state),
|
|
526
|
-
crate::transport_factory::TransportPurpose::Status,
|
|
527
|
-
) {
|
|
528
|
-
Ok(r) => r.backend,
|
|
529
|
-
Err(_) => Box::new(crate::tmux_backend::TmuxBackend::for_workspace(
|
|
530
|
-
&args.workspace,
|
|
531
|
-
)),
|
|
532
|
-
};
|
|
533
|
-
let windows = backend
|
|
534
|
-
.list_windows(&crate::transport::SessionName::new(session.clone()))
|
|
535
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
536
|
-
if !windows.iter().any(|w| w.as_str() == window) {
|
|
537
|
-
return Ok(peek_unavailable(&args.agent, args.json));
|
|
538
|
-
}
|
|
539
|
-
let range = args
|
|
540
|
-
.head
|
|
541
|
-
.map(|head| crate::transport::CaptureRange::Head(head as u32))
|
|
542
|
-
.unwrap_or_else(|| crate::transport::CaptureRange::Tail(args.tail as u32));
|
|
543
|
-
let capture = backend
|
|
544
|
-
.capture(
|
|
545
|
-
&crate::transport::Target::Pane(crate::transport::PaneId::new(target.clone())),
|
|
546
|
-
range,
|
|
547
|
-
)
|
|
548
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
549
|
-
let matches = args.search.as_ref().map(|needle| {
|
|
550
|
-
capture
|
|
551
|
-
.text
|
|
552
|
-
.lines()
|
|
553
|
-
.filter(|line| line.contains(needle))
|
|
554
|
-
.map(str::to_string)
|
|
555
|
-
.collect::<Vec<_>>()
|
|
556
|
-
});
|
|
557
|
-
Ok(CmdResult::from_json(
|
|
558
|
-
json!({
|
|
559
|
-
"ok": true,
|
|
560
|
-
"agent_id": args.agent,
|
|
561
|
-
"workspace": args.workspace.to_string_lossy().to_string(),
|
|
562
|
-
"tail": args.tail,
|
|
563
|
-
"head": args.head,
|
|
564
|
-
"search": args.search,
|
|
565
|
-
"matches": matches,
|
|
566
|
-
"pane_id": target,
|
|
567
|
-
"text": capture.text,
|
|
568
|
-
}),
|
|
569
|
-
args.json,
|
|
570
|
-
))
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
fn peek_unavailable(agent: &str, json: bool) -> CmdResult {
|
|
574
|
-
CmdResult::from_json(
|
|
575
|
-
json!({
|
|
576
|
-
"ok": false,
|
|
577
|
-
"agent_id": agent,
|
|
578
|
-
"error": format!("agent terminal is not available: {agent}"),
|
|
579
|
-
}),
|
|
580
|
-
json,
|
|
581
|
-
)
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
fn agent_pane_id(
|
|
585
|
-
state: &Value,
|
|
586
|
-
agent: &str,
|
|
587
|
-
agent_state: &Value,
|
|
588
|
-
) -> Option<(String, String, String)> {
|
|
589
|
-
let session = state
|
|
590
|
-
.get("session_name")
|
|
591
|
-
.and_then(Value::as_str)
|
|
592
|
-
.filter(|s| !s.is_empty())?;
|
|
593
|
-
let window = ["window", "window_name"]
|
|
594
|
-
.iter()
|
|
595
|
-
.find_map(|key| {
|
|
596
|
-
agent_state
|
|
597
|
-
.get(*key)
|
|
598
|
-
.and_then(Value::as_str)
|
|
599
|
-
.filter(|s| !s.is_empty())
|
|
600
|
-
})
|
|
601
|
-
.unwrap_or(agent);
|
|
602
|
-
Some((
|
|
603
|
-
session.to_string(),
|
|
604
|
-
window.to_string(),
|
|
605
|
-
format!("{session}:{window}"),
|
|
606
|
-
))
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
fn run_fake_e2e(workspace: &Path) -> Result<Value, CliError> {
|
|
610
|
-
std::fs::create_dir_all(workspace)?;
|
|
611
|
-
let spec_path = workspace.join("team.spec.yaml");
|
|
612
|
-
std::fs::write(&spec_path, fake_spec_yaml(workspace))?;
|
|
613
|
-
let launch = match crate::lifecycle::launch(&spec_path, true, true, true) {
|
|
614
|
-
Ok(report) => json!({
|
|
615
|
-
"ok": true,
|
|
616
|
-
"dry_run": report.dry_run,
|
|
617
|
-
"session_name": report.session_name.as_str(),
|
|
618
|
-
}),
|
|
619
|
-
Err(error) => json!({
|
|
620
|
-
"ok": false,
|
|
621
|
-
"error": error.to_string(),
|
|
622
|
-
}),
|
|
623
|
-
};
|
|
624
|
-
seed_fake_e2e_state(workspace)?;
|
|
625
|
-
let send = messaging::send_message(
|
|
626
|
-
workspace,
|
|
627
|
-
&MessageTarget::Single("fake_impl".to_string()),
|
|
628
|
-
"implement fake task",
|
|
629
|
-
&SendOptions {
|
|
630
|
-
task_id: Some(TaskId::new("task_impl")),
|
|
631
|
-
route_task_id: false,
|
|
632
|
-
sender: messaging::TrustedSender::leader(),
|
|
633
|
-
requires_ack: true,
|
|
634
|
-
..SendOptions::default()
|
|
635
|
-
},
|
|
636
|
-
)?;
|
|
637
|
-
let send_value = json!({
|
|
638
|
-
"ok": send.ok,
|
|
639
|
-
"status": send.status,
|
|
640
|
-
"message_status": send.message_status.0,
|
|
641
|
-
"message_id": send.message_id,
|
|
642
|
-
"reason": send.reason,
|
|
643
|
-
});
|
|
644
|
-
if send.ok {
|
|
645
|
-
if let Some(message_id) = send.message_id.as_deref() {
|
|
646
|
-
crate::message_store::MessageStore::open(workspace)
|
|
647
|
-
.map_err(crate::messaging::MessagingError::from)?
|
|
648
|
-
.mark(message_id, "delivered", None)
|
|
649
|
-
.map_err(crate::messaging::MessagingError::from)?;
|
|
650
|
-
}
|
|
651
|
-
let report = messaging::report_result(
|
|
652
|
-
workspace,
|
|
653
|
-
&json!({
|
|
654
|
-
"schema_version": "result_envelope_v1",
|
|
655
|
-
"task_id": "task_impl",
|
|
656
|
-
"agent_id": "fake_impl",
|
|
657
|
-
"status": "success",
|
|
658
|
-
"summary": "fake result finalized",
|
|
659
|
-
"changes": [],
|
|
660
|
-
"tests": [],
|
|
661
|
-
"risks": [],
|
|
662
|
-
"artifacts": [],
|
|
663
|
-
"next_actions": [],
|
|
664
|
-
}),
|
|
665
|
-
)?;
|
|
666
|
-
let auto_finalized =
|
|
667
|
-
report.get("finalization_status").and_then(Value::as_str) == Some("auto_finalized");
|
|
668
|
-
let shutdown = fake_shutdown(workspace)?;
|
|
669
|
-
let ok = launch.get("ok").and_then(Value::as_bool) == Some(true)
|
|
670
|
-
&& send.ok
|
|
671
|
-
&& auto_finalized
|
|
672
|
-
&& shutdown.get("ok").and_then(Value::as_bool) == Some(true);
|
|
673
|
-
return Ok(json!({
|
|
674
|
-
"ok": ok,
|
|
675
|
-
"launch": launch,
|
|
676
|
-
"send": send_value,
|
|
677
|
-
"report": report,
|
|
678
|
-
"shutdown": shutdown,
|
|
679
|
-
}));
|
|
680
|
-
}
|
|
681
|
-
let shutdown = fake_shutdown(workspace)?;
|
|
682
|
-
Ok(json!({
|
|
683
|
-
"ok": false,
|
|
684
|
-
"launch": launch,
|
|
685
|
-
"send": send_value,
|
|
686
|
-
"report": null,
|
|
687
|
-
"shutdown": shutdown,
|
|
688
|
-
}))
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
fn skipped_provider_e2e(provider: &str, real: bool) -> Value {
|
|
692
|
-
let command = provider_command(provider);
|
|
693
|
-
if !command_on_path(command) {
|
|
694
|
-
return json!({
|
|
695
|
-
"ok": false,
|
|
696
|
-
"skipped": true,
|
|
697
|
-
"reason": format!("{command} not installed"),
|
|
698
|
-
"version": null,
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
let version = command_version(command);
|
|
702
|
-
if !real {
|
|
703
|
-
return json!({
|
|
704
|
-
"ok": false,
|
|
705
|
-
"skipped": true,
|
|
706
|
-
"reason": "real provider launch disabled; rerun with --real on an authenticated machine",
|
|
707
|
-
"version": version,
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
json!({
|
|
711
|
-
"ok": false,
|
|
712
|
-
"skipped": true,
|
|
713
|
-
"reason": "real provider e2e is not available in this build",
|
|
714
|
-
"version": version,
|
|
715
|
-
})
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
fn command_on_path(command: &str) -> bool {
|
|
719
|
-
std::env::var_os("PATH").is_some_and(|paths| {
|
|
720
|
-
std::env::split_paths(&paths).any(|dir| {
|
|
721
|
-
let candidate = dir.join(command);
|
|
722
|
-
candidate.is_file()
|
|
723
|
-
})
|
|
724
|
-
})
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
fn command_version(command: &str) -> Value {
|
|
728
|
-
match std::process::Command::new(command)
|
|
729
|
-
.arg("--version")
|
|
730
|
-
.output()
|
|
731
|
-
{
|
|
732
|
-
Ok(output) if output.status.success() => {
|
|
733
|
-
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
734
|
-
if text.is_empty() {
|
|
735
|
-
Value::Null
|
|
736
|
-
} else {
|
|
737
|
-
Value::String(text)
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
_ => Value::Null,
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
fn provider_command(provider: &str) -> &str {
|
|
745
|
-
// E2E provider command grammar, not persisted provider identity parsing.
|
|
746
|
-
match provider {
|
|
747
|
-
"claude" | "claude_code" => "claude",
|
|
748
|
-
"gemini" | "gemini_cli" => "gemini",
|
|
749
|
-
other => other,
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
fn seed_fake_e2e_state(workspace: &Path) -> Result<(), CliError> {
|
|
754
|
-
crate::state::repository::StateRepository::new(workspace).save(
|
|
755
|
-
crate::state::repository::StateWriteIntent::FakeE2eSeed,
|
|
756
|
-
&json!({
|
|
757
|
-
"leader": {"id": "leader"},
|
|
758
|
-
"session_name": "team-agent-fake-e2e",
|
|
759
|
-
"agents": {
|
|
760
|
-
"fake_impl": {
|
|
761
|
-
"provider": "fake",
|
|
762
|
-
"status": "running",
|
|
763
|
-
"window": "fake_impl",
|
|
764
|
-
"pane_id": "%fake_impl",
|
|
765
|
-
"spawn_cwd": workspace.to_string_lossy().to_string(),
|
|
766
|
-
}
|
|
767
|
-
},
|
|
768
|
-
"tasks": [{
|
|
769
|
-
"id": "task_impl",
|
|
770
|
-
"title": "Fake implementation",
|
|
771
|
-
"type": "implementation",
|
|
772
|
-
"assignee": "fake_impl",
|
|
773
|
-
"status": "pending",
|
|
774
|
-
}]
|
|
775
|
-
}),
|
|
776
|
-
)?;
|
|
777
|
-
Ok(())
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
fn fake_shutdown(workspace: &Path) -> Result<Value, CliError> {
|
|
781
|
-
let mut state = crate::state::persist::load_runtime_state(workspace)?;
|
|
782
|
-
if let Some(agents) = state.get_mut("agents").and_then(Value::as_object_mut) {
|
|
783
|
-
for agent in agents.values_mut() {
|
|
784
|
-
if let Some(obj) = agent.as_object_mut() {
|
|
785
|
-
obj.insert("status".to_string(), Value::String("stopped".to_string()));
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
crate::state::repository::StateRepository::new(workspace).save(
|
|
790
|
-
crate::state::repository::StateWriteIntent::FakeE2eSeed,
|
|
791
|
-
&state,
|
|
792
|
-
)?;
|
|
793
|
-
Ok(json!({
|
|
794
|
-
"ok": true,
|
|
795
|
-
"session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
|
|
796
|
-
"session_killed": false,
|
|
797
|
-
"coordinator": {"status": "missing", "pid": null},
|
|
798
|
-
}))
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
fn fake_spec_yaml(workspace: &Path) -> String {
|
|
802
|
-
let ws = workspace.to_string_lossy();
|
|
803
|
-
format!(
|
|
804
|
-
r#"version: 1
|
|
805
|
-
team:
|
|
806
|
-
name: "fake-e2e"
|
|
807
|
-
mode: "supervisor_worker"
|
|
808
|
-
objective: "Exercise fake provider orchestration."
|
|
809
|
-
workspace: "{ws}"
|
|
810
|
-
leader:
|
|
811
|
-
id: "leader"
|
|
812
|
-
role: "leader"
|
|
813
|
-
provider: "fake"
|
|
814
|
-
model: null
|
|
815
|
-
tools:
|
|
816
|
-
- "fs_read"
|
|
817
|
-
- "fs_list"
|
|
818
|
-
- "mcp_team"
|
|
819
|
-
context_policy:
|
|
820
|
-
keep_user_thread: true
|
|
821
|
-
receive_worker_outputs: "structured_only"
|
|
822
|
-
max_worker_result_tokens: 2000
|
|
823
|
-
agents:
|
|
824
|
-
- id: "fake_impl"
|
|
825
|
-
role: "implementation_engineer"
|
|
826
|
-
provider: "fake"
|
|
827
|
-
model: null
|
|
828
|
-
working_directory: "{ws}"
|
|
829
|
-
system_prompt:
|
|
830
|
-
inline: "Handle fake implementation tasks."
|
|
831
|
-
file: null
|
|
832
|
-
tools:
|
|
833
|
-
- "fs_read"
|
|
834
|
-
- "fs_write"
|
|
835
|
-
- "fs_list"
|
|
836
|
-
- "execute_bash"
|
|
837
|
-
- "git_diff"
|
|
838
|
-
- "mcp_team"
|
|
839
|
-
- "provider_builtin"
|
|
840
|
-
permission_mode: "restricted"
|
|
841
|
-
preferred_for:
|
|
842
|
-
- "implementation"
|
|
843
|
-
avoid_for: []
|
|
844
|
-
output_contract:
|
|
845
|
-
format: "result_envelope_v1"
|
|
846
|
-
required_fields:
|
|
847
|
-
- "task_id"
|
|
848
|
-
- "status"
|
|
849
|
-
- "summary"
|
|
850
|
-
- "artifacts"
|
|
851
|
-
routing:
|
|
852
|
-
default_assignee: "leader"
|
|
853
|
-
rules:
|
|
854
|
-
- id: "implementation-to-fake"
|
|
855
|
-
match:
|
|
856
|
-
type:
|
|
857
|
-
- "implementation"
|
|
858
|
-
assign_to: "fake_impl"
|
|
859
|
-
priority: 10
|
|
860
|
-
communication:
|
|
861
|
-
protocol: "mcp_inbox"
|
|
862
|
-
topology: "leader_centered"
|
|
863
|
-
worker_to_worker: true
|
|
864
|
-
ack_timeout_sec: 2
|
|
865
|
-
result_format: "result_envelope_v1"
|
|
866
|
-
message_store:
|
|
867
|
-
sqlite: ".team/runtime/team.db"
|
|
868
|
-
mirror_files: ".team/messages"
|
|
869
|
-
runtime:
|
|
870
|
-
backend: "tmux"
|
|
871
|
-
session_name: "team-agent-fake-e2e"
|
|
872
|
-
auto_launch: true
|
|
873
|
-
require_user_approval_before_launch: false
|
|
874
|
-
max_active_agents: 1
|
|
875
|
-
startup_order:
|
|
876
|
-
- "fake_impl"
|
|
877
|
-
context:
|
|
878
|
-
state_file: "team_state.md"
|
|
879
|
-
artifact_dir: ".team/artifacts"
|
|
880
|
-
log_dir: ".team/logs"
|
|
881
|
-
summarization:
|
|
882
|
-
worker_full_logs: "retain_outside_leader_context"
|
|
883
|
-
state_update: "after_each_result"
|
|
884
|
-
tasks:
|
|
885
|
-
- id: "task_impl"
|
|
886
|
-
title: "Fake implementation"
|
|
887
|
-
type: "implementation"
|
|
888
|
-
assignee: "fake_impl"
|
|
889
|
-
deps: []
|
|
890
|
-
acceptance:
|
|
891
|
-
- "fake result collected"
|
|
892
|
-
status: "pending"
|
|
893
|
-
requires_tools:
|
|
894
|
-
- "fs_write"
|
|
895
|
-
- "execute_bash"
|
|
896
|
-
files:
|
|
897
|
-
- "src/example.py"
|
|
898
|
-
risk: "low"
|
|
899
|
-
"#
|
|
900
|
-
)
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
fn sessions_overview(state: &Value, spec: Option<&crate::model::yaml::Value>) -> Value {
|
|
904
|
-
let mut rows = Vec::new();
|
|
905
|
-
if let Some(agents) = spec
|
|
906
|
-
.and_then(|v| v.get("agents"))
|
|
907
|
-
.and_then(crate::model::yaml::Value::as_list)
|
|
908
|
-
{
|
|
909
|
-
for agent in agents {
|
|
910
|
-
let Some(agent_id) = agent.get("id").and_then(crate::model::yaml::Value::as_str) else {
|
|
911
|
-
continue;
|
|
912
|
-
};
|
|
913
|
-
let agent_state = state
|
|
914
|
-
.get("agents")
|
|
915
|
-
.and_then(|agents| agents.get(agent_id))
|
|
916
|
-
.unwrap_or(&Value::Null);
|
|
917
|
-
rows.push(session_row(state, agent, agent_id, agent_state));
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
Value::Array(rows)
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
fn session_row(
|
|
924
|
-
state: &Value,
|
|
925
|
-
spec_agent: &crate::model::yaml::Value,
|
|
926
|
-
agent_id: &str,
|
|
927
|
-
agent_state: &Value,
|
|
928
|
-
) -> Value {
|
|
929
|
-
json!({
|
|
930
|
-
"agent_id": agent_id,
|
|
931
|
-
"provider": yaml_str(spec_agent, "provider").map(Value::String).unwrap_or(Value::Null),
|
|
932
|
-
"model": yaml_str(spec_agent, "model").map(Value::String).unwrap_or(Value::Null),
|
|
933
|
-
"profile": yaml_str(spec_agent, "profile").map(Value::String).unwrap_or(Value::Null),
|
|
934
|
-
"session_id": field_or_null(agent_state, &["session_id"]),
|
|
935
|
-
"resume_id": field_or_null(agent_state, &["resume_id"]),
|
|
936
|
-
"rollout_path": field_or_null(agent_state, &["rollout_path"]),
|
|
937
|
-
"captured_at": field_or_null(agent_state, &["captured_at"]),
|
|
938
|
-
"captured_via": field_or_null(agent_state, &["captured_via"]),
|
|
939
|
-
"attribution_confidence": field_or_null(agent_state, &["attribution_confidence"]),
|
|
940
|
-
"spawn_cwd": field_or_null(agent_state, &["spawn_cwd", "working_directory"]),
|
|
941
|
-
"context_usage": field_or_null(agent_state, &["context_usage"]),
|
|
942
|
-
"status": field_or_default(agent_state, "status", "unknown"),
|
|
943
|
-
"last_task": last_task_for_agent(state, agent_id),
|
|
944
|
-
"handoff_path": field_or_null(agent_state, &["handoff_path"]),
|
|
945
|
-
"display_target": field_or_null(agent_state, &["display"]),
|
|
946
|
-
"terminal_target": terminal_target(state, agent_id, agent_state),
|
|
947
|
-
})
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
fn yaml_str(value: &crate::model::yaml::Value, key: &str) -> Option<String> {
|
|
951
|
-
value
|
|
952
|
-
.get(key)
|
|
953
|
-
.and_then(crate::model::yaml::Value::as_str)
|
|
954
|
-
.map(ToString::to_string)
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
fn field_or_null(value: &Value, keys: &[&str]) -> Value {
|
|
958
|
-
keys.iter()
|
|
959
|
-
.find_map(|key| value.get(*key).cloned())
|
|
960
|
-
.unwrap_or(Value::Null)
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
fn field_or_default(value: &Value, key: &str, default: &str) -> Value {
|
|
964
|
-
value
|
|
965
|
-
.get(key)
|
|
966
|
-
.cloned()
|
|
967
|
-
.unwrap_or_else(|| Value::String(default.to_string()))
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
fn last_task_for_agent(state: &Value, agent_id: &str) -> Value {
|
|
971
|
-
state
|
|
972
|
-
.get("tasks")
|
|
973
|
-
.and_then(Value::as_array)
|
|
974
|
-
.and_then(|tasks| {
|
|
975
|
-
tasks
|
|
976
|
-
.iter()
|
|
977
|
-
.rev()
|
|
978
|
-
.find(|task| task.get("assignee").and_then(Value::as_str) == Some(agent_id))
|
|
979
|
-
})
|
|
980
|
-
.and_then(|task| {
|
|
981
|
-
task.get("id")
|
|
982
|
-
.and_then(Value::as_str)
|
|
983
|
-
.map(ToString::to_string)
|
|
984
|
-
})
|
|
985
|
-
.map(Value::String)
|
|
986
|
-
.unwrap_or(Value::Null)
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
fn terminal_target(state: &Value, agent_id: &str, agent_state: &Value) -> Value {
|
|
990
|
-
json!({
|
|
991
|
-
"session": agent_state
|
|
992
|
-
.get("session_name")
|
|
993
|
-
.cloned()
|
|
994
|
-
.or_else(|| state.get("session_name").cloned())
|
|
995
|
-
.unwrap_or(Value::Null),
|
|
996
|
-
"window": window_target(agent_state, agent_id),
|
|
997
|
-
"pane": field_or_null(agent_state, &["pane_id"]),
|
|
998
|
-
})
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
fn window_target(agent_state: &Value, agent_id: &str) -> Value {
|
|
1002
|
-
let window = field_or_null(agent_state, &["window", "window_name"]);
|
|
1003
|
-
if window.is_null() {
|
|
1004
|
-
Value::String(agent_id.to_string())
|
|
1005
|
-
} else {
|
|
1006
|
-
window
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
fn load_team_spec_optional(
|
|
1011
|
-
workspace: &Path,
|
|
1012
|
-
state: &Value,
|
|
1013
|
-
) -> Result<Option<crate::model::yaml::Value>, CliError> {
|
|
1014
|
-
let spec_path = state
|
|
1015
|
-
.get("spec_path")
|
|
1016
|
-
.and_then(Value::as_str)
|
|
1017
|
-
.filter(|path| !path.is_empty())
|
|
1018
|
-
.map(PathBuf::from)
|
|
1019
|
-
.or_else(|| {
|
|
1020
|
-
state
|
|
1021
|
-
.get("team_dir")
|
|
1022
|
-
.and_then(Value::as_str)
|
|
1023
|
-
.filter(|path| !path.is_empty())
|
|
1024
|
-
.map(|path| PathBuf::from(path).join("team.spec.yaml"))
|
|
1025
|
-
})
|
|
1026
|
-
.unwrap_or_else(|| workspace.join("team.spec.yaml"));
|
|
1027
|
-
if !spec_path.exists() {
|
|
1028
|
-
return Ok(None);
|
|
1029
|
-
}
|
|
1030
|
-
let text = std::fs::read_to_string(&spec_path)?;
|
|
1031
|
-
crate::model::yaml::loads(&text)
|
|
1032
|
-
.map(Some)
|
|
1033
|
-
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
fn load_team_spec_at(spec_path: &Path) -> Result<Option<crate::model::yaml::Value>, CliError> {
|
|
1037
|
-
if !spec_path.exists() {
|
|
1038
|
-
return Ok(None);
|
|
1039
|
-
}
|
|
1040
|
-
let text = std::fs::read_to_string(spec_path)?;
|
|
1041
|
-
crate::model::yaml::loads(&text)
|
|
1042
|
-
.map(Some)
|
|
1043
|
-
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
345
|
/// `cmd_approvals`(`commands.py:112`)。
|
|
1047
346
|
pub fn cmd_approvals(args: &ApprovalsArgs) -> Result<CmdResult, CliError> {
|
|
1048
347
|
let selected = crate::state::selector::resolve_active_team(
|
|
@@ -1168,14 +467,6 @@ pub fn cmd_claim_leader(args: &ClaimLeaderArgs) -> Result<CmdResult, CliError> {
|
|
|
1168
467
|
Ok(CmdResult::from_json(value, args.json))
|
|
1169
468
|
}
|
|
1170
469
|
|
|
1171
|
-
/// `cmd_identity`(`commands.py:160`)。
|
|
1172
|
-
pub fn cmd_identity(args: &IdentityArgs) -> Result<CmdResult, CliError> {
|
|
1173
|
-
Ok(CmdResult::from_json(
|
|
1174
|
-
leader_port::leader_identity(&args.workspace, args.team.as_deref())?,
|
|
1175
|
-
args.json,
|
|
1176
|
-
))
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
470
|
/// `cmd_shutdown`(`commands.py:340`)。
|
|
1180
471
|
pub fn cmd_shutdown(args: &ShutdownArgs) -> Result<CmdResult, CliError> {
|
|
1181
472
|
Ok(CmdResult::from_json(
|
|
@@ -1711,12 +1002,13 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
|
|
|
1711
1002
|
|
|
1712
1003
|
#[cfg(test)]
|
|
1713
1004
|
mod tests {
|
|
1714
|
-
#![allow(clippy::unwrap_used)]
|
|
1005
|
+
#![allow(clippy::unwrap_used, clippy::panic)]
|
|
1715
1006
|
|
|
1716
1007
|
use super::{
|
|
1717
|
-
|
|
1718
|
-
split_shell_argv,
|
|
1008
|
+
append_send_guidance, finish_quick_start, format_inbox_human, quickstart_human,
|
|
1009
|
+
send_command, split_shell_argv,
|
|
1719
1010
|
};
|
|
1011
|
+
use crate::cli::{CmdOutput, ExitCode, QuickStartArgs};
|
|
1720
1012
|
use serde_json::{json, Value};
|
|
1721
1013
|
use std::path::{Path, PathBuf};
|
|
1722
1014
|
|
|
@@ -1754,11 +1046,11 @@ mod tests {
|
|
|
1754
1046
|
});
|
|
1755
1047
|
let out = quickstart_human(&value);
|
|
1756
1048
|
assert!(
|
|
1757
|
-
out.contains("
|
|
1049
|
+
out.contains("team started"),
|
|
1758
1050
|
"must show startup summary; got {out}"
|
|
1759
1051
|
);
|
|
1760
1052
|
assert!(
|
|
1761
|
-
out.contains("
|
|
1053
|
+
out.contains("Connect to the team:"),
|
|
1762
1054
|
"must render attach block; got {out}"
|
|
1763
1055
|
);
|
|
1764
1056
|
assert!(
|
|
@@ -1797,7 +1089,7 @@ mod tests {
|
|
|
1797
1089
|
assert_eq!(
|
|
1798
1090
|
quickstart_human(&value),
|
|
1799
1091
|
format!(
|
|
1800
|
-
"
|
|
1092
|
+
"quick-start complete\n{}",
|
|
1801
1093
|
crate::cli::QUICK_START_REMINDER
|
|
1802
1094
|
)
|
|
1803
1095
|
);
|
|
@@ -1806,7 +1098,7 @@ mod tests {
|
|
|
1806
1098
|
assert_eq!(
|
|
1807
1099
|
quickstart_human(&value2),
|
|
1808
1100
|
format!(
|
|
1809
|
-
"
|
|
1101
|
+
"s\n{}",
|
|
1810
1102
|
crate::cli::QUICK_START_REMINDER
|
|
1811
1103
|
)
|
|
1812
1104
|
);
|
|
@@ -1827,7 +1119,7 @@ mod tests {
|
|
|
1827
1119
|
"team-agent",
|
|
1828
1120
|
"send",
|
|
1829
1121
|
"worker name; echo unsafe",
|
|
1830
|
-
"
|
|
1122
|
+
"Complete the task and reply to the leader.",
|
|
1831
1123
|
"--workspace",
|
|
1832
1124
|
"/tmp/my workspace",
|
|
1833
1125
|
"--team",
|
|
@@ -1846,7 +1138,7 @@ mod tests {
|
|
|
1846
1138
|
.unwrap();
|
|
1847
1139
|
assert_eq!(
|
|
1848
1140
|
command,
|
|
1849
|
-
"team-agent send 'worker name; echo unsafe' '
|
|
1141
|
+
"team-agent send 'worker name; echo unsafe' 'Complete the task and reply to the leader.' --workspace '/tmp/my workspace' --team team-a"
|
|
1850
1142
|
);
|
|
1851
1143
|
}
|
|
1852
1144
|
|
|
@@ -1903,7 +1195,7 @@ mod tests {
|
|
|
1903
1195
|
"team-agent",
|
|
1904
1196
|
"send",
|
|
1905
1197
|
"worker",
|
|
1906
|
-
"
|
|
1198
|
+
"Complete the task and reply to the leader.",
|
|
1907
1199
|
"--workspace",
|
|
1908
1200
|
"/tmp/ws",
|
|
1909
1201
|
"--team",
|
|
@@ -1912,49 +1204,71 @@ mod tests {
|
|
|
1912
1204
|
);
|
|
1913
1205
|
}
|
|
1914
1206
|
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1207
|
+
fn quick_start_args(as_json: bool) -> QuickStartArgs {
|
|
1208
|
+
QuickStartArgs {
|
|
1209
|
+
workspace: PathBuf::from("/tmp/first-launch"),
|
|
1210
|
+
agents_dir: PathBuf::from("/tmp/first-launch/.team/current"),
|
|
1211
|
+
name: None,
|
|
1212
|
+
team_id: None,
|
|
1213
|
+
yes: true,
|
|
1214
|
+
json: as_json,
|
|
1215
|
+
detail: false,
|
|
1216
|
+
backend: None,
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1927
1219
|
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
"
|
|
1933
|
-
"
|
|
1934
|
-
|
|
1935
|
-
|
|
1220
|
+
#[test]
|
|
1221
|
+
fn quick_start_human_preserves_typed_refusals_without_error_field() {
|
|
1222
|
+
for status in ["existing_runtime", "preflight_blocked", "leader_binding_refused"] {
|
|
1223
|
+
let value = json!({
|
|
1224
|
+
"ok": false,
|
|
1225
|
+
"status": status,
|
|
1226
|
+
"reason": "the exact underlying refusal",
|
|
1227
|
+
"summary": "creation refused",
|
|
1228
|
+
"blockers": [{"stage": "identity", "reason": "owner mismatch"}],
|
|
1229
|
+
"next_actions": ["team-agent restart --workspace /tmp/first-launch --team current"],
|
|
1230
|
+
"state_path": "/tmp/first-launch/.team/runtime/state.json",
|
|
1231
|
+
"readiness": {"reason": "underlying readiness evidence"}
|
|
1232
|
+
});
|
|
1233
|
+
let result = finish_quick_start(value.clone(), &quick_start_args(false)).unwrap();
|
|
1234
|
+
assert_eq!(result.exit, ExitCode::Error);
|
|
1235
|
+
let CmdOutput::Human(text) = result.output else { panic!("expected human report") };
|
|
1236
|
+
let rendered: Value = serde_json::from_str(text.strip_prefix("quick-start report:\n").unwrap()).unwrap();
|
|
1237
|
+
assert_eq!(rendered, value, "human projection lost fields for {status}");
|
|
1238
|
+
assert!(!text.contains("team-agent doctor"), "must not replace actual actions");
|
|
1239
|
+
let json_result = finish_quick_start(value.clone(), &quick_start_args(true)).unwrap();
|
|
1240
|
+
assert_eq!(json_result.exit, ExitCode::Error);
|
|
1241
|
+
assert_eq!(json_result.output, CmdOutput::Json(value));
|
|
1242
|
+
}
|
|
1936
1243
|
}
|
|
1937
1244
|
|
|
1938
1245
|
#[test]
|
|
1939
|
-
fn
|
|
1940
|
-
let
|
|
1941
|
-
"
|
|
1942
|
-
"
|
|
1943
|
-
|
|
1944
|
-
|
|
1246
|
+
fn quick_start_human_preserves_original_error_and_actions() {
|
|
1247
|
+
let value = json!({
|
|
1248
|
+
"ok": false,
|
|
1249
|
+
"error": "database initialization failed at /tmp/first-launch/.team/team.db: permission denied",
|
|
1250
|
+
"action": "repair directory permissions",
|
|
1251
|
+
"next_actions": ["inspect the exact database path"]
|
|
1945
1252
|
});
|
|
1946
|
-
let
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1253
|
+
let result = finish_quick_start(value.clone(), &quick_start_args(false)).unwrap();
|
|
1254
|
+
assert_eq!(result.exit, ExitCode::Error);
|
|
1255
|
+
let CmdOutput::Human(text) = result.output else { panic!("expected human report") };
|
|
1256
|
+
let rendered: Value = serde_json::from_str(text.strip_prefix("quick-start report:\n").unwrap()).unwrap();
|
|
1257
|
+
assert_eq!(rendered, value);
|
|
1258
|
+
}
|
|
1950
1259
|
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
"
|
|
1956
|
-
"
|
|
1957
|
-
|
|
1958
|
-
|
|
1260
|
+
#[test]
|
|
1261
|
+
fn pending_tool_load_is_not_reported_as_failed_team_creation() {
|
|
1262
|
+
for as_json in [false, true] {
|
|
1263
|
+
let value = json!({
|
|
1264
|
+
"ok": true,
|
|
1265
|
+
"status": "pending_tool_load",
|
|
1266
|
+
"summary": "Team started; worker tool loading is not yet verified",
|
|
1267
|
+
"readiness": {"ready": false},
|
|
1268
|
+
"next_actions": ["send a task and wait for the actual reply"]
|
|
1269
|
+
});
|
|
1270
|
+
let result = finish_quick_start(value, &quick_start_args(as_json)).unwrap();
|
|
1271
|
+
assert_eq!(result.exit, ExitCode::Ok);
|
|
1272
|
+
}
|
|
1959
1273
|
}
|
|
1960
1274
|
}
|