Coverage for sources/agentsmgr/memorylinks.py: 69%
44 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-08 06:57 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-08 06:57 +0000
1# vim: set filetype=python fileencoding=utf-8:
2# -*- coding: utf-8 -*-
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#============================================================================#
21''' Memory file symlink management for coder configurations.
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'''
29from . import __
30from . import exceptions as _exceptions
31from . import resolver as _resolver
34_scribe = __.provide_scribe( __name__ )
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.
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
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 )
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.
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.
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' / 'configuration' / '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 ) )