{"record":{"id":"140476bf24bc83ed","repo":"abhigyanpatwari/GitNexus","slug":"phase-changed-the-evaluated-skill-fingerprint","errorCode":null,"errorMessage":"{phase} changed the evaluated skill fingerprint","messagePattern":"(.+?) changed the evaluated skill fingerprint","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"eval/workflow_bench/runner_artifacts.py","lineNumber":249,"sourceCode":"    while parent.parts:\n        parent_key = parent.as_posix()\n        if parent_key not in before and after.get(parent_key, \"\").startswith(\"d:\"):\n            allowed.add(parent_key)\n        parent = parent.parent\n    unauthorized = sorted(changed - allowed)\n    if unauthorized:\n        preview = \", \".join(unauthorized[:8])\n        suffix = \" …\" if len(unauthorized) > 8 else \"\"\n        raise ValueError(f\"phase changed unauthorized workspace path(s): {preview}{suffix}\")\n\n\ndef 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()","sourceCodeStart":231,"sourceCodeEnd":267,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/eval/workflow_bench/runner_artifacts.py#L231-L267","documentation":"Raised by require_skill_fingerprint (runner_artifacts.py:249) when skill_fingerprint(worktree, arm) itself raises OSError or ValueError while re-hashing the evaluated skill roots after a bounded phase. The wrapper converts any underlying read/parse failure into a single 'phase changed the evaluated skill fingerprint' error, failing closed: the harness cannot confirm the skill surface is unchanged, so it treats the phase as having tampered with it.","triggerScenarios":"skill_fingerprint() at evolution.py:440 walks .claude/skills/<skill>/ and encounters a missing directory (_require_directory_chain raises), a symlink/non-regular file inside the skill root (raises ValueError at line 470), or an OSError reading a file. The phase under inspection ran between the before/after fingerprint and disturbed the skill tree.","commonSituations":"A planning/work phase (or a setup step) writes a symlink into .claude/skills/, deletes a skill directory, changes permissions so a file is unreadable, or exceeds MAX_SKILL_FINGERPRINT_BYTES by writing a large file into the skill root.","solutions":["Unwrap the chained exception (__cause__) to see the original OSError/ValueError and the offending path; the original message names the file or directory.","Restore the skill root from git (git -C <worktree> checkout -- .claude/skills) and re-run the phase with a prompt that forbids editing .claude/skills.","If a large legitimate file triggered the size limit, move it out of the skill root before the phase."],"exampleFix":"# before: phase writes a symlink into the skill root\nln -s /etc/passwd .claude/skills/gitnexus-plan/SKILL.md\n\n# after: phase leaves skill roots untouched; restore if disturbed\ngit -C worktree checkout -- .claude/skills","handlingStrategy":"try-catch","validationCode":"from pathlib import Path\nfrom eval.workflow_bench.evolution import skill_fingerprint\n\n# Before the phase, confirm skill_fingerprint can be computed without error.\nroot = Path(worktree).resolve()\nfp = skill_fingerprint(root, arm)\nassert fp is not None, f\"arm {arm} has no evaluated skills\"\n# After the phase, recompute; any exception here is what 475 wraps.\nfp2 = skill_fingerprint(root, arm)\nassert fp == fp2, \"skill surface changed during phase preview\"","typeGuard":null,"tryCatchPattern":"try:\n    require_skill_fingerprint(worktree, arm, expected, phase=\"work\")\nexcept ValueError as exc:\n    # Unwrap the cause to find the offending path/type error\n    cause = exc.__cause__\n    log.error(\"skill fingerprint failure: %s\", cause)\n    # Restore skill roots from git before any retry\n    subprocess.run([\"git\", \"-C\", str(worktree), \"checkout\", \"--\", \".claude/skills\"], check=True)\n    raise","preventionTips":["Mount .claude/skills read-only for bounded phases.","Forbid the model from editing .claude/skills in the phase prompt.","Restore skill roots from git before retrying after a failure."],"tags":["skill-fingerprint","phase-boundary","integrity"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}