Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1550,17 +1550,35 @@ def collect_tool_dirs(root: Path | None = None) -> list[Path]:
return []

dirs = sorted(d for d in base.iterdir() if d.is_dir() and not d.name.startswith("."))
tracked_names = _git_tracked_tool_names(repo_root)
if tracked_names is None:
visible_names = _git_visible_tool_names(repo_root)
if visible_names is None:
return dirs
return [d for d in dirs if d.name in tracked_names]
return [d for d in dirs if d.name in visible_names]


def _git_tracked_tool_names(root: Path) -> set[str] | None:
"""Return top-level ``tools/<name>`` entries tracked by git, if available."""
def _git_visible_tool_names(root: Path) -> set[str] | None:
"""Return top-level ``tools/<name>`` entries git considers non-ignored
(tracked *or* untracked-but-not-``.gitignore``d), if git is available.

Using ``--cached --others --exclude-standard`` — rather than tracked
files alone — means a freshly-authored tool directory that has not been
``git add``ed yet is still validated, while gitignored artifact
directories (e.g. ``__pycache__``, build output) are excluded.
"""
try:
result = subprocess.run(
["git", "-C", str(root), "ls-files", "-z", "--", str(TOOLS_DIR)],
[
"git",
"-C",
str(root),
"ls-files",
"-z",
"--cached",
"--others",
"--exclude-standard",
"--",
str(TOOLS_DIR),
],
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
Expand Down
25 changes: 25 additions & 0 deletions tools/skill-and-tool-validator/tests/test_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2611,6 +2611,31 @@ def test_ignored_tool_artifact_directory_skipped(self, tmp_path: Path) -> None:
violations = list(validate_tools(root))
assert violations == []

def test_untracked_tool_directory_still_checked(self, tmp_path: Path) -> None:
# A freshly-authored tool directory that has not been ``git add``ed yet
# is not gitignored, so it must still be validated. Regression guard: an
# earlier tracked-only filter silently dropped such directories.
root = _make_tools_root(tmp_path)
subprocess.run(["git", "init"], cwd=root, check=True, capture_output=True)
(root / ".gitignore").write_text("tools/ignored-artifact/\n", encoding="utf-8")

untracked_tool = root / "tools" / "brand-new"
untracked_tool.mkdir()
(untracked_tool / "README.md").write_text(
"# tools/brand-new\n\n**Capability:** substrate:framework-dev\n\nNew tool.\n\n" + _VALID_PREREQS,
encoding="utf-8",
)

ignored_tool = root / "tools" / "ignored-artifact"
ignored_tool.mkdir()
(ignored_tool / "junk.txt").write_text("", encoding="utf-8")

# Only .gitignore is staged; the new tool is deliberately left untracked.
subprocess.run(["git", "add", ".gitignore"], cwd=root, check=True, capture_output=True)

assert [d.name for d in collect_tool_dirs(root)] == ["brand-new"]
assert list(validate_tools(root)) == []

def test_tool_missing_readme(self, tmp_path: Path) -> None:
root = _make_tools_root(tmp_path)
(root / "tools" / "no-readme").mkdir()
Expand Down