@team-agent/installer 0.5.111 → 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 +1 -743
- package/crates/team-agent/src/cli/diagnose.rs +2 -424
- package/crates/team-agent/src/cli/emit.rs +31 -275
- package/crates/team-agent/src/cli/mod.rs +2 -9
- package/crates/team-agent/src/cli/spec.rs +0 -3
- 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/types.rs +4 -108
- 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/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/restart_session_capture_red.rs +2 -51
- 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> {
|
|
@@ -350,699 +349,11 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
|
|
|
350
349
|
)))
|
|
351
350
|
}
|
|
352
351
|
|
|
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
352
|
pub fn cmd_wait(args: &WaitArgs) -> Result<CmdResult, CliError> {
|
|
443
353
|
let result = messaging::wait_for_result(&args.workspace, &args.task_id)?;
|
|
444
354
|
Ok(CmdResult::from_json(result.to_json(), args.json))
|
|
445
355
|
}
|
|
446
356
|
|
|
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
357
|
/// `cmd_approvals`(`commands.py:112`)。
|
|
1047
358
|
pub fn cmd_approvals(args: &ApprovalsArgs) -> Result<CmdResult, CliError> {
|
|
1048
359
|
let selected = crate::state::selector::resolve_active_team(
|
|
@@ -1168,14 +479,6 @@ pub fn cmd_claim_leader(args: &ClaimLeaderArgs) -> Result<CmdResult, CliError> {
|
|
|
1168
479
|
Ok(CmdResult::from_json(value, args.json))
|
|
1169
480
|
}
|
|
1170
481
|
|
|
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
482
|
/// `cmd_shutdown`(`commands.py:340`)。
|
|
1180
483
|
pub fn cmd_shutdown(args: &ShutdownArgs) -> Result<CmdResult, CliError> {
|
|
1181
484
|
Ok(CmdResult::from_json(
|
|
@@ -1714,7 +1017,7 @@ mod tests {
|
|
|
1714
1017
|
#![allow(clippy::unwrap_used)]
|
|
1715
1018
|
|
|
1716
1019
|
use super::{
|
|
1717
|
-
|
|
1020
|
+
append_send_guidance, format_inbox_human, quickstart_human, send_command,
|
|
1718
1021
|
split_shell_argv,
|
|
1719
1022
|
};
|
|
1720
1023
|
use serde_json::{json, Value};
|
|
@@ -1912,49 +1215,4 @@ mod tests {
|
|
|
1912
1215
|
);
|
|
1913
1216
|
}
|
|
1914
1217
|
|
|
1915
|
-
#[test]
|
|
1916
|
-
fn agent_pane_id_resolves_session_window_even_with_recorded_pane_id() {
|
|
1917
|
-
let state = json!({
|
|
1918
|
-
"session_name": "team-x",
|
|
1919
|
-
"agents": {
|
|
1920
|
-
"w1": {"pane_id": "%7", "window": "w1"}
|
|
1921
|
-
}
|
|
1922
|
-
});
|
|
1923
|
-
let agent = state
|
|
1924
|
-
.get("agents")
|
|
1925
|
-
.and_then(|agents| agents.get("w1"))
|
|
1926
|
-
.unwrap();
|
|
1927
|
-
|
|
1928
|
-
assert_eq!(
|
|
1929
|
-
agent_pane_id(&state, "w1", agent).unwrap(),
|
|
1930
|
-
(
|
|
1931
|
-
"team-x".to_string(),
|
|
1932
|
-
"w1".to_string(),
|
|
1933
|
-
"team-x:w1".to_string()
|
|
1934
|
-
)
|
|
1935
|
-
);
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
#[test]
|
|
1939
|
-
fn agent_pane_id_falls_back_to_session_window_target() {
|
|
1940
|
-
let state = json!({
|
|
1941
|
-
"session_name": "team-x",
|
|
1942
|
-
"agents": {
|
|
1943
|
-
"w1": {"window": "worker-one"}
|
|
1944
|
-
}
|
|
1945
|
-
});
|
|
1946
|
-
let agent = state
|
|
1947
|
-
.get("agents")
|
|
1948
|
-
.and_then(|agents| agents.get("w1"))
|
|
1949
|
-
.unwrap();
|
|
1950
|
-
|
|
1951
|
-
assert_eq!(
|
|
1952
|
-
agent_pane_id(&state, "w1", agent).unwrap(),
|
|
1953
|
-
(
|
|
1954
|
-
"team-x".to_string(),
|
|
1955
|
-
"worker-one".to_string(),
|
|
1956
|
-
"team-x:worker-one".to_string()
|
|
1957
|
-
)
|
|
1958
|
-
);
|
|
1959
|
-
}
|
|
1960
1218
|
}
|