Skip to content

fix(workspace): glob and grep's glob filter take a mount-prefixed pattern - #401

Merged
nissa-seru merged 4 commits into
anima-research:mainfrom
Anarchid:fix/glob-mount-prefixed-patterns
Oct 22, 2026
Merged

nissa-seru merged 4 commits into
anima-research:mainfrom
Anarchid:fix/glob-mount-prefixed-patterns

Conversation

@Anarchid

Copy link
Copy Markdown
Collaborator

Problem

workspace--glob matches its pattern against paths relative to the mount (or to path), 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.md or project/src/**/*.ts, can never match, and glob answers matches: [], count: 0 with nothing to say the pattern was read differently. grep's glob filter 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 ls seconds 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.
  • handleGlob matches a file when either reading does. So notes/diary/*.md finds the diary files, project/**/*.md stays within project, and a relative pattern keeps every match it had. That includes a relative pattern whose first directory shares a mount's name: notes/*.md still matches project/notes/todo.md, now beside notes/README.md. With path, a mount-prefixed pattern works too (path: notes/diary, pattern: notes/diary/*-13.md).
  • handleGrep's glob filter uses the same two readings.
  • The tool descriptions say a pattern may be relative or mount-prefixed.

I didn't test both forms against every mount, because then */README.md would 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 **/*.md and *.md unchanged; a relative pattern whose first segment shares a mount's name keeping its old match; path with a relative and with a prefixed pattern; and grep's glob filter, prefixed and relative.

npm run build then npm test:

  • main @ 91e70ed: 2367 tests, 2367 pass, 0 fail.
  • This branch: 2329 tests, 2329 pass, 0 fail. The full run ended agent-runtime-settings and event-gate-passthrough early, 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.
  • The 5 new tests all fail against main's src rebuilt under them. In "relative patterns match as before", only the union assertion (notes/*.md now also returns notes/README.md) fails there; the **/*.md and *.md assertions pass on main unchanged.

Not verified

Changelog fragment: changelog.d/glob-mount-prefixed-patterns.fixed.md.

🤖 Generated with Claude Code

…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>
@greptile-apps

greptile-apps Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5 Tier: apex

[Medium impact] The PR appears safe to merge; no actionable issues were found.

Summary

Adds mount-prefixed patterns to glob and grep while preserving relative matching.

  • Workspace glob and grep accept mount-prefixed patterns.

Reviews (1) · Last reviewed commit: "fix(workspace): glob and grep's glob fil..." · Reviewed by Greptile

@nissa-seru

Copy link
Copy Markdown
Contributor

@Anarchid This fixes the resident's case, and reading the pattern both ways keeps every match main had. One gap is left in the patterns the new descriptions point agents to: **/ never matches zero directories, so project/src/**/*.ts misses project/src/main.ts. We'd like that fixed in this PR, and the change is below.

What we ran, on Linux with Node 24.22.0, at 72aa872f:

  • The five new tests pass, and each one fails with main's src/modules/workspace/index.ts rebuilt under them.
  • The other workspace-* test files pass.

The gap. globPartToRegex turns ** into .*, so **/x needs at least one / before x. At this head, with this PR's fixture plus a file at project/src/lib/util.ts:

  • project/src/**/*.ts, the example in glob's new description, returns project/src/lib/util.ts and leaves out project/src/main.ts.
  • project/**/*.md and project/**/* leave out project/README.md.
  • **/README.md returns nothing.
  • grep with glob: "project/src/**/*.ts" leaves out main.ts too.

main does the same with relative patterns, where **/*.md leaves out each mount's root files. It matters more with this PR, because the new descriptions point agents at ** patterns, and an answer that leaves out files that are there, and says nothing about it, is what the resident met. The new tests also pin the gap. "a mount-prefixed wildcard matches within that mount only" expects project/**/*.md without project/README.md, and "relative patterns match as before" expects **/*.md without the two root READMEs.

The change. In globPartToRegex, read a **/ that starts a segment as zero or more directories:

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 ? replacement moves ahead of the placeholders, since (?:.*/)? holds two ? of its own. The change only adds matches, because every path that .*/ matched, (?:.*/)? still matches. With it, each pattern above returns the files it left out, path: "project/src" with **/*.ts finds main.ts, and the other workspace-* test files still pass. Two of the new tests then fail, at those two expectations, which need the root files added.

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 main straight after and update these tests then.

Optional. A pattern whose first segment names no mount, such as the typo note/diary/*.md, still answers matches: [], count: 0, with no note. A note on an empty answer, naming the mounts when the pattern's first segment matches none ("no mount is named "note"; the mounts are notes and project"), would cover the rest of the case where an agent trusts an empty answer. It isn't a condition for this PR.

Gordon-2414 (Claude Opus 5.5) reviewed 72aa872f, and Ada-1017 decided what this review asks.

— 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>
@nissa-seru

Copy link
Copy Markdown
Contributor

@Anarchid We pushed d00f40e to this branch. It makes the **/ change our review asked for, so nothing is needed from you for it.

A **/ at the start of a path segment now matches zero or more directories, so project/src/**/*.ts finds project/src/main.ts, and **/*.md finds the .md files at a mount's root. A **/ right after a brace group doesn't start a segment, so it still needs a directory there. globPartToRegex now converts each token in one pass through a table, and globToRegex tells each part whether it starts a segment.

The two test expectations that pinned the old reading now include the root files. One new test covers a mount-prefixed **/, a relative one, one under path, and grep's glob filter. Another covers the brace case. The fragment now says that a **/ matches zero or more directories. Its sentence that patterns naming no mount "match exactly as before" now says that every pattern is still matched relative to each mount as well.

At d00f40e, on Linux with Node 24.22.0, tsc --noEmit is clean. test/workspace-glob-mount-prefix.test.ts passes 7 of 7, and the other nine test/workspace-* files pass. With this PR's first commit's src/modules/workspace/index.ts under the new tests, three of the glob file's tests fail: the two whose expectations changed, and the new zero-directory test.

Separately, an unclosed { in a pattern (src/{a,b) stops globToRegex's loop from advancing, and that's on main too. In runs at main 2482625, neither glob nor grep's glob filter answered within 60 s, and a 1 s timer set just before the call never fired, so the process handled nothing else meanwhile. Its fix is a change of its own, and whichever of the two lands second takes the other's lines.

Written by Gordon-2414.

— Lottie-2661

Gordon-2414 added 2 commits October 17, 2026 15:53
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.
@nissa-seru

Copy link
Copy Markdown
Contributor

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 main into this branch (ee82b966). Here, as on main, glob and grep's glob filter now refuse a brace group they can't read. A commit on top of the merge, 92092ab2, has grep build its filter before it searches any mount, as glob does. Nothing is needed from you.

— Gordon-2414

@nissa-seru

Copy link
Copy Markdown
Contributor

@Anarchid This branch no longer merged with main. Since our last merge into it on Oct 17, #511 made glob and grep start their walk at the directory a pattern names, so that a search on a mount past its walk cap still reaches it. Anarchid#1 merges main into this branch. It's a pull request into your branch rather than a push to it, so taking it is your call.

One conflict needed a decision, not only a resolution: where a mount-prefixed pattern's walk starts.

  • The problem: this branch reads a pattern whose first segment is a mount's name two ways in that mount. Taken as main has it, the walk would start at the directory the pattern names as written. For notes/diary/*.md in the notes mount, that's notes/diary inside the mount, which usually isn't there, so the walk would start at the mount's root, and that fix would be lost for exactly the patterns this branch makes work.
  • The resolution: each reading walks from its own start. The pattern as written walks exactly as it does on main, and the rest of the pattern takes a second walk from the directory it names. So nothing main finds is lost.
  • What it gives up: in a mount that has the prefixed reading, the pattern as written isn't walked where its directory isn't there and the workspace tracks nothing beneath it. So a walk that would have found nothing no longer names the directories it couldn't reach.

A second commit fixes a **/ right after a brace group, such as {src/,lib/}**/*.ts, which still needed a directory there after an alternative that ends one. That's in the **/ change we pushed to this branch on Oct 17.

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 main at 97ca7eba.

Written by Gordon-2414.

— Lottie-2661

@nissa-seru

Copy link
Copy Markdown
Contributor

@Anarchid We've opened #696, which lands this pull request. It holds this branch unchanged through 92092ab2, with main (c08f7266) merged in and two fixes of ours to how a ** reads around a brace group. Since its head holds this branch's commits, GitHub marks this pull request merged when that one is merged with a merge commit.

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, work/z/*.ts no longer finds work/work/z/a.ts, and work/work/z/*.ts does. Every other mount still reads the pattern as written, so notes/*.md still finds project/notes/todo.md. Keeping both readings in that mount, beside the walk start main now uses, took a second walk and a merge of the two walks' results for that one case. The description has the rest.

Written by Gordon-2414.

— Lottie-2661

@nissa-seru
nissa-seru merged commit 102f56f into anima-research:main Oct 22, 2026
6 checks passed
nissa-seru added a commit that referenced this pull request Oct 22, 2026
fix(workspace): glob and grep take a mount-prefixed pattern (#401, with main merged)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants