From 202dfd19635ec34268ec940a9500d1fcd365f500 Mon Sep 17 00:00:00 2001 From: Alejandro Colomina Date: Thu, 26 Mar 2026 17:12:05 +0100 Subject: [PATCH 1/3] fix: sort instruction discovery order for deterministic Build IDs across platforms os.walk() returns directory entries in filesystem-native order (APFS on macOS, ext4 on Linux), causing instructions within each pattern group to be written in a different sequence per platform. This makes the Build ID hash non-deterministic, breaking CI checks and preventing apm compile from being used in pre-commit hooks. Sort dirs and files in _get_all_files() and sort pattern_instructions by file_path in template_builder, distributed_compiler, and claude_formatter before writing. Fixes #467 --- src/apm_cli/compilation/claude_formatter.py | 2 +- src/apm_cli/compilation/context_optimizer.py | 5 +++-- src/apm_cli/compilation/distributed_compiler.py | 2 +- src/apm_cli/compilation/template_builder.py | 6 +++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/apm_cli/compilation/claude_formatter.py b/src/apm_cli/compilation/claude_formatter.py index f06e5ef3..a4a6a8b2 100644 --- a/src/apm_cli/compilation/claude_formatter.py +++ b/src/apm_cli/compilation/claude_formatter.py @@ -291,7 +291,7 @@ def _generate_claude_content( sections.append(f"## Files matching `{pattern}`") sections.append("") - for instruction in pattern_instructions: + for instruction in sorted(pattern_instructions, key=lambda i: str(i.file_path)): content = instruction.content.strip() if content: # Add source attribution comment diff --git a/src/apm_cli/compilation/context_optimizer.py b/src/apm_cli/compilation/context_optimizer.py index 4fc4181d..256ecb32 100644 --- a/src/apm_cli/compilation/context_optimizer.py +++ b/src/apm_cli/compilation/context_optimizer.py @@ -172,8 +172,9 @@ def _get_all_files(self) -> List[Path]: self._file_list_cache = [] for root, dirs, files in os.walk(self.base_dir): # Skip hidden and excluded directories for performance - dirs[:] = [d for d in dirs if not d.startswith('.') and d not in DEFAULT_EXCLUDED_DIRNAMES] - for file in files: + # Sort to guarantee deterministic traversal order across filesystems + dirs[:] = sorted(d for d in dirs if not d.startswith('.') and d not in DEFAULT_EXCLUDED_DIRNAMES) + for file in sorted(files): if not file.startswith('.'): self._file_list_cache.append(Path(root) / file) return self._file_list_cache diff --git a/src/apm_cli/compilation/distributed_compiler.py b/src/apm_cli/compilation/distributed_compiler.py index 48c3214b..7a11d12d 100644 --- a/src/apm_cli/compilation/distributed_compiler.py +++ b/src/apm_cli/compilation/distributed_compiler.py @@ -536,7 +536,7 @@ def _generate_agents_content( sections.append(f"## Files matching `{pattern}`") sections.append("") - for instruction in pattern_instructions: + for instruction in sorted(pattern_instructions, key=lambda i: str(i.file_path)): content = instruction.content.strip() if content: # Add source attribution for individual instructions diff --git a/src/apm_cli/compilation/template_builder.py b/src/apm_cli/compilation/template_builder.py index e9b945d2..465b8a53 100644 --- a/src/apm_cli/compilation/template_builder.py +++ b/src/apm_cli/compilation/template_builder.py @@ -34,12 +34,12 @@ def build_conditional_sections(instructions: List[Instruction]) -> str: sections = [] - for pattern, pattern_instructions in pattern_groups.items(): + for pattern, pattern_instructions in sorted(pattern_groups.items()): sections.append(f"## Files matching `{pattern}`") sections.append("") - + # Combine content from all instructions for this pattern - for instruction in pattern_instructions: + for instruction in sorted(pattern_instructions, key=lambda i: str(i.file_path)): content = instruction.content.strip() if content: # Add source file comment before the content From 1ed503263f737b6cd37886a24249f9eed8ccc13d Mon Sep 17 00:00:00 2001 From: Daniel Meppiel <51440732+danielmeppiel@users.noreply.github.com> Date: Thu, 26 Mar 2026 22:01:38 +0100 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/apm_cli/compilation/claude_formatter.py | 5 ++++- src/apm_cli/compilation/distributed_compiler.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/apm_cli/compilation/claude_formatter.py b/src/apm_cli/compilation/claude_formatter.py index a4a6a8b2..fe78fdda 100644 --- a/src/apm_cli/compilation/claude_formatter.py +++ b/src/apm_cli/compilation/claude_formatter.py @@ -291,7 +291,10 @@ def _generate_claude_content( sections.append(f"## Files matching `{pattern}`") sections.append("") - for instruction in sorted(pattern_instructions, key=lambda i: str(i.file_path)): + for instruction in sorted( + pattern_instructions, + key=lambda i: portable_relpath(i.file_path, self.base_dir), + ): content = instruction.content.strip() if content: # Add source attribution comment diff --git a/src/apm_cli/compilation/distributed_compiler.py b/src/apm_cli/compilation/distributed_compiler.py index 7a11d12d..c986b8bb 100644 --- a/src/apm_cli/compilation/distributed_compiler.py +++ b/src/apm_cli/compilation/distributed_compiler.py @@ -536,7 +536,10 @@ def _generate_agents_content( sections.append(f"## Files matching `{pattern}`") sections.append("") - for instruction in sorted(pattern_instructions, key=lambda i: str(i.file_path)): + for instruction in sorted( + pattern_instructions, + key=lambda i: portable_relpath(i.file_path, self.base_dir), + ): content = instruction.content.strip() if content: # Add source attribution for individual instructions From e4d3a17efb6b5a45c7ae2ae72dc8d7bd40d8d05e Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Thu, 26 Mar 2026 22:06:19 +0100 Subject: [PATCH 3/3] fix: use portable_relpath for sort key in template_builder Align with distributed_compiler and claude_formatter which already use portable_relpath(). Ensures consistent POSIX-normalized sort keys across all three compilation paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/apm_cli/compilation/template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apm_cli/compilation/template_builder.py b/src/apm_cli/compilation/template_builder.py index 465b8a53..fd8799d1 100644 --- a/src/apm_cli/compilation/template_builder.py +++ b/src/apm_cli/compilation/template_builder.py @@ -39,7 +39,7 @@ def build_conditional_sections(instructions: List[Instruction]) -> str: sections.append("") # Combine content from all instructions for this pattern - for instruction in sorted(pattern_instructions, key=lambda i: str(i.file_path)): + for instruction in sorted(pattern_instructions, key=lambda i: portable_relpath(i.file_path, Path.cwd())): content = instruction.content.strip() if content: # Add source file comment before the content