feat: support global checklists shared by every project - #209
Conversation
Checklists in the global steplock directory ($STEPLOCK_GLOBAL_DIR, else $XDG_CONFIG_HOME/steplock, else ~/.config/steplock) now apply to every project, including projects without a .steplock/ directory. - Project checklists run first, then global checklists. - A project checklist with the same name overrides the global one; an empty directory with that name turns it off. - Global session state and audit events live in the global directory. - New CLI: `steplock init --global`, `steplock clean --global`; `steplock validate` also checks global checklists. - New library API: `run_with_global`, `global_steplock_dir`. - Promote clippy::manual_let_else to deny. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Tests used Unix-only absolute paths, and Windows usually has no HOME. Fall back to USERPROFILE and build test paths per platform. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
|
||
| /// Checklist directories under `<steplock_dir>/checklists/`, sorted by name. | ||
| /// Returns an empty list when the directory does not exist. | ||
| fn checklist_dirs(steplock_dir: &Path) -> Result<Vec<PathBuf>> { |
There was a problem hiding this comment.
Done in a517070: moved to core/src/catalog.rs (checklist_dirs).
| .unwrap_or("") | ||
| .to_owned(); | ||
| /// Evaluate one checklist. Returns `Some(Block)` when it blocks the event, `None` otherwise. | ||
| fn evaluate_checklist( |
There was a problem hiding this comment.
Done in a517070: moved evaluate_checklist to core/src/gate.rs, together with the helpers only it uses (block_reset_always, block_reset_session, get_scope_key, build_block_message), so gate.rs doesn't import back from run.rs. run.rs now only handles the project/global orchestration and session:stop cleanup.
| } | ||
|
|
||
| /// `true` when both paths resolve to the same existing directory. | ||
| fn is_same_dir(a: &Path, b: &Path) -> bool { |
There was a problem hiding this comment.
Done in a517070: replaced is_same_dir with same_file::is_same_file (MIT/Unlicense, already allowed by deny.toml). An error (e.g. a missing path) counts as not the same directory.
| let mut child = Command::new(STEPLOCK) | ||
| .current_dir(root) | ||
| .env("STEPLOCK_GLOBAL_DIR", global) | ||
| .stdin(Stdio::piped()) | ||
| .stdout(Stdio::piped()) | ||
| .stderr(Stdio::piped()) |
There was a problem hiding this comment.
I see this logic twice, extract
There was a problem hiding this comment.
Done in a517070: the parent-directory test now calls the shared run_steplock helper instead of its own copy of the spawn/stdin logic. The new global subcommand tests use one run_subcommand_with_global helper.
|
Open pr to use lib for the cli, and open a pr to extract the tests to _tests.rs files |
- Move checklist discovery into catalog.rs. - Move per-checklist gate evaluation (and the block/message helpers it owns) into gate.rs; run.rs now only orchestrates project and global directories. - Use the same-file crate instead of hand-rolled canonicalize comparison. - Share one spawn helper across CLI tests instead of duplicating it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| fn resolve_global_dir(var: impl Fn(&str) -> Option<OsString>) -> Option<PathBuf> { | ||
| if let Some(dir) = var(GLOBAL_DIR_ENV) { | ||
| return if dir.is_empty() { | ||
| None | ||
| } else { | ||
| Some(PathBuf::from(dir)) | ||
| }; | ||
| } | ||
| if let Some(xdg) = var("XDG_CONFIG_HOME").map(PathBuf::from) { | ||
| if xdg.is_absolute() { | ||
| return Some(xdg.join("steplock")); | ||
| } | ||
| } | ||
| ["HOME", "USERPROFILE"] | ||
| .into_iter() | ||
| .filter_map(&var) | ||
| .find(|home| !home.is_empty()) | ||
| .map(|home| PathBuf::from(home).join(".config").join("steplock")) | ||
| } |
There was a problem hiding this comment.
Use a lib for the home dir, i have one use it
There was a problem hiding this comment.
Done in b97401f: the lookup now uses dirs::home_dir(). replace-homedir has no public home-dir getter (only replace_homedir(path, replacement)), so I used dirs, the crate it is built on. The home dir is passed into resolve_global_dir, so the tests stay free of env.
Replace the hand-rolled HOME/USERPROFILE lookup with dirs::home_dir(), the same crate replace-homedir builds on. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Follow-ups opened as stacked draft PRs (they touch the same files as this one):
The home-dir comment is addressed here in b97401f ( |
Summary
.steplock/. Lookup order:$STEPLOCK_GLOBAL_DIR(an empty value turns it off), then$XDG_CONFIG_HOME/steplock, then~/.config/steplock. The layout is the same as.steplock/(checklists/,sessions/,audit.log)..steplock/checklists/<name>/turns that global gate off for the project. Global session state is written to the global directory, so nothing gets written into projects that have no.steplock/. If the global directory resolves to the project.steplock/, it is evaluated only once.session:stopcleans up both locations.steplock init --globalandsteplock clean --global.steplock validatenow also checks global checklists; their errors are labelled[global:<name>/...].run_with_global(event, repo_root, global_dir)andglobal_steplock_dir().run()keeps its current behaviour (project only).clippy::manual_let_elseis nowdeny(no existing violations).Motivation: gate an action such as
git pushfor every repo an agent works in, from one user-level hook (~/.claude/settings.json) plus one global checklist. Before this change, the only option was a.steplock/in a shared parent directory like~. That broke as soon as a repo had its own.steplock/, because the nearest directory wins and nothing merges.Test plan
cargo testpasses (6 new unit tests for directory resolution, 6 new gate integration tests, 6 new CLI tests)cargo clippy --all-targets -- -D warningspassescargo fmt -- --checkpassesRUSTDOCFLAGS="-D warnings" cargo doc --no-depspassesHOMEand an empty project directory, ransteplock init --global, then sent Claude CodePreToolUseevents forgit push. Step 1 blocked,ack.shran, step 2 blocked,ack.shran, then the push was approved.lswas approved immediately. All state was written under~/.config/steplock/and none in the project.validateandclean --globalreport correctly.The CLI tests now set
STEPLOCK_GLOBAL_DIR=""by default, so a developer's real global checklists cannot affect the test results.🤖 Generated with Claude Code