{"record":{"id":"04b40ae8cad745b3","repo":"abhigyanpatwari/GitNexus","slug":"plan-directory-must-be-a-real-directory-plans","errorCode":null,"errorMessage":"plan directory must be a real directory: {plans}","messagePattern":"plan directory must be a real directory: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"eval/workflow_bench/runner_artifacts.py","lineNumber":261,"sourceCode":"def require_skill_fingerprint(worktree: Path, arm: str, expected: str | None, *, phase: str) -> None:\n    \"\"\"Fail closed when a bounded phase changes the evaluated prompt roots.\"\"\"\n\n    try:\n        observed = skill_fingerprint(worktree, arm)\n    except (OSError, ValueError) as exc:\n        raise ValueError(f\"{phase} changed the evaluated skill fingerprint\") from exc\n    if observed != expected:\n        raise ValueError(f\"{phase} changed the evaluated skill fingerprint\")\n\n\ndef snapshot_plan_docs(worktree: Path) -> dict[Path, str]:\n    \"\"\"Hash direct, regular plan artifacts without following links.\"\"\"\n\n    plans = worktree / \"docs\" / \"plans\"\n    if not plans.exists():\n        return {}\n    if plans.is_symlink() or not plans.is_dir():\n        raise ValueError(f\"plan directory must be a real directory: {plans}\")\n\n    snapshot: dict[Path, str] = {}\n    for path in sorted(plans.iterdir()):\n        if path.suffix.lower() not in {\".md\", \".html\"}:\n            continue\n        metadata = path.lstat()\n        if stat.S_ISLNK(metadata.st_mode):\n            raise ValueError(f\"plan artifact cannot be a symlink: {path}\")\n        if not stat.S_ISREG(metadata.st_mode):\n            raise ValueError(f\"plan artifact must be a regular file: {path}\")\n        descriptor = os.open(path, os.O_RDONLY | getattr(os, \"O_NOFOLLOW\", 0))\n        try:\n            opened = os.fstat(descriptor)\n            if not stat.S_ISREG(opened.st_mode) or opened.st_dev != metadata.st_dev or opened.st_ino != metadata.st_ino:\n                raise ValueError(f\"plan artifact changed while opening: {path}\")\n            with os.fdopen(descriptor, \"rb\", closefd=False) as handle:\n                snapshot[path] = hashlib.file_digest(handle, \"sha256\").hexdigest()\n            after = os.fstat(descriptor)","sourceCodeStart":243,"sourceCodeEnd":279,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/eval/workflow_bench/runner_artifacts.py#L243-L279","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nrm -rf docs/plans && ln -s ../plans docs/plans\n\n# after\nrm docs/plans && mkdir -p docs/plans","handlingStrategy":"validation","validationCode":"import os, stat\nfrom pathlib import Path\n\nplans = Path(worktree) / \"docs\" / \"plans\"\nif plans.exists():\n    assert not plans.is_symlink(), f\"docs/plans is a symlink: {plans}\"\n    assert plans.is_dir(), f\"docs/plans is not a directory: {plans}\"\n    mode = plans.lstat().st_mode\n    assert stat.S_ISDIR(mode), f\"docs/plans mode is not dir: {mode:o}\"","typeGuard":"import stat\nfrom pathlib import Path\n\ndef is_real_plans_dir(worktree) -> bool:\n    plans = Path(worktree) / \"docs\" / \"plans\"\n    if not plans.exists():\n        return True  # absent is allowed (returns {})\n    if plans.is_symlink() or not plans.is_dir():\n        return False\n    return stat.S_ISDIR(plans.lstat().st_mode)","tryCatchPattern":null,"preventionTips":["Keep docs/plans as a real directory; do not symlink it.","Ensure no setup/phase step replaces the directory with a file or symlink."],"tags":["plan-docs","symlink","integrity"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}