@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +82 -22
- package/crates/team-agent/src/cli/diagnose.rs +6 -2
- package/crates/team-agent/src/cli/emit.rs +301 -38
- package/crates/team-agent/src/cli/mod.rs +7 -14
- package/crates/team-agent/src/cli/status_port.rs +12 -1
- package/crates/team-agent/src/cli/tests/base.rs +6 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
- package/crates/team-agent/src/cli/tests/leader_watch.rs +5 -3
- package/crates/team-agent/src/cli/tests/main_preserved.rs +28 -2
- package/crates/team-agent/src/cli/tests/peer_allow.rs +19 -0
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +72 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +35 -2
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +7 -2
- package/crates/team-agent/src/coordinator/tick.rs +86 -16
- package/crates/team-agent/src/diagnose/comms.rs +10 -2
- package/crates/team-agent/src/leader/lease.rs +98 -19
- package/crates/team-agent/src/leader/rediscover/tests.rs +21 -7
- package/crates/team-agent/src/leader/rediscover.rs +16 -7
- package/crates/team-agent/src/leader/start.rs +25 -12
- package/crates/team-agent/src/leader/tests/byte_findings.rs +11 -2
- package/crates/team-agent/src/leader/tests/lease_claim.rs +248 -7
- package/crates/team-agent/src/lifecycle/launch.rs +116 -100
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +5 -12
- package/crates/team-agent/src/lifecycle/tests/core.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +16 -15
- package/crates/team-agent/src/lifecycle/types.rs +1 -1
- package/crates/team-agent/src/messaging/leader_receiver.rs +22 -8
- package/crates/team-agent/src/messaging/send.rs +21 -4
- package/crates/team-agent/src/provider/adapter.rs +22 -0
- package/crates/team-agent/src/provider/session/capture.rs +228 -0
- package/crates/team-agent/src/state/identity.rs +42 -12
- package/crates/team-agent/src/state/identity_keys.rs +134 -0
- package/crates/team-agent/src/state/mod.rs +8 -0
- package/crates/team-agent/src/state/owner_gate.rs +11 -1
- package/crates/team-agent/src/state/ownership.rs +556 -0
- package/crates/team-agent/src/state/paths.rs +358 -0
- package/crates/team-agent/src/state/persist.rs +43 -5
- package/crates/team-agent/src/state/projection.rs +13 -5
- package/crates/team-agent/src/tmux_backend.rs +12 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +3 -3
|
@@ -1141,12 +1141,21 @@ fn drop_foreign_seeded_owner(
|
|
|
1141
1141
|
};
|
|
1142
1142
|
if owner_pane_belongs_to_other_team(existing, launched_key, pane) {
|
|
1143
1143
|
let replacement = unbound_launched_owner(launched, launched_key);
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1144
|
+
// Stage 3a (identity-boundary unified plan, architect direction
|
|
1145
|
+
// 2026-06-23): route owner replace through ownership repository.
|
|
1146
|
+
// Both branches (Some owner replacement; None = removal) preserve
|
|
1147
|
+
// pre-3a behaviour: write_owner overwrites the top-level fields
|
|
1148
|
+
// when given; the None branch falls back to direct mutation (the
|
|
1149
|
+
// repository has no remove API yet — removal is uncommon enough
|
|
1150
|
+
// to leave it inline).
|
|
1151
|
+
if let Some(owner) = replacement {
|
|
1152
|
+
let record = crate::state::ownership::OwnershipWrite::new().with_team_owner(owner);
|
|
1153
|
+
crate::state::ownership::write_owner(launched, launched_key, record);
|
|
1154
|
+
if let Some(obj) = launched.as_object_mut() {
|
|
1155
|
+
obj.remove("owner_epoch");
|
|
1149
1156
|
}
|
|
1157
|
+
} else if let Some(obj) = launched.as_object_mut() {
|
|
1158
|
+
obj.remove("team_owner");
|
|
1150
1159
|
obj.remove("owner_epoch");
|
|
1151
1160
|
}
|
|
1152
1161
|
}
|
|
@@ -1423,11 +1432,13 @@ fn seed_unbound_launched_owner(launched: &mut serde_json::Value, launched_key: &
|
|
|
1423
1432
|
"owner_epoch": owner_epoch,
|
|
1424
1433
|
"discovery": "quick_start",
|
|
1425
1434
|
});
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1435
|
+
// Stage 3a (identity-boundary unified plan, architect direction 2026-06-23):
|
|
1436
|
+
// route quick-start unbound seed through ownership repository.
|
|
1437
|
+
let record = crate::state::ownership::OwnershipWrite::new()
|
|
1438
|
+
.with_leader_receiver(receiver)
|
|
1439
|
+
.with_team_owner(owner)
|
|
1440
|
+
.with_owner_epoch(owner_epoch);
|
|
1441
|
+
crate::state::ownership::write_owner(launched, launched_key, record);
|
|
1431
1442
|
}
|
|
1432
1443
|
|
|
1433
1444
|
fn unbound_launched_owner(
|
|
@@ -1536,15 +1547,16 @@ mod e22_unbound_owner_provider_tests {
|
|
|
1536
1547
|
|
|
1537
1548
|
seed_unbound_launched_owner(&mut launched, "team-e22");
|
|
1538
1549
|
|
|
1550
|
+
// Stage 3d: canonical owner/receiver at teams.team-e22.
|
|
1539
1551
|
assert_eq!(
|
|
1540
1552
|
launched
|
|
1541
|
-
.pointer("/team_owner/provider")
|
|
1553
|
+
.pointer("/teams/team-e22/team_owner/provider")
|
|
1542
1554
|
.and_then(serde_json::Value::as_str),
|
|
1543
1555
|
Some("copilot")
|
|
1544
1556
|
);
|
|
1545
1557
|
assert_eq!(
|
|
1546
1558
|
launched
|
|
1547
|
-
.pointer("/leader_receiver/provider")
|
|
1559
|
+
.pointer("/teams/team-e22/leader_receiver/provider")
|
|
1548
1560
|
.and_then(serde_json::Value::as_str),
|
|
1549
1561
|
Some("copilot")
|
|
1550
1562
|
);
|
|
@@ -1561,13 +1573,16 @@ mod e22_unbound_owner_provider_tests {
|
|
|
1561
1573
|
|
|
1562
1574
|
seed_unbound_launched_owner(&mut launched, "team-e22");
|
|
1563
1575
|
|
|
1576
|
+
// Stage 3d: no receiver should be seeded at canonical location.
|
|
1564
1577
|
assert!(
|
|
1565
|
-
launched
|
|
1578
|
+
launched
|
|
1579
|
+
.pointer("/teams/team-e22/leader_receiver")
|
|
1580
|
+
.is_none(),
|
|
1566
1581
|
"unattributed unbound owner must not seed a codex receiver: {launched}"
|
|
1567
1582
|
);
|
|
1568
1583
|
assert!(
|
|
1569
1584
|
launched
|
|
1570
|
-
.pointer("/team_owner/provider")
|
|
1585
|
+
.pointer("/teams/team-e22/team_owner/provider")
|
|
1571
1586
|
.and_then(serde_json::Value::as_str)
|
|
1572
1587
|
!= Some("codex"),
|
|
1573
1588
|
"unattributed unbound owner must not silently become codex: {launched}"
|
|
@@ -1597,16 +1612,15 @@ mod e22_unbound_owner_provider_tests {
|
|
|
1597
1612
|
|pane| (pane.as_str() == "%0").then_some(Provider::Copilot),
|
|
1598
1613
|
));
|
|
1599
1614
|
|
|
1615
|
+
// Stage 3d: canonical owner/receiver at teams.<team_state_key>.
|
|
1616
|
+
// State has no team_key/team_dir/spec_path → fallback "current".
|
|
1617
|
+
let team_key = crate::state::projection::team_state_key(&state);
|
|
1600
1618
|
assert_eq!(
|
|
1601
|
-
state
|
|
1602
|
-
.pointer("/team_owner/provider")
|
|
1603
|
-
.and_then(serde_json::Value::as_str),
|
|
1619
|
+
state["teams"][&team_key]["team_owner"]["provider"].as_str(),
|
|
1604
1620
|
Some("copilot")
|
|
1605
1621
|
);
|
|
1606
1622
|
assert_eq!(
|
|
1607
|
-
state
|
|
1608
|
-
.pointer("/leader_receiver/provider")
|
|
1609
|
-
.and_then(serde_json::Value::as_str),
|
|
1623
|
+
state["teams"][&team_key]["leader_receiver"]["provider"].as_str(),
|
|
1610
1624
|
Some("copilot")
|
|
1611
1625
|
);
|
|
1612
1626
|
}
|
|
@@ -1623,30 +1637,21 @@ mod e22_unbound_owner_provider_tests {
|
|
|
1623
1637
|
caller("", "%0"),
|
|
1624
1638
|
|_| None,
|
|
1625
1639
|
));
|
|
1640
|
+
let team_key = crate::state::projection::team_state_key(&state);
|
|
1626
1641
|
assert_eq!(
|
|
1627
|
-
state
|
|
1628
|
-
.pointer("/team_owner/pane_id")
|
|
1629
|
-
.and_then(serde_json::Value::as_str),
|
|
1642
|
+
state["teams"][&team_key]["team_owner"]["pane_id"].as_str(),
|
|
1630
1643
|
Some("%0")
|
|
1631
1644
|
);
|
|
1632
1645
|
assert_eq!(
|
|
1633
|
-
state
|
|
1634
|
-
.pointer("/leader_receiver/pane_id")
|
|
1635
|
-
.and_then(serde_json::Value::as_str),
|
|
1646
|
+
state["teams"][&team_key]["leader_receiver"]["pane_id"].as_str(),
|
|
1636
1647
|
Some("%0")
|
|
1637
1648
|
);
|
|
1638
1649
|
assert!(
|
|
1639
|
-
state
|
|
1640
|
-
.pointer("/leader_receiver/provider")
|
|
1641
|
-
.and_then(serde_json::Value::as_str)
|
|
1642
|
-
!= Some("codex"),
|
|
1650
|
+
state["teams"][&team_key]["leader_receiver"]["provider"].as_str() != Some("codex"),
|
|
1643
1651
|
"unknown caller pane must not silently seed a codex receiver: {state}"
|
|
1644
1652
|
);
|
|
1645
1653
|
assert!(
|
|
1646
|
-
state
|
|
1647
|
-
.pointer("/team_owner/provider")
|
|
1648
|
-
.and_then(serde_json::Value::as_str)
|
|
1649
|
-
!= Some("codex"),
|
|
1654
|
+
state["teams"][&team_key]["team_owner"]["provider"].as_str() != Some("codex"),
|
|
1650
1655
|
"unknown caller pane must not silently become codex: {state}"
|
|
1651
1656
|
);
|
|
1652
1657
|
}
|
|
@@ -2653,18 +2658,17 @@ fn json_team_identity_matches(state: &serde_json::Value, team: &str) -> bool {
|
|
|
2653
2658
|
.is_some_and(|value| value == team)
|
|
2654
2659
|
}
|
|
2655
2660
|
|
|
2656
|
-
/// `quick_start(agents_dir, name, yes,
|
|
2661
|
+
/// `quick_start(agents_dir, name, yes, team_id)`(`diagnose/quick_start.py:18`)。
|
|
2657
2662
|
/// 面向用户的零配置入口:编译 team_dir → `launch` → autobind leader receiver → 起
|
|
2658
2663
|
/// coordinator → `wait_ready` 轮询就绪。归入 lifecycle module(不与 diagnose 混)。
|
|
2659
2664
|
pub fn quick_start(
|
|
2660
2665
|
agents_dir: &Path,
|
|
2661
2666
|
name: Option<&str>,
|
|
2662
2667
|
yes: bool,
|
|
2663
|
-
fresh: bool,
|
|
2664
2668
|
team_id: Option<&str>,
|
|
2665
2669
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
2666
2670
|
let workspace = team_workspace(agents_dir);
|
|
2667
|
-
quick_start_in_workspace(&workspace, agents_dir, name, yes,
|
|
2671
|
+
quick_start_in_workspace(&workspace, agents_dir, name, yes, team_id)
|
|
2668
2672
|
}
|
|
2669
2673
|
|
|
2670
2674
|
pub fn quick_start_in_workspace(
|
|
@@ -2672,7 +2676,6 @@ pub fn quick_start_in_workspace(
|
|
|
2672
2676
|
agents_dir: &Path,
|
|
2673
2677
|
name: Option<&str>,
|
|
2674
2678
|
yes: bool,
|
|
2675
|
-
fresh: bool,
|
|
2676
2679
|
team_id: Option<&str>,
|
|
2677
2680
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
2678
2681
|
let workspace = explicit_quick_start_workspace(workspace);
|
|
@@ -2682,7 +2685,6 @@ pub fn quick_start_in_workspace(
|
|
|
2682
2685
|
agents_dir,
|
|
2683
2686
|
name,
|
|
2684
2687
|
yes,
|
|
2685
|
-
fresh,
|
|
2686
2688
|
team_id,
|
|
2687
2689
|
&transport,
|
|
2688
2690
|
)
|
|
@@ -2693,7 +2695,6 @@ pub fn quick_start_in_workspace_with_display(
|
|
|
2693
2695
|
agents_dir: &Path,
|
|
2694
2696
|
name: Option<&str>,
|
|
2695
2697
|
yes: bool,
|
|
2696
|
-
fresh: bool,
|
|
2697
2698
|
team_id: Option<&str>,
|
|
2698
2699
|
open_display: bool,
|
|
2699
2700
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
@@ -2704,7 +2705,6 @@ pub fn quick_start_in_workspace_with_display(
|
|
|
2704
2705
|
agents_dir,
|
|
2705
2706
|
name,
|
|
2706
2707
|
yes,
|
|
2707
|
-
fresh,
|
|
2708
2708
|
team_id,
|
|
2709
2709
|
&transport,
|
|
2710
2710
|
open_display,
|
|
@@ -2779,13 +2779,12 @@ pub fn quick_start_with_transport(
|
|
|
2779
2779
|
agents_dir: &Path,
|
|
2780
2780
|
name: Option<&str>,
|
|
2781
2781
|
yes: bool,
|
|
2782
|
-
fresh: bool,
|
|
2783
2782
|
team_id: Option<&str>,
|
|
2784
2783
|
transport: &dyn Transport,
|
|
2785
2784
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
2786
2785
|
let workspace = team_workspace(agents_dir);
|
|
2787
2786
|
quick_start_with_transport_in_workspace(
|
|
2788
|
-
&workspace, agents_dir, name, yes,
|
|
2787
|
+
&workspace, agents_dir, name, yes, team_id, transport,
|
|
2789
2788
|
)
|
|
2790
2789
|
}
|
|
2791
2790
|
|
|
@@ -2794,12 +2793,11 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2794
2793
|
agents_dir: &Path,
|
|
2795
2794
|
name: Option<&str>,
|
|
2796
2795
|
yes: bool,
|
|
2797
|
-
fresh: bool,
|
|
2798
2796
|
team_id: Option<&str>,
|
|
2799
2797
|
transport: &dyn Transport,
|
|
2800
2798
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
2801
2799
|
quick_start_with_transport_in_workspace_with_display(
|
|
2802
|
-
workspace, agents_dir, name, yes,
|
|
2800
|
+
workspace, agents_dir, name, yes, team_id, transport, true,
|
|
2803
2801
|
)
|
|
2804
2802
|
}
|
|
2805
2803
|
|
|
@@ -2808,7 +2806,6 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
2808
2806
|
agents_dir: &Path,
|
|
2809
2807
|
name: Option<&str>,
|
|
2810
2808
|
yes: bool,
|
|
2811
|
-
fresh: bool,
|
|
2812
2809
|
team_id: Option<&str>,
|
|
2813
2810
|
transport: &dyn Transport,
|
|
2814
2811
|
open_display: bool,
|
|
@@ -2851,57 +2848,63 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
2851
2848
|
team_depth.team_depth.saturating_sub(1)
|
|
2852
2849
|
)));
|
|
2853
2850
|
}
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
.
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2851
|
+
let state_path = crate::state::persist::runtime_state_path(&workspace);
|
|
2852
|
+
if state_path.exists() {
|
|
2853
|
+
let state = crate::state::persist::load_runtime_state(&workspace)
|
|
2854
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
2855
|
+
if requested_team
|
|
2856
|
+
.as_deref()
|
|
2857
|
+
.is_none_or(|team| runtime_state_has_quick_start_team(&state, team))
|
|
2858
|
+
{
|
|
2859
|
+
let session_name = state
|
|
2860
|
+
.get("session_name")
|
|
2861
|
+
.and_then(serde_json::Value::as_str)
|
|
2862
|
+
.filter(|s| !s.is_empty())
|
|
2863
|
+
.map(SessionName::new);
|
|
2864
|
+
let attach_commands = session_name
|
|
2865
|
+
.as_ref()
|
|
2866
|
+
.map(|session| {
|
|
2867
|
+
let windows = quick_start_attach_window_names(&state);
|
|
2868
|
+
attach_commands_for_runtime_windows(
|
|
2869
|
+
state
|
|
2870
|
+
.get("tmux_endpoint")
|
|
2871
|
+
.and_then(serde_json::Value::as_str)
|
|
2872
|
+
.or_else(|| {
|
|
2873
|
+
state.get("tmux_socket").and_then(serde_json::Value::as_str)
|
|
2874
|
+
}),
|
|
2875
|
+
&workspace,
|
|
2876
|
+
session,
|
|
2877
|
+
windows.iter().map(String::as_str),
|
|
2878
|
+
)
|
|
2879
|
+
})
|
|
2880
|
+
.unwrap_or_default();
|
|
2881
|
+
// Stage QR (design doc .team/artifacts/quickstart-restart-separation-design.md):
|
|
2882
|
+
// quick-start is initial-creation-only. When the team
|
|
2883
|
+
// already has runtime state, do NOT mention `--fresh`
|
|
2884
|
+
// (it's been removed); steer the operator to the
|
|
2885
|
+
// restart flow which owns resume + reset semantics.
|
|
2886
|
+
let mut next_actions = vec![
|
|
2887
|
+
"this team already has runtime state — use `team-agent restart` \
|
|
2888
|
+
to resume it (quick-start is for first-time creation only). \
|
|
2889
|
+
If recovery is impossible and the operator EXPLICITLY \
|
|
2890
|
+
accepts losing context, restart accepts `--allow-fresh`."
|
|
2891
|
+
.to_string(),
|
|
2892
|
+
];
|
|
2893
|
+
if session_name.is_some() {
|
|
2894
|
+
if crate::tmux_backend::socket_probe_missing_for_workspace(&workspace) {
|
|
2895
|
+
next_actions.push(crate::tmux_backend::socket_missing_hint_for_workspace(
|
|
2896
|
+
&workspace,
|
|
2897
|
+
));
|
|
2896
2898
|
}
|
|
2897
|
-
|
|
2898
|
-
team: requested_team.clone(),
|
|
2899
|
-
session_name,
|
|
2900
|
-
state_path: Some(state_path),
|
|
2901
|
-
next_actions,
|
|
2902
|
-
attach_commands,
|
|
2903
|
-
});
|
|
2899
|
+
next_actions.extend(attach_commands.iter().cloned());
|
|
2904
2900
|
}
|
|
2901
|
+
return Ok(QuickStartReport::ExistingRuntime {
|
|
2902
|
+
team: requested_team.clone(),
|
|
2903
|
+
session_name,
|
|
2904
|
+
state_path: Some(state_path),
|
|
2905
|
+
next_actions,
|
|
2906
|
+
attach_commands,
|
|
2907
|
+
});
|
|
2905
2908
|
}
|
|
2906
2909
|
}
|
|
2907
2910
|
// CR-040/042: repeated quick-start from one template with distinct --team-id/--name
|
|
@@ -2982,6 +2985,16 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
2982
2985
|
let mut next_actions = vec![format!(
|
|
2983
2986
|
"team compiled; real spawn is behind the transport/provider boundary; {coordinator_action}"
|
|
2984
2987
|
)];
|
|
2988
|
+
// Stage QR (design doc
|
|
2989
|
+
// .team/artifacts/quickstart-restart-separation-design.md):
|
|
2990
|
+
// quick-start is initial-creation-only. After success, remind the
|
|
2991
|
+
// operator that subsequent starts must go through restart so the
|
|
2992
|
+
// user's context (sessions, ownership, tasks) survives.
|
|
2993
|
+
next_actions.push(
|
|
2994
|
+
"quick-start initialized this team; for all subsequent starts use \
|
|
2995
|
+
`team-agent restart` to resume context (quick-start refuses on \
|
|
2996
|
+
already-initialised teams)".to_string(),
|
|
2997
|
+
);
|
|
2985
2998
|
if crate::tmux_backend::socket_probe_missing_for_workspace(&workspace) {
|
|
2986
2999
|
next_actions.push(crate::tmux_backend::socket_missing_hint_for_workspace(&workspace));
|
|
2987
3000
|
}
|
|
@@ -4681,11 +4694,14 @@ fn seed_launched_owner_from_caller_with_provider_lookup(
|
|
|
4681
4694
|
) {
|
|
4682
4695
|
receiver.insert("tmux_socket".to_string(), serde_json::json!(socket));
|
|
4683
4696
|
}
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4697
|
+
// Stage 3a (identity-boundary unified plan, architect direction 2026-06-23):
|
|
4698
|
+
// route quick-start attached-from-env seed through ownership repository.
|
|
4699
|
+
let team_key = crate::state::projection::team_state_key(state);
|
|
4700
|
+
let record = crate::state::ownership::OwnershipWrite::new()
|
|
4701
|
+
.with_leader_receiver(receiver)
|
|
4702
|
+
.with_team_owner(owner)
|
|
4703
|
+
.with_owner_epoch(owner_epoch);
|
|
4704
|
+
crate::state::ownership::write_owner(state, &team_key, record);
|
|
4689
4705
|
true
|
|
4690
4706
|
}
|
|
4691
4707
|
|
|
@@ -606,18 +606,11 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
606
606
|
restart_readiness_deadline(readiness_deadline_ms),
|
|
607
607
|
restart_readiness_poll_interval(),
|
|
608
608
|
)?;
|
|
609
|
-
let
|
|
610
|
-
&
|
|
611
|
-
|
|
612
|
-
.
|
|
613
|
-
|
|
614
|
-
);
|
|
615
|
-
let attach_commands = crate::tmux_backend::attach_commands_for_windows(
|
|
616
|
-
&selected.run_workspace,
|
|
617
|
-
&session_name,
|
|
618
|
-
attach_windows.iter().map(String::as_str),
|
|
619
|
-
);
|
|
620
|
-
let mut next_actions = attach_commands.clone();
|
|
609
|
+
let attach_commands =
|
|
610
|
+
crate::tmux_backend::attach_command_for_session(&selected.run_workspace, &session_name)
|
|
611
|
+
.into_iter()
|
|
612
|
+
.collect::<Vec<_>>();
|
|
613
|
+
let mut next_actions = Vec::new();
|
|
621
614
|
if !failed_agents.is_empty() {
|
|
622
615
|
next_actions.extend(restart_failure_next_actions(&failed_agents));
|
|
623
616
|
write_restart_completed_event(
|
|
@@ -890,7 +890,7 @@ fn save_snapshot_writes_atomic_state_json_under_teams_dir() {
|
|
|
890
890
|
fn quick_start_empty_dir_returns_typed_report_not_error_path_only() {
|
|
891
891
|
let ws = temp_ws();
|
|
892
892
|
// 空目录无可编译 team:应是 typed 阻塞 / 编译错,绝不 Ready。
|
|
893
|
-
match quick_start(&ws, None, false,
|
|
893
|
+
match quick_start(&ws, None, false, None) {
|
|
894
894
|
Ok(QuickStartReport::Ready { .. }) => {
|
|
895
895
|
panic!("空 agents_dir 不应 Ready —— 无 team 可编译")
|
|
896
896
|
}
|
|
@@ -115,7 +115,7 @@ fn unowned_running_ws() -> PathBuf {
|
|
|
115
115
|
fn quick_start_compiles_real_spec_to_team_spec_yaml() {
|
|
116
116
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
117
117
|
let transport = OfflineTransport::new();
|
|
118
|
-
let result = quick_start_with_transport(&team, None, true,
|
|
118
|
+
let result = quick_start_with_transport(&team, None, true, None, &transport);
|
|
119
119
|
|
|
120
120
|
// OBSERVABLE 1 (real compiler ran; spawn-independent): the compiled spec is written.
|
|
121
121
|
// E5: spec lands in .team/runtime/<team_key>/ (NOT the user team dir).
|
|
@@ -153,7 +153,7 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
|
|
|
153
153
|
std::fs::write(team.join("agents").join("implementer.md"), QS_VALID_ROLE).unwrap();
|
|
154
154
|
|
|
155
155
|
let transport = OfflineTransport::new();
|
|
156
|
-
let result = quick_start_with_transport(&team, None, true,
|
|
156
|
+
let result = quick_start_with_transport(&team, None, true, None, &transport);
|
|
157
157
|
assert!(
|
|
158
158
|
matches!(result, Ok(QuickStartReport::Ready { .. })),
|
|
159
159
|
"quick-start failed: {result:?}"
|
|
@@ -208,6 +208,7 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
|
|
|
208
208
|
detail: false,
|
|
209
209
|
summary: false,
|
|
210
210
|
json: true,
|
|
211
|
+
team: None,
|
|
211
212
|
});
|
|
212
213
|
assert!(
|
|
213
214
|
status.is_ok(),
|
|
@@ -218,6 +219,7 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
|
|
|
218
219
|
result_file: None,
|
|
219
220
|
workspace: team.clone(),
|
|
220
221
|
json: true,
|
|
222
|
+
team: None,
|
|
221
223
|
});
|
|
222
224
|
assert!(
|
|
223
225
|
collect.is_ok(),
|
|
@@ -239,7 +241,6 @@ fn quick_start_default_workspace_compiled_spec_uses_project_root() {
|
|
|
239
241
|
&workspace,
|
|
240
242
|
None,
|
|
241
243
|
true,
|
|
242
|
-
true,
|
|
243
244
|
None,
|
|
244
245
|
&transport,
|
|
245
246
|
)
|
|
@@ -286,7 +287,7 @@ fn quick_start_default_workspace_compiled_spec_uses_project_root() {
|
|
|
286
287
|
fn quick_start_invalid_role_doc_surfaces_real_compile_error() {
|
|
287
288
|
let team = quick_start_team_dir(QS_ROLE_NO_PROVIDER);
|
|
288
289
|
let transport = OfflineTransport::new();
|
|
289
|
-
let result = quick_start_with_transport(&team, None, true,
|
|
290
|
+
let result = quick_start_with_transport(&team, None, true, None, &transport);
|
|
290
291
|
|
|
291
292
|
let text = format!("{result:?}");
|
|
292
293
|
assert!(
|
|
@@ -413,7 +414,7 @@ fn fork_agent_proceeds_past_owner_gate_for_unowned_workspace() {
|
|
|
413
414
|
happens inside launch's real spawn). Acceptance-crate boundary, not a unit RED."]
|
|
414
415
|
fn quick_start_full_ready_real_spawn() {
|
|
415
416
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
416
|
-
let report = quick_start(&team, None, true,
|
|
417
|
+
let report = quick_start(&team, None, true, None).expect("quick_start launches the team");
|
|
417
418
|
assert!(
|
|
418
419
|
matches!(report, QuickStartReport::Ready { .. }),
|
|
419
420
|
"got {report:?}"
|
|
@@ -532,7 +533,7 @@ fn unowned_running_ws_all_paused() -> PathBuf {
|
|
|
532
533
|
fn spine_quick_start_seeds_state_under_team_workspace_not_team_dir() {
|
|
533
534
|
let team = quick_start_team_dir(QS_VALID_ROLE); // <base>/teamdir
|
|
534
535
|
let transport = OfflineTransport::new();
|
|
535
|
-
let _ = quick_start_with_transport(&team, None, true,
|
|
536
|
+
let _ = quick_start_with_transport(&team, None, true, None, &transport);
|
|
536
537
|
let workspace = team.parent().unwrap(); // team_workspace(<base>/teamdir) = <base>
|
|
537
538
|
let ws_state = crate::model::paths::runtime_dir(workspace).join("state.json");
|
|
538
539
|
assert!(
|
|
@@ -1060,7 +1061,7 @@ fn quick_start_with_transport_spawns_workers_not_dry_run() {
|
|
|
1060
1061
|
seed_healthy_coordinator(workspace);
|
|
1061
1062
|
let transport = OfflineTransport::new();
|
|
1062
1063
|
|
|
1063
|
-
let report = quick_start_with_transport(&team, None, true,
|
|
1064
|
+
let report = quick_start_with_transport(&team, None, true, None, &transport)
|
|
1064
1065
|
.expect("quick_start_with_transport must reach a report");
|
|
1065
1066
|
|
|
1066
1067
|
let launch = match report {
|
|
@@ -1153,7 +1154,7 @@ fn quick_start_default_adaptive_groups_workers_into_layout_panes() {
|
|
|
1153
1154
|
seed_healthy_coordinator(workspace);
|
|
1154
1155
|
let transport = OfflineTransport::new();
|
|
1155
1156
|
|
|
1156
|
-
let report = quick_start_with_transport(&team, None, true,
|
|
1157
|
+
let report = quick_start_with_transport(&team, None, true, None, &transport)
|
|
1157
1158
|
.expect("quick_start_with_transport must reach Ready");
|
|
1158
1159
|
|
|
1159
1160
|
let (launch, attach_commands, display_backend) = match report {
|
|
@@ -1222,7 +1223,7 @@ fn quick_start_persists_selected_tmux_endpoint_and_attach_commands() {
|
|
|
1222
1223
|
let endpoint = "/tmp/team-agent-layout-selected-socket";
|
|
1223
1224
|
let transport = OfflineTransport::new().with_tmux_endpoint(endpoint);
|
|
1224
1225
|
|
|
1225
|
-
let report = quick_start_with_transport(&team, None, true,
|
|
1226
|
+
let report = quick_start_with_transport(&team, None, true, None, &transport)
|
|
1226
1227
|
.expect("quick_start_with_transport must reach Ready");
|
|
1227
1228
|
let attach_commands = match report {
|
|
1228
1229
|
QuickStartReport::Ready { attach_commands, .. } => attach_commands,
|
|
@@ -1297,7 +1298,7 @@ fn quick_start_preserves_managed_leader_topology_and_emits_leader_attach_command
|
|
|
1297
1298
|
.expect("seed managed leader state");
|
|
1298
1299
|
let transport = OfflineTransport::new();
|
|
1299
1300
|
|
|
1300
|
-
let report = quick_start_with_transport(&team, None, true,
|
|
1301
|
+
let report = quick_start_with_transport(&team, None, true, None, &transport)
|
|
1301
1302
|
.expect("quick_start_with_transport must reach Ready");
|
|
1302
1303
|
let attach_commands = match report {
|
|
1303
1304
|
QuickStartReport::Ready { attach_commands, .. } => attach_commands,
|
|
@@ -1375,7 +1376,7 @@ fn quick_start_no_display_keeps_one_window_per_agent() {
|
|
|
1375
1376
|
let transport = OfflineTransport::new();
|
|
1376
1377
|
|
|
1377
1378
|
let report = quick_start_with_transport_in_workspace_with_display(
|
|
1378
|
-
workspace, &team, None, true,
|
|
1379
|
+
workspace, &team, None, true, None, &transport, false,
|
|
1379
1380
|
)
|
|
1380
1381
|
.expect("quick_start_with_transport must reach Ready");
|
|
1381
1382
|
|
|
@@ -1407,7 +1408,7 @@ fn quick_start_no_display_keeps_one_window_per_agent() {
|
|
|
1407
1408
|
#[ignore = "real-machine: live tmux session + resident coordinator daemon (framework verifies via ps)"]
|
|
1408
1409
|
fn quick_start_fresh_ws_spawns_resident_tmux_and_coordinator() {
|
|
1409
1410
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
1410
|
-
let report = quick_start(&team, None, true,
|
|
1411
|
+
let report = quick_start(&team, None, true, None).expect("quick_start");
|
|
1411
1412
|
match report {
|
|
1412
1413
|
QuickStartReport::Ready { launch, .. } => {
|
|
1413
1414
|
assert!(
|
|
@@ -2462,7 +2463,7 @@ fn start_agent_adaptive_restarts_missing_pane_in_existing_layout_window() {
|
|
|
2462
2463
|
fn quick_start_seeds_tasks_key_from_compiled_spec() {
|
|
2463
2464
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
2464
2465
|
let transport = OfflineTransport::new();
|
|
2465
|
-
let _ = quick_start_with_transport(&team, None, true,
|
|
2466
|
+
let _ = quick_start_with_transport(&team, None, true, None, &transport);
|
|
2466
2467
|
let workspace = team
|
|
2467
2468
|
.parent()
|
|
2468
2469
|
.expect("team_workspace(<base>/teamdir) = <base>");
|
|
@@ -2519,7 +2520,7 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
|
|
|
2519
2520
|
let _tmux = EnvVarGuard::unset("TMUX");
|
|
2520
2521
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
2521
2522
|
let transport = OfflineTransport::new();
|
|
2522
|
-
let _ = quick_start_with_transport(&team, None, true,
|
|
2523
|
+
let _ = quick_start_with_transport(&team, None, true, None, &transport);
|
|
2523
2524
|
let workspace = team
|
|
2524
2525
|
.parent()
|
|
2525
2526
|
.expect("team_workspace(<base>/teamdir) = <base>");
|
|
@@ -2608,7 +2609,7 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
2608
2609
|
let _clock_guard = EnvVarGuard::set("TEAM_AGENT_TEST_FIXED_SPAWNED_AT", FIXED_SPAWNED_AT);
|
|
2609
2610
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
2610
2611
|
let transport = OfflineTransport::new();
|
|
2611
|
-
let _ = quick_start_with_transport(&team, None, true,
|
|
2612
|
+
let _ = quick_start_with_transport(&team, None, true, None, &transport);
|
|
2612
2613
|
let workspace = team
|
|
2613
2614
|
.parent()
|
|
2614
2615
|
.expect("team_workspace(<base>/teamdir) = <base>");
|
|
@@ -498,7 +498,7 @@ pub enum QuickStartReport {
|
|
|
498
498
|
/// failed to spawn) so the CLI surface cannot lie about availability.
|
|
499
499
|
worker_readiness: QuickStartReadiness,
|
|
500
500
|
},
|
|
501
|
-
/// 已有 runtime state
|
|
501
|
+
/// 已有 runtime state → quick-start 拒绝并引导用 restart。
|
|
502
502
|
ExistingRuntime {
|
|
503
503
|
team: Option<String>,
|
|
504
504
|
session_name: Option<SessionName>,
|
|
@@ -230,16 +230,23 @@ pub fn claim_leader_receiver(
|
|
|
230
230
|
"runtime state root is not an object".to_string(),
|
|
231
231
|
));
|
|
232
232
|
};
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
233
|
+
// Build the updated owner + receiver values in-place (field-level
|
|
234
|
+
// mutation preserves any extra fields the caller carries), then publish
|
|
235
|
+
// through the ownership repository. This keeps `claim_leader_receiver`'s
|
|
236
|
+
// legacy field-merge semantics while sourcing all owner mutations from
|
|
237
|
+
// one API surface (Stage 3a, architect direction 2026-06-23).
|
|
238
|
+
let mut owner_value = root
|
|
239
|
+
.get("team_owner")
|
|
240
|
+
.cloned()
|
|
241
|
+
.unwrap_or_else(|| serde_json::json!({}));
|
|
242
|
+
if let Some(owner) = owner_value.as_object_mut() {
|
|
237
243
|
owner.insert("owner_epoch".to_string(), serde_json::json!(next_epoch));
|
|
238
244
|
}
|
|
239
|
-
let
|
|
240
|
-
.
|
|
241
|
-
.
|
|
242
|
-
|
|
245
|
+
let mut receiver_value = root
|
|
246
|
+
.get("leader_receiver")
|
|
247
|
+
.cloned()
|
|
248
|
+
.unwrap_or_else(|| serde_json::json!({}));
|
|
249
|
+
if let Some(receiver) = receiver_value.as_object_mut() {
|
|
243
250
|
receiver.insert("mode".to_string(), serde_json::json!("direct_tmux"));
|
|
244
251
|
receiver.insert("owner_epoch".to_string(), serde_json::json!(next_epoch));
|
|
245
252
|
copy_candidate_field(receiver, candidate, "pane_id");
|
|
@@ -255,6 +262,13 @@ pub fn claim_leader_receiver(
|
|
|
255
262
|
receiver.insert("tmux_socket".to_string(), serde_json::json!(socket));
|
|
256
263
|
}
|
|
257
264
|
}
|
|
265
|
+
// The `root` borrow above is released by NLL before this call.
|
|
266
|
+
let team_key = crate::state::projection::team_state_key(state);
|
|
267
|
+
let record = crate::state::ownership::OwnershipWrite::new()
|
|
268
|
+
.with_team_owner(owner_value)
|
|
269
|
+
.with_leader_receiver(receiver_value)
|
|
270
|
+
.with_owner_epoch(next_epoch);
|
|
271
|
+
crate::state::ownership::write_owner(state, &team_key, record);
|
|
258
272
|
crate::state::persist::save_runtime_state(workspace, state)?;
|
|
259
273
|
event_log.write(
|
|
260
274
|
"leader_receiver.claimed",
|