@@ -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,118 @@ 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 :: default ( ) ;
1417+ config. config_path = root. join ( "config.toml" ) ;
1418+ config. data_dir = root. join ( "data" ) ;
1419+ std:: fs:: create_dir_all ( & config. data_dir ) . unwrap ( ) ;
1420+ config. agents . clear ( ) ;
1421+ config
1422+ }
1423+
1424+ fn add_enabled_agent ( config : & mut Config , alias : & str ) {
1425+ config. agents . insert (
1426+ alias. to_string ( ) ,
1427+ zeroclaw_config:: schema:: AliasedAgentConfig {
1428+ enabled : true ,
1429+ ..Default :: default ( )
1430+ } ,
1431+ ) ;
1432+ }
1433+
1434+ #[ test]
1435+ fn check_workspace_finds_soul_in_agent_workspace_not_data_dir ( ) {
1436+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
1437+ let mut config = workspace_test_config ( tmp. path ( ) ) ;
1438+ add_enabled_agent ( & mut config, "default" ) ;
1439+
1440+ // SOUL.md lives in the agent workspace — the real load location.
1441+ let ws = config. agent_workspace_dir ( "default" ) ;
1442+ std:: fs:: create_dir_all ( & ws) . unwrap ( ) ;
1443+ std:: fs:: write ( ws. join ( "SOUL.md" ) , b"# soul" ) . unwrap ( ) ;
1444+ // A decoy in data_dir must NOT satisfy the check (proves we don't
1445+ // probe data_dir for personality files).
1446+ std:: fs:: write ( config. data_dir . join ( "SOUL.md" ) , b"# decoy" ) . unwrap ( ) ;
1447+
1448+ let mut items = Vec :: new ( ) ;
1449+ check_workspace ( & config, & mut items) ;
1450+
1451+ let soul = items
1452+ . iter ( )
1453+ . find ( |i| i. message . contains ( "SOUL.md" ) )
1454+ . expect ( "SOUL.md diagnostic present" ) ;
1455+ assert_eq ! ( soul. severity, Severity :: Ok ) ;
1456+ assert_eq ! ( soul. message, "[default] SOUL.md present" ) ;
1457+ // No bare data_dir-style message ever surfaces.
1458+ assert ! (
1459+ !items. iter( ) . any( |i| i. message == "SOUL.md present" ) ,
1460+ "doctor must not report SOUL.md from data_dir"
1461+ ) ;
1462+ }
1463+
1464+ #[ test]
1465+ fn check_workspace_warns_when_agent_soul_missing ( ) {
1466+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
1467+ let mut config = workspace_test_config ( tmp. path ( ) ) ;
1468+ add_enabled_agent ( & mut config, "default" ) ;
1469+ // Workspace dir need not exist; the file simply isn't there.
1470+
1471+ let mut items = Vec :: new ( ) ;
1472+ check_workspace ( & config, & mut items) ;
1473+
1474+ let soul = items
1475+ . iter ( )
1476+ . find ( |i| i. message . contains ( "SOUL.md" ) )
1477+ . expect ( "SOUL.md diagnostic present" ) ;
1478+ assert_eq ! ( soul. severity, Severity :: Warn ) ;
1479+ assert_eq ! ( soul. message, "[default] SOUL.md not found (optional)" ) ;
1480+ }
1481+
1482+ #[ test]
1483+ fn check_workspace_skips_disabled_agents ( ) {
1484+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
1485+ let mut config = workspace_test_config ( tmp. path ( ) ) ;
1486+ config. agents . insert (
1487+ "dormant" . to_string ( ) ,
1488+ zeroclaw_config:: schema:: AliasedAgentConfig {
1489+ enabled : false ,
1490+ ..Default :: default ( )
1491+ } ,
1492+ ) ;
1493+
1494+ let mut items = Vec :: new ( ) ;
1495+ check_workspace ( & config, & mut items) ;
1496+
1497+ assert ! (
1498+ !items. iter( ) . any( |i| i. message. contains( "dormant" ) ) ,
1499+ "disabled agents must not produce workspace-file diagnostics"
1500+ ) ;
1501+ }
1502+
1503+ #[ test]
1504+ fn check_workspace_checks_each_enabled_agent ( ) {
1505+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
1506+ let mut config = workspace_test_config ( tmp. path ( ) ) ;
1507+ add_enabled_agent ( & mut config, "alpha" ) ;
1508+ add_enabled_agent ( & mut config, "zeta" ) ;
1509+
1510+ let mut items = Vec :: new ( ) ;
1511+ check_workspace ( & config, & mut items) ;
1512+
1513+ // Each enabled agent gets its own SOUL.md + AGENTS.md probe, named.
1514+ let messages: Vec < & str > = items. iter ( ) . map ( |i| i. message . as_str ( ) ) . collect ( ) ;
1515+ for alias in [ "alpha" , "zeta" ] {
1516+ let expected = format ! ( "[{alias}] SOUL.md not found (optional)" ) ;
1517+ assert ! (
1518+ messages. contains( & expected. as_str( ) ) ,
1519+ "expected per-agent SOUL.md diagnostic for {alias}; got {messages:?}"
1520+ ) ;
1521+ }
1522+ }
1523+
13941524 #[ test]
13951525 fn diagnose_flags_web_dist_dir_with_tilde ( ) {
13961526 // Asserts the localized Fluent message resolves and inlines the path +
0 commit comments