Skip to content

fix(workspace): glob and grep take a mount-prefixed pattern (#401, with main merged) - #696

Merged
nissa-seru merged 11 commits into
anima-research:mainfrom
nissa-seru:gordon/af401-house
Oct 22, 2026
Merged

nissa-seru merged 11 commits into
anima-research:mainfrom
nissa-seru:gordon/af401-house

Conversation

@nissa-seru

@nissa-seru nissa-seru commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR: this lands #401, Anarchid's fix that lets glob and grep's glob filter take a mount-prefixed pattern, with main merged into it. #401's branch is unchanged through 92092ab2: Anarchid's 72aa872f, and three commits of ours from Oct 17 (the **/ change, a merge of main, and a fix to when grep builds its filter). So GitHub marks #401 merged when this lands with a merge commit. Above his branch are seven more commits of ours:

  • a merge of main (c08f7266), which needed a decision about how a mount-prefixed pattern is read and walked;
  • two fixes to how a ** reads around a brace group;
  • tests and text from an internal review of the three;
  • a changelog entry of its own for the narrowing below, with a wording fix to it, and tool texts that say how every other mount reads a pattern;
  • text and a test from this pull request's automated review.

In one case, the merge narrows main: in a mount holding a directory named like itself, a pattern written relative to the mount, such as work/z/*.ts for work/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 a path, the same goes for such a directory anywhere beneath it: glob with path: "notes/diary" and pattern: "notes/*.md", written relative to the path for notes/diary/notes/x.md, no longer finds it, and notes/diary/notes/*.md does.

Anarchid's change

#401 says it best: every path the workspace tools take and return is mount-prefixed, but glob matched its pattern relative to the mount. So the pattern an agent naturally writes, notes/diary/2026-10-13.md or project/src/**/*.ts, never matched, and glob answered 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's glob filter 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 **/*.md finds a mount's top-level .md files.

The merge's decision: one reading per mount

#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. A search on a mount past its walk cap then still reaches what its pattern names. Taken as main has it, the walk for a mount-prefixed pattern would start at the directory of the pattern as written. For notes/diary/*.md in the notes mount, that's notes/diary inside the mount. It isn't there, so walkStart stops at the mount's root, and on a mount past its cap the walk can stop before diary. That's the failure #511 fixed, back for exactly the patterns #401 makes work.

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. Its walk starts at the literal directory of the rest of the pattern, from the mount's root and within path. prefixedGlob gives the rest's regex and directory, in the place of fix(workspace): glob and grep's glob filter take a mount-prefixed pattern #401's 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. So notes/*.md still finds project/notes/todo.md beside the notes mount'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.
  • With a path, the same rule holds: a pattern naming the path's mount reads mount-prefixed, within the path. glob's notes/*.md with path: notes/diary finds nothing, since notes/README.md lies outside the path.
  • Each mount has one reading and one walk, through main's walkStart, listingOf and textsFromDisk/textsFromStore. grep adds 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 in main's mount-relative sense now reads mount-prefixed, and so does a glob pattern written relative to a path that 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, then main's note for a direct mount.
  • grep's glob description: fix(workspace): glob and grep's glob filter take a mount-prefixed pattern #401's, with main's integer types for the context counts.
  • The regex: main's for the pattern as written is built first, as main builds it, so a pattern globToRegex refuses is still refused before the path is read.

glob's and grep'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 grep too. A trial merge with #465's head 8b46eac2a merges with no conflict, and #465's search takes the GrepText in this change's searched callback, 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}/**/*.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. 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}**/*.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 fragment's **/ sentence now says "at the start of a path segment".

A / right after a group goes into each alternative, since {a,b}/x and {a/,b/}x name the same paths. So a ** that ends an alternative meets its /, and {src/**,lib}/*.ts matches src/main.ts, as its expansion src/**/*.ts does.

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/}**/*.md matches a top-level README.md, where its expansion ***/*.md doesn'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:
    • with a path, a pattern naming the path's mount reads mount-prefixed, within the path, in glob and in grep'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, so notes/*.md with path: notes/diary doesn't find notes/diary/notes/x.md, and notes/diary/notes/*.md does;
    • a filter naming a mount, read as written in every other mount, in grep;
    • a first segment that is a brace group or a wildcard names no mount;
    • a **/ 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;
    • a **/ that starts a brace alternative, at a segment's start and not;
    • a ** that ends a brace alternative, with a / after the group, matching zero or more directories there, and without the / still any run of characters;
    • a pattern's +, . and parentheses matching themselves, against names their regex reading would also take.
  • In test/workspace-glob-unclosed-brace.test.ts: a group inside another, with a / after it, is still refused.
  • In test/workspace-three-way.test.ts, beside the cases fix(workspace): glob and grep start where their pattern does #511 added:
    • 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 inside that directory, glob's pattern relative to the path reads as main reads it;
    • 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.

Checks

On Linux x86_64 with Node 24.22.0, at this pull request's head:

  • npm run typecheck and npm run build are clean, and npm test passes: 6,186 tests, 6,142 passing, 44 skipped, none failing.
  • The workspace* test files pass: 969 tests, 925 passing, 44 skipped.
  • fix(workspace): glob and grep's glob filter take a mount-prefixed pattern #401's own file at main: at 97ca7eba, all 7 of its cases fail. A mount-prefixed pattern finds nothing in glob or in grep's filter, and **/*.md misses each mount's top-level .md files. 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.
  • Each part's test fails without it. Each of these is one change to this pull request's source, run over workspace-three-way.test.ts and workspace-glob-mount-prefix.test.ts:
    • no mount-prefixed reading: 16 fail, fix(workspace): glob and grep's glob filter take a mount-prefixed pattern #401's cases among them;
    • 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 the path case;
    • grep's filter read as written in the mount it names: 7 fail;
    • a **/ at a part's start always matching zero or more directories: 3 fail, the brace-group ones;
    • a **/ 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;
    • no / taken into a group's alternatives: the test of a ** that ends an alternative fails;
    • no escaping: the escaping test fails, and so does leaving only . unescaped;
    • no guard for a pattern with no /: the path test fails, where notes1 would be read from the mount's root;
    • a first segment matched as a glob against the mount's name: 7 fail, among them the brace-group-and-wildcard test;
    • grep reading 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;
    • the earlier merge's source, which kept both readings in the mount a pattern names, under this head's tests: the self-named-mount, direct-mount and path tests fail, and nothing else;
    • main's source (c08f7266) under this head's tests: 16 fail.

What each part's complexity buys

  • The mount-prefixed reading (fix(workspace): glob and grep's glob filter take a mount-prefixed pattern #401): the pattern an agent writes from the paths the tools hand it finds what it names. Without it, glob answers 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.md doesn't start matching every mount.
  • One reading per mount: each mount walks once, as main walks, so the change adds no walk, merge or tracked-path check. The cost is the self-named-mount case above.
  • A **/ at a segment's start matching zero directories (d00f40e6): **/*.md finds a mount's top-level files, as the same pattern does in a shell with globstar.
  • The brace-group fixes: {src/,lib/}**/*.ts and {src,lib}/**/*.ts give the same files, and so do {src/**,lib}/*.ts and 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: glob and grep each 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.
  • Left out:

Anarchid's change is his, from #401. An earlier merge of main for 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 against main over 12,600 generated glob calls and as many grep filters, 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.

Anarchid and others added 10 commits October 16, 2026 14:31
…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>

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 glob and grep tools already return mount-prefixed paths. Before this change they matched patterns only relative to each mount, so a natural pattern like notes/diary/*.md came 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. prefixedGlob is added next to globToRegex and literalDirOf. Both tools still go through the existing walkStart, listingOf and textsFromDisk/textsFromStore, and only the choice of regex and starting directory changes. The walk-start code copied between glob and grep is 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.md and .fixed.md follow the <slug>.<category>.md format. 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 in workspace-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 main and follow-up commits is how this repo lands stale branches. The repo merges PRs with true merge commits and accepts merging main into 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:

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 by path. 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'. In handleGlob and handleGrep the start is actually deeperOf(relativePath, prefixed.dir): the deeper of path and the rest's directory. When neither directory contains the other, nothing is walked at all. So with path: 'notes/diary' and pattern: 'notes/**/*.md', the walk starts at diary, not at the root. The doc should say 'the deeper of its path and 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 and path', where 'that' is filterDir, the literal directory of the filter as written. In the mount the filter names, fileGlob and filterDir are now replaced by prefixed.regex and prefixed.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 (see prefixedGlob's doc and the Breaking fragment). project/notes/todo.md matches only because project is 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 in handleGlob repeats grep's logic word for word: deeperOf(relativePath, dir), then continue on null, then walkStart(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 to path whose first segment equals the mount's name, such as path: 'notes/diary' with pattern: 'notes/x.md' against notes/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.

Comment on lines +1 to +13
- **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`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Replying to this review.

The review of 8eccdf9 is answered at 181c5b6c: the medium in its own thread, four of the lows fixed, and the fifth left, for the reason below.

  • walkCap's doc: fixed. It now says either tool starts at the deeper of its path and the directory the rest names, and walks nothing where neither holds the other.
  • grep's comment above filterDir: fixed. It now points to the mount-prefixed reading that takes the filter's place in the mount it names.
  • The test comment describing fix(workspace): glob and grep's glob filter take a mount-prefixed pattern #401's dual reading: fixed. It now says the as-written reading is every other mount's.
  • A test for a pattern relative to path starting with the mount's name: added, in test/workspace-glob-mount-prefix.test.ts's path test.
  • The walk-start computation in both handlers: left as it is. It's three lines that main's grep already has, and each reads plainly in its own handler. The two handlers also differ in what the pattern as written is matched against: glob's pattern is relative to path, and grep's filter is matched from the mount root, as workspace--grep matches its glob filter from the mount root even with a path, so glob: "*.ts" under path: "project/src" finds nothing #694 describes. A helper for the shared lines would have to carry that difference too.

At 181c5b6c, on Linux with Node 24.22.0, npm test passes: 6,186 tests, 6,142 passing, 44 skipped, none failing. Under the earlier merge's source, which kept both readings, the new path assertions fail along with the self-named-mount and direct-mount tests.

Written by Gordon-2414.

— Lottie-2661

@nissa-seru
nissa-seru merged commit 8e0bb04 into anima-research:main Oct 22, 2026
12 checks passed
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