@@ -77,6 +77,9 @@ pub struct SessionOptions {
7777 pub planning_enabled : bool ,
7878 /// Enable goal tracking
7979 pub goal_tracking : bool ,
80+ /// Extra directories to scan for skill files (*.md).
81+ /// Merged with any global `skill_dirs` from [`CodeConfig`].
82+ pub skill_dirs : Vec < PathBuf > ,
8083 /// Optional skill registry for instruction injection
8184 pub skill_registry : Option < Arc < crate :: skills:: SkillRegistry > > ,
8285 /// Optional memory store for long-term memory persistence
@@ -138,6 +141,7 @@ impl std::fmt::Debug for SessionOptions {
138141 f. debug_struct ( "SessionOptions" )
139142 . field ( "model" , & self . model )
140143 . field ( "agent_dirs" , & self . agent_dirs )
144+ . field ( "skill_dirs" , & self . skill_dirs )
141145 . field ( "queue_config" , & self . queue_config )
142146 . field ( "security_provider" , & self . security_provider . is_some ( ) )
143147 . field ( "context_providers" , & self . context_providers . len ( ) )
@@ -278,7 +282,14 @@ impl SessionOptions {
278282 self
279283 }
280284
281- /// Load skills from a directory
285+ /// Add skill directories to scan for skill files (*.md).
286+ /// Merged with any global `skill_dirs` from [`CodeConfig`] at session build time.
287+ pub fn with_skill_dirs ( mut self , dirs : impl IntoIterator < Item = impl Into < PathBuf > > ) -> Self {
288+ self . skill_dirs . extend ( dirs. into_iter ( ) . map ( Into :: into) ) ;
289+ self
290+ }
291+
292+ /// Load skills from a directory (eager — scans immediately into a registry).
282293 pub fn with_skills_from_dir ( mut self , dir : impl AsRef < std:: path:: Path > ) -> Self {
283294 let registry = self
284295 . skill_registry
@@ -503,7 +514,19 @@ impl Agent {
503514 /// Auto-detects: file path (.hcl/.json) vs inline JSON vs inline HCL.
504515 pub async fn new ( config_source : impl Into < String > ) -> Result < Self > {
505516 let source = config_source. into ( ) ;
506- let path = Path :: new ( & source) ;
517+
518+ // Expand leading `~/` to the user's home directory
519+ let expanded = if source. starts_with ( "~/" ) {
520+ if let Some ( home) = std:: env:: var_os ( "HOME" ) {
521+ format ! ( "{}/{}" , home. to_string_lossy( ) , & source[ 2 ..] )
522+ } else {
523+ source. clone ( )
524+ }
525+ } else {
526+ source. clone ( )
527+ } ;
528+
529+ let path = Path :: new ( & expanded) ;
507530
508531 let config = if path. extension ( ) . is_some ( ) && path. exists ( ) {
509532 CodeConfig :: from_file ( path)
@@ -538,11 +561,26 @@ impl Agent {
538561 ..AgentConfig :: default ( )
539562 } ;
540563
541- Ok ( Agent {
564+ let mut agent = Agent {
542565 llm_client,
543566 code_config : config,
544567 config : agent_config,
545- } )
568+ } ;
569+
570+ // Always initialize the skill registry with built-in skills, then load any user-defined dirs
571+ let registry = Arc :: new ( crate :: skills:: SkillRegistry :: with_builtins ( ) ) ;
572+ for dir in & agent. code_config . skill_dirs . clone ( ) {
573+ if let Err ( e) = registry. load_from_dir ( dir) {
574+ tracing:: warn!(
575+ dir = %dir. display( ) ,
576+ error = %e,
577+ "Failed to load skills from directory — skipping"
578+ ) ;
579+ }
580+ }
581+ agent. config . skill_registry = Some ( registry) ;
582+
583+ Ok ( agent)
546584 }
547585
548586 /// Bind to a workspace directory, returning an [`AgentSession`].
@@ -694,16 +732,41 @@ impl Agent {
694732 . clone ( )
695733 . unwrap_or_else ( || self . config . prompt_slots . clone ( ) ) ;
696734
697- // Append skill instructions to the extra slot
698- if let Some ( ref registry) = opts. skill_registry {
699- let skill_prompt = registry. to_system_prompt ( ) ;
700- if !skill_prompt. is_empty ( ) {
701- prompt_slots. extra = match prompt_slots. extra {
702- Some ( existing) => Some ( format ! ( "{}\n \n {}" , existing, skill_prompt) ) ,
703- None => Some ( skill_prompt) ,
704- } ;
735+ // Build effective skill registry: fork the agent-level registry (builtins + global
736+ // skill_dirs), then layer session-level skills on top. Forking ensures session skills
737+ // never pollute the shared agent-level registry.
738+ let base_registry = self
739+ . config
740+ . skill_registry
741+ . as_deref ( )
742+ . map ( |r| r. fork ( ) )
743+ . unwrap_or_else ( crate :: skills:: SkillRegistry :: with_builtins) ;
744+ // Merge explicit session registry on top of the fork
745+ if let Some ( ref r) = opts. skill_registry {
746+ for skill in r. all ( ) {
747+ base_registry. register_unchecked ( skill) ;
748+ }
749+ }
750+ // Load session-level skill dirs
751+ for dir in & opts. skill_dirs {
752+ if let Err ( e) = base_registry. load_from_dir ( dir) {
753+ tracing:: warn!(
754+ dir = %dir. display( ) ,
755+ error = %e,
756+ "Failed to load session skill dir — skipping"
757+ ) ;
705758 }
706759 }
760+ let effective_registry = Arc :: new ( base_registry) ;
761+
762+ // Append skill directory listing to the extra prompt slot
763+ let skill_prompt = effective_registry. to_system_prompt ( ) ;
764+ if !skill_prompt. is_empty ( ) {
765+ prompt_slots. extra = match prompt_slots. extra {
766+ Some ( existing) => Some ( format ! ( "{}\n \n {}" , existing, skill_prompt) ) ,
767+ None => Some ( skill_prompt) ,
768+ } ;
769+ }
707770
708771 // Resolve memory store: explicit store takes priority, then file_memory_dir
709772 let mut init_warning: Option < String > = None ;
@@ -751,7 +814,7 @@ impl Agent {
751814 context_providers : opts. context_providers . clone ( ) ,
752815 planning_enabled : opts. planning_enabled ,
753816 goal_tracking : opts. goal_tracking ,
754- skill_registry : opts . skill_registry . clone ( ) ,
817+ skill_registry : Some ( effective_registry ) ,
755818 max_parse_retries : opts. max_parse_retries . unwrap_or ( base. max_parse_retries ) ,
756819 tool_timeout_ms : opts. tool_timeout_ms . or ( base. tool_timeout_ms ) ,
757820 circuit_breaker_threshold : opts
@@ -850,6 +913,32 @@ impl Agent {
850913 . clone ( )
851914 . unwrap_or_else ( || uuid:: Uuid :: new_v4 ( ) . to_string ( ) ) ;
852915
916+ // Resolve session store: explicit opts store > config sessions_dir > None
917+ let session_store = if opts. session_store . is_some ( ) {
918+ opts. session_store . clone ( )
919+ } else if let Some ( ref dir) = self . code_config . sessions_dir {
920+ match tokio:: runtime:: Handle :: try_current ( ) {
921+ Ok ( handle) => {
922+ let dir = dir. clone ( ) ;
923+ match tokio:: task:: block_in_place ( || {
924+ handle. block_on ( crate :: store:: FileSessionStore :: new ( dir) )
925+ } ) {
926+ Ok ( store) => Some ( Arc :: new ( store) as Arc < dyn crate :: store:: SessionStore > ) ,
927+ Err ( e) => {
928+ tracing:: warn!( "Failed to create session store from sessions_dir: {}" , e) ;
929+ None
930+ }
931+ }
932+ }
933+ Err ( _) => {
934+ tracing:: warn!( "No async runtime for sessions_dir store — persistence disabled" ) ;
935+ None
936+ }
937+ }
938+ } else {
939+ None
940+ } ;
941+
853942 Ok ( AgentSession {
854943 llm_client,
855944 tool_executor,
@@ -860,7 +949,7 @@ impl Agent {
860949 session_id,
861950 history : RwLock :: new ( Vec :: new ( ) ) ,
862951 command_queue,
863- session_store : opts . session_store . clone ( ) ,
952+ session_store,
864953 auto_save : opts. auto_save ,
865954 hook_engine : Arc :: new ( crate :: hooks:: HookEngine :: new ( ) ) ,
866955 init_warning,
0 commit comments