@team-agent/installer 0.5.70 → 0.5.72
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 +120 -15
- 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 +33 -9
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +169 -10
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +116 -15
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +859 -12
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +23 -7
- package/crates/team-agent/src/lifecycle/restart.rs +2 -1
- 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_executable_mcp_red.rs +122 -3
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- 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
|
@@ -2522,10 +2522,10 @@ fn write_restart_agent_failed_event(
|
|
|
2522
2522
|
"session_id": decision.session_id.as_ref().map(|session| session.as_str()),
|
|
2523
2523
|
"phase": phase,
|
|
2524
2524
|
"error": error,
|
|
2525
|
-
"action":
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
decision.
|
|
2525
|
+
"action": restart_failure_action(
|
|
2526
|
+
&decision.agent_id,
|
|
2527
|
+
phase,
|
|
2528
|
+
decision.session_id.as_ref(),
|
|
2529
2529
|
),
|
|
2530
2530
|
"log": format!(
|
|
2531
2531
|
".team/logs/coordinator.log and .team/runtime/state.json agent={}",
|
|
@@ -3008,14 +3008,30 @@ fn restart_failure_next_actions(failed_agents: &[RestartFailedAgent]) -> Vec<Str
|
|
|
3008
3008
|
failed_agents
|
|
3009
3009
|
.iter()
|
|
3010
3010
|
.map(|failure| {
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
failure.
|
|
3011
|
+
restart_failure_action(
|
|
3012
|
+
&failure.agent_id,
|
|
3013
|
+
&failure.phase,
|
|
3014
|
+
failure.session_id.as_ref(),
|
|
3014
3015
|
)
|
|
3015
3016
|
})
|
|
3016
3017
|
.collect()
|
|
3017
3018
|
}
|
|
3018
3019
|
|
|
3020
|
+
pub(crate) fn restart_failure_action(
|
|
3021
|
+
agent_id: &AgentId,
|
|
3022
|
+
phase: &str,
|
|
3023
|
+
session_id: Option<&SessionId>,
|
|
3024
|
+
) -> String {
|
|
3025
|
+
if phase == "spawn" && session_id.is_none() {
|
|
3026
|
+
return format!(
|
|
3027
|
+
"worker {agent_id} failed before a pane was created; inspect the reported provider error, then rerun `team-agent restart --allow-fresh`"
|
|
3028
|
+
);
|
|
3029
|
+
}
|
|
3030
|
+
format!(
|
|
3031
|
+
"inspect worker {agent_id} output, then reset that worker with `team-agent reset-agent {agent_id}` or rerun `team-agent restart --allow-fresh`"
|
|
3032
|
+
)
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3019
3035
|
fn start_mode_wire(mode: StartMode) -> &'static str {
|
|
3020
3036
|
match mode {
|
|
3021
3037
|
StartMode::Resumed => "resumed",
|
|
@@ -92,7 +92,8 @@ pub(crate) use rebuild::restart_with_transport_with_session_convergence_deadline
|
|
|
92
92
|
// timer + worker timing writer so `lifecycle::launch::launch_with_transport_in_workspace`
|
|
93
93
|
// can emit the same instrumentation event family with `source="launch"`.
|
|
94
94
|
pub(crate) use rebuild::{
|
|
95
|
-
provider_wire_from_state,
|
|
95
|
+
provider_wire_from_state, restart_failure_action, write_worker_spawn_timing_event,
|
|
96
|
+
RestartPhaseTimer,
|
|
96
97
|
};
|
|
97
98
|
pub use rebuild::{restart, restart_with_session_convergence_deadline};
|
|
98
99
|
pub(crate) use rebuild::{
|
|
@@ -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.
|
|
@@ -3,9 +3,10 @@ use std::path::{Path, PathBuf};
|
|
|
3
3
|
use std::sync::atomic::{AtomicU32, Ordering};
|
|
4
4
|
|
|
5
5
|
use crate::lifecycle::launch::pi_mcp::{
|
|
6
|
-
parse_pi_list_models_table,
|
|
7
|
-
validate_pi_executable_chain, validate_pi_wrapper_source,
|
|
8
|
-
|
|
6
|
+
parse_pi_list_models_table, pi_seat_paths, resolve_pi_executable_chain,
|
|
7
|
+
validate_pi_adapter_identity, validate_pi_executable_chain, validate_pi_wrapper_source,
|
|
8
|
+
write_pi_wrapper, write_pi_wrapper_with_publish, PiAdapterIdentity, PiExecutableChain,
|
|
9
|
+
PiExecutableFileType, PiWrapperRequest,
|
|
9
10
|
};
|
|
10
11
|
use crate::provider::McpConfig;
|
|
11
12
|
|
|
@@ -336,6 +337,124 @@ fn pi_wrapper_is_atomic_body(hermetic: &HermeticTestEnv) {
|
|
|
336
337
|
std::fs::remove_dir_all(root).expect("remove wrapper fixture");
|
|
337
338
|
}
|
|
338
339
|
|
|
340
|
+
#[test]
|
|
341
|
+
fn pi_leader_wrapper_materializes_from_empty_unicode_wsl_workspace() {
|
|
342
|
+
let root = temp_root("empty-unicode-leader");
|
|
343
|
+
let workspace = root.join("mnt/f/测试基础设施");
|
|
344
|
+
let candidate = root.join("candidate/team-agent");
|
|
345
|
+
std::fs::create_dir_all(&workspace).expect("create empty Unicode workspace");
|
|
346
|
+
std::fs::create_dir_all(candidate.parent().expect("candidate parent"))
|
|
347
|
+
.expect("create candidate parent");
|
|
348
|
+
std::fs::write(&candidate, b"candidate").expect("write candidate fixture");
|
|
349
|
+
assert!(
|
|
350
|
+
!workspace.join(".team").exists(),
|
|
351
|
+
"fixture must begin without a .team directory"
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
let paths = pi_seat_paths(&workspace, "current", "leader");
|
|
355
|
+
let adapter = adapter_identity(&root);
|
|
356
|
+
let written = write_pi_wrapper(PiWrapperRequest {
|
|
357
|
+
destination: &paths.wrapper,
|
|
358
|
+
adapter: &adapter,
|
|
359
|
+
candidate_executable: &candidate,
|
|
360
|
+
mcp_config: &mcp_config(&candidate, &workspace, "leader"),
|
|
361
|
+
team_id: "current",
|
|
362
|
+
agent_id: "leader",
|
|
363
|
+
workspace: &workspace,
|
|
364
|
+
include_tools: &["assign_task", "send_message"],
|
|
365
|
+
})
|
|
366
|
+
.expect("materialize Pi leader wrapper from an empty Unicode workspace");
|
|
367
|
+
|
|
368
|
+
assert_eq!(written, paths.wrapper);
|
|
369
|
+
assert!(written.is_file(), "published leader wrapper must exist");
|
|
370
|
+
assert!(
|
|
371
|
+
paths.runtime_root.is_dir(),
|
|
372
|
+
"leader runtime root must be created recursively"
|
|
373
|
+
);
|
|
374
|
+
let source = std::fs::read_to_string(&written).expect("read complete leader wrapper");
|
|
375
|
+
validate_pi_wrapper_source(&source, &adapter, &candidate)
|
|
376
|
+
.expect("published leader wrapper must be complete and valid");
|
|
377
|
+
assert!(source.contains("TEAM_AGENT_ID") && source.contains("leader"));
|
|
378
|
+
assert!(source.contains("TEAM_AGENT_OWNER_TEAM_ID") && source.contains("current"));
|
|
379
|
+
std::fs::remove_dir_all(root).expect("remove Unicode leader fixture");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#[test]
|
|
383
|
+
fn pi_wrapper_permissions_are_finalized_before_atomic_publish() {
|
|
384
|
+
let source = include_str!("../launch/pi_mcp.rs");
|
|
385
|
+
let write_start = source
|
|
386
|
+
.find("pub(crate) fn write_pi_wrapper")
|
|
387
|
+
.expect("write_pi_wrapper source");
|
|
388
|
+
let write_source = &source[write_start..];
|
|
389
|
+
let permissions = write_source
|
|
390
|
+
.find("file.set_permissions")
|
|
391
|
+
.expect("wrapper permissions must use the open temp-file handle");
|
|
392
|
+
let publish = write_source
|
|
393
|
+
.find("publish(&temp, request.destination)")
|
|
394
|
+
.expect("atomic wrapper publish");
|
|
395
|
+
|
|
396
|
+
assert!(
|
|
397
|
+
permissions < publish,
|
|
398
|
+
"DrvFS may not resolve the destination immediately after rename; finalize mode through the open temp-file handle before publishing"
|
|
399
|
+
);
|
|
400
|
+
assert!(
|
|
401
|
+
!write_source.contains("std::fs::set_permissions(request.destination"),
|
|
402
|
+
"atomic publish must not require a second destination-path lookup"
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#[test]
|
|
407
|
+
fn pi_wrapper_publish_needs_no_final_path_metadata_lookup() {
|
|
408
|
+
let root = temp_root("publish-path-enoent");
|
|
409
|
+
let workspace = root.join("mnt/f/测试基础设施");
|
|
410
|
+
let candidate = root.join("candidate/team-agent");
|
|
411
|
+
std::fs::create_dir_all(&workspace).expect("create Unicode workspace");
|
|
412
|
+
std::fs::create_dir_all(candidate.parent().expect("candidate parent"))
|
|
413
|
+
.expect("create candidate parent");
|
|
414
|
+
std::fs::write(&candidate, b"candidate").expect("write candidate fixture");
|
|
415
|
+
|
|
416
|
+
let paths = pi_seat_paths(&workspace, "current", "leader");
|
|
417
|
+
let published_parent = paths.runtime_root.clone();
|
|
418
|
+
let hidden_parent = published_parent.with_file_name("leader-after-rename");
|
|
419
|
+
let adapter = adapter_identity(&root);
|
|
420
|
+
write_pi_wrapper_with_publish(
|
|
421
|
+
PiWrapperRequest {
|
|
422
|
+
destination: &paths.wrapper,
|
|
423
|
+
adapter: &adapter,
|
|
424
|
+
candidate_executable: &candidate,
|
|
425
|
+
mcp_config: &mcp_config(&candidate, &workspace, "leader"),
|
|
426
|
+
team_id: "current",
|
|
427
|
+
agent_id: "leader",
|
|
428
|
+
workspace: &workspace,
|
|
429
|
+
include_tools: &["assign_task", "send_message"],
|
|
430
|
+
},
|
|
431
|
+
|temp, destination| {
|
|
432
|
+
std::fs::rename(temp, destination)?;
|
|
433
|
+
std::fs::rename(&published_parent, &hidden_parent)?;
|
|
434
|
+
assert_eq!(
|
|
435
|
+
std::fs::metadata(destination)
|
|
436
|
+
.expect_err("simulate DrvFS final-path metadata ENOENT after rename")
|
|
437
|
+
.kind(),
|
|
438
|
+
std::io::ErrorKind::NotFound
|
|
439
|
+
);
|
|
440
|
+
Ok(())
|
|
441
|
+
},
|
|
442
|
+
)
|
|
443
|
+
.expect("successful publish must not perform final-path metadata operations");
|
|
444
|
+
|
|
445
|
+
let published = hidden_parent.join("team-mcp.ts");
|
|
446
|
+
assert!(published.is_file(), "rename published a complete wrapper");
|
|
447
|
+
assert_eq!(
|
|
448
|
+
std::fs::metadata(&published)
|
|
449
|
+
.expect("published wrapper metadata through converged path")
|
|
450
|
+
.permissions()
|
|
451
|
+
.mode()
|
|
452
|
+
& 0o777,
|
|
453
|
+
0o600
|
|
454
|
+
);
|
|
455
|
+
std::fs::remove_dir_all(root).expect("remove publish-path fixture");
|
|
456
|
+
}
|
|
457
|
+
|
|
339
458
|
#[test]
|
|
340
459
|
fn pi_wrapper_runtime_registration_does_not_copy_ambient_config() {
|
|
341
460
|
run_process_isolated(PI_WRAPPER_AMBIENT_CHILD, PI_WRAPPER_AMBIENT_TEST, || {
|
|
@@ -495,6 +495,14 @@ pub enum LifecycleError {
|
|
|
495
495
|
/// 编译 spec / role doc 失败(`compile_team`/`compile_role_doc_agent`)。
|
|
496
496
|
#[error("spec compile failed: {0}")]
|
|
497
497
|
Compile(String),
|
|
498
|
+
/// Pi model/schema admission failed before lifecycle reservation or writes.
|
|
499
|
+
#[error("pi model preflight failed: {requested}")]
|
|
500
|
+
PiModelPreflight {
|
|
501
|
+
requested: String,
|
|
502
|
+
candidates: Vec<String>,
|
|
503
|
+
action: String,
|
|
504
|
+
not_ready: bool,
|
|
505
|
+
},
|
|
498
506
|
/// provider 命令构造 / resume 不可用(`ResumeUnavailable`)。
|
|
499
507
|
#[error("provider error: {0}")]
|
|
500
508
|
Provider(String),
|