From c6b7f7abb0ed3d1387a5916234b3c5f865f56aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A8=B1=E5=85=83=E8=B1=AA?= <146086744+edenfunf@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:23:53 +0800 Subject: [PATCH] fix: route --target codex/opencode through AGENTS.md compiler (#766) 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 --- src/apm_cli/commands/compile/cli.py | 17 +++--- src/apm_cli/compilation/agents_compiler.py | 60 +++++++++++++++--- .../compilation/test_compile_target_flag.py | 61 ++++++++++++++++++- 3 files changed, 118 insertions(+), 20 deletions(-) diff --git a/src/apm_cli/commands/compile/cli.py b/src/apm_cli/commands/compile/cli.py index a09440654..1432af97a 100644 --- a/src/apm_cli/commands/compile/cli.py +++ b/src/apm_cli/commands/compile/cli.py @@ -380,23 +380,20 @@ def compile( # Handle distributed vs single-file compilation if config.strategy == "distributed" and not single_agents: - # Show target-aware message with detection reason + # Show target-aware message with detection reason. Use + # get_target_description() so any future target added to + # target_detection shows up here automatically. if detected_target == "minimal": logger.progress(f"Compiling for AGENTS.md only ({detection_reason})") logger.progress( - " Create .github/ or .claude/ folder for full integration", + " Create .github/, .claude/, .codex/, .opencode/ or .cursor/ folder for full integration", symbol="light_bulb", ) - elif detected_target == "vscode" or detected_target == "agents": - logger.progress( - f"Compiling for AGENTS.md (VSCode/Copilot) - {detection_reason}" - ) - elif detected_target == "claude": + else: + description = get_target_description(detected_target) logger.progress( - f"Compiling for CLAUDE.md (Claude Code) - {detection_reason}" + f"Compiling for {description} - {detection_reason}" ) - else: # "all" - logger.progress(f"Compiling for AGENTS.md + CLAUDE.md - {detection_reason}") if dry_run: logger.dry_run_notice( diff --git a/src/apm_cli/compilation/agents_compiler.py b/src/apm_cli/compilation/agents_compiler.py index f06fc1927..4d76c6a22 100644 --- a/src/apm_cli/compilation/agents_compiler.py +++ b/src/apm_cli/compilation/agents_compiler.py @@ -20,6 +20,15 @@ ) from .link_resolver import resolve_markdown_links, validate_link_targets from ..utils.paths import portable_relpath +from ..core.target_detection import should_compile_agents_md, should_compile_claude_md + + +# User-facing target aliases that map to the canonical "vscode" target. +# Kept in sync with target_detection.detect_target(). +_VSCODE_TARGET_ALIASES = ("copilot", "agents") +_KNOWN_TARGETS = ( + "vscode", "claude", "cursor", "opencode", "codex", "all", "minimal", +) + _VSCODE_TARGET_ALIASES @dataclass @@ -199,17 +208,52 @@ def compile(self, config: CompilationConfig, primitives: Optional[PrimitiveColle exclude_patterns=config.exclude, ) - # Route to targets based on config.target + # Route to targets based on config.target. + # Use target_detection helpers as the single source of truth so + # new targets (codex, opencode, cursor, minimal, ...) route + # correctly without touching this method again. + routing_target = ( + "vscode" if config.target in _VSCODE_TARGET_ALIASES else config.target + ) + + if routing_target not in _KNOWN_TARGETS and config.target not in _KNOWN_TARGETS: + self.errors.append( + f"Unknown compilation target: {config.target!r}. " + f"Expected one of: {', '.join(sorted(set(_KNOWN_TARGETS)))}" + ) + return CompilationResult( + success=False, + output_path="", + content="", + warnings=self.warnings.copy(), + errors=self.errors.copy(), + stats={}, + ) + results: List[CompilationResult] = [] - - # AGENTS.md target (vscode/agents) - if config.target in ("vscode", "agents", "all"): + + if should_compile_agents_md(routing_target): results.append(self._compile_agents_md(config, primitives)) - - # CLAUDE.md target - if config.target in ("claude", "all"): + + if should_compile_claude_md(routing_target): results.append(self._compile_claude_md(config, primitives)) - + + # Defensive: should never happen for a known target, but guards + # against future target_detection drift silently producing no-ops. + if not results: + self.errors.append( + f"Target {config.target!r} did not route to any compiler. " + "This is an internal bug in target routing." + ) + return CompilationResult( + success=False, + output_path="", + content="", + warnings=self.warnings.copy(), + errors=self.errors.copy(), + stats={}, + ) + # Merge results from all targets return self._merge_results(results) diff --git a/tests/unit/compilation/test_compile_target_flag.py b/tests/unit/compilation/test_compile_target_flag.py index 01df31c38..9faac208d 100644 --- a/tests/unit/compilation/test_compile_target_flag.py +++ b/tests/unit/compilation/test_compile_target_flag.py @@ -166,14 +166,71 @@ def test_target_all_generates_both(self, temp_project, sample_primitives): dry_run=True, single_agents=True # Use single-file for AGENTS.md ) - + compiler = AgentsCompiler(str(temp_project)) result = compiler.compile(config, sample_primitives) - + assert result.success # Output path should mention both targets assert "AGENTS.md" in result.output_path or "CLAUDE" in result.output_path + def test_target_codex_generates_agents_md(self, temp_project, sample_primitives): + """Regression for issue #766: --target codex must produce AGENTS.md, not a silent no-op.""" + config = CompilationConfig( + target="codex", + dry_run=True, + single_agents=True, + ) + + compiler = AgentsCompiler(str(temp_project)) + result = compiler.compile(config, sample_primitives) + + assert result.success + assert result.output_path, "codex target must route to a compiler, not return empty" + assert "AGENTS.md" in result.output_path + + def test_target_opencode_generates_agents_md(self, temp_project, sample_primitives): + """target='opencode' must route to AGENTS.md (same universal format as codex).""" + config = CompilationConfig( + target="opencode", + dry_run=True, + single_agents=True, + ) + + compiler = AgentsCompiler(str(temp_project)) + result = compiler.compile(config, sample_primitives) + + assert result.success + assert "AGENTS.md" in result.output_path + + def test_target_minimal_generates_agents_md(self, temp_project, sample_primitives): + """target='minimal' must route to AGENTS.md-only.""" + config = CompilationConfig( + target="minimal", + dry_run=True, + single_agents=True, + ) + + compiler = AgentsCompiler(str(temp_project)) + result = compiler.compile(config, sample_primitives) + + assert result.success + assert "AGENTS.md" in result.output_path + + def test_unknown_target_returns_failure(self, temp_project, sample_primitives): + """Unknown target must fail explicitly instead of silently succeeding.""" + config = CompilationConfig( + target="not-a-real-target", + dry_run=True, + single_agents=True, + ) + + compiler = AgentsCompiler(str(temp_project)) + result = compiler.compile(config, sample_primitives) + + assert result.success is False + assert any("Unknown compilation target" in e for e in result.errors) + class TestMergeResults: """Tests for _merge_results() method."""