Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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. (#1403)
- `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. (#1403)

## [0.14.0] - 2026-05-18

### Breaking
Expand Down
6 changes: 6 additions & 0 deletions docs/src/content/docs/reference/cli/compile.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ re-runs compilation automatically.
- Watched directories (when present): `.apm/`, `.github/instructions/`,
`.github/agents/`, `.github/chatmodes/`.
- Triggers on changes to `.md` files and `apm.yml`.
- Editing `apm.yml`'s `target:` / `targets:` mid-session takes effect on
the next file event; no need to restart the watcher. The CLI `--target`
flag, when passed to `apm compile --watch`, still outranks `apm.yml`.
- `--clean` is ignored in watch mode (a `[!]` warning is printed at
startup). Run `apm compile --clean` separately between watch sessions
to remove orphaned outputs.
- 1-second debounce to coalesce rapid edits.
- Press Ctrl+C to stop.
- Combine with `--dry-run` to validate placement on every save without
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
37 changes: 33 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,28 @@ 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, then persist the fresh value
# so subsequent instruction-file edits do not silently
# revert to the startup snapshot. 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.
self.effective_target = effective_target

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 +152,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 +187,7 @@ class _WatchdogAdapter(APMFileHandler, FileSystemEventHandler):
dry_run,
logger,
effective_target=effective_target,
cli_target=cli_target,
)
observer = Observer()

Expand Down
Loading