fix(compile): live-reload apm.yml and warn on --clean --watch#1403
fix(compile): live-reload apm.yml and warn on --clean --watch#1403edenfunf wants to merge 3 commits into
Conversation
Two behaviors microsoft#1349 left on the table: 1. `apm compile --watch` re-runs target resolution against the current `apm.yml` when `apm.yml` itself is the file event source. Pre-fix the value resolved at startup was reused for every recompile, so mid-session edits to `target:` / `targets:` did nothing until the watcher was restarted. Re-resolution is gated to the file that can change the answer (basename match -- not `endswith` -- so a stray `backup_apm.yml` cannot masquerade as the project root manifest); instruction-file edits keep using the startup snapshot. 2. `apm compile --watch --clean` prints an explicit warning that `--clean` is ignored in watch mode, then continues. Pre-fix the flag was silently dropped. CLI `--target X` still outranks mid-session `apm.yml` edits, matching the one-shot path's priority order: the resolver receives the raw `cli_target` on every re-run and applies the same precedence rules. Lazy `from .cli import _resolve_effective_target` inside `_recompile` to break the cli -> watcher -> cli import cycle.
There was a problem hiding this comment.
Pull request overview
This PR improves apm compile --watch parity with one-shot compile by (1) re-resolving targets when apm.yml itself changes and (2) surfacing an explicit warning when --clean is used with --watch (since --clean is ignored in watch mode).
Changes:
- Re-resolve the effective compile target when the changed file is
apm.yml, so mid-sessiontargets:edits can take effect. - Emit a warning for
apm compile --watch --cleaninstead of silently dropping--clean. - Add unit tests covering live reload gating behavior and the
--cleanwarning, plus update the Unreleased changelog.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/apm_cli/commands/compile/cli.py |
Adds a --clean+--watch warning and plumbs raw --target (cli_target) into watch mode. |
src/apm_cli/commands/compile/watcher.py |
Stores cli_target and conditionally re-resolves the effective target when apm.yml changes. |
tests/unit/commands/compile/test_watch_live_reload_and_clean_warning.py |
New regression tests for apm.yml-triggered re-resolution and the --clean watch-mode warning behavior. |
CHANGELOG.md |
Adds two Unreleased “Changed” entries describing the new watch-mode behavior. |
Comments suppressed due to low confidence (1)
src/apm_cli/commands/compile/watcher.py:167
- The
_watch_modedocstring says recompiles re-run the resolver against the currentapm.yml, but the implementation only re-resolves when the changed file’s basename is exactlyapm.yml(instruction-file events explicitly skip it). Please adjust the wording to reflect the actual gating (and, if you persist the updated target, mention the caching behavior).
``cli_target`` is the raw ``--target`` argument; recompiles re-run
the resolver against the current apm.yml so mid-session edits to
``targets:`` take effect on the next file event without restarting
the watcher.
…ent watch contract Three review comments from PR microsoft#1403: 1. After an apm.yml-driven re-resolve, persist the fresh value to `self.effective_target` so subsequent non-apm.yml events do not silently revert to the startup snapshot. Without this, the sequence `apm.yml edit -> instructions edit` emits the new family set on the first recompile and the wrong family set on the second -- AGENTS.md / GEMINI.md written by the apm.yml event become stale until the next apm.yml edit. New test `test_apm_yml_change_persists_fresh_target_for_subsequent_events` pins the sequence end-to-end. 2. Append (microsoft#1403) to both CHANGELOG entries to match the project's one-line-per-PR Keep-a-Changelog convention. 3. Document the two new watch-mode behaviors in the CLI reference: apm.yml `target:` / `targets:` mid-session live-reload (with the CLI `--target` priority note) and the `--clean` warning.
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 2 | 1 | Lazy import breaks a real cycle cleanly; in-place self.effective_target mutation is correct and tested; basename gate is better than endswith; one thread-safety gap if watchdog delivers concurrent events. |
| CLI Logging Expert | 0 | 1 | 1 | Warning renders correctly as [!] yellow; missing re-resolve log when apm.yml triggers reload is the only meaningful gap. |
| DevX UX Expert | 0 | 2 | 2 | Two solid UX fixes; one recommended gap: malformed apm.yml mid-watch has no graceful recovery path in _recompile, risking a silent watcher crash. |
| Supply Chain Security Expert | 0 | 1 | 1 | No blocking security regressions. basename guard is correct; lazy relative import is safe. One silent fail-open on YAML parse worth hardening. |
| OSS Growth Hacker | 0 | 1 | 2 | Solid DX fix that removes a frustrating restart-to-reload loop; CHANGELOG is accurate but undersells the "no restart needed" wow moment. |
| Doc Writer | 1 | 2 | 1 | Two recommended fixes: "at startup" is ambiguous (should be "when the watcher starts"), and --target priority rule needs one word of clarification for first-time readers. |
| Test Coverage Expert | 0 | 2 | 1 | 7 new tests cover all stated promises at unit/CliRunner tier; two gaps remain: apm.yml-parse-error swallowing and list[str] cli_target threading through re-resolve. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [DevX UX Expert + Supply Chain + Test Coverage] Add try/except around
_resolve_effective_targetin the apm.yml event handler; warn user and keep previous target on parse error; add missing unit test asserting error is surfaced not swallowed. -- Three-panelist convergence on a user-promise gap backed by a missing-evidence block on a resilience-critical surface; silently dropping config mid-watch is the most impactful correctness risk in the PR. - [Doc Writer] Change "a [!] warning is printed at startup" to "a [!] warning is printed when the watcher starts" in compile.md. -- Fast single-word fix that removes genuine first-reader ambiguity; labeled blocking by doc-writer and should ship in the same PR or an immediate follow commit.
- [CLI Logging Expert] Emit a progress line before
_resolve_effective_targetso users know targets are being re-read when apm.yml changes (not just "Recompiling..."). -- Closes a UX visibility gap that leaves the user unable to confirm live-reload actually fired; directly reinforces the "no restart needed" story. - [DevX UX Expert] Extend the --clean --watch warning to include a one-sentence explanation of WHY the two flags are incompatible. -- Without the causal "why", users will keep retrying the combined form; the explanation is also the seed for a FAQ entry.
- [Python Architect] Add
threading.Lockin__init__and wrap theself.effective_targetread-modify-write block; open a follow-up issue tracking the race window. -- Real race exists even with debounce on rapid burst events; low-probability but the fix is two lines and the debt should not accumulate.
Architecture
classDiagram
direction LR
class APMFileHandler {
<<EventHandler>>
+output str
+chatmode str|None
+effective_target CompileTargetType|None
+cli_target str|list|None
+debounce_delay float
+on_modified(event) None
+_recompile(changed_file) None
}
class FileSystemEventHandler {
<<watchdog>>
+on_modified(event) None
}
class CommandLogger {
<<Logger>>
+progress(msg) None
+warning(msg) None
}
class CompilationConfig {
<<ValueObject>>
+from_apm_yml(...) CompilationConfig
}
class AgentsCompiler {
+compile(config, logger) CompileResult
}
class _resolve_effective_target {
<<PureFunction>>
+__call__(cli_target) tuple
}
FileSystemEventHandler <|-- APMFileHandler : extends
APMFileHandler *-- CommandLogger : owns
APMFileHandler ..> CompilationConfig : creates
APMFileHandler ..> AgentsCompiler : creates
APMFileHandler ..> _resolve_effective_target : lazy import on apm.yml event
class APMFileHandler:::touched
class _resolve_effective_target:::touched
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A(["apm compile --watch"])
B{"--clean flag?"}
C["logger.warning: --clean ignored in watch mode"]
D["_resolve_effective_target(target)"]
E["_watch_mode(cli_target=target)"]
F["APMFileHandler.__init__\nself.effective_target = snapshot\nself.cli_target = cli_target"]
G["Observer.start() -- watchdog thread"]
H(["File event arrives"])
I{"basename(changed_file)\n== apm.yml?"}
J["lazy import _resolve_effective_target\nre-reads apm.yml from disk"]
K["self.effective_target = fresh_target\n(no lock -- see finding)"]
L["CompilationConfig.from_apm_yml(target=effective_target)"]
M["AgentsCompiler.compile(config)"]
N(["logger.success / logger.error"])
A --> B
B -- yes --> C
C --> D
B -- no --> D
D --> E
E --> F
F --> G
G --> H
H --> I
I -- yes --> J
J --> K
K --> L
I -- no --> L
L --> M
M --> N
Recommendation
Ship PR #1403. The two behavioral fixes are correct, tested, documented, and CHANGELOG'd. No panelist identified a blocking code correctness regression. The panel's highest-signal gap -- missing graceful fallback on malformed apm.yml -- is a hardening improvement, not a regression introduced by this PR (the watcher had no apm.yml event handling at all before this change). Track it as a priority follow-up issue alongside the doc wording fix and the --clean warning copy improvement. The thread-safety gap warrants an issue but not a block. Merge, then iterate fast.
Full per-persona findings
Python Architect
-
[recommended] self.effective_target mutated without a lock; concurrent watchdog events can race at
src/apm_cli/commands/compile/watcher.py
Watchdog dispatches events on a background thread. If two rapid apm.yml events arrive and the debounce guard is satisfied, two _recompile calls can interleave. Python's GIL serialises bytecode but not the read-modify-write sequence across the two attribute accesses.
Suggested: Addthreading.Lockin__init__and wrap theif os.path.basename(changed_file) == APM_YML_FILENAME:block withwith self._target_lock:to serialise target updates. -
[recommended] Lazy import in _recompile re-executes on every apm.yml event; module docstring lacks cycle explanation at
src/apm_cli/commands/compile/watcher.py
Python caches modules after first import so the performance hit is negligible, but the cycle is real and should be documented in the module docstring for future refactors.
Suggested: Add a module-level note:# cli.py imports this module; _resolve_effective_target is imported lazily in _recompile to break the cycle. -
[nit]
os.path.basenamecould bePath(changed_file).namefor pathlib consistency atsrc/apm_cli/commands/compile/watcher.py
Minor style nit; both are correct.
CLI Logging Expert
-
[recommended] No progress message when apm.yml triggers target re-resolution at
src/apm_cli/commands/compile/watcher.py
User sees "File changed: apm.yml" and "Recompiling..." but never learns that targets were re-read from disk. If the resolve picks up a new target, the output changes with no explanation.
Suggested: Emitself.logger.progress('apm.yml changed -- re-resolving targets...', symbol='gear')before calling_resolve_effective_target. -
[nit] Warning message leads with a flag name rather than the outcome at
src/apm_cli/commands/compile/cli.py
APM message rule: lead with the outcome.
Suggested: Rephrase to: "Orphaned outputs will not be removed mid-session -- --clean is ignored in watch mode. Run 'apm compile --clean' separately between sessions."
DevX UX Expert
-
[recommended] No graceful fallback when
_resolve_effective_targetthrows mid-watch (malformed apm.yml) atsrc/apm_cli/commands/compile/watcher.py
Any parse or FileNotFound error propagates up through the watchdog event handler, likely killing the watcher with a Python traceback. Correct behavior: warn and keep previous target.
Suggested: Wrap_resolve_effective_targetcall in try/except, emitlogger.warningwith error summary and "keeping previous target", continue withself.effective_targetunchanged. -
[recommended] Warning message omits WHY --clean is incompatible with --watch at
src/apm_cli/commands/compile/cli.py
Without the "why", users keep retrying the combined form thinking it is a transient issue.
Suggested: Extend to: "--clean is ignored in watch mode (running it on every recompile would remove outputs mid-session); run 'apm compile --clean' separately between watch sessions." -
[nit] compile.md says "[!] warning is printed at startup" but fires before the watcher loop at
docs/src/content/docs/reference/cli/compile.md
Suggested: Change to "a [!] warning is printed before the watcher starts" for precision. -
[nit] --target priority rule only in watch-mode bullet, not alongside the --target flag description at
docs/src/content/docs/reference/cli/compile.md
Suggested: Add a parenthetical to the --target flag description: "(in --watch mode, outranks apm.yml targets: on every recompile)".
Supply Chain Security Expert
-
[recommended] Silent exception swallow on YAML parse in
_resolve_effective_targetproduces fail-open behavior mid-watch atsrc/apm_cli/commands/compile/cli.py
cli.py swallows all exceptions during the parse_targets_field / load_yaml path with bareexcept Exception: pass. If apm.yml is partially written mid-watch, the resolver silently drops config_target to None and falls back to auto-detect with no user warning.
Suggested: Replaceexcept Exception: passwithexcept Exception as exc: logger.warning(f'apm.yml target parse failed: {exc}; falling back to auto-detect'). -
[nit] on_modified endswith filter admits backup_apm.yml events; basename guard downstream is correct but inconsistent at
src/apm_cli/commands/compile/watcher.py
Suggested: Change toos.path.basename(src_path) == APM_YML_FILENAMEto match the basename guard in_recompile.
OSS Growth Hacker
-
[recommended] CHANGELOG entry buries the lead -- "no restart needed" should open the sentence, not appear mid-clause
Power users scanning release notes make a split-second judgment on whether to upgrade.
Suggested: Rewrite to: "apm compile --watchno longer requires a restart when you edittarget:/targets:inapm.yml-- changes take effect on the next file event. Previously the startup value was cached for the whole session. (fix(compile): live-reload apm.yml and warn on --clean --watch #1403)" -
[nit] Docs bullet phrased as caveat, not capability; could use a Starlight tip admonition
Suggested: Wrap in:::tipadmonition: "Tip -- edit apm.yml mid-watch: target: / targets: changes take effect automatically on the next save." -
[nit] --clean warning copy could seed a docs FAQ / error-message index
Suggested: Add one FAQ bullet under a "Watch mode pitfalls" heading in compile.md.
Auth Expert -- inactive
No auth surface touched; changed files are scoped entirely to the compile --watch live-reload and --clean warning feature.
Doc Writer
-
[blocking] "a [!] warning is printed at startup" is ambiguous -- "startup" could mean process startup, not watch invocation at
docs/src/content/docs/reference/cli/compile.md
A first-time reader could interpret "startup" as application/process startup. (Note: CEO weighs this as a nit for ship-gating purposes; it is a fast single-word fix, not a code correctness issue.)
Suggested: Change to "a [!] warning is printed when the watcher starts". -
[recommended] The --target priority rule omits "over apm.yml" -- the override relationship is implicit at
docs/src/content/docs/reference/cli/compile.md
The word "still" implies the reader already knows the resolution order.
Suggested: Drop "still" and write: "Passing --target to apm compile --watch takes precedence over apm.yml target:/targets:." -
[recommended] No doc for what happens when apm.yml has a parse/syntax error mid-watch at
docs/src/content/docs/reference/cli/compile.md
The new bullet documents the happy path but omits the error path.
Suggested: Add: "If apm.yml is unparseable at the time of the file event, the recompile is skipped and an error is printed; the watcher continues running." -
[nit] CHANGELOG --clean entry is slightly longer than surrounding entries; trim remediation sentence at
CHANGELOG.md
Suggested: Trim to: "apm compile --watch --cleannow prints a[!]warning that--cleanis ignored in watch mode instead of silently dropping the flag. (fix(compile): live-reload apm.yml and warn on --clean --watch #1403)"
Test Coverage Expert
-
[recommended] apm.yml syntax-error mid-watch is silently swallowed; no test asserts user sees error message rather than silence at
tests/unit/commands/compile/test_watch_live_reload_and_clean_warning.py
No test exercises the path where_resolve_effective_targetraises. Probed tests/unit/commands/compile/ for YAMLError, ParseError, syntax, except -- zero hits.
Proof (missing at unit):tests/unit/commands/compile/test_watch_live_reload_and_clean_warning.py::test_recompile_on_apm_yml_change_resolver_raises_logs_error_not_silent-- proves: If apm.yml becomes unparseable mid-watch, the user sees an error message rather than a silent no-op. -
[recommended] Live-reload certified only at unit tier; integration-with-fixtures floor not met for CLI-visible watch-mode behavior change at
tests/unit/commands/compile/test_watch_live_reload_and_clean_warning.py
Tests 1-5 mock all module boundaries. Tests 6-7 use CliRunner with real apm.yml but mock _watch_mode entirely.
Proof (passed at unit):tests/unit/commands/compile/test_watch_live_reload_and_clean_warning.py::test_recompile_on_apm_yml_change_reresolves_against_current_file-- proves: apm.yml change mid-watch causes resolver to be called and fresh target forwarded to CompilationConfig.assert mock_from_apm_yml.call_args.kwargs["target"] == fresh -
[nit] cli_target as list[str] not exercised through the re-resolve path at
tests/unit/commands/compile/test_watch_live_reload_and_clean_warning.py
All five unit tests use cli_target=None or cli_target="claude". The list[str] branch goes through _resolve_effective_target untested.
Proof (missing at unit):tests/unit/commands/compile/test_watch_live_reload_and_clean_warning.py::test_recompile_on_apm_yml_change_with_cli_target_list_keeps_cli_priority-- proves: apm compile --watch --target claude --target gemini re-resolves with list cli_target, not just single string.
This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.
Note
🔒 Integrity filter blocked 2 items
The following items were blocked because they don't meet the GitHub integrity level.
- #1403
pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved". - fix(compile): live-reload apm.yml and warn on --clean --watch #1403
pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".
To allow these resources, lower min-integrity in your GitHub frontmatter:
tools:
github:
min-integrity: approved # merged | approved | unapproved | noneGenerated by PR Review Panel for issue #1403 · ● 3.3M · ◷
fix(compile): live-reload apm.yml and warn on --clean --watch
TL;DR
Two behaviors #1349 left on the table when it closed #1345, picked up at @danielmeppiel's invitation in #1351 (comment):
apm compile --watchre-readsapm.ymlwhenapm.ymlitself is modified. Editingtarget:/targets:mid-watch now takes effect on the next file event instead of needing a watcher restart. Pre-fix the value resolved at startup was reused for every recompile, so mid-session edits silently did nothing.apm compile --watch --cleanprints an explicit[!]warning that--cleanis ignored, then continues. Pre-fix the flag was silently dropped. Running--cleanon every recompile would surprise users mid-session by deleting orphans; running it only on the initial compile would re-introduce a watcher-specific code path — the same trap fix(compile): forward target to watch-mode recompile (closes #1345) #1349 exists to remove. To clean orphaned outputs, runapm compile --cleanseparately between watch sessions.Design notes I expect a reviewer to ask about
_recompileonly calls_resolve_effective_targetwhenos.path.basename(changed_file) == APM_YML_FILENAME. Instruction-file edits keep using the startup snapshot, so.instructions.mdedits don't pay an extra resolver round-trip. Basename equality (notendswith) so a straybackup_apm.ymlcannot masquerade as the project root manifest.--targetstill wins overapm.yml. Mid-session edits toapm.yml'stargets:are ignored when the watcher was launched with--target X, matching the one-shot path's priority order. The resolver receives the rawcli_targetand applies the same precedence rules. Pinned bytest_recompile_on_apm_yml_change_with_cli_target_keeps_cli_priority.target_label_userandcli_targetcarry the same value at the call site but have different roles.target_label_user(paired withtarget_label_config) feeds the startupCompiling for ...label;cli_targetis the resolver input on apm.yml change. Kept distinct so the label path and the re-resolve path don't accidentally fuse.from .cli import _resolve_effective_target) inside_recompileto break thecli.py → watcher.py → cli.pycycle. Same approach fix(compile): --watch path honors apm.yml targets and --target flag #1351 documented.How to test
--clean --watchwarningMid-session
apm.ymlreload (both directions)End-to-end run on Windows 11 + watchdog confirmed the watcher log shows
[>] File changed: .\apm.ymlfollowed by the correct emission set in both directions; mtimes confirm only the freshly-relevant outputs are rewritten.Files changed
src/apm_cli/commands/compile/cli.py—--clean --watchwarning in theif watch:branch; forward rawcli_target=targetinto_watch_mode.src/apm_cli/commands/compile/watcher.py—APMFileHandlerstorescli_target;_recompilere-resolves when the changed file's basename isapm.yml;_watch_modeplumbscli_targetthrough; docstring updated to describe the snapshot-vs-fresh contract.tests/unit/commands/compile/test_watch_live_reload_and_clean_warning.py— six tests:test_recompile_on_apm_yml_change_reresolves_against_current_file— core reload behavior.test_recompile_on_instruction_file_change_uses_snapshot— non-apm.yml edits skip the resolver round-trip.test_recompile_on_lookalike_filename_does_not_reresolve—backup_apm.ymland friends must NOT trigger reload (basename, notendswith).test_recompile_on_apm_yml_change_with_cli_target_keeps_cli_priority— explicit--targetoutranks mid-sessionapm.ymledits.test_clean_watch_emits_warning_and_does_not_run_clean— warning fires, watcher still launches.test_watch_without_clean_does_not_emit_clean_warning— positive control.CHANGELOG.md—### Changedentries under[Unreleased].Related