diff --git a/CHANGELOG.md b/CHANGELOG.md index f2f7a94..96844fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.2] - 2026-10-04 + +### Fixed +- In Claude Code, every session end showed `SessionEnd hook [steplock] failed: Hook JSON output validation failed`. polyhook labeled Claude Code's `SessionEnd` (and `SessionStart`) as Gemini CLI and answered `{"decision":"allow"}`. steplock now requires polyhook 0.1.12, which detects them as Claude Code and answers `{}` + ## [0.2.1] - 2026-09-29 ### Fixed diff --git a/core/Cargo.lock b/core/Cargo.lock index e1b4663..6e87aa8 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -626,18 +626,18 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "polyhook" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8b5c80f125dbd9cef5df7475ab359575f9f0558c64f727560ac14513f6ced3" +checksum = "064f93938b54a5dd1c471910f74c72096c127fd232afc6e017144c59d980d9f0" dependencies = [ "polyhook-core", ] [[package]] name = "polyhook-core" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21264c1c460c8d7b216ba7a66059a3b14b0814d6089674d2fd4e0ca27bab75c" +checksum = "510f41412f8e831bcd897a10be4aaa9d0bdc803c3bafddb3dfc64e03492604cb" dependencies = [ "prettyplease", "proc-macro2", @@ -994,7 +994,7 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "steplock" -version = "0.2.1" +version = "0.2.2" dependencies = [ "cel-interpreter", "chrono", @@ -1046,7 +1046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys", diff --git a/core/Cargo.toml b/core/Cargo.toml index d30f9eb..8111cd8 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "steplock" -version = "0.2.1" +version = "0.2.2" edition = "2021" rust-version = "1.88" description = "Stateful quality gate logic for steplock" @@ -54,7 +54,6 @@ use_self = "deny" ref_option_ref = "deny" cast_lossless = "deny" checked_conversions = "deny" -from_iter_instead_of_collect = "deny" doc_markdown = "deny" str_to_string = "deny" multiple_inherent_impl = "deny" @@ -85,6 +84,7 @@ unwrap_used = "deny" wildcard_imports = "deny" enum_glob_use = "deny" single_match_else = "deny" +unnecessary_wraps = "deny" manual_let_else = "deny" items_after_statements = "deny" needless_pass_by_value = "deny" @@ -95,7 +95,7 @@ cel-interpreter = "0.10" clap = { version = "4.5", features = ["derive"] } chrono = { version = "0.4", features = ["serde"] } dirs = "6" -polyhook = "0.1.5" +polyhook = "0.1.12" same-file = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/core/src/bin/main.rs b/core/src/bin/main.rs index e59f16b..e5cd67e 100644 --- a/core/src/bin/main.rs +++ b/core/src/bin/main.rs @@ -74,13 +74,8 @@ fn main() { Some(CliCommand::Validate) => { let dir = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); let root = find_repo_root_from(&dir).unwrap_or(dir); - match run_validate(&root, global_steplock_dir().as_deref()) { - Ok(true) => {} - Ok(false) => process::exit(1), - Err(e) => { - eprintln!("steplock: validate failed: {e}"); - process::exit(1); - } + if !run_validate(&root, global_steplock_dir().as_deref()) { + process::exit(1); } } Some(CliCommand::Clean { global: false }) => { @@ -102,19 +97,18 @@ fn exit_on_error(command: &str, result: io::Result<()>) { } /// Validate all checklists in `.steplock/checklists/` and in the global steplock directory. -/// Returns `Ok(true)` if all valid, `Ok(false)` if any checklist failed validation (errors -/// already printed), or `Err` on I/O. -fn run_validate(repo_root: &Path, global_dir: Option<&Path>) -> io::Result { - let project_ok = validate_dir(&repo_root.join(".steplock").join("checklists"), "")?; - let global_ok = match global_dir { - Some(global) => validate_dir(&global.join("checklists"), "global")?, - None => true, - }; - Ok(project_ok && global_ok) +/// Returns `true` if all valid, `false` if any checklist failed validation (errors already +/// printed). Both directories are always checked, so every error is reported. +fn run_validate(repo_root: &Path, global_dir: Option<&Path>) -> bool { + let project_ok = validate_dir(&repo_root.join(".steplock").join("checklists"), ""); + let global_ok = + global_dir.is_none_or(|global| validate_dir(&global.join("checklists"), "global")); + project_ok && global_ok } /// Validate one `checklists/` directory. `scope` names it in messages (`""` or `"global"`). -fn validate_dir(checklists_dir: &Path, scope: &str) -> io::Result { +/// Returns `false` if any checklist failed validation (errors already printed). +fn validate_dir(checklists_dir: &Path, scope: &str) -> bool { let shown = checklists_dir.display(); let (words, label_prefix) = if scope.is_empty() { (String::new(), String::new()) @@ -123,18 +117,18 @@ fn validate_dir(checklists_dir: &Path, scope: &str) -> io::Result { }; if !checklists_dir.exists() { println!("steplock: no {words}checklists found at {shown}"); - return Ok(true); + return true; } let errors = steplock::validate_checklists(checklists_dir); if errors.is_empty() { println!("steplock: all {words}checklists valid ({shown})"); - Ok(true) + true } else { for (label, err) in &errors { eprintln!("steplock: [{label_prefix}{label}] error: {err}"); } - Ok(false) + false } } diff --git a/core/src/bin/main_tests.rs b/core/src/bin/main_tests.rs index 83b0a4a..5679f00 100644 --- a/core/src/bin/main_tests.rs +++ b/core/src/bin/main_tests.rs @@ -206,21 +206,21 @@ fn clean_leaves_sessions_dir_intact() { #[test] fn validate_returns_true_when_no_checklists_dir() { let tmp = TempDir::new().unwrap(); - assert!(run_validate(tmp.path(), None).unwrap()); + assert!(run_validate(tmp.path(), None)); } #[test] fn validate_returns_true_when_checklists_empty() { let tmp = TempDir::new().unwrap(); fs::create_dir_all(tmp.path().join(".steplock/checklists")).unwrap(); - assert!(run_validate(tmp.path(), None).unwrap()); + assert!(run_validate(tmp.path(), None)); } #[test] fn validate_returns_true_for_valid_checklist() { let tmp = TempDir::new().unwrap(); setup_checklist(tmp.path()); - assert!(run_validate(tmp.path(), None).unwrap()); + assert!(run_validate(tmp.path(), None)); } #[test] @@ -233,7 +233,7 @@ fn validate_returns_false_when_config_toml_missing() { "stateDiagram-v2\n [*] --> s\n s --> [*]\n s: Step\n", ) .unwrap(); - assert!(!run_validate(tmp.path(), None).unwrap()); + assert!(!run_validate(tmp.path(), None)); } #[test] @@ -246,7 +246,7 @@ fn validate_returns_false_when_flow_mmd_missing() { "on_event = \"tool:before\"\nreset = \"session\"\n", ) .unwrap(); - assert!(!run_validate(tmp.path(), None).unwrap()); + assert!(!run_validate(tmp.path(), None)); } #[test] @@ -260,7 +260,7 @@ fn validate_returns_false_for_invalid_config_toml() { "stateDiagram-v2\n [*] --> s\n s --> [*]\n s: Step\n", ) .unwrap(); - assert!(!run_validate(tmp.path(), None).unwrap()); + assert!(!run_validate(tmp.path(), None)); } #[test] @@ -274,7 +274,7 @@ fn validate_returns_false_for_invalid_flow_mmd() { ) .unwrap(); fs::write(cl_dir.join("flow.mmd"), "stateDiagram-v2\n a --> b\n").unwrap(); - assert!(!run_validate(tmp.path(), None).unwrap()); + assert!(!run_validate(tmp.path(), None)); } #[test] @@ -289,7 +289,7 @@ fn validate_continues_checking_all_checklists_after_failure() { "stateDiagram-v2\n [*] --> s\n s --> [*]\n s: Step\n", ) .unwrap(); - assert!(!run_validate(tmp.path(), None).unwrap()); + assert!(!run_validate(tmp.path(), None)); } fn parse(args: &[&str]) -> Cli {