abhigyanpatwari/GitNexus · critical · ValueError

candidate overlays may only contain Markdown files under .cl

Error message

candidate overlays may only contain Markdown files under .claude/skills/gitnexus-{plan,work}: {relative}

What it means

Thrown by candidate_overlay_files (evolution.py:326) when any collected file fails the trust-boundary shape test: its relative path must have at least 4 parts, parts[:2] == ('.claude','skills'), parts[2] in CANDIDATE_SKILLS ({'gitnexus-plan','gitnexus-work'}), and suffix.lower() == '.md'. This enforces that candidates may only modify the canonical plan/work skill prompts — they cannot touch task code, tests, or other skills and thereby game the promotion gate.

Source

Thrown at eval/workflow_bench/evolution.py:326

                entries.append(path)
                if len(entries) > MAX_CANDIDATE_FILES:
                    raise ValueError(f"candidate overlay exceeds the {MAX_CANDIDATE_FILES}-file limit")
        pending.extend(child_directories)

    entries.sort(key=lambda path: path.relative_to(overlay).as_posix())
    if not entries:
        raise ValueError(f"candidate overlay contains no files: {overlay}")

    for path in entries:
        relative = path.relative_to(overlay)
        parts = relative.parts
        if (
            len(parts) < 4
            or parts[:2] != (".claude", "skills")
            or parts[2] not in CANDIDATE_SKILLS
            or path.suffix.lower() != ".md"
        ):
            raise ValueError(
                "candidate overlays may only contain Markdown files under "
                ".claude/skills/gitnexus-{plan,work}: "
                f"{relative}"
            )
    return entries


def required_candidate_arms(overlay: Path) -> list[str]:
    """Return the smallest candidate-arm set that exercises every change.

    Plan prompts are loaded only by the two-session workflow. Work prompts are
    loaded by both workflow shapes, so a work candidate must prove itself in
    both rather than inheriting a decision from an untested execution mode.
    """
    overlay = overlay.expanduser().absolute()
    touched = {path.relative_to(overlay).parts[2] for path in candidate_overlay_files(overlay)}
    required: list[str] = []
    if "gitnexus-plan" in touched or "gitnexus-work" in touched:

View on GitHub (pinned to d540b00184)

Solutions

  1. Move every overlay file under .claude/skills/gitnexus-plan/ or .claude/skills/gitnexus-work/ with a .md extension.
  2. Remove any file that is not a plan/work skill Markdown prompt.
  3. Validate each path's parts and suffix before invoking the harness.

Example fix

# before: overlay contains a review-skill file and a .txt
.claude/skills/gitnexus-review/SKILL.md
notes.txt

# after: only plan/work .md files
from pathlib import PurePosixPath
ALLOWED = {'gitnexus-plan', 'gitnexus-work'}
for p in Path('overlay').rglob('*'):
    if p.is_file():
        rel = PurePosixPath(p.relative_to('overlay').as_posix())
        ok = (len(rel.parts) >= 4 and rel.parts[:2] == ('.claude','skills')
              and rel.parts[2] in ALLOWED and rel.suffix.lower() == '.md')
        assert ok, f'outside trust boundary: {rel}'
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path, PurePosixPath
from workflow_bench.evolution import CANDIDATE_SKILLS

def overlay_within_trust_boundary(root: Path) -> bool:
    for p in Path(root).rglob('*'):
        if not p.is_file():
            continue
        rel = PurePosixPath(p.relative_to(root).as_posix())
        if (
            len(rel.parts) < 4
            or rel.parts[:2] != ('.claude', 'skills')
            or rel.parts[2] not in CANDIDATE_SKILLS
            or rel.suffix.lower() != '.md'
        ):
            return False
    return True

Type guard

from pathlib import PurePosixPath
from workflow_bench.evolution import CANDIDATE_SKILLS

def is_allowed_skill_path(rel: PurePosixPath) -> bool:
    return (
        len(rel.parts) >= 4
        and rel.parts[:2] == ('.claude', 'skills')
        and rel.parts[2] in CANDIDATE_SKILLS
        and rel.suffix.lower() == '.md'
    )

Try / catch

try:
    apply_candidate_overlay(overlay, worktree, sandbox=sandbox)
except ValueError as exc:
    if 'may only contain Markdown files under' in str(exc):
        # move/rename the offending file under .claude/skills/gitnexus-{plan,work}/, then retry
        ...

Prevention

When it happens

Trigger: An overlay file lives outside .claude/skills/gitnexus-plan/ or .claude/skills/gitnexus-work/; a file under a non-candidate skill (e.g. gitnexus-review); a non-.md file; or a path too shallow (<4 parts) such as a top-level file.

Common situations: Trying to update a review/debugging skill via the overlay; including a .txt/.json helper; placing a file directly under .claude/ instead of .claude/skills/gitnexus-work/.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12). Data as JSON: /api/errors/d72b5feaa01d033e. Report an issue: GitHub.