opendataloader-project/opendataloader-pdf · error · ConfigError

no SKILL.md found under {skill_dir} (nothing to lint)

Error message

no SKILL.md found under {skill_dir} (nothing to lint)

What it means

ConfigError (exit code 2) raised by _scanned_files when the --skill-dir does not contain a SKILL.md file. The lint is designed to scan agent-facing prose (SKILL.md + references/*.md); without SKILL.md the directory is not a skill bundle, so scanning 0 files and reporting PASS would be a false negative. This input/config error is deliberately separated (exit 2) from a real coupling violation (exit 1).

Source

Thrown at skills/odl-pdf-maintenance/sync-skill-refs.py:91

_FLAG_RE = re.compile(r"(?<![A-Za-z0-9])--[a-z0-9][a-z0-9-]*")
# A run of dot-separated integers (semver OR IPv4); classified after matching.
_DOTTED_RE = re.compile(r"\d+(?:\.\d+)+")
# Referenced bundle paths the prose points at (must exist under the skill dir).
_PATH_RE = re.compile(r"\b((?:references|scripts)/[A-Za-z0-9_.-]+\.(?:md|py|sh))\b")


class ConfigError(Exception):
    """Bad input/config (missing SKILL.md, unreadable file). Reported as exit 2,
    distinct from exit 1 (a coupling/structural violation in the authored prose)."""


def _scanned_files(skill_dir: Path):
    """Agent-facing prose: SKILL.md + references/*.md. ConfigError if the dir does
    not look like a skill bundle (no SKILL.md) — an empty/wrong --skill-dir must not
    silently scan 0 files and report a false PASS."""
    skill_md = skill_dir / "SKILL.md"
    if not skill_md.is_file():
        raise ConfigError(f"no SKILL.md found under {skill_dir} (nothing to lint)")
    files = [skill_md] + sorted((skill_dir / "references").glob("*.md"))
    return [f for f in files if f.is_file()]


def _read(f: Path) -> str:
    try:
        return f.read_text(encoding="utf-8")
    except OSError as e:
        raise ConfigError(f"{f}: {e}") from e


def _is_ipv4(run: str) -> bool:
    """A dotted run that is a valid dotted-quad IPv4 (4 octets, each 0-255)."""
    parts = run.split(".")
    return len(parts) == 4 and all(p.isdigit() and 0 <= int(p) <= 255 for p in parts)


def check_versions(rel: str, text: str, violations: list):

View on GitHub (pinned to a7789b8e77)

Solutions

  1. Point --skill-dir at the actual skill bundle directory containing SKILL.md (by default the script's sibling odl-pdf dir).
  2. Verify with ls <skill-dir>/SKILL.md before running.
  3. If the bundle is generated by a build step, ensure that step ran before the lint.
  4. Distinguish exit code 2 (this config error) from exit 1 (a real violation) in CI scripting.

Example fix

# before: wrong dir (the script's own dir, no SKILL.md)
$ ./sync-skill-refs.py --skill-dir skills/odl-pdf-maintenance  # -> ConfigError
# after: the distributed skill bundle
$ ./sync-skill-refs.py --skill-dir skills/odl-pdf
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path
def is_skill_bundle(skill_dir: str) -> bool:
    return (Path(skill_dir) / "SKILL.md").is_file()

Type guard

def is_no_skill_md_error(exc: Exception) -> bool:
    return exc.__class__.__name__ == "ConfigError" and "no SKILL.md found under" in str(exc)

Try / catch

import subprocess, sys
rc = subprocess.run([sys.executable, "sync-skill-refs.py", "--skill-dir", d]).returncode
if rc == 2:
    print("Config error (exit 2): bad --skill-dir; point at the dir containing SKILL.md")
elif rc == 1:
    print("Lint violation (exit 1): fix the coupling issue in the prose")

Prevention

When it happens

Trigger: Running sync-skill-refs.py with a --skill-dir that points at the wrong directory (e.g. the repo root, the maintenance dir instead of the odl-pdf bundle dir, or a path where the skill has not been assembled).

Common situations: CI invokes the lint with a path that changed after a directory rename. A developer passes the maintenance dir (which contains the script, not SKILL.md). The skill bundle has not been generated/copied yet. A typo in the path.

Related errors


AI-assisted analysis of opendataloader-project/opendataloader-pdf@a7789b8e77 (2026-08-14). Data as JSON: /api/errors/4fe3c5ad4b5206b7. Report an issue: GitHub.