fix(integration): respect comma-separated applyTo globs in Claude/Cursor/Windsurf#1387
Open
danielmeppiel wants to merge 1 commit into
Open
fix(integration): respect comma-separated applyTo globs in Claude/Cursor/Windsurf#1387danielmeppiel wants to merge 1 commit into
danielmeppiel wants to merge 1 commit into
Conversation
…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>
Contributor
There was a problem hiding this comment.
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-separatedapplyTovalues. - 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1366
WHY
applyTofrontmatter is documented as "Glob (or comma-separated globs)" indocs/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 fileswarnings).Copilot was already correct because its converter preserves
applyToverbatimand the consuming tool splits — that path is intentionally unchanged.
WHAT
parse_apply_to(value) -> list[str]insrc/apm_cli/utils/patterns.pyowns 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_rulesnow call the helper and branch:context_optimizer._file_matches_patternsplits only top-level commas(so brace alternation like
**/*.{css,scss}is unaffected) and treats anysegment match as a hit.
_extract_intended_directory_from_patternconsults the first segment whengiven a comma-list (previously it tried the whole literal as a path).
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: strbecause it is used as a dict key intemplate_builder.pyand a set member inclaude_formatter.py; converting tolist[str]would have broken hashability and silently changed grouping.Considered alternatives:
applyToonce at primitive-load time inparser.py. Rejected:forces the type change above and changes serialisation everywhere; only the
three converters and the optimizer actually need split form.
rules and drifts.
[[rules]]blocks. Rejected: notrequired by Cursor's
gray-matterparser — YAML list is accepted and farless invasive.
Validation evidence
Original repro from the issue: primitive with
applyTo: '**/src/**,**/api/**,**/services/**'compiled to all four targets:Optimizer on the same primitive (no misleading warning):
Local verification gate:
uv run --extra dev ruff check src/ tests/— silentuv run --extra dev ruff format --check src/ tests/— silentuv run --extra dev pytest tests/unit— 8762 passed (1 pre-existingunrelated failure:
test_runtime_windows::test_execute_runtime_command_uses_shlex_on_unix— environment-specific codex path resolution, exists on
main)tests/integration/test_apply_to_comma_e2e.py) — greenTestApplyToCommaInOptimizer) — greenTestApplyToCommaSplitting) — greentests/unit/utils/test_patterns.py) — greenScope 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.pyfor AGENTS injection) arealso out of scope; the issue specifically calls out the per-file Claude /
Cursor / Windsurf converters and the placement optimizer.