affaan-m/ECC · error · ValueError

output intermediate must be a directory

Error message

output intermediate must be a directory: {part}

What it means

tasteforge's _open_dir walks each intermediate path component and creates directories safely with dir_fd and O_NOFOLLOW. If an intermediate component exists but is not a directory (e.g. a regular file or symlink-to-file), it refuses to proceed with this ValueError rather than overwriting or following it.

Solutions

  1. Inspect the output root and remove/rename the conflicting non-directory entry at the reported path segment
  2. Check the workflow config's output layout for a path whose parent name collides with an existing file
  3. Re-run the workflow with a clean output directory

Example fix

// before: output root contains a regular file named 'assets'
// $ rm ./output/assets  (or mv it elsewhere)
mkdir -p output/assets
// after: re-run the workflow
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path
p = Path(output_root)
assert p.is_dir(), f"output root missing: {p}"
rel = Path(relative)
assert not rel.is_absolute()
for part in rel.parts[:-1]:
    target = p / part
    if target.exists():
        assert target.is_dir() and not target.is_symlink(), f"{target} is not a directory"

Type guard

def is_valid_artifact_dir(root: Path, rel: Path) -> bool:
    if rel.is_absolute() or not rel.name:
        return False
    d = root / rel.parent
    return d.exists() and d.is_dir() and not d.is_symlink()

Prevention

When it happens

Trigger: Calling write_json or artifact_metadata with a relative path whose parent segment collides with an existing non-directory entry in the output root, e.g. artifact path 'assets/file.json' where 'assets' is a regular file.

Common situations: A stale artifact or lock file occupies a name that config now treats as a directory; a previous run wrote a file where a subdirectory was later expected; path segments changed between workflow versions.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/8181260f54de4c03. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/tasteforge/workflow.py:213

        self.close()

    def _open_dir(self, parts: tuple[str, ...], *, create: bool) -> int:
        current = os.dup(self._root_fd)
        try:
            for part in parts:
                if not part or part in {".", ".."} or "/" in part:
                    raise ValueError("output path contains an invalid component")
                try:
                    metadata = os.stat(part, dir_fd=current, follow_symlinks=False)
                except FileNotFoundError:
                    if not create:
                        raise ValueError(f"missing output directory: {part}") from None
                    os.mkdir(part, mode=0o700, dir_fd=current)
                    metadata = os.stat(part, dir_fd=current, follow_symlinks=False)
                if stat.S_ISLNK(metadata.st_mode):
                    raise ValueError(f"output directory must not be a symlink: {part}")
                if not stat.S_ISDIR(metadata.st_mode):
                    raise ValueError(f"output intermediate must be a directory: {part}")
                child = os.open(
                    part,
                    os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
                    dir_fd=current,
                )
                os.close(current)
                current = child
            return current
        except Exception:
            os.close(current)
            raise

    def prepare(self, directories: tuple[str, ...]) -> None:
        """Validate every known intermediate before the first artifact write."""
        opened: list[int] = []
        try:
            for directory in directories:
                opened.append(self._open_dir((directory,), create=True))

View on GitHub (pinned to 8321021c54)