@@ -33,21 +33,30 @@ pub(crate) fn files_with_ext<'a>(
3333 . filter ( move |path| path. extension ( ) . is_some_and ( |e| e == ext) )
3434}
3535
36- /// Represents an MCP server entry parsed from an agent's config
37- #[ derive( Debug , Clone ) ]
36+ /// Represents an MCP server entry parsed from an agent's config.
37+ ///
38+ /// Serde representation is the canonical Kit blob: `{command, args, env}`.
39+ /// `name` is carried by the caller's context (asset name in the manifest);
40+ /// `enabled` is HarnessKit-internal and defaults to `true` on the install
41+ /// side (only OpenCode's source schema has a per-entry agent-native
42+ /// `enabled` — every other adapter always sets `true`).
43+ #[ derive( Debug , Clone , serde:: Serialize , serde:: Deserialize ) ]
3844pub struct McpServerEntry {
45+ #[ serde( skip) ]
3946 pub name : String ,
4047 pub command : String ,
48+ #[ serde( default ) ]
4149 pub args : Vec < String > ,
50+ #[ serde( default ) ]
4251 pub env : std:: collections:: HashMap < String , String > ,
43- /// Whether the agent itself considers this entry active. Currently only
44- /// OpenCode's schema has a per-entry `"enabled"` boolean; every other
45- /// adapter always sets this to `true` because their formats have no
46- /// agent-native disable concept. HarnessKit's own user-toggled disable
47- /// flow tracks state separately in SQLite — this field is orthogonal.
52+ #[ serde( skip, default = "default_enabled" ) ]
4853 pub enabled : bool ,
4954}
5055
56+ fn default_enabled ( ) -> bool {
57+ true
58+ }
59+
5160/// Represents a hook entry parsed from an agent's config
5261#[ derive( Debug , Clone ) ]
5362pub struct HookEntry {
@@ -210,6 +219,19 @@ pub trait AgentAdapter: Send + Sync {
210219 vec ! [ ]
211220 }
212221
222+ /// Canonical relative path used when writing a project-level Rules file
223+ /// from a Kit. Default: the unique non-glob, non-trailing-slash entry
224+ /// from `project_rules_patterns()`, else `None`. Adapters with multiple
225+ /// legitimate paths should override.
226+ fn project_rules_target_relpath ( & self ) -> Option < String > {
227+ pick_unique_concrete ( self . project_rules_patterns ( ) )
228+ }
229+
230+ /// Same as `project_rules_target_relpath` but for Memory.
231+ fn project_memory_target_relpath ( & self ) -> Option < String > {
232+ pick_unique_concrete ( self . project_memory_patterns ( ) )
233+ }
234+
213235 /// Relative paths/globs for settings within a project dir
214236 fn project_settings_patterns ( & self ) -> Vec < String > {
215237 vec ! [ ]
@@ -259,6 +281,16 @@ pub trait AgentAdapter: Send + Sync {
259281 vec ! [ ]
260282 }
261283
284+ /// Additional skill-dir aliases the agent READS from but doesn't write to
285+ /// — e.g. Copilot canonical is `.github/skills` but it also picks up
286+ /// `.claude/skills` and `.agents/skills` if present. Declaring these lets
287+ /// callers (e.g. the Kit-remove shared-dir warning) know that a sibling
288+ /// agent's install at one of these paths is visible to this agent too.
289+ /// Returning `vec![]` (the default) means "only the canonical dir".
290+ fn project_skill_read_dirs ( & self ) -> Vec < String > {
291+ vec ! [ ]
292+ }
293+
262294 /// Relative path of the project-level MCP config file (e.g. `.mcp.json`).
263295 fn project_mcp_config_relpath ( & self ) -> Option < String > {
264296 None
@@ -315,6 +347,17 @@ pub trait AgentAdapter: Send + Sync {
315347 }
316348}
317349
350+ fn pick_unique_concrete ( patterns : Vec < String > ) -> Option < String > {
351+ let mut concrete = patterns
352+ . into_iter ( )
353+ . filter ( |p| !p. contains ( '*' ) && !p. ends_with ( '/' ) ) ;
354+ let first = concrete. next ( ) ?;
355+ if concrete. next ( ) . is_some ( ) {
356+ return None ;
357+ }
358+ Some ( first)
359+ }
360+
318361/// Returns all agent adapters in canonical display order.
319362/// Must match AGENT_ORDER in src/lib/types.ts.
320363pub fn all_adapters ( ) -> Vec < Box < dyn AgentAdapter > > {
@@ -469,4 +512,56 @@ mod tests {
469512 assert_eq ! ( & actual, want, "{} project skill path mismatch" , a. name( ) ) ;
470513 }
471514 }
515+
516+ #[ test]
517+ fn project_rules_target_relpath_default_returns_single_non_glob ( ) {
518+ let adapters = crate :: adapter:: all_adapters ( ) ;
519+ for a in & adapters {
520+ let patterns = a. project_rules_patterns ( ) ;
521+ let non_glob: Vec < & String > = patterns
522+ . iter ( )
523+ . filter ( |p| !p. contains ( '*' ) && !p. ends_with ( '/' ) )
524+ . collect ( ) ;
525+ match non_glob. as_slice ( ) {
526+ [ only] => {
527+ assert_eq ! (
528+ a. project_rules_target_relpath( ) . as_deref( ) ,
529+ Some ( only. as_str( ) ) ,
530+ "{}: target relpath default should pick the unique non-glob" ,
531+ a. name( )
532+ ) ;
533+ }
534+ _ => {
535+ // Adapters with 0 or many candidates may legitimately return None
536+ // unless they override; just verify the contract holds.
537+ let _ = a. project_rules_target_relpath ( ) ;
538+ }
539+ }
540+ }
541+ }
542+
543+ #[ test]
544+ fn project_memory_target_relpath_default_returns_single_non_glob ( ) {
545+ let adapters = crate :: adapter:: all_adapters ( ) ;
546+ for a in & adapters {
547+ let patterns = a. project_memory_patterns ( ) ;
548+ let non_glob: Vec < & String > = patterns
549+ . iter ( )
550+ . filter ( |p| !p. contains ( '*' ) && !p. ends_with ( '/' ) )
551+ . collect ( ) ;
552+ match non_glob. as_slice ( ) {
553+ [ only] => {
554+ assert_eq ! (
555+ a. project_memory_target_relpath( ) . as_deref( ) ,
556+ Some ( only. as_str( ) ) ,
557+ "{}: target relpath default should pick the unique non-glob" ,
558+ a. name( )
559+ ) ;
560+ }
561+ _ => {
562+ let _ = a. project_memory_target_relpath ( ) ;
563+ }
564+ }
565+ }
566+ }
472567}
0 commit comments