abhigyanpatwari/GitNexus · error · ValueError
plan directory must be a real directory: {plans}
Error message
plan directory must be a real directory: {plans} What it means
Raised by snapshot_plan_docs (runner_artifacts.py:261) when docs/plans exists, is a symlink, or is not a directory. The planner artifact hasher walks docs/plans expecting a real directory of .md/.html files; a symlinked or file-like plans path would let a phase redirect or deny plan hashing, so it is rejected.
Source
Thrown at eval/workflow_bench/runner_artifacts.py:261
def require_skill_fingerprint(worktree: Path, arm: str, expected: str | None, *, phase: str) -> None:
"""Fail closed when a bounded phase changes the evaluated prompt roots."""
try:
observed = skill_fingerprint(worktree, arm)
except (OSError, ValueError) as exc:
raise ValueError(f"{phase} changed the evaluated skill fingerprint") from exc
if observed != expected:
raise ValueError(f"{phase} changed the evaluated skill fingerprint")
def snapshot_plan_docs(worktree: Path) -> dict[Path, str]:
"""Hash direct, regular plan artifacts without following links."""
plans = worktree / "docs" / "plans"
if not plans.exists():
return {}
if plans.is_symlink() or not plans.is_dir():
raise ValueError(f"plan directory must be a real directory: {plans}")
snapshot: dict[Path, str] = {}
for path in sorted(plans.iterdir()):
if path.suffix.lower() not in {".md", ".html"}:
continue
metadata = path.lstat()
if stat.S_ISLNK(metadata.st_mode):
raise ValueError(f"plan artifact cannot be a symlink: {path}")
if not stat.S_ISREG(metadata.st_mode):
raise ValueError(f"plan artifact must be a regular file: {path}")
descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
try:
opened = os.fstat(descriptor)
if not stat.S_ISREG(opened.st_mode) or opened.st_dev != metadata.st_dev or opened.st_ino != metadata.st_ino:
raise ValueError(f"plan artifact changed while opening: {path}")
with os.fdopen(descriptor, "rb", closefd=False) as handle:
snapshot[path] = hashlib.file_digest(handle, "sha256").hexdigest()
after = os.fstat(descriptor)View on GitHub (pinned to d540b00184)
Solutions
- Inspect docs/plans in the worktree: ls -ld docs/plans. If it is a symlink or file, remove it and recreate as a real directory.
- Ensure no setup/phase step replaces the directory with a symlink or file.
- If plans genuinely live elsewhere, restructure so docs/plans is the real directory the snapshot expects.
Example fix
# before rm -rf docs/plans && ln -s ../plans docs/plans # after rm docs/plans && mkdir -p docs/plans
Defensive patterns
Strategy: validation
Validate before calling
import os, stat
from pathlib import Path
plans = Path(worktree) / "docs" / "plans"
if plans.exists():
assert not plans.is_symlink(), f"docs/plans is a symlink: {plans}"
assert plans.is_dir(), f"docs/plans is not a directory: {plans}"
mode = plans.lstat().st_mode
assert stat.S_ISDIR(mode), f"docs/plans mode is not dir: {mode:o}" Type guard
import stat
from pathlib import Path
def is_real_plans_dir(worktree) -> bool:
plans = Path(worktree) / "docs" / "plans"
if not plans.exists():
return True # absent is allowed (returns {})
if plans.is_symlink() or not plans.is_dir():
return False
return stat.S_ISDIR(plans.lstat().st_mode) Prevention
- Keep docs/plans as a real directory; do not symlink it.
- Ensure no setup/phase step replaces the directory with a file or symlink.
When it happens
Trigger: plans = worktree/'docs'/'plans'; plans.exists() is True but plans.is_symlink() or not plans.is_dir(). Caused by docs/plans being a symlink to elsewhere, a regular file named 'plans', or a broken symlink that .exists() still sees via lstat-true semantics.
Common situations: A repo keeps plans elsewhere and symlinks docs/plans; a setup step created docs/plans as a file by mistake; a prior failed phase left a symlink where a directory should be.
Related errors
- plan artifact cannot be a symlink: {path}
- oracle stage parent changed during verification: {item.targe
- workspace snapshot root must be a real directory: {root}
- phase artifact must be a regular non-symlink file: {relative
- plan artifact must be a regular file: {path}
AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12).
Data as JSON: /api/errors/04b40ae8cad745b3.
Report an issue: GitHub.