@team-agent/installer 0.5.60 → 0.5.62

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 (265) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +49 -3
  4. package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +184 -0
  6. package/crates/team-agent/src/cli/emit.rs +104 -21
  7. package/crates/team-agent/src/cli/helpers.rs +1 -0
  8. package/crates/team-agent/src/cli/leader.rs +14 -2
  9. package/crates/team-agent/src/cli/leaders.rs +2 -0
  10. package/crates/team-agent/src/cli/mod.rs +296 -8
  11. package/crates/team-agent/src/cli/named_address.rs +26 -2
  12. package/crates/team-agent/src/cli/profile.rs +2 -1
  13. package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
  14. package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
  15. package/crates/team-agent/src/cli/send/persist.rs +1 -0
  16. package/crates/team-agent/src/cli/send/presentation.rs +3 -0
  17. package/crates/team-agent/src/cli/send/resolve.rs +206 -1
  18. package/crates/team-agent/src/cli/send.rs +2 -1
  19. package/crates/team-agent/src/cli/spec.rs +5 -1
  20. package/crates/team-agent/src/cli/status.rs +2 -1
  21. package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
  22. package/crates/team-agent/src/cli/status_port/format.rs +1 -0
  23. package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
  24. package/crates/team-agent/src/cli/status_port/store.rs +7 -3
  25. package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
  27. package/crates/team-agent/src/cli/types.rs +36 -2
  28. package/crates/team-agent/src/communication_mode/mod.rs +54 -0
  29. package/crates/team-agent/src/compiler/tests.rs +5 -5
  30. package/crates/team-agent/src/compiler.rs +54 -1
  31. package/crates/team-agent/src/conpty/backend.rs +1 -0
  32. package/crates/team-agent/src/conpty/mod.rs +1 -0
  33. package/crates/team-agent/src/coordinator/backoff.rs +5 -8
  34. package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
  35. package/crates/team-agent/src/coordinator/health.rs +1 -0
  36. package/crates/team-agent/src/coordinator/mod.rs +2 -2
  37. package/crates/team-agent/src/coordinator/orphan.rs +4 -116
  38. package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
  39. package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
  40. package/crates/team-agent/src/coordinator/steps/abnormal.rs +6 -5
  41. package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
  42. package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
  43. package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
  44. package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
  45. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
  46. package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
  47. package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
  48. package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +0 -9
  49. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
  50. package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
  51. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -42
  52. package/crates/team-agent/src/coordinator/tick.rs +24 -28
  53. package/crates/team-agent/src/coordinator/types.rs +3 -176
  54. package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
  55. package/crates/team-agent/src/db/message_store.rs +47 -2
  56. package/crates/team-agent/src/db/migration.rs +5 -4
  57. package/crates/team-agent/src/db/mod.rs +1 -0
  58. package/crates/team-agent/src/db/schema.rs +1 -0
  59. package/crates/team-agent/src/diagnose/comms.rs +2 -1
  60. package/crates/team-agent/src/diagnose/mod.rs +1 -0
  61. package/crates/team-agent/src/diagnose/orphans.rs +1 -0
  62. package/crates/team-agent/src/fake_worker.rs +21 -1
  63. package/crates/team-agent/src/layout/manager.rs +3 -7
  64. package/crates/team-agent/src/layout/mod.rs +2 -2
  65. package/crates/team-agent/src/layout/overlay.rs +6 -1
  66. package/crates/team-agent/src/layout/placement.rs +1 -0
  67. package/crates/team-agent/src/layout/recovery.rs +3 -0
  68. package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
  69. package/crates/team-agent/src/layout/sessions.rs +8 -1
  70. package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
  71. package/crates/team-agent/src/layout/worker_env.rs +3 -0
  72. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  73. package/crates/team-agent/src/leader/helpers.rs +1 -0
  74. package/crates/team-agent/src/leader/incident.rs +1 -0
  75. package/crates/team-agent/src/leader/lease.rs +27 -22
  76. package/crates/team-agent/src/leader/mod.rs +2 -3
  77. package/crates/team-agent/src/leader/owner_bind.rs +13 -3
  78. package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
  79. package/crates/team-agent/src/leader/rediscover.rs +4 -17
  80. package/crates/team-agent/src/leader/registry.rs +3 -15
  81. package/crates/team-agent/src/leader/start.rs +1072 -186
  82. package/crates/team-agent/src/leader/takeover.rs +10 -232
  83. package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
  84. package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
  85. package/crates/team-agent/src/leader/tests/identity.rs +71 -59
  86. package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
  87. package/crates/team-agent/src/leader/tests/idle.rs +0 -193
  88. package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
  89. package/crates/team-agent/src/leader/tests/mod.rs +4 -41
  90. package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
  91. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
  92. package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
  93. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
  94. package/crates/team-agent/src/leader/types.rs +1 -28
  95. package/crates/team-agent/src/lib.rs +7 -0
  96. package/crates/team-agent/src/lifecycle/display.rs +7 -1
  97. package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
  98. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
  99. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
  100. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -1
  101. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
  102. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
  103. package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
  104. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
  105. package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
  106. package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -28
  107. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
  108. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
  109. package/crates/team-agent/src/lifecycle/launch.rs +11 -12
  110. package/crates/team-agent/src/lifecycle/mod.rs +2 -1
  111. package/crates/team-agent/src/lifecycle/restart/agent.rs +4 -4
  112. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  113. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
  114. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
  115. package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
  116. package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
  117. package/crates/team-agent/src/lifecycle/restart.rs +13 -11
  118. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
  119. package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
  120. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
  121. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
  122. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
  123. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
  124. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
  125. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
  126. package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
  127. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
  128. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
  129. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
  130. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
  131. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
  132. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
  133. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
  134. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
  135. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
  136. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
  137. package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
  138. package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
  139. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
  140. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
  141. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
  142. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
  143. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
  144. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
  145. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
  146. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
  147. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
  148. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
  149. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
  150. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
  151. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
  152. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
  153. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
  154. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
  155. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
  156. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
  157. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
  158. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
  159. package/crates/team-agent/src/lifecycle/tests.rs +59 -0
  160. package/crates/team-agent/src/lifecycle/types.rs +0 -9
  161. package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
  162. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  163. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
  164. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
  165. package/crates/team-agent/src/mcp_server/mod.rs +1 -0
  166. package/crates/team-agent/src/mcp_server/normalize.rs +5 -3
  167. package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
  168. package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
  169. package/crates/team-agent/src/mcp_server/tools.rs +101 -66
  170. package/crates/team-agent/src/mcp_server/types.rs +6 -18
  171. package/crates/team-agent/src/mcp_server/wire.rs +32 -26
  172. package/crates/team-agent/src/messaging/activity.rs +1 -0
  173. package/crates/team-agent/src/messaging/address.rs +1 -0
  174. package/crates/team-agent/src/messaging/delivery.rs +109 -41
  175. package/crates/team-agent/src/messaging/helpers.rs +2 -1
  176. package/crates/team-agent/src/messaging/leader_channel.rs +34 -10
  177. package/crates/team-agent/src/messaging/leader_receiver.rs +42 -24
  178. package/crates/team-agent/src/messaging/mod.rs +10 -14
  179. package/crates/team-agent/src/messaging/peers.rs +2 -0
  180. package/crates/team-agent/src/messaging/persist.rs +3 -1
  181. package/crates/team-agent/src/messaging/presentation.rs +111 -1
  182. package/crates/team-agent/src/messaging/results.rs +166 -17
  183. package/crates/team-agent/src/messaging/scheduler.rs +1 -0
  184. package/crates/team-agent/src/messaging/selftest.rs +1 -1
  185. package/crates/team-agent/src/messaging/send.rs +135 -89
  186. package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
  187. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
  188. package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
  189. package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
  190. package/crates/team-agent/src/messaging/types.rs +7 -2
  191. package/crates/team-agent/src/messaging/watchers.rs +24 -13
  192. package/crates/team-agent/src/model/enums.rs +1 -0
  193. package/crates/team-agent/src/model/errors.rs +1 -0
  194. package/crates/team-agent/src/model/ids.rs +1 -0
  195. package/crates/team-agent/src/model/mod.rs +2 -0
  196. package/crates/team-agent/src/model/name_similarity.rs +1 -0
  197. package/crates/team-agent/src/model/pane_authority_refusal.rs +359 -0
  198. package/crates/team-agent/src/model/paths.rs +1 -0
  199. package/crates/team-agent/src/model/permissions.rs +3 -2
  200. package/crates/team-agent/src/model/routing.rs +1 -0
  201. package/crates/team-agent/src/model/spec.rs +17 -0
  202. package/crates/team-agent/src/model/task_graph.rs +1 -0
  203. package/crates/team-agent/src/model/yaml/tests.rs +1 -0
  204. package/crates/team-agent/src/model/yaml.rs +1 -0
  205. package/crates/team-agent/src/packaging/install.rs +9 -147
  206. package/crates/team-agent/src/packaging/migrate.rs +2 -0
  207. package/crates/team-agent/src/packaging/mod.rs +1 -0
  208. package/crates/team-agent/src/packaging/repair.rs +2 -0
  209. package/crates/team-agent/src/packaging/tests.rs +29 -218
  210. package/crates/team-agent/src/packaging/types.rs +8 -15
  211. package/crates/team-agent/src/platform/argv.rs +4 -0
  212. package/crates/team-agent/src/platform/errors.rs +10 -1
  213. package/crates/team-agent/src/platform/file_lock.rs +4 -157
  214. package/crates/team-agent/src/platform/mod.rs +0 -61
  215. package/crates/team-agent/src/platform/process.rs +1 -0
  216. package/crates/team-agent/src/provider/adapter.rs +1 -0
  217. package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
  218. package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
  219. package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
  220. package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
  221. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
  222. package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
  223. package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
  224. package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
  225. package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
  226. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
  227. package/crates/team-agent/src/provider/classify.rs +1 -0
  228. package/crates/team-agent/src/provider/faults.rs +1 -26
  229. package/crates/team-agent/src/provider/helpers.rs +1 -0
  230. package/crates/team-agent/src/provider/mod.rs +1 -1
  231. package/crates/team-agent/src/provider/session/capture.rs +41 -22
  232. package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
  233. package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
  234. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
  235. package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
  236. package/crates/team-agent/src/provider/session/mod.rs +2 -4
  237. package/crates/team-agent/src/provider/session/resume.rs +2 -209
  238. package/crates/team-agent/src/provider/session_scan/claude.rs +93 -3
  239. package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
  240. package/crates/team-agent/src/provider/session_scan/common.rs +24 -7
  241. package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
  242. package/crates/team-agent/src/provider/session_scan.rs +6 -42
  243. package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
  244. package/crates/team-agent/src/provider/types.rs +1 -0
  245. package/crates/team-agent/src/provider/wire.rs +1 -0
  246. package/crates/team-agent/src/state/identity.rs +16 -5
  247. package/crates/team-agent/src/state/identity_keys.rs +1 -0
  248. package/crates/team-agent/src/state/mod.rs +2 -0
  249. package/crates/team-agent/src/state/owner_gate.rs +5 -0
  250. package/crates/team-agent/src/state/ownership.rs +10 -4
  251. package/crates/team-agent/src/state/paths.rs +12 -0
  252. package/crates/team-agent/src/state/persist.rs +9 -3
  253. package/crates/team-agent/src/state/projection.rs +15 -2
  254. package/crates/team-agent/src/state/repository/intent.rs +1 -0
  255. package/crates/team-agent/src/state/repository/tests.rs +1 -0
  256. package/crates/team-agent/src/state/repository.rs +7 -0
  257. package/crates/team-agent/src/state/selector.rs +1 -0
  258. package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
  259. package/crates/team-agent/src/tmux_backend.rs +63 -0
  260. package/crates/team-agent/src/topology.rs +3 -0
  261. package/crates/team-agent/src/transport.rs +15 -0
  262. package/package.json +4 -4
  263. package/skills/team-agent/command-coverage.json +379 -0
  264. package/crates/team-agent/src/leader/inject.rs +0 -33
  265. package/crates/team-agent/src/provider/command.rs +0 -80
@@ -0,0 +1,1083 @@
1
+ //! #252 restart/rebind hotfix contracts.
2
+ //!
3
+ //! Local cargo/black-box contracts for the 0.3.3 hotfix surface. These avoid
4
+ //! real provider subscriptions and pin the externally visible behavior around
5
+ //! restart cwd, MCP owner scope, team-scoped coordinator writeback, pane identity,
6
+ //! unbound leader delivery, SQLite contention, and coordinator shutdown.
7
+
8
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
9
+
10
+ #[path = "../../../tests/support/hermetic.rs"]
11
+ mod hermetic_guard;
12
+ #[allow(dead_code)]
13
+ fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
14
+
15
+ use std::collections::BTreeMap;
16
+ use std::io::Write as _;
17
+ use std::path::{Path, PathBuf};
18
+ use std::process::{Command, Stdio};
19
+ use std::sync::atomic::{AtomicU64, Ordering};
20
+ use std::sync::{Arc, Mutex};
21
+ use std::time::Instant;
22
+
23
+ use serde_json::{json, Value};
24
+ use serial_test::serial;
25
+ use team_agent::coordinator::{Coordinator, ProviderRegistry, WorkspacePath};
26
+ use team_agent::db::schema::{initialize_schema, open_db};
27
+ use team_agent::event_log::EventLog;
28
+ use team_agent::lifecycle::{launch_with_transport_in_workspace, restart_with_transport};
29
+ use team_agent::message_store::MessageStore;
30
+ use team_agent::messaging::delivery::deliver_pending_message;
31
+ use team_agent::model::enums::{AuthMode, Provider};
32
+ use team_agent::provider::{
33
+ AuthHintStatus, CaptureVia, CapturedSession, Confidence, HandledPrompt, McpConfig,
34
+ ProviderAdapter, ProviderCaps, ProviderError, RolloutPath, SessionId, StatusPatterns,
35
+ };
36
+ use team_agent::state::persist::{load_runtime_state, save_runtime_state};
37
+ use team_agent::transport::{
38
+ AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
39
+ InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, PaneLiveness, SessionName,
40
+ SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
41
+ TurnVerification, WindowName,
42
+ };
43
+
44
+ const CURRENT: &str = "current";
45
+ const SUB: &str = "sub";
46
+
47
+ #[test]
48
+ #[ignore = "real-machine: restart/rebind lifecycle gate"]
49
+ #[serial(env)]
50
+ fn restart_resume_spawns_worker_with_team_dir_cwd_matching_launch() {
51
+ let case = HotfixCase::new("restart-cwd");
52
+ case.write_team(CURRENT, "greeter", Provider::Codex);
53
+ case.seed_restart_state(CURRENT, "greeter", "sess-current", "handled");
54
+ case.seed_healthy_coordinator();
55
+ let transport = RecordingTransport::new().with_session_present(false);
56
+
57
+ restart_with_transport(&case.team_dir(CURRENT), false, Some(CURRENT), &transport)
58
+ .expect("restart should resume the worker");
59
+
60
+ let spawn = transport.single_spawn();
61
+ assert_eq!(
62
+ spawn.cwd,
63
+ case.team_dir(CURRENT),
64
+ "RC0: restart/resume must spawn the worker in the same team directory cwd used by first launch. \
65
+ Passing the run workspace root makes Codex detect cwd != session cwd and show the \
66
+ 'Choose working directory to resume this session' prompt. spawn={spawn:?}"
67
+ );
68
+ }
69
+
70
+ #[test]
71
+ #[ignore = "real-machine: restart/rebind lifecycle gate"]
72
+ #[serial(env)]
73
+ fn restart_resets_startup_prompt_state_for_respawn() {
74
+ let case = HotfixCase::new("restart-startup-state");
75
+ case.write_team(CURRENT, "greeter", Provider::Codex);
76
+ case.seed_restart_state(CURRENT, "greeter", "sess-current", "handled");
77
+ case.seed_healthy_coordinator();
78
+ let transport = RecordingTransport::new().with_session_present(false);
79
+
80
+ restart_with_transport(&case.team_dir(CURRENT), false, Some(CURRENT), &transport)
81
+ .expect("restart should resume the worker");
82
+ let state = load_runtime_state(&case.workspace).expect("state after restart");
83
+ let agent = state
84
+ .pointer("/teams/current/agents/greeter")
85
+ .or_else(|| state.pointer("/agents/greeter"))
86
+ .expect("greeter state exists");
87
+ let status = agent.get("startup_prompts").and_then(Value::as_str);
88
+
89
+ assert!(
90
+ !matches!(status, Some("handled" | "complete")),
91
+ "RC0 defense: a respawned worker is a new pane and must be eligible for startup prompt detection. \
92
+ Restart must clear/version stale startup_prompts=handled instead of carrying it into the new process; state={state}"
93
+ );
94
+ }
95
+
96
+ #[test]
97
+ #[ignore = "real-machine: spawns team-agent mcp-server process"]
98
+ #[serial(env)]
99
+ fn report_result_respects_mcp_owner_team_env_when_active_team_differs() {
100
+ let case = HotfixCase::new("mcp-report-owner");
101
+ case.seed_dual_team_state(SUB, false);
102
+ let output = run_mcp_report_result(
103
+ &case.workspace,
104
+ "greeter",
105
+ CURRENT,
106
+ "res-current-env",
107
+ "current team result",
108
+ );
109
+ assert!(
110
+ !output["result"]["isError"].as_bool().unwrap_or(true),
111
+ "RC4 fixture sanity: real MCP tools/call report_result must not be an MCP error; frame={output}"
112
+ );
113
+
114
+ let conn = open_db(&case.db_path()).unwrap();
115
+ let result_owner: Option<String> = conn
116
+ .query_row(
117
+ "select owner_team_id from results where task_id='task-current' and agent_id='greeter' order by created_at desc limit 1",
118
+ [],
119
+ |row| row.get(0),
120
+ )
121
+ .unwrap_or_else(|error| {
122
+ let rows = db_rows(&conn, "results", "result_id, owner_team_id");
123
+ panic!("RC4 fixture: report_result did not persist any greeter/task-current result row: error={error} frame={output} rows={rows:?}");
124
+ });
125
+ let leader_message_owner: Option<String> = conn
126
+ .query_row(
127
+ "select owner_team_id from messages where sender='greeter' and recipient='leader' order by created_at desc limit 1",
128
+ [],
129
+ |row| row.get(0),
130
+ )
131
+ .unwrap_or_else(|error| {
132
+ let rows = db_rows(&conn, "messages", "message_id, owner_team_id, sender, recipient, status");
133
+ panic!("RC4 fixture: report_result did not create expected leader message row: error={error} frame={output} rows={rows:?}");
134
+ });
135
+
136
+ assert_eq!(
137
+ result_owner.as_deref(),
138
+ Some(CURRENT),
139
+ "RC4: MCP report_result must use TEAM_AGENT_OWNER_TEAM_ID=current even when raw state active_team_key=sub; frame={output}"
140
+ );
141
+ assert_eq!(
142
+ leader_message_owner.as_deref(),
143
+ Some(CURRENT),
144
+ "RC4: report_result leader notification message must be queued under current, not active sub"
145
+ );
146
+ }
147
+
148
+ #[test]
149
+ #[serial(env)]
150
+ fn coordinator_tick_saves_captured_session_into_nested_active_team() {
151
+ let case = HotfixCase::new("tick-nested-session");
152
+ case.seed_dual_team_state(SUB, false);
153
+ let rollout = case.workspace.join("sub-rollout.jsonl");
154
+ std::fs::write(&rollout, "{}").unwrap();
155
+ let registry = StaticCaptureRegistry {
156
+ session_id: "sess-sub-captured".to_string(),
157
+ rollout_path: rollout,
158
+ };
159
+ let transport = RecordingTransport::new()
160
+ .with_session_present(true)
161
+ .with_windows(vec![WindowName::new("subgreeter")]);
162
+ let coord = Coordinator::new(
163
+ WorkspacePath::new(case.workspace.clone()),
164
+ Box::new(registry),
165
+ Box::new(transport),
166
+ );
167
+
168
+ coord.tick().expect("coordinator tick");
169
+ let state = load_runtime_state(&case.workspace).expect("state after tick");
170
+
171
+ assert_eq!(
172
+ state.pointer("/teams/sub/agents/subgreeter/session_id").and_then(Value::as_str),
173
+ Some("sess-sub-captured"),
174
+ "RC1: coordinator tick must write captured provider session into the nested active team, not only the top-level projection; state={state}"
175
+ );
176
+ }
177
+
178
+ #[test]
179
+ #[serial(env)]
180
+ fn launch_persists_agent_pane_id_and_pid() {
181
+ let case = HotfixCase::new("launch-pane-id");
182
+ case.write_team(CURRENT, "greeter", Provider::Fake);
183
+ // 0.3.28 Step 4b: workers live in their own window named after the
184
+ // agent_id, not the adaptive `team-w1` anchor.
185
+ let transport = RecordingTransport::new()
186
+ .with_targets(vec![pane_info("%7", "team-current", "greeter", Some(4242))])
187
+ .with_windows(vec![WindowName::new("greeter")])
188
+ .with_default_liveness(PaneLiveness::Live);
189
+
190
+ launch_with_transport_in_workspace(
191
+ &case.workspace,
192
+ &case.team_dir(CURRENT).join("team.spec.yaml"),
193
+ false,
194
+ true,
195
+ true,
196
+ &transport,
197
+ )
198
+ .expect("launch should spawn fake worker");
199
+ let state = load_runtime_state(&case.workspace).expect("state after launch");
200
+ let agent = state
201
+ .pointer("/teams/current/agents/greeter")
202
+ .or_else(|| state.pointer("/agents/greeter"))
203
+ .expect("greeter state");
204
+
205
+ assert_eq!(
206
+ agent.get("pane_id").and_then(Value::as_str),
207
+ Some("%7"),
208
+ "RC2: launch must persist the concrete tmux %pane_id returned by spawn/list_targets; state={state}"
209
+ );
210
+ assert_eq!(
211
+ agent.get("pane_pid").and_then(Value::as_u64),
212
+ Some(4242),
213
+ "RC2: launch must continue persisting pane_pid alongside pane_id; state={state}"
214
+ );
215
+ }
216
+
217
+ #[test]
218
+ #[serial(env)]
219
+ fn unbound_leader_receiver_is_not_attached_and_delivery_never_injects_sentinel() {
220
+ let case = HotfixCase::new("unbound-leader");
221
+ case.seed_unbound_leader_state();
222
+ let store = MessageStore::open(&case.workspace).unwrap();
223
+ let message_id = store
224
+ .create_message(
225
+ None,
226
+ "subgreeter",
227
+ "leader",
228
+ "sentinel canary",
229
+ None,
230
+ false,
231
+ Some(SUB),
232
+ )
233
+ .unwrap();
234
+ let state = load_runtime_state(&case.workspace).unwrap();
235
+ let transport = RecordingTransport::new().with_default_liveness(PaneLiveness::Live);
236
+
237
+ let out = deliver_pending_message(
238
+ &case.workspace,
239
+ &store,
240
+ &transport,
241
+ &message_id,
242
+ &EventLog::new(&case.workspace),
243
+ &state,
244
+ )
245
+ .expect("delivery should return an explicit rebind outcome");
246
+
247
+ assert!(
248
+ !out.ok && out.channel.as_deref() == Some("rebind_required") && transport.inject_targets().is_empty(),
249
+ "RC3: __team_agent_unbound__ is not an attached leader receiver; delivery must return rebind_required and never inject; outcome={out:?} targets={:?}",
250
+ transport.inject_targets()
251
+ );
252
+ assert!(
253
+ transport.inject_targets().is_empty(),
254
+ "RC3: delivery must never physically inject/paste to the sentinel __team_agent_unbound__; outcome={out:?} targets={:?}",
255
+ transport.inject_targets()
256
+ );
257
+ }
258
+
259
+ #[test]
260
+ #[serial(env)]
261
+ fn sqlite_open_retries_under_concurrent_writer() {
262
+ let case = HotfixCase::new("sqlite-lock");
263
+ std::fs::create_dir_all(case.db_path().parent().unwrap()).unwrap();
264
+ let holder = rusqlite::Connection::open(case.db_path()).unwrap();
265
+ initialize_schema(&holder, Some(&case.db_path())).unwrap();
266
+ holder.execute_batch("BEGIN EXCLUSIVE;").unwrap();
267
+
268
+ let start = Instant::now();
269
+ let result = MessageStore::open(&case.workspace);
270
+ let elapsed = start.elapsed();
271
+ holder.execute_batch("ROLLBACK;").unwrap();
272
+
273
+ assert!(
274
+ result.is_ok(),
275
+ "RC5: opening/initializing team.db under a concurrent writer must retry/idempotently survive instead of surfacing database-is-locked. elapsed={elapsed:?} error={:?}",
276
+ result.err()
277
+ );
278
+ }
279
+
280
+ #[test]
281
+ #[ignore = "real-machine: coordinator pidfile SIGTERM process gate"]
282
+ #[serial(env)]
283
+ fn shutdown_pidfile_sigterm_waits_and_normalizes() {
284
+ let case = HotfixCase::new("shutdown-pidfile");
285
+ let mut child = Command::new("sh")
286
+ .arg("-c")
287
+ .arg("trap 'sleep 2; exit 0' TERM; while true; do sleep 1; done")
288
+ .stdin(Stdio::null())
289
+ .stdout(Stdio::null())
290
+ .stderr(Stdio::null())
291
+ .spawn()
292
+ .expect("spawn delayed SIGTERM child");
293
+ case.write_coordinator_pid(child.id());
294
+
295
+ let report =
296
+ team_agent::coordinator::stop_coordinator(&WorkspacePath::new(case.workspace.clone()))
297
+ .expect("stop coordinator");
298
+ let still_running = process_running(child.id());
299
+ if still_running {
300
+ let _ = child.kill();
301
+ }
302
+ let _ = child.wait();
303
+
304
+ assert!(
305
+ report.ok && !still_running,
306
+ "RC6: pidfile coordinator stop must wait until the SIGTERM target really exits before reporting ok/stopped and removing metadata. report={report:?} still_running={still_running}"
307
+ );
308
+ }
309
+
310
+ #[derive(Debug)]
311
+ struct HotfixCase {
312
+ workspace: PathBuf,
313
+ }
314
+
315
+ impl HotfixCase {
316
+ fn new(tag: &str) -> Self {
317
+ let root = tmp_dir(tag);
318
+ let workspace = root.join("ws");
319
+ std::fs::create_dir_all(workspace.join(".team/runtime")).unwrap();
320
+ Self { workspace }
321
+ }
322
+
323
+ fn team_dir(&self, team: &str) -> PathBuf {
324
+ self.workspace.join(".team").join(team)
325
+ }
326
+
327
+ fn db_path(&self) -> PathBuf {
328
+ self.workspace.join(".team/runtime/team.db")
329
+ }
330
+
331
+ fn write_team(&self, team: &str, agent_id: &str, provider: Provider) {
332
+ let dir = self.team_dir(team);
333
+ std::fs::create_dir_all(dir.join("agents")).unwrap();
334
+ std::fs::write(
335
+ dir.join("TEAM.md"),
336
+ format!(
337
+ "---\nname: {team}\nobjective: hotfix\nprovider: {}\n---\n\nTeam.\n",
338
+ provider_name(provider)
339
+ ),
340
+ )
341
+ .unwrap();
342
+ std::fs::write(
343
+ dir.join("agents").join(format!("{agent_id}.md")),
344
+ format!(
345
+ "---\nname: {agent_id}\nrole: Greeter\nprovider: {}\nmodel: test\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nWorker.\n",
346
+ provider_name(provider)
347
+ ),
348
+ )
349
+ .unwrap();
350
+ let spec = team_agent::compiler::compile_team(&dir).unwrap();
351
+ std::fs::write(
352
+ dir.join("team.spec.yaml"),
353
+ team_agent::model::yaml::dumps(&spec),
354
+ )
355
+ .unwrap();
356
+ }
357
+
358
+ fn seed_restart_state(
359
+ &self,
360
+ team: &str,
361
+ agent_id: &str,
362
+ session_id: &str,
363
+ startup_prompts: &str,
364
+ ) {
365
+ let team_dir = self.team_dir(team);
366
+ let state = json!({
367
+ "active_team_key": team,
368
+ "team_dir": team_dir.to_string_lossy(),
369
+ "spec_path": team_dir.join("team.spec.yaml").to_string_lossy(),
370
+ "session_name": format!("team-{team}"),
371
+ "agents": {
372
+ agent_id: restart_agent(agent_id, team, &team_dir, session_id, startup_prompts)
373
+ },
374
+ "tasks": [],
375
+ "teams": {
376
+ team: {
377
+ "status": "alive",
378
+ "team_dir": team_dir.to_string_lossy(),
379
+ "spec_path": team_dir.join("team.spec.yaml").to_string_lossy(),
380
+ "session_name": format!("team-{team}"),
381
+ "agents": {
382
+ agent_id: restart_agent(agent_id, team, &team_dir, session_id, startup_prompts)
383
+ },
384
+ "tasks": []
385
+ }
386
+ }
387
+ });
388
+ save_runtime_state(&self.workspace, &state).unwrap();
389
+ }
390
+
391
+ fn seed_dual_team_state(&self, active: &str, with_leader_receivers: bool) {
392
+ let current_dir = self.team_dir(CURRENT);
393
+ let sub_dir = self.team_dir(SUB);
394
+ std::fs::create_dir_all(&current_dir).unwrap();
395
+ std::fs::create_dir_all(&sub_dir).unwrap();
396
+ let current = team_state(CURRENT, "greeter", &current_dir, with_leader_receivers);
397
+ let sub = team_state(SUB, "subgreeter", &sub_dir, with_leader_receivers);
398
+ let mut state = if active == CURRENT {
399
+ current.clone()
400
+ } else {
401
+ sub.clone()
402
+ };
403
+ let obj = state.as_object_mut().unwrap();
404
+ obj.insert("active_team_key".to_string(), json!(active));
405
+ obj.insert("teams".to_string(), json!({CURRENT: current, SUB: sub}));
406
+ save_runtime_state(&self.workspace, &state).unwrap();
407
+ let _ = MessageStore::open(&self.workspace).unwrap();
408
+ }
409
+
410
+ fn seed_unbound_leader_state(&self) {
411
+ let sub_dir = self.team_dir(SUB);
412
+ std::fs::create_dir_all(&sub_dir).unwrap();
413
+ save_runtime_state(
414
+ &self.workspace,
415
+ &json!({
416
+ "active_team_key": SUB,
417
+ "session_name": "team-sub",
418
+ "team_dir": sub_dir.to_string_lossy(),
419
+ "spec_path": sub_dir.join("team.spec.yaml").to_string_lossy(),
420
+ "leader_receiver": {
421
+ "mode": "direct_tmux",
422
+ "status": "attached",
423
+ "pane_id": "__team_agent_unbound__"
424
+ },
425
+ "team_owner": {
426
+ "status": "attached",
427
+ "pane_id": "__team_agent_unbound__"
428
+ },
429
+ "agents": {
430
+ "subgreeter": {"status": "running", "provider": "fake", "window": "subgreeter", "owner_team_id": SUB}
431
+ },
432
+ "teams": {
433
+ SUB: {
434
+ "status": "alive",
435
+ "session_name": "team-sub",
436
+ "team_dir": sub_dir.to_string_lossy(),
437
+ "spec_path": sub_dir.join("team.spec.yaml").to_string_lossy(),
438
+ "leader_receiver": {
439
+ "mode": "direct_tmux",
440
+ "status": "attached",
441
+ "pane_id": "__team_agent_unbound__"
442
+ },
443
+ "team_owner": {
444
+ "status": "attached",
445
+ "pane_id": "__team_agent_unbound__"
446
+ },
447
+ "agents": {
448
+ "subgreeter": {"status": "running", "provider": "fake", "window": "subgreeter", "owner_team_id": SUB}
449
+ }
450
+ }
451
+ }
452
+ }),
453
+ )
454
+ .unwrap();
455
+ }
456
+
457
+ fn seed_healthy_coordinator(&self) {
458
+ let workspace = WorkspacePath::new(self.workspace.clone());
459
+ let _ = MessageStore::open(workspace.as_path()).unwrap();
460
+ let pid = team_agent::coordinator::Pid::new(std::process::id());
461
+ team_agent::coordinator::write_coordinator_metadata(
462
+ &workspace,
463
+ pid,
464
+ team_agent::coordinator::MetadataSource::Boot,
465
+ )
466
+ .unwrap();
467
+ std::fs::write(
468
+ team_agent::coordinator::coordinator_pid_path(&workspace),
469
+ pid.to_string(),
470
+ )
471
+ .unwrap();
472
+ }
473
+
474
+ fn write_coordinator_pid(&self, pid: u32) {
475
+ let workspace = WorkspacePath::new(self.workspace.clone());
476
+ std::fs::create_dir_all(self.workspace.join(".team/runtime")).unwrap();
477
+ std::fs::write(
478
+ team_agent::coordinator::coordinator_pid_path(&workspace),
479
+ pid.to_string(),
480
+ )
481
+ .unwrap();
482
+ team_agent::coordinator::write_coordinator_metadata(
483
+ &workspace,
484
+ team_agent::coordinator::Pid::new(pid),
485
+ team_agent::coordinator::MetadataSource::Start,
486
+ )
487
+ .unwrap();
488
+ }
489
+ }
490
+
491
+ fn restart_agent(
492
+ agent_id: &str,
493
+ team: &str,
494
+ team_dir: &Path,
495
+ session_id: &str,
496
+ startup_prompts: &str,
497
+ ) -> Value {
498
+ json!({
499
+ "status": "running",
500
+ "agent_id": agent_id,
501
+ "provider": "codex",
502
+ "role": "Greeter",
503
+ "tools": ["mcp_team"],
504
+ "window": agent_id,
505
+ "owner_team_id": team,
506
+ "session_id": session_id,
507
+ "first_send_at": "2026-06-07T01:00:00+00:00",
508
+ "spawn_cwd": team_dir.to_string_lossy(),
509
+ "startup_prompts": startup_prompts,
510
+ "startup_prompt_status": startup_prompts
511
+ })
512
+ }
513
+
514
+ fn team_state(team: &str, agent_id: &str, team_dir: &Path, with_leader_receiver: bool) -> Value {
515
+ let mut state = json!({
516
+ "status": "alive",
517
+ "team_dir": team_dir.to_string_lossy(),
518
+ "spec_path": team_dir.join("team.spec.yaml").to_string_lossy(),
519
+ "session_name": format!("team-{team}"),
520
+ "agents": {
521
+ agent_id: {
522
+ "status": "running",
523
+ "provider": "codex",
524
+ "window": agent_id,
525
+ "owner_team_id": team,
526
+ "spawn_cwd": team_dir.to_string_lossy(),
527
+ "session_id": null
528
+ }
529
+ },
530
+ "tasks": [{"id": format!("task-{team}"), "assignee": agent_id, "status": "pending"}]
531
+ });
532
+ if with_leader_receiver {
533
+ let obj = state.as_object_mut().unwrap();
534
+ obj.insert(
535
+ "leader_receiver".to_string(),
536
+ json!({"mode": "direct_tmux", "status": "attached", "pane_id": format!("%leader-{team}")}),
537
+ );
538
+ obj.insert(
539
+ "team_owner".to_string(),
540
+ json!({"status": "attached", "pane_id": format!("%leader-{team}")}),
541
+ );
542
+ }
543
+ state
544
+ }
545
+
546
+ fn run_mcp_report_result(
547
+ workspace: &Path,
548
+ agent_id: &str,
549
+ owner_team_id: &str,
550
+ result_id: &str,
551
+ summary: &str,
552
+ ) -> Value {
553
+ let exe = crate::lifecycle::tests::test_binary_path();
554
+ let mut child = Command::new(exe)
555
+ .arg("mcp-server")
556
+ .arg("--workspace")
557
+ .arg(workspace)
558
+ .env("TEAM_AGENT_WORKSPACE", workspace)
559
+ .env("TEAM_AGENT_ID", agent_id)
560
+ .env("TEAM_AGENT_OWNER_TEAM_ID", owner_team_id)
561
+ .stdin(Stdio::piped())
562
+ .stdout(Stdio::piped())
563
+ .stderr(Stdio::piped())
564
+ .spawn()
565
+ .expect("spawn mcp-server");
566
+ {
567
+ let stdin = child.stdin.as_mut().expect("stdin");
568
+ writeln!(stdin, r#"{{"jsonrpc":"2.0","id":1,"method":"initialize","params":{{"protocolVersion":"2024-11-05"}}}}"#).unwrap();
569
+ writeln!(
570
+ stdin,
571
+ "{}",
572
+ json!({
573
+ "jsonrpc": "2.0",
574
+ "id": 2,
575
+ "method": "tools/call",
576
+ "params": {
577
+ "name": "report_result",
578
+ "arguments": {
579
+ "envelope": {
580
+ "result_id": result_id,
581
+ "task_id": "task-current",
582
+ "agent_id": agent_id,
583
+ "status": "success",
584
+ "summary": summary,
585
+ "changes": [], "tests": [], "risks": [], "artifacts": [], "next_actions": []
586
+ }
587
+ }
588
+ }
589
+ })
590
+ )
591
+ .unwrap();
592
+ }
593
+ let output = child.wait_with_output().expect("wait mcp-server");
594
+ assert!(
595
+ output.status.success(),
596
+ "mcp-server exited unsuccessfully: status={:?} stderr={}",
597
+ output.status.code(),
598
+ String::from_utf8_lossy(&output.stderr)
599
+ );
600
+ let stdout = String::from_utf8(output.stdout).unwrap();
601
+ stdout
602
+ .lines()
603
+ .filter_map(|line| serde_json::from_str::<Value>(line).ok())
604
+ .find(|frame| frame["id"] == json!(2))
605
+ .unwrap_or_else(|| panic!("missing tools/call response; stdout={stdout}"))
606
+ }
607
+
608
+ #[derive(Debug, Clone)]
609
+ struct RecordedSpawn {
610
+ argv: Vec<String>,
611
+ cwd: PathBuf,
612
+ env: BTreeMap<String, String>,
613
+ pane_id: PaneId,
614
+ }
615
+
616
+ #[derive(Debug)]
617
+ struct TransportState {
618
+ spawns: Vec<RecordedSpawn>,
619
+ injections: Vec<Target>,
620
+ keys: Vec<Vec<Key>>,
621
+ captures: Vec<String>,
622
+ session_present: bool,
623
+ windows: Vec<WindowName>,
624
+ targets: Vec<PaneInfo>,
625
+ default_liveness: PaneLiveness,
626
+ liveness: BTreeMap<String, PaneLiveness>,
627
+ }
628
+
629
+ impl Default for TransportState {
630
+ fn default() -> Self {
631
+ Self {
632
+ spawns: Vec::new(),
633
+ injections: Vec::new(),
634
+ keys: Vec::new(),
635
+ captures: Vec::new(),
636
+ session_present: false,
637
+ windows: Vec::new(),
638
+ targets: Vec::new(),
639
+ default_liveness: PaneLiveness::Dead,
640
+ liveness: BTreeMap::new(),
641
+ }
642
+ }
643
+ }
644
+
645
+ #[derive(Debug, Clone, Default)]
646
+ struct RecordingTransport {
647
+ state: Arc<Mutex<TransportState>>,
648
+ }
649
+
650
+ impl RecordingTransport {
651
+ fn new() -> Self {
652
+ Self::default()
653
+ }
654
+
655
+ fn with_session_present(self, present: bool) -> Self {
656
+ self.state.lock().unwrap().session_present = present;
657
+ self
658
+ }
659
+
660
+ fn with_windows(self, windows: Vec<WindowName>) -> Self {
661
+ self.state.lock().unwrap().windows = windows;
662
+ self
663
+ }
664
+
665
+ fn with_targets(self, targets: Vec<PaneInfo>) -> Self {
666
+ self.state.lock().unwrap().targets = targets;
667
+ self
668
+ }
669
+
670
+ fn with_default_liveness(self, liveness: PaneLiveness) -> Self {
671
+ self.state.lock().unwrap().default_liveness = liveness;
672
+ self
673
+ }
674
+
675
+ fn single_spawn(&self) -> RecordedSpawn {
676
+ let state = self.state.lock().unwrap();
677
+ assert_eq!(
678
+ state.spawns.len(),
679
+ 1,
680
+ "expected one spawn; spawns={:?}",
681
+ state.spawns
682
+ );
683
+ state.spawns[0].clone()
684
+ }
685
+
686
+ fn inject_targets(&self) -> Vec<Target> {
687
+ self.state.lock().unwrap().injections.clone()
688
+ }
689
+
690
+ fn spawn_result(
691
+ &self,
692
+ session: &SessionName,
693
+ window: &WindowName,
694
+ argv: &[String],
695
+ cwd: &Path,
696
+ env: &BTreeMap<String, String>,
697
+ ) -> SpawnResult {
698
+ let mut state = self.state.lock().unwrap();
699
+ let pane_id = state
700
+ .targets
701
+ .iter()
702
+ .find(|pane| pane.session == *session && pane.window_name.as_ref() == Some(window))
703
+ .or_else(|| {
704
+ state
705
+ .targets
706
+ .iter()
707
+ .find(|pane| pane.window_name.as_ref() == Some(window))
708
+ })
709
+ .map(|pane| pane.pane_id.clone())
710
+ .unwrap_or_else(|| PaneId::new(format!("%{}", state.spawns.len() + 1)));
711
+ let child_pid = state
712
+ .targets
713
+ .iter()
714
+ .find(|pane| pane.pane_id == pane_id)
715
+ .and_then(|pane| pane.pane_pid);
716
+ state.spawns.push(RecordedSpawn {
717
+ argv: argv.to_vec(),
718
+ cwd: cwd.to_path_buf(),
719
+ env: env.clone(),
720
+ pane_id: pane_id.clone(),
721
+ });
722
+ SpawnResult {
723
+ pane_id,
724
+ session: session.clone(),
725
+ window: window.clone(),
726
+ child_pid,
727
+ }
728
+ }
729
+ }
730
+
731
+ impl Transport for RecordingTransport {
732
+ fn kind(&self) -> BackendKind {
733
+ BackendKind::Tmux
734
+ }
735
+
736
+ fn spawn_first(
737
+ &self,
738
+ session: &SessionName,
739
+ window: &WindowName,
740
+ argv: &[String],
741
+ cwd: &Path,
742
+ env: &BTreeMap<String, String>,
743
+ ) -> Result<SpawnResult, TransportError> {
744
+ Ok(self.spawn_result(session, window, argv, cwd, env))
745
+ }
746
+
747
+ fn spawn_into(
748
+ &self,
749
+ session: &SessionName,
750
+ window: &WindowName,
751
+ argv: &[String],
752
+ cwd: &Path,
753
+ env: &BTreeMap<String, String>,
754
+ ) -> Result<SpawnResult, TransportError> {
755
+ Ok(self.spawn_result(session, window, argv, cwd, env))
756
+ }
757
+
758
+ fn inject(
759
+ &self,
760
+ target: &Target,
761
+ _payload: &InjectPayload,
762
+ _submit: Key,
763
+ _bracketed: bool,
764
+ ) -> Result<InjectReport, TransportError> {
765
+ self.state.lock().unwrap().injections.push(target.clone());
766
+ Ok(InjectReport {
767
+ stage_reached: InjectStage::Submit,
768
+ inject_verification: InjectVerification::CaptureContainsToken,
769
+ submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
770
+ turn_verification: TurnVerification::NotYetObserved,
771
+ attempts: 1,
772
+ submit_diagnostics: None,
773
+ })
774
+ }
775
+
776
+ fn send_keys(&self, _target: &Target, keys: &[Key]) -> Result<(), TransportError> {
777
+ self.state.lock().unwrap().keys.push(keys.to_vec());
778
+ Ok(())
779
+ }
780
+
781
+ fn capture(
782
+ &self,
783
+ _target: &Target,
784
+ range: CaptureRange,
785
+ ) -> Result<CapturedText, TransportError> {
786
+ let text = self
787
+ .state
788
+ .lock()
789
+ .unwrap()
790
+ .captures
791
+ .first()
792
+ .cloned()
793
+ .unwrap_or_default();
794
+ Ok(CapturedText { text, range })
795
+ }
796
+
797
+ fn query(&self, _target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
798
+ match field {
799
+ PaneField::PaneWidth => Ok(Some("120".to_string())),
800
+ _ => Ok(None),
801
+ }
802
+ }
803
+
804
+ fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
805
+ let state = self.state.lock().unwrap();
806
+ Ok(state
807
+ .liveness
808
+ .get(pane.as_str())
809
+ .copied()
810
+ .unwrap_or(state.default_liveness))
811
+ }
812
+
813
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
814
+ Ok(self.state.lock().unwrap().targets.clone())
815
+ }
816
+
817
+ fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
818
+ Ok(self.state.lock().unwrap().session_present)
819
+ }
820
+
821
+ fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
822
+ Ok(self.state.lock().unwrap().windows.clone())
823
+ }
824
+
825
+ fn set_session_env(
826
+ &self,
827
+ _session: &SessionName,
828
+ _key: &str,
829
+ _value: &str,
830
+ ) -> Result<SetEnvOutcome, TransportError> {
831
+ Ok(SetEnvOutcome::Applied)
832
+ }
833
+
834
+ fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
835
+ Ok(())
836
+ }
837
+
838
+ fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
839
+ Ok(())
840
+ }
841
+
842
+ fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
843
+ Ok(AttachOutcome::Attached)
844
+ }
845
+ }
846
+
847
+ struct StaticCaptureRegistry {
848
+ session_id: String,
849
+ rollout_path: PathBuf,
850
+ }
851
+
852
+ impl ProviderRegistry for StaticCaptureRegistry {
853
+ fn adapter_for(&self, provider: Provider) -> Box<dyn ProviderAdapter> {
854
+ Box::new(StaticCaptureAdapter {
855
+ provider,
856
+ session_id: self.session_id.clone(),
857
+ rollout_path: self.rollout_path.clone(),
858
+ })
859
+ }
860
+ }
861
+
862
+ struct StaticCaptureAdapter {
863
+ provider: Provider,
864
+ session_id: String,
865
+ rollout_path: PathBuf,
866
+ }
867
+
868
+ impl ProviderAdapter for StaticCaptureAdapter {
869
+ fn provider(&self) -> Provider {
870
+ self.provider
871
+ }
872
+
873
+ fn caps(&self) -> ProviderCaps {
874
+ ProviderCaps {
875
+ resume: true,
876
+ fork: false,
877
+ native_mcp_config: false,
878
+ writes_global_settings: false,
879
+ }
880
+ }
881
+
882
+ fn is_installed(&self) -> bool {
883
+ true
884
+ }
885
+
886
+ fn version(&self) -> Result<String, ProviderError> {
887
+ Ok("test".to_string())
888
+ }
889
+
890
+ fn auth_hint(&self, _auth_mode: AuthMode) -> AuthHintStatus {
891
+ AuthHintStatus::Unknown
892
+ }
893
+
894
+ fn build_command(
895
+ &self,
896
+ _auth_mode: AuthMode,
897
+ _mcp_config: Option<&McpConfig>,
898
+ _system_prompt: Option<&str>,
899
+ _model: Option<&str>,
900
+ ) -> Result<Vec<String>, ProviderError> {
901
+ Ok(vec!["fake".to_string()])
902
+ }
903
+
904
+ fn build_command_with_tools(
905
+ &self,
906
+ auth_mode: AuthMode,
907
+ mcp_config: Option<&McpConfig>,
908
+ system_prompt: Option<&str>,
909
+ model: Option<&str>,
910
+ _tools: &[&str],
911
+ ) -> Result<Vec<String>, ProviderError> {
912
+ self.build_command(auth_mode, mcp_config, system_prompt, model)
913
+ }
914
+
915
+ fn capture_session_id(
916
+ &self,
917
+ _agent_id: &str,
918
+ spawn_cwd: &Path,
919
+ _timeout_s: u64,
920
+ ) -> Result<Option<CapturedSession>, ProviderError> {
921
+ Ok(Some(CapturedSession {
922
+ session_id: Some(SessionId::new(self.session_id.clone())),
923
+ rollout_path: Some(RolloutPath::new(self.rollout_path.clone())),
924
+ captured_via: CaptureVia::FsWatch,
925
+ attribution_confidence: Confidence::High,
926
+ spawn_cwd: spawn_cwd.to_path_buf(),
927
+ }))
928
+ }
929
+
930
+ fn recover_session_id(
931
+ &self,
932
+ _agent_id: &str,
933
+ _spawn_cwd: &Path,
934
+ ) -> Result<Option<SessionId>, ProviderError> {
935
+ Ok(Some(SessionId::new(self.session_id.clone())))
936
+ }
937
+
938
+ fn session_is_resumable(
939
+ &self,
940
+ _session_id: Option<&SessionId>,
941
+ _auth_mode: AuthMode,
942
+ ) -> Result<bool, ProviderError> {
943
+ Ok(true)
944
+ }
945
+
946
+ fn build_resume_command(
947
+ &self,
948
+ _session_id: Option<&SessionId>,
949
+ _auth_mode: AuthMode,
950
+ _mcp_config: Option<&McpConfig>,
951
+ ) -> Result<Vec<String>, ProviderError> {
952
+ Ok(vec!["fake".to_string(), "resume".to_string()])
953
+ }
954
+
955
+ fn build_resume_command_with_context(
956
+ &self,
957
+ session_id: Option<&SessionId>,
958
+ auth_mode: AuthMode,
959
+ mcp_config: Option<&McpConfig>,
960
+ _system_prompt: Option<&str>,
961
+ _model: Option<&str>,
962
+ _tools: &[&str],
963
+ ) -> Result<Vec<String>, ProviderError> {
964
+ self.build_resume_command(session_id, auth_mode, mcp_config)
965
+ }
966
+
967
+ fn fork(
968
+ &self,
969
+ _session_id: Option<&SessionId>,
970
+ _auth_mode: AuthMode,
971
+ _mcp_config: Option<&McpConfig>,
972
+ ) -> Result<Vec<String>, ProviderError> {
973
+ Ok(vec!["fake".to_string(), "fork".to_string()])
974
+ }
975
+
976
+ fn fork_with_context(
977
+ &self,
978
+ session_id: Option<&SessionId>,
979
+ auth_mode: AuthMode,
980
+ mcp_config: Option<&McpConfig>,
981
+ _system_prompt: Option<&str>,
982
+ _model: Option<&str>,
983
+ _tools: &[&str],
984
+ ) -> Result<Vec<String>, ProviderError> {
985
+ self.fork(session_id, auth_mode, mcp_config)
986
+ }
987
+
988
+ fn mcp_config(&self, _auth_mode: AuthMode) -> Result<McpConfig, ProviderError> {
989
+ Ok(McpConfig { raw: json!({}) })
990
+ }
991
+
992
+ fn install_mcp(&self, _config: &McpConfig) -> Result<(), ProviderError> {
993
+ Ok(())
994
+ }
995
+
996
+ fn status_patterns(&self) -> Result<StatusPatterns, ProviderError> {
997
+ Ok(StatusPatterns {
998
+ idle: regex::Regex::new("idle").unwrap(),
999
+ processing: regex::Regex::new("processing").unwrap(),
1000
+ error: regex::Regex::new("error").unwrap(),
1001
+ })
1002
+ }
1003
+
1004
+ fn validate_model(&self, _model: &str) -> Result<bool, ProviderError> {
1005
+ Ok(true)
1006
+ }
1007
+
1008
+ fn handle_startup_prompts(
1009
+ &self,
1010
+ _transport: &dyn Transport,
1011
+ _target: &Target,
1012
+ _checks: usize,
1013
+ _sleep_s: f64,
1014
+ ) -> Vec<HandledPrompt> {
1015
+ Vec::new()
1016
+ }
1017
+ }
1018
+
1019
+ fn pane_info(pane_id: &str, session: &str, window: &str, pane_pid: Option<u32>) -> PaneInfo {
1020
+ PaneInfo {
1021
+ pane_id: PaneId::new(pane_id),
1022
+ session: SessionName::new(session),
1023
+ window_index: Some(0),
1024
+ window_name: Some(WindowName::new(window)),
1025
+ pane_index: Some(0),
1026
+ tty: None,
1027
+ current_command: Some("fake".to_string()),
1028
+ current_path: None,
1029
+ active: true,
1030
+ pane_pid,
1031
+ leader_env: BTreeMap::new(),
1032
+ }
1033
+ }
1034
+
1035
+ fn process_running(pid: u32) -> bool {
1036
+ // 0.5.x Windows portability Batch 5: route through
1037
+ // `platform::process::pid_is_alive` so the helper compiles on
1038
+ // both platforms. Unix behavior byte-equivalent (kill(pid, 0)
1039
+ // via platform primitive).
1040
+ team_agent::platform::process::pid_is_alive(pid)
1041
+ }
1042
+
1043
+ fn db_rows(conn: &rusqlite::Connection, table: &str, columns: &str) -> Vec<String> {
1044
+ let mut stmt = conn
1045
+ .prepare(&format!("select {columns} from {table}"))
1046
+ .unwrap();
1047
+ let column_count = stmt.column_count();
1048
+ stmt.query_map([], |row| {
1049
+ let values = (0..column_count)
1050
+ .map(|idx| {
1051
+ row.get::<_, Option<String>>(idx)
1052
+ .unwrap_or(None)
1053
+ .unwrap_or_else(|| "NULL".to_string())
1054
+ })
1055
+ .collect::<Vec<_>>();
1056
+ Ok(values.join("|"))
1057
+ })
1058
+ .unwrap()
1059
+ .collect::<Result<Vec<_>, _>>()
1060
+ .unwrap()
1061
+ }
1062
+
1063
+ fn provider_name(provider: Provider) -> &'static str {
1064
+ match provider {
1065
+ Provider::Claude | Provider::ClaudeCode => "claude",
1066
+ Provider::Codex => "codex",
1067
+ Provider::Copilot => "copilot",
1068
+ Provider::GeminiCli => "gemini",
1069
+ Provider::Fake => "fake",
1070
+ }
1071
+ }
1072
+
1073
+ fn tmp_dir(tag: &str) -> PathBuf {
1074
+ static N: AtomicU64 = AtomicU64::new(0);
1075
+ let dir = std::env::temp_dir().join(format!(
1076
+ "ta-rs-252-{tag}-{}-{}",
1077
+ std::process::id(),
1078
+ N.fetch_add(1, Ordering::Relaxed)
1079
+ ));
1080
+ let _ = std::fs::remove_dir_all(&dir);
1081
+ std::fs::create_dir_all(&dir).unwrap();
1082
+ std::fs::canonicalize(dir).unwrap()
1083
+ }