fix(tools): glob '**/' now matches zero or more directories (root & direct children) - #1770
Merged
Merged
Conversation
Translate the glob to a single regex in a shared globMatcher helper, mirroring sun.nio.fs.Globs with one change: '**/' becomes (?:[^/]+/)* (zero or more directory segments) instead of forcing a literal separator. Each '**/' matches independently, so multiple/adjacent globstars work. findFiles now filters to regular files; DirectoryTextSearch (RAG) inherits the fix unchanged. Signed-off-by: Charles <azanux@gmail.com>
Signed-off-by: Charles <azanux@gmail.com>
igordayen
reviewed
Jul 8, 2026
Signed-off-by: Charles <azanux@gmail.com>
alexheifetz
approved these changes
Jul 9, 2026
alexheifetz
left a comment
Contributor
There was a problem hiding this comment.
Overall: LGTM, good test coverage. Two non-blocking questions:
-
Case-sensitivity: NIO's
getPathMatcher("glob:")was case-insensitive on Windows; the newPattern.compileisn't. SofindFiles("**/*.KT")on Windows now returns nothing. Intended? If so, worth noting in "Behavior change." -
Char classes match
/: unlike JDK Globs,[!x]→[^x]can cross a dir boundary (foo[!x]barmatchesfoo/bar). Edge case — worth excluding/from generated classes.
…l '[' Signed-off-by: Charles <azanux@gmail.com>
Collaborator
Author
|
Thanks @alexheifetz for the thorough review!
|
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.
Problem
Fixes #1751.
Glob matching used Java NIO
PathMatcher(glob:…), where a/after**requires at least one directory, unlike ripgrep/git/bash globstar, which treat**/as zero or more. As a result:src/**/*.solsilently skipped files directly insrc/(only returned nested ones)**/*.solmissed root-level filesFileReadTools.findFilesalso returned directories as if they were filesThe failure was silent (no error), hit the documented recommended pattern
**/*.kt, and caused two concrete harms:FileReadTools.findFiles, an@LlmTool) acted on incomplete file listings.DirectoryTextSearch.fileGlob) silently never indexed top-level files, causing context loss / hallucinations.Separately,
DirectoryTextSearch.extractChunksAroundMatcheslocated match positions on a lowercased copy of the file content (content.lowercase()) and then used those offsets to slice the original content viasubstring.String.lowercase()can change the string length for some Unicode characters (e.g.İU+0130 →i̇, 2 chars). When such a character appears before a match, the offsets computed on the lowercased copy are shifted relative to the original, so the resulting chunk is sliced from the wrong region and no longer contains the matched term.Fix
Glob semantics. Translate the glob to a single regex in the shared
globMatcherhelper (FileReadTools.kt), mirroring the JDK translator (sun.nio.fs.Globs) with one change: a**/segment becomes(?:[^/]+/)*(zero or more directory segments) instead of forcing a literal separator.Each
**/now matches independently in a single pass, so any number of them in one pattern works:a/**/b/**/cmatches all 0/1/n combinations, not just the all-zero or all-≥1 extremes. Every other glob feature is preserved:*/?stay within a segment, braces{a,b}, char classes[a-z]/[!a], andregex:passthrough.findFilesalso gained anisRegularFilefilter so directories are never returned.DirectoryTextSearch(RAG) previously built its own inlinePathMatcher; it now switches to the sharedglobMatcherhelper (a one-line swap) and inherits the same semantics.globMatcher's public signature is unchanged.Unicode offsets. Search the original content directly, case-insensitively, via
content.indexOf(term, index, ignoreCase = true). Offsets are then valid forsubstring, and the redundantcontent.lowercase()in this method is removed.Behavior change
findFilesnow applies anisRegularFilefilter, so directories matching the glob are no longer returned, only files.Why a hand-rolled glob to regex translator?
Glob patterns here are written by the LLM, so the matcher must support the full glob vocabulary it will reach for, otherwise a pattern silently matches nothing and the agent acts on a partial/empty file list.
Tests
FileReadToolsGlobTest: full glob spec: zero/one/many dirs, leading & prefixed**/, single-segment wildcards, braces/classes/ranges, literal-dot and backslash escaping,regex:passthrough, files-only, over-match guards (a look-alike filename and a sibling directory must not leak in), multiple/adjacent globstars, andfindHighestpruning.DirectoryTextSearchGlobTest: same glob semantics for the RAG file filter.DirectoryTextSearchTest.EncodingTests: failing-first test: a file with length-changing lowercase chars before the match now yields a chunk that actually contains the searched term.@Disabledtest documents a deliberate semantic divergence: a non-isolated**(src/**Foo.sol) falls back to.*and crosses/, where minimatch/bash treat it as a plain*. Product decision, not a bug.mvn testonembabel-agent-apiandembabel-agent-ragpasses green.