fix(compile): forward target to watch-mode recompile (closes #1345)#1349
Open
danielmeppiel wants to merge 1 commit into
Open
fix(compile): forward target to watch-mode recompile (closes #1345)#1349danielmeppiel wants to merge 1 commit into
danielmeppiel wants to merge 1 commit into
Conversation
apm compile --watch silently regenerated GEMINI.md even when apm.yml declared targets: [claude, cursor]. The non-watch path correctly omits GEMINI.md (#1019/#1074) because it resolves the effective target and forwards it as target= into CompilationConfig.from_apm_yml. The watch path bypassed that resolution and let the dataclass default fall back to the all-families fanout on every recompile. Fix: - Extract _resolve_effective_target() in commands/compile/cli.py so the resolution (apm.yml target/targets load + _resolve_compile_target + detect_target fallback) lives in one place. - Hoist the call above the if watch: branch and pass effective_target (plus the user-facing target / config_target labels) into _watch_mode. - Promote APMFileHandler to module scope, accept effective_target, and forward it as target= into both the initial compile and every debounced recompile. - Surface the same family-aware 'Compiling for AGENTS.md + CLAUDE.md (...)' label the one-shot path emits, so users see parity. Tests: tests/unit/commands/compile/test_watch_target_forwarding.py pins the regression with an outcome assertion (should_compile_gemini_md is False for the captured target) so the all-families fanout cannot silently come back. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes a regression where apm compile --watch did not forward the resolved compile target into CompilationConfig.from_apm_yml(...), causing watch-mode recompiles to fan out to all compiler families and regenerate GEMINI.md even when the project’s configured targets excluded Gemini.
Changes:
- Added a shared
_resolve_effective_target(...)helper in the compile CLI and used it in the watch-mode branch to compute/forward the same effective target used by one-shot compile. - Refactored the watch implementation to accept and forward
effective_targetinto both initial compile and debounced recompiles, plus added a target-aware “Compiling for …” progress label. - Added unit regression tests that capture the forwarded
target=and assert the “no GEMINI.md” outcome fortargets: [claude, cursor].
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/commands/compile/cli.py |
Adds _resolve_effective_target() and uses it to pass the resolved target + labels into watch mode. |
src/apm_cli/commands/compile/watcher.py |
Promotes APMFileHandler for testability, threads effective_target through initial and incremental compiles, and adds a watch-mode target label formatter. |
tests/unit/commands/compile/test_watch_target_forwarding.py |
Adds regression tests ensuring watch-mode recompiles forward target= and don’t imply Gemini output for claude+cursor. |
CHANGELOG.md |
Documents the watch-mode target forwarding fix under “Fixed”. |
Copilot's findings
- Files reviewed: 4/5 changed files
- Comments generated: 2
| return target # single string pass-through | ||
|
|
||
|
|
||
| def _resolve_effective_target(target): |
Comment on lines
+10
to
+15
| def _format_target_label(effective_target, target_label_user, target_label_config): | ||
| """Render a one-shot-parity 'Compiling for ...' label for the watch path. | ||
|
|
||
| Mirrors the family-aware label the one-shot compile path emits so the | ||
| user sees the same string in watch mode (#1345). | ||
| """ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
apm compile --watchsilently regeneratedGEMINI.mdeven whenapm.ymldeclaredtargets: [claude, cursor]. The watch path bypassed the target-resolver the one-shot path uses, so every recompile fell back to the all-families fanout. This PR factors the resolver into a shared helper, hoists it above theif watch:branch, and forwardstarget=into everyCompilationConfig.from_apm_yml(...)call the watcher makes. Adds a 3-test regression suite that pins the contract via an outcome assertion (should_compile_gemini_mdisFalsefor the captured target). Closes #1345.Problem (WHY)
From #1345:
The one-shot path computes an
effective_target(lines 506-531 ofcli.py) by resolving the CLI flag, then the apm.ymltarget:/targets:value, then auto-detection. The watch path skipped this entire block, so users on a Claude+Cursor stack sawGEMINI.mdreappear on every save.Approach (WHAT)
Two narrow surface changes inside
src/apm_cli/commands/compile/:cli.py-- Extract_resolve_effective_target(target)that returns(effective_target, detection_reason, config_target). The watch branch calls it before delegating to_watch_modeand forwards the resolved value plus the user-facing labels.watcher.py-- PromoteAPMFileHandlerto module scope (so unit tests can instantiate it without spinning up watchdogObserver). Accepteffective_targetin__init__and forward it astarget=into both the initial-compile and per-edit-recompile calls toCompilationConfig.from_apm_yml(...). Render a one-shot-parity "Compiling for AGENTS.md + CLAUDE.md (apm.yml target: [claude, cursor])" progress line so users see the same string in watch mode.The one-shot inline resolution at lines 463-531 of
cli.pyis left untouched on purpose: identical behavior, single round-trip with the helper in the watch branch only. Less restructuring, fully reversible.Implementation (HOW)
sequenceDiagram participant User participant CLI as cli.py (compile) participant Resolver as _resolve_effective_target participant Watcher as _watch_mode participant Handler as APMFileHandler participant Config as CompilationConfig.from_apm_yml participant Compiler as AgentsCompiler User->>CLI: apm compile --watch (apm.yml targets: [claude, cursor]) CLI->>Resolver: target=None, reads apm.yml Resolver-->>CLI: effective_target=frozenset({"claude","agents"}) CLI->>Watcher: _watch_mode(..., effective_target, target_label_config=[claude,cursor]) Watcher->>Handler: APMFileHandler(..., effective_target) Watcher->>Config: from_apm_yml(target=effective_target, ...) Config-->>Watcher: config (target=frozenset) Watcher->>Compiler: compile(config) -> CLAUDE.md + AGENTS.md (no GEMINI.md) Note over User,Handler: User edits .apm/instructions/foo.md Handler->>Handler: on_modified -> _recompile Handler->>Config: from_apm_yml(target=self.effective_target, ...) Config-->>Handler: config (target=frozenset, same as initial) Handler->>Compiler: compile(config) -> CLAUDE.md + AGENTS.md (still no GEMINI.md)Files touched
src/apm_cli/commands/compile/cli.py-- new helper_resolve_effective_target(); watch branch hoisted to call it.src/apm_cli/commands/compile/watcher.py-- module-scopeAPMFileHandler, neweffective_targetparameter, forwardstarget=into bothfrom_apm_ymlcalls, family-aware progress label.tests/unit/commands/compile/test_watch_target_forwarding.py-- 3 regression tests (frozenset target,None, single-string target) with an outcome assertion (should_compile_gemini_md(captured_target) is False).CHANGELOG.md-- one line under### Fixed.Trade-offs
apm.ymlonce per session; the resolver runs once before the watch loop starts. Re-running it on every recompile would honor liveapm.ymledits, but the existingon_modifiedfilter watchesapm.ymland the dataclass default-merge infrom_apm_ymlalready picks up most config drift on each call. Adding live re-resolution is a follow-up if requested.compilation/agents_compiler.py. The bug lives entirely in the watch-path call site.cli.pyto also call the new_resolve_effective_targethelper for a true single-source-of-truth pass -- intentionally deferred here to keep the diff small.Validation evidence
Lint (canonical contract per
.github/instructions/linting.instructions.md):Regression test suite (new):
Broader sweep (no collateral damage):
How to test
Reproducing the original report:
Create a fresh project directory and a minimal
apm.ymldeclaringtargets: [claude, cursor]:Add a primitive at
.apm/instructions/foo.instructions.mdwithapplyTo: "**".Run
apm compile --watchin that directory.In another terminal, append a newline to
.apm/instructions/foo.instructions.mdto trigger a recompile.Stop the watcher (Ctrl-C).
List the directory.
Expected after fix:
AGENTS.mdandCLAUDE.mdpresent,GEMINI.mdNOT present. Before the fix,GEMINI.mdwas regenerated on every recompile.The new regression test asserts the same outcome programmatically:
uv run --extra dev pytest tests/unit/commands/compile/test_watch_target_forwarding.py -x -vShip recommendation (CEO synthesis)
Ship as-is. The fix is a minimal, reversible plumbing change scoped to two files in
commands/compile/plus a focused regression test that asserts the outcome (noGEMINI.mdin the targets-claude-cursor case) rather than just the plumbing. The watch-mode UX now matches one-shot parity, including the family-aware "Compiling for AGENTS.md + CLAUDE.md (apm.yml target: [claude, cursor])" progress label. Deferred follow-up: collapsing the inline one-shot resolution into the new helper for a true single source of truth.