@team-agent/installer 0.5.71 → 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 +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 +33 -9
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +169 -10
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +76 -2
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +859 -12
- 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/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
|
@@ -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.
|
|
@@ -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),
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Resolve local Markdown links and optionally validate package/installed inventories. */
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const value = (name) => { const i = args.indexOf(name); return i < 0 ? null : args[i + 1]; };
|
|
8
|
+
const rootArg = value("--root") ?? "skills/team-agent";
|
|
9
|
+
const root = path.resolve(rootArg);
|
|
10
|
+
const installedArg = value("--installed");
|
|
11
|
+
const inventoryArg = value("--inventory");
|
|
12
|
+
const markdownLink = /!?(?:\[[^\]]*\])\(([^)]+)\)/g;
|
|
13
|
+
const files = [];
|
|
14
|
+
const fail = (message) => { throw new Error(message); };
|
|
15
|
+
const real = (file) => { try { return fs.realpathSync.native(file); } catch (error) { fail(`cannot resolve ${file}: ${error.message}`); } };
|
|
16
|
+
const rootReal = real(root);
|
|
17
|
+
const inside = (candidate, base = rootReal) => candidate === base || candidate.startsWith(`${base}${path.sep}`);
|
|
18
|
+
const lexicalInside = (candidate, base = root) => candidate === base || candidate.startsWith(`${base}${path.sep}`);
|
|
19
|
+
|
|
20
|
+
function walk(dir, boundary = rootReal) {
|
|
21
|
+
let entries;
|
|
22
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)); }
|
|
23
|
+
catch (error) { fail(`cannot read ${dir}: ${error.message}`); }
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const file = path.join(dir, entry.name);
|
|
26
|
+
const fileReal = real(file);
|
|
27
|
+
if (!inside(fileReal, boundary)) fail(`${file}: realpath escapes tree`);
|
|
28
|
+
if (entry.isDirectory()) walk(file, boundary);
|
|
29
|
+
else if (entry.isFile() && file.endsWith(".md")) files.push(file);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function normalizeFragment(fragment) {
|
|
33
|
+
return decodeURIComponent(fragment).toLowerCase().trim().replace(/<[^>]+>/g, "")
|
|
34
|
+
.replace(/[`*_~]/g, "").replace(/[^\p{L}\p{N} -]/gu, "").replace(/\s+/g, "-");
|
|
35
|
+
}
|
|
36
|
+
function anchors(text) {
|
|
37
|
+
const found = new Set();
|
|
38
|
+
const used = new Map();
|
|
39
|
+
const add = (value) => {
|
|
40
|
+
const base = normalizeFragment(value); if (!base) return;
|
|
41
|
+
const n = used.get(base) ?? 0; used.set(base, n + 1); found.add(n ? `${base}-${n}` : base);
|
|
42
|
+
};
|
|
43
|
+
for (const m of text.matchAll(/^(?: {0,3})#{1,6}\s+(.+?)\s*#*\s*$/gm) ) add(m[1]);
|
|
44
|
+
for (const m of text.matchAll(/^(?: {0,3})([^\n#]+)\n(?:=+|-+)\s*$/gm)) add(m[1]);
|
|
45
|
+
for (const m of text.matchAll(/<(?:a|[^>]+)\s+[^>]*(?:id|name)=["']([^"']+)["'][^>]*>/gi)) add(m[1]);
|
|
46
|
+
return found;
|
|
47
|
+
}
|
|
48
|
+
function checkTree(dir, baseRoot, baseReal) {
|
|
49
|
+
files.length = 0; walk(dir, baseReal);
|
|
50
|
+
const errors = [];
|
|
51
|
+
for (const file of [...files].sort()) {
|
|
52
|
+
const text = fs.readFileSync(file, "utf8");
|
|
53
|
+
const known = anchors(text);
|
|
54
|
+
for (const match of text.matchAll(markdownLink)) {
|
|
55
|
+
const href = match[1].trim();
|
|
56
|
+
if (!href || /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href)) continue;
|
|
57
|
+
const hash = href.indexOf("#");
|
|
58
|
+
const rawPath = hash < 0 ? href : href.slice(0, hash);
|
|
59
|
+
const fragment = hash < 0 ? "" : href.slice(hash + 1).split("?", 1)[0];
|
|
60
|
+
const target = rawPath ? path.resolve(path.dirname(file), rawPath) : file;
|
|
61
|
+
if (!lexicalInside(target, baseRoot)) { errors.push(`${file}: lexical target escapes tree: ${href}`); continue; }
|
|
62
|
+
const targetReal = real(target);
|
|
63
|
+
if (!inside(targetReal, baseReal)) { errors.push(`${file}: realpath target escapes tree: ${href}`); continue; }
|
|
64
|
+
if (!fs.statSync(targetReal).isFile()) { errors.push(`${file}: target is not a file: ${href}`); continue; }
|
|
65
|
+
if (fragment && targetReal.endsWith(".md")) {
|
|
66
|
+
const targetAnchors = targetReal === real(file) ? known : anchors(fs.readFileSync(targetReal, "utf8"));
|
|
67
|
+
if (!targetAnchors.has(normalizeFragment(fragment))) errors.push(`${file}: missing fragment: ${href}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (errors.length) fail(errors.sort().join("\n"));
|
|
72
|
+
return [...files].sort().length;
|
|
73
|
+
}
|
|
74
|
+
const count = checkTree(root, root, rootReal);
|
|
75
|
+
if (inventoryArg) {
|
|
76
|
+
const inventory = path.resolve(inventoryArg);
|
|
77
|
+
for (const relative of fs.readFileSync(inventory, "utf8").split(/\r?\n/).filter(Boolean).sort()) {
|
|
78
|
+
if (path.isAbsolute(relative)) fail(`inventory path must be relative: ${relative}`);
|
|
79
|
+
const candidate = path.resolve(root, relative);
|
|
80
|
+
if (!lexicalInside(candidate)) fail(`inventory path escapes tree: ${relative}`);
|
|
81
|
+
const candidateReal = real(candidate);
|
|
82
|
+
if (!inside(candidateReal)) fail(`inventory realpath escapes tree: ${relative}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (installedArg) {
|
|
86
|
+
const installed = path.resolve(installedArg); console.log(`checked ${checkTree(installed, installed, real(installed))} Markdown files in ${installed}`);
|
|
87
|
+
}
|
|
88
|
+
console.log(`checked ${count} Markdown files in ${root}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.72",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.72",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.72",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.72"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"files": [
|
|
34
34
|
"npm/bincheck.mjs",
|
|
35
35
|
"npm/install.mjs",
|
|
36
|
+
"npm/check-skill-links.mjs",
|
|
36
37
|
"skills",
|
|
37
38
|
"templates",
|
|
38
39
|
"examples",
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
name: team-agent
|
|
3
3
|
description: Use only when the user explicitly asks to start, operate, inspect, shutdown, or restart a Team Agent team. Treat the team-agent CLI as a sealed appliance.
|
|
4
4
|
requires_team_agent: ">=0.5.0"
|
|
5
|
-
last_verified_against: "0.5.
|
|
5
|
+
last_verified_against: "0.5.72"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Team Agent
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Use this skill only for Team Agent operation. The detailed operator handbook (permissions, models, routing, and recovery) is in [the operator reference](references/team-agent-operator.md). Nested teams have a separate [nested-team reference](references/team-in-team.md).
|
|
11
11
|
|
|
12
|
-
If `team-agent --version` does not match `last_verified_against`,
|
|
12
|
+
If `team-agent --version` does not match `last_verified_against`, inspect the live CLI before copying examples:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
team-agent --version
|
|
@@ -17,55 +17,21 @@ team-agent --help
|
|
|
17
17
|
team-agent doctor --help
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
## Pi TeamMate startup
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
TO has two co-equal logical forms: an **in-team short name** (`team-agent send reviewer "..."`) and a fully qualified `<workspace>::<team>/<agent>`. Use the qualified form across workspaces.
|
|
24
|
-
- Inspect: `team-agent status` / `status --json`. `ok: true` plus `ready: false` is not a crash.
|
|
25
|
-
- Lifecycle: `restart .` resumes a stopped team; `add-agent NAME --role-file FILE` adds or `--force` recreates one worker; `shutdown --workspace .` stops. Do not shutdown the whole team to add a worker.
|
|
26
|
-
- Roles: every `agents/*.md` must declare boolean `dangerously_skip_permissions`. Never rewrite a user-supplied model id. Never read `.env` files.
|
|
22
|
+
For a Pi worker, use this existing three-step flow:
|
|
27
23
|
|
|
28
|
-
|
|
24
|
+
1. Discover exact model ids: `team-agent models --provider pi` (add `--search TEXT` when useful).
|
|
25
|
+
2. Use the existing lifecycle command that matches the actual state: `team-agent quick-start` for a new team, `team-agent add-agent NAME --role-file FILE` for a new worker, or `team-agent start-agent NAME` for a missing worker.
|
|
26
|
+
3. Send work through the canonical route: `team-agent send AGENT MESSAGE`.
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
Pi roles preserve the direct Pi login, model defaults, extensions/plugins, skills, context, trust, and tools. Team Agent adds only per-seat identity/session isolation. `model` and `effort` are optional explicit pass-throughs. Use the shared role schema, a block-style `tools` list containing `mcp_team`, and the required boolean `dangerously_skip_permissions`.
|
|
31
29
|
|
|
32
|
-
##
|
|
30
|
+
## Operate
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
- Send: `team-agent send TO MESSAGE` using an in-team short name or fully qualified `<workspace>::<team>/<agent>`; these are co-equal positional TO forms. For example, `team-agent send reviewer "..."` uses an in-team short name. Do not poll after success.
|
|
33
|
+
- Inspect: `team-agent status` or `team-agent status --json`; `ok: true` with `ready: false` is not a crash.
|
|
34
|
+
- On a structured failure with an `action`, run that action first, then stop. Never guess flags.
|
|
35
|
+
- Never rewrite a user-supplied model id or read `.env` files.
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|---|---|---|---|---|
|
|
38
|
-
| `grok` | yes (`--resume <id>`, archive-gated) | no | yes (role `model` required) | yes (`--fork-session` + new `--session-id`) |
|
|
39
|
-
| `cursor_agent` | yes (argv `--resume <chatId>`, archive-gated) | no | required on role; same-family catalog id can take effect; unknown id silent-fallback; pane chrome ≠ proven live | **no — `CapabilityUnsupported`** |
|
|
40
|
-
|
|
41
|
-
Grok / `cursor_agent` have no JSONL turn-state reader (classify → Unknown).
|
|
42
|
-
|
|
43
|
-
## Provider Prep
|
|
44
|
-
|
|
45
|
-
### Pi TeamMate roles
|
|
46
|
-
|
|
47
|
-
Use the standard role schema with `provider: pi`, a block-style `tools` list containing `mcp_team`, and the required boolean `dangerously_skip_permissions`. That role boolean is the only bypass setting; `TEAM.md` `dangerous_auto_approve` is compatibility input, not a second user-facing switch.
|
|
48
|
-
|
|
49
|
-
Pi TeamMates keep the same login, model, extensions/plugins, skills, context files, project trust, and tools as a direct `pi` run on that machine. Team Agent only appends the role contract, a per-seat Team MCP registration, and per-seat session storage; do not duplicate those direct Pi settings in the role. `model` and `effort` remain optional compatibility overrides, and explicit values are passed through.
|
|
50
|
-
|
|
51
|
-
### Cursor provider notes
|
|
52
|
-
|
|
53
|
-
Frontmatter: `provider: cursor_agent` (not `cursor`; launcher verb is `team-agent cursor`), `auth_mode: subscription`, `name:` required (omit → `missing front matter field name`). Also required: `role:`, `tools:`, `dangerously_skip_permissions:` (bool). Subscription needs no `profile`.
|
|
54
|
-
|
|
55
|
-
`model:` is required (omit compile-fails; blocks a silent builtin `sonnet-4-thinking`). The flag stays on argv. Same-family catalog ids can change pane chrome. An id not in that provider's catalog silent-falls back (landing not stable; no events/stderr/pane warning). Pick names from the catalog; after spawn, `capture-pane` once for chrome. Do not treat the role field or pane chrome as proof of the live model.
|
|
56
|
-
|
|
57
|
-
One `cursor_agent` per **provider-config overlay** under `<workspace>/.team/runtime/provider-config/<id>/cursor`. Same workspace may run multiple cursor seats; each writes `TEAM_AGENT_ID` into that project's `.cursor/mcp.json`. Spawn points `--workspace` there and `--add-dir` at the real workspace. `HOME` stays the user home. If isolation is off or cannot be materialized, adding a second seat still fail-closes:
|
|
58
|
-
|
|
59
|
-
```
|
|
60
|
-
error: cursor_agent seat already occupies this workspace
|
|
61
|
-
reason: <workspace>/.cursor/mcp.json is directory-scoped; a second seat overwrites TEAM_AGENT_ID (last-writer)
|
|
62
|
-
action: do not add another CursorAgent in this workspace until per-seat MCP identity is isolated
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Do not share `<workspace>/.cursor/mcp.json` for team_orchestrator identity. Same seat, fresh context → `reset-agent --discard-session`.
|
|
66
|
-
|
|
67
|
-
`clone-agent` copies the source role (provider unchanged). Runtime add: `clone-agent` → `stop-agent` → `remove-agent --confirm` (deletes `.team/dynamic-role-files/`) → write the role file → `add-agent --role-file` → dispatch.
|
|
68
|
-
|
|
69
|
-
- Restart emits `--resume <chatId>` when `store.db`/`meta.json` exist; the gate does not read chat text. Persist anything that must survive restart.
|
|
70
|
-
- Delivery sends one Enter; a second Enter interrupts the turn.
|
|
71
|
-
- After spawn, the pane footer should show `Cursor Agent v<version>`. Do not use `strings` to probe the binary.
|
|
37
|
+
Workers report completion through `mcp_team` and call `report_result` exactly once.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"authority": {
|
|
4
4
|
"kind": "normative_handbook_plus_live_help",
|
|
5
5
|
"handbook": {
|
|
6
|
-
"path": "
|
|
6
|
+
"path": "skills/team-agent/references/team-agent-operator.md",
|
|
7
7
|
"start_marker": "<!-- command-coverage:normative-start -->",
|
|
8
8
|
"end_marker": "<!-- command-coverage:normative-end -->"
|
|
9
9
|
},
|
|
@@ -336,6 +336,13 @@
|
|
|
336
336
|
"owner": "runtime-owner:messaging",
|
|
337
337
|
"plan": "Execute the compact-skill positional send smoke with representative target and message tokens, then bind delivery truth."
|
|
338
338
|
},
|
|
339
|
+
{
|
|
340
|
+
"command": "team-agent send AGENT MESSAGE",
|
|
341
|
+
"bucket": "declared_gap",
|
|
342
|
+
"covered": false,
|
|
343
|
+
"owner": "runtime-owner:messaging",
|
|
344
|
+
"plan": "Execute a representative positional send with explicit agent and message tokens, then bind recipient and delivery truth."
|
|
345
|
+
},
|
|
339
346
|
{
|
|
340
347
|
"command": "team-agent send reviewer \"...\"",
|
|
341
348
|
"bucket": "declared_gap",
|