Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- `apm install` honors the SSH user portion of dependency URLs (`ssh://user@host/...` and scp shorthand `user@host:org/repo`) instead of hardcoding `git@`; unblocks EMU accounts and other non-`git` SSH identities. User values are validated against a strict allowlist before composing the clone URL. (#1385, closes #1383)

### Changed

- `apm compile --watch` picks up mid-session edits to `apm.yml`'s `target:` / `targets:` on the next file event instead of caching the resolved target until the watcher is restarted; previously the value resolved at startup was reused on every recompile. Follow-up to #1349.
- `apm compile --watch --clean` prints an explicit `[!]` warning that `--clean` is ignored in watch mode and continues; previously the flag was silently dropped. Run `apm compile --clean` separately between watch sessions to remove orphaned outputs.
Comment thread
edenfunf marked this conversation as resolved.
Outdated

## [0.14.0] - 2026-05-18

### Breaking
Expand Down
12 changes: 12 additions & 0 deletions src/apm_cli/commands/compile/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,17 @@ def compile(

# Watch mode
if watch:
# --clean removes orphaned outputs from a previous targets:
# configuration and would surprise users if run on every
# recompile mid-session; running it only on the initial
# compile would re-introduce a watcher-specific code path.
# Surface that --clean is ignored here so users can run
# `apm compile --clean` separately between watch sessions.
if clean:
logger.warning(
"--clean is ignored in watch mode; run 'apm compile --clean' "
"separately to remove orphaned outputs."
)
Comment thread
edenfunf marked this conversation as resolved.
# 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).
Expand All @@ -534,6 +545,7 @@ def compile(
effective_target=effective_target,
target_label_user=target,
target_label_config=config_target,
cli_target=target,
)
return

Expand Down
36 changes: 32 additions & 4 deletions src/apm_cli/commands/compile/watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import os
import time
from typing import TYPE_CHECKING, Any

Expand Down Expand Up @@ -67,13 +68,19 @@ def __init__(
dry_run: bool,
logger: CommandLogger,
effective_target: CompileTargetType | None = None,
cli_target: str | list[str] | 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
# Raw --target CLI argument retained so ``_recompile`` can
# re-run :func:`_resolve_effective_target` against the
# current apm.yml on every recompile, letting mid-session
# ``targets:`` edits take effect on the next file event.
self.cli_target = cli_target
self.last_compile = 0.0
self.debounce_delay = 1.0 # 1 second debounce

Expand All @@ -95,12 +102,27 @@ def _recompile(self, changed_file: str) -> None:
self.logger.progress(f"File changed: {changed_file}", symbol="eyes")
self.logger.progress("Recompiling...", symbol="gear")

# When apm.yml itself was the trigger, re-resolve so a
# mid-session edit to ``target:`` / ``targets:`` takes
# effect on this recompile. For instruction-file edits
# the startup snapshot remains correct, so we skip the
# extra resolver round-trip. Match on basename rather
# than ``endswith`` so a stray ``backup_apm.yml`` cannot
# masquerade as the project root manifest.
effective_target = self.effective_target
if os.path.basename(changed_file) == APM_YML_FILENAME:
from .cli import _resolve_effective_target

effective_target, _reason, _config_target = _resolve_effective_target(
self.cli_target
)
Comment thread
edenfunf marked this conversation as resolved.

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,
target=effective_target,
)

compiler = AgentsCompiler(".")
Expand Down Expand Up @@ -129,15 +151,20 @@ def _watch_mode(
effective_target: CompileTargetType | None = None,
target_label_user: str | list[str] | None = None,
target_label_config: str | list[str] | None = None,
cli_target: str | list[str] | None = None,
) -> 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).
into the initial compile so the startup label matches the one-shot
path (#1345).

``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.
"""
logger = CommandLogger("compile-watch", verbose=verbose, dry_run=dry_run)

Expand All @@ -159,6 +186,7 @@ class _WatchdogAdapter(APMFileHandler, FileSystemEventHandler):
dry_run,
logger,
effective_target=effective_target,
cli_target=cli_target,
)
observer = Observer()

Expand Down
Loading
Loading