fix(skills): stop walking the whole tree to answer "does any file match"

Merged
SA
2mo ago
+125-13

Type of change

  • Bug fix (performance)
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Skill applyPaths auto-load asks one question per skill — does at least one file in the worktree match this glob — and answered it with Glob.scan(...).length > 0. scan resolves only after the entire walk completes, so each skill paid for the whole tree even when the first directory already answered.

Two builtin skills ship with applyPaths (dbt-develop, dbt-schema-verify), so every session pays this twice, before the first token — no configuration, no custom skills, no workspace required.

How much it costs depends on what the worktree resolves to:

where the session startsInstance.worktreescan cost
inside a git repothe repo root~10ms
outside any git repo/ — Project.fromDirectory returns the global project (project.ts:293)walks the whole filesystem

Glob.exists uses globIterate, which yields lazily, so the walk is abandoned at the first match.

How did you verify your code works?

Same binary, same prompt, same account — only the implementation differs. Run from a directory outside a git repo:

implementationruns (s)
scan(...).length > 0 (current)48.4, 52.9, 51.6
Glob.exists (this PR)9.1, 6.8, 6.4
scan removed entirely (floor)7.1, 6.8, 6.7

Inside a repo: 7.5, 7.3s — unchanged. The fix lands within ~1s of the floor, so the remaining startup is not this scan.

Traces confirm where the time went: a session with 73.3s of startup returned its first generation in 0.08s. The model was never the bottleneck.

Tests: four cases on Glob.exists pinning agreement with scan, include: "file" behaviour, ignore pruning, and a missing directory. Mutation-tested — dropping the options forwarding fails two of them.

  • packages/core: 1072 pass, 26 fail — the same 26 fail on unmodified main (verified by stashing this change and re-running).
  • test/session + test/skill: 1516 pass, 0 fail.
  • bun typecheck: 13 tasks clean. Marker Guard: passes. No new Prettier violations.

Rejected alternative

Passing ignore: Glob.DEFAULT_IGNORE here, mirroring what #1184 did for the MCP scans, makes it slower: 61–82s against a ~51s baseline. Outside a repo the tree is not dependency-dominated, so every candidate path pays 12 minimatch tests while almost nothing gets pruned. Early exit is the right lever for an existence check; pruning is the right lever when the tree really is mostly node_modules.

Likely root cause

Two independent things combine; neither is a problem alone.

1. worktree is / outside a git repo — present since the initial import. Project.fromDirectory walks up looking for .git; finding none, it returns the global project with worktree and sandbox hardcoded to / (project.ts:293). That line is original to the repository's first commit (f2cd5c124, 2026-03-01) and carries no altimate_change markers, so it is inherited upstream code rather than something added here. As a sentinel meaning "no project", it was harmless — nothing walked it.

2. A per-skill worktree scan — added 2026-05-29. anyMatchInWorktree (#849, a490bd45e) walks Instance.worktree once per applyPaths skill, and the same change shipped two builtin skills carrying applyPaths. Inside a repo that root is bounded and each scan costs ~10ms, which is what the code was written against — the comment on the function reasons explicitly about catching dbt_project.yml "no matter how deep the user's cwd is". Outside a repo, the same call inherits /.

So the combination has existed since 2026-05-29, and what determines whether anyone feels it is simply where the CLI is launched from: inside a git repo it is invisible, outside one it costs ~45s per session.

No evidence of a recent regression. The function is byte-identical since it landed, the two builtin skills gained applyPaths in that same commit, and worktree resolution has not changed since the import. Reports clustering recently are more consistent with where sessions are being started than with any code change — a same-machine, same-account A/B differed by 51s vs 8s on working directory alone.

This PR fixes the second half, which is the part that turns an inert sentinel into a filesystem walk. The first half is noted below.

Not addressed here

Project.fromDirectory hardcodes worktree: "/" when no .git is found. Beyond performance, that means skills auto-load based on unrelated files elsewhere on the machine — with this PR applied, the scan returns matched=true in 11ms at root=/ because some unrelated dbt_project.yml exists on disk. That is a correctness question for whoever owns project identity, and deliberately left out of this change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

🤖 Generated with Claude Code

https://claude.ai/code/session_018fJ3X7pcGT4R9yzjsJnqsV


Summary by cubic

Fixes skill auto-load so applyPaths globs stop scanning the whole tree to answer "does any file match" and stop matching against the whole filesystem outside a git repo.

  • Adds Glob.exists to packages/core, which abandons the walk at the first match; startup outside a git repo drops from ~51s to ~7s, and startup inside a repo is unchanged.
  • Falls back to the session directory when Instance.worktree is / and there is no VCS — the no-project sentinel — so an empty non-git directory no longer auto-loads the dbt skills from an unrelated dbt_project.yml elsewhere on the machine; a git repo genuinely rooted at / keeps scanning from its root.
  • Rejected ignore: Glob.DEFAULT_IGNORE; it was slower (61–82s) because paths outside a git repo rarely get pruned.
  • Tests cover Glob.exists agreement with scan, include/ignore handling, missing directories, and the autoLoadScanRoot sentinel and VCS distinction.

Summary by CodeRabbit

  • New Features

    • Added a fast way to check whether files match a glob pattern.
    • File matching now supports efficient exclusions and stops once a match is found.
  • Performance

    • Improved worktree checks by avoiding unnecessary full-directory scans.
  • Bug Fixes

    • Glob-based checks now consistently honor inclusion and exclusion options.
    • Scans now use the session directory when no project worktree is available.
    • Scans correctly preserve the filesystem root when a Git repository is genuinely rooted there.