@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.
Files changed (38) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -743
  4. package/crates/team-agent/src/cli/diagnose.rs +2 -424
  5. package/crates/team-agent/src/cli/emit.rs +31 -275
  6. package/crates/team-agent/src/cli/mod.rs +2 -9
  7. package/crates/team-agent/src/cli/spec.rs +0 -3
  8. package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
  9. package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
  10. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
  11. package/crates/team-agent/src/cli/tests/mod.rs +0 -2
  12. package/crates/team-agent/src/cli/types.rs +4 -108
  13. package/crates/team-agent/src/coordinator/health.rs +3 -283
  14. package/crates/team-agent/src/coordinator/mod.rs +0 -5
  15. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
  16. package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
  17. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
  18. package/crates/team-agent/src/coordinator/types.rs +1 -21
  19. package/crates/team-agent/src/db/message_store.rs +0 -42
  20. package/crates/team-agent/src/db/migration.rs +49 -9
  21. package/crates/team-agent/src/db/schema.rs +27 -20
  22. package/crates/team-agent/src/leader/owner_bind.rs +0 -24
  23. package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
  24. package/crates/team-agent/src/leader/tests/identity.rs +0 -32
  25. package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
  26. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
  27. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
  28. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
  29. package/crates/team-agent/src/messaging/mod.rs +0 -2
  30. package/crates/team-agent/src/messaging/results.rs +0 -58
  31. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
  32. package/crates/team-agent/src/state/repository.rs +1 -5
  33. package/package.json +4 -4
  34. package/templates/team_state.md +1 -1
  35. package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
  36. package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
  37. package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
  38. 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
- agent_pane_id, append_send_guidance, format_inbox_human, quickstart_human, send_command,
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
  }