diff --git a/CHANGELOG.md b/CHANGELOG.md index b6b35e2..090eccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- `steplock init` and `steplock init --global` no longer write a default `example-gate` checklist. They create an empty `checklists/` directory + ### Fixed - The `steplock init` sample checklist and the `git-push-quality-gate` example matched the literal text `git push`, so `git -C push` (or any global option between `git` and `push`) skipped the gate. They now match on `command_words` diff --git a/Installation.md b/Installation.md index 6756e21..35735bf 100644 --- a/Installation.md +++ b/Installation.md @@ -61,7 +61,7 @@ cargo install steplock ## Project setup -Run inside your repo root. Creates the `.steplock/checklists/` directory tree: +Run inside your repo root. Creates an empty `.steplock/checklists/` directory and a `.steplock/.gitignore`. No checklist is written: ```sh steplock init @@ -177,7 +177,7 @@ Use the full path to the binary (`which steplock`; `~/.cargo/bin/steplock` after ```sh echo '{"args": {"command": "git push origin main"}, "session_id": "check"}' > /tmp/push.json hermes hooks test pre_tool_call --for-tool terminal --payload-file /tmp/push.json - # parsed (Hermes wire shape): {"action": "block", "message": "[example-gate: 1/2] ..."} + # parsed (Hermes wire shape): {"action": "block", "message": "[: 1/N] ..."} ``` Hermes runs hooks in the directory where Hermes started, not in the terminal tool's working directory. Global checklists do not depend on this. A project `.steplock/` is found only when Hermes was started inside that project. @@ -209,7 +209,7 @@ cat .steplock/audit.log ## Global checklists -Global checklists apply to every project. Create the global steplock directory with a sample checklist: +Global checklists apply to every project. Create the global steplock directory (with an empty `checklists/`): ```sh steplock init --global diff --git a/README.md b/README.md index 2a73040..f7ada58 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ Rules: - A project checklist with the same directory name replaces the global one. An empty `.steplock/checklists//` directory turns that global checklist off for the project. - Session state and the audit log for global checklists are written to the global directory, not to the project. -Run `steplock init --global` to create the directory with a sample checklist. Register the hook once in each agent's user-level settings so it runs in every project: `~/.claude/settings.json` for Claude Code, `~/.hermes/config.yaml` for Hermes Agent (see [Installation](Installation.md#hermes-agent)). Every agent reads the same global directory, so one checklist gates all of them. +Run `steplock init --global` to create the directory with an empty `checklists/`. Register the hook once in each agent's user-level settings so it runs in every project: `~/.claude/settings.json` for Claude Code, `~/.hermes/config.yaml` for Hermes Agent (see [Installation](Installation.md#hermes-agent)). Every agent reads the same global directory, so one checklist gates all of them. Some agents give hooks a sandboxed `HOME`. Hermes Agent does this in containers and with `TERMINAL_HOME_MODE=profile`, and exports the real home as `HERMES_REAL_HOME`. steplock uses that real home, so the global directory stays the same. To pin it explicitly, set `STEPLOCK_GLOBAL_DIR`. diff --git a/core/src/bin/main.rs b/core/src/bin/main.rs index a83f217..e59f16b 100644 --- a/core/src/bin/main.rs +++ b/core/src/bin/main.rs @@ -38,7 +38,7 @@ struct Cli { /// `steplock` subcommands. #[derive(Debug, Subcommand)] enum CliCommand { - /// Create .steplock/checklists/ with a sample checklist in the current directory + /// Create an empty .steplock/checklists/ in the current directory Init { /// Create checklists/ in the global steplock directory instead #[arg(long)] @@ -168,21 +168,12 @@ fn run_hook() { } } -const SAMPLE_CONFIG: &str = r#"on_event = "tool:before" -on_tool = "bash" -match_input = "input.command_words.exists(x, x == 'git') && input.command_words.exists(x, x == 'push')" -reset = "session" -"#; - -const SAMPLE_FLOW: &str = "stateDiagram-v2\n [*] --> tests_pass\n tests_pass --> reviewed\n reviewed --> [*]\n tests_pass : Tests pass locally\n reviewed : Code reviewed\n"; - /// Create `.steplock/checklists/` and a `.steplock/.gitignore` in `dir`. -/// Also writes a ready-to-use sample checklist so `git push` is blocked immediately. fn run_init(dir: &Path) -> io::Result<()> { init_steplock_dir(&dir.join(".steplock"), true) } -/// Create `checklists/` with a sample checklist in `steplock_dir`. +/// Create an empty `checklists/` in `steplock_dir`. No checklist is written. /// With `gitignore`, also writes a `.gitignore` for session state and the audit log. fn init_steplock_dir(steplock_dir: &Path, gitignore: bool) -> io::Result<()> { let checklists_dir = steplock_dir.join("checklists"); @@ -194,17 +185,8 @@ fn init_steplock_dir(steplock_dir: &Path, gitignore: bool) -> io::Result<()> { if gitignore { fs::write(steplock_dir.join(".gitignore"), "sessions/\naudit.log\n")?; } - let sample_dir = checklists_dir.join("example-gate"); - fs::create_dir_all(&sample_dir)?; - fs::write(sample_dir.join("config.toml"), SAMPLE_CONFIG)?; - fs::write(sample_dir.join("flow.mmd"), SAMPLE_FLOW)?; println!("steplock: initialized {}", checklists_dir.display()); - println!( - "A sample checklist was written to {}.", - sample_dir.display() - ); - println!("It will block `git push` until two quality checks are acknowledged."); - println!("Edit config.toml and flow.mmd to customize it, or add more checklists."); + println!("Add a checklist as checklists//config.toml and flow.mmd."); Ok(()) } diff --git a/core/src/bin/main_tests.rs b/core/src/bin/main_tests.rs index c8f238c..83b0a4a 100644 --- a/core/src/bin/main_tests.rs +++ b/core/src/bin/main_tests.rs @@ -144,25 +144,14 @@ fn init_creates_checklists_dir_and_gitignore() { } #[test] -fn init_scaffolds_sample_checklist() { - let tmp = TempDir::new().unwrap(); - run_init(tmp.path()).unwrap(); - let sample = tmp.path().join(".steplock/checklists/example-gate"); - assert!(sample.join("config.toml").exists()); - assert!(sample.join("flow.mmd").exists()); - let cfg = fs::read_to_string(sample.join("config.toml")).unwrap(); - assert!(cfg.contains("command_words")); - let flow = fs::read_to_string(sample.join("flow.mmd")).unwrap(); - assert!(flow.contains("stateDiagram-v2")); -} - -#[test] -fn init_sample_checklist_is_valid() { +fn init_writes_no_checklist() { let tmp = TempDir::new().unwrap(); run_init(tmp.path()).unwrap(); + let checklists = tmp.path().join(".steplock/checklists"); + assert_eq!(fs::read_dir(&checklists).unwrap().count(), 0); let stdin = claude_stdin("git push origin main", "s1"); let resp = run_app(stdin.as_bytes(), tmp.path(), None).unwrap(); - assert!(matches!(resp, polyhook::HookResponse::BlockResponse(_))); + assert!(!matches!(resp, polyhook::HookResponse::BlockResponse(_))); } #[test] @@ -477,28 +466,3 @@ fn hermes_session_end_cleans_global_session() { "on_session_end must clean the global session" ); } - -#[test] -fn init_sample_blocks_git_push_with_global_options() { - let tmp = TempDir::new().unwrap(); - run_init(tmp.path()).unwrap(); - for cmd in [ - "git -C /repo push origin main", - "git push", - "cd x && git push -q", - ] { - let stdin = claude_stdin(cmd, "s-opts"); - let resp = run_app(stdin.as_bytes(), tmp.path(), None).unwrap(); - assert!( - matches!(resp, polyhook::HookResponse::BlockResponse(_)), - "sample must block `{cmd}`" - ); - fs::remove_dir_all(tmp.path().join(".steplock/sessions")).unwrap(); - } - let stdin = claude_stdin("git status", "s-opts"); - let resp = run_app(stdin.as_bytes(), tmp.path(), None).unwrap(); - assert!( - !matches!(resp, polyhook::HookResponse::BlockResponse(_)), - "sample must not block unrelated git commands" - ); -} diff --git a/core/tests/cli_tests.rs b/core/tests/cli_tests.rs index 4effc9f..55cf6f0 100644 --- a/core/tests/cli_tests.rs +++ b/core/tests/cli_tests.rs @@ -302,9 +302,12 @@ fn init_global_scaffolds_global_dir() { let target = global.path().join("steplock"); let output = run_subcommand_with_global(&["init", "--global"], global.path(), &target); assert!(output.status.success(), "init --global should succeed"); - assert!( - target.join("checklists/example-gate/config.toml").exists(), - "sample checklist expected in global dir" + let checklists = target.join("checklists"); + assert!(checklists.is_dir(), "checklists/ expected in global dir"); + assert_eq!( + fs::read_dir(&checklists).unwrap().count(), + 0, + "init must not write a default checklist" ); assert!( !target.join(".gitignore").exists(),