Skip to content

Commit 893a668

Browse files
fix: route --target codex/opencode through AGENTS.md compiler (#766) (#769)
AgentsCompiler.compile() only routed for ("vscode", "agents", "all") and ("claude", "all"), so `apm compile --target codex` (and opencode, minimal) fell through both branches and _merge_results([]) returned a successful empty result -- a silent no-op that left any existing AGENTS.md stale. - Route via should_compile_agents_md() / should_compile_claude_md() so target_detection.py is the single source of truth - Normalize copilot/agents aliases locally before routing - Fail loud on unknown targets instead of silently succeeding - CLI progress message uses get_target_description() so the banner matches the actual work performed for every target Co-authored-by: Daniel Meppiel <51440732+danielmeppiel@users.noreply.github.com>
1 parent de098ed commit 893a668

File tree

3 files changed

+118
-20
lines changed

3 files changed

+118
-20
lines changed

src/apm_cli/commands/compile/cli.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -380,23 +380,20 @@ def compile(
380380

381381
# Handle distributed vs single-file compilation
382382
if config.strategy == "distributed" and not single_agents:
383-
# Show target-aware message with detection reason
383+
# Show target-aware message with detection reason. Use
384+
# get_target_description() so any future target added to
385+
# target_detection shows up here automatically.
384386
if detected_target == "minimal":
385387
logger.progress(f"Compiling for AGENTS.md only ({detection_reason})")
386388
logger.progress(
387-
" Create .github/ or .claude/ folder for full integration",
389+
" Create .github/, .claude/, .codex/, .opencode/ or .cursor/ folder for full integration",
388390
symbol="light_bulb",
389391
)
390-
elif detected_target == "vscode" or detected_target == "agents":
391-
logger.progress(
392-
f"Compiling for AGENTS.md (VSCode/Copilot) - {detection_reason}"
393-
)
394-
elif detected_target == "claude":
392+
else:
393+
description = get_target_description(detected_target)
395394
logger.progress(
396-
f"Compiling for CLAUDE.md (Claude Code) - {detection_reason}"
395+
f"Compiling for {description} - {detection_reason}"
397396
)
398-
else: # "all"
399-
logger.progress(f"Compiling for AGENTS.md + CLAUDE.md - {detection_reason}")
400397

401398
if dry_run:
402399
logger.dry_run_notice(

src/apm_cli/compilation/agents_compiler.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@
2020
)
2121
from .link_resolver import resolve_markdown_links, validate_link_targets
2222
from ..utils.paths import portable_relpath
23+
from ..core.target_detection import should_compile_agents_md, should_compile_claude_md
24+
25+
26+
# User-facing target aliases that map to the canonical "vscode" target.
27+
# Kept in sync with target_detection.detect_target().
28+
_VSCODE_TARGET_ALIASES = ("copilot", "agents")
29+
_KNOWN_TARGETS = (
30+
"vscode", "claude", "cursor", "opencode", "codex", "all", "minimal",
31+
) + _VSCODE_TARGET_ALIASES
2332

2433

2534
@dataclass
@@ -199,17 +208,52 @@ def compile(self, config: CompilationConfig, primitives: Optional[PrimitiveColle
199208
exclude_patterns=config.exclude,
200209
)
201210

202-
# Route to targets based on config.target
211+
# Route to targets based on config.target.
212+
# Use target_detection helpers as the single source of truth so
213+
# new targets (codex, opencode, cursor, minimal, ...) route
214+
# correctly without touching this method again.
215+
routing_target = (
216+
"vscode" if config.target in _VSCODE_TARGET_ALIASES else config.target
217+
)
218+
219+
if routing_target not in _KNOWN_TARGETS and config.target not in _KNOWN_TARGETS:
220+
self.errors.append(
221+
f"Unknown compilation target: {config.target!r}. "
222+
f"Expected one of: {', '.join(sorted(set(_KNOWN_TARGETS)))}"
223+
)
224+
return CompilationResult(
225+
success=False,
226+
output_path="",
227+
content="",
228+
warnings=self.warnings.copy(),
229+
errors=self.errors.copy(),
230+
stats={},
231+
)
232+
203233
results: List[CompilationResult] = []
204-
205-
# AGENTS.md target (vscode/agents)
206-
if config.target in ("vscode", "agents", "all"):
234+
235+
if should_compile_agents_md(routing_target):
207236
results.append(self._compile_agents_md(config, primitives))
208-
209-
# CLAUDE.md target
210-
if config.target in ("claude", "all"):
237+
238+
if should_compile_claude_md(routing_target):
211239
results.append(self._compile_claude_md(config, primitives))
212-
240+
241+
# Defensive: should never happen for a known target, but guards
242+
# against future target_detection drift silently producing no-ops.
243+
if not results:
244+
self.errors.append(
245+
f"Target {config.target!r} did not route to any compiler. "
246+
"This is an internal bug in target routing."
247+
)
248+
return CompilationResult(
249+
success=False,
250+
output_path="",
251+
content="",
252+
warnings=self.warnings.copy(),
253+
errors=self.errors.copy(),
254+
stats={},
255+
)
256+
213257
# Merge results from all targets
214258
return self._merge_results(results)
215259

tests/unit/compilation/test_compile_target_flag.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,14 +166,71 @@ def test_target_all_generates_both(self, temp_project, sample_primitives):
166166
dry_run=True,
167167
single_agents=True # Use single-file for AGENTS.md
168168
)
169-
169+
170170
compiler = AgentsCompiler(str(temp_project))
171171
result = compiler.compile(config, sample_primitives)
172-
172+
173173
assert result.success
174174
# Output path should mention both targets
175175
assert "AGENTS.md" in result.output_path or "CLAUDE" in result.output_path
176176

177+
def test_target_codex_generates_agents_md(self, temp_project, sample_primitives):
178+
"""Regression for issue #766: --target codex must produce AGENTS.md, not a silent no-op."""
179+
config = CompilationConfig(
180+
target="codex",
181+
dry_run=True,
182+
single_agents=True,
183+
)
184+
185+
compiler = AgentsCompiler(str(temp_project))
186+
result = compiler.compile(config, sample_primitives)
187+
188+
assert result.success
189+
assert result.output_path, "codex target must route to a compiler, not return empty"
190+
assert "AGENTS.md" in result.output_path
191+
192+
def test_target_opencode_generates_agents_md(self, temp_project, sample_primitives):
193+
"""target='opencode' must route to AGENTS.md (same universal format as codex)."""
194+
config = CompilationConfig(
195+
target="opencode",
196+
dry_run=True,
197+
single_agents=True,
198+
)
199+
200+
compiler = AgentsCompiler(str(temp_project))
201+
result = compiler.compile(config, sample_primitives)
202+
203+
assert result.success
204+
assert "AGENTS.md" in result.output_path
205+
206+
def test_target_minimal_generates_agents_md(self, temp_project, sample_primitives):
207+
"""target='minimal' must route to AGENTS.md-only."""
208+
config = CompilationConfig(
209+
target="minimal",
210+
dry_run=True,
211+
single_agents=True,
212+
)
213+
214+
compiler = AgentsCompiler(str(temp_project))
215+
result = compiler.compile(config, sample_primitives)
216+
217+
assert result.success
218+
assert "AGENTS.md" in result.output_path
219+
220+
def test_unknown_target_returns_failure(self, temp_project, sample_primitives):
221+
"""Unknown target must fail explicitly instead of silently succeeding."""
222+
config = CompilationConfig(
223+
target="not-a-real-target",
224+
dry_run=True,
225+
single_agents=True,
226+
)
227+
228+
compiler = AgentsCompiler(str(temp_project))
229+
result = compiler.compile(config, sample_primitives)
230+
231+
assert result.success is False
232+
assert any("Unknown compilation target" in e for e in result.errors)
233+
177234

178235
class TestMergeResults:
179236
"""Tests for _merge_results() method."""

0 commit comments

Comments
 (0)