@team-agent/installer 0.5.61 → 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 (250) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -2
  4. package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +1 -0
  6. package/crates/team-agent/src/cli/emit.rs +41 -7
  7. package/crates/team-agent/src/cli/helpers.rs +1 -0
  8. package/crates/team-agent/src/cli/leader.rs +6 -1
  9. package/crates/team-agent/src/cli/leaders.rs +2 -0
  10. package/crates/team-agent/src/cli/mod.rs +33 -3
  11. package/crates/team-agent/src/cli/named_address.rs +8 -1
  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 +1 -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 +1 -0
  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/tests/verb_profile.rs +38 -0
  25. package/crates/team-agent/src/cli/types.rs +3 -2
  26. package/crates/team-agent/src/communication_mode/mod.rs +1 -0
  27. package/crates/team-agent/src/compiler.rs +1 -0
  28. package/crates/team-agent/src/conpty/backend.rs +1 -0
  29. package/crates/team-agent/src/conpty/mod.rs +1 -0
  30. package/crates/team-agent/src/coordinator/backoff.rs +5 -8
  31. package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
  32. package/crates/team-agent/src/coordinator/health.rs +1 -0
  33. package/crates/team-agent/src/coordinator/mod.rs +2 -2
  34. package/crates/team-agent/src/coordinator/orphan.rs +4 -116
  35. package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
  36. package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
  37. package/crates/team-agent/src/coordinator/steps/abnormal.rs +6 -5
  38. package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
  39. package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
  40. package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
  41. package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
  42. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
  43. package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
  44. package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
  45. package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +0 -9
  46. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
  47. package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -42
  49. package/crates/team-agent/src/coordinator/tick.rs +24 -28
  50. package/crates/team-agent/src/coordinator/types.rs +3 -176
  51. package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
  52. package/crates/team-agent/src/db/message_store.rs +2 -1
  53. package/crates/team-agent/src/db/migration.rs +5 -4
  54. package/crates/team-agent/src/db/mod.rs +1 -0
  55. package/crates/team-agent/src/db/schema.rs +1 -0
  56. package/crates/team-agent/src/diagnose/comms.rs +2 -1
  57. package/crates/team-agent/src/diagnose/mod.rs +1 -0
  58. package/crates/team-agent/src/diagnose/orphans.rs +1 -0
  59. package/crates/team-agent/src/layout/manager.rs +3 -7
  60. package/crates/team-agent/src/layout/mod.rs +2 -2
  61. package/crates/team-agent/src/layout/overlay.rs +6 -1
  62. package/crates/team-agent/src/layout/placement.rs +1 -0
  63. package/crates/team-agent/src/layout/recovery.rs +3 -0
  64. package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
  65. package/crates/team-agent/src/layout/sessions.rs +8 -1
  66. package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
  67. package/crates/team-agent/src/layout/worker_env.rs +3 -0
  68. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  69. package/crates/team-agent/src/leader/helpers.rs +1 -0
  70. package/crates/team-agent/src/leader/incident.rs +1 -0
  71. package/crates/team-agent/src/leader/lease.rs +27 -22
  72. package/crates/team-agent/src/leader/mod.rs +2 -3
  73. package/crates/team-agent/src/leader/owner_bind.rs +13 -3
  74. package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
  75. package/crates/team-agent/src/leader/rediscover.rs +4 -17
  76. package/crates/team-agent/src/leader/registry.rs +3 -15
  77. package/crates/team-agent/src/leader/start.rs +373 -125
  78. package/crates/team-agent/src/leader/takeover.rs +10 -232
  79. package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
  80. package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
  81. package/crates/team-agent/src/leader/tests/identity.rs +7 -2
  82. package/crates/team-agent/src/leader/tests/idle.rs +0 -193
  83. package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
  84. package/crates/team-agent/src/leader/tests/mod.rs +4 -41
  85. package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
  86. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
  87. package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
  88. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
  89. package/crates/team-agent/src/leader/types.rs +1 -28
  90. package/crates/team-agent/src/lib.rs +3 -0
  91. package/crates/team-agent/src/lifecycle/display.rs +7 -1
  92. package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
  93. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
  94. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
  95. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
  96. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
  97. package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
  98. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
  99. package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
  100. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -27
  101. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
  102. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
  103. package/crates/team-agent/src/lifecycle/launch.rs +11 -12
  104. package/crates/team-agent/src/lifecycle/mod.rs +2 -1
  105. package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -3
  106. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
  107. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
  108. package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
  109. package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
  110. package/crates/team-agent/src/lifecycle/restart.rs +13 -11
  111. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
  112. package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
  113. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
  114. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
  115. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
  116. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
  117. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
  118. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
  119. package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
  120. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
  121. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
  122. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
  123. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
  124. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
  125. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
  126. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
  127. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
  128. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
  129. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
  130. package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
  131. package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
  132. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
  133. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
  134. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
  135. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
  136. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
  137. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
  138. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
  139. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
  140. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
  141. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
  142. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
  143. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
  144. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
  145. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
  146. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
  147. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
  148. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
  149. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
  150. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
  151. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
  152. package/crates/team-agent/src/lifecycle/tests.rs +59 -0
  153. package/crates/team-agent/src/lifecycle/types.rs +0 -9
  154. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  155. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
  156. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
  157. package/crates/team-agent/src/mcp_server/mod.rs +1 -0
  158. package/crates/team-agent/src/mcp_server/normalize.rs +4 -3
  159. package/crates/team-agent/src/mcp_server/tools.rs +1 -0
  160. package/crates/team-agent/src/mcp_server/types.rs +1 -18
  161. package/crates/team-agent/src/mcp_server/wire.rs +5 -5
  162. package/crates/team-agent/src/messaging/activity.rs +1 -0
  163. package/crates/team-agent/src/messaging/address.rs +1 -0
  164. package/crates/team-agent/src/messaging/delivery.rs +4 -3
  165. package/crates/team-agent/src/messaging/helpers.rs +2 -1
  166. package/crates/team-agent/src/messaging/leader_channel.rs +1 -0
  167. package/crates/team-agent/src/messaging/leader_receiver.rs +2 -1
  168. package/crates/team-agent/src/messaging/mod.rs +10 -14
  169. package/crates/team-agent/src/messaging/peers.rs +2 -0
  170. package/crates/team-agent/src/messaging/persist.rs +3 -1
  171. package/crates/team-agent/src/messaging/presentation.rs +2 -1
  172. package/crates/team-agent/src/messaging/results.rs +1 -0
  173. package/crates/team-agent/src/messaging/scheduler.rs +1 -0
  174. package/crates/team-agent/src/messaging/selftest.rs +1 -1
  175. package/crates/team-agent/src/messaging/send.rs +41 -8
  176. package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
  177. package/crates/team-agent/src/messaging/types.rs +2 -2
  178. package/crates/team-agent/src/messaging/watchers.rs +19 -13
  179. package/crates/team-agent/src/model/enums.rs +1 -0
  180. package/crates/team-agent/src/model/errors.rs +1 -0
  181. package/crates/team-agent/src/model/ids.rs +1 -0
  182. package/crates/team-agent/src/model/mod.rs +1 -0
  183. package/crates/team-agent/src/model/name_similarity.rs +1 -0
  184. package/crates/team-agent/src/model/pane_authority_refusal.rs +1 -0
  185. package/crates/team-agent/src/model/paths.rs +1 -0
  186. package/crates/team-agent/src/model/permissions.rs +3 -2
  187. package/crates/team-agent/src/model/routing.rs +1 -0
  188. package/crates/team-agent/src/model/spec.rs +1 -0
  189. package/crates/team-agent/src/model/task_graph.rs +1 -0
  190. package/crates/team-agent/src/model/yaml/tests.rs +1 -0
  191. package/crates/team-agent/src/model/yaml.rs +1 -0
  192. package/crates/team-agent/src/packaging/install.rs +9 -147
  193. package/crates/team-agent/src/packaging/migrate.rs +2 -0
  194. package/crates/team-agent/src/packaging/mod.rs +1 -0
  195. package/crates/team-agent/src/packaging/repair.rs +2 -0
  196. package/crates/team-agent/src/packaging/tests.rs +29 -218
  197. package/crates/team-agent/src/packaging/types.rs +8 -15
  198. package/crates/team-agent/src/platform/argv.rs +4 -0
  199. package/crates/team-agent/src/platform/errors.rs +10 -1
  200. package/crates/team-agent/src/platform/file_lock.rs +4 -157
  201. package/crates/team-agent/src/platform/mod.rs +0 -61
  202. package/crates/team-agent/src/platform/process.rs +1 -0
  203. package/crates/team-agent/src/provider/adapter.rs +1 -0
  204. package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
  205. package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
  206. package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
  207. package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
  208. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
  209. package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
  210. package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
  211. package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
  212. package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
  213. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
  214. package/crates/team-agent/src/provider/classify.rs +1 -0
  215. package/crates/team-agent/src/provider/faults.rs +1 -26
  216. package/crates/team-agent/src/provider/helpers.rs +1 -0
  217. package/crates/team-agent/src/provider/mod.rs +1 -1
  218. package/crates/team-agent/src/provider/session/capture.rs +1 -4
  219. package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
  220. package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
  221. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
  222. package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
  223. package/crates/team-agent/src/provider/session/mod.rs +2 -4
  224. package/crates/team-agent/src/provider/session/resume.rs +2 -209
  225. package/crates/team-agent/src/provider/session_scan/claude.rs +1 -0
  226. package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
  227. package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
  228. package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
  229. package/crates/team-agent/src/provider/session_scan.rs +1 -42
  230. package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
  231. package/crates/team-agent/src/provider/types.rs +1 -0
  232. package/crates/team-agent/src/provider/wire.rs +1 -0
  233. package/crates/team-agent/src/state/identity.rs +16 -5
  234. package/crates/team-agent/src/state/identity_keys.rs +1 -0
  235. package/crates/team-agent/src/state/mod.rs +2 -0
  236. package/crates/team-agent/src/state/owner_gate.rs +5 -0
  237. package/crates/team-agent/src/state/ownership.rs +10 -4
  238. package/crates/team-agent/src/state/paths.rs +12 -0
  239. package/crates/team-agent/src/state/persist.rs +9 -3
  240. package/crates/team-agent/src/state/projection.rs +15 -2
  241. package/crates/team-agent/src/state/repository/intent.rs +1 -0
  242. package/crates/team-agent/src/state/repository/tests.rs +1 -0
  243. package/crates/team-agent/src/state/repository.rs +7 -0
  244. package/crates/team-agent/src/state/selector.rs +1 -0
  245. package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
  246. package/crates/team-agent/src/tmux_backend.rs +63 -0
  247. package/crates/team-agent/src/transport.rs +15 -0
  248. package/package.json +4 -4
  249. package/crates/team-agent/src/leader/inject.rs +0 -33
  250. package/crates/team-agent/src/provider/command.rs +0 -80
@@ -0,0 +1,986 @@
1
+ //! P0 RED contract: host co-tenant death / worker cohort identity.
2
+ //!
3
+ //! Source: `.team/artifacts/host-cotenant-death-p0-architecture-locate.md` §8.
4
+
5
+ #![cfg(unix)]
6
+ #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
7
+
8
+ #[path = "../../../tests/support/hermetic.rs"]
9
+ mod hermetic_guard;
10
+ #[allow(dead_code)]
11
+ fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
12
+
13
+ use std::collections::{BTreeMap, BTreeSet};
14
+ use std::fs;
15
+ use std::io::Write;
16
+ use std::os::unix::fs::PermissionsExt;
17
+ use std::path::{Path, PathBuf};
18
+ use std::sync::{Arc, Mutex};
19
+
20
+ use serde_json::{json, Value};
21
+ use serial_test::serial;
22
+ use team_agent::coordinator::{
23
+ coordinator_pid_path, Coordinator, MetadataSource, Pid, ProviderRegistry, WorkspacePath,
24
+ };
25
+ use team_agent::lifecycle::{
26
+ reset_agent_with_transport, restart_with_transport_with_readiness_deadline,
27
+ start_agent_with_transport, LifecycleError, RestartReport,
28
+ };
29
+ use team_agent::message_store::MessageStore;
30
+ use team_agent::model::enums::{PaneLiveness, Provider};
31
+ use team_agent::model::ids::AgentId;
32
+ use team_agent::model::paths::{runtime_dir, runtime_spec_path};
33
+ use team_agent::provider::{get_adapter, CaptureSessionContext, ProviderAdapter};
34
+ use team_agent::state::persist::{load_runtime_state, save_runtime_state};
35
+ use team_agent::transport::{
36
+ AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
37
+ InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, SessionName,
38
+ SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
39
+ TurnVerification, WindowName,
40
+ };
41
+
42
+ const TEAM: &str = "current";
43
+ const SESSION: &str = "team-current";
44
+ const TMUX_ENDPOINT: &str = "/private/tmp/ta-p0-cohort.sock";
45
+ const POLLUTED_CALLER_AUTHORITY_ENVS: &[&str] = &[
46
+ "TEAM_AGENT_LEADER_PANE_ID",
47
+ "TEAM_AGENT_LEADER_SESSION_UUID",
48
+ "TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
49
+ "TEAM_AGENT_LEADER_SESSION_NAME",
50
+ "TEAM_AGENT_LEADER_PROVIDER",
51
+ "TEAM_AGENT_MACHINE_FINGERPRINT",
52
+ "TEAM_AGENT_WORKSPACE",
53
+ "TEAM_AGENT_TEAM_ID",
54
+ "TEAM_AGENT_OWNER_TEAM_ID",
55
+ "TEAM_AGENT_ACTIVE_TEAM",
56
+ "TEAM_AGENT_ID",
57
+ "TEAM_AGENT_AGENT_ID",
58
+ "TEAM_AGENT_AUTH_MODE",
59
+ "TEAM_AGENT_LEADER_BYPASS",
60
+ "TEAM_AGENT_LEADER_BYPASS_SOURCE",
61
+ "TEAM_AGENT_LEADER_BYPASS_PROVIDER",
62
+ "TEAM_AGENT_LEADER_BYPASS_FLAG",
63
+ "TEAM_AGENT_MCP_AUTO_APPROVE",
64
+ "TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE",
65
+ ];
66
+
67
+ #[test]
68
+ #[serial(env)]
69
+ fn polluted_caller_env_never_authorizes_teardown_to_kill_foreign_tmux() {
70
+ let env = hermetic_guard::HermeticTestEnv::enter("p0-caller-env-sentinel");
71
+ let shim_dir = env.root().join("bin");
72
+ fs::create_dir_all(&shim_dir).unwrap();
73
+ let recorder = hermetic_guard::short_tmux_socket("p0-caller-env-recorder")
74
+ .with_extension("log");
75
+ let _ = fs::remove_file(&recorder);
76
+ let sentinel = env.root().join("foreign-sentinel.sock");
77
+ let polluted_tmux = format!("{},4242,0", sentinel.display());
78
+
79
+ let real_path = std::env::var_os("PATH").unwrap_or_default();
80
+ let real_tmux = std::env::split_paths(&real_path)
81
+ .map(|dir| dir.join("tmux"))
82
+ .find(|path| path.is_file())
83
+ .expect("real tmux binary on PATH");
84
+ let shim = shim_dir.join("tmux");
85
+ fs::write(
86
+ &shim,
87
+ r#"#!/bin/sh
88
+ {
89
+ printf 'TMUX=%s' "${TMUX-}"
90
+ for arg in "$@"; do printf '\t%s' "$arg"; done
91
+ printf '\n'
92
+ } >> "$TEAM_AGENT_TEST_TMUX_RECORDER_LOG"
93
+ case " $* " in
94
+ *" kill-session "*|*" kill-server "*) exit 0 ;;
95
+ esac
96
+ exec "$TEAM_AGENT_TEST_REAL_TMUX" "$@"
97
+ "#,
98
+ )
99
+ .unwrap();
100
+ let mut permissions = fs::metadata(&shim).unwrap().permissions();
101
+ permissions.set_mode(0o755);
102
+ fs::set_permissions(&shim, permissions).unwrap();
103
+
104
+ let mut polluted_env = vec![
105
+ env.with_env(
106
+ "PATH",
107
+ &format!("{}:{}", shim_dir.display(), real_path.to_string_lossy()),
108
+ ),
109
+ env.with_env(
110
+ "TEAM_AGENT_TEST_TMUX_RECORDER_LOG",
111
+ recorder.to_str().unwrap(),
112
+ ),
113
+ env.with_env("TEAM_AGENT_TEST_REAL_TMUX", real_tmux.to_str().unwrap()),
114
+ env.with_env("TMUX", &polluted_tmux),
115
+ env.with_env("TMUX_PANE", "%foreign-sentinel"),
116
+ ];
117
+ for key in POLLUTED_CALLER_AUTHORITY_ENVS {
118
+ polluted_env.push(env.with_env(key, "foreign-caller-authority"));
119
+ }
120
+
121
+ let missing_scrub_keys = POLLUTED_CALLER_AUTHORITY_ENVS
122
+ .iter()
123
+ .filter(|key| !hermetic_guard::CALLER_IDENTITY_ENVS.contains(key))
124
+ .copied()
125
+ .collect::<Vec<_>>();
126
+
127
+ let _registration = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
128
+ env.register_owned_tmux_socket(&sentinel);
129
+ }));
130
+ drop(env);
131
+
132
+ let recorded = fs::read_to_string(&recorder).unwrap_or_default();
133
+ let destructive_hits = recorded
134
+ .lines()
135
+ .filter(|line| line.contains("\tkill-session") || line.contains("\tkill-server"))
136
+ .filter(|line| {
137
+ let explicit_endpoint = line.contains("\t-S\t") || line.contains("\t-L\t");
138
+ let targets_sentinel = line.split('\t').any(|arg| arg == sentinel.to_str().unwrap());
139
+ let inherits_sentinel = line.starts_with(&format!("TMUX={polluted_tmux}\t"));
140
+ targets_sentinel || (inherits_sentinel && !explicit_endpoint)
141
+ })
142
+ .collect::<Vec<_>>();
143
+ let _ = fs::remove_file(&recorder);
144
+
145
+ assert!(
146
+ missing_scrub_keys.is_empty() && destructive_hits.is_empty(),
147
+ "P0 caller-env boundary: polluted caller identity must never become teardown authority; missing_scrub_keys={missing_scrub_keys:?}; destructive tmux argv targeting ambient sentinel={destructive_hits:?}; all recorded argv={recorded:?}"
148
+ );
149
+ drop(polluted_env);
150
+ }
151
+
152
+ #[test]
153
+ #[serial(env)]
154
+ fn restart_allow_fresh_does_not_leave_same_role_host_cotenants() {
155
+ let mut failures = Vec::new();
156
+ let (env, workspace) = team_case("p0-restart", &["developer", "test-engineer", "arch-reviewer"]);
157
+ let shim_dir = env.root().join("bin");
158
+ fs::create_dir_all(&shim_dir).unwrap();
159
+ let recorder = env.root().join("ps-recorder.log");
160
+ let real_path = std::env::var_os("PATH").unwrap_or_default();
161
+ let real_ps = std::env::split_paths(&real_path)
162
+ .map(|dir| dir.join("ps"))
163
+ .find(|path| path.is_file())
164
+ .expect("real ps binary on PATH");
165
+ let shim = shim_dir.join("ps");
166
+ fs::write(
167
+ &shim,
168
+ r#"#!/bin/sh
169
+ {
170
+ first=1
171
+ for arg in "$@"; do
172
+ if [ "$first" -eq 0 ]; then printf '\t'; fi
173
+ printf '%s' "$arg"
174
+ first=0
175
+ done
176
+ printf '\n'
177
+ } >> "$TEAM_AGENT_TEST_PS_RECORDER_LOG"
178
+ if [ "$#" -eq 4 ] && [ "$1" = "-p" ] && [ "$2" = "$TEAM_AGENT_TEST_SELF_PID" ] && [ "$3" = "-o" ] && [ "$4" = "stat=" ]; then
179
+ printf 'S\n'
180
+ exit 0
181
+ fi
182
+ exec "$TEAM_AGENT_TEST_REAL_PS" "$@"
183
+ "#,
184
+ )
185
+ .unwrap();
186
+ let mut permissions = fs::metadata(&shim).unwrap().permissions();
187
+ permissions.set_mode(0o755);
188
+ fs::set_permissions(&shim, permissions).unwrap();
189
+ let self_pid = std::process::id().to_string();
190
+ let _ps_env = [
191
+ env.with_env(
192
+ "PATH",
193
+ &format!("{}:{}", shim_dir.display(), real_path.to_string_lossy()),
194
+ ),
195
+ env.with_env(
196
+ "TEAM_AGENT_TEST_PS_RECORDER_LOG",
197
+ recorder.to_str().unwrap(),
198
+ ),
199
+ env.with_env("TEAM_AGENT_TEST_REAL_PS", real_ps.to_str().unwrap()),
200
+ env.with_env("TEAM_AGENT_TEST_SELF_PID", &self_pid),
201
+ ];
202
+ let transport = CohortTransport::with_old_panes(&["developer", "test-engineer", "arch-reviewer"]);
203
+ let before = load_runtime_state(&workspace).unwrap();
204
+ let restart = restart_with_transport_with_readiness_deadline(
205
+ &workspace,
206
+ true,
207
+ Some(TEAM),
208
+ &transport,
209
+ Some(1_000),
210
+ );
211
+ if !matches!(&restart, Ok(RestartReport::Restarted { .. })) {
212
+ failures.push(format!(
213
+ "restart --allow-fresh: report must be Restarted, never readiness timeout; report={restart:?}"
214
+ ));
215
+ }
216
+ collect_lifecycle_cohort_result(
217
+ &mut failures,
218
+ "restart --allow-fresh",
219
+ restart.map(|_| ()),
220
+ &transport,
221
+ );
222
+ let restart_coordinator_pid = fs::read_to_string(coordinator_pid_path(&WorkspacePath::new(
223
+ workspace.clone(),
224
+ )))
225
+ .unwrap_or_default();
226
+ if restart_coordinator_pid.trim() != self_pid {
227
+ failures.push(format!(
228
+ "restart --allow-fresh: seeded coordinator pid must remain the integration-test self pid; expected={self_pid}; actual={restart_coordinator_pid:?}"
229
+ ));
230
+ }
231
+ let after = load_runtime_state(&workspace).unwrap();
232
+ for worker in ["developer", "test-engineer", "arch-reviewer"] {
233
+ let live = transport.live_panes_for_window(worker);
234
+ if live.len() != 1 {
235
+ failures.push(format!(
236
+ "restart --allow-fresh: {worker} must have one live tuple before state comparison; live={live:?}"
237
+ ));
238
+ continue;
239
+ }
240
+ let live = &live[0];
241
+ let saved = &after["agents"][worker];
242
+ let prior = &before["agents"][worker];
243
+ let saved_tuple = (
244
+ saved.get("window").and_then(Value::as_str),
245
+ saved.get("pane_id").and_then(Value::as_str),
246
+ saved.get("pane_pid").and_then(Value::as_u64),
247
+ );
248
+ let live_tuple = (
249
+ live.window_name.as_ref().map(WindowName::as_str),
250
+ Some(live.pane_id.as_str()),
251
+ live.pane_pid.map(u64::from),
252
+ );
253
+ if saved_tuple != live_tuple {
254
+ failures.push(format!(
255
+ "restart --allow-fresh: {worker} saved state tuple must equal its sole live tuple; saved={saved_tuple:?}; live={live_tuple:?}"
256
+ ));
257
+ }
258
+ let prior_pane = prior.get("pane_id").and_then(Value::as_str);
259
+ if saved.get("pane_id").and_then(Value::as_str) == prior_pane {
260
+ failures.push(format!(
261
+ "restart --allow-fresh: {worker} state must not remain bound to retired pane {prior_pane:?}"
262
+ ));
263
+ }
264
+ let prior_epoch = prior.get("spawn_epoch").and_then(Value::as_u64).unwrap_or(0);
265
+ let saved_epoch = saved.get("spawn_epoch").and_then(Value::as_u64).unwrap_or(0);
266
+ if saved_epoch <= prior_epoch {
267
+ failures.push(format!(
268
+ "restart --allow-fresh: {worker} spawn_epoch must increase monotonically; before={prior_epoch}; after={saved_epoch}"
269
+ ));
270
+ }
271
+ }
272
+ let (_env2, workspace2) = team_case("p0-start-force", &["developer"]);
273
+ let mut before_force = load_runtime_state(&workspace2).unwrap();
274
+ before_force["agents"]["developer"]["provider"] = json!("fake");
275
+ save_runtime_state(&workspace2, &before_force).unwrap();
276
+ let transport2 = CohortTransport::with_old_panes(&["developer"]);
277
+ collect_lifecycle_cohort_result(
278
+ &mut failures,
279
+ "start-agent --force",
280
+ start_agent_with_transport(&workspace2, &AgentId::new("developer"), true, false, true, Some(TEAM), &transport2).map(|_| ()),
281
+ &transport2,
282
+ );
283
+ let force_spawned = transport2
284
+ .pane_summary()
285
+ .into_iter()
286
+ .filter(|pane| pane.contains(":%new-"))
287
+ .collect::<Vec<_>>();
288
+ let after_force = load_runtime_state(&workspace2).unwrap();
289
+ if !force_spawned.is_empty()
290
+ || after_force["agents"]["developer"]["pane_id"]
291
+ != before_force["agents"]["developer"]["pane_id"]
292
+ {
293
+ failures.push(format!(
294
+ "start-agent --force: fake provider must share the same zero-mutation pre-spawn cohort refusal; spawned={force_spawned:?}; before={}; after={}",
295
+ before_force["agents"]["developer"], after_force["agents"]["developer"]
296
+ ));
297
+ }
298
+ let start_force_coordinator_pid = fs::read_to_string(coordinator_pid_path(
299
+ &WorkspacePath::new(workspace2.clone()),
300
+ ))
301
+ .unwrap_or_default();
302
+ if start_force_coordinator_pid.trim() != self_pid {
303
+ failures.push(format!(
304
+ "start-agent --force: seeded coordinator pid must remain the integration-test self pid; expected={self_pid}; actual={start_force_coordinator_pid:?}"
305
+ ));
306
+ }
307
+ let recorded_ps = fs::read_to_string(&recorder).unwrap_or_default();
308
+ let expected_probe = format!("-p\t{self_pid}\t-o\tstat=");
309
+ if !recorded_ps.lines().any(|line| line == expected_probe) {
310
+ failures.push(format!(
311
+ "fixture ps shim must observe the exact self-pid coordinator health probe {expected_probe:?}; recorded={recorded_ps:?}"
312
+ ));
313
+ }
314
+ assert!(
315
+ failures.is_empty(),
316
+ "P0 fixture1: lifecycle fresh/force paths must retire/swap/refuse so each (team_key, agent_id) has exactly one live tuple:\n{}",
317
+ failures.join("\n")
318
+ );
319
+ }
320
+
321
+ #[test]
322
+ #[serial(env)]
323
+ fn restart_bad_spawn_identity_fails_closed_before_stale_binding_and_new_orphan() {
324
+ let (_env, workspace) = team_case("p0-bad-spawn-identity", &["developer"]);
325
+ let before = load_runtime_state(&workspace).unwrap();
326
+ let old_pane = before["agents"]["developer"]["pane_id"]
327
+ .as_str()
328
+ .unwrap()
329
+ .to_string();
330
+ let old_epoch = before["agents"]["developer"]["spawn_epoch"]
331
+ .as_u64()
332
+ .unwrap();
333
+ let transport = CohortTransport::with_old_panes(&["developer"]).with_bad_spawn_identity();
334
+
335
+ let result = restart_with_transport_with_readiness_deadline(
336
+ &workspace,
337
+ true,
338
+ Some(TEAM),
339
+ &transport,
340
+ Some(0),
341
+ );
342
+ let report = format!("{result:?}");
343
+ let report_lower = report.to_ascii_lowercase();
344
+ let refused = !matches!(
345
+ &result,
346
+ Ok(RestartReport::Restarted { .. } | RestartReport::Partial { .. })
347
+ );
348
+ let named_identity_mismatch = report_lower.contains("spawn")
349
+ && (report_lower.contains("identity") || report_lower.contains("binding"))
350
+ && report_lower.contains("mismatch");
351
+ let after = load_runtime_state(&workspace).unwrap();
352
+ let saved = &after["agents"]["developer"];
353
+ let saved_pane = saved.get("pane_id").and_then(Value::as_str);
354
+ let saved_epoch = saved.get("spawn_epoch").and_then(Value::as_u64).unwrap_or(0);
355
+ let old_live = transport
356
+ .has_pane(&PaneId::new(old_pane.as_str()))
357
+ .unwrap()
358
+ .unwrap_or(false);
359
+ let new_live = transport
360
+ .live_panes_for_window("developer")
361
+ .into_iter()
362
+ .filter(|pane| pane.pane_id.as_str().starts_with("%new-"))
363
+ .map(|pane| pane.pane_id.as_str().to_string())
364
+ .collect::<Vec<_>>();
365
+ let stale_third_state = !old_live && saved_pane == Some(old_pane.as_str()) && !new_live.is_empty();
366
+ let old_redeclared_as_new = saved_pane == Some(old_pane.as_str()) && saved_epoch > old_epoch;
367
+
368
+ assert!(
369
+ refused && named_identity_mismatch && !stale_third_state && !old_redeclared_as_new,
370
+ "P0 bad-spawn-identity must fail closed with a spawn identity/binding mismatch and must never reach retired-old + state-bound-old + live-new-orphan third state; report={report}; old_live={old_live}; saved_pane={saved_pane:?}; epochs={old_epoch}->{saved_epoch}; new_live={new_live:?}; panes={:?}",
371
+ transport.pane_summary()
372
+ );
373
+ }
374
+
375
+ #[test]
376
+ #[serial(env)]
377
+ fn start_agent_spawn_verification_failure_rolls_back_new_pane() {
378
+ let (_env, workspace) = team_case("p0-start-spawn-rollback", &["developer"]);
379
+ let before = load_runtime_state(&workspace).unwrap();
380
+ let old_pane = before["agents"]["developer"]["pane_id"]
381
+ .as_str()
382
+ .unwrap()
383
+ .to_string();
384
+ let old_epoch = before["agents"]["developer"]["spawn_epoch"]
385
+ .as_u64()
386
+ .unwrap();
387
+ let transport = CohortTransport::with_old_panes(&["developer"]).with_hidden_new_panes();
388
+ transport
389
+ .kill_pane(&PaneId::new(old_pane.as_str()))
390
+ .unwrap();
391
+
392
+ let result = start_agent_with_transport(
393
+ &workspace,
394
+ &AgentId::new("developer"),
395
+ true,
396
+ false,
397
+ true,
398
+ Some(TEAM),
399
+ &transport,
400
+ );
401
+ let report = format!("{result:?}");
402
+ let after = load_runtime_state(&workspace).unwrap();
403
+ let live_new = transport
404
+ .live_panes_for_window("developer")
405
+ .into_iter()
406
+ .filter(|pane| pane.pane_id.as_str().starts_with("%new-"))
407
+ .map(|pane| pane.pane_id.as_str().to_string())
408
+ .collect::<Vec<_>>();
409
+
410
+ assert!(
411
+ result.is_err() && report.contains("spawned pane not addressable"),
412
+ "spawn verification must fail closed with identity evidence; report={report}"
413
+ );
414
+ assert!(
415
+ live_new.is_empty(),
416
+ "a failed start-agent must kill only the pane created by that attempt; live_new={live_new:?}; panes={:?}",
417
+ transport.pane_summary()
418
+ );
419
+ assert_eq!(after["agents"]["developer"]["pane_id"], old_pane);
420
+ assert_eq!(
421
+ after["agents"]["developer"]["spawn_epoch"], old_epoch,
422
+ "failed replacement must leave persisted state authoritative"
423
+ );
424
+ }
425
+
426
+ #[test]
427
+ #[serial(env)]
428
+ fn start_agent_list_targets_failure_refuses_before_spawn() {
429
+ let (_env, workspace) = team_case("p0-start-observation-failure", &["developer"]);
430
+ let before = load_runtime_state(&workspace).unwrap();
431
+ let transport = CohortTransport::with_old_panes(&["developer"]).with_list_targets_failure();
432
+
433
+ let result = start_agent_with_transport(
434
+ &workspace,
435
+ &AgentId::new("developer"),
436
+ true,
437
+ false,
438
+ true,
439
+ Some(TEAM),
440
+ &transport,
441
+ );
442
+ let after = load_runtime_state(&workspace).unwrap();
443
+ let report = format!("{result:?}");
444
+
445
+ assert!(
446
+ result.is_err() && report.contains("same-role cohort observation failed"),
447
+ "an unobservable cohort must refuse before spawn; report={report}"
448
+ );
449
+ assert_eq!(after["agents"]["developer"], before["agents"]["developer"]);
450
+ assert!(
451
+ transport
452
+ .pane_summary()
453
+ .iter()
454
+ .all(|pane| !pane.contains(":%new-")),
455
+ "observation failure must not spawn a pane; panes={:?}",
456
+ transport.pane_summary()
457
+ );
458
+ }
459
+
460
+ #[test]
461
+ #[serial(env)]
462
+ fn codex_capture_rejects_old_rollout_with_new_mtime_and_uses_typed_claim_keys() {
463
+ let env = hermetic_guard::HermeticTestEnv::enter("p0-capture");
464
+ let spawn_cwd = env.workspace("spawn-cwd");
465
+ let rollout = write_old_codex_rollout(env.home(), &spawn_cwd);
466
+ fs::OpenOptions::new()
467
+ .append(true)
468
+ .open(&rollout)
469
+ .unwrap()
470
+ .write_all(b"{\"type\":\"event\",\"payload\":{\"append\":\"after spawn\"}}\n")
471
+ .unwrap();
472
+
473
+ let context = CaptureSessionContext {
474
+ agent_id: "developer".to_string(),
475
+ spawn_cwd: spawn_cwd.clone(),
476
+ pane_id: Some("%42".to_string()),
477
+ pane_pid: Some(4242),
478
+ spawned_at: Some("2026-01-02T00:00:00+00:00".to_string()),
479
+ expected_session_id: None,
480
+ provider_projects_root: None,
481
+ };
482
+ let candidates = get_adapter(Provider::Codex)
483
+ .capture_session_candidates(&context, 0)
484
+ .expect("scan codex candidates");
485
+
486
+ let capture_src = include_str!(concat!(
487
+ env!("CARGO_MANIFEST_DIR"),
488
+ "/src/provider/session/capture.rs"
489
+ ));
490
+ let mut failures = Vec::new();
491
+ if candidates.iter().any(|candidate| {
492
+ candidate
493
+ .captured
494
+ .rollout_path
495
+ .as_ref()
496
+ .is_some_and(|path| path.as_path() == rollout)
497
+ }) {
498
+ failures.push(format!(
499
+ "old rollout was accepted as a fresh candidate: filename/created_at predate spawn, only mutable mtime is new; candidates={candidates:?}"
500
+ ));
501
+ }
502
+ if !capture_src.contains("SessionAttributionKey")
503
+ || capture_src.contains("format!(\"session:{}\"")
504
+ || capture_src.contains("format!(\"rollout:{}\"")
505
+ {
506
+ failures.push(
507
+ "claimed/captured keys must be typed by provider+team_key+agent_id(+session/rollout), not bare session:/rollout: strings".to_string(),
508
+ );
509
+ }
510
+
511
+ assert!(
512
+ failures.is_empty(),
513
+ "P0 capture contract failed:\n{}",
514
+ failures.join("\n")
515
+ );
516
+ }
517
+
518
+ #[test]
519
+ #[serial(env)]
520
+ fn reset_stopped_true_still_rechecks_same_role_residue_before_start() {
521
+ let (_env, workspace) = team_case("p0-reset", &["developer"]);
522
+ let transport = CohortTransport::with_old_panes(&["developer"]);
523
+
524
+ let result = reset_agent_with_transport(&workspace, &AgentId::new("developer"), true, false, Some(TEAM), &transport);
525
+ let error_text = result.as_ref().err().map(ToString::to_string).unwrap_or_default();
526
+ assert!(
527
+ error_text.contains("stop_not_proven")
528
+ || error_text.contains("residue")
529
+ || error_text.contains("same-role"),
530
+ "P0 reset: stop.stopped=true is not proof when the same-role pane remains visible; reset must return a named residue proof error (stop_not_proven/residue/same-role) before spawning; result={result:?} error={error_text:?} panes={:?}",
531
+ transport.pane_summary()
532
+ );
533
+ }
534
+
535
+ #[test]
536
+ fn mcp_reset_output_exposes_capture_outcome_and_weak_reset_proof() {
537
+ let src = include_str!(concat!(
538
+ env!("CARGO_MANIFEST_DIR"),
539
+ "/src/mcp_server/lifecycle_tools/agent_ops.rs"
540
+ ));
541
+ let reset = source_section(src, "pub(crate) fn reset_agent", "pub(crate) fn fork_agent");
542
+ let json_object = reset_success_json_object(reset);
543
+ let missing = ["\"capture_state\":", "\"reset_proof\":", "\"transcript_missing\"", "\"attribution_ambiguous\"", "\"weak\""]
544
+ .into_iter().filter(|needle| !json_object.contains(needle)).collect::<Vec<_>>();
545
+ assert!(
546
+ missing.is_empty(),
547
+ "P0 MCP reset: ok=true/status=reset must expose additive capture_state and reset_proof=weak inside the ResetAgentOutcome::Reset serde_json::json! object; missing={missing:?}; reset_json={json_object}"
548
+ );
549
+ }
550
+
551
+ #[test]
552
+ #[serial(env)]
553
+ fn abnormal_shell_pane_with_live_delivery_is_not_dead_and_dedupe_ignores_size() {
554
+ let env = hermetic_guard::HermeticTestEnv::enter("p0-abnormal");
555
+ let workspace = env.workspace("abnormal");
556
+ let rollout = workspace.join("architect-rollout.jsonl");
557
+ fs::write(&rollout, "{\"type\":\"session_meta\",\"payload\":{\"id\":\"sess-arch\",\"cwd\":\"/tmp\"}}\n").unwrap();
558
+ seed_healthy_coordinator(&workspace);
559
+ save_runtime_state(
560
+ &workspace,
561
+ &json!({
562
+ "active_team_key": TEAM,
563
+ "session_name": SESSION,
564
+ "agents": {
565
+ "architect": {
566
+ "status": "running",
567
+ "provider": "codex",
568
+ "agent_id": "architect",
569
+ "window": "architect",
570
+ "pane_id": "%old-architect",
571
+ "session_id": "sess-arch",
572
+ "rollout_path": rollout.to_string_lossy(),
573
+ "spawn_epoch": 7,
574
+ "spawn_cwd": workspace.to_string_lossy()
575
+ }
576
+ }
577
+ }),
578
+ )
579
+ .unwrap();
580
+ let transport = CohortTransport::with_old_panes(&["architect"]).with_current_command("bash");
581
+ transport
582
+ .inject(
583
+ &Target::Pane(PaneId::new("%old-architect")),
584
+ &InjectPayload::Text("ping live tuple".to_string()),
585
+ Key::Enter,
586
+ false,
587
+ )
588
+ .expect("fixture proves the same physical tuple can receive a message");
589
+ let coord = Coordinator::new(
590
+ WorkspacePath::new(workspace.clone()),
591
+ Box::new(RealAdapterRegistry),
592
+ Box::new(transport.clone()),
593
+ );
594
+ let tick = coord.tick();
595
+
596
+ let state = load_runtime_state(&workspace).unwrap();
597
+ let watch = state
598
+ .pointer("/coordinator/abnormal_exit_watch/architect")
599
+ .cloned()
600
+ .unwrap_or(Value::Null);
601
+ let src = include_str!(concat!(
602
+ env!("CARGO_MANIFEST_DIR"),
603
+ "/src/coordinator/steps/abnormal.rs"
604
+ ));
605
+ let mut failures = Vec::new();
606
+ if let Err(error) = tick { failures.push(format!("coordinator tick failed before abnormal fixture could be evaluated: {error}")); }
607
+ if watch.is_null() { failures.push("coordinator tick did not write abnormal_exit_watch for the live shell fixture".to_string()); }
608
+ if watch.pointer("/last_liveness").and_then(Value::as_str) == Some("dead")
609
+ || watch
610
+ .pointer("/last_liveness_detail")
611
+ .and_then(Value::as_str)
612
+ .is_some_and(|detail| detail.contains("provider_not_foreground:bash"))
613
+ {
614
+ failures.push(format!(
615
+ "live wrapper shell with no exit marker was classified dead; watch={watch}"
616
+ ));
617
+ }
618
+ for needle in [
619
+ "fn abnormal_dedupe_key(\n agent: &AbnormalWatchAgent,\n fact: &crate::provider::FaultFact,\n size: u64",
620
+ "fn abnormal_suppression_key(\n agent: &AbnormalWatchAgent,\n liveness: &ProcessCheck,\n reason: &str,\n size: u64",
621
+ "fn abnormal_check_key(\n agent: &AbnormalWatchAgent,\n liveness: &ProcessCheck,\n fact: Option<&crate::provider::FaultFact>,\n error_recency: ErrorRecency,\n error_observation_key: Option<&str>,\n size: u64",
622
+ "unwrap_or_else(|| size.to_string())",
623
+ ] {
624
+ if src.contains(needle) {
625
+ failures.push(format!("abnormal dedupe/check identity still includes mutable rollout size marker `{needle}`"));
626
+ }
627
+ }
628
+ assert!(
629
+ failures.is_empty(),
630
+ "P0 abnormal contract failed:\n{}",
631
+ failures.join("\n")
632
+ );
633
+ }
634
+
635
+ #[test]
636
+ #[serial(env)]
637
+ fn status_model_uses_effective_codex_source_not_stale_claude_state() {
638
+ let (_env, workspace) = team_case_with_model("p0-status", &[("developer", "gpt-5.6-sol")]);
639
+ let mut state = load_runtime_state(&workspace).unwrap();
640
+ state["agents"]["developer"]["provider"] = json!("codex");
641
+ state["agents"]["developer"]["model"] = json!("claude-opus-4-7");
642
+ save_runtime_state(&workspace, &state).unwrap();
643
+
644
+ let status = team_agent::cli::status_port::status(&workspace, false, true)
645
+ .expect("status --json --detail");
646
+ let agent = status.pointer("/agents/developer").unwrap_or(&Value::Null);
647
+ let model = agent.get("model").and_then(Value::as_str);
648
+ let model_stale = agent.get("model_stale").and_then(Value::as_bool) == Some(true);
649
+ let model_source = agent.get("model_source").and_then(Value::as_str);
650
+ assert!(
651
+ model == Some("gpt-5.6-sol") || model_stale || model_source == Some("role"),
652
+ "P0 provider/model projection: Codex seat must not render stale Claude model as truth; agent={agent} status={status}"
653
+ );
654
+ }
655
+
656
+ fn team_case(tag: &str, workers: &[&str]) -> (hermetic_guard::HermeticTestEnv, PathBuf) {
657
+ let pairs = workers.iter().map(|worker| (*worker, "gpt-5")).collect::<Vec<_>>();
658
+ team_case_with_model(tag, &pairs)
659
+ }
660
+
661
+ fn team_case_with_model(
662
+ tag: &str,
663
+ workers: &[(&str, &str)],
664
+ ) -> (hermetic_guard::HermeticTestEnv, PathBuf) {
665
+ let env = hermetic_guard::HermeticTestEnv::enter(tag);
666
+ env.scrub_tmux();
667
+ env.assert_no_real_tmux();
668
+ let workspace = env.workspace(tag);
669
+ write_team_docs(&workspace, workers);
670
+ write_runtime_spec(&workspace);
671
+ seed_state(&workspace, workers);
672
+ seed_healthy_coordinator(&workspace);
673
+ (env, workspace)
674
+ }
675
+
676
+ fn write_team_docs(workspace: &Path, workers: &[(&str, &str)]) {
677
+ fs::create_dir_all(workspace.join("agents")).unwrap();
678
+ fs::write(workspace.join("TEAM.md"), "---\nname: current\nobjective: P0 host co-tenant contract.\nprovider: codex\n---\n").unwrap();
679
+ for (worker, model) in workers {
680
+ let role = format!("---\nname: {worker}\nrole: {worker}\nprovider: codex\nmodel: {model}\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\n{worker}.\n");
681
+ fs::write(workspace.join("agents").join(format!("{worker}.md")), role).unwrap();
682
+ }
683
+ }
684
+
685
+ fn write_runtime_spec(workspace: &Path) {
686
+ let spec = team_agent::compiler::compile_team(workspace).unwrap();
687
+ let spec_text = team_agent::model::yaml::dumps(&spec);
688
+ fs::write(workspace.join("team.spec.yaml"), &spec_text).unwrap();
689
+ let path = runtime_spec_path(workspace, TEAM);
690
+ fs::create_dir_all(path.parent().unwrap()).unwrap();
691
+ fs::write(path, spec_text).unwrap();
692
+ }
693
+
694
+ fn seed_state(workspace: &Path, workers: &[(&str, &str)]) {
695
+ let agents = workers
696
+ .iter()
697
+ .map(|(worker, model)| ((*worker).to_string(), json!({
698
+ "status": "running", "provider": "codex", "agent_id": worker, "role": worker,
699
+ "model": model, "auth_mode": "subscription", "tools": ["mcp_team"],
700
+ "window": worker, "pane_id": format!("%old-{worker}"), "pane_pid": 10_000,
701
+ "owner_team_id": TEAM,
702
+ "session_id": format!("sess-{worker}"),
703
+ "rollout_path": workspace.join(format!("{worker}.jsonl")).to_string_lossy(),
704
+ "spawn_epoch": 1, "spawn_cwd": workspace.to_string_lossy()
705
+ })))
706
+ .collect::<serde_json::Map<_, _>>();
707
+ for (worker, _) in workers {
708
+ fs::write(workspace.join(format!("{worker}.jsonl")), "{}\n").unwrap();
709
+ }
710
+ save_runtime_state(workspace, &json!({
711
+ "active_team_key": TEAM, "team_dir": workspace.to_string_lossy(),
712
+ "spec_path": workspace.join("team.spec.yaml").to_string_lossy(),
713
+ "session_name": SESSION, "agents": agents
714
+ })).unwrap();
715
+ }
716
+
717
+ fn seed_healthy_coordinator(workspace: &Path) {
718
+ fs::create_dir_all(runtime_dir(workspace)).unwrap();
719
+ let _ = MessageStore::open(workspace).unwrap();
720
+ let workspace = WorkspacePath::new(workspace.to_path_buf());
721
+ let pid = Pid::new(std::process::id());
722
+ team_agent::coordinator::write_coordinator_metadata(&workspace, pid, MetadataSource::Boot)
723
+ .unwrap();
724
+ fs::write(coordinator_pid_path(&workspace), pid.to_string()).unwrap();
725
+ }
726
+
727
+ fn write_old_codex_rollout(home: &Path, spawn_cwd: &Path) -> PathBuf {
728
+ let dir = home.join(".codex/sessions/2026/01/01");
729
+ fs::create_dir_all(&dir).unwrap();
730
+ let rollout = dir.join("rollout-2026-01-01T00-00-00-old.jsonl");
731
+ let meta = json!({"type": "session_meta", "payload": {
732
+ "id": "old-session-before-spawn", "cwd": spawn_cwd.to_string_lossy(),
733
+ "created_at": "2026-01-01T00:00:00+00:00"
734
+ }});
735
+ fs::write(&rollout, format!("{meta}\n{}\n", json!({"type": "event", "payload": {"message": "old rollout"}}))).unwrap();
736
+ rollout
737
+ }
738
+
739
+ #[derive(Clone)]
740
+ struct CohortTransport {
741
+ state: Arc<Mutex<TransportState>>,
742
+ bad_spawn_identity: bool,
743
+ hide_new_from_list_targets: bool,
744
+ fail_list_targets: bool,
745
+ }
746
+
747
+ struct TransportState {
748
+ panes: Vec<PaneInfo>,
749
+ killed: BTreeSet<String>,
750
+ next: usize,
751
+ current_command: String,
752
+ }
753
+
754
+ impl CohortTransport {
755
+ fn with_old_panes(workers: &[&str]) -> Self {
756
+ let panes = workers.iter().map(|w| pane_info(w, &format!("%old-{w}"), "team-agent-worker")).collect();
757
+ Self {
758
+ state: Arc::new(Mutex::new(TransportState { panes, killed: BTreeSet::new(), next: 1, current_command: "team-agent-worker".to_string() })),
759
+ bad_spawn_identity: false,
760
+ hide_new_from_list_targets: false,
761
+ fail_list_targets: false,
762
+ }
763
+ }
764
+
765
+ fn with_bad_spawn_identity(mut self) -> Self {
766
+ self.bad_spawn_identity = true;
767
+ self
768
+ }
769
+
770
+ fn with_hidden_new_panes(mut self) -> Self {
771
+ self.hide_new_from_list_targets = true;
772
+ self
773
+ }
774
+
775
+ fn with_list_targets_failure(mut self) -> Self {
776
+ self.fail_list_targets = true;
777
+ self
778
+ }
779
+
780
+ fn with_current_command(self, command: &str) -> Self {
781
+ let mut state = self.state.lock().unwrap();
782
+ state.current_command = command.to_string();
783
+ for pane in &mut state.panes { pane.current_command = Some(command.to_string()); }
784
+ drop(state);
785
+ self
786
+ }
787
+
788
+ fn spawn(&self, session: &SessionName, window: &WindowName) -> SpawnResult {
789
+ let mut state = self.state.lock().unwrap();
790
+ let old_identity = state.panes.iter().find(|pane| {
791
+ pane.window_name.as_ref().is_some_and(|name| name == window)
792
+ && pane.pane_id.as_str().starts_with("%old-")
793
+ && !state.killed.contains(pane.pane_id.as_str())
794
+ }).map(|pane| (pane.pane_id.clone(), pane.pane_pid));
795
+ let pane_id = PaneId::new(format!("%new-{}-{}", window.as_str(), state.next));
796
+ let pane_pid = 20_000 + u32::try_from(state.next).unwrap();
797
+ state.next += 1;
798
+ let current_command = state.current_command.clone();
799
+ state.panes.push(pane_info_with_pid(
800
+ window.as_str(),
801
+ pane_id.as_str(),
802
+ current_command.as_str(),
803
+ pane_pid,
804
+ ));
805
+ if self.bad_spawn_identity {
806
+ let (old_pane, old_pid) = old_identity.expect("bad identity fixture needs old pane");
807
+ SpawnResult { pane_id: old_pane, session: session.clone(), window: window.clone(), child_pid: old_pid }
808
+ } else {
809
+ SpawnResult { pane_id, session: session.clone(), window: window.clone(), child_pid: Some(pane_pid) }
810
+ }
811
+ }
812
+
813
+ fn pane_summary(&self) -> Vec<String> {
814
+ self.state.lock().unwrap().panes.iter().map(|p| {
815
+ format!("{}:{}", p.window_name.as_ref().map(WindowName::as_str).unwrap_or("-"), p.pane_id.as_str())
816
+ }).collect()
817
+ }
818
+
819
+ fn window_counts(&self) -> BTreeMap<String, usize> {
820
+ let mut counts = BTreeMap::new();
821
+ let state = self.state.lock().unwrap();
822
+ for pane in state.panes.iter().filter(|pane| !state.killed.contains(pane.pane_id.as_str())) {
823
+ if let Some(window) = pane.window_name.as_ref() { *counts.entry(window.as_str().to_string()).or_insert(0) += 1; }
824
+ }
825
+ counts
826
+ }
827
+
828
+ fn live_panes_for_window(&self, window: &str) -> Vec<PaneInfo> {
829
+ let state = self.state.lock().unwrap();
830
+ state.panes.iter().filter(|pane| {
831
+ !state.killed.contains(pane.pane_id.as_str())
832
+ && pane.window_name.as_ref().is_some_and(|name| name.as_str() == window)
833
+ }).cloned().collect()
834
+ }
835
+ }
836
+
837
+ fn collect_lifecycle_cohort_result(
838
+ failures: &mut Vec<String>,
839
+ label: &str,
840
+ result: Result<(), LifecycleError>,
841
+ transport: &CohortTransport,
842
+ ) {
843
+ let spawned = transport.pane_summary().into_iter().filter(|pane| pane.contains(":%new-")).collect::<Vec<_>>();
844
+ if let Err(error) = result {
845
+ let text = error.to_string();
846
+ if spawned.is_empty()
847
+ && (text.contains("cohort") || text.contains("same-role") || text.contains("duplicate"))
848
+ {
849
+ return;
850
+ }
851
+ failures.push(if spawned.is_empty() {
852
+ format!("{label}: refusal must name cohort/duplicate proof; error={text}")
853
+ } else {
854
+ format!("{label}: refusal happened after spawning {spawned:?}, so it is not a safe pre-spawn cohort/duplicate refusal; error={text}; panes={:?}", transport.pane_summary())
855
+ });
856
+ return;
857
+ }
858
+ let duplicates = transport.window_counts().into_iter().filter(|(_, count)| *count != 1).map(|(window, count)| format!("{window}={count}")).collect::<Vec<_>>();
859
+ if !duplicates.is_empty() {
860
+ failures.push(format!(
861
+ "{label}: duplicate live tuple(s) {duplicates:?}; panes={:?}",
862
+ transport.pane_summary()
863
+ ));
864
+ }
865
+ }
866
+
867
+ impl Transport for CohortTransport {
868
+ fn kind(&self) -> BackendKind { BackendKind::Tmux }
869
+
870
+ fn tmux_endpoint(&self) -> Option<String> { Some(TMUX_ENDPOINT.to_string()) }
871
+
872
+ fn spawn_first(
873
+ &self,
874
+ session: &SessionName,
875
+ window: &WindowName,
876
+ _argv: &[String],
877
+ _cwd: &Path,
878
+ _env: &BTreeMap<String, String>,
879
+ ) -> Result<SpawnResult, TransportError> {
880
+ Ok(self.spawn(session, window))
881
+ }
882
+
883
+ fn spawn_into(&self, session: &SessionName, window: &WindowName, argv: &[String], cwd: &Path, env: &BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
884
+ self.spawn_first(session, window, argv, cwd, env)
885
+ }
886
+
887
+ fn inject(&self, _target: &Target, _payload: &InjectPayload, _submit: Key, _bracketed: bool) -> Result<InjectReport, TransportError> {
888
+ Ok(InjectReport { stage_reached: InjectStage::Submit, inject_verification: InjectVerification::CaptureContainsToken, submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck, turn_verification: TurnVerification::NotRequired, attempts: 1, submit_diagnostics: None })
889
+ }
890
+
891
+ fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> { Ok(()) }
892
+
893
+ fn capture(&self, _target: &Target, range: CaptureRange) -> Result<CapturedText, TransportError> {
894
+ Ok(CapturedText { text: "wrapper shell prompt without team-agent provider exit marker".to_string(), range })
895
+ }
896
+
897
+ fn query(&self, _target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
898
+ Ok(match field {
899
+ PaneField::PaneId => Some("%query".to_string()),
900
+ PaneField::SessionName => Some(SESSION.to_string()),
901
+ PaneField::PaneCurrentCommand => Some(self.state.lock().unwrap().current_command.clone()),
902
+ _ => None,
903
+ })
904
+ }
905
+
906
+ fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
907
+ Ok(if self.has_pane(pane)? == Some(true) { PaneLiveness::Live } else { PaneLiveness::Dead })
908
+ }
909
+
910
+ fn has_pane(&self, pane: &PaneId) -> Result<Option<bool>, TransportError> {
911
+ let state = self.state.lock().unwrap();
912
+ Ok(Some(!state.killed.contains(pane.as_str()) && state.panes.iter().any(|item| item.pane_id == *pane)))
913
+ }
914
+
915
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
916
+ if self.fail_list_targets {
917
+ return Err(TransportError::MuxUnavailable {
918
+ backend: BackendKind::Tmux,
919
+ detail: "injected list-targets failure".to_string(),
920
+ });
921
+ }
922
+ let state = self.state.lock().unwrap();
923
+ Ok(state.panes.iter().filter(|pane| {
924
+ !self.hide_new_from_list_targets || !pane.pane_id.as_str().starts_with("%new-")
925
+ }).cloned().collect())
926
+ }
927
+
928
+ fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> { Ok(true) }
929
+
930
+ fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
931
+ Ok(self.state.lock().unwrap().panes.iter().filter_map(|pane| pane.window_name.clone()).collect::<BTreeSet<_>>().into_iter().collect())
932
+ }
933
+
934
+ fn set_session_env(&self, _session: &SessionName, _key: &str, _value: &str) -> Result<SetEnvOutcome, TransportError> { Ok(SetEnvOutcome::Applied) }
935
+
936
+ fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> { Ok(()) }
937
+
938
+ fn kill_window(&self, target: &Target) -> Result<(), TransportError> {
939
+ if let Target::Pane(pane) = target { self.kill_pane(pane)?; }
940
+ Ok(())
941
+ }
942
+
943
+ fn kill_pane(&self, pane: &PaneId) -> Result<(), TransportError> {
944
+ self.state.lock().unwrap().killed.insert(pane.as_str().to_string());
945
+ Ok(())
946
+ }
947
+
948
+ fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> { Ok(AttachOutcome::Attached) }
949
+ }
950
+
951
+ fn pane_info(window: &str, pane: &str, current_command: &str) -> PaneInfo {
952
+ pane_info_with_pid(window, pane, current_command, 10_000)
953
+ }
954
+
955
+ fn pane_info_with_pid(window: &str, pane: &str, current_command: &str, pane_pid: u32) -> PaneInfo {
956
+ PaneInfo {
957
+ pane_id: PaneId::new(pane),
958
+ session: SessionName::new(SESSION),
959
+ window_name: Some(WindowName::new(window)),
960
+ window_index: None,
961
+ pane_index: None, tty: None,
962
+ current_command: Some(current_command.to_string()),
963
+ current_path: None, active: true, pane_pid: Some(pane_pid),
964
+ leader_env: BTreeMap::new(),
965
+ }
966
+ }
967
+
968
+ struct RealAdapterRegistry;
969
+
970
+ impl ProviderRegistry for RealAdapterRegistry {
971
+ fn adapter_for(&self, provider: Provider) -> Box<dyn ProviderAdapter> {
972
+ get_adapter(provider)
973
+ }
974
+ }
975
+
976
+ fn source_section<'a>(src: &'a str, start: &str, end: &str) -> &'a str { let start = src.find(start).unwrap_or(0); let rest = &src[start..]; &rest[..rest.find(end).unwrap_or(rest.len())] }
977
+
978
+ fn reset_success_json_object(reset_src: &str) -> &str {
979
+ let success = source_section(reset_src, "ResetAgentOutcome::Reset", "ResetAgentOutcome::Refused");
980
+ let macro_start = success.find("serde_json::json!({").expect("reset success json object"); let object_start = macro_start + success[macro_start..].find('{').unwrap(); let mut depth = 0_i32;
981
+ for (offset, ch) in success[object_start..].char_indices() {
982
+ if ch == '{' { depth += 1; } if ch == '}' { depth -= 1; }
983
+ if depth == 0 { return &success[object_start..object_start + offset + 1]; }
984
+ }
985
+ panic!("unterminated reset success json object")
986
+ }