{"record":{"id":"7bbea85718956046","repo":"Hmbown/CodeWhale","slug":"skill-version-marker-should-have-a-parent-director","errorCode":null,"errorMessage":"skill version marker should have a parent directory","messagePattern":"skill version marker should have a parent directory","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/skills/system.rs","lineNumber":474,"sourceCode":"/// are preserved.\nfn retire_unchanged_v4_best_practices(skills_dir: &Path) -> std::io::Result<bool> {\n    let dir = skills_dir.join(\"v4-best-practices\");\n    let file = dir.join(\"SKILL.md\");\n    if !file.exists() {\n        return Ok(false);\n    }\n    let existing = fs::read_to_string(&file)?;\n    if existing != v4_best_practices_body() {\n        return Ok(false);\n    }\n    fs::remove_dir_all(&dir)?;\n    Ok(true)\n}\n\nfn write_marker_atomically(marker: &Path, version: &str) -> std::io::Result<()> {\n    let parent = marker\n        .parent()\n        .expect(\"skill version marker should have a parent directory\");\n    let mut temporary = tempfile::NamedTempFile::new_in(parent)?;\n    temporary.write_all(version.as_bytes())?;\n    temporary.as_file().sync_all()?;\n    // `rename` atomically replaces a file on Unix. Windows refuses to replace\n    // an existing destination, so remove only this reserved marker first.\n    #[cfg(windows)]\n    if marker.exists() {\n        fs::remove_file(marker)?;\n    }\n    fs::rename(temporary.path(), marker)\n}\n\n#[cfg(test)]\nmod tests;\n","sourceCodeStart":456,"sourceCodeEnd":489,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/skills/system.rs#L456-L489","documentation":"Panic in the atomic skill version-marker writer. `Path::parent()` returns `None` only when the path is empty or the filesystem root (`/`), so the expect fires when the composed marker path degenerated to `\"\"` or `\"/\"` — e.g. an empty state-dir base joined with nothing — before the `NamedTempFile::new_in` call can run.","triggerScenarios":"A marker path built from an empty string (misconfigured/empty state-dir override, `PathBuf::from(\"\")` flowing through) or literally `\"/\"`; the first skill-system versioning write then panics at system.rs:474.","commonSituations":"Env/config overrides for the codewhale state dir set to empty; tests constructing marker paths from `String::new()`; path-joining code that drops the filename component under some condition.","solutions":["Trace where the marker path is composed (skills state dir) and guard: reject empty or root paths before calling `write_marker_atomically`.","Fix the override/config value that produced the degenerate path.","Return `Err(io::ErrorKind::InvalidInput)` naming the bad path instead of `expect`.","Add a test with an empty marker path asserting the graceful error."],"exampleFix":"// before\nlet parent = marker.parent().expect(\"skill version marker should have a parent directory\");\n\n// after: reject degenerate paths with a real error\nlet Some(parent) = marker.parent() else {\n    return Err(std::io::Error::new(\n        std::io::ErrorKind::InvalidInput,\n        format!(\"marker path `{}` has no parent directory\", marker.display()),\n    ));\n};","handlingStrategy":"validation","validationCode":"fn marker_path_ok(marker: &std::path::Path) -> bool {\n    !marker.as_os_str().is_empty()\n        && marker != std::path::Path::new(\"/\")\n        && marker.parent().is_some()\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never build marker paths from unvalidated empty strings or env overrides.","Unit-test the path composition for the skills state dir, including override values."],"tags":["rust","filesystem","atomic-write","path-validation","panic","expect"],"backgroundTag":"path-parent-missing","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}