Repository navigation
fix(workspace): glob and grep's glob filter take a mount-prefixed pattern - #401
Conversation
…tern Every workspace path is mount-prefixed, glob's own results included, but the pattern was matched only against paths relative to the mount (or to `path`), so `notes/diary/2026-10-13.md` answered "no matches" for a file that was there. When a pattern's first segment names a mount, its rest is now also matched within that mount; a pattern that names no mount matches as before, including a relative one whose first directory shares a mount's name. grep's `glob` filter takes the same two readings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Medium impact] The PR appears safe to merge; no actionable issues were found. SummaryAdds mount-prefixed patterns to
Reviews (1) · Last reviewed commit: "fix(workspace): glob and grep's glob fil..." · Reviewed by Greptile |
|
@Anarchid This fixes the resident's case, and reading the pattern both ways keeps every match What we ran, on Linux with Node 24.22.0, at
The gap.
The change. In function globPartToRegex(part: string): string {
return part
.replace(/[.+^$()|[\]\\]/g, '\\$&')
.replace(/(^|\/)\*\*\//g, '$1__DOUBLESTARSLASH__')
.replace(/\*\*/g, '__DOUBLESTAR__')
.replace(/\*/g, '[^/]*')
.replace(/\?/g, '[^/]')
.replace(/__DOUBLESTARSLASH__/g, '(?:.*/)?')
.replace(/__DOUBLESTAR__/g, '.*');
}The We'd like it in this PR, so its descriptions and tests don't teach agents the gap. If this PR merges without it, we'll fix it on Optional. A pattern whose first segment names no mount, such as the typo Gordon-2414 (Claude Opus 5.5) reviewed — Lottie-2661 |
globPartToRegex turned `**` into `.*`, so a `**/` needed at least one directory before what followed it: `project/src/**/*.ts` missed `project/src/main.ts`, and `**/*.md` missed the `.md` files at a mount's root. Both tools' descriptions give `**/` patterns as examples, and other glob tools read `**/` as zero or more directories. globPartToRegex now converts each glob token in one pass, through a table: a `**/` at the start of a path segment becomes `(?:.*/)?`, any other `**` is still `.*`, and `*`, `?` and the characters a regex treats specially are converted as before. globToRegex tells each part whether it starts a path segment, so a `**/` right after a brace group, or at the start of an alternative that doesn't start a segment, still needs a directory there. The fragment says so. Its sentence on patterns that name no mount now says that every pattern is still matched relative to each mount as well, which "match exactly as before" no longer covered. Tests: the two expectations that pinned the old reading now include the root files. A new test covers a mount-prefixed and a relative `**/`, a `**/` under `path`, and grep's glob filter, with a file one directory deeper than `project/src/main.ts` in the fixture. Another covers a `**/` right after a brace group. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@Anarchid We pushed A The two test expectations that pinned the old reading now include the root files. One new test covers a mount-prefixed At Separately, an unclosed Written by Gordon-2414. — Lottie-2661 |
main has anima-research#439, which refuses a glob brace group it can't read: a "{" that never closes, a group inside another, and a group with no ",". Both changes rewrote globToRegex, so they conflicted there. This merge keeps both: - anima-research#439's depth-matched close and its three refusals, with braceRefusal; - this branch's atSegmentStart, which every part, a brace group's alternatives included, now passes to globPartToRegex; - this branch's GLOB_TOKENS globPartToRegex. Against main, the merged module differs by this branch's change alone. As said on this pull request, whichever of the two landed second takes the other's lines; anima-research#439 landed first.
…t refuses is refused first Since anima-research#439, globToRegex refuses a brace group it can't read. This branch builds grep's glob filter for each mount inside that mount's withMount callback, after the pass. So a refused filter was refused only after the first mount had been searched, and not at all when every mount's pass gave up because the selected branch kept changing; grep then answered with its incomplete list. On main, the filter was built, and refused, before any mount was searched. It's now built before the pass, as glob builds its own pattern. anima-research#439's refusal tests pass either way: that the refusal now comes first is by reading.
|
As we said earlier, whichever of this pull request and #439 landed second would take the other's lines. #439 landed first, so I've merged — Gordon-2414 |
|
@Anarchid This branch no longer merged with One conflict needed a decision, not only a resolution: where a mount-prefixed pattern's walk starts.
A second commit fixes a The pull request's description has the details, the tests and the checks. The typecheck, the build and the whole test suite pass on Linux with Node 24, and this branch's own tests still fail on Written by Gordon-2414. — Lottie-2661 |
|
@Anarchid We've opened #696, which lands this pull request. It holds this branch unchanged through The merge changes one thing in what this branch does. In the mount a pattern's first segment names, the pattern is now read only mount-prefixed, where this branch also kept it as written there. So in a mount holding a directory named like itself, Written by Gordon-2414. — Lottie-2661 |
fix(workspace): glob and grep take a mount-prefixed pattern (#401, with main merged) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
workspace--globmatches its pattern against paths relative to the mount (or topath), but every path the workspace tools take and return is mount-prefixed, including glob's own results. So the pattern an agent naturally writes,notes/diary/2026-10-13.mdorproject/src/**/*.ts, can never match, and glob answersmatches: [], count: 0with nothing to say the pattern was read differently.grep'sglobfilter has the same gap: it is tested against the mount-relative path, so a mount-prefixed filter silently excludes every file.In production this showed up as a resident whose every glob over eleven days returned 0 matches (10 of 10), three times for files that an
lsseconds later listed. Once it trusted the empty answer, concluded that a journal entry another process had written that morning did not exist, and wrote a fresh file over it.Changes
src/modules/workspace/index.ts:mountGlobs(pattern, mountName)gives the two readings of a pattern for one mount: the pattern as written, matched relative to the search root exactly as before, and, when the pattern's first segment is that mount's name, the rest of the pattern matched against the mount-relative path.handleGlobmatches a file when either reading does. Sonotes/diary/*.mdfinds the diary files,project/**/*.mdstays withinproject, and a relative pattern keeps every match it had. That includes a relative pattern whose first directory shares a mount's name:notes/*.mdstill matchesproject/notes/todo.md, now besidenotes/README.md. Withpath, a mount-prefixed pattern works too (path: notes/diary,pattern: notes/diary/*-13.md).handleGrep'sglobfilter uses the same two readings.I didn't test both forms against every mount, because then
*/README.mdwould start matching every mount's root README. The prefixed reading applies only when the first segment is literally a mount's name.Tests
New
test/workspace-glob-mount-prefix.test.ts: two mounts, files written straight to disk as another process would. It covers a prefixed exact file; a prefixed wildcard staying within its mount; relative**/*.mdand*.mdunchanged; a relative pattern whose first segment shares a mount's name keeping its old match;pathwith a relative and with a prefixed pattern; and grep'sglobfilter, prefixed and relative.npm run buildthennpm test:main@ 91e70ed: 2367 tests, 2367 pass, 0 fail.agent-runtime-settingsandevent-gate-passthroughearly, so their tests weren't counted (the runner did the same to other files on another branch today). Run alone on this branch, they pass 51/51 and 17/17.main'ssrcrebuilt under them. In "relative patterns match as before", only the union assertion (notes/*.mdnow also returnsnotes/README.md) fails there; the**/*.mdand*.mdassertions pass onmainunchanged.Not verified
watchtool,write { append }#271 (direct backend) both touchhandleGlob/handleGrepin this file, so whichever lands second needs a small rebase. Under feat(workspace): direct backend (default),watchtool,write { append }#271's direct backend, glob and grep would need the same two readings; this PR doesn't touch that code, since it isn't onmain.Changelog fragment:
changelog.d/glob-mount-prefixed-patterns.fixed.md.🤖 Generated with Claude Code