Co-authored-by: Bastien <bastien.baret@gmail.com>
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Julien Legrand <72564015+JulienLGRD@users.noreply.github.com>
Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com>
Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Clément Drouin 2026-04-14 10:33:15 +02:00 committed by GitHub
parent e9a9217cc8
commit e1a25caa52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
85 changed files with 2830 additions and 594 deletions

View file

@ -2,8 +2,8 @@ from __future__ import annotations
from vibe.core.paths._local_config_walk import (
WALK_MAX_DEPTH,
has_config_dirs_nearby,
walk_local_config_dirs_all,
ConfigWalkResult,
walk_local_config_dirs,
)
from vibe.core.paths._vibe_home import (
DEFAULT_TOOL_DIR,
@ -31,7 +31,7 @@ __all__ = [
"TRUSTED_FOLDERS_FILE",
"VIBE_HOME",
"WALK_MAX_DEPTH",
"ConfigWalkResult",
"GlobalPath",
"has_config_dirs_nearby",
"walk_local_config_dirs_all",
"walk_local_config_dirs",
]

View file

@ -1,6 +1,7 @@
from __future__ import annotations
from collections import deque
from dataclasses import dataclass, field
from functools import cache
import logging
import os
@ -21,24 +22,59 @@ WALK_MAX_DEPTH = 4
_MAX_DIRS = 2000
def _collect_config_dirs_at(
path: Path,
entry_names: set[str],
tools: list[Path],
skills: list[Path],
agents: list[Path],
@dataclass(frozen=True)
class ConfigWalkResult:
"""Aggregated results of a config directory walk."""
config_dirs: tuple[Path, ...] = ()
tools: tuple[Path, ...] = ()
skills: tuple[Path, ...] = ()
agents: tuple[Path, ...] = ()
@dataclass
class _ConfigWalkCollector:
"""Mutable accumulator used during BFS, frozen into ConfigWalkResult at the end."""
config_dirs: list[Path] = field(default_factory=list)
tools: list[Path] = field(default_factory=list)
skills: list[Path] = field(default_factory=list)
agents: list[Path] = field(default_factory=list)
def freeze(self) -> ConfigWalkResult:
return ConfigWalkResult(
config_dirs=tuple(self.config_dirs),
tools=tuple(self.tools),
skills=tuple(self.skills),
agents=tuple(self.agents),
)
def _collect_at(
path: Path, entry_names: set[str], collector: _ConfigWalkCollector
) -> None:
"""Check a single directory for .vibe/ and .agents/ config subdirs."""
if _VIBE_DIR in entry_names:
if _VIBE_DIR in entry_names and (vibe_dir := path / _VIBE_DIR).is_dir():
has_content = False
if (candidate := path / _TOOLS_SUBDIR).is_dir():
tools.append(candidate)
collector.tools.append(candidate)
has_content = True
if (candidate := path / _VIBE_SKILLS_SUBDIR).is_dir():
skills.append(candidate)
collector.skills.append(candidate)
has_content = True
if (candidate := path / _AGENTS_SUBDIR).is_dir():
agents.append(candidate)
if _AGENTS_DIR in entry_names:
collector.agents.append(candidate)
has_content = True
if (
has_content
or (vibe_dir / "prompts").is_dir()
or (vibe_dir / "config.toml").is_file()
):
collector.config_dirs.append(vibe_dir)
if _AGENTS_DIR in entry_names and (agents_dir := path / _AGENTS_DIR).is_dir():
if (candidate := path / _AGENTS_SKILLS_SUBDIR).is_dir():
skills.append(candidate)
collector.skills.append(candidate)
collector.config_dirs.append(agents_dir)
def _scandir_entries(path: Path) -> tuple[set[str], list[Path]]:
@ -68,60 +104,22 @@ def _scandir_entries(path: Path) -> tuple[set[str], list[Path]]:
@cache
def walk_local_config_dirs_all(
root: Path,
) -> tuple[tuple[Path, ...], tuple[Path, ...], tuple[Path, ...]]:
def walk_local_config_dirs(
root: Path, *, max_depth: int = WALK_MAX_DEPTH, max_dirs: int = _MAX_DIRS
) -> ConfigWalkResult:
"""Discover .vibe/ and .agents/ config directories under *root*.
Uses breadth-first search bounded by ``WALK_MAX_DEPTH`` and ``_MAX_DIRS``
Uses breadth-first search bounded by *max_depth* and *max_dirs*
to avoid unbounded traversal in large repositories.
"""
tools_dirs: list[Path] = []
skills_dirs: list[Path] = []
agents_dirs: list[Path] = []
Returns a ``ConfigWalkResult`` containing both the parent config dirs
(for trust decisions) and the categorised subdirs (for loading).
"""
collector = _ConfigWalkCollector()
resolved_root = root.resolve()
queue: deque[tuple[Path, int]] = deque([(resolved_root, 0)])
visited = 0
while queue and visited < _MAX_DIRS:
current, depth = queue.popleft()
visited += 1
entry_names, children = _scandir_entries(current)
if not entry_names:
continue
_collect_config_dirs_at(
current, entry_names, tools_dirs, skills_dirs, agents_dirs
)
if depth < WALK_MAX_DEPTH:
queue.extend((child, depth + 1) for child in children)
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
@ -130,11 +128,16 @@ def has_config_dirs_nearby(
if not entry_names:
continue
_collect_config_dirs_at(current, entry_names, found, found, found)
if found:
return True
_collect_at(current, entry_names, collector)
if depth < max_depth:
queue.extend((child, depth + 1) for child in children)
return False
if visited >= max_dirs:
logger.warning(
"Config directory scan reached directory limit (%d dirs) at %s",
max_dirs,
resolved_root,
)
return collector.freeze()