Skip to content

fix(compile): resolve context links when folding instructions into CL… - #2918

Open
Pavel Vodrážka (pavelvodrazka-etnetera) wants to merge 2 commits into
microsoft:mainfrom
pavelvodrazka-etnetera:fix/claude-md-context-link-resolution
Open

fix(compile): resolve context links when folding instructions into CL…#2918
Pavel Vodrážka (pavelvodrazka-etnetera) wants to merge 2 commits into
microsoft:mainfrom
pavelvodrazka-etnetera:fix/claude-md-context-link-resolution

Conversation

@pavelvodrazka-etnetera

Copy link
Copy Markdown

Description

ClaudeFormatter (the CLAUDE.md generator) never ran its assembled instruction content through
the markdown-link resolver, so any relative link embedded in an instruction body -- most notably a
link to a .apm/context/*.context.md fragment -- was emitted byte-for-byte from the source file.
That is correct only when CLAUDE.md happens to land in the same directory as the instruction that
referenced it, and silently broken otherwise: a global (no-applyTo) instruction sourced from a
dependency, or placed anywhere but the project root, produces a link that no longer resolves from
CLAUDE.md's own location.

The equivalent AGENTS.md pipeline (distributed_compiler.py / agents_compiler.py) does not have
this problem -- it explicitly calls UnifiedLinkResolver.resolve_links_for_compilation() /
resolve_markdown_links() before returning the generated content. claude_formatter.py had no
equivalent call anywhere in the file.

This PR mirrors that call in ClaudeFormatter:

  • construct a UnifiedLinkResolver once in __init__
  • call register_contexts() at the start of format_distributed()
  • call resolve_links_for_compilation() on the assembled content before returning it from
    _generate_claude_content()

I found this while designing a monorepo/workspace layout where a root package depends on several
component-repo packages purely to pull in one "signpost" instruction each (an unconditional
instruction whose body links out to .apm/context/*.context.md fragments in the same package).
AGENTS.md resolved those links correctly out of the box; CLAUDE.md did not.

Fixes #2881

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Maintenance / refactor

Testing

  • Tested locally
  • All existing tests pass
  • Added tests for new functionality (if applicable)

Added TestContextLinkResolution to tests/unit/compilation/test_claude_formatter.py:

  • test_context_link_rewritten_relative_to_claude_md -- local context fragment, link must resolve
    from CLAUDE.md's own directory rather than the instruction's source directory.
  • test_context_link_rewritten_for_dependency_sourced_instruction -- same, but the instruction and
    its linked context fragment are sourced from a dependency materialized under apm_modules/, since
    that's the scenario where the bug actually bites (root instruction is never in the same directory
    as a dependency-sourced context file).

Confirmed both regression tests fail on the pre-fix code (reverting just
src/apm_cli/compilation/claude_formatter.py while keeping the tests) and pass after the fix.

Ran the full tests/unit/compilation/ suite (1402 tests) before and after: no regressions, all
green. Ran tests/unit/compilation/test_claude_formatter.py specifically (35 tests, up from 33):
all pass.

Minimal manual repro (also embedded as the reproduction steps in the test fixtures):

# apm.yml
name: my-pkg
version: 0.0.1
targets: [claude]
dependencies:
  apm: []
<!-- .apm/instructions/signpost.instructions.md -->
---
description: Signpost
---

See [conventions](../context/conventions.context.md) for details.
<!-- .apm/context/conventions.context.md -->
Real content lives here.
apm install --target claude
apm compile --target claude --force-instructions
cat CLAUDE.md

Before: [conventions](../context/conventions.context.md) (broken from CLAUDE.md's own
directory). After: [conventions](.apm/context/conventions.context.md) (correct, matches what
apm compile --target opencode already produces for the same source instruction).

Spec conformance (OpenAPM v0.1)

  • N/A -- this PR does not change OpenAPM-observable behaviour.

This is an internal implementation bug fix (a missing function call) that brings CLAUDE.md's
link-resolution behaviour in line with what AGENTS.md already does and what the spec/docs already
describe as the intended behaviour; it does not introduce, change, or remove any normative
req-XXX requirement.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The formatter holds a stateful link resolver whose context registry can leak across repeated format_distributed() calls, potentially rewriting links to stale/incorrect context paths.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Fixes apm compile --target claude so that when instruction bodies are folded into CLAUDE.md, any embedded relative links to .context.md / .memory.md fragments are rewritten to be correct relative to CLAUDE.md’s output location (mirroring the existing AGENTS.md pipeline behavior).

Changes:

  • Add a UnifiedLinkResolver to ClaudeFormatter, register contexts during compilation, and run resolve_links_for_compilation() on the assembled CLAUDE.md content before returning it.
  • Add regression tests covering link rewriting for both local and dependency-sourced instruction/context pairs.
File summaries
File Description
src/apm_cli/compilation/claude_formatter.py Registers context fragments and resolves context/memory links in generated CLAUDE.md content.
tests/unit/compilation/test_claude_formatter.py Adds regression tests ensuring rewritten .context.md links resolve from CLAUDE.md’s directory, including dependency scenarios.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/apm_cli/compilation/claude_formatter.py
@pavelvodrazka-etnetera
Pavel Vodrážka (pavelvodrazka-etnetera) force-pushed the fix/claude-md-context-link-resolution branch 2 times, most recently from 5312569 to 90f519f Compare September 9, 2026 14:46
@pavelvodrazka-etnetera

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="Etnetera"

…AUDE.md

claude_formatter.py never called into the link resolver, so any relative
link embedded in an instruction body (in particular a link to a
.apm/context/*.context.md fragment) was emitted verbatim from the source
file when folded into CLAUDE.md -- correct only when CLAUDE.md happens to
live in the same directory as the instruction that referenced it, and
broken for any dependency-sourced or non-root placement.

agents_compiler.py / distributed_compiler.py already call
UnifiedLinkResolver.resolve_links_for_compilation() for the equivalent
AGENTS.md fold; this mirrors that call in ClaudeFormatter:

- construct a UnifiedLinkResolver once in __init__
- register_contexts() at the start of format_distributed()
- resolve_links_for_compilation() on the assembled content before
  returning it from _generate_claude_content()

Adds regression tests covering a local context fragment and a
dependency-sourced one (apm_modules/_local/<repo>/... anchor), both of
which reproduce the stale/broken link before this change and resolve
correctly after it. All 35 existing + new unit tests in
test_claude_formatter.py pass; full unit/compilation suite (1402 tests)
unaffected.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] apm compile --target claude never resolves .context.md links when folding instructions into CLAUDE.md

2 participants