@team-agent/installer 0.5.42 → 0.5.44

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 (156) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +129 -54
  4. package/crates/team-agent/src/cli/diagnose.rs +1 -2
  5. package/crates/team-agent/src/cli/emit.rs +1 -7
  6. package/crates/team-agent/src/cli/helpers.rs +3 -1
  7. package/crates/team-agent/src/cli/leader.rs +2 -1
  8. package/crates/team-agent/src/cli/mod.rs +126 -16
  9. package/crates/team-agent/src/cli/named_address.rs +14 -5
  10. package/crates/team-agent/src/cli/profile.rs +19 -7
  11. package/crates/team-agent/src/cli/send.rs +9 -3
  12. package/crates/team-agent/src/cli/status.rs +8 -30
  13. package/crates/team-agent/src/cli/status_port.rs +1341 -1272
  14. package/crates/team-agent/src/cli/tests/base.rs +738 -660
  15. package/crates/team-agent/src/cli/tests/compile.rs +45 -18
  16. package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
  21. package/crates/team-agent/src/cli/tests/mod.rs +17 -7
  22. package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
  23. package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
  25. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
  26. package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
  27. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
  28. package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
  29. package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
  30. package/crates/team-agent/src/codex_app_server.rs +2 -5
  31. package/crates/team-agent/src/compiler/tests.rs +139 -33
  32. package/crates/team-agent/src/compiler.rs +55 -22
  33. package/crates/team-agent/src/conpty/backend.rs +23 -33
  34. package/crates/team-agent/src/coordinator/backoff.rs +2 -7
  35. package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
  36. package/crates/team-agent/src/coordinator/health.rs +46 -31
  37. package/crates/team-agent/src/coordinator/mod.rs +3 -3
  38. package/crates/team-agent/src/coordinator/orphan.rs +22 -10
  39. package/crates/team-agent/src/coordinator/steps/abnormal.rs +51 -56
  40. package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
  41. package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
  42. package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
  43. package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
  44. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
  45. package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
  46. package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
  47. package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
  49. package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
  50. package/crates/team-agent/src/db/message_store.rs +138 -30
  51. package/crates/team-agent/src/db/migration.rs +249 -61
  52. package/crates/team-agent/src/db/schema.rs +303 -82
  53. package/crates/team-agent/src/diagnose/comms.rs +9 -2
  54. package/crates/team-agent/src/diagnose/mod.rs +1 -3
  55. package/crates/team-agent/src/diagnose/orphans.rs +79 -61
  56. package/crates/team-agent/src/event_log.rs +70 -16
  57. package/crates/team-agent/src/layout/manager.rs +15 -4
  58. package/crates/team-agent/src/layout/mod.rs +4 -4
  59. package/crates/team-agent/src/layout/overlay.rs +10 -3
  60. package/crates/team-agent/src/layout/placement.rs +5 -1
  61. package/crates/team-agent/src/layout/recovery.rs +4 -2
  62. package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
  63. package/crates/team-agent/src/layout/sessions.rs +17 -9
  64. package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
  65. package/crates/team-agent/src/layout/worker_env.rs +87 -19
  66. package/crates/team-agent/src/leader/helpers.rs +7 -1
  67. package/crates/team-agent/src/leader/lease.rs +199 -89
  68. package/crates/team-agent/src/leader/owner_bind.rs +55 -22
  69. package/crates/team-agent/src/leader/provider_attribution.rs +25 -6
  70. package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
  71. package/crates/team-agent/src/leader/rediscover.rs +74 -25
  72. package/crates/team-agent/src/leader/registry.rs +1 -1
  73. package/crates/team-agent/src/leader/start.rs +75 -54
  74. package/crates/team-agent/src/leader/takeover.rs +46 -11
  75. package/crates/team-agent/src/leader/tests/basics.rs +320 -167
  76. package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
  77. package/crates/team-agent/src/leader/tests/identity.rs +428 -356
  78. package/crates/team-agent/src/leader/tests/idle.rs +285 -254
  79. package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
  80. package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
  81. package/crates/team-agent/src/leader/tests/mod.rs +115 -99
  82. package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
  83. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
  84. package/crates/team-agent/src/lib.rs +4 -4
  85. package/crates/team-agent/src/lifecycle/display.rs +7 -3
  86. package/crates/team-agent/src/lifecycle/launch.rs +55 -15
  87. package/crates/team-agent/src/lifecycle/mod.rs +9 -1
  88. package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
  89. package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
  90. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
  91. package/crates/team-agent/src/lifecycle/restart/common.rs +6 -2
  92. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
  93. package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
  94. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
  95. package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
  96. package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
  97. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
  98. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
  99. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
  100. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +3 -0
  103. package/crates/team-agent/src/lifecycle/tests.rs +2 -2
  104. package/crates/team-agent/src/main.rs +4 -4
  105. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
  106. package/crates/team-agent/src/mcp_server/mod.rs +11 -2
  107. package/crates/team-agent/src/mcp_server/types.rs +14 -3
  108. package/crates/team-agent/src/mcp_server/wire.rs +230 -68
  109. package/crates/team-agent/src/messaging/delivery.rs +20 -18
  110. package/crates/team-agent/src/messaging/helpers.rs +25 -4
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
  112. package/crates/team-agent/src/messaging/mod.rs +2 -3
  113. package/crates/team-agent/src/messaging/selftest.rs +46 -26
  114. package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
  115. package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
  116. package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
  117. package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
  118. package/crates/team-agent/src/messaging/trust.rs +25 -2
  119. package/crates/team-agent/src/messaging/watchers.rs +37 -9
  120. package/crates/team-agent/src/model/enums.rs +65 -16
  121. package/crates/team-agent/src/model/ids.rs +12 -3
  122. package/crates/team-agent/src/model/paths.rs +28 -7
  123. package/crates/team-agent/src/model/permissions.rs +176 -33
  124. package/crates/team-agent/src/model/routing.rs +66 -20
  125. package/crates/team-agent/src/model/spec.rs +365 -69
  126. package/crates/team-agent/src/model/task_graph.rs +36 -9
  127. package/crates/team-agent/src/model/yaml/tests.rs +24 -6
  128. package/crates/team-agent/src/model/yaml.rs +7 -6
  129. package/crates/team-agent/src/packaging/install.rs +23 -9
  130. package/crates/team-agent/src/packaging/migrate.rs +5 -7
  131. package/crates/team-agent/src/packaging/mod.rs +9 -1
  132. package/crates/team-agent/src/packaging/repair.rs +13 -6
  133. package/crates/team-agent/src/packaging/tests.rs +63 -16
  134. package/crates/team-agent/src/packaging/types.rs +22 -7
  135. package/crates/team-agent/src/platform/argv.rs +4 -1
  136. package/crates/team-agent/src/platform/file_lock.rs +22 -8
  137. package/crates/team-agent/src/platform/process.rs +54 -24
  138. package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
  139. package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
  140. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
  141. package/crates/team-agent/src/provider/classify.rs +127 -42
  142. package/crates/team-agent/src/provider/faults.rs +17 -5
  143. package/crates/team-agent/src/provider/helpers.rs +6 -5
  144. package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
  145. package/crates/team-agent/src/state/persist.rs +28 -0
  146. package/crates/team-agent/src/state/repository.rs +8 -3
  147. package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
  148. package/crates/team-agent/src/tmux_backend.rs +80 -44
  149. package/crates/team-agent/src/topology.rs +40 -20
  150. package/crates/team-agent/src/transport/test_support.rs +20 -23
  151. package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
  152. package/crates/team-agent/src/transport/tests/mod.rs +178 -187
  153. package/crates/team-agent/src/transport/tests/wire.rs +561 -525
  154. package/crates/team-agent/src/transport.rs +12 -17
  155. package/crates/team-agent/src/transport_factory.rs +29 -14
  156. package/package.json +4 -4
package/Cargo.lock CHANGED
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "team-agent"
578
- version = "0.5.42"
578
+ version = "0.5.44"
579
579
  dependencies = [
580
580
  "anyhow",
581
581
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.42"
12
+ version = "0.5.44"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -80,14 +80,15 @@ pub fn cmd_init(args: &InitArgs) -> Result<CmdResult, CliError> {
80
80
  if args.force || !state_path.exists() {
81
81
  std::fs::write(&state_path, INIT_STATE_TEMPLATE)?;
82
82
  }
83
- crate::event_log::EventLog::new(&args.workspace).write(
84
- "init",
85
- json!({
86
- "spec_path": spec_path.to_string_lossy().to_string(),
87
- "state_path": state_path.to_string_lossy().to_string(),
88
- }),
89
- )
90
- .map_err(|e| CliError::Runtime(e.to_string()))?;
83
+ crate::event_log::EventLog::new(&args.workspace)
84
+ .write(
85
+ "init",
86
+ json!({
87
+ "spec_path": spec_path.to_string_lossy().to_string(),
88
+ "state_path": state_path.to_string_lossy().to_string(),
89
+ }),
90
+ )
91
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
91
92
  Ok(CmdResult::from_json(
92
93
  json!({
93
94
  "ok": true,
@@ -122,7 +123,10 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
122
123
  .and_then(|readiness| readiness.get("ready"))
123
124
  .and_then(Value::as_bool)
124
125
  .unwrap_or(true);
125
- let status = value.get("status").and_then(Value::as_str).map(str::to_string);
126
+ let status = value
127
+ .get("status")
128
+ .and_then(Value::as_str)
129
+ .map(str::to_string);
126
130
  if args.json
127
131
  || value.get("ok").and_then(Value::as_bool) == Some(false)
128
132
  || session_capture_incomplete
@@ -173,8 +177,8 @@ fn append_reminder(text: String, reminder: &str) -> String {
173
177
 
174
178
  /// `cmd_compile`(`commands.py:42`)。
175
179
  pub fn cmd_compile(args: &CompileArgs) -> Result<CmdResult, CliError> {
176
- let spec = crate::compiler::compile_team(&args.team)
177
- .map_err(|e| CliError::Runtime(e.to_string()))?;
180
+ let spec =
181
+ crate::compiler::compile_team(&args.team).map_err(|e| CliError::Runtime(e.to_string()))?;
178
182
  warn_ignored_owner_team_id(&args.team);
179
183
  std::fs::write(&args.out, crate::model::yaml::dumps(&spec))?;
180
184
  Ok(CmdResult::from_json(
@@ -194,7 +198,10 @@ fn warn_ignored_owner_team_id(team_dir: &std::path::Path) {
194
198
  };
195
199
  let workspace = crate::model::paths::team_workspace(team_dir)
196
200
  .unwrap_or_else(|_| team_dir.parent().unwrap_or(team_dir).to_path_buf());
197
- eprintln!("Warning: ignored TEAM.md {}={}", ignored.field, ignored.value);
201
+ eprintln!(
202
+ "Warning: ignored TEAM.md {}={}",
203
+ ignored.field, ignored.value
204
+ );
198
205
  eprintln!("Reason: owner identity is the canonical runtime team key, not TEAM.md front matter");
199
206
  eprintln!("Action: remove {} from TEAM.md", ignored.field);
200
207
  let fields = json!({
@@ -205,7 +212,9 @@ fn warn_ignored_owner_team_id(team_dir: &std::path::Path) {
205
212
  "reason": "owner identity is derived from the canonical runtime team key",
206
213
  "action": "remove owner_team_id from TEAM.md",
207
214
  });
208
- if let Err(err) = crate::event_log::EventLog::new(&workspace).write("spec.field_ignored", fields) {
215
+ if let Err(err) =
216
+ crate::event_log::EventLog::new(&workspace).write("spec.field_ignored", fields)
217
+ {
209
218
  eprintln!("Warning: spec.field_ignored event write failed: {err}");
210
219
  }
211
220
  }
@@ -362,17 +371,17 @@ pub fn cmd_watch(args: &WatchArgs) -> Result<CmdResult, CliError> {
362
371
  }
363
372
  #[cfg(test)]
364
373
  {
365
- let store = crate::message_store::MessageStore::open(&args.workspace)
374
+ let store = crate::message_store::MessageStore::open(&args.workspace)
375
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
376
+ let mut cursor = crate::coordinator::WatchCursor::default();
377
+ let lines = crate::coordinator::collect_watch_lines(
378
+ &workspace,
379
+ &mut cursor,
380
+ &store,
381
+ args.team.as_deref(),
382
+ )
366
383
  .map_err(|e| CliError::Runtime(e.to_string()))?;
367
- let mut cursor = crate::coordinator::WatchCursor::default();
368
- let lines = crate::coordinator::collect_watch_lines(
369
- &workspace,
370
- &mut cursor,
371
- &store,
372
- args.team.as_deref(),
373
- )
374
- .map_err(|e| CliError::Runtime(e.to_string()))?;
375
- Ok(CmdResult::human(lines.join("\n")))
384
+ Ok(CmdResult::human(lines.join("\n")))
376
385
  }
377
386
  }
378
387
 
@@ -494,7 +503,11 @@ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
494
503
  )
495
504
  .map_err(|e| CliError::Runtime(e.to_string()))?;
496
505
  let state = selected.state;
497
- let event_log = selected.run_workspace.join(".team").join("logs").join("events.jsonl");
506
+ let event_log = selected
507
+ .run_workspace
508
+ .join(".team")
509
+ .join("logs")
510
+ .join("events.jsonl");
498
511
  // 0.5.x Phase 1d Batch 3: factory-routed diagnose backend so
499
512
  // conpty teams get accurate `has_session` / capture from the shim
500
513
  // rather than a workspace tmux fallback (which would always miss
@@ -537,19 +550,13 @@ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
537
550
  /// `cmd_preflight`(`parser.py:160`)。
538
551
  pub fn cmd_preflight(args: &PreflightArgs) -> Result<CmdResult, CliError> {
539
552
  let report = build_preflight_report(&args.team)?;
540
- Ok(CmdResult::from_json(
541
- report,
542
- args.json,
543
- ))
553
+ Ok(CmdResult::from_json(report, args.json))
544
554
  }
545
555
 
546
556
  /// `cmd_wait_ready`(`parser.py:171`)。
547
557
  pub fn cmd_wait_ready(args: &WaitReadyArgs) -> Result<CmdResult, CliError> {
548
558
  let report = build_wait_ready_report(&args.workspace, args.timeout, args.team.as_deref())?;
549
- Ok(CmdResult::from_json(
550
- report,
551
- args.json,
552
- ))
559
+ Ok(CmdResult::from_json(report, args.json))
553
560
  }
554
561
 
555
562
  /// `cmd_e2e`(`parser.py:449`)。
@@ -579,10 +586,15 @@ pub fn cmd_e2e(args: &E2eArgs) -> Result<CmdResult, CliError> {
579
586
  /// `cmd_peek`(`commands.py:118`)。
580
587
  pub fn cmd_peek(args: &PeekArgs) -> Result<CmdResult, CliError> {
581
588
  if !args.allow_raw_screen {
582
- return Err(CliError::Usage("peek requires --allow-raw-screen".to_string()));
589
+ return Err(CliError::Usage(
590
+ "peek requires --allow-raw-screen".to_string(),
591
+ ));
583
592
  }
584
593
  let state = crate::state::persist::load_runtime_state(&args.workspace)?;
585
- let Some(agent_state) = state.get("agents").and_then(|agents| agents.get(&args.agent)) else {
594
+ let Some(agent_state) = state
595
+ .get("agents")
596
+ .and_then(|agents| agents.get(&args.agent))
597
+ else {
586
598
  return Ok(CmdResult::from_json(
587
599
  json!({
588
600
  "ok": false,
@@ -661,13 +673,29 @@ fn peek_unavailable(agent: &str, json: bool) -> CmdResult {
661
673
  )
662
674
  }
663
675
 
664
- fn agent_pane_id(state: &Value, agent: &str, agent_state: &Value) -> Option<(String, String, String)> {
665
- let session = state.get("session_name").and_then(Value::as_str).filter(|s| !s.is_empty())?;
676
+ fn agent_pane_id(
677
+ state: &Value,
678
+ agent: &str,
679
+ agent_state: &Value,
680
+ ) -> Option<(String, String, String)> {
681
+ let session = state
682
+ .get("session_name")
683
+ .and_then(Value::as_str)
684
+ .filter(|s| !s.is_empty())?;
666
685
  let window = ["window", "window_name"]
667
686
  .iter()
668
- .find_map(|key| agent_state.get(*key).and_then(Value::as_str).filter(|s| !s.is_empty()))
687
+ .find_map(|key| {
688
+ agent_state
689
+ .get(*key)
690
+ .and_then(Value::as_str)
691
+ .filter(|s| !s.is_empty())
692
+ })
669
693
  .unwrap_or(agent);
670
- Some((session.to_string(), window.to_string(), format!("{session}:{window}")))
694
+ Some((
695
+ session.to_string(),
696
+ window.to_string(),
697
+ format!("{session}:{window}"),
698
+ ))
671
699
  }
672
700
 
673
701
  fn run_fake_e2e(workspace: &Path) -> Result<Value, CliError> {
@@ -787,7 +815,10 @@ fn command_on_path(command: &str) -> bool {
787
815
  }
788
816
 
789
817
  fn command_version(command: &str) -> Value {
790
- match std::process::Command::new(command).arg("--version").output() {
818
+ match std::process::Command::new(command)
819
+ .arg("--version")
820
+ .output()
821
+ {
791
822
  Ok(output) if output.status.success() => {
792
823
  let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
793
824
  if text.is_empty() {
@@ -959,7 +990,10 @@ tasks:
959
990
 
960
991
  fn sessions_overview(state: &Value, spec: Option<&crate::model::yaml::Value>) -> Value {
961
992
  let mut rows = Vec::new();
962
- if let Some(agents) = spec.and_then(|v| v.get("agents")).and_then(crate::model::yaml::Value::as_list) {
993
+ if let Some(agents) = spec
994
+ .and_then(|v| v.get("agents"))
995
+ .and_then(crate::model::yaml::Value::as_list)
996
+ {
963
997
  for agent in agents {
964
998
  let Some(agent_id) = agent.get("id").and_then(crate::model::yaml::Value::as_str) else {
965
999
  continue;
@@ -1015,7 +1049,10 @@ fn session_row(
1015
1049
  }
1016
1050
 
1017
1051
  fn yaml_str(value: &crate::model::yaml::Value, key: &str) -> Option<String> {
1018
- value.get(key).and_then(crate::model::yaml::Value::as_str).map(ToString::to_string)
1052
+ value
1053
+ .get(key)
1054
+ .and_then(crate::model::yaml::Value::as_str)
1055
+ .map(ToString::to_string)
1019
1056
  }
1020
1057
 
1021
1058
  fn field_or_null(value: &Value, keys: &[&str]) -> Value {
@@ -1041,7 +1078,11 @@ fn last_task_for_agent(state: &Value, agent_id: &str) -> Value {
1041
1078
  .rev()
1042
1079
  .find(|task| task.get("assignee").and_then(Value::as_str) == Some(agent_id))
1043
1080
  })
1044
- .and_then(|task| task.get("id").and_then(Value::as_str).map(ToString::to_string))
1081
+ .and_then(|task| {
1082
+ task.get("id")
1083
+ .and_then(Value::as_str)
1084
+ .map(ToString::to_string)
1085
+ })
1045
1086
  .map(Value::String)
1046
1087
  .unwrap_or(Value::Null)
1047
1088
  }
@@ -1067,8 +1108,10 @@ fn window_target(agent_state: &Value, agent_id: &str) -> Value {
1067
1108
  }
1068
1109
  }
1069
1110
 
1070
-
1071
- fn load_team_spec_optional(workspace: &Path, state: &Value) -> Result<Option<crate::model::yaml::Value>, CliError> {
1111
+ fn load_team_spec_optional(
1112
+ workspace: &Path,
1113
+ state: &Value,
1114
+ ) -> Result<Option<crate::model::yaml::Value>, CliError> {
1072
1115
  let spec_path = state
1073
1116
  .get("spec_path")
1074
1117
  .and_then(Value::as_str)
@@ -1196,7 +1239,10 @@ fn format_inbox_human(
1196
1239
  Ok(lines.join("\n"))
1197
1240
  }
1198
1241
 
1199
- fn uncollected_result_count(workspace: &Path, owner_team_id: Option<&str>) -> Result<i64, CliError> {
1242
+ fn uncollected_result_count(
1243
+ workspace: &Path,
1244
+ owner_team_id: Option<&str>,
1245
+ ) -> Result<i64, CliError> {
1200
1246
  let store = crate::message_store::MessageStore::open(workspace)
1201
1247
  .map_err(|e| CliError::Runtime(e.to_string()))?;
1202
1248
  let conn = crate::db::schema::open_db(store.db_path())
@@ -1401,10 +1447,16 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
1401
1447
  ));
1402
1448
  }
1403
1449
  if args.comms || matches!(args.gate, Some(DoctorGate::Comms)) {
1404
- let value = crate::diagnose::comms::doctor_comms_json(&args.workspace, args.team.as_deref(), Some("comms"))?;
1450
+ let value = crate::diagnose::comms::doctor_comms_json(
1451
+ &args.workspace,
1452
+ args.team.as_deref(),
1453
+ Some("comms"),
1454
+ )?;
1405
1455
  if !args.json {
1406
1456
  let json_tail = serde_json::to_string_pretty(&sort_json(&value))?;
1407
- return Ok(CmdResult::human(format!("{COMMS_BOUNDARY_TEXT}\n{json_tail}")));
1457
+ return Ok(CmdResult::human(format!(
1458
+ "{COMMS_BOUNDARY_TEXT}\n{json_tail}"
1459
+ )));
1408
1460
  }
1409
1461
  return Ok(CmdResult::from_json(value, true));
1410
1462
  }
@@ -1439,9 +1491,18 @@ mod tests {
1439
1491
  });
1440
1492
  let out = quickstart_human(&value);
1441
1493
  assert!(out.contains("team started"), "must keep summary; got {out}");
1442
- assert!(out.contains("attach:"), "must render attach block; got {out}");
1443
- assert!(out.contains("team-y:w1") && out.contains("team-y:w2"), "must list each attach cmd; got {out}");
1444
- assert!(out.ends_with(crate::cli::QUICK_START_REMINDER), "must append harness reminder; got {out}");
1494
+ assert!(
1495
+ out.contains("attach:"),
1496
+ "must render attach block; got {out}"
1497
+ );
1498
+ assert!(
1499
+ out.contains("team-y:w1") && out.contains("team-y:w2"),
1500
+ "must list each attach cmd; got {out}"
1501
+ );
1502
+ assert!(
1503
+ out.ends_with(crate::cli::QUICK_START_REMINDER),
1504
+ "must append harness reminder; got {out}"
1505
+ );
1445
1506
  }
1446
1507
 
1447
1508
  #[test]
@@ -1467,11 +1528,18 @@ mod tests {
1467
1528
  "w1": {"pane_id": "%7", "window": "w1"}
1468
1529
  }
1469
1530
  });
1470
- let agent = state.get("agents").and_then(|agents| agents.get("w1")).unwrap();
1531
+ let agent = state
1532
+ .get("agents")
1533
+ .and_then(|agents| agents.get("w1"))
1534
+ .unwrap();
1471
1535
 
1472
1536
  assert_eq!(
1473
1537
  agent_pane_id(&state, "w1", agent).unwrap(),
1474
- ("team-x".to_string(), "w1".to_string(), "team-x:w1".to_string())
1538
+ (
1539
+ "team-x".to_string(),
1540
+ "w1".to_string(),
1541
+ "team-x:w1".to_string()
1542
+ )
1475
1543
  );
1476
1544
  }
1477
1545
 
@@ -1483,11 +1551,18 @@ mod tests {
1483
1551
  "w1": {"window": "worker-one"}
1484
1552
  }
1485
1553
  });
1486
- let agent = state.get("agents").and_then(|agents| agents.get("w1")).unwrap();
1554
+ let agent = state
1555
+ .get("agents")
1556
+ .and_then(|agents| agents.get("w1"))
1557
+ .unwrap();
1487
1558
 
1488
1559
  assert_eq!(
1489
1560
  agent_pane_id(&state, "w1", agent).unwrap(),
1490
- ("team-x".to_string(), "worker-one".to_string(), "team-x:worker-one".to_string())
1561
+ (
1562
+ "team-x".to_string(),
1563
+ "worker-one".to_string(),
1564
+ "team-x:worker-one".to_string()
1565
+ )
1491
1566
  );
1492
1567
  }
1493
1568
  }
@@ -57,8 +57,7 @@ pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value
57
57
  let error_str = error.to_string();
58
58
  let issue_id = classify_tmux_server_error(&error_str);
59
59
  issues.push(json!(issue_id));
60
- let mut hint =
61
- recovery_hint(session_name, issue_id, "team-agent diagnose");
60
+ let mut hint = recovery_hint(session_name, issue_id, "team-agent diagnose");
62
61
  if let Some(obj) = hint.as_object_mut() {
63
62
  obj.insert("reason".to_string(), Value::String(error_str));
64
63
  }
@@ -1673,13 +1673,7 @@ mod tests {
1673
1673
  fn hidden_commands_not_in_default_help() {
1674
1674
  let top_help = command_help(None);
1675
1675
  let visible = visible_help_commands(&top_help);
1676
- for command in [
1677
- "leaders",
1678
- "doctor",
1679
- "e2e",
1680
- "peek",
1681
- "coordinator",
1682
- ] {
1676
+ for command in ["leaders", "doctor", "e2e", "peek", "coordinator"] {
1683
1677
  assert!(
1684
1678
  !visible.iter().any(|visible| visible == command),
1685
1679
  "`{command}` must stay hidden from default help"
@@ -30,7 +30,9 @@ pub(crate) fn python_truthy(value: &Value) -> bool {
30
30
  match value {
31
31
  Value::Null => false,
32
32
  Value::Bool(b) => *b,
33
- Value::Number(n) => n.as_i64().map(|v| v != 0)
33
+ Value::Number(n) => n
34
+ .as_i64()
35
+ .map(|v| v != 0)
34
36
  .or_else(|| n.as_u64().map(|v| v != 0))
35
37
  .or_else(|| n.as_f64().map(|v| v != 0.0))
36
38
  .unwrap_or(false),
@@ -30,7 +30,8 @@ pub fn leader_launcher_args(values: &[String]) -> Result<LeaderLauncherArgs, Cli
30
30
  )));
31
31
  }
32
32
  }
33
- out.provider_args.extend(values.iter().skip(idx + 1).cloned());
33
+ out.provider_args
34
+ .extend(values.iter().skip(idx + 1).cloned());
34
35
  break;
35
36
  } else if token == "--attach" || token == "--attach-existing" {
36
37
  out.attach_existing = true;
@@ -3347,8 +3347,7 @@ pub mod lifecycle_port {
3347
3347
  let repair_team = team
3348
3348
  .filter(|team| !team.is_empty())
3349
3349
  .unwrap_or(session_name.as_str());
3350
- let shutdown_scoped =
3351
- format!("team-agent shutdown --team {repair_team}");
3350
+ let shutdown_scoped = format!("team-agent shutdown --team {repair_team}");
3352
3351
  json!({
3353
3352
  "ok": false,
3354
3353
  "status": "refused_build_before_destroy",
@@ -4101,7 +4100,7 @@ pub mod leader_port {
4101
4100
  "action": "rerun with --confirm to claim ownership of this team",
4102
4101
  }));
4103
4102
  }
4104
- let state = crate::state::persist::load_runtime_state(workspace)
4103
+ let state = crate::state::persist::load_runtime_state_without_migrations(workspace)
4105
4104
  .map_err(|e| CliError::Runtime(e.to_string()))?;
4106
4105
  let Some(team_id) = resolve_owner_team_id(&state, team) else {
4107
4106
  return Ok(json!({
@@ -4113,8 +4112,7 @@ pub mod leader_port {
4113
4112
  }));
4114
4113
  };
4115
4114
  let requested_team = team;
4116
- let explicit_team = requested_team.filter(|team| !team.is_empty());
4117
- let resolved_team = explicit_team.map(|_| team_id.as_str().to_string());
4115
+ let resolved_team = Some(team_id.as_str().to_string());
4118
4116
  if !positive_caller_pane_env_present() {
4119
4117
  let bind = crate::leader::bind_owner_from_caller_pane(workspace, &team_id, None)
4120
4118
  .map_err(|e| CliError::Runtime(e.to_string()))?;
@@ -4125,9 +4123,21 @@ pub mod leader_port {
4125
4123
  .map_err(|e| CliError::Runtime(e.to_string()))?;
4126
4124
  let mut value = lease_value(result);
4127
4125
  insert_resolved_team(&mut value, requested_team, resolved_team.as_deref());
4126
+ append_team_session_ready(workspace, resolved_team.as_deref(), &mut value);
4128
4127
  if value.get("ok").and_then(Value::as_bool) == Some(true) {
4129
- emit_topology_convergence_event(workspace, resolved_team.as_deref(), "takeover", &value);
4130
- register_after_binding_success(workspace, resolved_team.as_deref(), "takeover", &mut value);
4128
+ emit_topology_convergence_event(
4129
+ workspace,
4130
+ resolved_team.as_deref(),
4131
+ "takeover",
4132
+ &value,
4133
+ );
4134
+ register_after_binding_success(
4135
+ workspace,
4136
+ resolved_team.as_deref(),
4137
+ "takeover",
4138
+ &mut value,
4139
+ );
4140
+ restore_non_target_teams(workspace, &state, resolved_team.as_deref());
4131
4141
  }
4132
4142
  Ok(value)
4133
4143
  }
@@ -4137,7 +4147,7 @@ pub mod leader_port {
4137
4147
  team: Option<&str>,
4138
4148
  confirm: bool,
4139
4149
  ) -> Result<Value, CliError> {
4140
- let state = crate::state::persist::load_runtime_state(workspace)
4150
+ let state = crate::state::persist::load_runtime_state_without_migrations(workspace)
4141
4151
  .map_err(|e| CliError::Runtime(e.to_string()))?;
4142
4152
  let Some(team_id) = resolve_owner_team_id(&state, team) else {
4143
4153
  return Ok(json!({
@@ -4156,15 +4166,26 @@ pub mod leader_port {
4156
4166
  }
4157
4167
  return Ok(owner_bind_value(bind));
4158
4168
  }
4159
- let explicit_team = team.filter(|team| !team.is_empty());
4160
- let resolved_team = explicit_team.map(|_| team_id.as_str().to_string());
4169
+ let resolved_team = Some(team_id.as_str().to_string());
4161
4170
  let result = crate::leader::claim_leader(workspace, resolved_team.as_deref(), confirm)
4162
4171
  .map_err(|e| CliError::Runtime(e.to_string()))?;
4163
4172
  let mut value = lease_value(result);
4164
4173
  insert_resolved_team(&mut value, team, resolved_team.as_deref());
4174
+ append_team_session_ready(workspace, resolved_team.as_deref(), &mut value);
4165
4175
  if value.get("ok").and_then(Value::as_bool) == Some(true) {
4166
- emit_topology_convergence_event(workspace, resolved_team.as_deref(), "claim-leader", &value);
4167
- register_after_binding_success(workspace, resolved_team.as_deref(), "claim-leader", &mut value);
4176
+ emit_topology_convergence_event(
4177
+ workspace,
4178
+ resolved_team.as_deref(),
4179
+ "claim-leader",
4180
+ &value,
4181
+ );
4182
+ register_after_binding_success(
4183
+ workspace,
4184
+ resolved_team.as_deref(),
4185
+ "claim-leader",
4186
+ &mut value,
4187
+ );
4188
+ restore_non_target_teams(workspace, &state, resolved_team.as_deref());
4168
4189
  }
4169
4190
  Ok(value)
4170
4191
  }
@@ -4260,6 +4281,7 @@ pub mod leader_port {
4260
4281
  "owner_epoch": convergence.get("owner_epoch").cloned().unwrap_or(Value::Null),
4261
4282
  "persisted": convergence.get("persisted").cloned().unwrap_or(Value::Bool(false)),
4262
4283
  "checked_paths": convergence.get("checked_paths").cloned().unwrap_or_else(|| json!([])),
4284
+ "team_session_ready": response.get("team_session_ready").cloned().unwrap_or(Value::Null),
4263
4285
  }),
4264
4286
  );
4265
4287
  }
@@ -4325,14 +4347,102 @@ pub mod leader_port {
4325
4347
  | crate::state::projection::OwnerTeamResolution::Ambiguous { .. } => None,
4326
4348
  }
4327
4349
  }
4328
- None => Some(TeamKey::new(crate::state::projection::team_state_key(
4329
- state,
4330
- ))),
4350
+ None => Some(TeamKey::new(active_or_derived_team_key(state))),
4351
+ }
4352
+ }
4353
+
4354
+ fn active_or_derived_team_key(state: &Value) -> String {
4355
+ state
4356
+ .get("active_team_key")
4357
+ .and_then(Value::as_str)
4358
+ .filter(|team| !team.is_empty())
4359
+ .map(str::to_string)
4360
+ .unwrap_or_else(|| crate::state::projection::team_state_key(state))
4361
+ }
4362
+
4363
+ fn append_team_session_ready(workspace: &Path, team: Option<&str>, response: &mut Value) {
4364
+ let Some(obj) = response.as_object_mut() else {
4365
+ return;
4366
+ };
4367
+ let Some(convergence) = obj.get("topology_convergence") else {
4368
+ return;
4369
+ };
4370
+ if convergence.get("status").and_then(Value::as_str) != Some("converged") {
4371
+ return;
4372
+ }
4373
+ let Some(endpoint) = convergence
4374
+ .get("new_tmux_endpoint")
4375
+ .and_then(Value::as_str)
4376
+ .filter(|endpoint| !endpoint.is_empty())
4377
+ else {
4378
+ obj.insert("team_session_ready".to_string(), Value::Null);
4379
+ return;
4380
+ };
4381
+ let session_name = crate::state::persist::load_runtime_state_without_migrations(workspace)
4382
+ .ok()
4383
+ .and_then(|state| target_team_session_name(&state, team));
4384
+ let ready = session_name
4385
+ .as_deref()
4386
+ .and_then(|session| crate::topology::team_session_ready_on_endpoint(endpoint, session));
4387
+ obj.insert(
4388
+ "team_session_ready".to_string(),
4389
+ ready.map(Value::Bool).unwrap_or(Value::Null),
4390
+ );
4391
+ }
4392
+
4393
+ fn target_team_session_name(state: &Value, team: Option<&str>) -> Option<String> {
4394
+ team.and_then(|team| {
4395
+ state
4396
+ .get("teams")
4397
+ .and_then(Value::as_object)
4398
+ .and_then(|teams| teams.get(team))
4399
+ })
4400
+ .and_then(|team_state| team_state.get("session_name"))
4401
+ .and_then(Value::as_str)
4402
+ .filter(|session| !session.is_empty())
4403
+ .or_else(|| state.get("session_name").and_then(Value::as_str))
4404
+ .filter(|session| !session.is_empty())
4405
+ .map(str::to_string)
4406
+ }
4407
+
4408
+ fn restore_non_target_teams(workspace: &Path, before: &Value, target: Option<&str>) {
4409
+ let Some(target) = target.filter(|team| !team.is_empty()) else {
4410
+ return;
4411
+ };
4412
+ let Some(before_teams) = before.get("teams").and_then(Value::as_object) else {
4413
+ return;
4414
+ };
4415
+ let Ok(mut latest) =
4416
+ crate::state::persist::load_runtime_state_without_migrations(workspace)
4417
+ else {
4418
+ return;
4419
+ };
4420
+ let Some(latest_teams) = latest.get_mut("teams").and_then(Value::as_object_mut) else {
4421
+ return;
4422
+ };
4423
+ let mut changed = false;
4424
+ for (team, before_entry) in before_teams {
4425
+ if team == target {
4426
+ continue;
4427
+ }
4428
+ if latest_teams.get(team) != Some(before_entry) {
4429
+ latest_teams.insert(team.clone(), before_entry.clone());
4430
+ changed = true;
4431
+ }
4432
+ }
4433
+ if changed {
4434
+ let _ = crate::state::repository::StateRepository::new(workspace).save(
4435
+ crate::state::repository::StateWriteIntent::LeaderBindingRestoreNonTargetTeams {
4436
+ target_team_key: target,
4437
+ },
4438
+ &latest,
4439
+ );
4331
4440
  }
4332
4441
  }
4333
4442
 
4334
4443
  fn insert_resolved_team(value: &mut Value, requested: Option<&str>, resolved: Option<&str>) {
4335
- let (Some(requested), Some(resolved)) = (requested.filter(|team| !team.is_empty()), resolved)
4444
+ let (Some(requested), Some(resolved)) =
4445
+ (requested.filter(|team| !team.is_empty()), resolved)
4336
4446
  else {
4337
4447
  return;
4338
4448
  };
@@ -97,9 +97,15 @@ impl NamedAddressError {
97
97
  obj.insert("error".to_string(), Value::String(self.message.clone()));
98
98
  obj.insert("action".to_string(), Value::String(self.action.clone()));
99
99
  obj.insert("log".to_string(), Value::String(self.log.clone()));
100
- obj.insert("candidates".to_string(), Value::Array(self.candidates.clone()));
100
+ obj.insert(
101
+ "candidates".to_string(),
102
+ Value::Array(self.candidates.clone()),
103
+ );
101
104
  if let Some(requested) = self.requested_team.as_deref() {
102
- obj.insert("requested_team".to_string(), Value::String(requested.to_string()));
105
+ obj.insert(
106
+ "requested_team".to_string(),
107
+ Value::String(requested.to_string()),
108
+ );
103
109
  }
104
110
  if !self.available_team_keys.is_empty() {
105
111
  obj.insert(
@@ -113,7 +119,10 @@ impl NamedAddressError {
113
119
  );
114
120
  }
115
121
  if let Some(suggested) = self.suggested_name.as_deref() {
116
- obj.insert("suggested_name".to_string(), Value::String(suggested.to_string()));
122
+ obj.insert(
123
+ "suggested_name".to_string(),
124
+ Value::String(suggested.to_string()),
125
+ );
117
126
  }
118
127
  Value::Object(obj)
119
128
  }
@@ -662,8 +671,8 @@ fn resolve_leader(
662
671
  // silently reused the `current` team's receiver and reported a stale
663
672
  // pane_id). Legacy single-team compat is preserved only when the state
664
673
  // has no `teams` map yet and the requested key equals `active_team_key`.
665
- let receiver = resolve_team_scoped_leader_receiver(state, team, target_workspace)?
666
- .ok_or_else(|| {
674
+ let receiver =
675
+ resolve_team_scoped_leader_receiver(state, team, target_workspace)?.ok_or_else(|| {
667
676
  let mut err = leader_not_attached_third_party(
668
677
  target_workspace,
669
678
  team,