fix(acms): context path matching silently ignores exclude/include globs on absolute paths #10972

Closed
opened 2026-05-05 14:26:45 +00:00 by hamza.khyari · 2 comments
Member

Summary

_path_matches() in execute_phase_context_assembler.py uses PurePath.full_match(pattern) but the fragment metadata stores absolute paths (/app/.opencode/skills/SKILL.md) while project context --exclude-path / --include-path settings produce relative globs (.opencode/*, docs/*). full_match requires the ENTIRE path to match, so relative globs NEVER match absolute paths — making the include/exclude filters silently ineffective.

Steps to Reproduce

  1. Create a project with a linked git-checkout resource
  2. Set context exclusions:
    agents project context set local/my-project \
      --exclude-path ".opencode/*" \
      --exclude-path "docs/*" \
      --exclude-path "features/*"
    
  3. Run agents plan execute
  4. Observe that files matching the exclusion patterns still appear in the tier routing logs:
    [warning] tier.oversized_fragment_redirected
      fragment_id=<id>:.opencode/skills/.../SKILL.md
    

Root Cause

execute_phase_context_assembler.py:74-81:

@staticmethod
def _path_matches(path: str, include: list[str], exclude: list[str]) -> bool:
    pure_path = PurePath(path)
    if include and not any(pure_path.full_match(pattern) for pattern in include):
        return False
    return not (
        exclude and any(pure_path.full_match(pattern) for pattern in exclude)
    )

Verification:

>>> PurePath("/app/.opencode/skills/SKILL.md").full_match(".opencode/*")
False  # relative glob vs absolute path — never matches
>>> PurePath("/app/.opencode/skills/SKILL.md").match("**/.opencode/*")
False  # match() doesn't recurse into directories
>>> PurePath("/app/.opencode/skills/SKILL.md").full_match("**/.opencode/**/*")
True   # only works with **/ prefix

Impact

  • Exclude paths silently ignored — all files enter the tier routing pipeline regardless of project context settings, eating the hot-tier token budget
  • Include paths silently ineffective — when include filters are set, zero fragments match because relative patterns never match absolute paths, resulting in empty context
  • Affects both include AND exclude paths — both paths through _path_matches
  • Same pattern in context_phase_analysis.py:98 — same bug, same result

Affected Files

  • src/cleveragents/application/services/execute_phase_context_assembler.py:78 — primary bug location
  • src/cleveragents/application/services/context_phase_analysis.py:98 — same full_match pattern
  • src/cleveragents/application/services/context_service.py:355-362 — may also be affected; uses different matching logic

Fix

Option A — Normalize paths to relative before matching:

pure_path = PurePath(path)
rel_path = pure_path.relative_to(pure_path.anchor) if pure_path.is_absolute() else pure_path
if include and not any(rel_path.full_match(pattern) for pattern in include):
    return False

Option B — Auto-prefix patterns with **/ to match from any root (simpler, no path normalization needed):

def _path_matches(path: str, include: list[str], exclude: list[str]) -> bool:
    pure_path = PurePath(path)
    if include and not any(pure_path.match(f"**/{p}") for p in include):
        return False
    return not (exclude and any(pure_path.match(f"**/{p}") for p in exclude))

Metadata

  • Commit Message: fix(acms): normalize context path matching for absolute paths in _path_matches
  • Branch: bugfix/acms-path-matching-absolute
## Summary `_path_matches()` in `execute_phase_context_assembler.py` uses `PurePath.full_match(pattern)` but the fragment metadata stores absolute paths (`/app/.opencode/skills/SKILL.md`) while project context `--exclude-path` / `--include-path` settings produce relative globs (`.opencode/*`, `docs/*`). `full_match` requires the ENTIRE path to match, so relative globs NEVER match absolute paths — making the include/exclude filters silently ineffective. ## Steps to Reproduce 1. Create a project with a linked git-checkout resource 2. Set context exclusions: ```bash agents project context set local/my-project \ --exclude-path ".opencode/*" \ --exclude-path "docs/*" \ --exclude-path "features/*" ``` 3. Run `agents plan execute` 4. Observe that files matching the exclusion patterns still appear in the tier routing logs: ``` [warning] tier.oversized_fragment_redirected fragment_id=<id>:.opencode/skills/.../SKILL.md ``` ## Root Cause `execute_phase_context_assembler.py:74-81`: ```python @staticmethod def _path_matches(path: str, include: list[str], exclude: list[str]) -> bool: pure_path = PurePath(path) if include and not any(pure_path.full_match(pattern) for pattern in include): return False return not ( exclude and any(pure_path.full_match(pattern) for pattern in exclude) ) ``` **Verification:** ```python >>> PurePath("/app/.opencode/skills/SKILL.md").full_match(".opencode/*") False # relative glob vs absolute path — never matches >>> PurePath("/app/.opencode/skills/SKILL.md").match("**/.opencode/*") False # match() doesn't recurse into directories >>> PurePath("/app/.opencode/skills/SKILL.md").full_match("**/.opencode/**/*") True # only works with **/ prefix ``` ## Impact - **Exclude paths silently ignored** — all files enter the tier routing pipeline regardless of project context settings, eating the hot-tier token budget - **Include paths silently ineffective** — when include filters are set, zero fragments match because relative patterns never match absolute paths, resulting in empty context - **Affects both include AND exclude paths** — both paths through `_path_matches` - **Same pattern in `context_phase_analysis.py:98`** — same bug, same result ## Affected Files - `src/cleveragents/application/services/execute_phase_context_assembler.py:78` — primary bug location - `src/cleveragents/application/services/context_phase_analysis.py:98` — same `full_match` pattern - `src/cleveragents/application/services/context_service.py:355-362` — may also be affected; uses different matching logic ## Fix Option A — Normalize paths to relative before matching: ```python pure_path = PurePath(path) rel_path = pure_path.relative_to(pure_path.anchor) if pure_path.is_absolute() else pure_path if include and not any(rel_path.full_match(pattern) for pattern in include): return False ``` Option B — Auto-prefix patterns with `**/` to match from any root (simpler, no path normalization needed): ```python def _path_matches(path: str, include: list[str], exclude: list[str]) -> bool: pure_path = PurePath(path) if include and not any(pure_path.match(f"**/{p}") for p in include): return False return not (exclude and any(pure_path.match(f"**/{p}") for p in exclude)) ``` ## Metadata - **Commit Message**: `fix(acms): normalize context path matching for absolute paths in _path_matches` - **Branch**: `bugfix/acms-path-matching-absolute`
hamza.khyari added this to the v3.5.0 milestone 2026-05-05 14:26:46 +00:00
Owner

Implementation Attempt — Tier 3: sonnet — Success

Fixed _path_matches() in execute_phase_context_assembler.py and _matches_pattern() in context_phase_analysis.py to correctly handle absolute paths with relative glob patterns.

Root cause: PurePath.full_match(pattern) requires the ENTIRE path to match, so relative globs like .opencode/* never matched absolute paths like /app/.opencode/skills/SKILL.md.

Fix applied:

  • Added _glob_matches() static helper in ACMSExecutePhaseContextAssembler that auto-prefixes relative patterns with **/ so they match any trailing segment of an absolute path
  • Updated _path_matches() to use _glob_matches() instead of full_match()
  • Updated _matches_pattern() in context_phase_analysis.py with the same approach

Tests added:

  • 6 new BDD scenarios in execute_phase_context_assembler_coverage.feature for absolute path matching
  • 2 new BDD scenarios in project_context_phase_analysis.feature for absolute path matching in phase analysis
  • Corresponding step definitions in project_context_phase_analysis_steps.py

Quality gates: lint ✓, typecheck ✓

Note: unit_tests gate is hanging locally (pre-existing issue — master branch CI also shows unit_tests failing). CI will verify on the PR.

PR: #10975


Automated by CleverAgents Bot
Supervisor: Implementation | Agent: implementation-worker

**Implementation Attempt** — Tier 3: sonnet — Success Fixed `_path_matches()` in `execute_phase_context_assembler.py` and `_matches_pattern()` in `context_phase_analysis.py` to correctly handle absolute paths with relative glob patterns. **Root cause**: `PurePath.full_match(pattern)` requires the ENTIRE path to match, so relative globs like `.opencode/*` never matched absolute paths like `/app/.opencode/skills/SKILL.md`. **Fix applied**: - Added `_glob_matches()` static helper in `ACMSExecutePhaseContextAssembler` that auto-prefixes relative patterns with `**/` so they match any trailing segment of an absolute path - Updated `_path_matches()` to use `_glob_matches()` instead of `full_match()` - Updated `_matches_pattern()` in `context_phase_analysis.py` with the same approach **Tests added**: - 6 new BDD scenarios in `execute_phase_context_assembler_coverage.feature` for absolute path matching - 2 new BDD scenarios in `project_context_phase_analysis.feature` for absolute path matching in phase analysis - Corresponding step definitions in `project_context_phase_analysis_steps.py` **Quality gates**: lint ✓, typecheck ✓ Note: unit_tests gate is hanging locally (pre-existing issue — master branch CI also shows unit_tests failing). CI will verify on the PR. PR: https://git.cleverthis.com/cleveragents/cleveragents-core/pulls/10975 --- Automated by CleverAgents Bot Supervisor: Implementation | Agent: implementation-worker
Author
Member

PR #10975 Review Findings & Spec Decision

1. Trailing ** Bug in _matches_any()

The **/ prefix approach in _matches_any() fails when the user pattern ends with **:

# User provides:  ".opencode/**"
# Prefix becomes: "**/.opencode/**"
# PurePath.match("**/.opencode/**") returns False
# Because match() requires trailing ** to consume >=1 component

Fix (if keeping Option B):

if not pattern.startswith("**/"):
    prefixed = f"**/{pattern}"
    if pattern.endswith("**"):
        prefixed += "/*"  # trailing ** needs explicit wildcard
    if pure_path.match(prefixed):
        return True

2. match() semantics shift

full_match()match() changes behavior: a bare "README.md" exclude would match any README.md at any depth, not just root. Acceptable for this use case but should be documented.

3. Spec Gap — Absolute vs Relative Path Matching

The specification uses mixed patterns — "**/node_modules/**" (root-anchored) for exclusions and "src/**/*.py" (relative) for includes — but never specifies whether fragment metadata paths are absolute or relative, or how matching should work across the two.

Decision: Option A — Normalize fragment paths to relative before matching

Pro Con
Aligns with spec examples ("src/**/*.py") Must know project root per fragment
Single match(), no edge cases Multi-project needs per-root mapping
User writes what they see in ls
"README.md" = root only (correct semantics)

Implementation approach:

  1. Store project root path per resource in the tier service or fragment metadata
  2. Before matching, strip the project root from fragment.metadata["path"] to get a relative path
  3. Match relative path against user-supplied globs directly
  4. Apply in both execute_phase_context_assembler.py and context_phase_analysis.py

This avoids all **/ prefix edge cases and matches spec examples exactly.


@hamza.khyari

## PR #10975 Review Findings & Spec Decision ### 1. Trailing `**` Bug in `_matches_any()` The `**/` prefix approach in `_matches_any()` fails when the user pattern ends with `**`: ```python # User provides: ".opencode/**" # Prefix becomes: "**/.opencode/**" # PurePath.match("**/.opencode/**") returns False # Because match() requires trailing ** to consume >=1 component ``` Fix (if keeping Option B): ```python if not pattern.startswith("**/"): prefixed = f"**/{pattern}" if pattern.endswith("**"): prefixed += "/*" # trailing ** needs explicit wildcard if pure_path.match(prefixed): return True ``` ### 2. `match()` semantics shift `full_match()` → `match()` changes behavior: a bare `"README.md"` exclude would match any `README.md` at any depth, not just root. Acceptable for this use case but should be documented. ### 3. Spec Gap — Absolute vs Relative Path Matching The specification uses mixed patterns — `"**/node_modules/**"` (root-anchored) for exclusions and `"src/**/*.py"` (relative) for includes — but never specifies whether fragment metadata paths are absolute or relative, or how matching should work across the two. **Decision: Option A — Normalize fragment paths to relative before matching** | Pro | Con | |-----|-----| | Aligns with spec examples (`"src/**/*.py"`) | Must know project root per fragment | | Single `match()`, no edge cases | Multi-project needs per-root mapping | | User writes what they see in `ls` | | | `"README.md"` = root only (correct semantics) | | **Implementation approach:** 1. Store project root path per resource in the tier service or fragment metadata 2. Before matching, strip the project root from `fragment.metadata["path"]` to get a relative path 3. Match relative path against user-supplied globs directly 4. Apply in both `execute_phase_context_assembler.py` and `context_phase_analysis.py` This avoids all `**/` prefix edge cases and matches spec examples exactly. --- @hamza.khyari
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#10972
No description provided.