Describe the bug
apm compile writes the literal string <!-- Build ID: __BUILD_ID__ --> to disk on every compilation target except the legacy --single-agents path. The placeholder is documented in compilation/constants.py as something that "a deterministic Build ID (content hash) is substituted post-generation", but the substitution code only exists in compile/cli.py:506-524 and is gated by if config.strategy == "distributed" and not single_agents -- only the --single-agents legacy path reaches it.
This makes Build-ID-based byte-stable rebuild and cross-platform pre-commit verification (the user-story documented in #467) impossible for the default apm compile and every multi-target setup. #467 / #468 fixed cross-platform sort determinism but did not extend substitution to the affected formatters.
Affected paths
All produce files containing literal __BUILD_ID__:
| Path |
Caller |
Output |
apm compile (default distributed) |
agents_compiler._write_distributed_file |
AGENTS.md (root + every distributed sub-dir) |
apm compile -t claude |
agents_compiler._compile_claude_md (inline write) |
CLAUDE.md |
apm compile -t gemini |
agents_compiler._compile_gemini_md (inline write) |
GEMINI.md |
apm compile --watch (single-file mode) |
agents_compiler._write_output_file |
AGENTS.md |
compile_agents_md() (public API) |
agents_compiler._write_output_file |
AGENTS.md |
To Reproduce
mkdir t && cd t && apm init -y
mkdir -p .apm/instructions
cat > .apm/instructions/x.instructions.md <<'INSTR'
---
applyTo: "**/*.py"
description: Test
---
# Test
INSTR
apm compile
apm compile -t claude
apm compile -t gemini
grep '__BUILD_ID__' AGENTS.md CLAUDE.md GEMINI.md
Output on main (666925f):
AGENTS.md:<!-- Build ID: __BUILD_ID__ -->
CLAUDE.md:<!-- Build ID: __BUILD_ID__ -->
GEMINI.md:<!-- Build ID: __BUILD_ID__ -->
Expected behavior
Every compiled output should contain a real 12-char SHA256 hash, e.g. <!-- Build ID: e316dee8ef7f -->, deterministic across re-execution and sensitive to content changes (so pre-commit git diff --exit-code AGENTS.md works).
Why PR #468 did not cover this
PR #468 closed #467 with the verification claim "Both now produce <!-- Build ID: e316dee8ef7f -->". However the PR diff only adds sorted(...) calls in 4 files (context_optimizer.py, template_builder.py, distributed_compiler.py, claude_formatter.py); it does not touch the placeholder-replacement logic. That logic lives at compile/cli.py:506-524 and is reachable only via the --single-agents legacy path, so PR #468's verification must have been run with that flag. The default apm compile and -t claude / -t gemini paths still write __BUILD_ID__ literally on main today.
Root cause
The placeholder substitution is a cross-cutting concern scattered across five compiled-output write sites:
commands/compile/cli.py:506-548 -- single-file path: stabilizes
compilation/agents_compiler.py:_write_distributed_file (line 870): no stabilization
compilation/agents_compiler.py claude write (line 545): no stabilization
compilation/agents_compiler.py gemini write (line 640): no stabilization
compilation/agents_compiler.py:_write_output_file (line 814): no stabilization (used by watch mode + public API)
There is no chokepoint that guarantees stabilization runs before persisting compiled output, so adding a new target naturally re-exposes the bug. PR #468's pattern (sibling-by-sibling fix in 4 files) is structurally the same anti-pattern and is the reason this case was missed.
Suggested fix
Introduce a CompiledOutputWriter chokepoint in compilation/output_writer.py that all five write sites must route through. The writer extracts the hash logic into a stabilize_build_id() helper, defensively asserts the placeholder is gone before persisting, and atomic-writes to disk. Direct path.write_text / open(...).write on compiled output becomes a contract violation -- adding a new target without using the writer raises rather than silently emitting __BUILD_ID__.
Environment
- OS: Windows 11
- Python Version: 3.13.4
- APM Version: 0.9.2 / latest
main (666925f)
Describe the bug
apm compilewrites the literal string<!-- Build ID: __BUILD_ID__ -->to disk on every compilation target except the legacy--single-agentspath. The placeholder is documented incompilation/constants.pyas something that "a deterministic Build ID (content hash) is substituted post-generation", but the substitution code only exists incompile/cli.py:506-524and is gated byif config.strategy == "distributed" and not single_agents-- only the--single-agentslegacy path reaches it.This makes Build-ID-based byte-stable rebuild and cross-platform pre-commit verification (the user-story documented in #467) impossible for the default
apm compileand every multi-target setup. #467 / #468 fixed cross-platform sort determinism but did not extend substitution to the affected formatters.Affected paths
All produce files containing literal
__BUILD_ID__:apm compile(default distributed)agents_compiler._write_distributed_fileAGENTS.md(root + every distributed sub-dir)apm compile -t claudeagents_compiler._compile_claude_md(inline write)CLAUDE.mdapm compile -t geminiagents_compiler._compile_gemini_md(inline write)GEMINI.mdapm compile --watch(single-file mode)agents_compiler._write_output_fileAGENTS.mdcompile_agents_md()(public API)agents_compiler._write_output_fileAGENTS.mdTo Reproduce
Output on
main(666925f):Expected behavior
Every compiled output should contain a real 12-char SHA256 hash, e.g.
<!-- Build ID: e316dee8ef7f -->, deterministic across re-execution and sensitive to content changes (so pre-commitgit diff --exit-code AGENTS.mdworks).Why PR #468 did not cover this
PR #468 closed #467 with the verification claim "Both now produce
<!-- Build ID: e316dee8ef7f -->". However the PR diff only addssorted(...)calls in 4 files (context_optimizer.py,template_builder.py,distributed_compiler.py,claude_formatter.py); it does not touch the placeholder-replacement logic. That logic lives atcompile/cli.py:506-524and is reachable only via the--single-agentslegacy path, so PR #468's verification must have been run with that flag. The defaultapm compileand-t claude/-t geminipaths still write__BUILD_ID__literally onmaintoday.Root cause
The placeholder substitution is a cross-cutting concern scattered across five compiled-output write sites:
commands/compile/cli.py:506-548-- single-file path: stabilizescompilation/agents_compiler.py:_write_distributed_file(line 870): no stabilizationcompilation/agents_compiler.pyclaude write (line 545): no stabilizationcompilation/agents_compiler.pygemini write (line 640): no stabilizationcompilation/agents_compiler.py:_write_output_file(line 814): no stabilization (used by watch mode + public API)There is no chokepoint that guarantees stabilization runs before persisting compiled output, so adding a new target naturally re-exposes the bug. PR #468's pattern (sibling-by-sibling fix in 4 files) is structurally the same anti-pattern and is the reason this case was missed.
Suggested fix
Introduce a
CompiledOutputWriterchokepoint incompilation/output_writer.pythat all five write sites must route through. The writer extracts the hash logic into astabilize_build_id()helper, defensively asserts the placeholder is gone before persisting, and atomic-writes to disk. Directpath.write_text/open(...).writeon compiled output becomes a contract violation -- adding a new target without using the writer raises rather than silently emitting__BUILD_ID__.Environment
main(666925f)