@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,1273 @@
1
+ //! Team-in-team Bug 1/2 RED contracts.
2
+ //!
3
+ //! Evidence anchor:
4
+ //! `.team/artifacts/macmini-e2e/rs235-stageA-20260605T195214Z/rs235-a-evid-rs235-stageA-20260605T195214Z/`.
5
+ //!
6
+ //! User-facing contract:
7
+ //! - Starting a child team must preserve every running team in `state.teams`.
8
+ //! - A first-layer leader operating on `--team parent` may manage its parent worker
9
+ //! `subleader_w`, even when the same pane is also the child team's leader.
10
+ //! - The same first-layer leader must not manage child-team workers unless it has
11
+ //! explicitly claimed that child team.
12
+ //!
13
+ //! Mac mini follow-up, not enforced by this cargo file:
14
+ //! recovered subleader pane -> `claim-leader --team child` -> L3 worker result appears
15
+ //! on the recovered subleader pane.
16
+
17
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
18
+
19
+ use crate as team_agent;
20
+
21
+ #[path = "../../../tests/support/composite_source.rs"]
22
+ mod composite_source;
23
+ use std::collections::BTreeMap;
24
+ use std::path::{Path, PathBuf};
25
+ use std::sync::atomic::{AtomicU64, Ordering};
26
+ use std::sync::Mutex;
27
+
28
+ use serde_json::{json, Value};
29
+ use serial_test::serial;
30
+ use team_agent::lifecycle::{
31
+ quick_start_with_transport, reset_agent_with_transport, start_agent_with_transport,
32
+ stop_agent_with_transport,
33
+ };
34
+ use team_agent::messaging::{send_message, MessageTarget, SendOptions, TrustedSender};
35
+ use team_agent::model::ids::{AgentId, TeamKey};
36
+ use team_agent::state::persist::{load_runtime_state, save_runtime_state};
37
+ use team_agent::transport::{
38
+ AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
39
+ InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, PaneLiveness, SessionName,
40
+ SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
41
+ TurnVerification, WindowName,
42
+ };
43
+
44
+ #[test]
45
+ #[ignore = "real-machine: nested quick-start/start-agent lifecycle gate"]
46
+ #[serial]
47
+ fn bug1_quick_start_child_preserves_runtime_teams_and_status_bindings() {
48
+ let root = tmp_dir("bug1-state");
49
+ let parent = team_dir(&root, "parent", &[("subleader_w", "Parent subleader")]);
50
+ let child = team_dir(&root, "child", &[("child_worker", "Child worker")]);
51
+ let transport = RecordingTransport::new();
52
+
53
+ quick_start_with_transport(&parent, None, true, Some("parent"), &transport)
54
+ .expect("fixture: parent quick-start should complete with recording transport");
55
+ seed_canonical_state(
56
+ &root,
57
+ vec![team_entry(
58
+ "parent",
59
+ &parent,
60
+ "%2",
61
+ 1,
62
+ &[(
63
+ "subleader_w",
64
+ "running",
65
+ json!(["mcp_team", "dangerous_auto_approve"]),
66
+ )],
67
+ )],
68
+ "parent",
69
+ );
70
+
71
+ quick_start_with_transport(&child, None, true, Some("child"), &transport)
72
+ .expect("Bug 1 trigger: child quick-start should run without a real provider");
73
+ let after_child = load_runtime_state(&root).expect("state after child quick-start");
74
+ let child_failures = missing_team_binding_failures(&after_child, &["parent", "child"]);
75
+
76
+ let status = team_agent::cli::status_port::status(&root, false, true)
77
+ .expect("status --json should be available for command-substitute validation");
78
+ let status_failures = missing_status_team_binding_failures(&status, &["parent", "child"]);
79
+
80
+ let mut failures = Vec::new();
81
+ failures.extend(child_failures);
82
+ failures.extend(status_failures);
83
+ assert!(
84
+ failures.is_empty(),
85
+ "Bug 1 contract: quick-start must upsert launched parent/child teams into state.teams and status --json must expose every allowed team owner/receiver. Grandchild depth refusal is covered by sweep_new_reds_254_red::quick_start_grandchild_depth_limit_refuses_before_state_or_spawn.\nstate_after_child={after_child}\nstatus={status}\nfailures:\n{}",
86
+ failures.join("\n")
87
+ );
88
+ }
89
+
90
+ #[test]
91
+ #[ignore = "real-machine: nested quick-start/start-agent lifecycle gate"]
92
+ #[serial]
93
+ fn bug2_owner_gate_is_target_team_and_target_role_scoped_for_subleader_dual_identity() {
94
+ let mut failures = Vec::new();
95
+
96
+ for case in [
97
+ OwnerGateCase::positive(
98
+ "start-agent --team parent subleader_w",
99
+ "parent",
100
+ "subleader_w",
101
+ Operation::Start,
102
+ ),
103
+ OwnerGateCase::positive(
104
+ "restart-agent/resume --team parent subleader_w",
105
+ "parent",
106
+ "subleader_w",
107
+ Operation::Resume,
108
+ ),
109
+ OwnerGateCase::positive(
110
+ "stop-agent --team parent subleader_w",
111
+ "parent",
112
+ "subleader_w",
113
+ Operation::Stop,
114
+ ),
115
+ OwnerGateCase::positive(
116
+ "reset-agent --team parent subleader_w",
117
+ "parent",
118
+ "subleader_w",
119
+ Operation::Reset,
120
+ ),
121
+ OwnerGateCase::positive(
122
+ "send --team parent --to subleader_w",
123
+ "parent",
124
+ "subleader_w",
125
+ Operation::Send,
126
+ ),
127
+ ] {
128
+ if let Err(err) = run_owner_gate_case(case) {
129
+ failures.push(err);
130
+ }
131
+ }
132
+
133
+ for case in [
134
+ OwnerGateCase::negative(
135
+ "start-agent --team child child_worker",
136
+ "child",
137
+ "child_worker",
138
+ Operation::Start,
139
+ ),
140
+ OwnerGateCase::negative(
141
+ "stop-agent --team child child_worker",
142
+ "child",
143
+ "child_worker",
144
+ Operation::Stop,
145
+ ),
146
+ OwnerGateCase::negative(
147
+ "reset-agent --team child child_worker",
148
+ "child",
149
+ "child_worker",
150
+ Operation::Reset,
151
+ ),
152
+ OwnerGateCase::negative(
153
+ "send --team child --to child_worker",
154
+ "child",
155
+ "child_worker",
156
+ Operation::Send,
157
+ ),
158
+ ] {
159
+ if let Err(err) = run_owner_gate_case(case) {
160
+ failures.push(err);
161
+ }
162
+ }
163
+
164
+ if let Err(err) = assert_owner_gate_static_guard() {
165
+ failures.push(err);
166
+ }
167
+
168
+ assert!(
169
+ failures.is_empty(),
170
+ "Bug 2 contract: owner gate must key on (target_team, target_role), not pane id alone; it must not let the L1 leader operate on child team workers without explicit child claim.\n{}",
171
+ failures.join("\n\n")
172
+ );
173
+ }
174
+
175
+ #[test]
176
+ #[ignore = "real-machine: nested quick-start/start-agent lifecycle gate"]
177
+ #[serial]
178
+ fn bug2_owner_binding_is_team_scoped_for_claim_projection_quick_start_and_parent_start_agent() {
179
+ let mut failures = Vec::new();
180
+
181
+ if let Err(err) = assert_claim_leader_writes_selected_team_owner_not_top_level() {
182
+ failures.push(err);
183
+ }
184
+ if let Err(err) = assert_projection_does_not_borrow_top_level_owner_for_other_team() {
185
+ failures.push(err);
186
+ }
187
+ if let Err(err) = assert_child_quick_start_does_not_inherit_parent_owner() {
188
+ failures.push(err);
189
+ }
190
+ if let Err(err) = assert_parent_start_agent_ignores_child_owner_projection() {
191
+ failures.push(err);
192
+ }
193
+
194
+ assert!(
195
+ failures.is_empty(),
196
+ "Bug 2 residual contract: owner bindings are team-scoped. claim-leader --team T writes teams[T], team projection never borrows another team's top-level owner, child quick-start does not inherit parent owner, and start-agent --team parent must not see child owner/sticky_bind_collision.\n{}",
197
+ failures.join("\n\n")
198
+ );
199
+ }
200
+
201
+ #[test]
202
+ #[ignore = "real-machine: nested quick-start/start-agent lifecycle gate"]
203
+ #[serial]
204
+ fn bug2_full_parent_child_sequence_keeps_owner_writeback_team_scoped() {
205
+ let mut failures = Vec::new();
206
+ for parent_quick_start_caller in [Some("%2"), None] {
207
+ if let Err(err) = run_full_parent_child_owner_writeback_sequence(parent_quick_start_caller)
208
+ {
209
+ failures.push(err);
210
+ }
211
+ }
212
+
213
+ assert!(
214
+ failures.is_empty(),
215
+ "Bug 2 full-sequence contract: parent quick-start must never persist an empty-pane fake owner; claim-leader --team parent must write teams.parent even when parent is active; child quick-start must not stale parent owner; start-agent --team parent must not read empty/stale parent owner or raise sticky_bind_collision.\n{}",
216
+ failures.join("\n\n")
217
+ );
218
+ }
219
+
220
+ #[derive(Clone, Copy)]
221
+ enum Operation {
222
+ Start,
223
+ Resume,
224
+ Stop,
225
+ Reset,
226
+ Send,
227
+ }
228
+
229
+ struct OwnerGateCase {
230
+ label: &'static str,
231
+ team: &'static str,
232
+ agent: &'static str,
233
+ operation: Operation,
234
+ should_allow: bool,
235
+ }
236
+
237
+ impl OwnerGateCase {
238
+ fn positive(
239
+ label: &'static str,
240
+ team: &'static str,
241
+ agent: &'static str,
242
+ operation: Operation,
243
+ ) -> Self {
244
+ Self {
245
+ label,
246
+ team,
247
+ agent,
248
+ operation,
249
+ should_allow: true,
250
+ }
251
+ }
252
+
253
+ fn negative(
254
+ label: &'static str,
255
+ team: &'static str,
256
+ agent: &'static str,
257
+ operation: Operation,
258
+ ) -> Self {
259
+ Self {
260
+ label,
261
+ team,
262
+ agent,
263
+ operation,
264
+ should_allow: false,
265
+ }
266
+ }
267
+ }
268
+
269
+ fn run_owner_gate_case(case: OwnerGateCase) -> Result<(), String> {
270
+ let fixture = ScopeFixture::new(case.label);
271
+ let _env = EnvGuard::set("TMUX_PANE", "%2");
272
+ let before = load_runtime_state(&fixture.root).map_err(|e| e.to_string())?;
273
+ let before_child_receiver = before
274
+ .pointer("/teams/child/leader_receiver/pane_id")
275
+ .cloned();
276
+ let before_capability = before
277
+ .pointer("/teams/parent/agents/subleader_w/tools")
278
+ .cloned();
279
+
280
+ let result = match case.operation {
281
+ Operation::Start => start_agent_with_transport(
282
+ &fixture.root,
283
+ &AgentId::new(case.agent),
284
+ true,
285
+ false,
286
+ true,
287
+ Some(case.team),
288
+ &fixture.transport,
289
+ )
290
+ .map(|_| json!({"ok": true}))
291
+ .map_err(|e| e.to_string()),
292
+ Operation::Resume => start_agent_with_transport(
293
+ &fixture.root,
294
+ &AgentId::new(case.agent),
295
+ false,
296
+ false,
297
+ true,
298
+ Some(case.team),
299
+ &fixture.transport,
300
+ )
301
+ .map(|_| json!({"ok": true}))
302
+ .map_err(|e| e.to_string()),
303
+ Operation::Stop => stop_agent_with_transport(
304
+ &fixture.root,
305
+ &AgentId::new(case.agent),
306
+ Some(case.team),
307
+ &fixture.transport,
308
+ )
309
+ .map(|_| json!({"ok": true}))
310
+ .map_err(|e| e.to_string()),
311
+ Operation::Reset => reset_agent_with_transport(
312
+ &fixture.root,
313
+ &AgentId::new(case.agent),
314
+ true,
315
+ false,
316
+ Some(case.team),
317
+ &fixture.transport,
318
+ )
319
+ .map(|_| json!({"ok": true}))
320
+ .map_err(|e| e.to_string()),
321
+ Operation::Send => {
322
+ let opts = SendOptions {
323
+ sender: TrustedSender::leader(),
324
+ team: Some(TeamKey::new(case.team.to_string())),
325
+ requires_ack: false,
326
+ wait_visible: false,
327
+ route_task_id: false,
328
+ ..SendOptions::default()
329
+ };
330
+ send_message(
331
+ &fixture.root,
332
+ &MessageTarget::Single(case.agent.to_string()),
333
+ "TEAM_IN_TEAM_SCOPE_CANARY",
334
+ &opts,
335
+ )
336
+ .map(|out| json!({"ok": out.ok, "status": format!("{:?}", out.status), "reason": format!("{:?}", out.reason)}))
337
+ .map_err(|e| e.to_string())
338
+ }
339
+ };
340
+
341
+ let after = load_runtime_state(&fixture.root).map_err(|e| e.to_string())?;
342
+ let after_child_receiver = after
343
+ .pointer("/teams/child/leader_receiver/pane_id")
344
+ .cloned();
345
+ let after_capability = after
346
+ .pointer("/teams/parent/agents/subleader_w/tools")
347
+ .cloned();
348
+ if before_child_receiver != after_child_receiver {
349
+ return Err(format!(
350
+ "{}: L1 operation on parent subleader must not rewrite child leader_receiver; before={before_child_receiver:?} after={after_child_receiver:?}",
351
+ case.label
352
+ ));
353
+ }
354
+ if before_capability != after_capability {
355
+ return Err(format!(
356
+ "{}: L1 resume/operation must not change subleader_w capability/MUST-16 safety; before={before_capability:?} after={after_capability:?}",
357
+ case.label
358
+ ));
359
+ }
360
+
361
+ match (case.should_allow, result) {
362
+ (true, Ok(value)) if value.get("ok").and_then(Value::as_bool) == Some(true) => Ok(()),
363
+ (true, Ok(value)) => Err(format!("{}: expected allow for parent-team subleader operation, got {value}", case.label)),
364
+ (true, Err(error)) => Err(format!("{}: expected allow for parent-team subleader operation, got error={error}", case.label)),
365
+ (false, Ok(value)) if value.get("ok").and_then(Value::as_bool) == Some(false) => Ok(()),
366
+ (false, Err(error)) if owner_refusal_like(&error) => Ok(()),
367
+ (false, Ok(value)) => Err(format!(
368
+ "{}: expected refusal before explicit claim-leader --team child, got allowed value={value}",
369
+ case.label
370
+ )),
371
+ (false, Err(error)) => Err(format!(
372
+ "{}: expected owner-gate refusal before explicit claim-leader --team child, got unrelated error={error}",
373
+ case.label
374
+ )),
375
+ }
376
+ }
377
+
378
+ fn assert_owner_gate_static_guard() -> Result<(), String> {
379
+ let restart_agent = source("src/lifecycle/restart/agent.rs");
380
+ let launch = source("src/lifecycle/launch.rs");
381
+ let send = source("src/messaging/send.rs");
382
+ let mut failures = Vec::new();
383
+ if restart_agent.contains("ensure_owner_allowed(workspace)") {
384
+ failures.push(
385
+ "restart/agent.rs must not call raw ensure_owner_allowed(workspace); lifecycle owner gate must receive selected target_team/target_role state".to_string(),
386
+ );
387
+ }
388
+ if !launch.contains("ensure_owner_allowed_for")
389
+ && !restart_agent.contains("ensure_owner_allowed_for")
390
+ {
391
+ failures.push(
392
+ "lifecycle needs an owner-gate API that accepts selected target team state plus target role; raw pane-id-only owner gate is insufficient".to_string(),
393
+ );
394
+ }
395
+ if send.contains("let state = crate::state::persist::load_runtime_state(workspace)?;")
396
+ && send.contains(".get(\"agents\")")
397
+ {
398
+ failures.push(
399
+ "messaging/send.rs must resolve opts.team into that team's agents before membership/owner checks; raw top-level agents make send --team parent/child scope drift".to_string(),
400
+ );
401
+ }
402
+ if failures.is_empty() {
403
+ Ok(())
404
+ } else {
405
+ Err(failures.join("\n"))
406
+ }
407
+ }
408
+
409
+ fn assert_claim_leader_writes_selected_team_owner_not_top_level() -> Result<(), String> {
410
+ let root = tmp_dir("bug2-owner-claim-scoped");
411
+ let parent = team_dir(&root, "parent", &[("subleader_w", "Parent subleader")]);
412
+ let child = team_dir(&root, "child", &[("child_worker", "Child worker")]);
413
+ seed_canonical_state(
414
+ &root,
415
+ vec![
416
+ team_entry(
417
+ "parent",
418
+ &parent,
419
+ "%2",
420
+ 7,
421
+ &[(
422
+ "subleader_w",
423
+ "running",
424
+ json!(["mcp_team", "dangerous_auto_approve"]),
425
+ )],
426
+ ),
427
+ team_entry(
428
+ "child",
429
+ &child,
430
+ "%4",
431
+ 3,
432
+ &[("child_worker", "running", json!(["mcp_team"]))],
433
+ ),
434
+ ],
435
+ "parent",
436
+ );
437
+
438
+ let mut state = load_runtime_state(&root).map_err(|e| e.to_string())?;
439
+ let event_log = team_agent::event_log::EventLog::new(&root);
440
+ let result = team_agent::leader::claim_lease_no_incident(
441
+ &root,
442
+ &mut state,
443
+ Some("child"),
444
+ &TeamKey::new("child"),
445
+ &PaneId::new("%6"),
446
+ true,
447
+ &event_log,
448
+ &AlwaysLive,
449
+ )
450
+ .map_err(|e| e.to_string())?;
451
+ if !result.ok {
452
+ return Err(format!(
453
+ "claim-leader --team child command-substitute should succeed, got {result:?}"
454
+ ));
455
+ }
456
+
457
+ let persisted = load_runtime_state(&root).map_err(|e| e.to_string())?;
458
+ let mut failures = Vec::new();
459
+ if pane_at(&persisted, "/teams/child/team_owner/pane_id") != Some("%6") {
460
+ failures.push(format!(
461
+ "claim-leader --team child must write teams.child.team_owner.pane_id=%6; state={persisted}"
462
+ ));
463
+ }
464
+ if pane_at(&persisted, "/teams/child/leader_receiver/pane_id") != Some("%6") {
465
+ failures.push(format!(
466
+ "claim-leader --team child must write teams.child.leader_receiver.pane_id=%6; state={persisted}"
467
+ ));
468
+ }
469
+ if pane_at(&persisted, "/teams/parent/team_owner/pane_id") != Some("%2") {
470
+ failures.push(format!(
471
+ "claim-leader --team child must not rewrite teams.parent.team_owner; state={persisted}"
472
+ ));
473
+ }
474
+ if pane_at(&persisted, "/team_owner/pane_id") == Some("%6") {
475
+ failures.push(format!(
476
+ "claim-leader --team child must not bind the workspace top-level owner to the child caller; state={persisted}"
477
+ ));
478
+ }
479
+ if failures.is_empty() {
480
+ Ok(())
481
+ } else {
482
+ Err(failures.join("\n"))
483
+ }
484
+ }
485
+
486
+ fn assert_projection_does_not_borrow_top_level_owner_for_other_team() -> Result<(), String> {
487
+ let state = json!({
488
+ "active_team_key": "child",
489
+ "session_name": "team-child",
490
+ "team_owner": owner("%4", "uuid-child", 5),
491
+ "leader_receiver": receiver("%4", "uuid-child", 5),
492
+ "teams": {
493
+ "parent": {
494
+ "status": "alive",
495
+ "session_name": "team-parent",
496
+ "team_dir": "/tmp/team-in-team-parent",
497
+ "agents": {"subleader_w": {"status": "running"}}
498
+ },
499
+ "child": {
500
+ "status": "alive",
501
+ "session_name": "team-child",
502
+ "team_dir": "/tmp/team-in-team-child",
503
+ "team_owner": owner("%4", "uuid-child", 5),
504
+ "leader_receiver": receiver("%4", "uuid-child", 5),
505
+ "agents": {"child_worker": {"status": "running"}}
506
+ }
507
+ }
508
+ });
509
+ let projected = team_agent::state::projection::project_top_level_view(&state, "parent");
510
+ let mut failures = Vec::new();
511
+ if pane_at(&projected, "/team_owner/pane_id").is_some() {
512
+ failures.push(format!(
513
+ "project_top_level_view(parent) must not fallback to top-level child team_owner; projected={projected}"
514
+ ));
515
+ }
516
+ if pane_at(&projected, "/leader_receiver/pane_id").is_some() {
517
+ failures.push(format!(
518
+ "project_top_level_view(parent) must not fallback to top-level child leader_receiver; projected={projected}"
519
+ ));
520
+ }
521
+ if failures.is_empty() {
522
+ Ok(())
523
+ } else {
524
+ Err(failures.join("\n"))
525
+ }
526
+ }
527
+
528
+ fn assert_child_quick_start_does_not_inherit_parent_owner() -> Result<(), String> {
529
+ let root = tmp_dir("bug2-child-quick-start-owner-scope");
530
+ let parent = team_dir(&root, "parent", &[("subleader_w", "Parent subleader")]);
531
+ let child = team_dir(&root, "child", &[("child_worker", "Child worker")]);
532
+ seed_canonical_state(
533
+ &root,
534
+ vec![team_entry(
535
+ "parent",
536
+ &parent,
537
+ "%2",
538
+ 11,
539
+ &[(
540
+ "subleader_w",
541
+ "running",
542
+ json!(["mcp_team", "dangerous_auto_approve"]),
543
+ )],
544
+ )],
545
+ "parent",
546
+ );
547
+ quick_start_with_transport(
548
+ &child,
549
+ None,
550
+ true,
551
+ Some("child"),
552
+ &RecordingTransport::new(),
553
+ )
554
+ .map_err(|e| e.to_string())?;
555
+ let after = load_runtime_state(&root).map_err(|e| e.to_string())?;
556
+ let mut failures = Vec::new();
557
+ if pane_at(&after, "/teams/child/team_owner/pane_id") == Some("%2") {
558
+ failures.push(format!(
559
+ "child quick-start must not copy parent owner into teams.child.team_owner; state={after}"
560
+ ));
561
+ }
562
+ if pane_at(&after, "/team_owner/pane_id") == Some("%2")
563
+ && after.get("active_team_key").and_then(Value::as_str) == Some("child")
564
+ {
565
+ failures.push(format!(
566
+ "child quick-start projected top-level active child must not carry parent owner; state={after}"
567
+ ));
568
+ }
569
+ if pane_at(&after, "/teams/parent/team_owner/pane_id") != Some("%2") {
570
+ failures.push(format!(
571
+ "child quick-start must preserve parent owner under teams.parent; state={after}"
572
+ ));
573
+ }
574
+ if failures.is_empty() {
575
+ Ok(())
576
+ } else {
577
+ Err(failures.join("\n"))
578
+ }
579
+ }
580
+
581
+ fn assert_parent_start_agent_ignores_child_owner_projection() -> Result<(), String> {
582
+ let root = tmp_dir("bug2-parent-start-agent-owner-scope");
583
+ let parent = team_dir(&root, "parent", &[("subleader_w", "Parent subleader")]);
584
+ let child = team_dir(&root, "child", &[("child_worker", "Child worker")]);
585
+ seed_canonical_state(
586
+ &root,
587
+ vec![team_entry_without_owner(
588
+ "parent",
589
+ &parent,
590
+ &[(
591
+ "subleader_w",
592
+ "running",
593
+ json!(["mcp_team", "dangerous_auto_approve"]),
594
+ )],
595
+ )],
596
+ "parent",
597
+ );
598
+ let mut state = load_runtime_state(&root).map_err(|e| e.to_string())?;
599
+ state["active_team_key"] = json!("child");
600
+ state["session_name"] = json!("team-child");
601
+ state["team_owner"] = owner("%4", "uuid-child", 9);
602
+ state["leader_receiver"] = receiver("%4", "uuid-child", 9);
603
+ state["teams"]["child"] = team_entry(
604
+ "child",
605
+ &child,
606
+ "%4",
607
+ 9,
608
+ &[("child_worker", "running", json!(["mcp_team"]))],
609
+ )
610
+ .1;
611
+ save_runtime_state(&root, &state).map_err(|e| e.to_string())?;
612
+
613
+ let transport = RecordingTransport::new()
614
+ .with_session_present(true)
615
+ .with_windows(vec![WindowName::new("subleader_w")]);
616
+ let _pane = EnvGuard::set("TMUX_PANE", "%2");
617
+ let _uuid = EnvGuard::set("TEAM_AGENT_LEADER_SESSION_UUID", "uuid-child");
618
+ let result = start_agent_with_transport(
619
+ &root,
620
+ &AgentId::new("subleader_w"),
621
+ false,
622
+ false,
623
+ true,
624
+ Some("parent"),
625
+ &transport,
626
+ );
627
+ match result {
628
+ Ok(_) => Ok(()),
629
+ Err(error) => {
630
+ let error = error.to_string();
631
+ if error.contains("sticky_bind_collision") || error.contains("team_owner_mismatch") {
632
+ Err(format!(
633
+ "start-agent --team parent subleader_w must use parent team-scoped projection and must not see child owner/sticky_bind_collision; got error={error}"
634
+ ))
635
+ } else {
636
+ Err(format!(
637
+ "start-agent --team parent subleader_w should succeed in this command-substitute fixture; got unrelated error={error}"
638
+ ))
639
+ }
640
+ }
641
+ }
642
+ }
643
+
644
+ fn run_full_parent_child_owner_writeback_sequence(
645
+ parent_quick_start_caller: Option<&str>,
646
+ ) -> Result<(), String> {
647
+ let label = match parent_quick_start_caller {
648
+ Some(_) => "with caller pane",
649
+ None => "without caller pane",
650
+ };
651
+ let root = tmp_dir(&format!("bug2-full-owner-writeback-{label}"));
652
+ let parent = team_dir(&root, "parent", &[("subleader_w", "Parent subleader")]);
653
+ let child = team_dir(&root, "child", &[("child_worker", "Child worker")]);
654
+ let quick_start_transport = RecordingTransport::new();
655
+ let start_transport = RecordingTransport::new()
656
+ .with_session_present(true)
657
+ .with_windows(vec![
658
+ WindowName::new("subleader_w"),
659
+ WindowName::new("child_worker"),
660
+ ]);
661
+ let mut failures = Vec::new();
662
+
663
+ {
664
+ let _pane = match parent_quick_start_caller {
665
+ Some(pane) => EnvGuard::set("TMUX_PANE", pane),
666
+ None => EnvGuard::remove("TMUX_PANE"),
667
+ };
668
+ let _leader_pane = EnvGuard::remove("TEAM_AGENT_LEADER_PANE_ID");
669
+ let _uuid = EnvGuard::remove("TEAM_AGENT_LEADER_SESSION_UUID");
670
+ quick_start_with_transport(&parent, None, true, Some("parent"), &quick_start_transport)
671
+ .map_err(|e| format!("{label}: parent quick-start failed: {e}"))?;
672
+ }
673
+ let after_parent = load_runtime_state(&root).map_err(|e| e.to_string())?;
674
+ failures.extend(empty_owner_failures(
675
+ &after_parent,
676
+ &format!("{label}: after parent quick-start"),
677
+ ));
678
+ if let Some(pane) = parent_quick_start_caller {
679
+ if pane_at(&after_parent, "/team_owner/pane_id") != Some(pane) {
680
+ failures.push(format!(
681
+ "{label}: parent quick-start with caller pane must bind top-level owner to {pane}; state={after_parent}"
682
+ ));
683
+ }
684
+ if pane_at(&after_parent, "/teams/parent/team_owner/pane_id") != Some(pane) {
685
+ failures.push(format!(
686
+ "{label}: parent quick-start with caller pane must bind teams.parent owner to {pane}; state={after_parent}"
687
+ ));
688
+ }
689
+ }
690
+
691
+ let mut claim_state = load_runtime_state(&root).map_err(|e| e.to_string())?;
692
+ let event_log = team_agent::event_log::EventLog::new(&root);
693
+ let claim = team_agent::leader::claim_lease_no_incident(
694
+ &root,
695
+ &mut claim_state,
696
+ Some("parent"),
697
+ &TeamKey::new("parent"),
698
+ &PaneId::new("%3"),
699
+ true,
700
+ &event_log,
701
+ &AlwaysLive,
702
+ )
703
+ .map_err(|e| format!("{label}: claim-leader --team parent failed: {e}"))?;
704
+ if !claim.ok {
705
+ failures.push(format!(
706
+ "{label}: claim-leader --team parent should succeed, got {claim:?}"
707
+ ));
708
+ }
709
+ let after_claim = load_runtime_state(&root).map_err(|e| e.to_string())?;
710
+ failures.extend(empty_owner_failures(
711
+ &after_claim,
712
+ &format!("{label}: after claim-leader --team parent"),
713
+ ));
714
+ if pane_at(&after_claim, "/team_owner/pane_id") != Some("%3") {
715
+ failures.push(format!(
716
+ "{label}: claim-leader --team parent should update active top-level owner to %3; state={after_claim}"
717
+ ));
718
+ }
719
+ if pane_at(&after_claim, "/teams/parent/team_owner/pane_id") != Some("%3") {
720
+ failures.push(format!(
721
+ "{label}: claim-leader --team parent must update teams.parent.team_owner even when parent is active; state={after_claim}"
722
+ ));
723
+ }
724
+ if pane_at(&after_claim, "/teams/parent/leader_receiver/pane_id") != Some("%3") {
725
+ failures.push(format!(
726
+ "{label}: claim-leader --team parent must update teams.parent.leader_receiver even when parent is active; state={after_claim}"
727
+ ));
728
+ }
729
+
730
+ {
731
+ let _pane = EnvGuard::set("TMUX_PANE", "%4");
732
+ let _leader_pane = EnvGuard::remove("TEAM_AGENT_LEADER_PANE_ID");
733
+ let _uuid = EnvGuard::remove("TEAM_AGENT_LEADER_SESSION_UUID");
734
+ quick_start_with_transport(&child, None, true, Some("child"), &quick_start_transport)
735
+ .map_err(|e| format!("{label}: child quick-start failed: {e}"))?;
736
+ }
737
+ let after_child = load_runtime_state(&root).map_err(|e| e.to_string())?;
738
+ failures.extend(empty_owner_failures(
739
+ &after_child,
740
+ &format!("{label}: after child quick-start"),
741
+ ));
742
+ if pane_at(&after_child, "/teams/parent/team_owner/pane_id") != Some("%3") {
743
+ failures.push(format!(
744
+ "{label}: child quick-start must preserve claimed parent owner in teams.parent=%3; state={after_child}"
745
+ ));
746
+ }
747
+ if pane_at(&after_child, "/teams/child/team_owner/pane_id") != Some("%4") {
748
+ failures.push(format!(
749
+ "{label}: child quick-start with caller pane must bind teams.child owner to %4 and not inherit parent; state={after_child}"
750
+ ));
751
+ }
752
+
753
+ let parent_uuid = after_child
754
+ .pointer("/teams/parent/team_owner/leader_session_uuid")
755
+ .and_then(Value::as_str)
756
+ .unwrap_or("");
757
+ let _pane = EnvGuard::set("TMUX_PANE", "%3");
758
+ let _leader_pane = EnvGuard::remove("TEAM_AGENT_LEADER_PANE_ID");
759
+ let _uuid = if parent_uuid.is_empty() {
760
+ EnvGuard::remove("TEAM_AGENT_LEADER_SESSION_UUID")
761
+ } else {
762
+ EnvGuard::set("TEAM_AGENT_LEADER_SESSION_UUID", parent_uuid)
763
+ };
764
+ let start = start_agent_with_transport(
765
+ &root,
766
+ &AgentId::new("subleader_w"),
767
+ false,
768
+ false,
769
+ true,
770
+ Some("parent"),
771
+ &start_transport,
772
+ );
773
+ if let Err(error) = start {
774
+ let error = error.to_string();
775
+ if error.contains("sticky_bind_collision")
776
+ || error.contains("team_owner_mismatch")
777
+ || error.contains("\"pane_id\":\"\"")
778
+ {
779
+ failures.push(format!(
780
+ "{label}: start-agent --team parent must not read stale/empty teams.parent owner or raise sticky_bind_collision; got error={error}"
781
+ ));
782
+ } else {
783
+ failures.push(format!(
784
+ "{label}: start-agent --team parent failed unexpectedly: {error}"
785
+ ));
786
+ }
787
+ }
788
+
789
+ if failures.is_empty() {
790
+ Ok(())
791
+ } else {
792
+ Err(failures.join("\n"))
793
+ }
794
+ }
795
+
796
+ fn owner_refusal_like(error: &str) -> bool {
797
+ error.contains("team_owner_mismatch")
798
+ || error.contains("owner_takeover_required")
799
+ || error.contains("not_owner")
800
+ || error.contains("refused")
801
+ }
802
+
803
+ struct ScopeFixture {
804
+ root: PathBuf,
805
+ transport: RecordingTransport,
806
+ }
807
+
808
+ impl ScopeFixture {
809
+ fn new(label: &str) -> Self {
810
+ let root = tmp_dir(label);
811
+ let parent = team_dir(&root, "parent", &[("subleader_w", "Parent subleader")]);
812
+ let child = team_dir(&root, "child", &[("child_worker", "Child worker")]);
813
+ std::fs::write(
814
+ root.join("team.spec.yaml"),
815
+ std::fs::read_to_string(parent.join("team.spec.yaml")).unwrap(),
816
+ )
817
+ .unwrap();
818
+ seed_canonical_state(
819
+ &root,
820
+ vec![
821
+ team_entry(
822
+ "parent",
823
+ &parent,
824
+ "%2",
825
+ 1,
826
+ &[(
827
+ "subleader_w",
828
+ "running",
829
+ json!(["mcp_team", "dangerous_auto_approve"]),
830
+ )],
831
+ ),
832
+ team_entry(
833
+ "child",
834
+ &child,
835
+ "%4",
836
+ 1,
837
+ &[("child_worker", "running", json!(["mcp_team"]))],
838
+ ),
839
+ ],
840
+ "child",
841
+ );
842
+ Self {
843
+ root,
844
+ transport: RecordingTransport::new()
845
+ .with_session_present(true)
846
+ .with_windows(vec![
847
+ WindowName::new("subleader_w"),
848
+ WindowName::new("child_worker"),
849
+ ]),
850
+ }
851
+ }
852
+ }
853
+
854
+ fn seed_canonical_state(root: &Path, entries: Vec<(&'static str, Value)>, active: &str) {
855
+ let mut teams = serde_json::Map::new();
856
+ for (key, entry) in entries {
857
+ teams.insert(key.to_string(), entry);
858
+ }
859
+ let active_entry = teams.get(active).cloned().unwrap_or_else(|| json!({}));
860
+ let mut root_state = active_entry.as_object().cloned().unwrap_or_default();
861
+ root_state.insert("active_team_key".to_string(), json!(active));
862
+ root_state.insert("teams".to_string(), Value::Object(teams));
863
+ save_runtime_state(root, &Value::Object(root_state)).unwrap();
864
+ let _ = team_agent::message_store::MessageStore::open(root).unwrap();
865
+ }
866
+
867
+ fn team_entry(
868
+ key: &'static str,
869
+ team_dir: &Path,
870
+ leader_pane: &str,
871
+ owner_epoch: u64,
872
+ agents: &[(&str, &str, Value)],
873
+ ) -> (&'static str, Value) {
874
+ let mut agent_map = serde_json::Map::new();
875
+ for (agent_id, status, tools) in agents {
876
+ agent_map.insert(
877
+ (*agent_id).to_string(),
878
+ json!({
879
+ "agent_id": agent_id,
880
+ "status": status,
881
+ "provider": "codex",
882
+ "auth_mode": "subscription",
883
+ "role": agent_id,
884
+ "window": agent_id,
885
+ "owner_team_id": key,
886
+ "session_id": format!("sess-{key}-{agent_id}"),
887
+ "rollout_path": team_dir.join(format!("{agent_id}.jsonl")).to_string_lossy().to_string(),
888
+ "tools": tools,
889
+ }),
890
+ );
891
+ }
892
+ (
893
+ key,
894
+ json!({
895
+ "status": "alive",
896
+ "active_team_key": key,
897
+ "team_dir": team_dir.to_string_lossy().to_string(),
898
+ "spec_path": team_dir.join("team.spec.yaml").to_string_lossy().to_string(),
899
+ "workspace": team_dir.parent().unwrap().to_string_lossy().to_string(),
900
+ "session_name": format!("team-{key}"),
901
+ "leader": {"id": "leader"},
902
+ "leader_receiver": {
903
+ "pane_id": leader_pane,
904
+ "provider": "codex",
905
+ "leader_session_uuid": format!("uuid-{key}"),
906
+ "owner_epoch": owner_epoch
907
+ },
908
+ "team_owner": {
909
+ "pane_id": leader_pane,
910
+ "provider": "codex",
911
+ "machine_fingerprint": "",
912
+ "leader_session_uuid": format!("uuid-{key}"),
913
+ "owner_epoch": owner_epoch,
914
+ "claimed_at": "2026-06-06T00:00:00Z",
915
+ "claimed_via": "claim-leader",
916
+ "os_user": "alauda"
917
+ },
918
+ "agents": Value::Object(agent_map),
919
+ "tasks": [],
920
+ }),
921
+ )
922
+ }
923
+
924
+ fn team_entry_without_owner(
925
+ key: &'static str,
926
+ team_dir: &Path,
927
+ agents: &[(&str, &str, Value)],
928
+ ) -> (&'static str, Value) {
929
+ let (_, mut entry) = team_entry(key, team_dir, "%unused", 0, agents);
930
+ if let Some(obj) = entry.as_object_mut() {
931
+ obj.remove("team_owner");
932
+ obj.remove("leader_receiver");
933
+ }
934
+ (key, entry)
935
+ }
936
+
937
+ fn owner(pane_id: &str, uuid: &str, owner_epoch: u64) -> Value {
938
+ json!({
939
+ "pane_id": pane_id,
940
+ "provider": "codex",
941
+ "machine_fingerprint": "",
942
+ "leader_session_uuid": uuid,
943
+ "owner_epoch": owner_epoch,
944
+ "claimed_at": "2026-06-06T00:00:00Z",
945
+ "claimed_via": "claim-leader",
946
+ "os_user": "alauda"
947
+ })
948
+ }
949
+
950
+ fn receiver(pane_id: &str, uuid: &str, owner_epoch: u64) -> Value {
951
+ json!({
952
+ "pane_id": pane_id,
953
+ "provider": "codex",
954
+ "leader_session_uuid": uuid,
955
+ "owner_epoch": owner_epoch
956
+ })
957
+ }
958
+
959
+ fn pane_at<'a>(value: &'a Value, pointer: &str) -> Option<&'a str> {
960
+ value.pointer(pointer).and_then(Value::as_str)
961
+ }
962
+
963
+ fn empty_owner_failures(state: &Value, context: &str) -> Vec<String> {
964
+ let mut failures = Vec::new();
965
+ for pointer in [
966
+ "/team_owner/pane_id",
967
+ "/leader_receiver/pane_id",
968
+ "/teams/parent/team_owner/pane_id",
969
+ "/teams/parent/leader_receiver/pane_id",
970
+ "/teams/child/team_owner/pane_id",
971
+ "/teams/child/leader_receiver/pane_id",
972
+ ] {
973
+ if state
974
+ .pointer(pointer)
975
+ .is_some_and(|value| value.as_str() == Some(""))
976
+ {
977
+ failures.push(format!(
978
+ "{context}: {pointer} must never be an empty pane_id fake owner/receiver; state={state}"
979
+ ));
980
+ }
981
+ }
982
+ failures
983
+ }
984
+
985
+ fn missing_team_binding_failures(state: &Value, teams: &[&str]) -> Vec<String> {
986
+ teams
987
+ .iter()
988
+ .filter_map(|team| {
989
+ let entry = state.pointer(&format!("/teams/{team}"));
990
+ match entry {
991
+ None => Some(format!("missing state.teams.{team}")),
992
+ Some(entry) => {
993
+ let mut fields = Vec::new();
994
+ for field in ["session_name", "agents", "leader_receiver", "team_owner"] {
995
+ if entry.get(field).is_none() {
996
+ fields.push(field);
997
+ }
998
+ }
999
+ let epoch = entry
1000
+ .pointer("/team_owner/owner_epoch")
1001
+ .or_else(|| entry.pointer("/leader_receiver/owner_epoch"));
1002
+ if epoch.is_none() {
1003
+ fields.push("owner_epoch");
1004
+ }
1005
+ (!fields.is_empty())
1006
+ .then(|| format!("state.teams.{team} missing fields {fields:?}: {entry}"))
1007
+ }
1008
+ }
1009
+ })
1010
+ .collect()
1011
+ }
1012
+
1013
+ fn missing_status_team_binding_failures(status: &Value, teams: &[&str]) -> Vec<String> {
1014
+ teams
1015
+ .iter()
1016
+ .filter_map(|team| {
1017
+ let entry = status.pointer(&format!("/teams/{team}"));
1018
+ match entry {
1019
+ None => Some(format!("status --json missing teams.{team} binding view")),
1020
+ Some(entry) if entry.get("leader_receiver").is_none() || entry.get("team_owner").is_none() => {
1021
+ Some(format!("status --json teams.{team} must include leader_receiver and team_owner: {entry}"))
1022
+ }
1023
+ Some(_) => None,
1024
+ }
1025
+ })
1026
+ .collect()
1027
+ }
1028
+
1029
+ fn team_dir(root: &Path, name: &str, agents: &[(&str, &str)]) -> PathBuf {
1030
+ let team = root.join(name);
1031
+ std::fs::create_dir_all(team.join("agents")).unwrap();
1032
+ std::fs::write(
1033
+ team.join("TEAM.md"),
1034
+ format!(
1035
+ "---\nname: {name}\nobjective: Team-in-team contract fixture.\nprovider: codex\n---\n\n{name} team.\n"
1036
+ ),
1037
+ )
1038
+ .unwrap();
1039
+ for (agent_id, role) in agents {
1040
+ std::fs::write(
1041
+ team.join("agents").join(format!("{agent_id}.md")),
1042
+ role_doc(agent_id, role),
1043
+ )
1044
+ .unwrap();
1045
+ }
1046
+ let spec = team_agent::compiler::compile_team(&team).unwrap();
1047
+ std::fs::write(
1048
+ team.join("team.spec.yaml"),
1049
+ team_agent::model::yaml::dumps(&spec),
1050
+ )
1051
+ .unwrap();
1052
+ team
1053
+ }
1054
+
1055
+ fn role_doc(name: &str, role: &str) -> String {
1056
+ format!(
1057
+ "---\nname: {name}\nrole: {role}\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\n{role}.\n"
1058
+ )
1059
+ }
1060
+
1061
+ fn tmp_dir(tag: &str) -> PathBuf {
1062
+ static N: AtomicU64 = AtomicU64::new(0);
1063
+ let sanitized = tag
1064
+ .chars()
1065
+ .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
1066
+ .collect::<String>();
1067
+ let dir = std::env::temp_dir().join(format!(
1068
+ "ta-rs-team-in-team-{sanitized}-{}-{}",
1069
+ std::process::id(),
1070
+ N.fetch_add(1, Ordering::Relaxed)
1071
+ ));
1072
+ let _ = std::fs::remove_dir_all(&dir);
1073
+ std::fs::create_dir_all(&dir).unwrap();
1074
+ std::fs::canonicalize(dir).unwrap()
1075
+ }
1076
+
1077
+ fn source(rel: &str) -> String {
1078
+ composite_source::composite_source(rel)
1079
+ }
1080
+
1081
+ struct EnvGuard {
1082
+ key: &'static str,
1083
+ previous: Option<String>,
1084
+ }
1085
+
1086
+ impl EnvGuard {
1087
+ fn set(key: &'static str, value: &str) -> Self {
1088
+ let previous = std::env::var(key).ok();
1089
+ unsafe {
1090
+ std::env::set_var(key, value);
1091
+ }
1092
+ Self { key, previous }
1093
+ }
1094
+
1095
+ fn remove(key: &'static str) -> Self {
1096
+ let previous = std::env::var(key).ok();
1097
+ unsafe {
1098
+ std::env::remove_var(key);
1099
+ }
1100
+ Self { key, previous }
1101
+ }
1102
+ }
1103
+
1104
+ impl Drop for EnvGuard {
1105
+ fn drop(&mut self) {
1106
+ unsafe {
1107
+ if let Some(previous) = self.previous.take() {
1108
+ std::env::set_var(self.key, previous);
1109
+ } else {
1110
+ std::env::remove_var(self.key);
1111
+ }
1112
+ }
1113
+ }
1114
+ }
1115
+
1116
+ struct AlwaysLive;
1117
+
1118
+ impl team_agent::state::owner_gate::PaneLivenessProbe for AlwaysLive {
1119
+ fn liveness(&self, _pane_id: &str) -> PaneLiveness {
1120
+ PaneLiveness::Live
1121
+ }
1122
+ }
1123
+
1124
+ #[derive(Debug)]
1125
+ struct RecordedSpawn {
1126
+ kind: &'static str,
1127
+ session: String,
1128
+ window: String,
1129
+ }
1130
+
1131
+ #[derive(Debug, Default)]
1132
+ struct RecordingTransport {
1133
+ spawns: Mutex<Vec<RecordedSpawn>>,
1134
+ session_present: bool,
1135
+ windows: Vec<WindowName>,
1136
+ }
1137
+
1138
+ impl RecordingTransport {
1139
+ fn new() -> Self {
1140
+ Self::default()
1141
+ }
1142
+
1143
+ fn with_session_present(mut self, present: bool) -> Self {
1144
+ self.session_present = present;
1145
+ self
1146
+ }
1147
+
1148
+ fn with_windows(mut self, windows: Vec<WindowName>) -> Self {
1149
+ self.windows = windows;
1150
+ self
1151
+ }
1152
+
1153
+ fn record_spawn(
1154
+ &self,
1155
+ kind: &'static str,
1156
+ session: &SessionName,
1157
+ window: &WindowName,
1158
+ ) -> SpawnResult {
1159
+ let mut spawns = self.spawns.lock().unwrap();
1160
+ spawns.push(RecordedSpawn {
1161
+ kind,
1162
+ session: session.as_str().to_string(),
1163
+ window: window.as_str().to_string(),
1164
+ });
1165
+ SpawnResult {
1166
+ pane_id: PaneId::new(format!("%{}", spawns.len())),
1167
+ session: session.clone(),
1168
+ window: window.clone(),
1169
+ child_pid: None,
1170
+ }
1171
+ }
1172
+ }
1173
+
1174
+ impl Transport for RecordingTransport {
1175
+ fn kind(&self) -> BackendKind {
1176
+ BackendKind::Tmux
1177
+ }
1178
+
1179
+ fn spawn_first(
1180
+ &self,
1181
+ session: &SessionName,
1182
+ window: &WindowName,
1183
+ _argv: &[String],
1184
+ _cwd: &Path,
1185
+ _env: &BTreeMap<String, String>,
1186
+ ) -> Result<SpawnResult, TransportError> {
1187
+ Ok(self.record_spawn("spawn_first", session, window))
1188
+ }
1189
+
1190
+ fn spawn_into(
1191
+ &self,
1192
+ session: &SessionName,
1193
+ window: &WindowName,
1194
+ _argv: &[String],
1195
+ _cwd: &Path,
1196
+ _env: &BTreeMap<String, String>,
1197
+ ) -> Result<SpawnResult, TransportError> {
1198
+ Ok(self.record_spawn("spawn_into", session, window))
1199
+ }
1200
+
1201
+ fn inject(
1202
+ &self,
1203
+ _target: &Target,
1204
+ _payload: &InjectPayload,
1205
+ _submit: Key,
1206
+ _bracketed: bool,
1207
+ ) -> Result<InjectReport, TransportError> {
1208
+ Ok(InjectReport {
1209
+ stage_reached: InjectStage::Submit,
1210
+ inject_verification: InjectVerification::CaptureContainsToken,
1211
+ submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
1212
+ turn_verification: TurnVerification::NotYetObserved,
1213
+ attempts: 1,
1214
+ submit_diagnostics: None,
1215
+ })
1216
+ }
1217
+
1218
+ fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
1219
+ Ok(())
1220
+ }
1221
+
1222
+ fn capture(
1223
+ &self,
1224
+ _target: &Target,
1225
+ range: CaptureRange,
1226
+ ) -> Result<CapturedText, TransportError> {
1227
+ Ok(CapturedText {
1228
+ text: String::new(),
1229
+ range,
1230
+ })
1231
+ }
1232
+
1233
+ fn query(&self, _target: &Target, _field: PaneField) -> Result<Option<String>, TransportError> {
1234
+ Ok(None)
1235
+ }
1236
+
1237
+ fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
1238
+ Ok(PaneLiveness::Live)
1239
+ }
1240
+
1241
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
1242
+ Ok(Vec::new())
1243
+ }
1244
+
1245
+ fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
1246
+ Ok(self.session_present)
1247
+ }
1248
+
1249
+ fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
1250
+ Ok(self.windows.clone())
1251
+ }
1252
+
1253
+ fn set_session_env(
1254
+ &self,
1255
+ _session: &SessionName,
1256
+ _key: &str,
1257
+ _value: &str,
1258
+ ) -> Result<SetEnvOutcome, TransportError> {
1259
+ Ok(SetEnvOutcome::Applied)
1260
+ }
1261
+
1262
+ fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
1263
+ Ok(())
1264
+ }
1265
+
1266
+ fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
1267
+ Ok(())
1268
+ }
1269
+
1270
+ fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
1271
+ Ok(AttachOutcome::Attached)
1272
+ }
1273
+ }