Repository navigation
[BUG] apm compile --target claude never resolves .context.md links when folding instructions into CLAUDE.md #2881
Description
Describe the bug
apm compile --target claude folds instruction bodies (global + applyTo-scoped) directly
into CLAUDE.md, but never runs them through the markdown-link resolver. Any relative link
inside an instruction body — in particular a link to a .apm/context/*.context.md fragment —
is emitted byte-for-byte as written in the source file, with no adjustment for the fact that
CLAUDE.md may live in a different directory than the source instruction (e.g. when the
instruction comes from a dependency, or from a non-root placement).
The equivalent AGENTS.md pipeline (agents_compiler.py / distributed_compiler.py) does not
have this problem: it explicitly calls the link resolver
(UnifiedLinkResolver.resolve_links_for_compilation in distributed_compiler.py, or
resolve_markdown_links in agents_compiler.py) before returning the generated content.
claude_formatter.py has no equivalent call anywhere in the file — grepping the module for
resolve_links / link_resolver returns zero matches.
Practical effect: an instruction with no applyTo (global, e.g. a "signpost" file whose only
job is a short pointer to a .context.md file with the real content) will fold correctly into
AGENTS.md with a correctly rewritten link, but the same instruction, when it ends up folded
into CLAUDE.md, keeps the original unadjusted relative link and the link no longer resolves
from CLAUDE.md's location.
To Reproduce
-
Create a package with one instruction and one context fragment it links to:
my-pkg/ apm.yml .apm/ instructions/ signpost.instructions.md context/ conventions.context.mdapm.yml:name: my-pkg version: 0.0.1 targets: [claude] dependencies: apm: []
.apm/instructions/signpost.instructions.md(noapplyTo— global instruction):--- description: Signpost --- See [conventions](../context/conventions.context.md) for details.
.apm/context/conventions.context.md:Real content lives here.
-
Compile, forcing the instructions section into
CLAUDE.mdeven though a
.claude/rules/directory may already exist (--force-instructionsis the fastest way to
force the code path; the same missing resolution also affects the natural, non-forced path
any time.claude/rules/is not yet populated, e.g. firstapm compilerun before
apm install, orapm compile --root <scratch>):apm install --target claude apm compile --target claude --force-instructions
-
Inspect
CLAUDE.md:## Project Standards ### Global Instructions See [conventions](../context/conventions.context.md) for details.
The link
../context/conventions.context.mdis relative to
.apm/instructions/signpost.instructions.md's own directory (where it was authored), not to
CLAUDE.md's directory (project root). Resolved fromCLAUDE.md's location it points one
directory above the project root and does not exist. -
Compile the same package for
opencode(or any other AGENTS.md-based target) instead:apm compile --target opencode
AGENTS.mdcontains the same link, correctly rewritten to
.apm/context/conventions.context.md(relative to the project root) — because
agents_compiler.py/distributed_compiler.pydo call the link resolver.
Expected behavior
CLAUDE.md should resolve the same relative links to .context.md / .memory.md fragments
that AGENTS.md already resolves correctly, using the same anchor (the compiled output file's
own directory), regardless of whether the instruction section is emitted via the normal path or
via --force-instructions.
Environment:
- OS: Linux
- Python Version: 3.13
- APM Version: 0.30.0 (commit
8c2e0d9c, tagv0.30.0)
Logs
No error is raised; the bug is a silently-wrong output (broken relative link), not a crash.
Additional context
Root cause, confirmed by reading the source at v0.30.0:
src/apm_cli/compilation/claude_formatter.pybuilds theCLAUDE.mdbody in
_generate_claude_content()and returns"\n".join(sections)directly — the file contains
zero references toresolve_links,resolve_markdown_links,link_resolver, or
UnifiedLinkResolveranywhere.- Contrast with
src/apm_cli/compilation/distributed_compiler.py:754, which — for the exact
same kind of assembledAGENTS.mdsection content — explicitly calls:right before returning, using acontent = self.link_resolver.resolve_links_for_compilation( content=content, source_file=placement.agents_path.parent, compiled_output=placement.agents_path, )
UnifiedLinkResolverinstance that hadregister_contexts()
called on it earlier in the same compile pass. src/apm_cli/compilation/agents_compiler.pyadditionally calls the (simpler,
content-inlining)resolve_markdown_links()helper fromlink_resolver.pyin its own
non-distributed rendering path (generate_output(), lines ~1467 and ~1658).claude_formatter.pyhas no equivalent of either call.
I verified the fix locally by adding the missing call, mirroring the distributed_compiler.py
pattern:
- In
ClaudeFormatter.__init__, construct a resolver once:self.link_resolver = UnifiedLinkResolver(self.source_dir). - In
ClaudeFormatter.format_distributed, before generating any placement content, call
self.link_resolver.register_contexts(primitives). - In
ClaudeFormatter._generate_claude_content, right beforereturn, replace
return "\n".join(sections)with:content = "\n".join(sections) content = self.link_resolver.resolve_links_for_compilation( content=content, source_file=placement.claude_path.parent, compiled_output=placement.claude_path, ) return content
With this change, the reproduction above produces the correctly-resolved
.apm/context/conventions.context.md link in CLAUDE.md, identical to what AGENTS.md
already produces for the same source instruction. I'm happy to open a PR with this patch (plus
a regression test) if that's useful — flagging as an issue first in case there's a preferred
approach or a reason this was intentionally left out that I'm not aware of.
Separately (not part of this report, noting for completeness): I checked whether other
per-target root-context formatters have the same gap. gemini_formatter.py also has no
resolve_links call, but it turns out to be a 12-line stub that only ever emits a single
@./AGENTS.md import line and never assembles instruction content itself, so it isn't affected.
No other target has its own root-context formatter (cursor doesn't produce one; copilot,
codex, opencode, windsurf, antigravity all share the correctly-resolving AGENTS.md
pipeline).
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
github-actions commentedon Sep 9, 2026
Triage decision
accept
Proposed labels
Milestone
null -- open-milestone read failed (401 from milestones API); no milestone assigned this run.
Suggested next action
Call the existing markdown-link resolver (UnifiedLinkResolver.resolve_links_for_compilation or resolve_markdown_links) from claude_formatter.py before returning generated content, matching the AGENTS.md pipeline's existing behavior.
Suggested issue comment
Clear, well-localized gap -- claude_formatter.py has no equivalent link-resolution call that the AGENTS.md pipeline already has. Accepting as a bug: relative links (especially to .apm/context/*.context.md fragments) must resolve correctly when folded into CLAUDE.md regardless of the source instruction's original directory. No milestone assigned yet (milestone read failed this run).Per-lens notes (collapsed)
DevX UX Expert -- User-Need Reviewer
Broken relative links inside compiled CLAUDE.md content directly undermines the multi-target compile promise -- the same instruction source should compile correctly for every target, and Claude's formatter is the one falling short here.
Supply Chain Security Expert -- Risk-Surface Reviewer
No risk-surface implication; this is a content-correctness gap in link rewriting, not a trust boundary.
OSS Growth Hacker -- Contributor-Tone Reviewer
Not activated -- pavelvodrazka-etnetera has 3 prior issues on this repo and the report is a precise code-level diagnosis (grep evidence of the missing call), indicating an experienced contributor.
Python Architect -- Architecture Reviewer
Not activated -- the fix is a single missing call to an existing, already-used resolver function; no new interface or cross-cutting design is required.
Doc Writer -- Documentation Reviewer
Not activated -- no user-facing documentation change implied; this is an internal formatter parity fix.
APM CEO -- Triage Arbiter
Precise root cause with an existing precedent (AGENTS.md pipeline) to match. Accept as type/bug, theme/portability, area/multi-target.
{ "decision": "accept", "decision_detail": "", "theme": "theme/portability", "areas": ["area/multi-target"], "type": "type/bug", "status": "status/accepted", "priority": null, "preserved_labels": [], "milestone": null, "next_action": "Call the existing markdown-link resolver (UnifiedLinkResolver.resolve_links_for_compilation or resolve_markdown_links) from claude_formatter.py before returning generated content, matching the AGENTS.md pipeline's existing behavior.", "comment_markdown": "Clear, well-localized gap -- claude_formatter.py has no equivalent link-resolution call that the AGENTS.md pipeline already has. Accepting as a bug: relative links (especially to .apm/context/*.context.md fragments) must resolve correctly when folded into CLAUDE.md regardless of the source instruction's original directory. No milestone assigned yet (milestone read failed this run)." }github-actions commentedon Sep 10, 2026
Triage decision
accept
Proposed labels
Milestone
null
Suggested next action
Call the existing markdown-link resolver (
UnifiedLinkResolver.resolve_links_for_compilationorresolve_markdown_links) fromclaude_formatter.pybefore folding instruction bodies intoCLAUDE.md, matching the AGENTS.md pipeline's existing behavior.Suggested issue comment
Per-lens notes (collapsed)
DevX UX Expert -- User-Need Reviewer
Real, well-diagnosed inconsistency: the exact same instruction resolves correctly when compiled to AGENTS.md but breaks when compiled to CLAUDE.md, purely because one pipeline calls the link resolver and the other doesn't. This directly undermines the "signpost + context fragment" authoring pattern for Claude users.
Supply Chain Security Expert -- Risk-Surface Reviewer
No risk-surface implication -- this is a link-resolution correctness bug in the compile pipeline, not a trust or lockfile boundary.
OSS Growth Hacker -- Contributor-Tone Reviewer
Not activated -- author_association is NONE but the report pinpoints the exact missing function call via grep evidence; standard precise, technical tone is appropriate.
Python Architect -- Architecture Reviewer
Not activated -- this is a small, well-isolated fix (call an existing resolver function that another formatter already uses) rather than a new interface or cross-cutting design decision.
Doc Writer -- Documentation Reviewer
Not activated -- internal compile-pipeline parity fix; no new user-facing doc surface implied.
APM CEO -- Triage Arbiter
Precisely diagnosed pipeline-parity bug with a known-good pattern to copy from the sibling AGENTS.md compiler. Accepting as theme/portability, area/multi-target, type/bug.
{ "decision": "accept", "decision_detail": "", "theme": "theme/portability", "areas": ["area/multi-target"], "type": "type/bug", "status": "status/accepted", "priority": null, "preserved_labels": [], "milestone": null, "next_action": "Call the existing markdown-link resolver from claude_formatter.py before folding instruction bodies into CLAUDE.md, matching the AGENTS.md pipeline's existing behavior.", "comment_markdown": "Confirmed via code inspection -- claude_formatter.py has no call to the link resolver that agents_compiler.py/distributed_compiler.py already use, so relative links inside folded instructions are emitted unadjusted and break once CLAUDE.md lives in a different directory than the source. This is accepted; the fix is to call the existing resolver from claude_formatter.py before returning generated content, mirroring the AGENTS.md pipeline exactly." }danielmeppiel commentedon Sep 12, 2026
At the lead maintainer's direction, the current acceptance of this item is withdrawn as part of APM's roadmap and contribution reset. Previous acceptance labels or comments must not be treated as approval to start or continue substantive implementation for inclusion in APM.
Thank you for the work and ideas already contributed. Our small maintainer team needs to agree supported scope and review capacity before inviting more implementation. This is not a rejection of the proposal or a judgment of your work, and it does not undo completed work or erase its history.
Please follow the contribution rules: a responsible human maintainer needs to record the bounded scope, acceptance criteria and review contact on the issue before substantive work resumes. Existing issue discussion, assignments and contributions remain available for that decision. Trivial typo and broken-link corrections retain their standing preapproval.
danielmeppiel commentedon Sep 24, 2026
Decision: approve
Area: project
Scope: Approve the bounded Claude compilation link-resolution repair described here, replacing the September 12 withdrawal for this work only. Reuse the existing link resolver so relative context and memory links in instruction bodies folded into CLAUDE.md resolve to their intended files from each generated output location, including local and dependency-provided instructions under normal compilation and forced instruction folding.
Done when: Regression coverage proves the affected links remain correct for local and dependency instructions, global and applyTo-scoped folding, and normal and forced instruction emission; repeated compilation does not reuse stale context locations; existing resolver containment and link-handling boundaries, unrelated content and other targets are preserved; directly affected user guidance remains accurate.
Out of scope: New link formats or arbitrary file access; relaxation of existing resolver or security boundaries; a compiler redesign; changes to unrelated target behavior; broader symlink support or resource deployment changes.
Review contact: Daniel Meppiel (@danielmeppiel)
test(compile): trap stale context registry and document CLAUDE.md lin…
fix(compile): resolve context links when folding instructions into CL… (