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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `apm compile` scoped instructions can now opt into `placement:` frontmatter (`root`, `subdirectory`, or a project-relative directory) to override automatic instruction placement. (#1234)

### Fixed

- Pin `Path.home()` under unit tests via a session-scoped autouse conftest fixture, fixing 56 Windows runner failures on the new `windows-2025-vs2026` GitHub-hosted image where `USERPROFILE`/`HOMEDRIVE`+`HOMEPATH` are not seeded for pytest workers; also patch the `_check_and_notify_updates` import binding in the disabled-self-update test so it no longer races on the version-check cache. (#1270)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,20 @@ applyTo: "**/*.py"
|---|---|---|
| `description` | yes | One-line summary; used in compiled context indexes |
| `applyTo` | yes for instructions | Glob (or comma-separated globs) the rule binds to |
| `placement` | optional | Compile placement override for scoped instructions: `root`, `subdirectory`, or a project-relative directory |

`applyTo` is the load-bearing field. Without it the rule is treated as
unconditional and gets folded into compiled context files
(`AGENTS.md`, `GEMINI.md`) instead of a per-file rule directory. With
it, each harness wraps the body in its own scoping syntax.

`placement` lets a scoped instruction opt out of automatic compile
placement. Use `root` to force the root context file, `subdirectory`
to use the lowest common directory for `applyTo` matches, or a
project-relative directory such as `apps/bar`. Directory overrides
fall back to automatic placement when `applyTo` has no matches or the
directory cannot be validated.

### Body conventions

- Lead with bullets, not prose. Instructions are read by an agent
Expand Down
36 changes: 36 additions & 0 deletions docs/src/content/docs/producer/compile.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,42 @@ Per target, with the rules shape on disk after compile:
| `opencode` | `AGENTS.md` (folded) | none -- compile-only, no per-file deploy | Yes -- folded into `AGENTS.md` |
| `windsurf` | -- | `.windsurf/rules/<name>.md` | Yes -- compiled to Windsurf rules |

## Override one instruction's placement

By default, APM chooses instruction placement automatically from the
instruction's `applyTo:` coverage and the selected target's output
shape. A scoped instruction can override that decision with
`placement:` frontmatter:

```yaml
---
description: Bar app rules
applyTo: "apps/bar/**"
placement: "subdirectory"
---
```

Supported values:

| Value | Effect |
|---|---|
| `root` | Place the instruction in the root `AGENTS.md` / `CLAUDE.md` style file. |
| `subdirectory` | Place the instruction at the lowest common directory that covers all files matched by `applyTo`. |
| Project-relative directory, such as `apps/bar` | Place the instruction at that directory when it exists and covers all matched files. |

Use this when an app- or package-scoped rule would otherwise be
hoisted too high. For example, `placement: "subdirectory"` on an
`apps/bar/**` rule lets `apm compile --dry-run --verbose` preview the
rule under `apps/bar/AGENTS.md` and report the strategy as
`Manual Override`.

Invalid overrides are ignored with a warning, and APM falls back to
automatic placement. Absolute paths, `..` traversal, missing paths,
non-directories, and paths that do not cover the matched files are
rejected. `subdirectory` and project-relative directory overrides also
require `applyTo` to match at least one existing directory so APM can
verify coverage.

## compile vs install

| You want to... | Run |
Expand Down
202 changes: 199 additions & 3 deletions src/apm_cli/compilation/context_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
)
from ..primitives.models import Instruction
from ..utils.exclude import should_exclude, validate_exclude_patterns
from ..utils.path_security import ensure_path_within, validate_path_segments
from ..utils.paths import portable_relpath

# CRITICAL: Shadow Click commands to prevent namespace collision
Expand Down Expand Up @@ -566,6 +567,15 @@ def _solve_placement_optimization(

# Find all directories with matching files
matching_directories = self._find_matching_directories(pattern)
distribution_score = self._calculate_distribution_score(matching_directories)

manual_placements = self._try_manual_placement_override(
instruction,
matching_directories,
distribution_score,
)
if manual_placements is not None:
return manual_placements

if not matching_directories:
# Smart fallback: Try to place in semantically appropriate directory
Expand Down Expand Up @@ -606,9 +616,6 @@ def _solve_placement_optimization(

return [placement]

# Calculate distribution score with diversity factor
distribution_score = self._calculate_distribution_score(matching_directories)

# Apply three-tier placement strategy based on mathematical analysis
if distribution_score < self.LOW_DISTRIBUTION_THRESHOLD:
# Low distribution: Single Point Placement
Expand Down Expand Up @@ -655,6 +662,195 @@ def _solve_placement_optimization(

return placements

def _try_manual_placement_override(
self,
instruction: Instruction,
matching_directories: builtins.set[Path],
distribution_score: float,
) -> builtins.list[Path] | None:
"""Apply an instruction-level placement override when it is valid."""
raw_placement = getattr(instruction, "placement", None)
if raw_placement is None:
return None
if not isinstance(raw_placement, str):
self._warn_invalid_placement_override(
instruction,
str(raw_placement),
"placement must be a string",
)
return None

placement = raw_placement.strip()
if not placement:
self._warn_invalid_placement_override(
instruction,
raw_placement,
"placement must not be empty",
)
return None

placement_mode = placement.lower()
if placement_mode == "root":
return self._record_manual_placement_decision(
instruction,
matching_directories,
distribution_score,
self.base_dir,
"Manual placement override 'root' placed instruction at project root",
)

if not matching_directories:
self._warn_invalid_placement_override(
instruction,
placement,
"applyTo matched no directories, so coverage cannot be verified",
)
return None

if placement_mode == "subdirectory":
placement_dir = self._find_minimal_coverage_placement(matching_directories)
if placement_dir is None:
self._warn_invalid_placement_override(
instruction,
placement,
"APM could not find a common subdirectory for matched files",
)
return None

rel_path = portable_relpath(placement_dir, self.base_dir)
return self._record_manual_placement_decision(
instruction,
matching_directories,
distribution_score,
placement_dir,
f"Manual placement override 'subdirectory' placed instruction at '{rel_path}'",
)

placement_dir = self._resolve_explicit_placement_override(
instruction,
placement,
matching_directories,
)
if placement_dir is None:
return None

rel_path = portable_relpath(placement_dir, self.base_dir)
return self._record_manual_placement_decision(
instruction,
matching_directories,
distribution_score,
placement_dir,
f"Manual placement override '{placement}' placed instruction at '{rel_path}'",
)

def _resolve_explicit_placement_override(
self,
instruction: Instruction,
placement: str,
matching_directories: builtins.set[Path],
) -> Path | None:
"""Validate and resolve a project-relative placement override."""
placement_path = Path(placement)
if placement_path.is_absolute():
self._warn_invalid_placement_override(
instruction,
placement,
"absolute paths are not allowed",
)
return None

try:
validate_path_segments(placement, context="placement override")
except ValueError as exc:
self._warn_invalid_placement_override(
instruction,
placement,
str(exc),
)
return None

try:
placement_dir = ensure_path_within(self.base_dir / placement_path, self.base_dir)
except (OSError, ValueError) as exc:
self._warn_invalid_placement_override(
instruction,
placement,
str(exc),
)
return None

if not placement_dir.exists():
self._warn_invalid_placement_override(
instruction,
placement,
"path does not exist",
)
return None

if not placement_dir.is_dir():
self._warn_invalid_placement_override(
instruction,
placement,
"path is not a directory",
)
return None

covered_directories = self._calculate_hierarchical_coverage(
[placement_dir],
matching_directories,
)
if covered_directories != matching_directories:
self._warn_invalid_placement_override(
instruction,
placement,
"path does not cover all directories matched by applyTo",
)
return None

return placement_dir

def _record_manual_placement_decision(
self,
instruction: Instruction,
matching_directories: builtins.set[Path],
distribution_score: float,
placement: Path,
reasoning: str,
) -> builtins.list[Path]:
"""Record and return a successful manual placement override."""
relevance_score = 0.0
if placement in self._directory_cache:
relevance_score = self._calculate_coverage_efficiency(placement, instruction.apply_to)

decision = OptimizationDecision(
instruction=instruction,
pattern=instruction.apply_to,
matching_directories=len(matching_directories),
total_directories=len(self._directory_cache),
distribution_score=distribution_score,
strategy=PlacementStrategy.MANUAL_OVERRIDE,
placement_directories=[placement],
reasoning=reasoning,
relevance_score=relevance_score,
)
self._optimization_decisions.append(decision)
return [placement]

def _warn_invalid_placement_override(
self,
instruction: Instruction,
placement: str,
problem: str,
) -> None:
"""Warn that a placement override was ignored and auto-placement will be used."""
fallback_name = getattr(instruction, "file_path", "<unknown>")
instruction_name = getattr(instruction, "name", None) or str(fallback_name)
self._warnings.append(
f"Ignoring placement override '{placement}' for instruction '{instruction_name}': "
f"{problem}. Use 'root', 'subdirectory', or a project-relative directory that "
"covers all files matching applyTo. Falling back to automatic placement."
)

def _extract_intended_directory_from_pattern(self, pattern: str) -> Path | None:
"""Extract the intended directory from a pattern like 'docs/**/*.md' -> 'docs'.

Expand Down
Loading