@team-agent/installer 0.5.106 → 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/crates/team-agent/src/cli/tests/base.rs +112 -225
- 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/tests/agent_ops.rs +0 -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 +0 -66
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +0 -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.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/tmux_backend/tests.rs +178 -501
- package/crates/team-agent/src/transport/tests/wire.rs +0 -55
- package/package.json +4 -4
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
|
|
4
|
+
const [wrapperPath, receiptPath, cwd] = process.argv.slice(2);
|
|
5
|
+
if (!wrapperPath || !receiptPath || !cwd) throw new Error("expected wrapper, receipt, and cwd");
|
|
6
|
+
|
|
7
|
+
const registrations = [];
|
|
8
|
+
const unregistrations = [];
|
|
9
|
+
const handlers = new Map();
|
|
10
|
+
const extensionCommands = [{
|
|
11
|
+
name: "mcp",
|
|
12
|
+
source: "extension",
|
|
13
|
+
sourceInfo: { path: "builtin:mcp", source: "builtin", scope: "user", origin: "top-level" },
|
|
14
|
+
}];
|
|
15
|
+
const pi = {
|
|
16
|
+
on(event, handler) {
|
|
17
|
+
const list = handlers.get(event) ?? [];
|
|
18
|
+
list.push(handler);
|
|
19
|
+
handlers.set(event, list);
|
|
20
|
+
},
|
|
21
|
+
getCommands() {
|
|
22
|
+
return extensionCommands;
|
|
23
|
+
},
|
|
24
|
+
getMcpServers() {
|
|
25
|
+
return registrations.map(({ name, config }) => ({ name, config, extensionPath: "<native-n1-fixture>" }));
|
|
26
|
+
},
|
|
27
|
+
getAllTools() {
|
|
28
|
+
return registrations.flatMap(({ name, config }) => Object.entries(config.toolExposure ?? {})
|
|
29
|
+
.map(([tool, exposure]) => ({ name: `mcp__${name}__${tool}`, namespace: { name: `mcp__${name}` }, exposure })));
|
|
30
|
+
},
|
|
31
|
+
getActiveTools() {
|
|
32
|
+
return this.getAllTools().filter((tool) => tool.exposure !== "hidden").map((tool) => tool.name);
|
|
33
|
+
},
|
|
34
|
+
registerMcpServer(name, config) {
|
|
35
|
+
registrations.push({ name, config });
|
|
36
|
+
},
|
|
37
|
+
unregisterMcpServer(name) {
|
|
38
|
+
unregistrations.push(name);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const extension = await import(pathToFileURL(wrapperPath).href);
|
|
43
|
+
if (typeof extension.default !== "function") throw new Error("generated wrapper has no default extension factory");
|
|
44
|
+
await extension.default(pi);
|
|
45
|
+
const context = {
|
|
46
|
+
cwd,
|
|
47
|
+
sessionManager: {
|
|
48
|
+
getSessionFile: () => undefined,
|
|
49
|
+
getLeafId: () => undefined,
|
|
50
|
+
getEntries: () => [],
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
for (const handler of handlers.get("session_start") ?? []) {
|
|
54
|
+
await handler({ type: "session_start" }, context);
|
|
55
|
+
}
|
|
56
|
+
if (registrations.length !== 1) throw new Error(`expected one native MCP registration, saw ${registrations.length}`);
|
|
57
|
+
const expectedTools = Object.keys(registrations[0].config.toolExposure ?? {})
|
|
58
|
+
.map((tool) => `mcp__${registrations[0].name}__${tool}`);
|
|
59
|
+
const deadline = Date.now() + 3_000;
|
|
60
|
+
while (!expectedTools.every((name) => pi.getActiveTools().includes(name))) {
|
|
61
|
+
if (Date.now() >= deadline) throw new Error(`native MCP tools never became active: ${pi.getActiveTools()}`);
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
63
|
+
}
|
|
64
|
+
const beforeAgentStart = handlers.get("before_agent_start")?.[0];
|
|
65
|
+
if (typeof beforeAgentStart !== "function") throw new Error("generated wrapper omitted before_agent_start wire binding");
|
|
66
|
+
const binding = await beforeAgentStart({ systemPrompt: "N1 fixture" }, context);
|
|
67
|
+
const wirePrompt = binding?.systemPrompt;
|
|
68
|
+
if (typeof wirePrompt !== "string" || !expectedTools.every((name) => wirePrompt.includes(name))) {
|
|
69
|
+
throw new Error(`native Team tool wire binding is incomplete: ${wirePrompt}`);
|
|
70
|
+
}
|
|
71
|
+
await writeFile(receiptPath, `${JSON.stringify({ registrations, unregistrations, activeTools: pi.getActiveTools(), expectedTools, wirePrompt }, null, 2)}\n`, "utf8");
|
|
@@ -1321,33 +1321,6 @@ fn lanea_stop_window_present_kills_and_stopped_true() {
|
|
|
1321
1321
|
);
|
|
1322
1322
|
}
|
|
1323
1323
|
|
|
1324
|
-
// ── STOP #2 (stop-display-noop-2) [RED] — close ghostty_workspace slot: persist display.status/pane_title
|
|
1325
|
-
// Golden operations.py:88-92 -> display/close.py:84-85 relabels the slot: display["status"]="stopped",
|
|
1326
|
-
// display["pane_title"]=f"stopped: {agent_id}", written back into the persisted agent entry. Rust never
|
|
1327
|
-
// touches the display (mark_agent_stopped leaves it as-is) and hardcodes display_closed:false. RED: the
|
|
1328
|
-
// persisted display.status/pane_title are the in-process observable.
|
|
1329
|
-
#[test]
|
|
1330
|
-
#[ignore = "legacy external display contract removed; runtime is silent tmux only"]
|
|
1331
|
-
fn lanea_stop_ghostty_workspace_relabels_slot_to_stopped() {
|
|
1332
|
-
let ws = lanea_ws_agents(json!({
|
|
1333
|
-
"alpha": { "status": "running", "provider": "codex", "window": "alpha",
|
|
1334
|
-
"display": { "backend": "ghostty_workspace", "pane_id": "%5", "linked_session": "disp-alpha", "status": "running", "pane_title": "alpha" } }
|
|
1335
|
-
}));
|
|
1336
|
-
let tx = LaneTransport::new("team-laneateam", &["alpha"]);
|
|
1337
|
-
let _ = stop_agent_with_transport(&ws, &aid("alpha"), None, &tx).expect("stop ok");
|
|
1338
|
-
let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
|
|
1339
|
-
assert_eq!(
|
|
1340
|
-
state.pointer("/agents/alpha/display/status").and_then(serde_json::Value::as_str),
|
|
1341
|
-
Some("stopped"),
|
|
1342
|
-
"stop must relabel a ghostty_workspace slot: display.status='stopped' (close.py:84); Rust leaves it 'running'"
|
|
1343
|
-
);
|
|
1344
|
-
assert_eq!(
|
|
1345
|
-
state.pointer("/agents/alpha/display/pane_title").and_then(serde_json::Value::as_str),
|
|
1346
|
-
Some("stopped: alpha"),
|
|
1347
|
-
"stop must set display.pane_title='stopped: <id>' (close.py:85); Rust never touches the display"
|
|
1348
|
-
);
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
1324
|
// ── RESET #3 (reset-paused-restart-2) [RED] — reset of a PAUSED agent returns ok=true (NOT an Err) ───
|
|
1352
1325
|
// Golden operations.py:126-140: after discard, reset re-spawns via start_agent(force,allow_fresh).
|
|
1353
1326
|
// discard does NOT clear `paused`, so start_agent returns the refusal-shaped {ok:False,status:paused,
|
|
@@ -1851,42 +1824,3 @@ fn fresh_start_persists_boundary_from_before_transport_spawn() {
|
|
|
1851
1824
|
spawned_at={spawned_at}, transport_observed_at={transport_observed_at}"
|
|
1852
1825
|
);
|
|
1853
1826
|
}
|
|
1854
|
-
|
|
1855
|
-
// ── REMOVE #6/#12 (remove-rollback-no-agent-health-3 / remove-rollback-health-1) [SEAM #[ignore]] ────
|
|
1856
|
-
// Golden _RemoveRollback captures `self.health = copy.deepcopy(store.agent_health().get(agent_id))`
|
|
1857
|
-
// (agents.py:185) and restore() re-upserts it via _restore_agent_health (agents.py:215-218,268-278). The
|
|
1858
|
-
// Rust RemoveRollback has NO health field and never restores it (restart.rs:972-1067). This is only
|
|
1859
|
-
// observable when a step AFTER the agent_health delete fails — but Rust's only post-delete step is the
|
|
1860
|
-
// snapshot, which golden runs OUTSIDE the rollback-protected region (agents.py:135). Exercising it
|
|
1861
|
-
// golden-faithfully needs a production failure-injection seam at an in-try step after the delete (mirror
|
|
1862
|
-
// the coordinator SaveHook). PORTER: add `health: Option<Value>` to RemoveRollback (capture the row
|
|
1863
|
-
// before delete; restore re-upserts status||"IDLE"/last_output_at/context_usage_pct/current_task_id, or
|
|
1864
|
-
// deletes if None) AND move save_team_runtime_snapshot OUTSIDE the rollback region (golden agents.py:135).
|
|
1865
|
-
#[test]
|
|
1866
|
-
#[ignore = "seam: agent_health rollback restore needs a failure-injection hook (post-delete, in-try) to \
|
|
1867
|
-
exercise in-process; golden agents.py:185/215-218/268-278. Porter adds RemoveRollback.health + \
|
|
1868
|
-
moves save_team_runtime_snapshot outside the rollback region."]
|
|
1869
|
-
fn lanea_remove_rollback_restores_agent_health() {
|
|
1870
|
-
// Golden contract (verified by reading agents.py): on a mid-remove failure after the agent_health
|
|
1871
|
-
// row is deleted, rollback re-upserts the captured row so the health history is not lost.
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
// ── FORK (fork-incomplete-rollback) [SEAM #[ignore]] — post-spawn rollback arms ─────────────────────
|
|
1875
|
-
// Golden operations.py:384-394 wraps spec-mutation..start_coordinator in try/except; on ANY failure it
|
|
1876
|
-
// (1) kills the spawned tmux window if present, (2) adapter.cleanup_mcp, (3) restores old spec text, and
|
|
1877
|
-
// (4) restores prior state. Rust only restores the spec on the spawn_into arm (launch.rs:481); the
|
|
1878
|
-
// save_runtime_state (486-487) and start_coordinator (488-493) failure arms leave the spec mutated, the
|
|
1879
|
-
// already-spawned window un-killed, and the state un-rolled-back; install_mcp/cleanup_mcp are absent.
|
|
1880
|
-
// The adapter.fork_plan arm IS covered HARD above (lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm).
|
|
1881
|
-
// The post-SPAWN arms need a failure-injection seam after spawn_into (codex+subscription forks past
|
|
1882
|
-
// adapter.fork_plan, so the spawn succeeds and there is no in-process way to fail save/coordinator cleanly).
|
|
1883
|
-
// PORTER: a Drop guard armed after the spec write, disarmed on success — kills the window, restores spec
|
|
1884
|
-
// + state, runs cleanup_mcp on every post-write error arm.
|
|
1885
|
-
#[test]
|
|
1886
|
-
#[ignore = "seam: fork post-spawn rollback arms (save_runtime_state / start_coordinator failure) need a \
|
|
1887
|
-
failure-injection hook after spawn_into; golden operations.py:384-394. Porter wires a Drop \
|
|
1888
|
-
guard (kill window + restore spec/state + cleanup_mcp) armed after the spec write."]
|
|
1889
|
-
fn lanea_fork_rollback_complete_on_post_spawn_failure() {
|
|
1890
|
-
// Golden contract (operations.py:384-394): a post-spawn failure kills the spawned window, restores
|
|
1891
|
-
// the old spec text + prior state, and runs cleanup_mcp before re-raising.
|
|
1892
|
-
}
|
|
@@ -2800,130 +2800,6 @@ fn fork_refuses_source_with_partial_tuple() {
|
|
|
2800
2800
|
);
|
|
2801
2801
|
}
|
|
2802
2802
|
|
|
2803
|
-
#[test]
|
|
2804
|
-
#[ignore = "legacy adaptive display layout contract removed; runtime is silent tmux only"]
|
|
2805
|
-
fn add_agent_adaptive_splits_last_non_full_layout_window() {
|
|
2806
|
-
let roles = [("w1.md", role_doc("w1")), ("w2.md", role_doc("w2"))];
|
|
2807
|
-
let role_refs = roles
|
|
2808
|
-
.iter()
|
|
2809
|
-
.map(|(file, doc)| (*file, doc.as_str()))
|
|
2810
|
-
.collect::<Vec<_>>();
|
|
2811
|
-
let team = quick_start_team_dir_with_roles(&role_refs);
|
|
2812
|
-
let session = "team-layout-add";
|
|
2813
|
-
crate::state::persist::save_runtime_state(
|
|
2814
|
-
&team,
|
|
2815
|
-
&json!({
|
|
2816
|
-
"session_name": session,
|
|
2817
|
-
"legacy_display_key": "adaptive",
|
|
2818
|
-
"active_team_key": "teamdir",
|
|
2819
|
-
"agents": {
|
|
2820
|
-
"w1": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 0, "pane_id": "%1"},
|
|
2821
|
-
"w2": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 1, "pane_id": "%2"}
|
|
2822
|
-
}
|
|
2823
|
-
}),
|
|
2824
|
-
)
|
|
2825
|
-
.unwrap();
|
|
2826
|
-
seed_healthy_coordinator(&team);
|
|
2827
|
-
let role_file = team.join("w3-role.md");
|
|
2828
|
-
std::fs::write(&role_file, role_doc("w3")).unwrap();
|
|
2829
|
-
let transport = OfflineTransport::new()
|
|
2830
|
-
.with_session_present(true)
|
|
2831
|
-
.with_targets(vec![
|
|
2832
|
-
layout_pane(session, "team-w1", "%1", 0),
|
|
2833
|
-
layout_pane(session, "team-w1", "%2", 1),
|
|
2834
|
-
])
|
|
2835
|
-
.with_pane_presence("%1", true)
|
|
2836
|
-
.with_pane_presence("%2", true);
|
|
2837
|
-
|
|
2838
|
-
add_agent_with_transport(
|
|
2839
|
-
&team,
|
|
2840
|
-
&AgentId::new("w3"),
|
|
2841
|
-
&role_file,
|
|
2842
|
-
true,
|
|
2843
|
-
None,
|
|
2844
|
-
&transport,
|
|
2845
|
-
)
|
|
2846
|
-
.expect("adaptive add-agent should start");
|
|
2847
|
-
|
|
2848
|
-
assert_eq!(
|
|
2849
|
-
transport.spawn_window_records().last(),
|
|
2850
|
-
Some(&(String::from("spawn_split"), String::from("team-w1"))),
|
|
2851
|
-
"last non-full layout window should be split"
|
|
2852
|
-
);
|
|
2853
|
-
let (_raw, state) = raw_runtime_state(&team);
|
|
2854
|
-
assert_eq!(
|
|
2855
|
-
state.pointer("/agents/w3/layout_window"),
|
|
2856
|
-
Some(&json!("team-w1"))
|
|
2857
|
-
);
|
|
2858
|
-
assert_eq!(state.pointer("/agents/w3/pane_index"), Some(&json!(2)));
|
|
2859
|
-
}
|
|
2860
|
-
|
|
2861
|
-
#[test]
|
|
2862
|
-
#[ignore = "legacy adaptive display layout contract removed; runtime is silent tmux only"]
|
|
2863
|
-
fn add_agent_adaptive_creates_suffix_window_when_last_layout_full_and_name_collides() {
|
|
2864
|
-
let roles = [
|
|
2865
|
-
("w1.md", role_doc("w1")),
|
|
2866
|
-
("w2.md", role_doc("w2")),
|
|
2867
|
-
("w3.md", role_doc("w3")),
|
|
2868
|
-
];
|
|
2869
|
-
let role_refs = roles
|
|
2870
|
-
.iter()
|
|
2871
|
-
.map(|(file, doc)| (*file, doc.as_str()))
|
|
2872
|
-
.collect::<Vec<_>>();
|
|
2873
|
-
let team = quick_start_team_dir_with_roles(&role_refs);
|
|
2874
|
-
let session = "team-layout-add-full";
|
|
2875
|
-
crate::state::persist::save_runtime_state(
|
|
2876
|
-
&team,
|
|
2877
|
-
&json!({
|
|
2878
|
-
"session_name": session,
|
|
2879
|
-
"legacy_display_key": "adaptive",
|
|
2880
|
-
"active_team_key": "teamdir",
|
|
2881
|
-
"agents": {
|
|
2882
|
-
"w1": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 0, "pane_id": "%1"},
|
|
2883
|
-
"w2": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 1, "pane_id": "%2"},
|
|
2884
|
-
"w3": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 2, "pane_id": "%3"}
|
|
2885
|
-
}
|
|
2886
|
-
}),
|
|
2887
|
-
)
|
|
2888
|
-
.unwrap();
|
|
2889
|
-
seed_healthy_coordinator(&team);
|
|
2890
|
-
let role_file = team.join("w4-role.md");
|
|
2891
|
-
std::fs::write(&role_file, role_doc("w4")).unwrap();
|
|
2892
|
-
let transport = OfflineTransport::new()
|
|
2893
|
-
.with_session_present(true)
|
|
2894
|
-
.with_targets(vec![
|
|
2895
|
-
layout_pane(session, "team-w1", "%1", 0),
|
|
2896
|
-
layout_pane(session, "team-w1", "%2", 1),
|
|
2897
|
-
layout_pane(session, "team-w1", "%3", 2),
|
|
2898
|
-
layout_pane(session, "team-w2", "%9", 0),
|
|
2899
|
-
])
|
|
2900
|
-
.with_pane_presence("%1", true)
|
|
2901
|
-
.with_pane_presence("%2", true)
|
|
2902
|
-
.with_pane_presence("%3", true);
|
|
2903
|
-
|
|
2904
|
-
add_agent_with_transport(
|
|
2905
|
-
&team,
|
|
2906
|
-
&AgentId::new("w4"),
|
|
2907
|
-
&role_file,
|
|
2908
|
-
true,
|
|
2909
|
-
None,
|
|
2910
|
-
&transport,
|
|
2911
|
-
)
|
|
2912
|
-
.expect("adaptive add-agent should start");
|
|
2913
|
-
|
|
2914
|
-
assert_eq!(
|
|
2915
|
-
transport.spawn_window_records().last(),
|
|
2916
|
-
Some(&(String::from("spawn_into"), String::from("team-w2-2"))),
|
|
2917
|
-
"full last layout window should create next window with collision suffix"
|
|
2918
|
-
);
|
|
2919
|
-
let (_raw, state) = raw_runtime_state(&team);
|
|
2920
|
-
assert_eq!(
|
|
2921
|
-
state.pointer("/agents/w4/layout_window"),
|
|
2922
|
-
Some(&json!("team-w2-2"))
|
|
2923
|
-
);
|
|
2924
|
-
assert_eq!(state.pointer("/agents/w4/pane_index"), Some(&json!(0)));
|
|
2925
|
-
}
|
|
2926
|
-
|
|
2927
2803
|
#[test]
|
|
2928
2804
|
fn stop_agent_adaptive_kills_target_pane_not_shared_layout_window() {
|
|
2929
2805
|
let roles = [("w1.md", role_doc("w1")), ("w2.md", role_doc("w2"))];
|
|
@@ -2978,55 +2854,6 @@ fn stop_agent_adaptive_kills_target_pane_not_shared_layout_window() {
|
|
|
2978
2854
|
);
|
|
2979
2855
|
}
|
|
2980
2856
|
|
|
2981
|
-
#[test]
|
|
2982
|
-
#[ignore = "legacy adaptive display layout contract removed; runtime is silent tmux only"]
|
|
2983
|
-
fn start_agent_adaptive_restarts_missing_pane_in_existing_layout_window() {
|
|
2984
|
-
let ws = temp_ws();
|
|
2985
|
-
crate::state::persist::save_runtime_state(
|
|
2986
|
-
&ws,
|
|
2987
|
-
&json!({
|
|
2988
|
-
"session_name": "team-layout-restart",
|
|
2989
|
-
"legacy_display_key": "adaptive",
|
|
2990
|
-
"agents": {
|
|
2991
|
-
"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"},
|
|
2992
|
-
"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"}
|
|
2993
|
-
}
|
|
2994
|
-
}),
|
|
2995
|
-
)
|
|
2996
|
-
.unwrap();
|
|
2997
|
-
seed_healthy_coordinator(&ws);
|
|
2998
|
-
let transport = OfflineTransport::new()
|
|
2999
|
-
.with_session_present(true)
|
|
3000
|
-
.with_targets(vec![layout_pane("team-layout-restart", "team-w1", "%2", 1)])
|
|
3001
|
-
.with_pane_presence("%1", false)
|
|
3002
|
-
.with_pane_presence("%2", true);
|
|
3003
|
-
|
|
3004
|
-
let outcome = start_agent_with_transport(
|
|
3005
|
-
&ws,
|
|
3006
|
-
&AgentId::new("w1"),
|
|
3007
|
-
false,
|
|
3008
|
-
true,
|
|
3009
|
-
true,
|
|
3010
|
-
None,
|
|
3011
|
-
&transport,
|
|
3012
|
-
)
|
|
3013
|
-
.expect("adaptive restart should spawn");
|
|
3014
|
-
|
|
3015
|
-
assert!(matches!(outcome, StartAgentOutcome::Running { .. }));
|
|
3016
|
-
assert_eq!(
|
|
3017
|
-
transport.spawn_window_records().last(),
|
|
3018
|
-
Some(&(String::from("spawn_split"), String::from("team-w1"))),
|
|
3019
|
-
"missing worker pane should be rebuilt in its existing layout window"
|
|
3020
|
-
);
|
|
3021
|
-
let (_raw, state) = raw_runtime_state(&ws);
|
|
3022
|
-
assert_eq!(
|
|
3023
|
-
state.pointer("/agents/w1/layout_window"),
|
|
3024
|
-
Some(&json!("team-w1"))
|
|
3025
|
-
);
|
|
3026
|
-
assert_eq!(state.pointer("/agents/w1/window"), Some(&json!("team-w1")));
|
|
3027
|
-
assert_eq!(state.pointer("/agents/w2/pane_id"), Some(&json!("%2")));
|
|
3028
|
-
}
|
|
3029
|
-
|
|
3030
2857
|
// ═════════════════════════════════════════════════════════════════════════
|
|
3031
2858
|
// WAVE 2 · LANE A — agent-lifecycle byte-parity contracts. stop_agent / reset_agent / remove_agent /
|
|
3032
2859
|
// fork_agent are stubs (OwnerRefused / RequirementUnmet / "session_id missing"). These RED contracts
|
|
@@ -3714,175 +3541,6 @@ fn e43_adaptive_placement_skips_existing_agent_whose_live_pane_window_name_diffe
|
|
|
3714
3541
|
}
|
|
3715
3542
|
}
|
|
3716
3543
|
|
|
3717
|
-
/// **Regression guard — Fix A**: when an existing agent's live pane DOES
|
|
3718
|
-
/// reside under the claimed layout_window, placement keeps the prior
|
|
3719
|
-
/// behaviour (groups the new agent into that window). This is the canonical
|
|
3720
|
-
/// adaptive 1-window-multiple-panes use case.
|
|
3721
|
-
#[test]
|
|
3722
|
-
#[ignore = "legacy adaptive display layout contract removed; runtime is silent tmux only"]
|
|
3723
|
-
fn e43_adaptive_placement_groups_into_layout_window_when_live_pane_matches_claim() {
|
|
3724
|
-
use crate::lifecycle::launch::adaptive_placement_for_agent;
|
|
3725
|
-
use crate::model::ids::AgentId;
|
|
3726
|
-
use crate::transport::{SessionName, WindowName};
|
|
3727
|
-
|
|
3728
|
-
let state = json!({
|
|
3729
|
-
"legacy_display_key": "adaptive",
|
|
3730
|
-
"session_name": "team-iso",
|
|
3731
|
-
"agents": {
|
|
3732
|
-
"alpha": {
|
|
3733
|
-
"status": "running",
|
|
3734
|
-
"provider": "codex",
|
|
3735
|
-
"layout_window": "team-w1",
|
|
3736
|
-
"pane_id": "%1",
|
|
3737
|
-
}
|
|
3738
|
-
}
|
|
3739
|
-
});
|
|
3740
|
-
|
|
3741
|
-
let transport = OfflineTransport::new()
|
|
3742
|
-
.with_targets(vec![layout_pane("team-iso", "team-w1", "%1", 0)])
|
|
3743
|
-
.with_windows(vec![WindowName::new("team-w1")]);
|
|
3744
|
-
|
|
3745
|
-
let placement = adaptive_placement_for_agent(
|
|
3746
|
-
&state,
|
|
3747
|
-
&transport,
|
|
3748
|
-
&SessionName::new("team-iso"),
|
|
3749
|
-
&AgentId::new("beta"),
|
|
3750
|
-
)
|
|
3751
|
-
.expect("placement should succeed");
|
|
3752
|
-
|
|
3753
|
-
assert_eq!(
|
|
3754
|
-
placement.layout_window.as_str(),
|
|
3755
|
-
"team-w1",
|
|
3756
|
-
"E43 Fix A regression guard: when alpha's live pane DOES live under \
|
|
3757
|
-
layout_window 'team-w1', the new agent beta should be grouped into \
|
|
3758
|
-
the same window (canonical adaptive behaviour). Got {placement:?}"
|
|
3759
|
-
);
|
|
3760
|
-
assert!(
|
|
3761
|
-
!placement.starts_window,
|
|
3762
|
-
"E43 Fix A regression guard: grouping into an existing window means \
|
|
3763
|
-
starts_window=false (split into the window). Got {placement:?}"
|
|
3764
|
-
);
|
|
3765
|
-
}
|
|
3766
|
-
|
|
3767
|
-
/// **RED — Fix B**: `adaptive_existing_placement_for_agent` must NOT return a
|
|
3768
|
-
/// `starts_window=true` placement that names a window NOT present in
|
|
3769
|
-
/// `live_windows`. When the agent's claimed `layout_window` is stale and the
|
|
3770
|
-
/// session is effectively per-agent (live windows are named after agent_ids),
|
|
3771
|
-
/// the placement must fall back to a per-agent window named after the
|
|
3772
|
-
/// agent_id itself — so the subsequent spawn opens a new window, not a split
|
|
3773
|
-
/// of a phantom.
|
|
3774
|
-
#[test]
|
|
3775
|
-
#[ignore = "legacy adaptive display layout contract removed; runtime is silent tmux only"]
|
|
3776
|
-
fn e43_adaptive_existing_placement_falls_back_to_agent_id_window_when_claim_missing_from_live() {
|
|
3777
|
-
use crate::lifecycle::launch::adaptive_existing_placement_for_agent;
|
|
3778
|
-
use crate::model::ids::AgentId;
|
|
3779
|
-
use crate::transport::{SessionName, WindowName};
|
|
3780
|
-
|
|
3781
|
-
let state = json!({
|
|
3782
|
-
"legacy_display_key": "adaptive",
|
|
3783
|
-
"session_name": "team-iso",
|
|
3784
|
-
"agents": {
|
|
3785
|
-
"demo-director": {
|
|
3786
|
-
"status": "running",
|
|
3787
|
-
"provider": "codex",
|
|
3788
|
-
// Stale claim — live session has no team-w2 window.
|
|
3789
|
-
"layout_window": "team-w2",
|
|
3790
|
-
"pane_id": "%99",
|
|
3791
|
-
}
|
|
3792
|
-
}
|
|
3793
|
-
});
|
|
3794
|
-
|
|
3795
|
-
let transport = OfflineTransport::new()
|
|
3796
|
-
.with_targets(vec![
|
|
3797
|
-
layout_pane("team-iso", "architect", "%1", 0),
|
|
3798
|
-
layout_pane("team-iso", "test-engineer", "%2", 0),
|
|
3799
|
-
])
|
|
3800
|
-
.with_windows(vec![
|
|
3801
|
-
WindowName::new("architect"),
|
|
3802
|
-
WindowName::new("test-engineer"),
|
|
3803
|
-
]);
|
|
3804
|
-
|
|
3805
|
-
let placement = adaptive_existing_placement_for_agent(
|
|
3806
|
-
&state,
|
|
3807
|
-
&transport,
|
|
3808
|
-
&SessionName::new("team-iso"),
|
|
3809
|
-
&AgentId::new("demo-director"),
|
|
3810
|
-
)
|
|
3811
|
-
.expect("placement should resolve");
|
|
3812
|
-
|
|
3813
|
-
assert_ne!(
|
|
3814
|
-
placement.layout_window.as_str(),
|
|
3815
|
-
"team-w2",
|
|
3816
|
-
"E43 Fix B: stale claim 'team-w2' is not in live_windows; placement \
|
|
3817
|
-
must NOT reuse that name (otherwise spawn issues split -t :team-w2 \
|
|
3818
|
-
and tmux says 'can't find window: team-w2'). Got {placement:?}"
|
|
3819
|
-
);
|
|
3820
|
-
assert_eq!(
|
|
3821
|
-
placement.layout_window.as_str(),
|
|
3822
|
-
"demo-director",
|
|
3823
|
-
"E43 Fix B: when the claim is stale and the live layout is \
|
|
3824
|
-
per-agent-named, fall back to a window named after the agent_id \
|
|
3825
|
-
(canonical per-agent pattern). Got {placement:?}"
|
|
3826
|
-
);
|
|
3827
|
-
assert!(
|
|
3828
|
-
placement.starts_window,
|
|
3829
|
-
"E43 Fix B: the fallback opens a NEW window (starts_window=true); \
|
|
3830
|
-
do not try to split an existing window with the wrong name. Got \
|
|
3831
|
-
{placement:?}"
|
|
3832
|
-
);
|
|
3833
|
-
}
|
|
3834
|
-
|
|
3835
|
-
/// **Regression guard — Fix B**: when the agent's claimed layout_window IS
|
|
3836
|
-
/// in live_windows, the existing-placement path keeps its current behaviour
|
|
3837
|
-
/// (return a starts_window=false placement that splits into the existing
|
|
3838
|
-
/// window).
|
|
3839
|
-
#[test]
|
|
3840
|
-
#[ignore = "legacy adaptive display layout contract removed; runtime is silent tmux only"]
|
|
3841
|
-
fn e43_adaptive_existing_placement_keeps_starts_window_false_when_claim_in_live_windows() {
|
|
3842
|
-
use crate::lifecycle::launch::adaptive_existing_placement_for_agent;
|
|
3843
|
-
use crate::model::ids::AgentId;
|
|
3844
|
-
use crate::transport::{SessionName, WindowName};
|
|
3845
|
-
|
|
3846
|
-
let state = json!({
|
|
3847
|
-
"legacy_display_key": "adaptive",
|
|
3848
|
-
"session_name": "team-iso",
|
|
3849
|
-
"agents": {
|
|
3850
|
-
"alpha": {
|
|
3851
|
-
"status": "running",
|
|
3852
|
-
"provider": "codex",
|
|
3853
|
-
"layout_window": "team-w1",
|
|
3854
|
-
"pane_id": "%1",
|
|
3855
|
-
"pane_index": 1u64,
|
|
3856
|
-
}
|
|
3857
|
-
}
|
|
3858
|
-
});
|
|
3859
|
-
|
|
3860
|
-
let transport = OfflineTransport::new()
|
|
3861
|
-
.with_targets(vec![layout_pane("team-iso", "team-w1", "%1", 0)])
|
|
3862
|
-
.with_windows(vec![WindowName::new("team-w1")]);
|
|
3863
|
-
|
|
3864
|
-
let placement = adaptive_existing_placement_for_agent(
|
|
3865
|
-
&state,
|
|
3866
|
-
&transport,
|
|
3867
|
-
&SessionName::new("team-iso"),
|
|
3868
|
-
&AgentId::new("alpha"),
|
|
3869
|
-
)
|
|
3870
|
-
.expect("placement should resolve");
|
|
3871
|
-
|
|
3872
|
-
assert_eq!(
|
|
3873
|
-
placement.layout_window.as_str(),
|
|
3874
|
-
"team-w1",
|
|
3875
|
-
"E43 Fix B regression guard: when claim 'team-w1' IS in live_windows, \
|
|
3876
|
-
existing-placement must keep it. Got {placement:?}"
|
|
3877
|
-
);
|
|
3878
|
-
// pane_index=1 → starts_window=false in the existing behaviour.
|
|
3879
|
-
assert!(
|
|
3880
|
-
!placement.starts_window,
|
|
3881
|
-
"E43 Fix B regression guard: existing window + pane_index>0 → \
|
|
3882
|
-
starts_window=false. Got {placement:?}"
|
|
3883
|
-
);
|
|
3884
|
-
}
|
|
3885
|
-
|
|
3886
3544
|
/// **RED — Fix C end-to-end**: `add_agent` end-to-end must NOT emit a
|
|
3887
3545
|
/// spawn_split with the stale window name. With state carrying
|
|
3888
3546
|
/// `legacy_display_key=adaptive` + stale `layout_window=team-w2` residue and
|
|
@@ -4178,56 +3836,6 @@ fn e45_add_agent_opens_new_window_does_not_split_into_per_agent_window() {
|
|
|
4178
3836
|
);
|
|
4179
3837
|
}
|
|
4180
3838
|
|
|
4181
|
-
/// **Regression guard**: when the live session DOES have a real `team-w1`
|
|
4182
|
-
/// adaptive window with a live pane that matches state's claim, the
|
|
4183
|
-
/// existing E43+adaptive grouping behaviour is preserved (split into team-w1
|
|
4184
|
-
/// as before). E45 only changes behaviour for per-agent windows.
|
|
4185
|
-
#[test]
|
|
4186
|
-
#[ignore = "legacy adaptive display layout contract removed; runtime is silent tmux only"]
|
|
4187
|
-
fn e45_real_team_w_adaptive_layout_still_groups_into_layout_window() {
|
|
4188
|
-
use crate::lifecycle::launch::adaptive_placement_for_agent;
|
|
4189
|
-
use crate::model::ids::AgentId;
|
|
4190
|
-
use crate::transport::{SessionName, WindowName};
|
|
4191
|
-
|
|
4192
|
-
let state = json!({
|
|
4193
|
-
"legacy_display_key": "adaptive",
|
|
4194
|
-
"session_name": "team-iso",
|
|
4195
|
-
"agents": {
|
|
4196
|
-
"alpha": {
|
|
4197
|
-
"status": "running",
|
|
4198
|
-
"provider": "codex",
|
|
4199
|
-
"layout_window": "team-w1",
|
|
4200
|
-
"layout_index": 0u64,
|
|
4201
|
-
"pane_id": "%1",
|
|
4202
|
-
}
|
|
4203
|
-
}
|
|
4204
|
-
});
|
|
4205
|
-
|
|
4206
|
-
let transport = OfflineTransport::new()
|
|
4207
|
-
.with_targets(vec![layout_pane("team-iso", "team-w1", "%1", 0)])
|
|
4208
|
-
.with_windows(vec![WindowName::new("team-w1")]);
|
|
4209
|
-
|
|
4210
|
-
let placement = adaptive_placement_for_agent(
|
|
4211
|
-
&state,
|
|
4212
|
-
&transport,
|
|
4213
|
-
&SessionName::new("team-iso"),
|
|
4214
|
-
&AgentId::new("beta"),
|
|
4215
|
-
)
|
|
4216
|
-
.expect("E45 regression guard: real team-w1 adaptive layout must still group new agents in");
|
|
4217
|
-
|
|
4218
|
-
assert_eq!(
|
|
4219
|
-
placement.layout_window.as_str(),
|
|
4220
|
-
"team-w1",
|
|
4221
|
-
"E45 regression guard: real `team-w1` adaptive window must still \
|
|
4222
|
-
host new agents (canonical adaptive behaviour). Got {placement:?}"
|
|
4223
|
-
);
|
|
4224
|
-
assert!(
|
|
4225
|
-
!placement.starts_window,
|
|
4226
|
-
"E45 regression guard: grouping into existing team-w1 → \
|
|
4227
|
-
starts_window=false. Got {placement:?}"
|
|
4228
|
-
);
|
|
4229
|
-
}
|
|
4230
|
-
|
|
4231
3839
|
/// **Regression guard**: helper `is_adaptive_layout_window` golden cases.
|
|
4232
3840
|
/// `team-w1`, `team-w42`, `team-w7-foo` are adaptive; agent_id names and
|
|
4233
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();
|