Coverage for sources/agentsmgr/memorylinks.py: 69%

44 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-15 21:08 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

4#============================================================================# 

5# # 

6# Licensed under the Apache License, Version 2.0 (the "License"); # 

7# you may not use this file except in compliance with the License. # 

8# You may obtain a copy of the License at # 

9# # 

10# http://www.apache.org/licenses/LICENSE-2.0 # 

11# # 

12# Unless required by applicable law or agreed to in writing, software # 

13# distributed under the License is distributed on an "AS IS" BASIS, # 

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

15# See the License for the specific language governing permissions and # 

16# limitations under the License. # 

17# # 

18#============================================================================# 

19 

20 

21''' Memory file symlink management for coder configurations. 

22 

23 Provides functionality to create symlinks from coder-specific memory 

24 filenames to shared project conventions file. Follows patterns from 

25 .auxiliary/scripts/prepare-agents for consistent behavior. 

26''' 

27 

28 

29from . import __ 

30from . import exceptions as _exceptions 

31from . import resolver as _resolver 

32 

33 

34_scribe = __.provide_scribe( __name__ ) 

35 

36 

37def create_memory_symlink( 

38 source: __.Path, 

39 link_path: __.Path, 

40 simulate: bool = False, 

41) -> tuple[ bool, str ]: 

42 ''' Creates symlink from coder memory file to project conventions. 

43 

44 Follows patterns from .auxiliary/scripts/prepare-agents: 

45 - If link is symlink to correct target: Skip silently 

46 - If link is symlink to wrong target: Update it 

47 - If link is broken symlink: Remove and recreate 

48 - If link is regular file/directory: Warn and skip 

49 - If link doesn't exist: Create symlink 

50 

51 Returns tuple of (created, symlink_name) where created indicates 

52 if symlink was created/updated and symlink_name is the name 

53 relative to parent directory. 

54 ''' 

55 symlink_name = link_path.name 

56 try: 

57 relative_source = __.os.path.relpath( 

58 source, start = link_path.parent ) 

59 except ValueError: 

60 relative_source = str( source.resolve( ) ) 

61 if link_path.is_symlink( ): 

62 try: current_target = __.os.readlink( link_path ) 

63 except OSError as exception: 

64 _scribe.warning( 

65 f"Cannot read symlink {link_path}: {exception}." ) 

66 return ( False, symlink_name ) 

67 if current_target == relative_source: 67 ↛ 69line 67 didn't jump to line 69 because the condition on line 67 was always true

68 return ( False, symlink_name ) 

69 _scribe.info( 

70 f"Updating symlink {link_path.name}: " 

71 f"{current_target} → {relative_source}" ) 

72 if not simulate: link_path.unlink( ) 

73 elif link_path.exists( ): 73 ↛ 74line 73 didn't jump to line 74 because the condition on line 73 was never true

74 _scribe.warning( 

75 f"File or directory already exists at {link_path}. Skipping." ) 

76 return ( False, symlink_name ) 

77 elif not link_path.exists( ) and link_path.is_symlink( ): 77 ↛ 78line 77 didn't jump to line 78 because the condition on line 77 was never true

78 _scribe.info( f"Fixing broken symlink: {link_path.name}" ) 

79 if not simulate: link_path.unlink( ) 

80 if not simulate: 80 ↛ 84line 80 didn't jump to line 84 because the condition on line 80 was always true

81 link_path.symlink_to( relative_source ) 

82 _scribe.info( f"Created memory symlink: {link_path.name}" ) 

83 else: 

84 _scribe.info( 

85 f"[SIMULATE] Would create symlink: " 

86 f"{link_path.name} → {relative_source}" ) 

87 return ( True, symlink_name ) 

88 

89 

90def create_memory_symlinks_for_coders( 

91 coders: __.cabc.Sequence[ str ], 

92 target: __.Path, 

93 simulate: bool = False, 

94) -> tuple[ int, int, tuple[ str, ... ] ]: 

95 ''' Creates memory symlinks for all configured coders. 

96 

97 Memory symlinks are always created at project root, pointing to 

98 project-specific conventions file. They are created regardless 

99 of targeting mode since memory files are project-specific. 

100 

101 Returns tuple of (attempted, created, symlink_names) where 

102 symlink_names contains names of all symlinks (both newly created 

103 and pre-existing). 

104 ''' 

105 source = target / '.auxiliary' / 'agents' / 'agents.md' 

106 if not source.exists( ): 106 ↛ 107line 106 didn't jump to line 107 because the condition on line 106 was never true

107 raise _exceptions.MemoryFileAbsence( source ) 

108 attempted = 0 

109 created = 0 

110 symlink_names: list[ str ] = [ ] 

111 for coder_name, renderer in _resolver.resolve_coders( coders ): 

112 link_path = target / renderer.memory_filename 

113 attempted += 1 

114 was_created, symlink_name = create_memory_symlink( 

115 source, link_path, simulate ) 

116 if was_created: created += 1 

117 symlink_names.append( symlink_name ) 

118 return ( attempted, created, tuple( symlink_names ) )