Skip to content

fix(integration): respect comma-separated applyTo globs in Claude/Cursor/Windsurf#1387

Open
danielmeppiel wants to merge 1 commit into
mainfrom
danielmeppiel/fix-1366-applyto-comma-globs
Open

fix(integration): respect comma-separated applyTo globs in Claude/Cursor/Windsurf#1387
danielmeppiel wants to merge 1 commit into
mainfrom
danielmeppiel/fix-1366-applyto-comma-globs

Conversation

@danielmeppiel
Copy link
Copy Markdown
Collaborator

Fixes #1366

WHY

applyTo frontmatter is documented as "Glob (or comma-separated globs)" in
docs/src/content/docs/producer/author-primitives/instructions-and-agents.md,
but the Claude, Cursor, and Windsurf converters emit comma-list values as a
single literal glob string, and the context optimizer feeds the unsplit value
to glob/fnmatch (producing misleading Pattern '...' matches no files warnings).

Copilot was already correct because its converter preserves applyTo verbatim
and the consuming tool splits — that path is intentionally unchanged.

WHAT

  • New helper parse_apply_to(value) -> list[str] in src/apm_cli/utils/patterns.py
    owns the canonical comma-split semantics (whitespace trim, empty / trailing /
    leading / lone-comma tolerance).
  • _convert_to_cursor_rules, _convert_to_windsurf_rules, and
    _convert_to_claude_rules now call the helper and branch:
    • single glob → existing scalar form (backwards compatible)
    • multi glob → YAML list under the target's native key
  • context_optimizer._file_matches_pattern splits only top-level commas
    (so brace alternation like **/*.{css,scss} is unaffected) and treats any
    segment match as a hit.
  • _extract_intended_directory_from_pattern consults the first segment when
    given a comma-list (previously it tried the whole literal as a path).
  • Optimizer's "no matches" warning now names the primitive instead of
    echoing the (potentially 80+ char) raw pattern, and still fires exactly
    once per primitive — not once per comma segment.

Design summary

Picked python-architect option (a): single shared helper instead of per-converter
splits, so the comma-split contract (trim, drop empties) is defined once. Kept
Instruction.apply_to: str because it is used as a dict key in
template_builder.py and a set member in claude_formatter.py; converting to
list[str] would have broken hashability and silently changed grouping.

Considered alternatives:

  • Normalising applyTo once at primitive-load time in parser.py. Rejected:
    forces the type change above and changes serialisation everywhere; only the
    three converters and the optimizer actually need split form.
  • Per-converter split. Rejected: triplicates the trimming / empty-segment
    rules and drifts.
  • Emitting Cursor multi-glob as multiple [[rules]] blocks. Rejected: not
    required by Cursor's gray-matter parser — YAML list is accepted and far
    less invasive.

Validation evidence

Original repro from the issue: primitive with
applyTo: '**/src/**,**/api/**,**/services/**' compiled to all four targets:

===== copilot =====
---
applyTo: '**/src/**,**/api/**,**/services/**'
description: 'Rules for src, api, services'
---

# Multi-glob rules

Body content

===== cursor =====
---
description: Rules for src, api, services
globs:
  - "**/src/**"
  - "**/api/**"
  - "**/services/**"
---

# Multi-glob rules

Body content

===== windsurf =====
---
trigger: glob
globs:
  - "**/src/**"
  - "**/api/**"
  - "**/services/**"
---

# Multi-glob rules

Body content

===== claude =====
---
paths:
  - "**/src/**"
  - "**/api/**"
  - "**/services/**"
---

# Multi-glob rules

Body content

Optimizer on the same primitive (no misleading warning):

Placements: {PosixPath('/private/tmp/repro-1366'): [Instruction(name='multi', ...)]}
Warnings: []

Local verification gate:

  • uv run --extra dev ruff check src/ tests/ — silent
  • uv run --extra dev ruff format --check src/ tests/ — silent
  • uv run --extra dev pytest tests/unit — 8762 passed (1 pre-existing
    unrelated failure: test_runtime_windows::test_execute_runtime_command_uses_shlex_on_unix
    — environment-specific codex path resolution, exists on main)
  • New e2e test (tests/integration/test_apply_to_comma_e2e.py) — green
  • New optimizer tests (TestApplyToCommaInOptimizer) — green
  • New converter tests (TestApplyToCommaSplitting) — green
  • New helper tests (tests/unit/utils/test_patterns.py) — green

Scope note

The Copilot target is intentionally untouched in this fix — it copies the
instruction verbatim, and the consuming tool handles the comma split. The
monolithic AGENTS.md / GEMINI.md compilers (agents_compiler.py,
distributed_compiler.py, claude_formatter.py for AGENTS injection) are
also out of scope; the issue specifically calls out the per-file Claude /
Cursor / Windsurf converters and the placement optimizer.

…sor/Windsurf

The applyTo frontmatter is documented as 'Glob (or comma-separated globs)'
but the Claude, Cursor, and Windsurf converters emitted the comma-list as
one literal glob string. The context optimizer also fed the unsplit value
to glob/fnmatch, producing misleading 'matches no files' warnings.

Changes:
- New helper parse_apply_to() in src/apm_cli/utils/patterns.py owns the
  single source of truth for comma-splitting semantics (whitespace trim,
  empty/trailing/lone-comma tolerance).
- Cursor, Windsurf, and Claude converters now branch single-glob (scalar,
  backwards-compatible) vs multi-glob (YAML list) when emitting native
  frontmatter.
- Optimizer's _file_matches_pattern splits only top-level commas (so brace
  alternation like '**/*.{css,scss}' still works), and any-segment-matches
  semantics flow through unchanged caches.
- Optimizer fallback warning now names the primitive (instead of echoing
  a potentially 80+ char comma-list) and only fires when zero segments
  matched anything.
- _extract_intended_directory_from_pattern uses the first segment when
  given a comma-list (was previously trying the whole literal as a path).
- Copilot target is intentionally untouched - it preserves applyTo
  verbatim and the consuming tool splits.

Fixes #1366

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 19, 2026 09:46
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes documented applyTo behavior by treating comma-separated values as multiple globs across the Claude/Cursor/Windsurf converters and the compilation context optimizer, while preserving Copilot’s verbatim behavior.

Changes:

  • Introduces parse_apply_to() as a shared helper for trimming/splitting comma-separated applyTo values.
  • Updates Claude/Cursor/Windsurf instruction converters to emit either a scalar (single glob) or a YAML list (multi-glob).
  • Updates the context optimizer to treat comma-lists as “match any segment”, improves fallback directory inference, and reduces warning noise (names primitive instead of echoing the raw pattern).
Show a summary per file
File Description
src/apm_cli/utils/patterns.py Adds shared applyTo parsing helper used by converters/optimizer.
src/apm_cli/integration/instruction_integrator.py Uses shared parser to emit correct multi-glob YAML for Claude/Cursor/Windsurf.
src/apm_cli/compilation/context_optimizer.py Handles comma-lists in matching/placement and improves warning messages.
tests/unit/utils/test_patterns.py Adds unit coverage for parse_apply_to().
tests/unit/integration/test_instruction_integrator.py Adds converter coverage for comma-lists + Copilot verbatim regression test.
tests/unit/compilation/test_context_optimizer.py Adds optimizer coverage for comma-lists and warning behavior.
tests/integration/test_apply_to_comma_e2e.py Adds end-to-end test across all four targets.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 2

Comment on lines +11 to +23
def parse_apply_to(value: str | None) -> list[str]:
"""Split a primitive ``applyTo`` value into individual glob patterns.

The input is either a single glob (``"**/*.py"``) or a
comma-separated list (``"**/src/**,**/api/**"``). Each segment is
stripped of surrounding whitespace; empty segments are discarded so
leading, trailing, doubled-up, and lone commas are tolerated.

Returns an empty list for ``None``, empty, or whitespace-only input.
"""
if not value:
return []
return [segment for segment in (part.strip() for part in value.split(",")) if segment]
Comment on lines +748 to +754
# applyTo accepts a comma-separated list of globs; treat any
# segment match as a hit so list patterns mirror per-glob semantics.
# Only split on top-level commas - commas inside brace alternation
# (e.g. ``**/*.{css,scss}``) must stay attached for brace expansion.
if _has_top_level_comma(pattern):
segments = parse_apply_to(pattern)
return any(self._file_matches_pattern(file_path, seg) for seg in segments)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] applyTo comma-separated globs are not split ? Claude target emits broken paths: and compiler warns "matches no files"

2 participants