@team-agent/installer 0.5.105 → 0.5.107
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/README.md +5 -3
- package/README.zh.md +4 -3
- package/crates/team-agent/src/cli/adapters.rs +15 -1
- package/crates/team-agent/src/cli/emit.rs +33 -13
- package/crates/team-agent/src/cli/mod.rs +148 -21
- package/crates/team-agent/src/cli/status_port/brief.rs +62 -3
- package/crates/team-agent/src/cli/tests/base.rs +112 -225
- package/crates/team-agent/src/cli/tests/run_delegation.rs +4 -2
- package/crates/team-agent/src/cli/tests/status_send.rs +2 -2
- package/crates/team-agent/src/compiler/tests.rs +46 -59
- package/crates/team-agent/src/leader/tests/byte_findings.rs +1 -14
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +57 -173
- package/crates/team-agent/src/lifecycle/launch/pi_mcp_extension.js +226 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +0 -37
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +8 -3
- package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +24 -17
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +22 -9
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_native_mcp_host.mjs +71 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +43 -66
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +71 -392
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +0 -5
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -361
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +1 -16
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +20 -112
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1 -12
- package/crates/team-agent/src/lifecycle/tests/pi_native_mcp_capability_red.rs +546 -0
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +6 -2
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +11 -54
- package/crates/team-agent/src/messaging/tests/wave2.rs +2 -302
- package/crates/team-agent/src/model/name_similarity.rs +52 -67
- package/crates/team-agent/src/model/task_graph.rs +37 -124
- package/crates/team-agent/src/model/yaml/tests.rs +46 -165
- package/crates/team-agent/src/provider/tests/adapter.rs +0 -20
- package/crates/team-agent/src/provider/tests/classify.rs +106 -198
- package/crates/team-agent/src/state/projection.rs +15 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +315 -501
- package/crates/team-agent/src/tmux_backend.rs +110 -0
- package/crates/team-agent/src/transport/test_support.rs +35 -0
- package/crates/team-agent/src/transport/tests/wire.rs +32 -58
- package/crates/team-agent/src/transport.rs +26 -12
- package/package.json +4 -4
- package/skills/team-agent/references/recovery-runbook.md +2 -2
|
@@ -4,6 +4,7 @@ mod hermetic_guard;
|
|
|
4
4
|
use super::*;
|
|
5
5
|
use crate::tmux_backend::TmuxBackend;
|
|
6
6
|
use crate::transport::test_support::OfflineTransport;
|
|
7
|
+
use crate::transport::Transport;
|
|
7
8
|
use serde_json::json;
|
|
8
9
|
use serial_test::serial;
|
|
9
10
|
|
|
@@ -1231,6 +1232,64 @@ fn launch_with_transport_records_one_spawn_per_agent_carrying_build_command() {
|
|
|
1231
1232
|
"LaunchReport.started must list the compiled agent; got {:?}",
|
|
1232
1233
|
report.started
|
|
1233
1234
|
);
|
|
1235
|
+
let workspace = crate::model::paths::team_workspace(&team).unwrap();
|
|
1236
|
+
let state = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
1237
|
+
let generation = report.started[0].spawned_at.as_str();
|
|
1238
|
+
assert_eq!(state["generation"], generation);
|
|
1239
|
+
let team_key = crate::state::projection::team_state_key(&state);
|
|
1240
|
+
assert_eq!(state["teams"][&team_key]["generation"], generation);
|
|
1241
|
+
let owner = transport.session_owner(&SessionName::new("team-quickteam")).unwrap().unwrap();
|
|
1242
|
+
assert_eq!(owner.generation, generation);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
#[test]
|
|
1246
|
+
#[serial(env)]
|
|
1247
|
+
fn start_agent_session_generation_inherits_birth_or_stamps_recreated_session() {
|
|
1248
|
+
for canonical_state in [false, true] {
|
|
1249
|
+
for existing_session in [true, false] {
|
|
1250
|
+
for explicit_generation in [true, false] {
|
|
1251
|
+
let ws = restart_ws_one_resumable_worker();
|
|
1252
|
+
let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
1253
|
+
let birth = "2026-07-14T00:00:00+00:00";
|
|
1254
|
+
state["agents"]["alpha"]["status"] = json!("stopped");
|
|
1255
|
+
state["agents"]["alpha"]["spawned_at"] = json!(birth);
|
|
1256
|
+
if explicit_generation {
|
|
1257
|
+
state["generation"] = json!("explicit-session-birth");
|
|
1258
|
+
}
|
|
1259
|
+
let team_key = crate::state::projection::team_state_key(&state);
|
|
1260
|
+
if canonical_state {
|
|
1261
|
+
state["team_key"] = json!(team_key);
|
|
1262
|
+
state["active_team_key"] = json!(team_key);
|
|
1263
|
+
state["teams"] = json!({});
|
|
1264
|
+
state["teams"][&team_key] = crate::state::projection::compact_team_state(&state);
|
|
1265
|
+
}
|
|
1266
|
+
let inherited = if explicit_generation { "explicit-session-birth" } else { birth };
|
|
1267
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
1268
|
+
let transport = OfflineTransport::new()
|
|
1269
|
+
.with_session_present(existing_session)
|
|
1270
|
+
.with_session_owner(&ws, &team_key, inherited);
|
|
1271
|
+
let result = start_agent_with_transport(
|
|
1272
|
+
&ws, &aid("alpha"), false, false, false, None, &transport,
|
|
1273
|
+
).unwrap();
|
|
1274
|
+
assert!(matches!(result, StartAgentOutcome::Running { .. }), "{result:?}");
|
|
1275
|
+
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
1276
|
+
let spawned_at = state["agents"]["alpha"]["spawned_at"].as_str().unwrap();
|
|
1277
|
+
assert_ne!(spawned_at, birth);
|
|
1278
|
+
let expected = if existing_session { inherited } else { spawned_at };
|
|
1279
|
+
assert_eq!(state["generation"], expected);
|
|
1280
|
+
if canonical_state {
|
|
1281
|
+
assert_eq!(state["teams"][&team_key]["generation"], expected);
|
|
1282
|
+
} else {
|
|
1283
|
+
// Legacy flat state stays flat; generation persistence does
|
|
1284
|
+
// not implicitly migrate it to a canonical teams table.
|
|
1285
|
+
assert!(state.get("teams").is_none());
|
|
1286
|
+
}
|
|
1287
|
+
let owner = transport.session_owner(&SessionName::new("team-restartone")).unwrap().unwrap();
|
|
1288
|
+
assert_eq!(owner.generation, expected);
|
|
1289
|
+
assert_eq!(transport.calls().contains(&"set_session_owner_with_generation"), !existing_session);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1234
1293
|
}
|
|
1235
1294
|
|
|
1236
1295
|
// ═════════════════════════════════════════════════════════════════════════
|
|
@@ -1704,6 +1763,9 @@ fn restart_ws_one_resumable_worker() -> PathBuf {
|
|
|
1704
1763
|
#[test]
|
|
1705
1764
|
fn restart_with_transport_spawns_resumable_workers_not_stub() {
|
|
1706
1765
|
let ws = restart_ws_two_resumable_workers();
|
|
1766
|
+
let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
1767
|
+
state["generation"] = json!("old-destroyed-session-birth");
|
|
1768
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
1707
1769
|
let transport = OfflineTransport::new();
|
|
1708
1770
|
|
|
1709
1771
|
let result = restart_with_transport(&ws, false, None, &transport);
|
|
@@ -1731,6 +1793,13 @@ fn restart_with_transport_spawns_resumable_workers_not_stub() {
|
|
|
1731
1793
|
matches!(result, Ok(RestartReport::Restarted { coordinator_started: true, .. })),
|
|
1732
1794
|
"restart must reach RestartReport::Restarted with coordinator_started=true (AlreadyRunning, seeded); got {result:?}"
|
|
1733
1795
|
);
|
|
1796
|
+
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
1797
|
+
let generation = state["agents"]["alpha"]["spawned_at"].as_str().unwrap();
|
|
1798
|
+
assert_ne!(generation, "old-destroyed-session-birth");
|
|
1799
|
+
assert_eq!(state["generation"], generation);
|
|
1800
|
+
assert!(state.get("teams").is_none(), "legacy flat state must remain flat");
|
|
1801
|
+
let owner = transport.session_owner(&SessionName::new("team-restartteam")).unwrap().unwrap();
|
|
1802
|
+
assert_eq!(owner.generation, generation);
|
|
1734
1803
|
let events = crate::event_log::EventLog::new(&ws).tail(20).unwrap();
|
|
1735
1804
|
let completed = events
|
|
1736
1805
|
.iter()
|
|
@@ -2731,130 +2800,6 @@ fn fork_refuses_source_with_partial_tuple() {
|
|
|
2731
2800
|
);
|
|
2732
2801
|
}
|
|
2733
2802
|
|
|
2734
|
-
#[test]
|
|
2735
|
-
#[ignore = "legacy adaptive display layout contract removed; runtime is silent tmux only"]
|
|
2736
|
-
fn add_agent_adaptive_splits_last_non_full_layout_window() {
|
|
2737
|
-
let roles = [("w1.md", role_doc("w1")), ("w2.md", role_doc("w2"))];
|
|
2738
|
-
let role_refs = roles
|
|
2739
|
-
.iter()
|
|
2740
|
-
.map(|(file, doc)| (*file, doc.as_str()))
|
|
2741
|
-
.collect::<Vec<_>>();
|
|
2742
|
-
let team = quick_start_team_dir_with_roles(&role_refs);
|
|
2743
|
-
let session = "team-layout-add";
|
|
2744
|
-
crate::state::persist::save_runtime_state(
|
|
2745
|
-
&team,
|
|
2746
|
-
&json!({
|
|
2747
|
-
"session_name": session,
|
|
2748
|
-
"legacy_display_key": "adaptive",
|
|
2749
|
-
"active_team_key": "teamdir",
|
|
2750
|
-
"agents": {
|
|
2751
|
-
"w1": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 0, "pane_id": "%1"},
|
|
2752
|
-
"w2": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 1, "pane_id": "%2"}
|
|
2753
|
-
}
|
|
2754
|
-
}),
|
|
2755
|
-
)
|
|
2756
|
-
.unwrap();
|
|
2757
|
-
seed_healthy_coordinator(&team);
|
|
2758
|
-
let role_file = team.join("w3-role.md");
|
|
2759
|
-
std::fs::write(&role_file, role_doc("w3")).unwrap();
|
|
2760
|
-
let transport = OfflineTransport::new()
|
|
2761
|
-
.with_session_present(true)
|
|
2762
|
-
.with_targets(vec![
|
|
2763
|
-
layout_pane(session, "team-w1", "%1", 0),
|
|
2764
|
-
layout_pane(session, "team-w1", "%2", 1),
|
|
2765
|
-
])
|
|
2766
|
-
.with_pane_presence("%1", true)
|
|
2767
|
-
.with_pane_presence("%2", true);
|
|
2768
|
-
|
|
2769
|
-
add_agent_with_transport(
|
|
2770
|
-
&team,
|
|
2771
|
-
&AgentId::new("w3"),
|
|
2772
|
-
&role_file,
|
|
2773
|
-
true,
|
|
2774
|
-
None,
|
|
2775
|
-
&transport,
|
|
2776
|
-
)
|
|
2777
|
-
.expect("adaptive add-agent should start");
|
|
2778
|
-
|
|
2779
|
-
assert_eq!(
|
|
2780
|
-
transport.spawn_window_records().last(),
|
|
2781
|
-
Some(&(String::from("spawn_split"), String::from("team-w1"))),
|
|
2782
|
-
"last non-full layout window should be split"
|
|
2783
|
-
);
|
|
2784
|
-
let (_raw, state) = raw_runtime_state(&team);
|
|
2785
|
-
assert_eq!(
|
|
2786
|
-
state.pointer("/agents/w3/layout_window"),
|
|
2787
|
-
Some(&json!("team-w1"))
|
|
2788
|
-
);
|
|
2789
|
-
assert_eq!(state.pointer("/agents/w3/pane_index"), Some(&json!(2)));
|
|
2790
|
-
}
|
|
2791
|
-
|
|
2792
|
-
#[test]
|
|
2793
|
-
#[ignore = "legacy adaptive display layout contract removed; runtime is silent tmux only"]
|
|
2794
|
-
fn add_agent_adaptive_creates_suffix_window_when_last_layout_full_and_name_collides() {
|
|
2795
|
-
let roles = [
|
|
2796
|
-
("w1.md", role_doc("w1")),
|
|
2797
|
-
("w2.md", role_doc("w2")),
|
|
2798
|
-
("w3.md", role_doc("w3")),
|
|
2799
|
-
];
|
|
2800
|
-
let role_refs = roles
|
|
2801
|
-
.iter()
|
|
2802
|
-
.map(|(file, doc)| (*file, doc.as_str()))
|
|
2803
|
-
.collect::<Vec<_>>();
|
|
2804
|
-
let team = quick_start_team_dir_with_roles(&role_refs);
|
|
2805
|
-
let session = "team-layout-add-full";
|
|
2806
|
-
crate::state::persist::save_runtime_state(
|
|
2807
|
-
&team,
|
|
2808
|
-
&json!({
|
|
2809
|
-
"session_name": session,
|
|
2810
|
-
"legacy_display_key": "adaptive",
|
|
2811
|
-
"active_team_key": "teamdir",
|
|
2812
|
-
"agents": {
|
|
2813
|
-
"w1": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 0, "pane_id": "%1"},
|
|
2814
|
-
"w2": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 1, "pane_id": "%2"},
|
|
2815
|
-
"w3": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 2, "pane_id": "%3"}
|
|
2816
|
-
}
|
|
2817
|
-
}),
|
|
2818
|
-
)
|
|
2819
|
-
.unwrap();
|
|
2820
|
-
seed_healthy_coordinator(&team);
|
|
2821
|
-
let role_file = team.join("w4-role.md");
|
|
2822
|
-
std::fs::write(&role_file, role_doc("w4")).unwrap();
|
|
2823
|
-
let transport = OfflineTransport::new()
|
|
2824
|
-
.with_session_present(true)
|
|
2825
|
-
.with_targets(vec![
|
|
2826
|
-
layout_pane(session, "team-w1", "%1", 0),
|
|
2827
|
-
layout_pane(session, "team-w1", "%2", 1),
|
|
2828
|
-
layout_pane(session, "team-w1", "%3", 2),
|
|
2829
|
-
layout_pane(session, "team-w2", "%9", 0),
|
|
2830
|
-
])
|
|
2831
|
-
.with_pane_presence("%1", true)
|
|
2832
|
-
.with_pane_presence("%2", true)
|
|
2833
|
-
.with_pane_presence("%3", true);
|
|
2834
|
-
|
|
2835
|
-
add_agent_with_transport(
|
|
2836
|
-
&team,
|
|
2837
|
-
&AgentId::new("w4"),
|
|
2838
|
-
&role_file,
|
|
2839
|
-
true,
|
|
2840
|
-
None,
|
|
2841
|
-
&transport,
|
|
2842
|
-
)
|
|
2843
|
-
.expect("adaptive add-agent should start");
|
|
2844
|
-
|
|
2845
|
-
assert_eq!(
|
|
2846
|
-
transport.spawn_window_records().last(),
|
|
2847
|
-
Some(&(String::from("spawn_into"), String::from("team-w2-2"))),
|
|
2848
|
-
"full last layout window should create next window with collision suffix"
|
|
2849
|
-
);
|
|
2850
|
-
let (_raw, state) = raw_runtime_state(&team);
|
|
2851
|
-
assert_eq!(
|
|
2852
|
-
state.pointer("/agents/w4/layout_window"),
|
|
2853
|
-
Some(&json!("team-w2-2"))
|
|
2854
|
-
);
|
|
2855
|
-
assert_eq!(state.pointer("/agents/w4/pane_index"), Some(&json!(0)));
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
2803
|
#[test]
|
|
2859
2804
|
fn stop_agent_adaptive_kills_target_pane_not_shared_layout_window() {
|
|
2860
2805
|
let roles = [("w1.md", role_doc("w1")), ("w2.md", role_doc("w2"))];
|
|
@@ -2909,55 +2854,6 @@ fn stop_agent_adaptive_kills_target_pane_not_shared_layout_window() {
|
|
|
2909
2854
|
);
|
|
2910
2855
|
}
|
|
2911
2856
|
|
|
2912
|
-
#[test]
|
|
2913
|
-
#[ignore = "legacy adaptive display layout contract removed; runtime is silent tmux only"]
|
|
2914
|
-
fn start_agent_adaptive_restarts_missing_pane_in_existing_layout_window() {
|
|
2915
|
-
let ws = temp_ws();
|
|
2916
|
-
crate::state::persist::save_runtime_state(
|
|
2917
|
-
&ws,
|
|
2918
|
-
&json!({
|
|
2919
|
-
"session_name": "team-layout-restart",
|
|
2920
|
-
"legacy_display_key": "adaptive",
|
|
2921
|
-
"agents": {
|
|
2922
|
-
"w1": {"status": "running", "provider": "codex", "role": "w1", "model": "gpt-5.5", "auth_mode": "subscription", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 0, "pane_id": "%1"},
|
|
2923
|
-
"w2": {"status": "running", "provider": "codex", "role": "w2", "model": "gpt-5.5", "auth_mode": "subscription", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 1, "pane_id": "%2"}
|
|
2924
|
-
}
|
|
2925
|
-
}),
|
|
2926
|
-
)
|
|
2927
|
-
.unwrap();
|
|
2928
|
-
seed_healthy_coordinator(&ws);
|
|
2929
|
-
let transport = OfflineTransport::new()
|
|
2930
|
-
.with_session_present(true)
|
|
2931
|
-
.with_targets(vec![layout_pane("team-layout-restart", "team-w1", "%2", 1)])
|
|
2932
|
-
.with_pane_presence("%1", false)
|
|
2933
|
-
.with_pane_presence("%2", true);
|
|
2934
|
-
|
|
2935
|
-
let outcome = start_agent_with_transport(
|
|
2936
|
-
&ws,
|
|
2937
|
-
&AgentId::new("w1"),
|
|
2938
|
-
false,
|
|
2939
|
-
true,
|
|
2940
|
-
true,
|
|
2941
|
-
None,
|
|
2942
|
-
&transport,
|
|
2943
|
-
)
|
|
2944
|
-
.expect("adaptive restart should spawn");
|
|
2945
|
-
|
|
2946
|
-
assert!(matches!(outcome, StartAgentOutcome::Running { .. }));
|
|
2947
|
-
assert_eq!(
|
|
2948
|
-
transport.spawn_window_records().last(),
|
|
2949
|
-
Some(&(String::from("spawn_split"), String::from("team-w1"))),
|
|
2950
|
-
"missing worker pane should be rebuilt in its existing layout window"
|
|
2951
|
-
);
|
|
2952
|
-
let (_raw, state) = raw_runtime_state(&ws);
|
|
2953
|
-
assert_eq!(
|
|
2954
|
-
state.pointer("/agents/w1/layout_window"),
|
|
2955
|
-
Some(&json!("team-w1"))
|
|
2956
|
-
);
|
|
2957
|
-
assert_eq!(state.pointer("/agents/w1/window"), Some(&json!("team-w1")));
|
|
2958
|
-
assert_eq!(state.pointer("/agents/w2/pane_id"), Some(&json!("%2")));
|
|
2959
|
-
}
|
|
2960
|
-
|
|
2961
2857
|
// ═════════════════════════════════════════════════════════════════════════
|
|
2962
2858
|
// WAVE 2 · LANE A — agent-lifecycle byte-parity contracts. stop_agent / reset_agent / remove_agent /
|
|
2963
2859
|
// fork_agent are stubs (OwnerRefused / RequirementUnmet / "session_id missing"). These RED contracts
|
|
@@ -3071,6 +2967,8 @@ fn quick_start_state_seeds_spec_path_workspace_leader() {
|
|
|
3071
2967
|
// `ResolvedTransport.source` through the launch call site.
|
|
3072
2968
|
"transport",
|
|
3073
2969
|
"active_team_key",
|
|
2970
|
+
// Persist physical session birth independently of worker respawns.
|
|
2971
|
+
"generation",
|
|
3074
2972
|
"teams",
|
|
3075
2973
|
],
|
|
3076
2974
|
"state.json top-level key order must match golden launch/core.py:62-71 \
|
|
@@ -3643,175 +3541,6 @@ fn e43_adaptive_placement_skips_existing_agent_whose_live_pane_window_name_diffe
|
|
|
3643
3541
|
}
|
|
3644
3542
|
}
|
|
3645
3543
|
|
|
3646
|
-
/// **Regression guard — Fix A**: when an existing agent's live pane DOES
|
|
3647
|
-
/// reside under the claimed layout_window, placement keeps the prior
|
|
3648
|
-
/// behaviour (groups the new agent into that window). This is the canonical
|
|
3649
|
-
/// adaptive 1-window-multiple-panes use case.
|
|
3650
|
-
#[test]
|
|
3651
|
-
#[ignore = "legacy adaptive display layout contract removed; runtime is silent tmux only"]
|
|
3652
|
-
fn e43_adaptive_placement_groups_into_layout_window_when_live_pane_matches_claim() {
|
|
3653
|
-
use crate::lifecycle::launch::adaptive_placement_for_agent;
|
|
3654
|
-
use crate::model::ids::AgentId;
|
|
3655
|
-
use crate::transport::{SessionName, WindowName};
|
|
3656
|
-
|
|
3657
|
-
let state = json!({
|
|
3658
|
-
"legacy_display_key": "adaptive",
|
|
3659
|
-
"session_name": "team-iso",
|
|
3660
|
-
"agents": {
|
|
3661
|
-
"alpha": {
|
|
3662
|
-
"status": "running",
|
|
3663
|
-
"provider": "codex",
|
|
3664
|
-
"layout_window": "team-w1",
|
|
3665
|
-
"pane_id": "%1",
|
|
3666
|
-
}
|
|
3667
|
-
}
|
|
3668
|
-
});
|
|
3669
|
-
|
|
3670
|
-
let transport = OfflineTransport::new()
|
|
3671
|
-
.with_targets(vec![layout_pane("team-iso", "team-w1", "%1", 0)])
|
|
3672
|
-
.with_windows(vec![WindowName::new("team-w1")]);
|
|
3673
|
-
|
|
3674
|
-
let placement = adaptive_placement_for_agent(
|
|
3675
|
-
&state,
|
|
3676
|
-
&transport,
|
|
3677
|
-
&SessionName::new("team-iso"),
|
|
3678
|
-
&AgentId::new("beta"),
|
|
3679
|
-
)
|
|
3680
|
-
.expect("placement should succeed");
|
|
3681
|
-
|
|
3682
|
-
assert_eq!(
|
|
3683
|
-
placement.layout_window.as_str(),
|
|
3684
|
-
"team-w1",
|
|
3685
|
-
"E43 Fix A regression guard: when alpha's live pane DOES live under \
|
|
3686
|
-
layout_window 'team-w1', the new agent beta should be grouped into \
|
|
3687
|
-
the same window (canonical adaptive behaviour). Got {placement:?}"
|
|
3688
|
-
);
|
|
3689
|
-
assert!(
|
|
3690
|
-
!placement.starts_window,
|
|
3691
|
-
"E43 Fix A regression guard: grouping into an existing window means \
|
|
3692
|
-
starts_window=false (split into the window). Got {placement:?}"
|
|
3693
|
-
);
|
|
3694
|
-
}
|
|
3695
|
-
|
|
3696
|
-
/// **RED — Fix B**: `adaptive_existing_placement_for_agent` must NOT return a
|
|
3697
|
-
/// `starts_window=true` placement that names a window NOT present in
|
|
3698
|
-
/// `live_windows`. When the agent's claimed `layout_window` is stale and the
|
|
3699
|
-
/// session is effectively per-agent (live windows are named after agent_ids),
|
|
3700
|
-
/// the placement must fall back to a per-agent window named after the
|
|
3701
|
-
/// agent_id itself — so the subsequent spawn opens a new window, not a split
|
|
3702
|
-
/// of a phantom.
|
|
3703
|
-
#[test]
|
|
3704
|
-
#[ignore = "legacy adaptive display layout contract removed; runtime is silent tmux only"]
|
|
3705
|
-
fn e43_adaptive_existing_placement_falls_back_to_agent_id_window_when_claim_missing_from_live() {
|
|
3706
|
-
use crate::lifecycle::launch::adaptive_existing_placement_for_agent;
|
|
3707
|
-
use crate::model::ids::AgentId;
|
|
3708
|
-
use crate::transport::{SessionName, WindowName};
|
|
3709
|
-
|
|
3710
|
-
let state = json!({
|
|
3711
|
-
"legacy_display_key": "adaptive",
|
|
3712
|
-
"session_name": "team-iso",
|
|
3713
|
-
"agents": {
|
|
3714
|
-
"demo-director": {
|
|
3715
|
-
"status": "running",
|
|
3716
|
-
"provider": "codex",
|
|
3717
|
-
// Stale claim — live session has no team-w2 window.
|
|
3718
|
-
"layout_window": "team-w2",
|
|
3719
|
-
"pane_id": "%99",
|
|
3720
|
-
}
|
|
3721
|
-
}
|
|
3722
|
-
});
|
|
3723
|
-
|
|
3724
|
-
let transport = OfflineTransport::new()
|
|
3725
|
-
.with_targets(vec![
|
|
3726
|
-
layout_pane("team-iso", "architect", "%1", 0),
|
|
3727
|
-
layout_pane("team-iso", "test-engineer", "%2", 0),
|
|
3728
|
-
])
|
|
3729
|
-
.with_windows(vec![
|
|
3730
|
-
WindowName::new("architect"),
|
|
3731
|
-
WindowName::new("test-engineer"),
|
|
3732
|
-
]);
|
|
3733
|
-
|
|
3734
|
-
let placement = adaptive_existing_placement_for_agent(
|
|
3735
|
-
&state,
|
|
3736
|
-
&transport,
|
|
3737
|
-
&SessionName::new("team-iso"),
|
|
3738
|
-
&AgentId::new("demo-director"),
|
|
3739
|
-
)
|
|
3740
|
-
.expect("placement should resolve");
|
|
3741
|
-
|
|
3742
|
-
assert_ne!(
|
|
3743
|
-
placement.layout_window.as_str(),
|
|
3744
|
-
"team-w2",
|
|
3745
|
-
"E43 Fix B: stale claim 'team-w2' is not in live_windows; placement \
|
|
3746
|
-
must NOT reuse that name (otherwise spawn issues split -t :team-w2 \
|
|
3747
|
-
and tmux says 'can't find window: team-w2'). Got {placement:?}"
|
|
3748
|
-
);
|
|
3749
|
-
assert_eq!(
|
|
3750
|
-
placement.layout_window.as_str(),
|
|
3751
|
-
"demo-director",
|
|
3752
|
-
"E43 Fix B: when the claim is stale and the live layout is \
|
|
3753
|
-
per-agent-named, fall back to a window named after the agent_id \
|
|
3754
|
-
(canonical per-agent pattern). Got {placement:?}"
|
|
3755
|
-
);
|
|
3756
|
-
assert!(
|
|
3757
|
-
placement.starts_window,
|
|
3758
|
-
"E43 Fix B: the fallback opens a NEW window (starts_window=true); \
|
|
3759
|
-
do not try to split an existing window with the wrong name. Got \
|
|
3760
|
-
{placement:?}"
|
|
3761
|
-
);
|
|
3762
|
-
}
|
|
3763
|
-
|
|
3764
|
-
/// **Regression guard — Fix B**: when the agent's claimed layout_window IS
|
|
3765
|
-
/// in live_windows, the existing-placement path keeps its current behaviour
|
|
3766
|
-
/// (return a starts_window=false placement that splits into the existing
|
|
3767
|
-
/// window).
|
|
3768
|
-
#[test]
|
|
3769
|
-
#[ignore = "legacy adaptive display layout contract removed; runtime is silent tmux only"]
|
|
3770
|
-
fn e43_adaptive_existing_placement_keeps_starts_window_false_when_claim_in_live_windows() {
|
|
3771
|
-
use crate::lifecycle::launch::adaptive_existing_placement_for_agent;
|
|
3772
|
-
use crate::model::ids::AgentId;
|
|
3773
|
-
use crate::transport::{SessionName, WindowName};
|
|
3774
|
-
|
|
3775
|
-
let state = json!({
|
|
3776
|
-
"legacy_display_key": "adaptive",
|
|
3777
|
-
"session_name": "team-iso",
|
|
3778
|
-
"agents": {
|
|
3779
|
-
"alpha": {
|
|
3780
|
-
"status": "running",
|
|
3781
|
-
"provider": "codex",
|
|
3782
|
-
"layout_window": "team-w1",
|
|
3783
|
-
"pane_id": "%1",
|
|
3784
|
-
"pane_index": 1u64,
|
|
3785
|
-
}
|
|
3786
|
-
}
|
|
3787
|
-
});
|
|
3788
|
-
|
|
3789
|
-
let transport = OfflineTransport::new()
|
|
3790
|
-
.with_targets(vec![layout_pane("team-iso", "team-w1", "%1", 0)])
|
|
3791
|
-
.with_windows(vec![WindowName::new("team-w1")]);
|
|
3792
|
-
|
|
3793
|
-
let placement = adaptive_existing_placement_for_agent(
|
|
3794
|
-
&state,
|
|
3795
|
-
&transport,
|
|
3796
|
-
&SessionName::new("team-iso"),
|
|
3797
|
-
&AgentId::new("alpha"),
|
|
3798
|
-
)
|
|
3799
|
-
.expect("placement should resolve");
|
|
3800
|
-
|
|
3801
|
-
assert_eq!(
|
|
3802
|
-
placement.layout_window.as_str(),
|
|
3803
|
-
"team-w1",
|
|
3804
|
-
"E43 Fix B regression guard: when claim 'team-w1' IS in live_windows, \
|
|
3805
|
-
existing-placement must keep it. Got {placement:?}"
|
|
3806
|
-
);
|
|
3807
|
-
// pane_index=1 → starts_window=false in the existing behaviour.
|
|
3808
|
-
assert!(
|
|
3809
|
-
!placement.starts_window,
|
|
3810
|
-
"E43 Fix B regression guard: existing window + pane_index>0 → \
|
|
3811
|
-
starts_window=false. Got {placement:?}"
|
|
3812
|
-
);
|
|
3813
|
-
}
|
|
3814
|
-
|
|
3815
3544
|
/// **RED — Fix C end-to-end**: `add_agent` end-to-end must NOT emit a
|
|
3816
3545
|
/// spawn_split with the stale window name. With state carrying
|
|
3817
3546
|
/// `legacy_display_key=adaptive` + stale `layout_window=team-w2` residue and
|
|
@@ -4107,56 +3836,6 @@ fn e45_add_agent_opens_new_window_does_not_split_into_per_agent_window() {
|
|
|
4107
3836
|
);
|
|
4108
3837
|
}
|
|
4109
3838
|
|
|
4110
|
-
/// **Regression guard**: when the live session DOES have a real `team-w1`
|
|
4111
|
-
/// adaptive window with a live pane that matches state's claim, the
|
|
4112
|
-
/// existing E43+adaptive grouping behaviour is preserved (split into team-w1
|
|
4113
|
-
/// as before). E45 only changes behaviour for per-agent windows.
|
|
4114
|
-
#[test]
|
|
4115
|
-
#[ignore = "legacy adaptive display layout contract removed; runtime is silent tmux only"]
|
|
4116
|
-
fn e45_real_team_w_adaptive_layout_still_groups_into_layout_window() {
|
|
4117
|
-
use crate::lifecycle::launch::adaptive_placement_for_agent;
|
|
4118
|
-
use crate::model::ids::AgentId;
|
|
4119
|
-
use crate::transport::{SessionName, WindowName};
|
|
4120
|
-
|
|
4121
|
-
let state = json!({
|
|
4122
|
-
"legacy_display_key": "adaptive",
|
|
4123
|
-
"session_name": "team-iso",
|
|
4124
|
-
"agents": {
|
|
4125
|
-
"alpha": {
|
|
4126
|
-
"status": "running",
|
|
4127
|
-
"provider": "codex",
|
|
4128
|
-
"layout_window": "team-w1",
|
|
4129
|
-
"layout_index": 0u64,
|
|
4130
|
-
"pane_id": "%1",
|
|
4131
|
-
}
|
|
4132
|
-
}
|
|
4133
|
-
});
|
|
4134
|
-
|
|
4135
|
-
let transport = OfflineTransport::new()
|
|
4136
|
-
.with_targets(vec![layout_pane("team-iso", "team-w1", "%1", 0)])
|
|
4137
|
-
.with_windows(vec![WindowName::new("team-w1")]);
|
|
4138
|
-
|
|
4139
|
-
let placement = adaptive_placement_for_agent(
|
|
4140
|
-
&state,
|
|
4141
|
-
&transport,
|
|
4142
|
-
&SessionName::new("team-iso"),
|
|
4143
|
-
&AgentId::new("beta"),
|
|
4144
|
-
)
|
|
4145
|
-
.expect("E45 regression guard: real team-w1 adaptive layout must still group new agents in");
|
|
4146
|
-
|
|
4147
|
-
assert_eq!(
|
|
4148
|
-
placement.layout_window.as_str(),
|
|
4149
|
-
"team-w1",
|
|
4150
|
-
"E45 regression guard: real `team-w1` adaptive window must still \
|
|
4151
|
-
host new agents (canonical adaptive behaviour). Got {placement:?}"
|
|
4152
|
-
);
|
|
4153
|
-
assert!(
|
|
4154
|
-
!placement.starts_window,
|
|
4155
|
-
"E45 regression guard: grouping into existing team-w1 → \
|
|
4156
|
-
starts_window=false. Got {placement:?}"
|
|
4157
|
-
);
|
|
4158
|
-
}
|
|
4159
|
-
|
|
4160
3839
|
/// **Regression guard**: helper `is_adaptive_layout_window` golden cases.
|
|
4161
3840
|
/// `team-w1`, `team-w42`, `team-w7-foo` are adaptive; agent_id names and
|
|
4162
3841
|
/// other shapes are not.
|
|
@@ -130,11 +130,6 @@ fn lifecycle_lock_long_held_event_records_holder_and_waiter() {
|
|
|
130
130
|
);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
#[test]
|
|
134
|
-
fn lifecycle_lock_phase_b_golden_source_guard() {
|
|
135
|
-
r2_lifecycle_lock_exists_precondition();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
133
|
#[test]
|
|
139
134
|
fn r2_lifecycle_lock_exists_precondition() {
|
|
140
135
|
let src = manifest_src();
|