Summary
apm install (and the placement optimizer at compile time) treats the applyTo frontmatter field as a single glob even when it contains a comma-separated list of globs. The docs explicitly state that applyTo accepts "Glob (or comma-separated globs)" (instructions-and-agents.md), so this is a regression vs documented behavior.
Two visible symptoms:
-
Broken Claude target output. .claude/rules/<name>.md gets paths: as a single-item YAML list with the literal comma-string — Claude Code can't filter by path, so the rule never auto-attaches:
paths:
- "**/src/**,**/app/**,**/api/**,**/services/**" # ← one literal glob with commas
-
Noisy compile-time warnings with no actionable signal:
[!] Warning: Pattern '**/src/**,**/app/**,**/api/**,**/services/**,...' matches no files - placing at project root
[!] Warning: Pattern 'tasks/**/*.md,**/proposal.md,**/tasks.md,**/design.md,**/docs/**/*.md' matches no files - placing in intended directory 'tasks'
The patterns above all match plenty of files when split correctly. The warnings only fire because the matcher is fed the joined comma-string.
Reproduction
Minimal source file under any package, e.g. packages/foo/.apm/instructions/foo.instructions.md:
---
description: Backend standards
applyTo: "**/src/**,**/api/**,**/services/**"
---
# Foo
body...
Run apm install in a project that consumes this package and has matching files under src/, api/, services/.
Expected
Actual
-
.claude/rules/foo.md frontmatter:
paths:
- "**/src/**,**/api/**,**/services/**"
-
Warning emitted: Pattern '**/src/**,**/api/**,**/services/**' matches no files - placing at project root.
Root Cause
A) Claude target conversion — src/apm_cli/integration/instruction_integrator.py::_convert_to_claude_rules
The function reads applyTo as a single string and wraps it as a single list item without splitting on commas:
for line in fm_block.splitlines():
line_stripped = line.strip()
if line_stripped.startswith("applyTo:"):
apply_to = line_stripped[len("applyTo:"):].strip().strip("'\"")
if apply_to:
parts = ["---"]
parts.append("paths:")
parts.append(f' - "{apply_to}"') # ← no comma-split
parts.append("---")
B) Placement optimizer — src/apm_cli/compilation/context_optimizer.py::_solve_placement_optimization
The optimizer feeds the unsplit string straight to glob.glob() / fnmatch.fnmatch(), then emits a misleading warning when the resulting match set is empty:
pattern = instruction.apply_to # ← single string, even when comma-separated
# ...
matches = self._cached_glob(expanded_pattern)
# ...
"Pattern '{pattern}' matches no files - placing at project root"
C) Test gap — tests/unit/integration/test_instruction_integrator.py::TestConvertToClaudeRules
All existing tests pass a single glob to applyTo ('src/**/*.py', '**/*.ts', …). No test covers comma-separated input, so the behavior is unconstrained.
D) Docs promise multi-glob
docs/src/content/docs/producer/author-primitives/instructions-and-agents.md documents applyTo as "Glob (or comma-separated globs) the rule binds to". The implementation contradicts this.
Impact
- Claude target is effectively broken for any source using comma-separated globs. The
paths: filter never matches → the rule does not auto-attach to relevant files. The author thinks the rule is scope-attached when it is silently inert.
- Cursor and Windsurf targets likely have the same problem (they also convert
applyTo → globs: per the targets matrix); needs verification.
- Compile-time warnings are noise: every comma-separated
applyTo in the source tree produces a "matches no files" warning that the author cannot fix without changing the source format — which the docs say should be valid.
Proposed Fix
Fix A — _convert_to_claude_rules
if apply_to:
items = [s.strip() for s in apply_to.split(",") if s.strip()]
parts = ["---", "paths:"]
parts.extend(f' - "{item}"' for item in items)
parts.append("---")
Fix B — _solve_placement_optimization
Split pattern on commas before glob/fnmatch. Consider the placement "matched" if any sub-pattern matches; emit the warning only when all sub-patterns are empty (and quote the offending sub-pattern, not the joined string, for actionable diagnostics).
Fix C — Cursor / Windsurf targets
Apply the same comma-split to the globs: emission so behavior is consistent across targets.
Fix D — Tests
Add to TestConvertToClaudeRules:
def test_maps_comma_separated_apply_to_to_paths_list():
fm = "---\napplyTo: '**/*.py,**/*.ts,**/*.js'\n---\n\n# body"
result = _convert_to_claude_rules(fm)
assert 'paths:\n - "**/*.py"\n - "**/*.ts"\n - "**/*.js"' in result
def test_handles_whitespace_around_commas():
fm = "---\napplyTo: '**/*.py , **/*.ts'\n---\n\n# body"
result = _convert_to_claude_rules(fm)
assert ' - "**/*.py"' in result
assert ' - "**/*.ts"' in result
And an integration-level test for the placement optimizer verifying that a comma-separated applyTo matching real files does not produce the warning.
Workaround (consumers)
Until upstream lands, run a post-install script to split the comma-string into a real list:
import re
from pathlib import Path
p = re.compile(r'^paths:\n - "([^"]+)"$', re.MULTILINE)
for f in Path(".claude/rules").glob("*.md"):
text = f.read_text()
m = p.search(text)
if not m or "," not in m.group(1):
continue
items = [s.strip() for s in m.group(1).split(",") if s.strip()]
new_block = "paths:\n" + "\n".join(f' - "{i}"' for i in items)
f.write_text(text[:m.start()] + new_block + text[m.end():])
This is brittle (re-run on every apm install); a real fix upstream is preferable.
Environment
apm CLI version: 0.13.0
- Python: 3.13.13
- OS: Linux 6.8 (Ubuntu)
- Targets in
apm.yml: claude, opencode
References
Summary
apm install(and the placement optimizer at compile time) treats theapplyTofrontmatter field as a single glob even when it contains a comma-separated list of globs. The docs explicitly state thatapplyToaccepts "Glob (or comma-separated globs)" (instructions-and-agents.md), so this is a regression vs documented behavior.Two visible symptoms:
Broken Claude target output.
.claude/rules/<name>.mdgetspaths:as a single-item YAML list with the literal comma-string — Claude Code can't filter by path, so the rule never auto-attaches:Noisy compile-time warnings with no actionable signal:
The patterns above all match plenty of files when split correctly. The warnings only fire because the matcher is fed the joined comma-string.
Reproduction
Minimal source file under any package, e.g.
packages/foo/.apm/instructions/foo.instructions.md:Run
apm installin a project that consumes this package and has matching files undersrc/,api/,services/.Expected
.claude/rules/foo.mdfrontmatter:No "matches no files" warning during compile.
Actual
.claude/rules/foo.mdfrontmatter:Warning emitted:
Pattern '**/src/**,**/api/**,**/services/**' matches no files - placing at project root.Root Cause
A) Claude target conversion —
src/apm_cli/integration/instruction_integrator.py::_convert_to_claude_rulesThe function reads
applyToas a single string and wraps it as a single list item without splitting on commas:B) Placement optimizer —
src/apm_cli/compilation/context_optimizer.py::_solve_placement_optimizationThe optimizer feeds the unsplit string straight to
glob.glob()/fnmatch.fnmatch(), then emits a misleading warning when the resulting match set is empty:C) Test gap —
tests/unit/integration/test_instruction_integrator.py::TestConvertToClaudeRulesAll existing tests pass a single glob to
applyTo('src/**/*.py','**/*.ts', …). No test covers comma-separated input, so the behavior is unconstrained.D) Docs promise multi-glob
docs/src/content/docs/producer/author-primitives/instructions-and-agents.mddocumentsapplyToas "Glob (or comma-separated globs) the rule binds to". The implementation contradicts this.Impact
paths:filter never matches → the rule does not auto-attach to relevant files. The author thinks the rule is scope-attached when it is silently inert.applyTo→globs:per the targets matrix); needs verification.applyToin the source tree produces a "matches no files" warning that the author cannot fix without changing the source format — which the docs say should be valid.Proposed Fix
Fix A —
_convert_to_claude_rulesFix B —
_solve_placement_optimizationSplit
patternon commas before glob/fnmatch. Consider the placement "matched" if any sub-pattern matches; emit the warning only when all sub-patterns are empty (and quote the offending sub-pattern, not the joined string, for actionable diagnostics).Fix C — Cursor / Windsurf targets
Apply the same comma-split to the
globs:emission so behavior is consistent across targets.Fix D — Tests
Add to
TestConvertToClaudeRules:And an integration-level test for the placement optimizer verifying that a comma-separated
applyTomatching real files does not produce the warning.Workaround (consumers)
Until upstream lands, run a post-install script to split the comma-string into a real list:
This is brittle (re-run on every
apm install); a real fix upstream is preferable.Environment
apmCLI version: 0.13.0apm.yml:claude, opencodeReferences
.claude/rules/for Claude Code #527 — Deploy instructions to.claude/rules/for Claude Code (introduced the broken converter)