@team-agent/installer 0.4.0 → 0.4.2

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 (45) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +82 -22
  4. package/crates/team-agent/src/cli/diagnose.rs +6 -2
  5. package/crates/team-agent/src/cli/emit.rs +301 -38
  6. package/crates/team-agent/src/cli/mod.rs +7 -14
  7. package/crates/team-agent/src/cli/status_port.rs +12 -1
  8. package/crates/team-agent/src/cli/tests/base.rs +6 -2
  9. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
  10. package/crates/team-agent/src/cli/tests/leader_watch.rs +5 -3
  11. package/crates/team-agent/src/cli/tests/main_preserved.rs +28 -2
  12. package/crates/team-agent/src/cli/tests/peer_allow.rs +19 -0
  13. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +72 -0
  14. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -3
  15. package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
  16. package/crates/team-agent/src/cli/types.rs +35 -2
  17. package/crates/team-agent/src/coordinator/runtime_detectors.rs +7 -2
  18. package/crates/team-agent/src/coordinator/tick.rs +86 -16
  19. package/crates/team-agent/src/diagnose/comms.rs +10 -2
  20. package/crates/team-agent/src/leader/lease.rs +98 -19
  21. package/crates/team-agent/src/leader/rediscover/tests.rs +21 -7
  22. package/crates/team-agent/src/leader/rediscover.rs +16 -7
  23. package/crates/team-agent/src/leader/start.rs +25 -12
  24. package/crates/team-agent/src/leader/tests/byte_findings.rs +11 -2
  25. package/crates/team-agent/src/leader/tests/lease_claim.rs +248 -7
  26. package/crates/team-agent/src/lifecycle/launch.rs +116 -100
  27. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +5 -12
  28. package/crates/team-agent/src/lifecycle/tests/core.rs +1 -1
  29. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +16 -15
  30. package/crates/team-agent/src/lifecycle/types.rs +1 -1
  31. package/crates/team-agent/src/messaging/leader_receiver.rs +22 -8
  32. package/crates/team-agent/src/messaging/send.rs +21 -4
  33. package/crates/team-agent/src/provider/adapter.rs +22 -0
  34. package/crates/team-agent/src/provider/session/capture.rs +228 -0
  35. package/crates/team-agent/src/state/identity.rs +42 -12
  36. package/crates/team-agent/src/state/identity_keys.rs +134 -0
  37. package/crates/team-agent/src/state/mod.rs +8 -0
  38. package/crates/team-agent/src/state/owner_gate.rs +11 -1
  39. package/crates/team-agent/src/state/ownership.rs +556 -0
  40. package/crates/team-agent/src/state/paths.rs +358 -0
  41. package/crates/team-agent/src/state/persist.rs +43 -5
  42. package/crates/team-agent/src/state/projection.rs +13 -5
  43. package/crates/team-agent/src/tmux_backend.rs +12 -0
  44. package/package.json +4 -4
  45. package/skills/team-agent/SKILL.md +3 -3
@@ -1031,6 +1031,17 @@ fn scan_agents_for_pane(agents: Option<&Value>, caller_pane: &PaneId) -> Option<
1031
1031
  None
1032
1032
  }
1033
1033
 
1034
+ /// Stage 3a (identity-boundary unified plan, architect direction 2026-06-23)
1035
+ /// + Stage 3 owner persist fix (architect direction 2026-06-24): route the
1036
+ /// in-memory binding write through the `state::ownership` API. The
1037
+ /// canonical team_key is derived from `active_team_key` first (the explicit
1038
+ /// scope set by `project_top_level_view` when an explicit team was passed
1039
+ /// to claim-leader) and only falls back to `team_state_key(state)` for the
1040
+ /// legacy unscoped flow. Without the `active_team_key` preference, an
1041
+ /// explicit `claim-leader --team alpha` whose projected state was emptied
1042
+ /// of team_dir/session_name would derive "current" and write the owner to
1043
+ /// `teams.current`, while `save_claim_team_scoped_state` expected
1044
+ /// `teams.alpha` — the S3-OWNER-001 persistence loss shape.
1034
1045
  fn write_binding_to_state(
1035
1046
  state: &mut Value,
1036
1047
  receiver: &LeaderReceiver,
@@ -1039,12 +1050,15 @@ fn write_binding_to_state(
1039
1050
  if !state.is_object() {
1040
1051
  *state = json!({});
1041
1052
  }
1042
- let Some(root) = state.as_object_mut() else {
1053
+ if !state.is_object() {
1043
1054
  return Err(LeaderError::Validation("state root is not an object".to_string()));
1044
- };
1045
- root.insert("leader_receiver".to_string(), serde_json::to_value(receiver)?);
1046
- root.insert("team_owner".to_string(), serde_json::to_value(owner)?);
1047
- root.insert("owner_epoch".to_string(), json!(owner.owner_epoch.0));
1055
+ }
1056
+ let team_key = canonical_owner_write_key(state);
1057
+ let record = crate::state::ownership::OwnershipWrite::new()
1058
+ .with_leader_receiver(serde_json::to_value(receiver)?)
1059
+ .with_team_owner(serde_json::to_value(owner)?)
1060
+ .with_owner_epoch(owner.owner_epoch.0);
1061
+ crate::state::ownership::write_owner(state, &team_key, record);
1048
1062
  Ok(())
1049
1063
  }
1050
1064
 
@@ -1055,13 +1069,34 @@ fn write_receiver_to_state(
1055
1069
  if !state.is_object() {
1056
1070
  *state = json!({});
1057
1071
  }
1058
- let Some(root) = state.as_object_mut() else {
1072
+ if !state.is_object() {
1059
1073
  return Err(LeaderError::Validation("state root is not an object".to_string()));
1060
- };
1061
- root.insert("leader_receiver".to_string(), serde_json::to_value(receiver)?);
1074
+ }
1075
+ let team_key = canonical_owner_write_key(state);
1076
+ let record = crate::state::ownership::OwnershipWrite::new()
1077
+ .with_leader_receiver(serde_json::to_value(receiver)?);
1078
+ crate::state::ownership::write_owner(state, &team_key, record);
1062
1079
  Ok(())
1063
1080
  }
1064
1081
 
1082
+ /// Stage 3 owner persist fix (architect direction 2026-06-24): determine
1083
+ /// the canonical team_key for an in-memory ownership write. When the state
1084
+ /// carries an `active_team_key` (set by `project_top_level_view` whenever
1085
+ /// an explicit team was scoped into the claim path), trust it — that's the
1086
+ /// requested team that `save_claim_team_scoped_state` will read back. Only
1087
+ /// fall back to `team_state_key(state)` for the legacy unscoped flow where
1088
+ /// the writer must derive team identity from session/dir/spec_path fields.
1089
+ fn canonical_owner_write_key(state: &Value) -> String {
1090
+ if let Some(active) = state
1091
+ .get("active_team_key")
1092
+ .and_then(Value::as_str)
1093
+ .filter(|key| !key.is_empty())
1094
+ {
1095
+ return active.to_string();
1096
+ }
1097
+ crate::state::projection::team_state_key(state)
1098
+ }
1099
+
1065
1100
  fn state_receiver(state: &Value) -> Option<LeaderReceiver> {
1066
1101
  state
1067
1102
  .get("leader_receiver")
@@ -1139,9 +1174,16 @@ fn save_claim_team_scoped_state(workspace: &Path, state: &Value, target_key: &st
1139
1174
  .entry(existing_key)
1140
1175
  .or_insert_with(|| crate::state::projection::compact_team_state(&existing));
1141
1176
  }
1177
+ // Stage 3 owner persist fix (architect direction 2026-06-24,
1178
+ // .team/artifacts/stage3-owner-persist-fix.md): after Stage 3d removed
1179
+ // top-level dual-write, the canonical ownership record lives ONLY at
1180
+ // `state.teams[target_key].{team_owner, leader_receiver, owner_epoch}`.
1181
+ // `compact_team_state(state)` strips the `teams` key entirely — that
1182
+ // dropped the just-written nested ownership record on disk. Use the
1183
+ // preserving helper so the canonical fields survive the compaction.
1142
1184
  teams.insert(
1143
1185
  target_key.to_string(),
1144
- crate::state::projection::compact_team_state(state),
1186
+ compact_team_state_preserving_ownership(state, target_key),
1145
1187
  );
1146
1188
  let existing_primary_key = existing
1147
1189
  .get("session_name")
@@ -1154,16 +1196,18 @@ fn save_claim_team_scoped_state(workspace: &Path, state: &Value, target_key: &st
1154
1196
  } else {
1155
1197
  value_object(&existing)
1156
1198
  };
1157
- if !had_existing_teams
1158
- || existing_primary_key.as_deref().is_none_or(|key| key == target_key)
1159
- || existing_active_key == Some(target_key)
1160
- {
1161
- for key in ["leader_receiver", "team_owner", "owner_epoch"] {
1162
- if let Some(value) = state.get(key) {
1163
- merged.insert(key.to_string(), value.clone());
1164
- }
1165
- }
1166
- }
1199
+ // Stage 3 top-level cleanup (architect direction 2026-06-24,
1200
+ // .team/artifacts/stage3-toplevel-cleanup-fix.md + stage3-save-strip-fix.md):
1201
+ // the legacy top-level owner copy loop is gone; the canonical-aware
1202
+ // strip is now performed at the persistence boundary
1203
+ // (`state::persist::save_runtime_state_with_merge_exceptions`), which
1204
+ // every save_runtime_state* path funnels through. This per-call-site
1205
+ // strip call has been removed — the save output handles cleanup
1206
+ // uniformly for claim / restart / shutdown / start-agent / stop-agent
1207
+ // / coordinator tick / promote sibling. `had_existing_teams` /
1208
+ // primary/active key computations are retained as dead writes only
1209
+ // via _ binding — they no longer gate any owner promote.
1210
+ let _ = (had_existing_teams, existing_primary_key, existing_active_key);
1167
1211
  merged.insert("teams".to_string(), Value::Object(teams));
1168
1212
  crate::state::persist::save_runtime_state(workspace, &Value::Object(merged))?;
1169
1213
  Ok(())
@@ -1173,6 +1217,41 @@ fn value_object(value: &Value) -> serde_json::Map<String, Value> {
1173
1217
  value.as_object().cloned().unwrap_or_default()
1174
1218
  }
1175
1219
 
1220
+ /// Stage 3 owner persist fix (architect direction 2026-06-24,
1221
+ /// .team/artifacts/stage3-owner-persist-fix.md): build the per-team
1222
+ /// snapshot used by `save_claim_team_scoped_state` so the canonical
1223
+ /// `teams.<target_key>.{team_owner, leader_receiver, owner_epoch}` record
1224
+ /// survives the `compact_team_state` strip. Without this helper the
1225
+ /// claim-leader save loses the just-written nested ownership record (the
1226
+ /// S3-OWNER-001 evidence shape: `persisted_owner_locations=[]`).
1227
+ ///
1228
+ /// Behaviour:
1229
+ /// - Start from the existing `compact_team_state(state)` (strips `teams`).
1230
+ /// - Look up `state.teams[target_key].{team_owner, leader_receiver, owner_epoch}`
1231
+ /// and copy each present field into the compacted entry.
1232
+ /// - Top-level owner fields stay UNTOUCHED — Stage 3d's canonical-only
1233
+ /// shape is preserved on disk.
1234
+ fn compact_team_state_preserving_ownership(state: &Value, target_key: &str) -> Value {
1235
+ let mut entry = crate::state::projection::compact_team_state(state);
1236
+ let Some(entry_obj) = entry.as_object_mut() else {
1237
+ return entry;
1238
+ };
1239
+ let Some(owner_obj) = state
1240
+ .get("teams")
1241
+ .and_then(Value::as_object)
1242
+ .and_then(|teams| teams.get(target_key))
1243
+ .and_then(Value::as_object)
1244
+ else {
1245
+ return entry;
1246
+ };
1247
+ for key in ["team_owner", "leader_receiver", "owner_epoch"] {
1248
+ if let Some(value) = owner_obj.get(key) {
1249
+ entry_obj.insert(key.to_string(), value.clone());
1250
+ }
1251
+ }
1252
+ entry
1253
+ }
1254
+
1176
1255
  /// `_detect_dual_state_divergence`(card §85 C18;`__init__.py:556`)。workspace-level 与
1177
1256
  /// team-level snapshot 在 owner_uuid/receiver_pane_id/owner_epoch 上是否分叉 → `Some(详情)`。
1178
1257
  /// **CROSS-LANE**:snapshot 读经 step 13 restart。
@@ -127,10 +127,14 @@ fn try_readopt_writes_owner_receiver_dual_state_and_events() {
127
127
  assert_eq!(got.1["pane"]["pane_id"], json!("%new"));
128
128
  assert_eq!(got.1["readopted"], json!(true));
129
129
  assert!(got.1.get("warning").is_some_and(Value::is_null));
130
- assert_eq!(state["team_owner"]["pane_id"], json!("%new"));
131
- assert_eq!(state["team_owner"]["claimed_via"], json!("attach-leader"));
132
- assert_eq!(state["team_owner"]["owner_epoch"], json!(4));
133
- assert_eq!(state["leader_receiver"]["discovery"], json!("attach_readopt"));
130
+ // Stage 3d: owner + receiver live at canonical teams.<team_key>.
131
+ let team_key = crate::state::projection::team_state_key(&state);
132
+ let owner_view = &state["teams"][&team_key]["team_owner"];
133
+ let receiver_view = &state["teams"][&team_key]["leader_receiver"];
134
+ assert_eq!(owner_view["pane_id"], json!("%new"));
135
+ assert_eq!(owner_view["claimed_via"], json!("attach-leader"));
136
+ assert_eq!(owner_view["owner_epoch"], json!(4));
137
+ assert_eq!(receiver_view["discovery"], json!("attach_readopt"));
134
138
  assert!(crate::model::paths::runtime_dir(&ws)
135
139
  .join("teams")
136
140
  .join("sess")
@@ -185,6 +189,10 @@ fn try_readopt_refuses_when_different_live_owner_uuid_mismatches() {
185
189
  .unwrap();
186
190
 
187
191
  assert!(got.is_none());
192
+ // Stage 3d: no owner anywhere — check both legacy top-level and the
193
+ // canonical teams.<key> location via the ownership repository.
194
+ let team_key = crate::state::projection::team_state_key(&state);
195
+ assert!(crate::state::ownership::read_owner_value(&state, &team_key).is_none());
188
196
  assert!(state.get("team_owner").is_none());
189
197
  }
190
198
 
@@ -227,7 +235,10 @@ fn rediscover_updates_unique_env_triple_match() {
227
235
  assert_eq!(out["status"], json!("updated"));
228
236
  assert!(out.get("ok").is_none(), "golden rediscover result has no ok wrapper");
229
237
  assert_eq!(out["owner_identity"]["pane_id"], json!("%r"));
230
- assert_eq!(state["leader_receiver"]["pane_id"], json!("%r"));
238
+ // Stage 3d: receiver lives at canonical teams.<team_key>.leader_receiver.
239
+ let team_key = crate::state::projection::team_state_key(&state);
240
+ let receiver = &state["teams"][&team_key]["leader_receiver"];
241
+ assert_eq!(receiver["pane_id"], json!("%r"));
231
242
  let events = event_log.tail(10).unwrap();
232
243
  let rediscovered = event_named(&events, "leader_receiver.rediscovered");
233
244
  assert_eq!(rediscovered["provider"], json!("codex"));
@@ -241,7 +252,7 @@ fn rediscover_updates_unique_env_triple_match() {
241
252
  assert_eq!(rebound["new_pane_id"], json!("%r"));
242
253
  assert_eq!(rebound["owner_identity"]["machine_fingerprint"], json!("fp"));
243
254
  assert_eq!(rebound["uuid_prefix"], json!("OWNERUUI"));
244
- assert_eq!(state["leader_receiver"]["discovery"], json!("stale_rediscovery_unique_candidate"));
255
+ assert_eq!(receiver["discovery"], json!("stale_rediscovery_unique_candidate"));
245
256
  }
246
257
 
247
258
  #[test]
@@ -286,7 +297,10 @@ fn rediscover_owner_identity_passthrough_sets_discovery_and_reason() {
286
297
 
287
298
  assert_eq!(out["status"], json!("updated"));
288
299
  assert_eq!(out["owner_identity"], raw_identity);
289
- assert_eq!(state["leader_receiver"]["discovery"], json!("stale_rediscovery_owner_identity"));
300
+ // Stage 3d: receiver lives at canonical teams.<team_key>.leader_receiver.
301
+ let team_key = crate::state::projection::team_state_key(&state);
302
+ let receiver = &state["teams"][&team_key]["leader_receiver"];
303
+ assert_eq!(receiver["discovery"], json!("stale_rediscovery_owner_identity"));
290
304
  let rebound = event_named(&event_log.tail(10).unwrap(), "leader_receiver.rebind_applied");
291
305
  assert_eq!(rebound["reason"], json!("stale_receiver"));
292
306
  assert_eq!(rebound["owner_identity"], raw_identity);
@@ -1064,6 +1064,9 @@ fn state_receiver(state: &Value) -> Option<LeaderReceiver> {
1064
1064
  .and_then(|value| serde_json::from_value(value).ok())
1065
1065
  }
1066
1066
 
1067
+ /// Stage 3a (identity-boundary unified plan, architect direction 2026-06-23):
1068
+ /// route the readopt binding write through the ownership repository.
1069
+ /// Behaviour byte-identical to the pre-3a hand-rolled inserts.
1067
1070
  fn write_readopt_state(
1068
1071
  workspace: &Path,
1069
1072
  state: &mut Value,
@@ -1073,11 +1076,14 @@ fn write_readopt_state(
1073
1076
  if !state.is_object() {
1074
1077
  *state = json!({});
1075
1078
  }
1076
- let Some(root) = state.as_object_mut() else {
1079
+ if !state.is_object() {
1077
1080
  return Err(LeaderError::Validation("state root is not an object".to_string()));
1078
- };
1079
- root.insert("leader_receiver".to_string(), serde_json::to_value(receiver)?);
1080
- root.insert("team_owner".to_string(), serde_json::to_value(owner)?);
1081
+ }
1082
+ let team_key = crate::state::projection::team_state_key(state);
1083
+ let record = crate::state::ownership::OwnershipWrite::new()
1084
+ .with_leader_receiver(serde_json::to_value(receiver)?)
1085
+ .with_team_owner(serde_json::to_value(owner)?);
1086
+ crate::state::ownership::write_owner(state, &team_key, record);
1081
1087
  crate::leader::write_lease_dual_state(workspace, state)
1082
1088
  }
1083
1089
 
@@ -1089,10 +1095,13 @@ fn write_receiver_state(
1089
1095
  if !state.is_object() {
1090
1096
  *state = json!({});
1091
1097
  }
1092
- let Some(root) = state.as_object_mut() else {
1098
+ if !state.is_object() {
1093
1099
  return Err(LeaderError::Validation("state root is not an object".to_string()));
1094
- };
1095
- root.insert("leader_receiver".to_string(), serde_json::to_value(receiver)?);
1100
+ }
1101
+ let team_key = crate::state::projection::team_state_key(state);
1102
+ let record = crate::state::ownership::OwnershipWrite::new()
1103
+ .with_leader_receiver(serde_json::to_value(receiver)?);
1104
+ crate::state::ownership::write_owner(state, &team_key, record);
1096
1105
  crate::leader::write_lease_dual_state(workspace, state)
1097
1106
  }
1098
1107
 
@@ -587,10 +587,13 @@ fn persist_managed_leader_binding(
587
587
  "tmux": std::env::var("TMUX").ok(),
588
588
  }),
589
589
  );
590
- obj.insert("leader_receiver".to_string(), receiver);
591
- obj.insert("team_owner".to_string(), owner);
592
- obj.insert("owner_epoch".to_string(), serde_json::json!(owner_epoch));
593
590
  }
591
+ // Stage 3a/d (identity-boundary unified plan, architect direction
592
+ // 2026-06-23): build the teams.<key> entry from the non-owner fields
593
+ // of state (compact_team_state strips the `teams` key), then publish
594
+ // the canonical owner record AFTER teams.insert so write_owner's
595
+ // teams.<key>.{team_owner,leader_receiver,owner_epoch} is the final
596
+ // word and doesn't get overwritten by the compacted entry.
594
597
  let entry = crate::state::projection::compact_team_state(&state);
595
598
  if let Some(obj) = state.as_object_mut() {
596
599
  let teams = obj
@@ -600,6 +603,11 @@ fn persist_managed_leader_binding(
600
603
  teams.insert(identity.team_id.as_str().to_string(), entry);
601
604
  }
602
605
  }
606
+ let record = crate::state::ownership::OwnershipWrite::new()
607
+ .with_leader_receiver(receiver)
608
+ .with_team_owner(owner)
609
+ .with_owner_epoch(owner_epoch);
610
+ crate::state::ownership::write_owner(&mut state, identity.team_id.as_str(), record);
603
611
  crate::state::persist::save_runtime_state(workspace, &state)?;
604
612
  Ok(())
605
613
  }
@@ -689,10 +697,11 @@ fn persist_exec_provider_leader_binding(
689
697
  "tmux": std::env::var("TMUX").ok(),
690
698
  }),
691
699
  );
692
- obj.insert("leader_receiver".to_string(), receiver);
693
- obj.insert("team_owner".to_string(), owner);
694
- obj.insert("owner_epoch".to_string(), serde_json::json!(owner_epoch));
695
700
  }
701
+ // Stage 3a/d (identity-boundary unified plan, architect direction
702
+ // 2026-06-23): compact-then-write-owner ordering as in
703
+ // persist_managed_leader_binding above. write_owner must be the final
704
+ // write so the canonical teams.<key> owner record survives.
696
705
  let entry = crate::state::projection::compact_team_state(&state);
697
706
  if let Some(obj) = state.as_object_mut() {
698
707
  let teams = obj
@@ -702,6 +711,11 @@ fn persist_exec_provider_leader_binding(
702
711
  teams.insert(identity.team_id.as_str().to_string(), entry);
703
712
  }
704
713
  }
714
+ let record = crate::state::ownership::OwnershipWrite::new()
715
+ .with_leader_receiver(receiver)
716
+ .with_team_owner(owner)
717
+ .with_owner_epoch(owner_epoch);
718
+ crate::state::ownership::write_owner(&mut state, identity.team_id.as_str(), record);
705
719
  crate::state::persist::save_runtime_state(workspace, &state)?;
706
720
  Ok(())
707
721
  }
@@ -1351,16 +1365,15 @@ mod tests {
1351
1365
  assert_eq!(outcome.status, crate::leader::LeaderLaunchStatus::Exited);
1352
1366
  let state = crate::state::persist::load_runtime_state(&workspace).unwrap();
1353
1367
  assert_eq!(state["is_external_leader"], serde_json::json!(false));
1354
- assert_eq!(state["leader_receiver"]["pane_id"], serde_json::json!("%77"));
1355
- assert_eq!(
1356
- state["leader_receiver"]["tmux_socket"],
1357
- serde_json::json!("/private/tmp/tmux-501/default")
1358
- );
1359
- assert_eq!(state["team_owner"]["pane_id"], serde_json::json!("%77"));
1368
+ // Stage 3d: canonical owner/receiver at teams.<team_key>.
1360
1369
  assert_eq!(
1361
1370
  state["teams"]["current"]["leader_receiver"]["pane_id"],
1362
1371
  serde_json::json!("%77")
1363
1372
  );
1373
+ assert_eq!(
1374
+ state["teams"]["current"]["leader_receiver"]["tmux_socket"],
1375
+ serde_json::json!("/private/tmp/tmux-501/default")
1376
+ );
1364
1377
  assert_eq!(
1365
1378
  state["teams"]["current"]["team_owner"]["pane_id"],
1366
1379
  serde_json::json!("%77")
@@ -10,7 +10,12 @@ use super::*;
10
10
  let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
11
11
  &PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
12
12
  assert!(r.ok && r.status == LeaseStatus::Claimed, "precondition: vacant acquire");
13
- let owner = state["team_owner"].as_object().expect("team_owner object");
13
+ // Stage 3d (identity-boundary unified plan, architect direction
14
+ // 2026-06-23): canonical owner now at teams.<team_key>.team_owner.
15
+ let team_key = crate::state::projection::team_state_key(&state);
16
+ let owner = state["teams"][&team_key]["team_owner"]
17
+ .as_object()
18
+ .expect("Stage 3d: team_owner at canonical teams.<team_key>");
14
19
  assert!(owner.contains_key("os_user"),
15
20
  "golden claim-path team_owner carries os_user even when empty; keys={:?}", owner.keys().collect::<Vec<_>>());
16
21
  let keys: Vec<&str> = owner.keys().map(String::as_str).collect();
@@ -37,7 +42,11 @@ use super::*;
37
42
  let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
38
43
  &PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
39
44
  assert!(r.ok && r.status == LeaseStatus::Claimed, "precondition: vacant acquire");
40
- let recv = state["leader_receiver"].as_object().expect("leader_receiver object");
45
+ // Stage 3d: canonical receiver at teams.<team_key>.leader_receiver.
46
+ let team_key = crate::state::projection::team_state_key(&state);
47
+ let recv = state["teams"][&team_key]["leader_receiver"]
48
+ .as_object()
49
+ .expect("Stage 3d: leader_receiver at canonical teams.<team_key>");
41
50
  for extra in ["fingerprint", "requested_provider", "warning"] {
42
51
  assert!(!recv.contains_key(extra),
43
52
  "golden leader_receiver has NO '{extra}' key (Rust must add skip_serializing_if); keys={:?}", recv.keys().collect::<Vec<_>>());