@team-agent/installer 0.5.72 → 0.5.74

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 (37) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +19 -13
  4. package/crates/team-agent/src/cli/emit.rs +6 -3
  5. package/crates/team-agent/src/cli/mod.rs +151 -0
  6. package/crates/team-agent/src/cli/tests/leader_watch.rs +28 -0
  7. package/crates/team-agent/src/cli/tests/run_delegation.rs +3 -0
  8. package/crates/team-agent/src/cli/types.rs +6 -0
  9. package/crates/team-agent/src/compiler.rs +14 -1
  10. package/crates/team-agent/src/leader/lease.rs +106 -21
  11. package/crates/team-agent/src/leader/registry.rs +0 -89
  12. package/crates/team-agent/src/leader/start.rs +8 -0
  13. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +43 -7
  14. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +457 -73
  15. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +16 -6
  16. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -0
  17. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +5 -3
  18. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -11
  19. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +60 -0
  20. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +1 -1
  21. package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +71 -1
  22. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +34 -1
  23. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +25 -23
  24. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +396 -3
  25. package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +3 -1
  26. package/crates/team-agent/src/messaging/delivery.rs +26 -8
  27. package/crates/team-agent/src/messaging/tests/runtime.rs +234 -0
  28. package/crates/team-agent/src/model/spec.rs +81 -7
  29. package/crates/team-agent/src/provider/adapters/pi.rs +26 -6
  30. package/crates/team-agent/src/state/persist.rs +272 -3
  31. package/crates/team-agent/src/state/repository.rs +9 -2
  32. package/crates/team-agent/src/tmux_backend/tests.rs +64 -0
  33. package/crates/team-agent/src/tmux_backend.rs +57 -0
  34. package/package.json +4 -4
  35. package/skills/team-agent/SKILL.md +4 -1
  36. package/skills/team-agent/command-coverage.json +21 -0
  37. package/skills/team-agent/references/team-agent-operator.md +9 -2
@@ -2,13 +2,19 @@ use std::os::unix::fs::PermissionsExt;
2
2
  use std::path::{Path, PathBuf};
3
3
  use std::sync::atomic::{AtomicU32, Ordering};
4
4
 
5
+ use crate::lifecycle::launch::launch_with_transport_in_workspace;
5
6
  use crate::lifecycle::launch::pi_mcp::{
7
+ PiAdapterIdentity, PiExecutableChain, PiExecutableFileType, PiMaterializeRequest,
8
+ PiSessionScope, PiWrapperRequest, materialize_pi_plan, materialize_pi_resume_plan,
6
9
  parse_pi_list_models_table, pi_seat_paths, resolve_pi_executable_chain,
7
10
  validate_pi_adapter_identity, validate_pi_executable_chain, validate_pi_wrapper_source,
8
- write_pi_wrapper, write_pi_wrapper_with_publish, PiAdapterIdentity, PiExecutableChain,
9
- PiExecutableFileType, PiWrapperRequest,
11
+ write_pi_wrapper, write_pi_wrapper_with_publish,
10
12
  };
11
- use crate::provider::McpConfig;
13
+ use crate::lifecycle::start_agent_with_transport;
14
+ use crate::model::enums::ProviderEffort;
15
+ use crate::provider::session_scan::{CaptureSessionContext, scan_session_candidates_once};
16
+ use crate::provider::{McpConfig, Provider, SessionId};
17
+ use crate::transport::test_support::OfflineTransport;
12
18
 
13
19
  #[path = "../../../tests/support/hermetic.rs"]
14
20
  mod hermetic_guard;
@@ -34,6 +40,11 @@ const PI_SYMLINK_TEST: &str = concat!(
34
40
  "lifecycle::tests::pi_executable_mcp_red::",
35
41
  "pi_standard_npm_symlink_is_a_verified_launch_entry"
36
42
  );
43
+ const PI_MATERIALIZER_CHILD: &str = "TEAM_AGENT_TEST_PI_MATERIALIZER_CHILD";
44
+ const PI_MATERIALIZER_TEST: &str = concat!(
45
+ "lifecycle::tests::pi_executable_mcp_red::",
46
+ "pi_materializer_and_worker_routes_use_the_recorded_scope"
47
+ );
37
48
 
38
49
  fn run_process_isolated(marker: &str, test_name: &str, body: impl FnOnce()) {
39
50
  if std::env::var_os(marker).is_some() {
@@ -504,3 +515,385 @@ fn pi_wrapper_import_failure_body(hermetic: &HermeticTestEnv) {
504
515
 
505
516
  std::fs::remove_dir_all(root).expect("remove wrapper fixture");
506
517
  }
518
+
519
+ #[test]
520
+ fn pi_materializer_and_worker_routes_use_the_recorded_scope() {
521
+ run_process_isolated(PI_MATERIALIZER_CHILD, PI_MATERIALIZER_TEST, || {
522
+ let hermetic = HermeticTestEnv::enter("pi-materializer-scope");
523
+ assert_isolated_child();
524
+ pi_materializer_and_worker_routes_body(&hermetic);
525
+ });
526
+ }
527
+
528
+ fn pi_materializer_and_worker_routes_body(hermetic: &HermeticTestEnv) {
529
+ let fixture_root = hermetic.root().join("materializer-fixture");
530
+ let bin = fixture_root.join("bin");
531
+ let adapter_root = fixture_root.join("pi-mcp-adapter");
532
+ let candidate = fixture_root.join("candidate/team-agent");
533
+ let real = fixture_root.join("pi-real");
534
+ std::fs::create_dir_all(&bin).expect("create Pi fixture bin");
535
+ std::fs::create_dir_all(&adapter_root).expect("create Pi adapter fixture");
536
+ std::fs::create_dir_all(candidate.parent().expect("candidate parent"))
537
+ .expect("create candidate parent");
538
+ std::fs::write(&candidate, b"candidate").expect("write candidate fixture");
539
+ std::fs::write(
540
+ adapter_root.join("package.json"),
541
+ br#"{"name":"pi-mcp-adapter","version":"2.30.0","pi":{"extensions":["./index.ts"]}}"#,
542
+ )
543
+ .expect("write adapter package");
544
+ std::fs::write(
545
+ adapter_root.join("index.ts"),
546
+ b"export const createMcpAdapter = () => {};\n",
547
+ )
548
+ .expect("write adapter entry");
549
+ let pi_script = format!(
550
+ "#!/bin/sh\ncase \"${{1-}}\" in\n --version) printf '0.84.4\\n' ;;\n --list-models) printf 'provider model\\nteam-agent qwen3.8-27b\\n' ;;\n list) printf 'npm:pi-mcp-adapter\\n{}\\n' ;;\n *) exit 0 ;;\nesac\n",
551
+ adapter_root.display()
552
+ );
553
+ std::fs::write(&real, &pi_script).expect("write Pi real fixture");
554
+ std::fs::set_permissions(&real, std::fs::Permissions::from_mode(0o755))
555
+ .expect("make Pi real fixture executable");
556
+ std::os::unix::fs::symlink(&real, bin.join("pi")).expect("create Pi PATH symlink");
557
+ let _path = hermetic.with_env(
558
+ "PATH",
559
+ std::env::join_paths([&bin])
560
+ .expect("join Pi fixture PATH")
561
+ .to_str()
562
+ .expect("Pi fixture PATH is UTF-8"),
563
+ );
564
+
565
+ let native_workspace = hermetic.workspace("native");
566
+ let native_paths = pi_seat_paths(&native_workspace, "team-a", "leader");
567
+ let native_config = mcp_config(&candidate, &native_workspace, "leader");
568
+ let native = materialize_pi_plan(PiMaterializeRequest {
569
+ workspace: &native_workspace,
570
+ team_id: "team-a",
571
+ agent_id: "leader",
572
+ model: None,
573
+ effort: None,
574
+ system_prompt: "native leader fixture",
575
+ tool_categories: &["mcp_team"],
576
+ team_mcp_tools: &["send_message"],
577
+ mcp_config: &native_config,
578
+ session_scope: PiSessionScope::NativeDefault,
579
+ })
580
+ .expect("NativeDefault materializer");
581
+ assert!(
582
+ native.provider_projects_root.is_none(),
583
+ "NativeDefault must not expose a provider capture root: {native:?}"
584
+ );
585
+ assert!(
586
+ !native_paths.sessions.exists(),
587
+ "NativeDefault must not create the Team Agent seat session root"
588
+ );
589
+ for forbidden in ["--session-dir", "--cwd", "--workspace", "--session"] {
590
+ assert!(
591
+ !native.argv.iter().any(|arg| arg == forbidden),
592
+ "NativeDefault fresh argv must omit {forbidden}: {:?}",
593
+ native.argv
594
+ );
595
+ }
596
+
597
+ let isolated_workspace = hermetic.workspace("isolated");
598
+ let isolated_paths = pi_seat_paths(&isolated_workspace, "team-a", "worker-a");
599
+ let isolated_config = mcp_config(&candidate, &isolated_workspace, "worker-a");
600
+ let isolated = materialize_pi_plan(PiMaterializeRequest {
601
+ workspace: &isolated_workspace,
602
+ team_id: "team-a",
603
+ agent_id: "worker-a",
604
+ model: Some("team-agent/qwen3.8-27b"),
605
+ effort: Some(ProviderEffort::Medium),
606
+ system_prompt: "isolated worker fixture",
607
+ tool_categories: &["mcp_team"],
608
+ team_mcp_tools: &["send_message"],
609
+ mcp_config: &isolated_config,
610
+ session_scope: PiSessionScope::Isolated,
611
+ })
612
+ .expect("Isolated materializer");
613
+ assert_eq!(
614
+ isolated.provider_projects_root.as_deref(),
615
+ Some(isolated_paths.sessions.as_path())
616
+ );
617
+ assert_session_pair(&isolated.argv, &isolated_paths.sessions);
618
+ assert!(!isolated.argv.iter().any(|arg| arg == "--cwd"));
619
+ assert!(!isolated.argv.iter().any(|arg| arg == "--workspace"));
620
+
621
+ let session_id = SessionId::new("pi-materializer-resume");
622
+ let exact = isolated_paths.sessions.join("2026/09/exact.jsonl");
623
+ std::fs::create_dir_all(exact.parent().expect("exact session parent"))
624
+ .expect("create exact session parent");
625
+ std::fs::write(&exact, b"{}\n").expect("write exact session fixture");
626
+ let resumed = materialize_pi_resume_plan(
627
+ PiMaterializeRequest {
628
+ workspace: &isolated_workspace,
629
+ team_id: "team-a",
630
+ agent_id: "worker-a",
631
+ model: None,
632
+ effort: None,
633
+ system_prompt: "isolated resume fixture",
634
+ tool_categories: &["mcp_team"],
635
+ team_mcp_tools: &["send_message"],
636
+ mcp_config: &isolated_config,
637
+ session_scope: PiSessionScope::Isolated,
638
+ },
639
+ &session_id,
640
+ &exact,
641
+ &isolated_workspace,
642
+ )
643
+ .expect("Isolated exact-path resume materializer");
644
+ assert_eq!(
645
+ resumed.provider_projects_root.as_deref(),
646
+ Some(isolated_paths.sessions.as_path())
647
+ );
648
+ assert_session_pair(&resumed.argv, &isolated_paths.sessions);
649
+ assert!(
650
+ resumed
651
+ .argv
652
+ .windows(2)
653
+ .any(|pair| { pair == ["--session", exact.to_string_lossy().as_ref()] })
654
+ );
655
+
656
+ let native_resume = materialize_pi_resume_plan(
657
+ PiMaterializeRequest {
658
+ workspace: &native_workspace,
659
+ team_id: "team-a",
660
+ agent_id: "leader",
661
+ model: None,
662
+ effort: None,
663
+ system_prompt: "native resume fixture",
664
+ tool_categories: &["mcp_team"],
665
+ team_mcp_tools: &["send_message"],
666
+ mcp_config: &native_config,
667
+ session_scope: PiSessionScope::NativeDefault,
668
+ },
669
+ &session_id,
670
+ &exact,
671
+ &native_workspace,
672
+ );
673
+ assert!(
674
+ matches!(
675
+ native_resume,
676
+ Err(crate::provider::ProviderError::ResumeUnavailable(_))
677
+ ),
678
+ "NativeDefault resume must fail closed before emitting a native HOME path: {native_resume:?}"
679
+ );
680
+
681
+ let home_candidate = hermetic.home().join(".pi/agent/sessions/home.jsonl");
682
+ std::fs::create_dir_all(home_candidate.parent().expect("home session parent"))
683
+ .expect("create home session parent");
684
+ std::fs::write(&home_candidate, b"{}\n").expect("write home candidate");
685
+ let other_paths = pi_seat_paths(&isolated_workspace, "team-a", "worker-b");
686
+ let other_candidate = other_paths.sessions.join("other.jsonl");
687
+ std::fs::create_dir_all(other_candidate.parent().expect("other session parent"))
688
+ .expect("create other session parent");
689
+ std::fs::write(&other_candidate, b"{}\n").expect("write other seat candidate");
690
+ for (label, outside) in [("HOME", &home_candidate), ("other seat", &other_candidate)] {
691
+ let refused = materialize_pi_resume_plan(
692
+ PiMaterializeRequest {
693
+ workspace: &isolated_workspace,
694
+ team_id: "team-a",
695
+ agent_id: "worker-a",
696
+ model: None,
697
+ effort: None,
698
+ system_prompt: "isolated negative resume fixture",
699
+ tool_categories: &["mcp_team"],
700
+ team_mcp_tools: &["send_message"],
701
+ mcp_config: &isolated_config,
702
+ session_scope: PiSessionScope::Isolated,
703
+ },
704
+ &session_id,
705
+ &outside,
706
+ &isolated_workspace,
707
+ );
708
+ assert!(
709
+ matches!(
710
+ refused,
711
+ Err(crate::provider::ProviderError::ResumeUnavailable(_))
712
+ ),
713
+ "{label} candidate must be refused by the exact seat-root check: {refused:?}"
714
+ );
715
+ }
716
+
717
+ let worker_workspace = hermetic.workspace("worker-spawn");
718
+ let spec_path = worker_workspace
719
+ .join(".team")
720
+ .join("team-a")
721
+ .join("team.spec.yaml");
722
+ std::fs::create_dir_all(spec_path.parent().expect("worker Pi spec parent"))
723
+ .expect("create worker Pi spec parent");
724
+ let spec = crate::model::yaml::loads(
725
+ "team:\n name: team-a\nruntime:\n session_name: team-workers\n display_backend: tmux_attach\nagents:\n - id: worker-a\n role: Worker A\n provider: pi\n model: team-agent/qwen3.8-27b\n auth_mode: subscription\n dangerously_skip_permissions: false\n tools:\n - mcp_team\n - id: worker-b\n role: Worker B\n provider: pi\n model: team-agent/qwen3.8-27b\n auth_mode: subscription\n dangerously_skip_permissions: false\n tools:\n - mcp_team\n",
726
+ )
727
+ .expect("parse worker Pi spec");
728
+ std::fs::write(&spec_path, crate::model::yaml::dumps(&spec)).expect("write worker Pi spec");
729
+ let transport = OfflineTransport::new();
730
+ let started = launch_with_transport_in_workspace(
731
+ &worker_workspace,
732
+ &spec_path,
733
+ false,
734
+ true,
735
+ true,
736
+ &transport,
737
+ )
738
+ .expect("worker fresh spawn construction")
739
+ .started;
740
+ assert_eq!(started.len(), 2, "both Pi worker seats must be spawned");
741
+ for worker in ["worker-a", "worker-b"] {
742
+ let paths = pi_seat_paths(&worker_workspace, "team-a", worker);
743
+ let started_agent = started
744
+ .iter()
745
+ .find(|entry| entry.agent_id.as_str() == worker)
746
+ .expect("started worker entry");
747
+ assert_eq!(
748
+ started_agent.provider_projects_root.as_deref(),
749
+ Some(paths.sessions.as_path()),
750
+ "fresh worker must expose its exact seat capture root"
751
+ );
752
+ let record = transport
753
+ .spawn_records()
754
+ .into_iter()
755
+ .find(|(_, argv)| {
756
+ argv.iter()
757
+ .any(|arg| arg.as_str() == paths.wrapper.to_string_lossy().as_ref())
758
+ })
759
+ .expect("transport spawn record for worker seat");
760
+ assert_session_pair(&record.1, &paths.sessions);
761
+ assert!(
762
+ !record
763
+ .1
764
+ .iter()
765
+ .any(|arg| arg.as_str() == home_candidate.to_string_lossy().as_ref())
766
+ );
767
+ assert!(
768
+ !record
769
+ .1
770
+ .iter()
771
+ .any(|arg| arg.as_str() == other_candidate.to_string_lossy().as_ref())
772
+ );
773
+
774
+ let pending_session_id = started_agent
775
+ .pending_session_id
776
+ .clone()
777
+ .expect("fresh worker carries a pending Pi session id");
778
+ write_pi_session_header(
779
+ &home_candidate,
780
+ pending_session_id.as_str(),
781
+ &worker_workspace,
782
+ "2099-01-01T00:00:00Z",
783
+ );
784
+ let other_worker = if worker == "worker-a" {
785
+ "worker-b"
786
+ } else {
787
+ "worker-a"
788
+ };
789
+ let cross_seat = pi_seat_paths(&worker_workspace, "team-a", other_worker)
790
+ .sessions
791
+ .join("foreign.jsonl");
792
+ write_pi_session_header(
793
+ &cross_seat,
794
+ pending_session_id.as_str(),
795
+ &worker_workspace,
796
+ "2099-01-01T00:00:00Z",
797
+ );
798
+ let candidates = scan_session_candidates_once(
799
+ Provider::Pi,
800
+ &CaptureSessionContext {
801
+ agent_id: worker.to_string(),
802
+ spawn_cwd: worker_workspace.clone(),
803
+ pane_id: None,
804
+ pane_pid: None,
805
+ spawned_at: Some(started_agent.spawned_at.clone()),
806
+ expected_session_id: Some(pending_session_id),
807
+ provider_projects_root: Some(paths.sessions.clone()),
808
+ },
809
+ )
810
+ .expect("scan the recorded worker seat only");
811
+ assert!(
812
+ candidates.is_empty(),
813
+ "HOME and another worker seat must not become capture candidates: {candidates:?}"
814
+ );
815
+ }
816
+
817
+ let restart_workspace = hermetic.workspace("worker-restart");
818
+ let restart_paths = pi_seat_paths(&restart_workspace, "team-a", "worker-a");
819
+ let restart_exact = restart_paths.sessions.join("2026/09/restart.jsonl");
820
+ write_pi_session_header(
821
+ &restart_exact,
822
+ "pi-restart-session",
823
+ &restart_workspace,
824
+ "2026-09-01T00:00:01Z",
825
+ );
826
+ crate::state::persist::save_runtime_state(
827
+ &restart_workspace,
828
+ &serde_json::json!({
829
+ "session_name": "team-workers",
830
+ "active_team_key": "team-a",
831
+ "agents": {
832
+ "worker-a": {
833
+ "id": "worker-a",
834
+ "status": "running",
835
+ "provider": "pi",
836
+ "role": "Worker A",
837
+ "model": "team-agent/qwen3.8-27b",
838
+ "auth_mode": "subscription",
839
+ "tools": ["mcp_team"],
840
+ "session_id": "pi-restart-session",
841
+ "rollout_path": restart_exact.to_string_lossy(),
842
+ "spawn_cwd": restart_workspace.to_string_lossy(),
843
+ "first_send_at": "2026-09-01T00:00:00Z"
844
+ }
845
+ }
846
+ }),
847
+ )
848
+ .expect("write restart state");
849
+ let restart_transport = OfflineTransport::new();
850
+ let _ = start_agent_with_transport(
851
+ &restart_workspace,
852
+ &crate::model::ids::AgentId::new("worker-a"),
853
+ false,
854
+ false,
855
+ false,
856
+ None,
857
+ &restart_transport,
858
+ );
859
+ let restart_record = restart_transport
860
+ .spawn_records()
861
+ .into_iter()
862
+ .next()
863
+ .expect("restart must reach the worker spawn construction");
864
+ assert_session_pair(&restart_record.1, &restart_paths.sessions);
865
+ assert!(
866
+ restart_record
867
+ .1
868
+ .windows(2)
869
+ .any(|pair| { pair == ["--session", restart_exact.to_string_lossy().as_ref()] })
870
+ );
871
+ assert!(
872
+ !restart_record
873
+ .1
874
+ .iter()
875
+ .any(|arg| arg.as_str() == home_candidate.to_string_lossy().as_ref())
876
+ );
877
+ }
878
+
879
+ fn assert_session_pair(argv: &[String], session_root: &Path) {
880
+ let expected = session_root.to_string_lossy();
881
+ assert!(
882
+ argv.windows(2)
883
+ .any(|pair| pair[0] == "--session-dir" && pair[1].as_str() == expected.as_ref()),
884
+ "argv must contain exact --session-dir pair for {}: {argv:?}",
885
+ session_root.display()
886
+ );
887
+ }
888
+
889
+ fn write_pi_session_header(path: &Path, id: &str, cwd: &Path, timestamp: &str) {
890
+ std::fs::create_dir_all(path.parent().expect("session parent")).expect("create session parent");
891
+ let header = serde_json::json!({
892
+ "type": "session",
893
+ "version": 3,
894
+ "id": id,
895
+ "cwd": cwd,
896
+ "timestamp": timestamp,
897
+ });
898
+ std::fs::write(path, format!("{header}\n")).expect("write Pi session header");
899
+ }
@@ -60,7 +60,9 @@ fn resume_argv(path: &Path) -> Result<Vec<String>, ProviderError> {
60
60
  effort: Some(ProviderEffort::Medium),
61
61
  system_prompt: "worker contract",
62
62
  tool_categories: &["mcp_team"],
63
- session_dir: Path::new("/workspace/.team/runtime/pi/team-a/worker-a/sessions"),
63
+ session_dir: Some(Path::new(
64
+ "/workspace/.team/runtime/pi/team-a/worker-a/sessions",
65
+ )),
64
66
  session: PiSessionSelector::Resume { path },
65
67
  agent_id: "worker-a",
66
68
  })
@@ -2866,7 +2866,9 @@ fn project_state_for_owner_team(
2866
2866
  return Ok(OwnerTeamProjection::Refused(outcome));
2867
2867
  }
2868
2868
  };
2869
- if top_level_state_matches_owner_team(fallback, &canonical_team) {
2869
+ if top_level_state_matches_owner_team(fallback, &canonical_team)
2870
+ && state_has_no_team_entries(fallback)
2871
+ {
2870
2872
  let mut state = fallback.clone();
2871
2873
  carry_top_level_leader_binding(&mut state, &raw);
2872
2874
  return Ok(OwnerTeamProjection::Projected {
@@ -2874,7 +2876,8 @@ fn project_state_for_owner_team(
2874
2876
  canonical_team,
2875
2877
  });
2876
2878
  }
2877
- if top_level_state_matches_owner_team(&raw, &canonical_team) {
2879
+ if top_level_state_matches_owner_team(&raw, &canonical_team) && state_has_no_team_entries(&raw)
2880
+ {
2878
2881
  return Ok(OwnerTeamProjection::Projected {
2879
2882
  state: raw,
2880
2883
  canonical_team,
@@ -2999,16 +3002,31 @@ fn project_state_for_owner_team_value(
2999
3002
  None
3000
3003
  }
3001
3004
 
3002
- fn top_level_state_matches_owner_team(state: &serde_json::Value, team: &str) -> bool {
3003
- state
3005
+ pub(crate) fn top_level_state_matches_owner_team(state: &serde_json::Value, team: &str) -> bool {
3006
+ if !state.is_object() {
3007
+ return false;
3008
+ }
3009
+ if state
3004
3010
  .get("active_team_key")
3005
3011
  .and_then(serde_json::Value::as_str)
3006
3012
  .is_some_and(|value| value == team)
3007
- || crate::state::projection::team_state_key(state) == team
3008
- || state
3009
- .get("session_name")
3013
+ {
3014
+ return true;
3015
+ }
3016
+ if state
3017
+ .get("session_name")
3018
+ .and_then(serde_json::Value::as_str)
3019
+ .is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team))
3020
+ {
3021
+ return true;
3022
+ }
3023
+ let has_explicit_key = ["team_key", "team_dir", "spec_path"].iter().any(|field| {
3024
+ state
3025
+ .get(*field)
3010
3026
  .and_then(serde_json::Value::as_str)
3011
- .is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team))
3027
+ .is_some_and(|value| !value.is_empty())
3028
+ });
3029
+ has_explicit_key && crate::state::projection::team_state_key(state) == team
3012
3030
  }
3013
3031
 
3014
3032
  /// `retry_injection_after_trust_auto_answer` (`trust_auto_answer.py`):leader 路径 trust 应答