Skip to content

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

Open
sahrizvi wants to merge 1 commit into
mainfrom
fix/skill-autoload-glob-early-exit
Open

fix(skills): stop walking the whole tree to answer "does any file match"#1213
sahrizvi wants to merge 1 commit into
mainfrom
fix/skill-autoload-glob-early-exit

Conversation

@sahrizvi

@sahrizvi sahrizvi commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

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 starts Instance.worktree scan cost
inside a git repo the 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:

implementation runs (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

Replaces Glob.scan(...).length > 0 in skill auto-load with a new Glob.exists that stops walking at the first match, cutting startup outside a git repo from ~51s to ~7s; startup inside a repo is unchanged.

  • Adds Glob.exists to packages/core using globIterate, which yields lazily and abandons the walk at the first hit.
  • Switches the applyPaths check in packages/opencode to the new call; two builtin skills (dbt-develop, dbt-schema-verify) run it every session.
  • Passing ignore: Glob.DEFAULT_IGNORE was tried and rejected — it made startup slower (61–82s) because paths outside a git repo rarely get pruned.
  • Known limitation: outside a git repo, worktree is /, so skills can still match unrelated files elsewhere on the machine; that's left for whoever owns project identity.
  • Adds four tests covering agreement with scan, include/ignore handling, and a missing directory.

Written for commit c7e9371. Summary will update on new commits.

Review in cubic

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.

`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 once the entire walk has
finished, so every skill paid for the full 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, with no configuration and no workspace involved.

The cost depends entirely on what the worktree resolves to. Inside a git
repo it is the repo root and the scans take ~10ms. Outside one,
`Project.fromDirectory` returns the global project whose worktree is `/`,
and the two scans walk the entire filesystem.

Measured from a directory outside a git repo, same binary, same prompt:

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

Inside a repo: 7.5 / 7.3s, unchanged.

`Glob.exists` uses `globIterate`, which yields lazily, so the walk is
abandoned at the first match. It takes the same options as `scan` — the
tests pin `include`, `ignore` and the missing-directory case, and fail if
the options stop being forwarded.

Also tried and rejected: passing `ignore: Glob.DEFAULT_IGNORE` here, the
way #1184 did for the MCP scans. It made this *slower* — 61-82s against a
~51s baseline — because outside a repo the tree is not dependency-heavy,
so every candidate path pays 12 minimatch tests and almost nothing gets
pruned. Early exit is the right lever for an existence check.

Not addressed here: the worktree being `/` outside a git repo. That makes
skills auto-load off unrelated files elsewhere on the machine, which is a
correctness question for whoever owns project identity.

core suite: 1072 pass, 26 fail — the same 26 fail on unmodified main.
session/skill suites: 1516 pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fJ3X7pcGT4R9yzjsJnqsV

@claude claude 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.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@github-actions

Copy link
Copy Markdown

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions

Copy link
Copy Markdown

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0eb5d1ef-eccf-4124-8d16-d76ba65cbdbe

📥 Commits

Reviewing files that changed from the base of the PR and between 41e98f6 and c7e9371.

📒 Files selected for processing (3)
  • packages/core/src/util/glob.ts
  • packages/core/test/util/glob.test.ts
  • packages/opencode/src/session/system.ts

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

The glob utility adds a lazy exists API that stops after the first match. Tests cover matching options and pruning. Worktree skill matching now uses this API instead of collecting all matches.

Changes

Glob existence checks

Layer / File(s) Summary
Add and validate Glob.exists
packages/core/src/util/glob.ts, packages/core/test/util/glob.test.ts
Glob.exists uses globIterate and preserves translated options. Tests cover matches, filters, ignored paths, and missing directories.
Use lazy matching in SystemPrompt
packages/opencode/src/session/system.ts
anyMatchInWorktree uses Glob.exists with the existing matching options and error handling.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to c7e93

This PR changes file-existence checks to stop after the first match, reducing startup time without changing the matching scope or behavior. No actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant SystemPrompt
  participant GlobExists
  participant globIterate
  SystemPrompt->>GlobExists: Check applyPaths pattern
  GlobExists->>globIterate: Traverse with matching options
  globIterate-->>GlobExists: Return on first match
  GlobExists-->>SystemPrompt: Return boolean
Loading

Suggested reviewers: anandgupta42

Poem

A rabbit found a path in the hay
Glob.exists stopped without delay
It pruned the brush and checked the trail
One match was enough to tell the tale
The worktree hopped along more lightly

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main performance fix: replacing full-tree matching with an early-exit existence check.
Description check ✅ Passed The description explains the issue, implementation, performance impact, verification results, tests, limitations, and checklist status. It omits the template's Issue section and issue number, but the …
Full details: Description check

Explanation

The description explains the issue, implementation, performance impact, verification results, tests, limitations, and checklist status. It omits the template's Issue section and issue number, but the required change information is otherwise complete.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/skill-autoload-glob-early-exit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@kilo-code-bot

kilo-code-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (3 files)
  • packages/core/src/util/glob.ts
  • packages/core/test/util/glob.test.ts
  • packages/opencode/src/session/system.ts

Reviewed by deepseek-v4-pro · Input: 34K · Output: 15.2K · Cached: 334.1K

Review guidance: REVIEW.md from base branch main

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 3 files

Re-trigger cubic

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant