@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,603 @@
1
+ //! team-key slice2b · L1 — retirement tombstone TRANSACTION (RED).
2
+ //!
3
+ //! From owner-b's real product shape (no new public API; a single remove.rs
4
+ //! tombstone primitive writes/clears `agent_lifecycle.<id>.state=retired` inside
5
+ //! the SAME selected-state save, reusing add --force's single-lock
6
+ //! snapshot->rollback fact) + locate §6.2, NOT the root-cause reasoning. On the
7
+ //! public injected-transport surface (pure state, no real tmux). Reads go through
8
+ //! the CANONICAL team-scoped projection, and remove asserts at the OUTCOME enum
9
+ //! level, so no case reds for a fixture artifact (refused-not-executed / nested-
10
+ //! read-null). Two ACTIVE L1 REDs + one candidate regression guard; two scenarios
11
+ //! deferred to reachable layers (see inline NOTEs):
12
+ //! 1. [RED] remove leaves `state=retired` for the id;
13
+ //! 2. [RED] a plain re-add clears the tombstone (transition pinned: tombstone
14
+ //! must EXIST after remove, then be cleared — no vacuous terminal-None pass);
15
+ //! - [deferred] failed PLAIN re-add rollback: unreachable at L1 (no plain-add
16
+ //! fail seam) -> L3 real-machine re-add case;
17
+ //! 3. [regression guard, green at baseline] a force remove->add whose post-
18
+ //! consumption add fails restores the seat AND leaks no stray tombstone;
19
+ //! - [deferred] same-id sibling non-interference: cannot red for the right
20
+ //! reason at a no-semantics baseline -> L3 + owner-b scoped src unit.
21
+
22
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
23
+
24
+ #[path = "../../../tests/support/hermetic.rs"]
25
+ mod hermetic_guard;
26
+ #[allow(dead_code)]
27
+ fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
28
+
29
+ use std::collections::{BTreeMap, BTreeSet};
30
+ use std::path::{Path, PathBuf};
31
+ use std::sync::atomic::{AtomicU64, Ordering};
32
+ use std::sync::{Arc, Mutex};
33
+
34
+ use rusqlite::params;
35
+ use serde_json::json;
36
+ use serial_test::serial;
37
+ use team_agent::coordinator::{
38
+ coordinator_pid_path, write_coordinator_metadata, MetadataSource, Pid, WorkspacePath,
39
+ };
40
+ use team_agent::db::schema::open_db;
41
+ use team_agent::lifecycle::{
42
+ add_agent_with_transport, add_agent_with_transport_force, fork_agent_with_transport,
43
+ remove_agent_with_transport, RemoveAgentOutcome,
44
+ };
45
+ use team_agent::message_store::MessageStore;
46
+ use team_agent::model::ids::AgentId;
47
+ use team_agent::state::persist::{load_runtime_state, save_runtime_state};
48
+ use team_agent::transport::{
49
+ AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
50
+ InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, PaneLiveness, SessionName,
51
+ SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
52
+ TurnVerification, WindowName,
53
+ };
54
+
55
+ const FAIL_REMOVE_AFTER_HEALTH_DELETE: &str =
56
+ "TEAM_AGENT_TEST_FAIL_REMOVE_AFTER_AGENT_HEALTH_DELETE";
57
+ const FAIL_FORK_AFTER_SPAWN: &str = "TEAM_AGENT_TEST_FAIL_FORK_AFTER_SPAWN";
58
+ const FAIL_FORCE_RECREATE_AFTER_SPAWN: &str = "TEAM_AGENT_TEST_FAIL_FORCE_RECREATE_AFTER_SPAWN";
59
+
60
+ /// Read the retirement tombstone via the CANONICAL team-scoped projection, not a
61
+ /// hard-coded `teams.<key>` nested path: the legacy single-team seed keeps agents
62
+ /// at state root and the scoped writer's compat leg does not physicalize `teams`,
63
+ /// so a nested read would always be None regardless of product correctness.
64
+ fn retired_state(ws: &Path, key: &str, id: &str) -> Option<String> {
65
+ team_agent::state::projection::select_runtime_state(ws, Some(key))
66
+ .ok()?
67
+ .get("agent_lifecycle")?
68
+ .get(id)?
69
+ .get("state")?
70
+ .as_str()
71
+ .map(str::to_string)
72
+ }
73
+
74
+ fn role_path(fixture: &RollbackFixture, id: &str) -> PathBuf {
75
+ let p = fixture.team.join(format!("agents/{id}.md"));
76
+ std::fs::write(&p, agent_doc(id, &format!("{id} worker"))).unwrap();
77
+ p
78
+ }
79
+
80
+ #[test]
81
+ #[serial(env)]
82
+ fn remove_writes_retired_tombstone() {
83
+ let fixture = RollbackFixture::new("txn-remove");
84
+ fixture.seed_team();
85
+ // alpha is a state+spec seat with NO live pane (transport carries only bravo),
86
+ // so from_spec=true alone removes it cleanly. Assert at the OUTCOME enum level
87
+ // so an `Ok(Refused*)` cannot be swallowed as success (fixture-artifact red).
88
+ let transport = RecordingTransport::new("team-rollbackteam", &["bravo"]);
89
+ let outcome =
90
+ remove_agent_with_transport(&fixture.team, &aid("alpha"), true, false, None, &transport)
91
+ .expect("remove alpha must not error");
92
+ assert!(
93
+ matches!(outcome, RemoveAgentOutcome::Removed { .. }),
94
+ "remove must actually execute (Removed), not refuse; got {outcome:?}"
95
+ );
96
+ assert_eq!(
97
+ retired_state(&fixture.team, "rollbackteam", "alpha").as_deref(),
98
+ Some("retired"),
99
+ "a successful remove must write agent_lifecycle.alpha.state=retired in the same save"
100
+ );
101
+ }
102
+
103
+ #[test]
104
+ #[serial(env)]
105
+ fn plain_re_add_clears_the_tombstone() {
106
+ let fixture = RollbackFixture::new("txn-readd");
107
+ fixture.seed_team();
108
+ let transport = RecordingTransport::new("team-rollbackteam", &["bravo"]);
109
+ let outcome =
110
+ remove_agent_with_transport(&fixture.team, &aid("alpha"), true, false, None, &transport)
111
+ .expect("remove alpha must not error");
112
+ assert!(
113
+ matches!(outcome, RemoveAgentOutcome::Removed { .. }),
114
+ "remove must actually execute (Removed) before re-add; got {outcome:?}"
115
+ );
116
+ // Pin the STATE TRANSITION, not just the terminal None: the tombstone must
117
+ // first EXIST after remove (else the final `None` passes vacuously on a
118
+ // no-tombstone baseline = false green), then be cleared by the re-add.
119
+ assert_eq!(
120
+ retired_state(&fixture.team, "rollbackteam", "alpha").as_deref(),
121
+ Some("retired"),
122
+ "precondition: remove must have written the retired tombstone to clear"
123
+ );
124
+ let role = role_path(&fixture, "alpha");
125
+ add_agent_with_transport(&fixture.team, &aid("alpha"), &role, false, None, &transport)
126
+ .expect("re-add alpha");
127
+ assert_eq!(
128
+ retired_state(&fixture.team, "rollbackteam", "alpha"),
129
+ None,
130
+ "a successful re-add must clear the retired tombstone for the id"
131
+ );
132
+ }
133
+
134
+ // NOTE (honest reachability): a "failed PLAIN re-add restores the tombstone"
135
+ // scenario is NOT reachable at this L1 pure-transport surface. The only failure
136
+ // seam after spawn (`TEAM_AGENT_TEST_FAIL_FORCE_RECREATE_AFTER_SPAWN`) is read
137
+ // ONLY on the force-recreate path (`force_recreate_with_transport_locked`), so a
138
+ // plain `add_agent_with_transport` cannot be made to fail here — injecting that
139
+ // env would be a no-op and the case would red for the wrong reason (fixture
140
+ // artifact, not the guarded rollback fault). The failed-re-add rollback that
141
+ // restores the tombstone is contracted where a real add failure is reachable:
142
+ // the L3 real-machine re-add case. The reachable single-lock rollback at L1 is
143
+ // the force remove->add case below.
144
+
145
+ /// Candidate-side REGRESSION GUARD (not a RED): this case is GREEN at baseline
146
+ /// on purpose. The force-recreate snapshot restore predates slice2b (the seat
147
+ /// already survives a failed post-consumption add), and with no tombstone
148
+ /// semantics there is nothing to leak — so neither assert can red at baseline.
149
+ /// Its job is to pin, once the candidate DOES write tombstones, that a rolled-
150
+ /// back force transaction leaves no STRAY retired tombstone. The two active L1
151
+ /// REDs are `remove_writes_retired_tombstone` and `plain_re_add_clears_the_tombstone`.
152
+ #[test]
153
+ #[serial(env)]
154
+ fn force_remove_add_failed_post_consumption_restores_seat_without_new_tombstone() {
155
+ let fixture = RollbackFixture::new("txn-force-fail");
156
+ fixture.seed_team();
157
+ let transport = RecordingTransport::new("team-rollbackteam", &["alpha", "bravo"]);
158
+ let _env = EnvGuard::set(FAIL_FORCE_RECREATE_AFTER_SPAWN, "post-consumption");
159
+ let role = role_path(&fixture, "alpha");
160
+ let _ = add_agent_with_transport_force(
161
+ &fixture.team,
162
+ &aid("alpha"),
163
+ &role,
164
+ false,
165
+ None,
166
+ true,
167
+ &transport,
168
+ );
169
+ // Read the restored seat via the CANONICAL projection (not a hard-coded
170
+ // `teams.<key>` nested path, which is null under the legacy single-team seed).
171
+ // The pre-existing force-recreate snapshot restore already survives the seat;
172
+ // the slice2b-NEW invariant this case pins is the second assert: the rolled-
173
+ // back transaction must NOT leak a stray retired tombstone.
174
+ let restored =
175
+ team_agent::state::projection::select_runtime_state(&fixture.team, Some("rollbackteam"))
176
+ .unwrap();
177
+ let agents = restored
178
+ .get("agents")
179
+ .and_then(serde_json::Value::as_object);
180
+ assert!(
181
+ agents.is_some_and(|m| m.contains_key("alpha")),
182
+ "force remove->add with a failed post-consumption must restore the original alpha seat"
183
+ );
184
+ assert_eq!(
185
+ retired_state(&fixture.team, "rollbackteam", "alpha"),
186
+ None,
187
+ "a rolled-back force recreate must not leave a stray retired tombstone"
188
+ );
189
+ }
190
+
191
+ // NOTE (honest boundary): the "same-id sibling team lifecycle is untouched"
192
+ // scenario is NOT a sound L1 RED at this baseline. Without any tombstone
193
+ // semantics, a hand-seeded sibling agent_lifecycle is dropped by the remove's
194
+ // selected-state save regardless of cross-team scoping — so it reds for the
195
+ // wrong reason (a fixture artifact, not the guarded cross-team-damage fault).
196
+ // Cross-team non-interference is contracted where it can red for the right
197
+ // reason: the L3 real-machine cross-team case and owner-b's scoped src unit on
198
+ // the tombstone primitive. Fabricating it here would be a false red.
199
+
200
+ fn aid(id: &str) -> AgentId {
201
+ AgentId::new(id)
202
+ }
203
+
204
+ #[derive(Debug, Clone, PartialEq, Eq)]
205
+ struct HealthRow {
206
+ owner_team_id: Option<String>,
207
+ status: String,
208
+ last_output_at: Option<String>,
209
+ context_usage_pct: Option<i64>,
210
+ current_task_id: Option<String>,
211
+ }
212
+
213
+ struct RollbackFixture {
214
+ team: PathBuf,
215
+ }
216
+
217
+ impl RollbackFixture {
218
+ fn new(label: &str) -> Self {
219
+ static SEQ: AtomicU64 = AtomicU64::new(0);
220
+ let n = SEQ.fetch_add(1, Ordering::Relaxed);
221
+ let root = std::env::temp_dir().join(format!(
222
+ "ta_lifecycle_rollback_{label}_{}_{}",
223
+ std::process::id(),
224
+ n
225
+ ));
226
+ if root.exists() {
227
+ let _ = std::fs::remove_dir_all(&root);
228
+ }
229
+ std::fs::create_dir_all(&root).unwrap();
230
+ Self {
231
+ team: root.join("team"),
232
+ }
233
+ }
234
+
235
+ fn seed_team(&self) {
236
+ std::fs::create_dir_all(self.team.join("agents")).unwrap();
237
+ std::fs::write(
238
+ self.team.join("TEAM.md"),
239
+ "---\nname: rollbackteam\nobjective: Lifecycle rollback contract.\nprovider: codex\n---\n\nRollback team.\n",
240
+ )
241
+ .unwrap();
242
+ std::fs::write(
243
+ self.team.join("agents/alpha.md"),
244
+ agent_doc("alpha", "Alpha worker"),
245
+ )
246
+ .unwrap();
247
+ std::fs::write(
248
+ self.team.join("agents/bravo.md"),
249
+ agent_doc("bravo", "Bravo worker"),
250
+ )
251
+ .unwrap();
252
+ let spec = team_agent::compiler::compile_team(&self.team).expect("compile rollback team");
253
+ let yaml = team_agent::model::yaml::dumps(&spec)
254
+ .replace("default_assignee: \"alpha\"", "default_assignee: \"bravo\"")
255
+ .replace("assign_to: \"alpha\"", "assign_to: \"bravo\"")
256
+ .replace("assignee: \"alpha\"", "assignee: \"bravo\"");
257
+ assert!(
258
+ yaml.contains("id: \"alpha\"") && yaml.contains("id: \"bravo\""),
259
+ "fixture must compile agent ids alpha/bravo before lifecycle calls; spec={yaml}"
260
+ );
261
+ std::fs::write(self.team.join("team.spec.yaml"), yaml).unwrap();
262
+ // 0.4.6: seed real rollout files so fork tuple guard passes.
263
+ std::fs::write(self.team.join("alpha-rollout.jsonl"), b"{}\n").unwrap();
264
+ std::fs::write(self.team.join("bravo-rollout.jsonl"), b"{}\n").unwrap();
265
+ save_runtime_state(
266
+ &self.team,
267
+ &json!({
268
+ "session_name": "team-rollbackteam",
269
+ "active_team_key": "rollbackteam",
270
+ // 0.4.6 tuple-atomic contract: fork now requires complete
271
+ // source tuple. Seed real rollout files + captured_at/via
272
+ // so the fork-source backing guard passes and the test
273
+ // reaches the rollback behaviour it actually asserts.
274
+ "agents": {
275
+ "alpha": {
276
+ "status": "stopped",
277
+ "provider": "codex",
278
+ "auth_mode": "subscription",
279
+ "window": "alpha",
280
+ "session_id": "sess-alpha",
281
+ "rollout_path": self.team.join("alpha-rollout.jsonl").to_string_lossy(),
282
+ "captured_at": "2026-06-25T10:00:00+00:00",
283
+ "captured_via": "session.captured",
284
+ "owner_team_id": "rollbackteam"
285
+ },
286
+ "bravo": {
287
+ "status": "running",
288
+ "provider": "codex",
289
+ "auth_mode": "subscription",
290
+ "window": "bravo",
291
+ "session_id": "sess-bravo",
292
+ "rollout_path": self.team.join("bravo-rollout.jsonl").to_string_lossy(),
293
+ "captured_at": "2026-06-25T10:00:00+00:00",
294
+ "captured_via": "session.captured",
295
+ "owner_team_id": "rollbackteam"
296
+ }
297
+ }
298
+ }),
299
+ )
300
+ .unwrap();
301
+ let _ = MessageStore::open(&self.team).unwrap();
302
+ }
303
+
304
+ fn seed_healthy_coordinator(&self) {
305
+ let ws = WorkspacePath::new(self.team.clone());
306
+ let pid = Pid::new(std::process::id());
307
+ if let Some(parent) = coordinator_pid_path(&ws).parent() {
308
+ std::fs::create_dir_all(parent).unwrap();
309
+ }
310
+ std::fs::write(coordinator_pid_path(&ws), pid.to_string()).unwrap();
311
+ write_coordinator_metadata(&ws, pid, MetadataSource::Start).unwrap();
312
+ }
313
+
314
+ fn seed_sibling_team_with_same_fork_id(&self) {
315
+ let mut state = load_runtime_state(&self.team).unwrap();
316
+ let selected = state.clone();
317
+ state.as_object_mut().unwrap().insert(
318
+ "teams".to_string(),
319
+ json!({
320
+ "rollbackteam": selected,
321
+ "sibling": {
322
+ "agents": {
323
+ "newfork": {
324
+ "status": "running",
325
+ "provider": "codex",
326
+ "pane_id": "%sibling-newfork",
327
+ "owner_team_id": "sibling"
328
+ }
329
+ }
330
+ }
331
+ }),
332
+ );
333
+ save_runtime_state(&self.team, &state).unwrap();
334
+ let selected =
335
+ team_agent::state::projection::select_runtime_state(&self.team, Some("rollbackteam"))
336
+ .unwrap();
337
+ team_agent::state::projection::save_team_scoped_state(&self.team, &selected).unwrap();
338
+ }
339
+
340
+ fn seed_agent_health(
341
+ &self,
342
+ owner_team_id: &str,
343
+ agent_id: &str,
344
+ status: &str,
345
+ last_output_at: Option<&str>,
346
+ context_usage_pct: Option<i64>,
347
+ current_task_id: Option<&str>,
348
+ ) {
349
+ let store = MessageStore::open(&self.team).unwrap();
350
+ let conn = open_db(store.db_path()).unwrap();
351
+ conn.execute(
352
+ "insert or replace into agent_health(owner_team_id, agent_id, status, last_output_at, context_usage_pct, current_task_id, updated_at)
353
+ values (?1, ?2, ?3, ?4, ?5, ?6, '2026-06-09T00:00:00Z')",
354
+ params![
355
+ owner_team_id,
356
+ agent_id,
357
+ status,
358
+ last_output_at,
359
+ context_usage_pct,
360
+ current_task_id
361
+ ],
362
+ )
363
+ .unwrap();
364
+ }
365
+
366
+ fn agent_health_row(&self, agent_id: &str) -> Option<HealthRow> {
367
+ let store = MessageStore::open(&self.team).unwrap();
368
+ let conn = open_db(store.db_path()).unwrap();
369
+ conn.query_row(
370
+ "select owner_team_id, status, last_output_at, context_usage_pct, current_task_id
371
+ from agent_health where agent_id = ?1",
372
+ [agent_id],
373
+ |row| {
374
+ Ok(HealthRow {
375
+ owner_team_id: row.get(0)?,
376
+ status: row.get(1)?,
377
+ last_output_at: row.get(2)?,
378
+ context_usage_pct: row.get(3)?,
379
+ current_task_id: row.get(4)?,
380
+ })
381
+ },
382
+ )
383
+ .ok()
384
+ }
385
+
386
+ fn spec_text(&self) -> String {
387
+ std::fs::read_to_string(self.team.join("team.spec.yaml")).unwrap()
388
+ }
389
+ }
390
+
391
+ fn agent_doc(name: &str, role: &str) -> String {
392
+ format!(
393
+ "---\nname: {name}\nrole: {role}\nprovider: codex\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\n{role}.\n"
394
+ )
395
+ }
396
+
397
+ struct EnvGuard {
398
+ key: &'static str,
399
+ old: Option<String>,
400
+ }
401
+
402
+ impl EnvGuard {
403
+ fn set(key: &'static str, value: &str) -> Self {
404
+ let old = std::env::var(key).ok();
405
+ std::env::set_var(key, value);
406
+ Self { key, old }
407
+ }
408
+ }
409
+
410
+ impl Drop for EnvGuard {
411
+ fn drop(&mut self) {
412
+ match &self.old {
413
+ Some(value) => std::env::set_var(self.key, value),
414
+ None => std::env::remove_var(self.key),
415
+ }
416
+ }
417
+ }
418
+
419
+ #[derive(Clone)]
420
+ struct RecordingTransport {
421
+ session: String,
422
+ windows: Arc<Mutex<BTreeSet<String>>>,
423
+ killed: Arc<Mutex<Vec<String>>>,
424
+ }
425
+
426
+ impl RecordingTransport {
427
+ fn new(session: &str, windows: &[&str]) -> Self {
428
+ Self {
429
+ session: session.to_string(),
430
+ windows: Arc::new(Mutex::new(
431
+ windows.iter().map(|window| (*window).to_string()).collect(),
432
+ )),
433
+ killed: Arc::new(Mutex::new(Vec::new())),
434
+ }
435
+ }
436
+
437
+ fn killed(&self) -> Vec<String> {
438
+ self.killed.lock().unwrap().clone()
439
+ }
440
+
441
+ fn record_spawn(
442
+ &self,
443
+ session: &SessionName,
444
+ window: &WindowName,
445
+ ) -> Result<SpawnResult, TransportError> {
446
+ self.windows
447
+ .lock()
448
+ .unwrap()
449
+ .insert(window.as_str().to_string());
450
+ Ok(SpawnResult {
451
+ pane_id: PaneId::new(format!("%{}", window.as_str())),
452
+ session: session.clone(),
453
+ window: window.clone(),
454
+ child_pid: Some(4242),
455
+ })
456
+ }
457
+ }
458
+
459
+ impl Transport for RecordingTransport {
460
+ fn kind(&self) -> BackendKind {
461
+ BackendKind::Tmux
462
+ }
463
+
464
+ fn spawn_first(
465
+ &self,
466
+ session: &SessionName,
467
+ window: &WindowName,
468
+ _argv: &[String],
469
+ _cwd: &Path,
470
+ _env: &BTreeMap<String, String>,
471
+ ) -> Result<SpawnResult, TransportError> {
472
+ self.record_spawn(session, window)
473
+ }
474
+
475
+ fn spawn_into(
476
+ &self,
477
+ session: &SessionName,
478
+ window: &WindowName,
479
+ _argv: &[String],
480
+ _cwd: &Path,
481
+ _env: &BTreeMap<String, String>,
482
+ ) -> Result<SpawnResult, TransportError> {
483
+ self.record_spawn(session, window)
484
+ }
485
+
486
+ fn inject(
487
+ &self,
488
+ _target: &Target,
489
+ _payload: &InjectPayload,
490
+ _submit: Key,
491
+ _bracketed: bool,
492
+ ) -> Result<InjectReport, TransportError> {
493
+ Ok(InjectReport {
494
+ stage_reached: InjectStage::Submit,
495
+ inject_verification: InjectVerification::EmptyTextSendKeys,
496
+ submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
497
+ turn_verification: TurnVerification::NotRequired,
498
+ attempts: 1,
499
+ submit_diagnostics: None,
500
+ })
501
+ }
502
+
503
+ fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
504
+ Ok(())
505
+ }
506
+
507
+ fn capture(
508
+ &self,
509
+ _target: &Target,
510
+ range: CaptureRange,
511
+ ) -> Result<CapturedText, TransportError> {
512
+ Ok(CapturedText {
513
+ text: String::new(),
514
+ range,
515
+ })
516
+ }
517
+
518
+ fn query(&self, _target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
519
+ Ok(match field {
520
+ PaneField::PaneId => Some("%0".to_string()),
521
+ PaneField::PaneMode => None,
522
+ PaneField::PaneWidth => Some("120".to_string()),
523
+ PaneField::PaneCurrentPath => None,
524
+ PaneField::PaneCurrentCommand => None,
525
+ PaneField::SessionName => Some(self.session.clone()),
526
+ PaneField::PaneTty => None,
527
+ })
528
+ }
529
+
530
+ fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
531
+ Ok(PaneLiveness::Live)
532
+ }
533
+
534
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
535
+ Ok(self
536
+ .windows
537
+ .lock()
538
+ .unwrap()
539
+ .iter()
540
+ .map(|window| PaneInfo {
541
+ pane_id: PaneId::new(format!("%{window}")),
542
+ session: SessionName::new(self.session.clone()),
543
+ window_index: None,
544
+ window_name: Some(WindowName::new(window.clone())),
545
+ pane_index: None,
546
+ tty: None,
547
+ current_command: None,
548
+ current_path: None,
549
+ active: false,
550
+ pane_pid: Some(4242),
551
+ leader_env: BTreeMap::new(),
552
+ })
553
+ .collect())
554
+ }
555
+
556
+ fn has_session(&self, session: &SessionName) -> Result<bool, TransportError> {
557
+ Ok(session.as_str() == self.session)
558
+ }
559
+
560
+ fn list_windows(&self, session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
561
+ if session.as_str() != self.session {
562
+ return Ok(Vec::new());
563
+ }
564
+ Ok(self
565
+ .windows
566
+ .lock()
567
+ .unwrap()
568
+ .iter()
569
+ .map(|window| WindowName::new(window.clone()))
570
+ .collect())
571
+ }
572
+
573
+ fn set_session_env(
574
+ &self,
575
+ _session: &SessionName,
576
+ _key: &str,
577
+ _value: &str,
578
+ ) -> Result<SetEnvOutcome, TransportError> {
579
+ Ok(SetEnvOutcome::Applied)
580
+ }
581
+
582
+ fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
583
+ Ok(())
584
+ }
585
+
586
+ fn kill_window(&self, target: &Target) -> Result<(), TransportError> {
587
+ let label = match target {
588
+ Target::Pane(pane) => pane.as_str().to_string(),
589
+ Target::SessionWindow { session, window } => {
590
+ format!("{}:{}", session.as_str(), window.as_str())
591
+ }
592
+ };
593
+ if let Some(window) = label.split(':').next_back() {
594
+ self.windows.lock().unwrap().remove(window);
595
+ }
596
+ self.killed.lock().unwrap().push(label);
597
+ Ok(())
598
+ }
599
+
600
+ fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
601
+ Ok(AttachOutcome::Attached)
602
+ }
603
+ }