@team-agent/installer 0.5.71 → 0.5.73
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 +107 -9
- package/crates/team-agent/src/cli/emit.rs +35 -5
- package/crates/team-agent/src/cli/mod.rs +78 -7
- package/crates/team-agent/src/cli/models.rs +443 -0
- package/crates/team-agent/src/cli/spec.rs +4 -1
- package/crates/team-agent/src/cli/types.rs +8 -0
- package/crates/team-agent/src/compiler.rs +157 -0
- package/crates/team-agent/src/leader/owner_bind.rs +21 -0
- package/crates/team-agent/src/leader/provider_attribution.rs +25 -0
- package/crates/team-agent/src/leader/registry.rs +25 -1
- package/crates/team-agent/src/leader/start.rs +41 -9
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +169 -10
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +119 -9
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +859 -12
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +168 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +209 -0
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +71 -1
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +3 -1
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +396 -3
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +3 -1
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/provider/adapters/pi.rs +12 -6
- package/npm/check-skill-links.mjs +88 -0
- package/package.json +5 -4
- package/skills/team-agent/SKILL.md +15 -49
- package/skills/team-agent/command-coverage.json +8 -1
- package/skills/team-agent/references/team-agent-operator.md +665 -0
|
@@ -189,6 +189,7 @@ pub(super) fn spawn_agents(
|
|
|
189
189
|
tool_categories: &resolved_tool_refs,
|
|
190
190
|
team_mcp_tools: &["send_message", "report_result"],
|
|
191
191
|
mcp_config: &mcp_config,
|
|
192
|
+
session_scope: crate::lifecycle::launch::pi_mcp::PiSessionScope::Isolated,
|
|
192
193
|
},
|
|
193
194
|
)
|
|
194
195
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?
|
|
@@ -402,6 +402,7 @@ pub(super) fn spawn_agent_window(
|
|
|
402
402
|
tool_categories: &resolved_tool_refs,
|
|
403
403
|
team_mcp_tools: &["send_message", "report_result"],
|
|
404
404
|
mcp_config: &mcp_config,
|
|
405
|
+
session_scope: crate::lifecycle::launch::pi_mcp::PiSessionScope::Isolated,
|
|
405
406
|
};
|
|
406
407
|
match resume_session_id {
|
|
407
408
|
Some(session_id) => {
|
|
@@ -1846,3 +1846,171 @@ fn lanea_fork_rollback_complete_on_post_spawn_failure() {
|
|
|
1846
1846
|
// Golden contract (operations.py:384-394): a post-spawn failure kills the spawned window, restores
|
|
1847
1847
|
// the old spec text + prior state, and runs cleanup_mcp before re-raising.
|
|
1848
1848
|
}
|
|
1849
|
+
|
|
1850
|
+
#[test]
|
|
1851
|
+
fn schema_invalid_pi_role_is_rejected_before_normal_and_force_mutation() {
|
|
1852
|
+
let ws = lanea_ws_agents(json!({
|
|
1853
|
+
"alpha": { "status": "stopped", "provider": "codex", "window": "alpha" },
|
|
1854
|
+
"bravo": { "status": "running", "provider": "codex", "window": "bravo" }
|
|
1855
|
+
}));
|
|
1856
|
+
let tx = LaneTransport::new("team-laneateam", &[]);
|
|
1857
|
+
seed_healthy_coordinator(&ws);
|
|
1858
|
+
let role_dir = ws.join("role-masters");
|
|
1859
|
+
std::fs::create_dir_all(&role_dir).unwrap();
|
|
1860
|
+
let role = role_dir.join("invalid-pi.md");
|
|
1861
|
+
std::fs::write(
|
|
1862
|
+
&role,
|
|
1863
|
+
CHARLIE_ROLE
|
|
1864
|
+
.replace(
|
|
1865
|
+
"provider: codex\nmodel: gpt-5.5",
|
|
1866
|
+
"provider: pi\nmodel: openai-codex/gpt-5.6-sol",
|
|
1867
|
+
)
|
|
1868
|
+
.replace(" - mcp_team", " - fs_read"),
|
|
1869
|
+
)
|
|
1870
|
+
.unwrap();
|
|
1871
|
+
|
|
1872
|
+
let spec_path = selected_spec_path(&ws);
|
|
1873
|
+
let state_path = crate::state::persist::runtime_state_path(&ws);
|
|
1874
|
+
let event_path = ws.join(".team/logs/events.jsonl");
|
|
1875
|
+
let copied_role = ws.join("agents/charlie.md");
|
|
1876
|
+
let team_state_path = ws.join("team_state.md");
|
|
1877
|
+
let charlie_paths = crate::lifecycle::launch::pi_mcp::pi_seat_paths(
|
|
1878
|
+
&ws,
|
|
1879
|
+
"laneateam",
|
|
1880
|
+
"charlie",
|
|
1881
|
+
);
|
|
1882
|
+
let alpha_paths =
|
|
1883
|
+
crate::lifecycle::launch::pi_mcp::pi_seat_paths(&ws, "laneateam", "alpha");
|
|
1884
|
+
let spec_before = std::fs::read(&spec_path).unwrap();
|
|
1885
|
+
let state_before = std::fs::read(&state_path).unwrap();
|
|
1886
|
+
let role_before = std::fs::read(&role).unwrap();
|
|
1887
|
+
let alpha_before = serde_json::from_slice::<serde_json::Value>(&state_before).unwrap()
|
|
1888
|
+
["agents"]["alpha"]
|
|
1889
|
+
.clone();
|
|
1890
|
+
for path in [
|
|
1891
|
+
&event_path,
|
|
1892
|
+
&copied_role,
|
|
1893
|
+
&team_state_path,
|
|
1894
|
+
&charlie_paths.wrapper,
|
|
1895
|
+
&charlie_paths.sessions,
|
|
1896
|
+
&alpha_paths.wrapper,
|
|
1897
|
+
&alpha_paths.sessions,
|
|
1898
|
+
] {
|
|
1899
|
+
assert!(!path.exists(), "precondition: {} must be absent", path.display());
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
let assert_schema_error = |error: crate::lifecycle::LifecycleError| match error {
|
|
1903
|
+
crate::lifecycle::LifecycleError::Compile(message) => {
|
|
1904
|
+
assert!(message.contains("Pi roles require mcp_team"), "{message}");
|
|
1905
|
+
}
|
|
1906
|
+
other => panic!("expected Pi role schema error, got {other:?}"),
|
|
1907
|
+
};
|
|
1908
|
+
let assert_unchanged = |expected_role: &[u8]| {
|
|
1909
|
+
assert_eq!(spec_before, std::fs::read(&spec_path).unwrap());
|
|
1910
|
+
assert_eq!(state_before, std::fs::read(&state_path).unwrap());
|
|
1911
|
+
assert_eq!(expected_role, std::fs::read(&role).unwrap());
|
|
1912
|
+
for path in [
|
|
1913
|
+
&event_path,
|
|
1914
|
+
&copied_role,
|
|
1915
|
+
&team_state_path,
|
|
1916
|
+
&charlie_paths.wrapper,
|
|
1917
|
+
&charlie_paths.sessions,
|
|
1918
|
+
&alpha_paths.wrapper,
|
|
1919
|
+
&alpha_paths.sessions,
|
|
1920
|
+
] {
|
|
1921
|
+
assert!(!path.exists(), "preflight created {}", path.display());
|
|
1922
|
+
}
|
|
1923
|
+
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
1924
|
+
assert_eq!(state["agents"]["alpha"], alpha_before);
|
|
1925
|
+
assert!(state["agents"].get("charlie").is_none(), "no reservation row");
|
|
1926
|
+
assert!(tx.spawns().is_empty());
|
|
1927
|
+
assert!(tx.killed().is_empty());
|
|
1928
|
+
};
|
|
1929
|
+
|
|
1930
|
+
let mut normal_calls = 0;
|
|
1931
|
+
let mut normal_discover = |_requested: &str| {
|
|
1932
|
+
normal_calls += 1;
|
|
1933
|
+
Ok(Vec::new())
|
|
1934
|
+
};
|
|
1935
|
+
let normal = crate::lifecycle::add_agent_with_transport_pi_preflight(
|
|
1936
|
+
&ws,
|
|
1937
|
+
&aid("charlie"),
|
|
1938
|
+
&role,
|
|
1939
|
+
false,
|
|
1940
|
+
None,
|
|
1941
|
+
&tx,
|
|
1942
|
+
&mut normal_discover,
|
|
1943
|
+
)
|
|
1944
|
+
.expect_err("invalid Pi schema must fail before reservation");
|
|
1945
|
+
drop(normal_discover);
|
|
1946
|
+
assert_eq!(normal_calls, 0);
|
|
1947
|
+
assert_schema_error(normal);
|
|
1948
|
+
assert_unchanged(&role_before);
|
|
1949
|
+
|
|
1950
|
+
let mut force_calls = 0;
|
|
1951
|
+
let mut force_discover = |_requested: &str| {
|
|
1952
|
+
force_calls += 1;
|
|
1953
|
+
Ok(Vec::new())
|
|
1954
|
+
};
|
|
1955
|
+
let force = crate::lifecycle::add_agent_with_transport_force_pi_preflight(
|
|
1956
|
+
&ws,
|
|
1957
|
+
&aid("alpha"),
|
|
1958
|
+
&role,
|
|
1959
|
+
false,
|
|
1960
|
+
None,
|
|
1961
|
+
true,
|
|
1962
|
+
&tx,
|
|
1963
|
+
&mut force_discover,
|
|
1964
|
+
)
|
|
1965
|
+
.expect_err("invalid Pi replacement schema must fail before old-seat consumption");
|
|
1966
|
+
drop(force_discover);
|
|
1967
|
+
assert_eq!(force_calls, 0);
|
|
1968
|
+
assert_schema_error(force);
|
|
1969
|
+
assert_unchanged(&role_before);
|
|
1970
|
+
|
|
1971
|
+
std::fs::write(
|
|
1972
|
+
&role,
|
|
1973
|
+
CHARLIE_ROLE.replace(
|
|
1974
|
+
"provider: codex\nmodel: gpt-5.5",
|
|
1975
|
+
"provider: pi\nmodel: gpt-5.6-sol",
|
|
1976
|
+
),
|
|
1977
|
+
)
|
|
1978
|
+
.unwrap();
|
|
1979
|
+
let model_role_before = std::fs::read(&role).unwrap();
|
|
1980
|
+
let assert_model_diagnostic = |error: crate::lifecycle::LifecycleError| match error {
|
|
1981
|
+
crate::lifecycle::LifecycleError::PiModelPreflight {
|
|
1982
|
+
requested,
|
|
1983
|
+
candidates,
|
|
1984
|
+
not_ready,
|
|
1985
|
+
..
|
|
1986
|
+
} => {
|
|
1987
|
+
assert_eq!(requested, "gpt-5.6-sol");
|
|
1988
|
+
assert_eq!(candidates, vec!["openai-codex/gpt-5.6-sol"]);
|
|
1989
|
+
assert!(!not_ready);
|
|
1990
|
+
}
|
|
1991
|
+
other => panic!("expected typed Pi preflight error, got {other:?}"),
|
|
1992
|
+
};
|
|
1993
|
+
for (force, agent_id) in [(false, "charlie"), (true, "alpha")] {
|
|
1994
|
+
let mut calls = 0;
|
|
1995
|
+
let mut discover = |requested: &str| {
|
|
1996
|
+
calls += 1;
|
|
1997
|
+
assert_eq!(requested, "gpt-5.6-sol");
|
|
1998
|
+
Ok(vec!["openai-codex/gpt-5.6-sol".into()])
|
|
1999
|
+
};
|
|
2000
|
+
let error = crate::lifecycle::add_agent_with_transport_force_pi_preflight(
|
|
2001
|
+
&ws,
|
|
2002
|
+
&aid(agent_id),
|
|
2003
|
+
&role,
|
|
2004
|
+
false,
|
|
2005
|
+
None,
|
|
2006
|
+
force,
|
|
2007
|
+
&tx,
|
|
2008
|
+
&mut discover,
|
|
2009
|
+
)
|
|
2010
|
+
.expect_err("unqualified Pi model must fail before mutation");
|
|
2011
|
+
drop(discover);
|
|
2012
|
+
assert_eq!(calls, 1);
|
|
2013
|
+
assert_model_diagnostic(error);
|
|
2014
|
+
assert_unchanged(&model_role_before);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
@@ -375,6 +375,215 @@ fn quick_start_invalid_role_doc_surfaces_real_compile_error() {
|
|
|
375
375
|
);
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
#[test]
|
|
379
|
+
fn quick_start_absent_or_empty_roles_preserves_compile_outcome_without_pi_preflight_mutation() {
|
|
380
|
+
for (label, create_agents, expected) in [
|
|
381
|
+
("absent", false, "missing agents directory"),
|
|
382
|
+
("empty", true, "no role docs found"),
|
|
383
|
+
] {
|
|
384
|
+
let team = temp_ws().join(format!("teamdir-{label}"));
|
|
385
|
+
std::fs::create_dir_all(&team).unwrap();
|
|
386
|
+
std::fs::write(team.join("TEAM.md"), QS_TEAM_MD).unwrap();
|
|
387
|
+
if create_agents {
|
|
388
|
+
std::fs::create_dir(team.join("agents")).unwrap();
|
|
389
|
+
}
|
|
390
|
+
let workspace = crate::model::paths::team_workspace(&team).unwrap();
|
|
391
|
+
let team_md_before = std::fs::read(team.join("TEAM.md")).unwrap();
|
|
392
|
+
let absent_paths = [
|
|
393
|
+
team.join("team.spec.yaml"),
|
|
394
|
+
crate::state::persist::runtime_state_path(&workspace),
|
|
395
|
+
crate::model::paths::runtime_spec_path(&workspace, "quickteam"),
|
|
396
|
+
workspace.join("team_state.md"),
|
|
397
|
+
workspace.join(".team/logs/events.jsonl"),
|
|
398
|
+
workspace.join(".team/runtime/team.db"),
|
|
399
|
+
];
|
|
400
|
+
for path in &absent_paths {
|
|
401
|
+
assert!(!path.exists(), "precondition: {} must be absent", path.display());
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let transport = OfflineTransport::new();
|
|
405
|
+
let mut calls = 0;
|
|
406
|
+
let mut discover = |_requested: &str| {
|
|
407
|
+
calls += 1;
|
|
408
|
+
Ok(Vec::new())
|
|
409
|
+
};
|
|
410
|
+
let error = quick_start_with_transport_in_workspace_with_display_pi_preflight(
|
|
411
|
+
&workspace,
|
|
412
|
+
&team,
|
|
413
|
+
None,
|
|
414
|
+
true,
|
|
415
|
+
None,
|
|
416
|
+
&transport,
|
|
417
|
+
true,
|
|
418
|
+
&mut discover,
|
|
419
|
+
)
|
|
420
|
+
.expect_err("an empty role input must preserve the existing compile outcome");
|
|
421
|
+
drop(discover);
|
|
422
|
+
assert_eq!(calls, 0, "{label} role input must not invoke Pi discovery");
|
|
423
|
+
match error {
|
|
424
|
+
LifecycleError::Compile(message) => {
|
|
425
|
+
assert!(message.contains(expected), "{label}: {message}");
|
|
426
|
+
}
|
|
427
|
+
other => panic!("{label}: expected existing compile outcome, got {other:?}"),
|
|
428
|
+
}
|
|
429
|
+
assert_eq!(team_md_before, std::fs::read(team.join("TEAM.md")).unwrap());
|
|
430
|
+
for path in &absent_paths {
|
|
431
|
+
assert!(!path.exists(), "{label} role input created {}", path.display());
|
|
432
|
+
}
|
|
433
|
+
assert!(transport.calls().is_empty(), "{label}: no transport mutation");
|
|
434
|
+
assert!(transport.spawn_records().is_empty(), "{label}: no spawn");
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#[test]
|
|
439
|
+
fn quick_start_injected_pi_preflight_is_typed_and_mutation_free() {
|
|
440
|
+
let role_doc = QS_VALID_ROLE.replace(
|
|
441
|
+
"provider: codex\nmodel: gpt-5.5",
|
|
442
|
+
"provider: pi\nmodel: gpt-5.6-sol",
|
|
443
|
+
);
|
|
444
|
+
let team = quick_start_team_dir(&role_doc);
|
|
445
|
+
let workspace = crate::model::paths::team_workspace(&team).unwrap();
|
|
446
|
+
let role_path = team.join("agents/implementer.md");
|
|
447
|
+
let team_md_path = team.join("TEAM.md");
|
|
448
|
+
let local_spec_path = team.join("team.spec.yaml");
|
|
449
|
+
let runtime_state_path = crate::state::persist::runtime_state_path(&workspace);
|
|
450
|
+
let runtime_spec_path = crate::model::paths::runtime_spec_path(&workspace, "quickteam");
|
|
451
|
+
let pi_paths = crate::lifecycle::launch::pi_mcp::pi_seat_paths(
|
|
452
|
+
&workspace,
|
|
453
|
+
"quickteam",
|
|
454
|
+
"implementer",
|
|
455
|
+
);
|
|
456
|
+
let role_before = std::fs::read(&role_path).unwrap();
|
|
457
|
+
let team_md_before = std::fs::read(&team_md_path).unwrap();
|
|
458
|
+
for path in [
|
|
459
|
+
&local_spec_path,
|
|
460
|
+
&runtime_state_path,
|
|
461
|
+
&runtime_spec_path,
|
|
462
|
+
&pi_paths.wrapper,
|
|
463
|
+
&pi_paths.sessions,
|
|
464
|
+
] {
|
|
465
|
+
assert!(!path.exists(), "precondition: {} must be absent", path.display());
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let transport = OfflineTransport::new();
|
|
469
|
+
let mut calls = 0;
|
|
470
|
+
let mut discover = |requested: &str| {
|
|
471
|
+
calls += 1;
|
|
472
|
+
assert_eq!(requested, "gpt-5.6-sol");
|
|
473
|
+
Ok(vec!["openai-codex/gpt-5.6-sol".into()])
|
|
474
|
+
};
|
|
475
|
+
let error = quick_start_with_transport_in_workspace_with_display_pi_preflight(
|
|
476
|
+
&workspace,
|
|
477
|
+
&team,
|
|
478
|
+
None,
|
|
479
|
+
true,
|
|
480
|
+
None,
|
|
481
|
+
&transport,
|
|
482
|
+
true,
|
|
483
|
+
&mut discover,
|
|
484
|
+
)
|
|
485
|
+
.expect_err("unqualified Pi role must fail before quick-start mutation");
|
|
486
|
+
drop(discover);
|
|
487
|
+
assert_eq!(calls, 1);
|
|
488
|
+
match error {
|
|
489
|
+
LifecycleError::PiModelPreflight {
|
|
490
|
+
requested,
|
|
491
|
+
candidates,
|
|
492
|
+
action,
|
|
493
|
+
not_ready,
|
|
494
|
+
} => {
|
|
495
|
+
assert_eq!(requested, "gpt-5.6-sol");
|
|
496
|
+
assert_eq!(candidates, vec!["openai-codex/gpt-5.6-sol"]);
|
|
497
|
+
assert_eq!(
|
|
498
|
+
action,
|
|
499
|
+
"copy a candidate into the role, or run `team-agent models --provider pi --search gpt-5.6-sol`"
|
|
500
|
+
);
|
|
501
|
+
assert!(!not_ready);
|
|
502
|
+
}
|
|
503
|
+
other => panic!("expected typed Pi preflight error, got {other:?}"),
|
|
504
|
+
}
|
|
505
|
+
assert_eq!(role_before, std::fs::read(&role_path).unwrap());
|
|
506
|
+
assert_eq!(team_md_before, std::fs::read(&team_md_path).unwrap());
|
|
507
|
+
for path in [
|
|
508
|
+
&local_spec_path,
|
|
509
|
+
&runtime_state_path,
|
|
510
|
+
&runtime_spec_path,
|
|
511
|
+
&pi_paths.wrapper,
|
|
512
|
+
&pi_paths.sessions,
|
|
513
|
+
] {
|
|
514
|
+
assert!(!path.exists(), "quick-start preflight created {}", path.display());
|
|
515
|
+
}
|
|
516
|
+
assert!(transport.calls().is_empty(), "no transport calls before preflight rejection");
|
|
517
|
+
assert!(transport.spawn_records().is_empty());
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
#[test]
|
|
521
|
+
fn quick_start_schema_invalid_pi_role_is_mutation_free() {
|
|
522
|
+
let role_doc = QS_VALID_ROLE
|
|
523
|
+
.replace(
|
|
524
|
+
"provider: codex\nmodel: gpt-5.5",
|
|
525
|
+
"provider: pi\nmodel: openai-codex/gpt-5.6-sol",
|
|
526
|
+
)
|
|
527
|
+
.replace(" - mcp_team", " - fs_read");
|
|
528
|
+
let team = quick_start_team_dir(&role_doc);
|
|
529
|
+
let workspace = crate::model::paths::team_workspace(&team).unwrap();
|
|
530
|
+
let role_path = team.join("agents/implementer.md");
|
|
531
|
+
let team_md_path = team.join("TEAM.md");
|
|
532
|
+
let role_before = std::fs::read(&role_path).unwrap();
|
|
533
|
+
let team_md_before = std::fs::read(&team_md_path).unwrap();
|
|
534
|
+
let pi_paths = crate::lifecycle::launch::pi_mcp::pi_seat_paths(
|
|
535
|
+
&workspace,
|
|
536
|
+
"quickteam",
|
|
537
|
+
"implementer",
|
|
538
|
+
);
|
|
539
|
+
let absent_paths = [
|
|
540
|
+
team.join("team.spec.yaml"),
|
|
541
|
+
crate::state::persist::runtime_state_path(&workspace),
|
|
542
|
+
crate::model::paths::runtime_spec_path(&workspace, "quickteam"),
|
|
543
|
+
workspace.join("team_state.md"),
|
|
544
|
+
workspace.join(".team/logs/events.jsonl"),
|
|
545
|
+
workspace.join(".team/runtime/team.db"),
|
|
546
|
+
pi_paths.wrapper,
|
|
547
|
+
pi_paths.sessions,
|
|
548
|
+
];
|
|
549
|
+
for path in &absent_paths {
|
|
550
|
+
assert!(!path.exists(), "precondition: {} must be absent", path.display());
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
let transport = OfflineTransport::new();
|
|
554
|
+
let mut calls = 0;
|
|
555
|
+
let mut discover = |_requested: &str| {
|
|
556
|
+
calls += 1;
|
|
557
|
+
Ok(Vec::new())
|
|
558
|
+
};
|
|
559
|
+
let error = quick_start_with_transport_in_workspace_with_display_pi_preflight(
|
|
560
|
+
&workspace,
|
|
561
|
+
&team,
|
|
562
|
+
None,
|
|
563
|
+
true,
|
|
564
|
+
None,
|
|
565
|
+
&transport,
|
|
566
|
+
true,
|
|
567
|
+
&mut discover,
|
|
568
|
+
)
|
|
569
|
+
.expect_err("invalid Pi schema must fail before quick-start mutation");
|
|
570
|
+
drop(discover);
|
|
571
|
+
assert_eq!(calls, 0, "qualified models must not invoke discovery");
|
|
572
|
+
match error {
|
|
573
|
+
LifecycleError::Compile(message) => {
|
|
574
|
+
assert!(message.contains("Pi roles require mcp_team"), "{message}");
|
|
575
|
+
}
|
|
576
|
+
other => panic!("expected Pi role schema error, got {other:?}"),
|
|
577
|
+
}
|
|
578
|
+
assert_eq!(role_before, std::fs::read(&role_path).unwrap());
|
|
579
|
+
assert_eq!(team_md_before, std::fs::read(&team_md_path).unwrap());
|
|
580
|
+
for path in &absent_paths {
|
|
581
|
+
assert!(!path.exists(), "schema preflight created {}", path.display());
|
|
582
|
+
}
|
|
583
|
+
assert!(transport.calls().is_empty(), "no transport calls before schema rejection");
|
|
584
|
+
assert!(transport.spawn_records().is_empty());
|
|
585
|
+
}
|
|
586
|
+
|
|
378
587
|
// P0 — launch (dry_run) over a real compiled spec must resolve the REAL route/permission plan
|
|
379
588
|
// (no spawn) — NOT the hardcoded RequirementUnmet stub. Golden: launch/core.py dry_run resolves
|
|
380
589
|
// routing + permissions without starting any process.
|
|
@@ -22,7 +22,7 @@ fn command_for_model(model: &str) -> Vec<String> {
|
|
|
22
22
|
effort: Some(ProviderEffort::Medium),
|
|
23
23
|
system_prompt: "worker contract",
|
|
24
24
|
tool_categories: &["mcp_team"],
|
|
25
|
-
session_dir: Path::new("/workspace/.team/runtime/pi/t1/w1/sessions"),
|
|
25
|
+
session_dir: Some(Path::new("/workspace/.team/runtime/pi/t1/w1/sessions")),
|
|
26
26
|
session: PiSessionSelector::Fresh {
|
|
27
27
|
session_id: "716ba0cb-c491-4471-b41a-43c8d5e1f05a",
|
|
28
28
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use std::path::Path;
|
|
2
2
|
|
|
3
|
+
use crate::lifecycle::launch::pi_mcp::{pi_leader_session_scope, PiSessionScope};
|
|
3
4
|
use crate::model::enums::ProviderEffort;
|
|
4
5
|
use crate::provider::adapters::pi::{build_pi_command_argv, PiCommandRequest, PiSessionSelector};
|
|
5
6
|
|
|
@@ -11,7 +12,9 @@ fn request(session: PiSessionSelector<'static>) -> PiCommandRequest<'static> {
|
|
|
11
12
|
effort: Some(ProviderEffort::High),
|
|
12
13
|
system_prompt: "frozen worker prompt",
|
|
13
14
|
tool_categories: &["mcp_team", "fs_read", "fs_write", "execute_bash"],
|
|
14
|
-
session_dir: Path::new(
|
|
15
|
+
session_dir: Some(Path::new(
|
|
16
|
+
"/workspace/.team/runtime/pi/team-a/worker-a/sessions",
|
|
17
|
+
)),
|
|
15
18
|
session,
|
|
16
19
|
agent_id: "worker-a",
|
|
17
20
|
}
|
|
@@ -30,6 +33,73 @@ fn pi_command_omits_unset_model_and_thinking_flags() {
|
|
|
30
33
|
assert!(!argv.iter().any(|arg| arg == "--thinking"), "argv={argv:?}");
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
fn leader_request() -> PiCommandRequest<'static> {
|
|
37
|
+
PiCommandRequest {
|
|
38
|
+
executable: Path::new("/verified/pi"),
|
|
39
|
+
extension: Path::new("/workspace/.team/runtime/pi/current/leader/team-mcp.ts"),
|
|
40
|
+
model: None,
|
|
41
|
+
effort: None,
|
|
42
|
+
system_prompt: "frozen leader prompt",
|
|
43
|
+
tool_categories: &["mcp_team", "fs_read", "fs_write", "execute_bash"],
|
|
44
|
+
session_dir: None,
|
|
45
|
+
session: PiSessionSelector::Fresh {
|
|
46
|
+
session_id: "da8c3622-2378-4d05-a26c-e826a6ef6d63",
|
|
47
|
+
},
|
|
48
|
+
agent_id: "leader",
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[test]
|
|
53
|
+
fn pi_interactive_leader_argv_preserves_native_session_discovery() {
|
|
54
|
+
let argv = build_pi_command_argv(leader_request()).expect("native Pi leader command");
|
|
55
|
+
|
|
56
|
+
assert_eq!(
|
|
57
|
+
argv,
|
|
58
|
+
[
|
|
59
|
+
"/verified/pi",
|
|
60
|
+
"-e",
|
|
61
|
+
"/workspace/.team/runtime/pi/current/leader/team-mcp.ts",
|
|
62
|
+
"--append-system-prompt",
|
|
63
|
+
"frozen leader prompt",
|
|
64
|
+
"--session-id",
|
|
65
|
+
"da8c3622-2378-4d05-a26c-e826a6ef6d63",
|
|
66
|
+
"--name",
|
|
67
|
+
"leader",
|
|
68
|
+
]
|
|
69
|
+
.map(str::to_string)
|
|
70
|
+
);
|
|
71
|
+
assert!(!argv.iter().any(|arg| arg == "--session-dir"));
|
|
72
|
+
for forbidden in ["--cwd", "--workspace", "--session"] {
|
|
73
|
+
assert!(!argv.iter().any(|arg| arg == forbidden), "argv={argv:?}");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#[test]
|
|
78
|
+
fn pi_leader_session_scope_keeps_explicit_and_external_routes_isolated() {
|
|
79
|
+
assert_eq!(
|
|
80
|
+
pi_leader_session_scope(&[], false, false),
|
|
81
|
+
PiSessionScope::NativeDefault
|
|
82
|
+
);
|
|
83
|
+
for (args, external_path, allow_nested_attach) in [
|
|
84
|
+
(
|
|
85
|
+
vec![
|
|
86
|
+
"--model".to_string(),
|
|
87
|
+
"openai-codex/gpt-5.6-sol".to_string(),
|
|
88
|
+
],
|
|
89
|
+
false,
|
|
90
|
+
false,
|
|
91
|
+
),
|
|
92
|
+
(Vec::new(), true, false),
|
|
93
|
+
(Vec::new(), false, true),
|
|
94
|
+
] {
|
|
95
|
+
assert_eq!(
|
|
96
|
+
pi_leader_session_scope(&args, external_path, allow_nested_attach),
|
|
97
|
+
PiSessionScope::Isolated,
|
|
98
|
+
"only the zero-argument managed route may use Pi's native root"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
33
103
|
#[test]
|
|
34
104
|
fn pi_command_preserves_direct_pi_defaults_with_only_team_runtime_additions() {
|
|
35
105
|
let fresh = build_pi_command_argv(request(PiSessionSelector::Fresh {
|
|
@@ -59,7 +59,9 @@ fn command(tools: &[&str], effort: ProviderEffort, model: &str) -> Result<Vec<St
|
|
|
59
59
|
effort: Some(effort),
|
|
60
60
|
system_prompt: "Pi worker contract.",
|
|
61
61
|
tool_categories: tools,
|
|
62
|
-
session_dir: Path::new(
|
|
62
|
+
session_dir: Some(Path::new(
|
|
63
|
+
"/workspace/.team/runtime/pi/t1/pi-worker/sessions",
|
|
64
|
+
)),
|
|
63
65
|
session: PiSessionSelector::Fresh {
|
|
64
66
|
session_id: "c5ddf218-24a5-4e74-960c-ab6606ea7e8c",
|
|
65
67
|
},
|