fix(#4783): mtime probe follows symlinked skill dirs to match compute path (gate finding 3)

Codex round-3 reproduced: _skill_tree_max_mtime_ns used os.walk(followlinks=False)
while the compute path iter_skill_index_files uses followlinks=True, so an edit to
a symlinked skill's target SKILL.md was invisible to the probe (stale up to TTL).
Added followlinks=True to the probe walk so it descends symlinked skill dirs and
sees target mtime changes. Regression test added + proven non-vacuous.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
This commit is contained in:
nesquena-hermes 2026-06-24 21:21:57 +00:00
parent 8a46d88daf
commit e4e3b46830
2 changed files with 40 additions and 1 deletions

View file

@ -1597,7 +1597,10 @@ def _skill_tree_max_mtime_ns(skills_dir: Path, config_path: Path) -> int:
return max_ns
try:
# Directory mtimes catch nested out-of-band deletes that leave file mtimes unchanged.
for root, dirnames, filenames in os.walk(skills_dir):
# followlinks=True mirrors agent.skill_utils.iter_skill_index_files (the compute
# path), so a symlinked skill directory is descended into and edits to its target
# SKILL.md change the probe value — otherwise such edits would stay stale up to the TTL.
for root, dirnames, filenames in os.walk(skills_dir, followlinks=True):
root_path = Path(root)
try:
max_ns = max(max_ns, root_path.stat().st_mtime_ns)

View file

@ -308,6 +308,42 @@ class TestNestedDeletionDetected:
assert result == (1, 1)
class TestSymlinkedSkillProbeFollowsLinks:
"""The mtime probe must follow symlinked skill directories the same way the
compute path (iter_skill_index_files, followlinks=True) does — otherwise an
edit to a symlinked skill's target SKILL.md is invisible to the probe and
the counts stay stale up to the TTL (gate finding)."""
def test_symlinked_skill_target_edit_changes_probe(self, profiles_mod, tmp_path):
mod, profile_dir = profiles_mod
skills_dir = profile_dir / "skills"
skills_dir.mkdir(parents=True, exist_ok=True)
config_path = profile_dir / "config.yaml"
# Real skill living OUTSIDE the profile, linked in via a symlinked dir.
target = tmp_path / "external-skill"
target.mkdir()
skill_md = target / "SKILL.md"
skill_md.write_text("---\nname: linked\n---\n# linked\n", encoding="utf-8")
link = skills_dir / "linked"
try:
link.symlink_to(target, target_is_directory=True)
except (OSError, NotImplementedError):
import pytest
pytest.skip("symlinks not supported on this platform")
before = mod._skill_tree_max_mtime_ns(skills_dir, config_path)
# Edit the target SKILL.md (bump its mtime well past granularity).
import os as _os
future = time.time() + 100
_os.utime(skill_md, (future, future))
after = mod._skill_tree_max_mtime_ns(skills_dir, config_path)
assert after > before, (
"probe must follow the symlinked skill dir and see the target SKILL.md mtime change"
)
class TestReturnSignature:
"""Proof matrix row 6: return signature is tuple[int, int]."""