{"record":{"id":"d72b5feaa01d033e","repo":"abhigyanpatwari/GitNexus","slug":"candidate-overlays-may-only-contain-markdown-files","errorCode":null,"errorMessage":"candidate overlays may only contain Markdown files under .claude/skills/gitnexus-{plan,work}: {relative}","messagePattern":"candidate overlays may only contain Markdown files under \\.claude/skills/gitnexus-(.+?): (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"critical","filePath":"eval/workflow_bench/evolution.py","lineNumber":326,"sourceCode":"                entries.append(path)\n                if len(entries) > MAX_CANDIDATE_FILES:\n                    raise ValueError(f\"candidate overlay exceeds the {MAX_CANDIDATE_FILES}-file limit\")\n        pending.extend(child_directories)\n\n    entries.sort(key=lambda path: path.relative_to(overlay).as_posix())\n    if not entries:\n        raise ValueError(f\"candidate overlay contains no files: {overlay}\")\n\n    for path in entries:\n        relative = path.relative_to(overlay)\n        parts = relative.parts\n        if (\n            len(parts) < 4\n            or parts[:2] != (\".claude\", \"skills\")\n            or parts[2] not in CANDIDATE_SKILLS\n            or path.suffix.lower() != \".md\"\n        ):\n            raise ValueError(\n                \"candidate overlays may only contain Markdown files under \"\n                \".claude/skills/gitnexus-{plan,work}: \"\n                f\"{relative}\"\n            )\n    return entries\n\n\ndef required_candidate_arms(overlay: Path) -> list[str]:\n    \"\"\"Return the smallest candidate-arm set that exercises every change.\n\n    Plan prompts are loaded only by the two-session workflow. Work prompts are\n    loaded by both workflow shapes, so a work candidate must prove itself in\n    both rather than inheriting a decision from an untested execution mode.\n    \"\"\"\n    overlay = overlay.expanduser().absolute()\n    touched = {path.relative_to(overlay).parts[2] for path in candidate_overlay_files(overlay)}\n    required: list[str] = []\n    if \"gitnexus-plan\" in touched or \"gitnexus-work\" in touched:","sourceCodeStart":308,"sourceCodeEnd":344,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/eval/workflow_bench/evolution.py#L308-L344","documentation":"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.","triggerScenarios":"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.","commonSituations":"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/.","solutions":["Move every overlay file under .claude/skills/gitnexus-plan/ or .claude/skills/gitnexus-work/ with a .md extension.","Remove any file that is not a plan/work skill Markdown prompt.","Validate each path's parts and suffix before invoking the harness."],"exampleFix":"# before: overlay contains a review-skill file and a .txt\n.claude/skills/gitnexus-review/SKILL.md\nnotes.txt\n\n# after: only plan/work .md files\nfrom pathlib import PurePosixPath\nALLOWED = {'gitnexus-plan', 'gitnexus-work'}\nfor p in Path('overlay').rglob('*'):\n    if p.is_file():\n        rel = PurePosixPath(p.relative_to('overlay').as_posix())\n        ok = (len(rel.parts) >= 4 and rel.parts[:2] == ('.claude','skills')\n              and rel.parts[2] in ALLOWED and rel.suffix.lower() == '.md')\n        assert ok, f'outside trust boundary: {rel}'","handlingStrategy":"validation","validationCode":"from pathlib import Path, PurePosixPath\nfrom workflow_bench.evolution import CANDIDATE_SKILLS\n\ndef overlay_within_trust_boundary(root: Path) -> bool:\n    for p in Path(root).rglob('*'):\n        if not p.is_file():\n            continue\n        rel = PurePosixPath(p.relative_to(root).as_posix())\n        if (\n            len(rel.parts) < 4\n            or rel.parts[:2] != ('.claude', 'skills')\n            or rel.parts[2] not in CANDIDATE_SKILLS\n            or rel.suffix.lower() != '.md'\n        ):\n            return False\n    return True","typeGuard":"from pathlib import PurePosixPath\nfrom workflow_bench.evolution import CANDIDATE_SKILLS\n\ndef is_allowed_skill_path(rel: PurePosixPath) -> bool:\n    return (\n        len(rel.parts) >= 4\n        and rel.parts[:2] == ('.claude', 'skills')\n        and rel.parts[2] in CANDIDATE_SKILLS\n        and rel.suffix.lower() == '.md'\n    )","tryCatchPattern":"try:\n    apply_candidate_overlay(overlay, worktree, sandbox=sandbox)\nexcept ValueError as exc:\n    if 'may only contain Markdown files under' in str(exc):\n        # move/rename the offending file under .claude/skills/gitnexus-{plan,work}/, then retry\n        ...","preventionTips":["Only place files under .claude/skills/gitnexus-plan/ or gitnexus-work/.","Use .md extensions exclusively for overlay content.","Validate every path's parts and suffix against CANDIDATE_SKILLS before running."],"tags":["trust-boundary","security","overlay","config","skills"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}