From 84f3f9f72f56af6a5f44fdc42ec40fc6eb3e9772 Mon Sep 17 00:00:00 2001 From: Daniel Meppiel Date: Fri, 15 May 2026 16:43:12 +0200 Subject: [PATCH 1/2] fix(compile): forward target to watch-mode recompile (closes #1345) 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> --- CHANGELOG.md | 1 + src/apm_cli/commands/compile/cli.py | 75 +++++- src/apm_cli/commands/compile/watcher.py | 217 ++++++++++++------ tests/unit/commands/compile/__init__.py | 0 .../compile/test_watch_target_forwarding.py | 121 ++++++++++ 5 files changed, 338 insertions(+), 76 deletions(-) create mode 100644 tests/unit/commands/compile/__init__.py create mode 100644 tests/unit/commands/compile/test_watch_target_forwarding.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a5cc53e42..e90af7023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- `apm compile --watch` now honors `targets: [claude, cursor]` (and every other multi-target / single-target configuration) on every recompile. Previously the watch path bypassed the resolver the one-shot path uses and let `CompilationConfig.from_apm_yml` fall back to the all-families default, silently regenerating `GEMINI.md` after every file edit. The watch path now resolves the effective target via the same helper the one-shot path uses and forwards it as `target=` into both the initial compile and every debounced recompile. (#1345) - MCP server installation now respects the `targets:` whitelist exactly like `apm install`: drop a non-listed runtime even when its `.cursor/`, `.codex/`, or other on-disk signal exists. Previously the MCP install path called `active_targets()` reading the singular `target:` key only, so projects whitelisting `targets: [copilot]` could still receive `~/.codex/config.toml` and `.cursor/mcp.json` writes from foreign signals. The fix audits both paths: (a) the call site at `local_bundle_handler.py` now forwards the canonical plural list; (b) the gate now delegates to the same `resolve_targets` resolver that backs `apm install` skills, so a malformed `targets:` field (conflicting `target:` + `targets:`, `targets: []`, or unknown target name) fails closed with the same `[x]` red error voice + remediation block. The same delegation closes a related asymmetry: a greenfield project (no `targets:`, no `--target` flag, no detected signals) used to silently fall back to `[copilot]` for MCP-only invocations, while `apm install` raised `NoHarnessError` on the same input -- both surfaces now error consistently. Drop lines now use the `[i] Skipped MCP config for X (active targets: Y)` format mirroring the canonical `Targets: X (source: Y)` provenance line. The `-g`/`--global` carve-out is unchanged: `apm install -g --mcp NAME` writes to user-scope (`~/.config/...`, `~/.codex/`, etc.) bypassing the project-scope gate by design. (#1335) - Gemini CLI: `apm install -g --mcp NAME` now correctly writes to `~/.gemini/settings.json` (user scope) and `apm install` from outside the target project writes to `/.gemini/settings.json` instead of `cwd`. Previously `--global` had no effect on Gemini and project-scope writes silently landed in the wrong directory. The matching opt-in gate and cleanup paths in `MCPIntegrator` are aligned in the same change. (#1299) - `apm install --target claude` now preserves self-defined stdio MCP `env` values from `apm.yml` and writes non-string values such as `PORT: 3000` and `DEBUG: false` as MCP-compatible strings. (#1222) diff --git a/src/apm_cli/commands/compile/cli.py b/src/apm_cli/commands/compile/cli.py index 9e871c7ce..8bb22a2fc 100644 --- a/src/apm_cli/commands/compile/cli.py +++ b/src/apm_cli/commands/compile/cli.py @@ -1,7 +1,7 @@ """APM compile command CLI.""" import sys -from pathlib import Path # noqa: F401 +from pathlib import Path import click @@ -252,6 +252,64 @@ def _family_of(name: str) -> str | None: return target # single string pass-through +def _resolve_effective_target(target): + """Resolve the CLI --target arg to the compiler-understood effective target. + + Mirrors the resolution the one-shot compile path performs (load + apm.yml ``target:`` / ``targets:``, run :func:`_resolve_compile_target` + on both, fall back to :func:`detect_target` for the auto-detect case) + so the watch path can build ``CompilationConfig`` with the same + ``target=`` value the one-shot path uses (#1345). + + Args: + target: The raw ``--target`` CLI argument (None, str, or list). + + Returns: + Tuple ``(effective_target, detection_reason, config_target)`` where + ``effective_target`` is what to pass as ``target=`` to + :meth:`CompilationConfig.from_apm_yml`, ``detection_reason`` is the + provenance label, and ``config_target`` is the raw apm.yml value + (str | list | None) for user-facing label rendering. + """ + from ...core.target_detection import detect_target + from ...models.apm_package import APMPackage + + config_target = None + apm_yml_path = Path(APM_YML_FILENAME) + if apm_yml_path.exists(): + apm_pkg = APMPackage.from_apm_yml(apm_yml_path) + config_target = apm_pkg.target + if config_target is None: + try: + from ...core.apm_yml import parse_targets_field + from ...utils.yaml_io import load_yaml + + _raw = load_yaml(apm_yml_path) + if isinstance(_raw, dict): + _yaml_targets = parse_targets_field(_raw) + if _yaml_targets: + config_target = ( + _yaml_targets[0] if len(_yaml_targets) == 1 else _yaml_targets + ) + except Exception: + pass + + compile_target = _resolve_compile_target(target) + compile_config_target = _resolve_compile_target(config_target) + + if isinstance(compile_target, frozenset): + return compile_target, "explicit --target flag", config_target + if isinstance(compile_config_target, frozenset) and compile_target is None: + return compile_config_target, "apm.yml target", config_target + + detected_target, detection_reason = detect_target( + project_root=Path("."), + explicit_target=compile_target, + config_target=compile_config_target if isinstance(compile_config_target, str) else None, + ) + return detected_target, detection_reason, config_target + + @click.command(help="Compile APM context into distributed AGENTS.md files") @click.option( "--output", @@ -455,7 +513,20 @@ def compile( # Watch mode if watch: - _watch_mode(output, chatmode, no_links, dry_run, verbose=verbose) + # Resolve the same effective target the one-shot path uses so + # `targets: [claude, cursor]` does not silently regress to the + # all-families fanout on every recompile (#1345). + effective_target, _detection_reason, config_target = _resolve_effective_target(target) + _watch_mode( + output, + chatmode, + no_links, + dry_run, + verbose=verbose, + effective_target=effective_target, + target_label_user=target, + target_label_config=config_target, + ) return logger.start("Starting context compilation...", symbol="cogs") diff --git a/src/apm_cli/commands/compile/watcher.py b/src/apm_cli/commands/compile/watcher.py index f5b0253a9..a4fbaaa10 100644 --- a/src/apm_cli/commands/compile/watcher.py +++ b/src/apm_cli/commands/compile/watcher.py @@ -7,106 +7,169 @@ from ...core.command_logger import CommandLogger -def _watch_mode(output, chatmode, no_links, dry_run, verbose=False): - """Watch for changes in .apm/ directories and auto-recompile.""" +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). + """ + from ...core.target_detection import ( + get_target_description, + should_compile_agents_md, + should_compile_claude_md, + should_compile_gemini_md, + ) + + if isinstance(effective_target, frozenset): + if isinstance(target_label_user, list): + source = f"--target {','.join(target_label_user)}" + elif isinstance(target_label_config, list): + source = f"apm.yml target: [{', '.join(target_label_config)}]" + else: + source = "multi-target" + parts = [] + if should_compile_agents_md(effective_target): + parts.append("AGENTS.md") + if should_compile_claude_md(effective_target): + parts.append("CLAUDE.md") + if should_compile_gemini_md(effective_target): + parts.append("GEMINI.md") + return f"Compiling for {' + '.join(parts)} ({source})" + if effective_target is None: + return None + return f"Compiling for {get_target_description(effective_target)}" + + +class APMFileHandler: + """Watchdog file-system handler that recompiles APM context on edits. + + Defined at module scope (rather than inside ``_watch_mode``) so unit + tests can instantiate it without spinning up a watchdog ``Observer`` + -- the regression for #1345 lives entirely in the ``from_apm_yml`` + call site this class owns. + """ + + def __init__( + self, + output, + chatmode, + no_links, + dry_run, + logger, + effective_target=None, + ): + self.output = output + self.chatmode = chatmode + self.no_links = no_links + self.dry_run = dry_run + self.logger = logger + self.effective_target = effective_target + self.last_compile = 0 + self.debounce_delay = 1.0 # 1 second debounce + + def on_modified(self, event): + if getattr(event, "is_directory", False): + return + src_path = getattr(event, "src_path", "") + if not src_path.endswith(".md") and not src_path.endswith(APM_YML_FILENAME): + return + current_time = time.time() + if current_time - self.last_compile < self.debounce_delay: + return + self.last_compile = current_time + self._recompile(src_path) + + def _recompile(self, changed_file): + """Recompile after a file change, honoring the resolved target.""" + try: + self.logger.progress(f"File changed: {changed_file}", symbol="eyes") + self.logger.progress("Recompiling...", symbol="gear") + + config = CompilationConfig.from_apm_yml( + output_path=self.output if self.output != AGENTS_MD_FILENAME else None, + chatmode=self.chatmode, + resolve_links=not self.no_links if self.no_links else None, + dry_run=self.dry_run, + target=self.effective_target, + ) + + compiler = AgentsCompiler(".") + result = compiler.compile(config, logger=self.logger) + + if result.success: + if self.dry_run: + self.logger.success("Recompilation successful (dry run)", symbol="sparkles") + else: + self.logger.success(f"Recompiled to {result.output_path}", symbol="sparkles") + else: + self.logger.error("Recompilation failed") + for error in result.errors: + self.logger.error(f" {error}") + + except Exception as e: + self.logger.error(f"Error during recompilation: {e}") + + +def _watch_mode( + output, + chatmode, + no_links, + dry_run, + verbose=False, + effective_target=None, + target_label_user=None, + target_label_config=None, +): + """Watch for changes in .apm/ directories and auto-recompile. + + ``effective_target`` is the compiler-understood target resolved by + :func:`apm_cli.commands.compile.cli._resolve_effective_target` (the + same resolver the one-shot path uses) and is forwarded as ``target=`` + into every :meth:`CompilationConfig.from_apm_yml` call so watch mode + honors ``targets: [claude, cursor]`` instead of silently fanning out + to all families on every recompile (#1345). + """ logger = CommandLogger("compile-watch", verbose=verbose, dry_run=dry_run) try: - # Try to import watchdog for file system monitoring from pathlib import Path from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer - class APMFileHandler(FileSystemEventHandler): - def __init__(self, output, chatmode, no_links, dry_run, logger): - self.output = output - self.chatmode = chatmode - self.no_links = no_links - self.dry_run = dry_run - self.logger = logger - self.last_compile = 0 - self.debounce_delay = 1.0 # 1 second debounce - - def on_modified(self, event): - if event.is_directory: - return - # Only react to relevant files - if not event.src_path.endswith(".md") and not event.src_path.endswith( - APM_YML_FILENAME - ): - return - # Debounce rapid changes - current_time = time.time() - if current_time - self.last_compile < self.debounce_delay: - return - - self.last_compile = current_time - self._recompile(event.src_path) - - def _recompile(self, changed_file): - """Recompile after file change.""" - try: - self.logger.progress(f"File changed: {changed_file}", symbol="eyes") - self.logger.progress("Recompiling...", symbol="gear") - - # Create configuration from apm.yml with overrides - config = CompilationConfig.from_apm_yml( - output_path=self.output if self.output != AGENTS_MD_FILENAME else None, - chatmode=self.chatmode, - resolve_links=not self.no_links if self.no_links else None, - dry_run=self.dry_run, - ) - - # Create compiler and compile - compiler = AgentsCompiler(".") - result = compiler.compile(config, logger=self.logger) - - if result.success: - if self.dry_run: - self.logger.success( - "Recompilation successful (dry run)", symbol="sparkles" - ) - else: - self.logger.success( - f"Recompiled to {result.output_path}", symbol="sparkles" - ) - else: - self.logger.error("Recompilation failed") - for error in result.errors: - self.logger.error(f" {error}") - - except Exception as e: - self.logger.error(f"Error during recompilation: {e}") - - # Set up file watching - event_handler = APMFileHandler(output, chatmode, no_links, dry_run, logger) + # Adapt the test-friendly module-level handler to watchdog's + # FileSystemEventHandler base so Observer.schedule() accepts it. + class _WatchdogAdapter(APMFileHandler, FileSystemEventHandler): + pass + + event_handler = _WatchdogAdapter( + output, + chatmode, + no_links, + dry_run, + logger, + effective_target=effective_target, + ) observer = Observer() - # Watch patterns for APM files watch_paths = [] - # Check for .apm directory if Path(APM_DIR).exists(): observer.schedule(event_handler, APM_DIR, recursive=True) watch_paths.append(f"{APM_DIR}/") - # Check for .github/instructions and agents/chatmodes if Path(".github/instructions").exists(): observer.schedule(event_handler, ".github/instructions", recursive=True) watch_paths.append(".github/instructions/") - # Watch .github/agents/ (new standard) if Path(".github/agents").exists(): observer.schedule(event_handler, ".github/agents", recursive=True) watch_paths.append(".github/agents/") - # Watch .github/chatmodes/ (legacy) if Path(".github/chatmodes").exists(): observer.schedule(event_handler, ".github/chatmodes", recursive=True) watch_paths.append(".github/chatmodes/") - # Watch apm.yml if it exists if Path(APM_YML_FILENAME).exists(): observer.schedule(event_handler, ".", recursive=False) watch_paths.append(APM_YML_FILENAME) @@ -116,12 +179,17 @@ def _recompile(self, changed_file): logger.progress("Run 'apm init' to create an APM project") return - # Start watching observer.start() logger.progress(f" Watching for changes in: {', '.join(watch_paths)}", symbol="eyes") logger.progress("Press Ctrl+C to stop watching...", symbol="info") - # Do initial compilation + # Surface the same family-aware label the one-shot path prints so + # users see at-a-glance which AGENTS / CLAUDE / GEMINI files watch + # mode will (re)write (#1345). + label = _format_target_label(effective_target, target_label_user, target_label_config) + if label: + logger.progress(label, symbol="gear") + logger.progress("Performing initial compilation...", symbol="gear") config = CompilationConfig.from_apm_yml( @@ -129,6 +197,7 @@ def _recompile(self, changed_file): chatmode=chatmode, resolve_links=not no_links if no_links else None, dry_run=dry_run, + target=effective_target, ) compiler = AgentsCompiler(".") diff --git a/tests/unit/commands/compile/__init__.py b/tests/unit/commands/compile/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/commands/compile/test_watch_target_forwarding.py b/tests/unit/commands/compile/test_watch_target_forwarding.py new file mode 100644 index 000000000..816f2193f --- /dev/null +++ b/tests/unit/commands/compile/test_watch_target_forwarding.py @@ -0,0 +1,121 @@ +"""Regression tests for #1345: watch mode must forward the resolved target. + +The non-watch ``apm compile`` path correctly omits ``GEMINI.md`` when +``apm.yml`` declares ``targets: [claude, cursor]`` (#1019/#1074). Before +this fix, the watch path bypassed target resolution and let +``CompilationConfig.from_apm_yml`` fall back to the all-families default, +silently regenerating ``GEMINI.md`` on every recompile. + +These tests pin the contract: + +1. ``APMFileHandler._recompile`` calls ``CompilationConfig.from_apm_yml`` + with ``target=``. +2. The captured target is the exact frozenset the resolver produced -- + and ``should_compile_gemini_md`` returns False for it, so the bug + cannot silently come back. +3. When no target is configured (``effective_target=None``), the watcher + forwards ``None`` and lets ``from_apm_yml`` keep its dataclass default. +""" + +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest + +from apm_cli.commands.compile.watcher import APMFileHandler +from apm_cli.core.target_detection import should_compile_gemini_md + + +@pytest.fixture +def fake_logger(): + return SimpleNamespace( + progress=MagicMock(), + success=MagicMock(), + error=MagicMock(), + warning=MagicMock(), + ) + + +def _make_handler(fake_logger, effective_target): + return APMFileHandler( + output="AGENTS.md", + chatmode=None, + no_links=False, + dry_run=False, + logger=fake_logger, + effective_target=effective_target, + ) + + +def test_recompile_forwards_frozenset_target(fake_logger): + """`targets: [claude, cursor]` -> frozenset({'claude','agents'}) is forwarded.""" + effective = frozenset({"claude", "agents"}) + handler = _make_handler(fake_logger, effective) + + with ( + patch( + "apm_cli.commands.compile.watcher.CompilationConfig.from_apm_yml" + ) as mock_from_apm_yml, + patch("apm_cli.commands.compile.watcher.AgentsCompiler") as mock_compiler_cls, + ): + mock_from_apm_yml.return_value = MagicMock() + mock_compiler_cls.return_value.compile.return_value = SimpleNamespace( + success=True, output_path="AGENTS.md", errors=[] + ) + + handler._recompile("dummy.md") + + assert mock_from_apm_yml.call_count == 1 + captured_kwargs = mock_from_apm_yml.call_args.kwargs + assert "target" in captured_kwargs, ( + "Watcher must forward target= to CompilationConfig.from_apm_yml; " + "missing it is the #1345 regression." + ) + assert captured_kwargs["target"] == effective + + # Outcome assertion: no GEMINI.md is emitted for this target. If a + # future change reintroduces the all-families fanout, this fails. + assert should_compile_gemini_md(captured_kwargs["target"]) is False + + +def test_recompile_forwards_none_when_no_target_configured(fake_logger): + """Auto-detect / unset target case: forward None, no surprise override.""" + handler = _make_handler(fake_logger, None) + + with ( + patch( + "apm_cli.commands.compile.watcher.CompilationConfig.from_apm_yml" + ) as mock_from_apm_yml, + patch("apm_cli.commands.compile.watcher.AgentsCompiler") as mock_compiler_cls, + ): + mock_from_apm_yml.return_value = MagicMock() + mock_compiler_cls.return_value.compile.return_value = SimpleNamespace( + success=True, output_path="AGENTS.md", errors=[] + ) + + handler._recompile("dummy.md") + + assert mock_from_apm_yml.call_args.kwargs["target"] is None + + +def test_recompile_forwards_single_string_target(fake_logger): + """Single-target case (e.g. `--target claude`) is forwarded verbatim.""" + handler = _make_handler(fake_logger, "claude") + + with ( + patch( + "apm_cli.commands.compile.watcher.CompilationConfig.from_apm_yml" + ) as mock_from_apm_yml, + patch("apm_cli.commands.compile.watcher.AgentsCompiler") as mock_compiler_cls, + ): + mock_from_apm_yml.return_value = MagicMock() + mock_compiler_cls.return_value.compile.return_value = SimpleNamespace( + success=True, output_path="AGENTS.md", errors=[] + ) + + handler._recompile("dummy.md") + + assert mock_from_apm_yml.call_args.kwargs["target"] == "claude" + assert should_compile_gemini_md("claude") is False From 49c5975c4140484119939b0682e1e51ff802ad85 Mon Sep 17 00:00:00 2001 From: Daniel Meppiel Date: Mon, 18 May 2026 16:40:58 +0200 Subject: [PATCH 2/2] fix(compile): add type hints to watch-mode target helpers Address Copilot review feedback on PR #1349: annotate the new public surfaces introduced by the #1345 fix so the target-resolution contract is explicit at the call boundary. - cli.py: `_resolve_effective_target(target: str | list[str] | None) -> tuple[CompileTargetType, str, str | list[str] | None]`. - watcher.py: type-hint `_format_target_label`, `APMFileHandler` (`__init__`, `on_modified`, `_recompile`), and `_watch_mode`. - Both files gain `from __future__ import annotations` so the CompileTargetType forward ref is import-cycle-safe behind TYPE_CHECKING. No behavior change. Regression test suite still passes (3/3) and broader sweep (`pytest -k 'compile or watch or target_detection'`) is 363 passed, 6 skipped, 2 xfailed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/apm_cli/commands/compile/cli.py | 10 ++++- src/apm_cli/commands/compile/watcher.py | 50 +++++++++++++++---------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/apm_cli/commands/compile/cli.py b/src/apm_cli/commands/compile/cli.py index b25dcd55c..5dd10cf8f 100644 --- a/src/apm_cli/commands/compile/cli.py +++ b/src/apm_cli/commands/compile/cli.py @@ -1,10 +1,16 @@ """APM compile command CLI.""" +from __future__ import annotations + import sys from pathlib import Path +from typing import TYPE_CHECKING import click +if TYPE_CHECKING: + from ...core.target_detection import CompileTargetType + from ...compilation import AgentsCompiler, CompilationConfig from ...constants import AGENTS_MD_FILENAME, APM_DIR, APM_MODULES_DIR, APM_YML_FILENAME from ...core.command_logger import CommandLogger @@ -252,7 +258,9 @@ def _family_of(name: str) -> str | None: return target # single string pass-through -def _resolve_effective_target(target): +def _resolve_effective_target( + target: str | list[str] | None, +) -> tuple[CompileTargetType, str, str | list[str] | None]: """Resolve the CLI --target arg to the compiler-understood effective target. Mirrors the resolution the one-shot compile path performs (load diff --git a/src/apm_cli/commands/compile/watcher.py b/src/apm_cli/commands/compile/watcher.py index a4fbaaa10..612fd4cfb 100644 --- a/src/apm_cli/commands/compile/watcher.py +++ b/src/apm_cli/commands/compile/watcher.py @@ -1,13 +1,23 @@ """APM compile watch mode.""" +from __future__ import annotations + import time +from typing import TYPE_CHECKING, Any from ...compilation import AgentsCompiler, CompilationConfig from ...constants import AGENTS_MD_FILENAME, APM_DIR, APM_YML_FILENAME from ...core.command_logger import CommandLogger +if TYPE_CHECKING: + from ...core.target_detection import CompileTargetType + -def _format_target_label(effective_target, target_label_user, target_label_config): +def _format_target_label( + effective_target: CompileTargetType | None, + target_label_user: str | list[str] | None, + target_label_config: str | list[str] | None, +) -> str | None: """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 @@ -51,23 +61,23 @@ class APMFileHandler: def __init__( self, - output, - chatmode, - no_links, - dry_run, - logger, - effective_target=None, - ): + output: str, + chatmode: str | None, + no_links: bool, + dry_run: bool, + logger: CommandLogger, + effective_target: CompileTargetType | None = None, + ) -> None: self.output = output self.chatmode = chatmode self.no_links = no_links self.dry_run = dry_run self.logger = logger self.effective_target = effective_target - self.last_compile = 0 + self.last_compile = 0.0 self.debounce_delay = 1.0 # 1 second debounce - def on_modified(self, event): + def on_modified(self, event: Any) -> None: if getattr(event, "is_directory", False): return src_path = getattr(event, "src_path", "") @@ -79,7 +89,7 @@ def on_modified(self, event): self.last_compile = current_time self._recompile(src_path) - def _recompile(self, changed_file): + def _recompile(self, changed_file: str) -> None: """Recompile after a file change, honoring the resolved target.""" try: self.logger.progress(f"File changed: {changed_file}", symbol="eyes") @@ -111,15 +121,15 @@ def _recompile(self, changed_file): def _watch_mode( - output, - chatmode, - no_links, - dry_run, - verbose=False, - effective_target=None, - target_label_user=None, - target_label_config=None, -): + output: str, + chatmode: str | None, + no_links: bool, + dry_run: bool, + verbose: bool = False, + effective_target: CompileTargetType | None = None, + target_label_user: str | list[str] | None = None, + target_label_config: str | list[str] | None = None, +) -> None: """Watch for changes in .apm/ directories and auto-recompile. ``effective_target`` is the compiler-understood target resolved by