Repository navigation
fix(workspace): glob and grep take a mount-prefixed pattern (#401, with main merged) - #696
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>
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>
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.
Since this branch's last merge with main, anima-research#511 made glob start its walk at the directory its pattern names before the first wildcard, beneath its path, and grep at the deeper of its path and its filter's directory, so a search on a mount past its walk cap still reaches what its pattern names. This branch matches a pattern whose first segment is a mount's name, in that mount, from the mount's root. Taken as main has it, the walk for such a pattern would start at the directory of the pattern as written: for notes/diary/*.md in the notes mount, notes/diary inside it. That isn't there, so walkStart stops at the root, and past the cap the walk can stop before diary. So in the mount a pattern's first segment names, the pattern is read mount-prefixed only, in place of the pattern as written there, and its walk starts at the rest's literal directory, from the mount's root and within the path (prefixedGlob gives the rest's regex and directory, in the place of mountGlobs). In every other mount, and for a pattern whose first segment names no mount, the pattern is read as written, and the walk is main's. Each mount has one reading and one walk, through main's walkStart, listingOf and textsFrom*. What that gives up: this branch also kept the pattern as written in the mount it names, which can match only beneath a directory named like the mount itself. Keeping both readings there and anima-research#511's walk start takes two walks, a merge of their entries and a check of what the workspace tracks beneath each, for that one case. So in a mount holding a directory named like itself, a pattern written relative to the mount, such as work/z/*.ts meaning work/work/z/a.ts, now reads mount-prefixed and can answer "no matches" for that file. work/work/z/*.ts, as the tools show the path, finds it. The fragment says so. The other conflicts: glob's description says how a pattern is read, with main's direct-mount note. grep's glob description is this branch's, with main's integer context types. main's regex for the pattern as written is built first, as main builds it, so a pattern globToRegex refuses is refused before the path is read. Tests, beside anima-research#511's: - past the walk cap, a mount-prefixed pattern walks from the directory the rest of it names, in glob and in grep's filter, and names no region outside it, also where the mount holds part of a directory named like itself; - in such a mount, a pattern naming the mount reads mount-prefixed only, and the tools' own path for a file beneath that directory finds it, within the cap and past it, with a path too; - on a direct mount, a mount-prefixed pattern finds what lies beneath the ignored directory it names; - a mount-prefixed pattern lists a draft the workspace holds where disk has no directory; - with a path, a pattern naming the path's mount reads mount-prefixed, within the path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ro or more
A `**/` right after a brace group was always read as not starting a path
segment, so after `{src/,lib/}` it still needed a directory, though each
alternative ends one and `{src,lib}/**/*.ts` finds the same files with
none. Whether it starts a segment there depends on the alternative that
matched, so a `**/` at the start of any part of a pattern now reads by
what comes before it in the path: zero or more directories where the path
so far is empty or ends with a `/`, and otherwise any run of characters
and then a directory, as before. So `{src/,draft}**/*.ts` matches
`src/main.ts` and `draftx/y.ts`, `draft{s,ing}**/` still needs a
directory, and an empty alternative ends a directory only where the group
starts one. The parts no longer carry whether they start a segment.
Tests: those cases; a `**/` that starts a brace alternative, at a
segment's start and not; and a pattern's `+`, `.` and parentheses
matching themselves against names their regex reading would also take.
The fragment's `**/` sentence says "at the start of a path segment", as
the code does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… so a `**` ending one reads as `**/`
A `**` that ends a brace alternative, with a `/` right after the group,
was read as any run of characters and then a literal `/`, never as a
`**/`. So `{src/**,lib}/*.ts` didn't match `src/main.ts`, though its
expansion `src/**/*.ts` does, since a `**/` at the start of a path
segment matches zero or more directories here. `{a,b}/x` and `{a/,b/}x`
name the same paths, so the `/` now goes into each alternative before it
is read. Each alternative's own `**` meets it and reads as its expansion
does. Without a `/` after the group, an alternative's `**` is any run of
characters, as before. A group inside another is still refused before
any of this.
Tests:
- test/workspace-glob-mount-prefix.test.ts: `project/{src/**,docs}/*.ts`
and `project/{src/**,docs/**}/*.ts` find `project/src/main.ts` beside
the rest, and `project/{src/**,docs}.ts` still reads `**` as any run
of characters.
- test/workspace-glob-unclosed-brace.test.ts: `{a,{b/**,c}}/x` is
refused as a group inside another.
Found by an internal review run (af401-merge-r3).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… across mounts; text for the tool-text change From an internal review run (af401-cut) of the merge of main: - a pattern with no `/` that starts with a mount's name is read relative to the path, not as naming the mount, and a first segment that is a brace group or a wildcard names no mount; - with a path, a mount-prefixed pattern whose rest names a directory outside the path finds nothing there; - grep's filter, with a path, is confined to it: notes/*.md finds notes/README.md without the path, and nothing within notes/diary; - grep reads a filter naming a mount as written in every other mount; - the escaping test's decoys now include axb(c).ts, so an unescaped `.` fails it. walkCap's doc names the directory a mount-prefixed pattern's walk starts from. The fragment says that glob's and grep's tool texts changed, so the first request after the upgrade that carries the workspace's tools misses the prompt cache from its tool block on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; glob's and grep's texts say how other mounts read a pattern In a mount holding a directory named like itself, a pattern written relative to the mount (work/z/*.ts for work/work/z/a.ts) stops finding a file it finds today, so that narrowing moves out of the fixed fragment into glob-mount-prefixed-patterns.breaking.md, with whom it reaches, the path to write instead and what's unchanged. The fixed fragment keeps the mount-prefixed reading, the zero-directory `**/` and the brace-group reading, and points to Breaking for the narrowing. glob's description and grep's glob parameter now say that in every other mount a pattern is read as written, so a mount-prefixed pattern also finding a matching path inside another mount follows from the text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oted tool string
"the agent meets it as "no matches"" read as the tools' own words, which
neither tool says: glob answers {"matches": [], "count": 0} and grep
{"results": [], "totalMatches": 0}, each a success, not an error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Automated review
Recommendation: merge and fast-follow. 1 medium severity finding can be fixed after the merge.
Reviewed commit 8eccdf9 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. This fits. It fixes how the existing workspace glob and grep tools read patterns, stays inside the workspace module, and lands with the tests and changelog fragments that CONTRIBUTING.md asks for, the narrowed behaviour included.
5 reasons with sources
- It fixes behaviour of tools the project already has, so it is in scope. The
globandgreptools already return mount-prefixed paths. Before this change they matched patterns only relative to each mount, so a natural pattern likenotes/diary/*.mdcame back empty. The change makes the tools accept the same path form they already return. (src/modules/workspace/index.ts) - The code stays inside the workspace module and reuses its existing walk instead of adding a second one.
prefixedGlobis added next toglobToRegexandliteralDirOf. Both tools still go through the existingwalkStart,listingOfandtextsFromDisk/textsFromStore, and only the choice of regex and starting directory changes. The walk-start code copied betweenglobandgrepis a code-quality point that the separate code review already raised. (src/modules/workspace/index.ts) - The narrowed behaviour is declared as Breaking in its own changelog fragment, as the changelog rules require.
changelog.d/glob-mount-prefixed-patterns.breaking.mdand.fixed.mdfollow the<slug>.<category>.mdformat. The code review found the Breaking note incomplete because it leaves out path-relative patterns. That needs a fix in the text, but it does not break the requirement that a fragment lands with the change. (CONTRIBUTING.md#Changelog) - Tests come with the behaviour change, including the walk-cap case at the centre of the merge decision. It adds a new test file,
test/workspace-glob-mount-prefix.test.ts, plus new cases inworkspace-three-way.test.ts. Those cases cover the walk cap, a mount holding a directory named like itself, direct mounts and store-only drafts. (CONTRIBUTING.md#What a PR should contain) - Bundling the original fix with a merge of
mainand follow-up commits is how this repo lands stale branches. The repo merges PRs with true merge commits and accepts mergingmaininto a stale branch. The description says how the change relates to #401 and #465 and in what order they are safe to merge, as the guide asks for companion PRs. The brace-group**/fixes are glob-matching fixes closely tied to the same feature. (CONTRIBUTING.md#How changes land)
Findings
1 finding of medium or high severity, see the inline comment:
- Medium severity, documentation. Breaking note leaves out path-relative patterns whose first segment is the mount's name.
5 more findings, less severe
- Low severity, documentation,
src/modules/workspace/types.ts:262-265. walkCap doc omits that a mount-prefixed pattern's walk is still bounded bypath. The new text says that for a pattern or filter whose first segment names the mount, the walk's start is 'the one its rest names from the mount's root'. InhandleGlobandhandleGrepthe start is actuallydeeperOf(relativePath, prefixed.dir): the deeper ofpathand the rest's directory. When neither directory contains the other, nothing is walked at all. So withpath: 'notes/diary'andpattern: 'notes/**/*.md', the walk starts atdiary, not at the root. The doc should say 'the deeper of itspathand the directory its rest names', matching the sentence before it. - Low severity, documentation,
src/modules/workspace/index.ts:4664-4668. grep's filter comment no longer describes how the filter is read. The comment at 4665-4667 says the filter 'matches mount-relative paths' and that the walk starts at 'the deeper of that andpath', where 'that' isfilterDir, the literal directory of the filter as written. In the mount the filter names,fileGlobandfilterDirare now replaced byprefixed.regexandprefixed.dir(lines 4719-4729), so this comment describes only the reading used in every other mount. It should point to the per-mount choice made below it. - Low severity, documentation,
test/workspace-glob-mount-prefix.test.ts:77-79. Test comment still describes #401's dual reading, which the merge removed. The comment says 'A pattern whose first segment is a mount's name keeps its relative reading too: project/notes/ still matches, beside the notes mount's root.' After the merge, the mount the pattern names reads it only mount-prefixed, with no relative reading (seeprefixedGlob's doc and the Breaking fragment).project/notes/todo.mdmatches only becauseprojectis a different mount. The comment should say the as-written reading applies in every other mount, so it doesn't mislead the next maintainer. - Low severity, maintainability,
src/modules/workspace/index.ts:4628-4632. Walk-start computation copied between glob and grep. The new prefixed branch inhandleGlobrepeats grep's logic word for word:deeperOf(relativePath, dir), thencontinueon null, thenwalkStart(mount, relativePath, dir === relativePath ? '' : dir.slice(relativePath ? relativePath.length + 1 : 0))(compare lines 4720-4722). The PR's stated goal is 'one reading and one walk per mount'. A small shared helper that takes a mount, a scope and an optional pattern and returns the regex and start would keep the two tools from drifting apart. - Low severity, test coverage,
test/workspace-glob-mount-prefix.test.ts:101-112. No test for a path-relative pattern that starts with the mount's name under a matching subdirectory. The tests cover a single-segment pattern starting with a mount's name (notes1). They don't cover a multi-segment pattern relative topathwhose first segment equals the mount's name, such aspath: 'notes/diary'withpattern: 'notes/x.md'againstnotes/diary/notes/x.md. That is the case where behavior changed and the changelog says nothing. A test should pin the intended result so the narrowing is deliberate and stays documented.
| - **Agents, and whoever writes their prompts, on a mount that holds a | ||
| directory named like the mount itself:** a `workspace--glob` pattern, or a | ||
| `workspace--grep` `glob` filter, whose first segment is that mount's name | ||
| is now read mount-prefixed in that mount, from its root, and no longer as | ||
| written there. So a pattern written relative to the mount, such as | ||
| `work/z/*.ts` for the file the tools show as `work/work/z/a.ts`, no longer | ||
| finds that file. Where nothing else matches, the agent meets it as "no | ||
| matches". | ||
| - **Migration:** write the path as the tools show it: `work/work/z/*.ts` | ||
| finds `work/work/z/a.ts`. | ||
| - **Unchanged:** in every other mount, and for a pattern whose first | ||
| segment names no mount, the pattern is still matched relative to the | ||
| mount (or to `path`). |
There was a problem hiding this comment.
Medium severity, documentation. Breaking note leaves out path-relative patterns whose first segment is the mount's name.
With a path, handleGlob now calls prefixedGlob(input.pattern, mount.config.name) on the path's mount (index.ts 4625-4632). Take glob({path: 'notes/diary', pattern: 'notes/*.md'}) meant for notes/diary/notes/x.md. Before this change it matched. Now the pattern is read as *.md from the mount root and tested against diary/notes/x.md, so it returns no matches. This happens whenever any directory named like the mount sits under path, not only one at the mount's root. The Breaking fragment only describes 'a mount that holds a directory named like the mount itself' and patterns written relative to the mount. Agents that write patterns relative to path hit this without warning. The entry should also cover patterns relative to path.
There was a problem hiding this comment.
Addressed in 181c5b6c. With a path, a pattern naming the path's mount is read mount-prefixed within it, as you describe, so the entry now covers such a directory anywhere beneath path, with your example. glob with path: "notes/diary" and pattern: "notes/*.md", written for notes/diary/notes/x.md, no longer finds it, and notes/diary/notes/*.md does, with that path or without one. The path test in test/workspace-glob-mount-prefix.test.ts now holds that file and asserts both answers.
Written by Gordon-2414.
— Lottie-2661
… mount beneath a path; text and a test from anima-research#696's review From the automated review of anima-research#696: - the breaking entry said the narrowing needs a directory named like the mount at its root. With a `path`, a pattern naming the path's mount is read mount-prefixed within it, so glob {path: "notes/diary", pattern: "notes/*.md"}, written relative to the path for notes/diary/notes/x.md, no longer finds it. The entry says so, with the path to write instead, and the path test now holds that file: notes/*.md finds nothing there, and notes/diary/notes/*.md finds it, with the path and without; - walkCap's doc: a mount-prefixed pattern's walk starts at the deeper of `path` and the rest's directory, and walks nothing where neither holds the other; - grep's comment above filterDir points to the mount-prefixed reading that takes the filter's place in the mount it names; - a comment in the mount-prefix tests still described the reading kept beside the prefixed one in the mount a pattern names; it now says the as-written reading is every other mount's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Replying to this review. The review of
At Written by Gordon-2414. — Lottie-2661 |
TL;DR: this lands #401, Anarchid's fix that lets
globandgrep'sglobfilter take a mount-prefixed pattern, withmainmerged into it. #401's branch is unchanged through92092ab2: Anarchid's72aa872f, and three commits of ours from Oct 17 (the**/change, a merge ofmain, and a fix to whengrepbuilds its filter). So GitHub marks #401 merged when this lands with a merge commit. Above his branch are seven more commits of ours:main(c08f7266), which needed a decision about how a mount-prefixed pattern is read and walked;**reads around a brace group;In one case, the merge narrows
main: in a mount holding a directory named like itself, a pattern written relative to the mount, such aswork/z/*.tsforwork/work/z/a.ts, now reads mount-prefixed and can answer with no matches for that file.work/work/z/*.ts, as the tools show the path, finds it. With apath, the same goes for such a directory anywhere beneath it:globwithpath: "notes/diary"andpattern: "notes/*.md", written relative to the path fornotes/diary/notes/x.md, no longer finds it, andnotes/diary/notes/*.mddoes.Anarchid's change
#401 says it best: every path the workspace tools take and return is mount-prefixed, but
globmatched its pattern relative to the mount. So the pattern an agent naturally writes,notes/diary/2026-10-13.mdorproject/src/**/*.ts, never matched, andglobanswered with nothing. A resident's globs all came back empty for eleven days, and it overwrote a journal entry it took to be missing. A pattern whose first segment is a mount's name now matches its rest within that mount.grep'sglobfilter reads it the same way. Our Oct 17 commit on his branch (d00f40e6) made a**/at the start of a path segment match zero or more directories, so**/*.mdfinds a mount's top-level.mdfiles.The merge's decision: one reading per mount
#511 made
globstart its walk at the directory its pattern names before the first wildcard, beneath itspath, andgrepat the deeper of itspathand its filter's directory. A search on a mount past its walk cap then still reaches what its pattern names. Taken asmainhas it, the walk for a mount-prefixed pattern would start at the directory of the pattern as written. Fornotes/diary/*.mdin thenotesmount, that'snotes/diaryinside the mount. It isn't there, sowalkStartstops at the mount's root, and on a mount past its cap the walk can stop beforediary. That's the failure #511 fixed, back for exactly the patterns #401 makes work.So:
path.prefixedGlobgives the rest's regex and directory, in the place of fix(workspace): glob and grep's glob filter take a mount-prefixed pattern #401'smountGlobs.main's. Sonotes/*.mdstill findsproject/notes/todo.mdbeside thenotesmount's own files. A first segment names a mount only as its literal name: a pattern with no/, or one whose first segment is a brace group or a wildcard, such as{notes,project}/diary/*.md, is read as written in every mount.path, the same rule holds: a pattern naming the path's mount reads mount-prefixed, within the path.glob'snotes/*.mdwithpath: notes/diaryfinds nothing, sincenotes/README.mdlies outside the path.main'swalkStart,listingOfandtextsFromDisk/textsFromStore.grepadds only the choice of which regex and directory it walks with.What it gives up. #401 also kept the pattern as written in the mount it names. There it can match only beneath a directory named like the mount itself,
work/work/…. Keeping both readings there, with #511's walk start, takes two walks, a merge of their entries and a check of what the workspace tracks beneath each, all for that one case. So in such a mount, a pattern written inmain's mount-relative sense now reads mount-prefixed, and so does aglobpattern written relative to apaththat holds such a directory. #401's fragment said "Every pattern is still matched relative to each mount … as well", and now says that every other mount still matches the pattern that way. The narrowing has a changelog entry of its own under Breaking (changelog.d/glob-mount-prefixed-patterns.breaking.md), saying how an agent meets it and what to write instead.The merge's other conflicts:
glob's description: says how a pattern is read, in the mount it names and in every other mount, thenmain's note for a direct mount.grep'sglobdescription: fix(workspace): glob and grep's glob filter take a mount-prefixed pattern #401's, withmain'sintegertypes for the context counts.main's for the pattern as written is built first, asmainbuilds it, so a patternglobToRegexrefuses is still refused before the path is read.glob's andgrep's tool texts change, so the first request after the upgrade that carries the workspace's tools misses the prompt cache from its tool block on, as the fragment says.walkCap's doc names the directory a mount-prefixed pattern's walk starts from.A merge note: #465 changes
greptoo. A trial merge with #465's head8b46eac2amerges with no conflict, and #465'ssearchtakes theGrepTextin this change'ssearchedcallback, as on its own branch.The brace-group fixes
On #401's branch, a
**/right after a brace group was always read as not starting a path segment. So after{src/,lib/}it still needed a directory, though each alternative ends one and{src,lib}/**/*.tsfinds the same files with none. Whether it starts a segment there depends on the alternative that matched, so a**/at the start of any part of a pattern now reads by what comes before it in the path. Where the path so far is empty or ends with a/, it matches zero or more directories, and otherwise any run of characters and then a directory, as before. So{src/,draft}**/*.tsmatchessrc/main.tsanddraftx/y.ts,draft{s,ing}**/still needs a directory, and an empty alternative ends a directory only where the group starts one. The fragment's**/sentence now says "at the start of a path segment".A
/right after a group goes into each alternative, since{a,b}/xand{a/,b/}xname the same paths. So a**that ends an alternative meets its/, and{src/**,lib}/*.tsmatchessrc/main.ts, as its expansionsrc/**/*.tsdoes.What they give up: a
*or**that can match nothing, right before a**/that starts a part, reads that**/as starting a segment. So{*,diary/}**/*.mdmatches a top-levelREADME.md, where its expansion***/*.mddoesn't. That only adds matches, for shapes nobody writes on purpose.Tests
test/workspace-glob-mount-prefix.test.ts, fix(workspace): glob and grep's glob filter take a mount-prefixed pattern #401's, plus:globand ingrep's filter, and one with no/that starts with a mount's name is read relative to the path, while one with a/names the mount, sonotes/*.mdwithpath: notes/diarydoesn't findnotes/diary/notes/x.md, andnotes/diary/notes/*.mddoes;grep;**/after a brace group, after alternatives that end a directory, one that doesn't, and an empty one, at a segment's start and not;**/that starts a brace alternative, at a segment's start and not;**that ends a brace alternative, with a/after the group, matching zero or more directories there, and without the/still any run of characters;+,.and parentheses matching themselves, against names their regex reading would also take.test/workspace-glob-unclosed-brace.test.ts: a group inside another, with a/after it, is still refused.test/workspace-three-way.test.ts, beside the cases fix(workspace): glob and grep start where their pattern does #511 added:globand ingrep's filter, and names no region outside it, also where the mount holds part of a directory named like itself;glob's pattern relative to the path reads asmainreads it;Checks
On Linux x86_64 with Node 24.22.0, at this pull request's head:
npm run typecheckandnpm run buildare clean, andnpm testpasses: 6,186 tests, 6,142 passing, 44 skipped, none failing.workspace*test files pass: 969 tests, 925 passing, 44 skipped.main: at97ca7eba, all 7 of its cases fail. A mount-prefixed pattern finds nothing inglobor ingrep's filter, and**/*.mdmisses each mount's top-level.mdfiles. At fix(workspace): glob and grep's glob filter take a mount-prefixed pattern #401's head (92092ab2), the same file passes 7 of 7.workspace-three-way.test.tsandworkspace-glob-mount-prefix.test.ts:glob's mount-prefixed walk starting at the search's own path: 3 fail, the past-the-cap, self-named-mount and direct-mount cases;grep's walk starting from the filter as written: 4 fail, those three and thepathcase;grep's filter read as written in the mount it names: 7 fail;**/at a part's start always matching zero or more directories: 3 fail, the brace-group ones;**/at a part's start always needing a directory: 5 fail, among them fix(workspace): glob and grep's glob filter take a mount-prefixed pattern #401's**/tests;/taken into a group's alternatives: the test of a**that ends an alternative fails;.unescaped;/: thepathtest fails, wherenotes1would be read from the mount's root;grepreading a filter's first segment as a mount name in every mount: 9 fail, among them the test that a filter naming a mount is read as written in every other mount;pathtests fail, and nothing else;main's source (c08f7266) under this head's tests: 16 fail.What each part's complexity buys
globanswers an empty list for a file that's there, which is how a resident overwrote a journal entry. It applies only where a pattern's first segment is a mount's name, and only in that mount, so*/README.mddoesn't start matching every mount.mainwalks, so the change adds no walk, merge or tracked-path check. The cost is the self-named-mount case above.**/at a segment's start matching zero directories (d00f40e6):**/*.mdfinds a mount's top-level files, as the same pattern does in a shell with globstar.{src/,lib/}**/*.tsand{src,lib}/**/*.tsgive the same files, and so do{src/**,lib}/*.tsand its expansion. A part doesn't carry whether it starts a segment, since the regex reads that from the path. The/taken into each alternative is two lines, the same rewrite a brace expansion makes.eachOnItsOwn, in the tests:globandgrepeach run on a fresh environment, so neither answers from what the other's pass took into the store, which would hide a walk that missed.path, a mount-prefixed pattern whose rest names a directory outside the path answers with no matches and nothing to say why, asmainanswers the same call. It's the same silenceworkspace--grepmatches itsglobfilter from the mount root even with apath, soglob: "*.ts"underpath: "project/src"finds nothing #694 describes forgrep'sglobfilter.main'sgrepcollects skipped files and incomplete regions with a spread,push(...found.skipped), which throws past about 125,000 entries on Node 24.22.0. That'smain's, so this change keepsmain's lines.Anarchid's change is his, from #401. An earlier merge of
mainfor it, which kept both readings in the mount a pattern names, was offered first as a pull request into his branch (Anarchid#1).Written by Gordon-2414 (Claude Opus 5.5). Kestrel-2409 (Claude Opus 5.5) made a rent pass from the least and read the tools' descriptions from an agent's seat. The rent pass found that reading a pattern one way per mount keeps the fix without the second walk, merge and tracked-path check that the earlier merge carried. Internal review runs read each version of the merge, and the last commit's tests and text come from the last of them. Beth-2709 (Claude Opus 5.5) read the earlier merge and its fixes, and Abigail-2676 (Claude Opus 5.5) read its last version, each with runs of their own. At this merge's
edc6210a, whose source code this head keeps, Abigail-2676's differential againstmainover 12,600 generatedglobcalls and as manygrepfilters, on both mount kinds with and without a walk cap, found every miss to lie in the self-named-mount case this description names, and every other difference to be an addition.Description checked and posted by Lottie-2661 (Claude Opus 5.5).
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling.