Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Gauthier Guinet <43207538+Gguinet@users.noreply.github.com>
Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai>
Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com>
Co-authored-by: Quentin <torroba.q@gmail.com>
Co-authored-by: Simon <80467011+sorgfresser@users.noreply.github.com>
Co-authored-by: Simon Van de Kerckhove <simon.vandekerckhove@mistral.ai>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: angelapopopo <angele.lenglemetz@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-03-23 18:45:21 +01:00 committed by GitHub
parent 5103019b01
commit eb580209d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
180 changed files with 11136 additions and 1030 deletions

View file

@ -1,6 +1,10 @@
from __future__ import annotations
from vibe.core.paths._local_config_walk import walk_local_config_dirs_all
from vibe.core.paths._local_config_walk import (
WALK_MAX_DEPTH,
has_config_dirs_nearby,
walk_local_config_dirs_all,
)
from vibe.core.paths._vibe_home import (
DEFAULT_TOOL_DIR,
GLOBAL_ENV_FILE,
@ -26,6 +30,8 @@ __all__ = [
"SESSION_LOG_DIR",
"TRUSTED_FOLDERS_FILE",
"VIBE_HOME",
"WALK_MAX_DEPTH",
"GlobalPath",
"has_config_dirs_nearby",
"walk_local_config_dirs_all",
]

View file

@ -1,11 +1,15 @@
from __future__ import annotations
from collections import deque
from functools import cache
import logging
import os
from pathlib import Path
from vibe.core.autocompletion.file_indexer.ignore_rules import WALK_SKIP_DIR_NAMES
logger = logging.getLogger("vibe")
_VIBE_DIR = ".vibe"
_TOOLS_SUBDIR = Path(_VIBE_DIR) / "tools"
_VIBE_SKILLS_SUBDIR = Path(_VIBE_DIR) / "skills"
@ -13,27 +17,117 @@ _AGENTS_SUBDIR = Path(_VIBE_DIR) / "agents"
_AGENTS_DIR = ".agents"
_AGENTS_SKILLS_SUBDIR = Path(_AGENTS_DIR) / "skills"
WALK_MAX_DEPTH = 4
_MAX_DIRS = 2000
def _collect_config_dirs_at(
path: Path,
entries: set[str],
tools: list[Path],
skills: list[Path],
agents: list[Path],
) -> None:
"""Check a single directory for .vibe/ and .agents/ config subdirs."""
if _VIBE_DIR in entries:
if (candidate := path / _TOOLS_SUBDIR).is_dir():
tools.append(candidate)
if (candidate := path / _VIBE_SKILLS_SUBDIR).is_dir():
skills.append(candidate)
if (candidate := path / _AGENTS_SUBDIR).is_dir():
agents.append(candidate)
if _AGENTS_DIR in entries:
if (candidate := path / _AGENTS_SKILLS_SUBDIR).is_dir():
skills.append(candidate)
def _iter_child_dirs(path: Path, entries: set[str]) -> list[Path]:
"""Return sorted child directories to descend into, skipping ignored and dot-dirs."""
children: list[Path] = []
for name in sorted(entries):
if name in WALK_SKIP_DIR_NAMES or name.startswith("."):
continue
child = path / name
try:
if child.is_dir():
children.append(child)
except OSError:
continue
return children
@cache
def walk_local_config_dirs_all(
root: Path,
) -> tuple[tuple[Path, ...], tuple[Path, ...], tuple[Path, ...]]:
"""Discover .vibe/ and .agents/ config directories under *root*.
Uses breadth-first search bounded by ``WALK_MAX_DEPTH`` and ``_MAX_DIRS``
to avoid unbounded traversal in large repositories.
"""
tools_dirs: list[Path] = []
skills_dirs: list[Path] = []
agents_dirs: list[Path] = []
resolved_root = root.resolve()
for dirpath, dirnames, _ in os.walk(resolved_root, topdown=True):
dir_set = frozenset(dirnames)
path = Path(dirpath)
if _VIBE_DIR in dir_set:
if (candidate := path / _TOOLS_SUBDIR).is_dir():
tools_dirs.append(candidate)
if (candidate := path / _VIBE_SKILLS_SUBDIR).is_dir():
skills_dirs.append(candidate)
if (candidate := path / _AGENTS_SUBDIR).is_dir():
agents_dirs.append(candidate)
if _AGENTS_DIR in dir_set:
if (candidate := path / _AGENTS_SKILLS_SUBDIR).is_dir():
skills_dirs.append(candidate)
dirnames[:] = sorted(d for d in dirnames if d not in WALK_SKIP_DIR_NAMES)
queue: deque[tuple[Path, int]] = deque([(resolved_root, 0)])
visited = 0
while queue and visited < _MAX_DIRS:
current, depth = queue.popleft()
visited += 1
try:
entries = set(os.listdir(current))
except OSError:
continue
_collect_config_dirs_at(current, entries, tools_dirs, skills_dirs, agents_dirs)
if depth < WALK_MAX_DEPTH:
queue.extend(
(child, depth + 1) for child in _iter_child_dirs(current, entries)
)
if visited >= _MAX_DIRS:
logger.warning(
"Config directory scan reached directory limit (%d dirs) at %s",
_MAX_DIRS,
resolved_root,
)
return (tuple(tools_dirs), tuple(skills_dirs), tuple(agents_dirs))
def has_config_dirs_nearby(
root: Path, *, max_depth: int = WALK_MAX_DEPTH, max_dirs: int = 200
) -> bool:
"""Quick check for .vibe/ or .agents/ config dirs in the near subtree.
Returns ``True`` as soon as any config directory is found, without
enumerating all of them.
"""
resolved = root.resolve()
queue: deque[tuple[Path, int]] = deque([(resolved, 0)])
visited = 0
found: list[Path] = []
while queue and visited < max_dirs:
current, depth = queue.popleft()
visited += 1
try:
entries = set(os.listdir(current))
except OSError:
continue
_collect_config_dirs_at(current, entries, found, found, found)
if found:
return True
if depth < max_depth:
queue.extend(
(child, depth + 1) for child in _iter_child_dirs(current, entries)
)
return False