@@ -12,6 +12,7 @@ import * as path from "node:path"
1212import { existsSync , readFileSync } from "node:fs"
1313import { resolvePackageRoot } from "../../../../shared/runtime/root.js"
1414import { getAllInstalledImports } from "../../../../shared/imports/registry.js"
15+ import { debug } from "../../../../shared/logging/logger.js"
1516import type { ActivityType , AgentCharactersConfig , Persona } from "./agent-characters.types.js"
1617
1718let cachedConfig : AgentCharactersConfig | null = null
@@ -30,14 +31,44 @@ function getPackageRoot(): string | null {
3031/**
3132 * Merges two agent characters configs
3233 * Imported config extends/overrides the base config
34+ * @param packageName - If provided, namespace agent IDs with this package name
3335 */
3436function mergeCharactersConfigs (
3537 base : AgentCharactersConfig ,
36- imported : AgentCharactersConfig
38+ imported : Partial < AgentCharactersConfig > ,
39+ packageName ?: string
3740) : AgentCharactersConfig {
41+ // Namespace agent IDs if from an imported package
42+ const namespacedAgents : Record < string , string > = { }
43+ if ( imported . agents ) {
44+ for ( const [ agentId , personaName ] of Object . entries ( imported . agents ) ) {
45+ const namespacedId = packageName ? `${ packageName } :${ agentId } ` : agentId
46+ namespacedAgents [ namespacedId ] = personaName
47+ }
48+ }
49+
50+ // Namespace persona names if from an imported package
51+ const namespacedPersonas : Record < string , Persona > = { }
52+ if ( imported . personas ) {
53+ for ( const [ personaName , persona ] of Object . entries ( imported . personas ) ) {
54+ const namespacedName = packageName ? `${ packageName } :${ personaName } ` : personaName
55+ namespacedPersonas [ namespacedName ] = persona
56+ }
57+ }
58+
59+ // Update agent mappings to use namespaced persona names
60+ const finalAgents : Record < string , string > = { ...base . agents }
61+ for ( const [ agentId , personaName ] of Object . entries ( namespacedAgents ) ) {
62+ // If persona was namespaced AND the persona exists in imported, update the mapping
63+ // Otherwise keep the original persona name (allows referencing base personas)
64+ const namespacedPersonaName = packageName ? `${ packageName } :${ personaName } ` : personaName
65+ const finalPersonaName = imported . personas ?. [ personaName ] ? namespacedPersonaName : personaName
66+ finalAgents [ agentId ] = finalPersonaName
67+ }
68+
3869 return {
39- personas : { ...base . personas , ...imported . personas } ,
40- agents : { ... base . agents , ... imported . agents } ,
70+ personas : { ...base . personas , ...namespacedPersonas } ,
71+ agents : finalAgents ,
4172 defaultPersona : base . defaultPersona , // Keep base default
4273 }
4374}
@@ -47,13 +78,17 @@ function mergeCharactersConfigs(
4778 */
4879function loadCharactersFromPath ( configPath : string ) : AgentCharactersConfig | null {
4980 if ( ! existsSync ( configPath ) ) {
81+ debug ( '[Characters] loadCharactersFromPath: file does not exist: %s' , configPath )
5082 return null
5183 }
5284
5385 try {
5486 const content = readFileSync ( configPath , "utf-8" )
55- return JSON . parse ( content ) as AgentCharactersConfig
56- } catch {
87+ const parsed = JSON . parse ( content ) as AgentCharactersConfig
88+ debug ( '[Characters] loadCharactersFromPath: successfully loaded %s' , configPath )
89+ return parsed
90+ } catch ( err ) {
91+ debug ( '[Characters] loadCharactersFromPath: parse error for %s: %s' , configPath , err )
5792 return null
5893 }
5994}
@@ -68,6 +103,8 @@ export function loadAgentCharactersConfig(): AgentCharactersConfig {
68103 return cachedConfig
69104 }
70105
106+ debug ( '[Characters] loadAgentCharactersConfig: loading fresh config...' )
107+
71108 // Start with default config
72109 let config = getDefaultConfig ( )
73110
@@ -78,22 +115,47 @@ export function loadAgentCharactersConfig(): AgentCharactersConfig {
78115 const baseConfig = loadCharactersFromPath ( basePath )
79116 if ( baseConfig ) {
80117 config = baseConfig
118+ debug ( '[Characters] Loaded base config from %s, personas=%o, agents=%o, default=%s' ,
119+ basePath , Object . keys ( config . personas ) , Object . keys ( config . agents ) , config . defaultPersona )
81120 }
82121 }
83122
84- // Merge characters from all installed imports
123+ // Merge characters from all installed imports with namespacing
85124 try {
86125 const imports = getAllInstalledImports ( )
126+ debug ( '[Characters] Found %d imports to merge' , imports . length )
87127 for ( const imp of imports ) {
88- const importedConfig = loadCharactersFromPath ( imp . resolvedPaths . characters )
89- if ( importedConfig ) {
90- config = mergeCharactersConfigs ( config , importedConfig )
128+ try {
129+ const charactersPath = imp . resolvedPaths ?. characters
130+ debug ( '[Characters] Import %s: charactersPath=%s, exists=%s' ,
131+ imp . name , charactersPath , charactersPath ? existsSync ( charactersPath ) : 'no-path' )
132+ if ( ! charactersPath ) {
133+ debug ( '[Characters] Import %s has no characters path, skipping' , imp . name )
134+ continue
135+ }
136+ const importedConfig = loadCharactersFromPath ( charactersPath )
137+ if ( importedConfig ) {
138+ debug ( '[Characters] Merging import %s: personas=%o, agents=%o' ,
139+ imp . name ,
140+ importedConfig . personas ? Object . keys ( importedConfig . personas ) : [ ] ,
141+ importedConfig . agents ? Object . keys ( importedConfig . agents ) : [ ] )
142+ // Namespace agent IDs and persona names with the import package name
143+ config = mergeCharactersConfigs ( config , importedConfig , imp . name )
144+ } else {
145+ debug ( '[Characters] Import %s: failed to load config from %s' , imp . name , charactersPath )
146+ }
147+ } catch ( importErr ) {
148+ // Log error for this specific import but continue with others
149+ debug ( '[Characters] Error processing import %s: %s' , imp . name , importErr )
91150 }
92151 }
93- } catch {
94- // Silently ignore import errors - may not have imports system initialized
152+ } catch ( err ) {
153+ debug ( '[Characters] Error loading imports registry: %s' , err )
95154 }
96155
156+ debug ( '[Characters] Final config: personas=%o, agents=%o, default=%s' ,
157+ Object . keys ( config . personas ) , Object . keys ( config . agents ) , config . defaultPersona )
158+
97159 cachedConfig = config
98160 return cachedConfig
99161}
@@ -124,14 +186,55 @@ function getDefaultConfig(): AgentCharactersConfig {
124186 }
125187}
126188
189+ /**
190+ * Resolve agent ID to find matching character config
191+ * Handles workflow agent IDs with step suffixes (e.g., "bmad-analyst-step-0")
192+ * and finds namespaced agents (e.g., "bmad:bmad-analyst")
193+ */
194+ function resolveAgentId ( agentId : string , agents : Record < string , string > ) : string | null {
195+ debug ( '[Characters] resolveAgentId: input=%s, available agents=%o' , agentId , Object . keys ( agents ) )
196+
197+ // 1. Try exact match first
198+ if ( agents [ agentId ] ) {
199+ debug ( '[Characters] resolveAgentId: exact match found for %s' , agentId )
200+ return agentId
201+ }
202+
203+ // 2. Strip -step-N suffix and try again (workflow agent IDs)
204+ const baseId = agentId . replace ( / - s t e p - \d + $ / , '' )
205+ debug ( '[Characters] resolveAgentId: baseId (stripped suffix)=%s' , baseId )
206+
207+ if ( baseId !== agentId && agents [ baseId ] ) {
208+ debug ( '[Characters] resolveAgentId: match found for baseId %s' , baseId )
209+ return baseId
210+ }
211+
212+ // 3. Look for namespaced version (e.g., "bmad:bmad-analyst" for "bmad-analyst")
213+ for ( const key of Object . keys ( agents ) ) {
214+ // Check if key ends with ":baseId" (namespaced match)
215+ if ( key . endsWith ( `:${ baseId } ` ) ) {
216+ debug ( '[Characters] resolveAgentId: namespaced match found %s for baseId %s' , key , baseId )
217+ return key
218+ }
219+ }
220+
221+ debug ( '[Characters] resolveAgentId: no match found for %s, will use default' , agentId )
222+ return null
223+ }
224+
127225/**
128226 * Gets the persona configuration for a specific agent
129227 * Looks up agent -> persona mapping, falls back to defaultPersona
228+ * Handles workflow agent IDs with step suffixes and namespaced imports
130229 */
131230export function getCharacter ( agentId : string ) : Persona {
132231 const config = loadAgentCharactersConfig ( )
133- const personaName = config . agents [ agentId ] ?? config . defaultPersona
134- return config . personas [ personaName ] ?? config . personas [ config . defaultPersona ] ?? getDefaultConfig ( ) . personas . default
232+ const resolvedId = resolveAgentId ( agentId , config . agents )
233+ const personaName = resolvedId ? config . agents [ resolvedId ] : config . defaultPersona
234+ const persona = config . personas [ personaName ] ?? config . personas [ config . defaultPersona ] ?? getDefaultConfig ( ) . personas . default
235+ debug ( '[Characters] getCharacter: agentId=%s → resolvedId=%s → persona=%s → face=%s' ,
236+ agentId , resolvedId ?? 'null' , personaName , persona . baseFace )
237+ return persona
135238}
136239
137240/**
0 commit comments