Repository navigation
fix(workspace): glob and grep start where their pattern does - #511
Merged
nissa-seru merged 1 commit intoOct 18, 2026
Merged
nissa-seru merged 1 commit into
nissa-seru merged 1 commit into
Conversation
On a mount larger than its walk cap, glob and grep walked the whole mount, or their path, and could reach the cap before the directory a pattern names, so a glob for one repository's sources in a mount of several checkouts answered with no matches. glob now starts its pass at the directory its pattern's segments name before the first wildcard, beneath its path, and grep at the deeper of its path and its glob filter's directory, since the filter matches mount-relative paths. Every path a pattern matches lies beneath that directory. The start goes down only through directories disk lists with exactly the pattern's spelling, and never through a link or into a directory the mount passes over by design, so on a case-insensitive disk a pattern spelled in another case walks from above and matches nothing, as before, rather than taking files in under its spelling. It lists only directories it has checked lie inside the mount. Within the cap the matches are the same. A pattern into a directory the mount passes over by design names that directory under incomplete, as a search given it as its path does, and a search's incomplete names only what lies beneath the directory its walk starts at. A grep whose path and filter don't overlap searches nothing. A search that starts below the root takes in, and announces, only what changed beneath its start, and no longer marks the mount's first whole sync done. The docs of walkCap and initialSyncDone say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Required label not found on this PR. |
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
…ion beside W4a's error-result line and UNBOUND_REASON main moved from 6dfea06 to d1b9d35 (anima-research#510, anima-research#438, anima-research#511, anima-research#501, anima-research#513, anima-research#508). anima-research#510 (F27) conflicts in two places, as Pippa-2571 said (room-203 #142353), and the merge takes both sides of each: - executeMcplToolCall: W4a's mcplToolFailure(result.content, serverId, call.name) for an error result, then anima-research#510's registration of a successful result's references (autoFetchReferences(..., false)). - references.ts: UNBOUND_REASON keeps W4a's value, "not tied to any server, so there's nowhere to fetch it from", under anima-research#510's comment wording ("a server that delivered the reference") with W4a's "in the agent's words". No test from main pins the old unbound or expiry texts. - mcpl-agent-words' empty-link test passes convertBlock the serverId anima-research#510 made it take.
1 task done
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
0.22.3 is cut (Ada-1017, room-203 #143802), so anima-research#468, approved for 0.23.0 (nissa, personal:oscar-1808 #141438), merges main before its train, as Ada set (#141457). main brings 54 merges since e5a54ae, three of them in the workspace: anima-research#496 (forced removal unlinks only a regular file or a link to one, and a pipe, socket, device or directory at a deleted path isn't left for applyDeletions), anima-research#500 (a mount's catch-up pass runs once its watcher is ready), and anima-research#511 (glob and grep start where their pattern does, through walkStart). The conflicts were two import lines in workspace/index.ts, and the resolution takes both sides' names: node:fs/promises gains main's readdir and keeps this change's dropping of readFile, which nothing calls; the observe.js import gains main's checkDir. git merged the rest on its own. One place main's new code meets this change without a conflict: anima-research#511's walkStart asks checkDir of the start it has reached before going a segment deeper, and checkDir now refuses a directory whose path goes through a link on a mount that doesn't follow symlinks. So walkStart goes no deeper beneath a path given through such a link. A link among the pattern's own directories it already stopped before, as on main, since it goes on only through entries readdir lists as directories. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
The include (9f3e429) is on main 2482625. Since then main has gained, in the workspace module, anima-research#460's nested mounts and editor files, anima-research#468's refusal of a link on a mount that doesn't follow links, anima-research#511's walk start for glob and grep, and anima-research#525's disk spelling. Where they meet the include: - Nested mounts (anima-research#460): a path in a nested mount's directory is that mount's, whatever the outer mount's include covers. For every tool the nested mount's refusal comes first and the include's after: refuseNotHeld, which parseFilePath and resolveAbsolutePath share, and refusesAsOutside, which defers to holderOf. A nested mount's own include decides inside its directory. Status and entriesByEvidence leave out a path outside the include beside one in another mount's directory (ownPaths). - Editor files (anima-research#460): main passes over editors' swap and backup files on every mount. The walk checks those reasons first and the include after, so a file both editor-named and uncovered counts under the editor reason, and passedOverByDesign counts OUTSIDE_INCLUDE with them. The include's walk, history and watcher tests now use names anima-research#460 doesn't pass over. - Links (anima-research#468): a link's code says no more than its message: other_mount only where the refusal names the nested mount, parent_symlink otherwise. namesHolder (observe.ts) is the one test linkWords and linkReadError use, and nestedHolder takes any { dir, name }, so linkWords keeps the nested mount's include. - glob and grep (anima-research#511): the include refuses an uncovered path before walkStart, and the walk judges each file by the include wherever it starts. - Disk spelling (anima-research#525): parsePathAsGiven is the include's reader, and a tool's path reaches diskSpelling only after it, so the step never sees an empty, `.` or `..` segment, and a path whose part after the mount's name is absolute is refused before it. The include judges a path as disk spells it, and its refusal shows the path as given, since disk's spelling of a name the include doesn't cover would tell whether a file is there. Past a nested mount's directory, an answer shows the rest of the path as given, set once in parsePath (nestedDepth). resolveAbsolutePath stays synchronous and judges the path as given. diskSpelling keeps its branches for '', '.', '..' and an absolute rest, which no tool's path reaches now: they keep it total, and its own test pins them. Texts: MountConfig.include's doc and the two fragments say how links, nested mounts and disks that fold names meet the include, and the spellings fragment names a draft stored under a key the one spelling no longer names, such as `a//b.txt`, with how to recover it from the store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 22, 2026
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>
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.
globnow starts its walk at the directory its pattern names before its first wildcard, andgrepat the one itsglobfilter names. On a workspace mount larger than its walk cap, a search for one directory's files then finds them. Before, a walk of the whole mount could reach the cap before it got there, and the search answered with no matches.Problem
One walk of a mount takes at most
walkCapentries, 5,000 by default.globwalked itspath, or each mount's root, and matched its pattern against what the walk took.grepwalked the same way and applied itsglobfilter to what it took. On a mount past the cap, the walk could stop before it reached the directory the pattern names. The search then found nothing there, and listed the directories it never reached underincomplete.A probe mounted six repositories and fourteen worktrees of agent-framework side by side, read-only, under one mount: about 6,000 files after a typical ignore list. It looked for membrane's sources with
glob(membrane/src/**/*.ts) and withgrep's filter. Onmain(c5d857b1), each found none of the 56 files on disk that match. The first four regions each listed underincompletewere:Changes
In
src/modules/workspace/index.ts:literalDirOf(pattern)gives the directory a pattern names before its first wildcard, made of its leading segments that hold none ofglobToRegex's wildcards (*,?,{), short of the last segment.walkStart(mount, base, beneath)gives where a search's walk starts. It takes the search'spathas given, then each segment of the pattern's directory while disk lists that segment, in the directory reached so far, as a directory with exactly that spelling. It stops before a segment spelled any other way, not there, or not a directory, a link included. It lists only directories it has checked lie inside the mount, and none the mount passes over by design.globstarts its walk atwalkStartof itspathand its pattern's directory.grepstarts at the deeper of itspathand its filter's directory, the same way. The filter matches paths relative to the mount, so its directory is relative to the mount too. When neither directory holds the other, no file beneath thepathcan pass the filter, andgrepsearches nothing in that mount.deeperOf(a, b)gives the deeper of two directories, ornullwhen neither holds the other.In
src/modules/workspace/types.ts, the doc ofwalkCapsays where each search's walk starts, and the doc ofinitialSyncDonesays the flag records that a pass over the whole mount has run.The changelog fragment is
changelog.d/search-starts-at-pattern-dir.fixed.md.With #401. #401 lets a pattern start with the mount's name, so
project/src/**/*.tsin mountprojectalso matchessrc/a.tsthere. The two pull requests conflict inglobandgrep. Merged by taking both sides of each conflict, #401's seven tests and this pull request's pass. A probe of that merge, beside #401 merged ontomainalone, shows two gaps (its source is under Tests):notes,notes/diary/*.mdfinds nothing, wherediary/*.mdfindsnotes/diary/x.md.project, which holdsproject/README.mdandsrc/main.ts,project/src/*.tsfinds nothing, where fix(workspace): glob and grep's glob filter take a mount-prefixed pattern #401 alone findsproject/src/main.ts.Closing both gaps needs each reading of a pattern to start its own walk.
With #468. #468 refuses a path through a symbolic link to a directory, on a mount that doesn't follow symlinks. The two conflict only in two import lines. Merged, with both sides of those lines kept, this pull request's tests pass, and so do the
workspace-*test files, 514 tests.What each part is for, and what was cut
globToRegexanchors its regex and reads these segments literally. So the walk misses no match, and the cap counts only entries beneath it.walkStartwould stop at a file anyway, so leaving the segment in fails no test. Leaving it out spareswalkStartlisting that directory, which the walk lists next anyway.l?bor{a,b}.l?b/x.tsalso matcheslib/x.ts, and a walk started atl?bwould miss it.SRCfor a directory listed assrcwould, by our reading, name everythingSRC/…, soSRC/*.tswould takesrc/a.tsin a second time, asSRC/a.ts, and announce it as created. From the deepest directory disk lists, the walk names files as disk lists them, and the case-sensitive pattern matches none, as before. A segment that's empty,.or.., which no path holds, stops it the same way.grep's deeper directory. Thepathand the filter each narrow the search, and every file both allow lies beneath the deeper of the two.**/*.ts, still walks its whole scope and can still stop at the cap, and its answer names what it didn't reach, as before.srcdirectories one level down for*/src/*.ts. That would change the walk every pass shares, for patterns that don't start with their directory.What callers will notice
incompletenames only what lies beneath the directory its walk starts at. When that's the pattern's directory, regions elsewhere in the mount, where nothing could match, are no longer named.incomplete, as a search given that directory as itspathdoes. Before, a walk of the whole mount passed it over, and named it only when it held files the store tracked, and never when it was another mount's.pathor the mount's root before.grepwhosepathand filter don't overlap searches nothing and takes nothing in from disk. Before, it walked thepath, and named underincompletewhat that walk couldn't reach.status(initialSyncDone).Tests
Ten new tests in
test/workspace-three-way.test.ts, in "a search starts at the directory its pattern names":z/deep/*.tsfindsz/deep's two files, and so dodeep/*.tswithpath: work/zand*.tswithpath: work/z/deep.**/*.tsstops inaand finds none.z/deep/*.tsfinds the match, and so do a filter deeper than thepath, apathdeeper than the filter, and a filter naming thepath's own directory.globpatterns and asgrepfilters, find what a walk of the whole mount finds, and none leaves anything underincomplete. Among them aresrc/{lib,x}/*.js,src/l?b/*,./src/*.ts,../work/src/*.ts,src//a.tsandREADME.md/*.ts.lib,l?b,a,b,{a,b}and*, the patternsl?b/x.ts,{a,b}/x.tsand*/x.tseach find every directory they match.grepwithpath: work/testfinds nothing with the filtersrc/**, nor withtesting/**, a sibling whose name starts with the path's, and takes none of the three directories in.src/a.ts,SRC/*.tsfinds nothing, takes nothing in asSRC/a.ts, and walks from the root, which takessrc/a.tsin under its own spelling. Its first two checks can fail only on a case-insensitive disk.z/nowhere/*.tswalks fromz, and namesz/deep, which that walk didn't reach, underincomplete.src/*.tstakes insrc/a.tsand announces it, and leavesdocs/b.mdto a later pass.initialSyncDonestays false through it and agrepwhose filter namessrc, until**/*.mdwalks the whole mount.vendor/lib/*.js, withvendoron the ignore list, anda/b/sub/*.js, witha/banother mount's directory, each name that directory underincomplete, asglobandgrep, with the region apathnaming it gets.link/*.ts, through a link inside the mount, andout/sub/*.ts, through one leading out of it, find nothing and take nothing in, asglobandgrep.sub/*.tswithpath: work/outnameswork/outunderincomplete, as before.At
b3026f64, onmainc5d857b1, on Linux with Node 24.22.0:tsc --noEmitand the build are clean.The ten new tests pass, and so do the eleven
workspace-*test files, 462 tests in all.Red-first: with
main'ssrc/in place, six of the ten fail. Four pass there too, as controls that pin what doesn't change: the tests within the cap, of wildcard names, of linked directories, and of another spelling, which on Linux passes either way.Removals: sixteen variants of the change, each run alone, fail at least one of the ten:
globorgrepstarting at itspath,grepstarting at the deeper directory withoutwalkStart, andgrepwalking itspathwhen it doesn't overlap the filter;walkStarttaking the pattern's directory unchecked, starting at the mount's root when the pattern has no directory, listing a directory it hasn't checked, going down through a link, or going down into a directory passed over by design;literalDirOfreading*,?or{as literal;deeperOfalways giving the filter's directory, or always thepath, dropping its equal-directory case, or matching a prefix without its/.A seventeenth,
literalDirOfkeeping the last segment, fails none, as "What each part is for" says.The probe above, at this head:
globfound 56 files andgrep's filter 56, the 56 on disk, with nothing underincomplete. Its source is below. Its agent-framework and fourteen worktrees are at3adbdb86, and the other five repositories are clones of their default branch, taken for each run. The run at this head counted 6,011 files, and the run onmain, minutes later, 6,015.The probe (run with
npx tsxfrom the repository's root, with dependencies installed)The probe of #401 merged with this pull request
Not verified
Found, not changed here
pathspelled in another case.parsePathtakes apath's spelling as it stands. On a case-insensitive disk, by our reading of the code, apathspelled in another case reaches the directory disk lists under its own spelling, and what a tool finds or writes there is named, and stored, under the path's spelling. We haven't run a case-insensitive disk.glob,grep,ls,readandwriteall take theirpaththis way, so it needs one fix where paths are parsed, not one in each search.changelog.d/search-starts-at-pattern-dir.fixed.mdWritten by Beth-2709 (Claude Opus 5.5), on Ada-1017's calls, from Gladys-2592's design. Phoebe-2419 found the defect, measuring the workspace at breadth. Esther-2584 (Claude Opus 5.5) reviewed the change, and two rounds of internal review read it before it opened.
Description checked and posted by Fiona-2757 (Claude Opus 5.5).
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's Pi tooling, and posted from the shared
nissa-seruaccount.