@@ -832,25 +832,43 @@ fn check_workspace(config: &Config, items: &mut Vec<DiagItem>) {
832832 }
833833 }
834834
835- // Key workspace files
836- check_file_exists ( ws, "SOUL.md" , false , cat, items) ;
837- check_file_exists ( ws, "AGENTS.md" , false , cat, items) ;
835+ // Per-agent personality files. These are resolved per agent from
836+ // `<install>/agents/<alias>/workspace/` (or an explicit
837+ // `[agents.<alias>.workspace.path]` override) — never from `data_dir`.
838+ // Iterate every enabled agent so multi-agent installs each get checked,
839+ // and name the alias in the result so the report is unambiguous. Sorted
840+ // for deterministic output (HashMap iteration order is unspecified).
841+ let mut agent_aliases: Vec < & String > = config. agents . keys ( ) . collect ( ) ;
842+ agent_aliases. sort ( ) ;
843+ for alias in agent_aliases {
844+ let agent = config. agents . get ( alias) . expect ( "alias from keys()" ) ;
845+ if !agent. enabled {
846+ continue ;
847+ }
848+ let agent_ws = config. agent_workspace_dir ( alias) ;
849+ check_agent_file ( & agent_ws, "SOUL.md" , alias, cat, items) ;
850+ check_agent_file ( & agent_ws, "AGENTS.md" , alias, cat, items) ;
851+ }
838852}
839853
840- fn check_file_exists (
841- base : & Path ,
854+ /// Existence check for an optional per-agent workspace file. Prefixes the
855+ /// owning agent alias as `[alias]` so a multi-agent report stays legible and
856+ /// `(optional)` keeps its single, consistent meaning as the severity hint
857+ /// (e.g. `[default] SOUL.md present`, `[default] AGENTS.md not found (optional)`).
858+ fn check_agent_file (
859+ workspace_dir : & Path ,
842860 name : & str ,
843- required : bool ,
861+ alias : & str ,
844862 cat : & ' static str ,
845863 items : & mut Vec < DiagItem > ,
846864) {
847- let path = base. join ( name) ;
848- if path. is_file ( ) {
849- items. push ( DiagItem :: ok ( cat, format ! ( "{name} present" ) ) ) ;
850- } else if required {
851- items. push ( DiagItem :: error ( cat, format ! ( "{name} missing" ) ) ) ;
865+ if workspace_dir. join ( name) . is_file ( ) {
866+ items. push ( DiagItem :: ok ( cat, format ! ( "[{alias}] {name} present" ) ) ) ;
852867 } else {
853- items. push ( DiagItem :: warn ( cat, format ! ( "{name} not found (optional)" ) ) ) ;
868+ items. push ( DiagItem :: warn (
869+ cat,
870+ format ! ( "[{alias}] {name} not found (optional)" ) ,
871+ ) ) ;
854872 }
855873}
856874
@@ -1391,6 +1409,120 @@ mod tests {
13911409 ) ;
13921410 }
13931411
1412+ /// Build a Config whose install root is `root`, with an existing
1413+ /// `data_dir` (so `check_workspace` doesn't early-return) and no agents.
1414+ /// `config_path` anchors `install_root_dir()` → `agent_workspace_dir()`.
1415+ fn workspace_test_config ( root : & Path ) -> Config {
1416+ let mut config = Config {
1417+ config_path : root. join ( "config.toml" ) ,
1418+ data_dir : root. join ( "data" ) ,
1419+ ..Config :: default ( )
1420+ } ;
1421+ std:: fs:: create_dir_all ( & config. data_dir ) . unwrap ( ) ;
1422+ config. agents . clear ( ) ;
1423+ config
1424+ }
1425+
1426+ fn add_enabled_agent ( config : & mut Config , alias : & str ) {
1427+ config. agents . insert (
1428+ alias. to_string ( ) ,
1429+ zeroclaw_config:: schema:: AliasedAgentConfig {
1430+ enabled : true ,
1431+ ..Default :: default ( )
1432+ } ,
1433+ ) ;
1434+ }
1435+
1436+ #[ test]
1437+ fn check_workspace_finds_soul_in_agent_workspace_not_data_dir ( ) {
1438+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
1439+ let mut config = workspace_test_config ( tmp. path ( ) ) ;
1440+ add_enabled_agent ( & mut config, "default" ) ;
1441+
1442+ // SOUL.md lives in the agent workspace — the real load location.
1443+ let ws = config. agent_workspace_dir ( "default" ) ;
1444+ std:: fs:: create_dir_all ( & ws) . unwrap ( ) ;
1445+ std:: fs:: write ( ws. join ( "SOUL.md" ) , b"# soul" ) . unwrap ( ) ;
1446+ // A decoy in data_dir must NOT satisfy the check (proves we don't
1447+ // probe data_dir for personality files).
1448+ std:: fs:: write ( config. data_dir . join ( "SOUL.md" ) , b"# decoy" ) . unwrap ( ) ;
1449+
1450+ let mut items = Vec :: new ( ) ;
1451+ check_workspace ( & config, & mut items) ;
1452+
1453+ let soul = items
1454+ . iter ( )
1455+ . find ( |i| i. message . contains ( "SOUL.md" ) )
1456+ . expect ( "SOUL.md diagnostic present" ) ;
1457+ assert_eq ! ( soul. severity, Severity :: Ok ) ;
1458+ assert_eq ! ( soul. message, "[default] SOUL.md present" ) ;
1459+ // No bare data_dir-style message ever surfaces.
1460+ assert ! (
1461+ !items. iter( ) . any( |i| i. message == "SOUL.md present" ) ,
1462+ "doctor must not report SOUL.md from data_dir"
1463+ ) ;
1464+ }
1465+
1466+ #[ test]
1467+ fn check_workspace_warns_when_agent_soul_missing ( ) {
1468+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
1469+ let mut config = workspace_test_config ( tmp. path ( ) ) ;
1470+ add_enabled_agent ( & mut config, "default" ) ;
1471+ // Workspace dir need not exist; the file simply isn't there.
1472+
1473+ let mut items = Vec :: new ( ) ;
1474+ check_workspace ( & config, & mut items) ;
1475+
1476+ let soul = items
1477+ . iter ( )
1478+ . find ( |i| i. message . contains ( "SOUL.md" ) )
1479+ . expect ( "SOUL.md diagnostic present" ) ;
1480+ assert_eq ! ( soul. severity, Severity :: Warn ) ;
1481+ assert_eq ! ( soul. message, "[default] SOUL.md not found (optional)" ) ;
1482+ }
1483+
1484+ #[ test]
1485+ fn check_workspace_skips_disabled_agents ( ) {
1486+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
1487+ let mut config = workspace_test_config ( tmp. path ( ) ) ;
1488+ config. agents . insert (
1489+ "dormant" . to_string ( ) ,
1490+ zeroclaw_config:: schema:: AliasedAgentConfig {
1491+ enabled : false ,
1492+ ..Default :: default ( )
1493+ } ,
1494+ ) ;
1495+
1496+ let mut items = Vec :: new ( ) ;
1497+ check_workspace ( & config, & mut items) ;
1498+
1499+ assert ! (
1500+ !items. iter( ) . any( |i| i. message. contains( "dormant" ) ) ,
1501+ "disabled agents must not produce workspace-file diagnostics"
1502+ ) ;
1503+ }
1504+
1505+ #[ test]
1506+ fn check_workspace_checks_each_enabled_agent ( ) {
1507+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
1508+ let mut config = workspace_test_config ( tmp. path ( ) ) ;
1509+ add_enabled_agent ( & mut config, "alpha" ) ;
1510+ add_enabled_agent ( & mut config, "zeta" ) ;
1511+
1512+ let mut items = Vec :: new ( ) ;
1513+ check_workspace ( & config, & mut items) ;
1514+
1515+ // Each enabled agent gets its own SOUL.md + AGENTS.md probe, named.
1516+ let messages: Vec < & str > = items. iter ( ) . map ( |i| i. message . as_str ( ) ) . collect ( ) ;
1517+ for alias in [ "alpha" , "zeta" ] {
1518+ let expected = format ! ( "[{alias}] SOUL.md not found (optional)" ) ;
1519+ assert ! (
1520+ messages. contains( & expected. as_str( ) ) ,
1521+ "expected per-agent SOUL.md diagnostic for {alias}; got {messages:?}"
1522+ ) ;
1523+ }
1524+ }
1525+
13941526 #[ test]
13951527 fn diagnose_flags_web_dist_dir_with_tilde ( ) {
13961528 // Asserts the localized Fluent message resolves and inlines the path +
0 commit comments