Skip to content

Releases: GreyforgeLabs/slopbrake

Slopbrake 0.2.3

Choose a tag to compare

@GreyforgeLabs GreyforgeLabs released this 14 Oct 22:04

Faster hooks on every agent tool call, and two recording fixes for the user-level Stop gate (H6, H7).

  • Faster: slopbrake-hook imports only what the common path needs; subprocess, tempfile, shutil, hashlib and argparse load only where git, the Stop gate or the installer use them. Median wall time per call on a busy 12-core host, Python 3.13: a Bash call outside managed repos 48 → 35 ms, an Edit outside 44 → 32 ms, an untouched Stop 44 → 31 ms. Peak RSS 18.7 → 14.8 MB.
  • Faster: a snapshot of a managed repo is one git status --porcelain=v2 --branch instead of rev-parse plus status, and stats each path relative to the repo's directory. PreToolUse in a managed repo 67 → 56 ms; on a repo with 30,000 untracked files a snapshot takes 2.7x less time (546 → 202 ms; the status parser is now linear).
  • Faster: an Edit in a managed repo reads HEAD only the first time the session changes that repo, with rev-parse, never a full git status: 64 → 32 ms per Edit.
  • Faster: the project guard (.claude/hooks/block-dangerous-git.sh) imports git_guard.py instead of running it as a script, so Python reuses its bytecode instead of compiling 900 lines per Bash call, and parses its one option by hand (argparse alone pulled in a dozen modules on Python 3.14): 67 → 31 ms per call with Python 3.14, 17.9 → 12.9 MB. The bytecode lives under ${XDG_CACHE_HOME:-~/.cache}/slopbrake/pycache, never in the repo, and the guard runs with python3 -I, so the session's cwd and PYTHON* variables stay out of its imports. It reads the hook JSON as UTF-8 whatever the locale.
  • Fixed: an edit made through a symlinked directory was recorded as ../link/... instead of the repo's own path, so the user-level Stop gate skipped a repo whose only change came that way. Edit and apply_patch targets are now resolved like the repo itself.
  • Fixed: a worktree rename (git add -N on the new name) made the snapshot record a garbage path (porcelain v1 puts the R in the second column); the v2 parser reads renames by entry type.
  • slopbrake verify and Slopbrake's own gate keep the guard's bytecode in their scratch directories.

Full Changelog: v0.2.2...v0.2.3

Slopbrake 0.2.2

Choose a tag to compare

@GreyforgeLabs GreyforgeLabs released this 06 Oct 05:26
  • Fixed: the user-level Stop gate held a session red for another session's uncommitted work in the same repo (and spent its whole budget mutating it). It now records the files each session changed and the HEAD it started from, skips repos where nothing of the session's is left, and gates HEAD plus the session's own files in a clean temporary worktree when other uncommitted work is present.
  • New: boundaries_py.py --skip-rule RULE lets a repo keep only the D1 rules that fit its architecture (for example, cycles only).

Slopbrake 0.2.1

Choose a tag to compare

@GreyforgeLabs GreyforgeLabs released this 06 Oct 03:55

Found by putting 0.2.0 to work on two more repos and three more harnesses.

Codex and OpenCode (H5, H6, H7)

  • New: slopbrake user-hooks install|uninstall|status --harness codex merges slopbrake-hook --harness codex pre-tool-use|post-tool-use|stop into ~/.codex/hooks.json, keeping other tools' entries. It refuses while ~/.codex/config.toml lacks [features] hooks = true and never edits that file; status reminds you to trust the entries in Codex's /hooks screen and reports which ones [hooks.state] already trusts.

  • New: --harness opencode installs the OpenCode plugin ~/.config/opencode/plugins/slopbrake.js (one dependency-free module; it refuses to overwrite a file that is not its own). It guards bash before it runs, records bash, edit, multiedit, write and apply_patch changes, and on session.idle runs the Stop gate for root sessions, sending a red report back as a new prompt at most 3 times in a row. It does nothing when slopbrake-hook is not on PATH.

  • Both installers refuse, and status is not ok, unless the slopbrake-hook on PATH answers --harness codex|opencode pre-tool-use: an older build that rejects --harness would make every hook a silent no-op. Codex entries count as installed only when matcher and timeout match too; install rewrites one that differs. Codex keys its trust by entry position, so removing or replacing slopbrake's entries can renumber a later entry in the same event, which then needs trusting again (status says so).

  • Fixed: Grok runs a repo's .claude hooks with camelCase JSON (toolInput, sessionId), so the git guard saw no command and allowed everything while looking wired. The kit's hooks and slopbrake-hook --harness grok now read both shapes; Grok's write and hashline_edit edits are recorded.

  • Fixed: a CLAUDE_PROJECT_DIR leaked into another harness made the user-level Stop gate skip the project repo; it is honoured only for Claude Code. slopbrake hook --harness ... (the alias) no longer dies in argparse with exit 2, which every harness reads as a block.

The gate

  • Fixed: when only tests changed, mutation mutated every module the changed test files imported, whole; on a large repo one changed allowlist made the Stop gate time out on every stop. It now mutates only the functions the changed test functions call (compared with the base, so a deleted assertion counts), and skips when they call nothing first-party.
  • Fixed: the last-green ratchet and the default-branch door (C8) counted untracked files, so a permanent untracked folder froze the ratchet and kept the pr stage red on main. Both now look at tracked files; an untracked file counts for C8 once it is staged.
  • Fixed: the Python scripts/check in a linked worktree tested the main checkout's code (the borrowed .venv holds an editable install of the main tree) and lacked the venv's tools on PATH. The template now puts this tree first on PYTHONPATH and the venv's bin on PATH. (Repo-owned: hand-merge into an existing scripts/check.)
  • Fixed: the kit scripts wrote __pycache__ next to themselves, and status did not notice bytecode an older install had committed.

init and status

  • Fixed: init in a linked worktree set the shared core.hooksPath, switching the main checkout's hooks before the kit was merged; it now leaves it alone and says to set it after the merge. init --no-trust skips the trusted-repo list, for staging a branch. CLAUDE.md is named after the repository, not the worktree directory.
  • Fixed: core.hooksPath=/dev/null is reported as disabled hooks, not as existing hooks to chain into.
  • Changed: status counts only tracked TypeScript sources for its coverage gap, and UNCOVERED=(dir ...) in scripts/check declares sources left out on purpose (vendored code).

Slopbrake 0.2.0

Choose a tag to compare

@GreyforgeLabs GreyforgeLabs released this 05 Oct 20:56

An audit of 0.1.0 in real use found that Slopbrake was installed but never fired, and that several of its guarantees were false while verify and status stayed green. This release makes the gate do what the README says. Rule IDs refer to docs/RULES.md.

Hooks that actually run (H5, H6, H7)

  • Broken: Claude Code loads project hooks only from the directory a session starts in. Sessions started in ~ never ran the git guard or the Stop gate, and nothing said so.
  • New: slopbrake user-hooks install|uninstall|status|trust|untrust wires slopbrake-hook pre-tool-use|post-tool-use|stop into ~/.claude/settings.json. The guard acts in every repo with .claude/slopbrake.json; the Stop gate runs only for repos the session actually changed and that you trust (init trusts its repo; a cloned repo's scripts never run until you user-hooks trust it). Outside managed repos the hooks do nothing. install refuses while the slopbrake on PATH is too old, and an older install without slopbrake-hook fails harmlessly instead of blocking every command.
  • Broken: the git guard was a regex with many bypasses (push -f;, +main, --force-with-lease, restore/checkout forms, --no-verify, -c core.hooksPath=) and false positives on commit messages and grep patterns. It failed open without jq.
  • Changed: the guard is now git_guard.py, which parses the command like a shell (bash -c, eval, wrappers, substitutions, heredocs) and judges git's own argv. It blocks the forms above plus branch, stash, reflog, ref and object deletion, and aliases or rebase --exec that run them. The shell wrapper fails closed without python3.
  • Broken: the Stop gate looked only at uncommitted code, so committing (or working in another worktree) let an agent stop with an unchecked change.
  • Changed: a clean tree with commits no full green run has covered gets the full gate, with sampled mutants and one 540 s budget inside the hook's 600 s timeout. Worktrees of the same repo are gated too.

The gate (L1)

  • Broken: stages with nothing to run printed pass (types was an echo, smoke without a script, pr without a PR), and verify proved stages by name, so trimming STAGES in scripts/check switched off the real gate with all proofs green.
  • Changed: a stage with nothing to check exits 78 and reports skip <stage> with its reason. The L1 proof fails unless the summary reports every stage the kit's scripts/check defines, and status flags missing stages and an unconfigured types stage.
  • Broken: MUTATION_FLOOR=0 git push and TEST_CMD=true lowered the gate from the environment.
  • Changed: scripts/check fixes the floor and the test command; pre-push drops inherited overrides and runs the full gate once per pushed ref, against the commit the remote already has.
  • Broken: committing straight on the default branch with no remote (both pilots' workflow) measured mutation and the door against HEAD itself: nothing changed, so the floor and the door never ran on committed work.
  • New: the last-green ratchet. A full, all-green run on a clean tree records refs/slopbrake/last-green/<branch> (/ in the branch name written as %2F); on the default branch, checks measure from it, or from the merge-base with it after an amend or rebase, or from the empty tree when there is none yet. init starts it at HEAD.
  • Changed: pre-commit refuses when a staged file also has unstaged edits, and pre-push refuses a dirty tree or a ref that isn't checked out: the gate checks the working tree, so it must be the tree being committed or pushed. A new branch is measured against what the remote already has.
  • Changed: a change that touches only tests is mutation-checked against the modules those tests import (Python), so weakening an assertion no longer slips through. Marking every changed line # slopbrake: no-mutate fails the stage.
  • Changed: Python repos get pytest when it's configured or declared anywhere (pytest-style tests were silently skipped under unittest discover), the project's interpreter (.venv, found at run time, also from a linked worktree), and a CI install that matches. The TypeScript CI is set up for the repo's package manager instead of always pnpm.

Test quality (T1, T4)

  • Broken: an assertion-free test cleared the mutation floor when the code used string + (the mutant crashed) and 0 mutants scored 100%. T1 missed its own headline form, assert total(xs) == sum(xs), and the inline reduce/Math.* forms in TypeScript.
  • Changed: T1 flags tests with no assertion (Python check; TypeScript slopbrake/expect-in-test), the direct aggregate forms and more shapes, and stops flagging determinism, identity and property checks. # slopbrake: allow-no-assert: <why> suppresses one.
  • Changed: more mutation operators (boundaries, strings, deleted calls), no arithmetic mutants on string concatenation, nothing to mutate is a skip, # slopbrake: no-mutate for equivalent mutants, and timed-out mutants no longer leave orphaned processes or scratch copies.

TypeScript (D1, T1, T4)

  • Broken: under npm, npm exec <tool> --flags swallowed every flag (test-quality always red, Stryker lost --mutate), and with dependency-cruiser missing, npm exec depcruise installed a squatted registry placeholder that let boundaries pass.
  • Changed: tools run from node_modules/.bin only; a missing tool fails its stage and names the install command. Boundaries fail when dependency-cruiser read nothing; import type and co-located tests are checked; the smoke stage runs a smoke package script.

Boundaries (D1)

  • Broken: Python boundaries were enforced only between top-level packages, so a single-package app had no enforcement at all; monorepos under packages/ and importlib imports were invisible.
  • Changed: entry points are enforced at every package level, plus importlib.import_module/__import__, namespace subpackages, packages/*/ monorepos and stricter TYPE_CHECKING cycle handling.

Doors (G1-G4)

  • Broken: a PR could lower its own floor: the rules were read from the PR's tree, deleting door-rules.yml disabled the check, and SLOPBRAKE_BASE beat the CI base. Gate configs (Stryker, ESLint rules, dependency-cruiser, the reviewer, skills, CLAUDE.md) classified two-way.
  • Changed: the base revision's rules count too, and ignore lists come from the base only; no rules anywhere is an error. The default rules cover many more one-way paths and destructive SQL spellings, with fewer false positives (prose, test fixtures, .env.example).
  • New: without a PR, the pr stage refuses an uncommitted one-way change on the default branch: commit it on a feature branch for review. The PR-body check is stricter: the Door and Blast Radius must be in prose, not code blocks, and a one-way door's rollback must name a concrete step that isn't negated ("no way to restore" is not a plan).

CLI

  • Fixed: init --dry-run crashed on fresh Python repos; verify leaked a worktree when setup failed; environment errors exit 2 with one line instead of a traceback; init in a monorepo subdirectory no longer touches the parent repo's hooks.
  • Changed: verify also fails when the Claude Code hooks aren't wired or are disabled, proves the committed HEAD only, and shows the failing stage. status reports a stale kit (by version and by file), unwired hooks and the missing ESLint plugin. init tells you to commit the kit on a feature branch.

Community

Thanks to @nixfred for the first community fixes, all in this release:

  • #1: the git guard catches git's global options, clean --force and checkout <ref> -- ..
  • #2: the diff parser no longer drops added lines that start with ++.
  • #3: a scalar or empty rule list in door-rules.yml is an error, not a silently wrong rule.

Upgrading from 0.1.0

  1. Install 0.2.0: uv tool install --force git+https://github.com/GreyforgeLabs/slopbrake (or pipx install --force …).
  2. On a feature branch, run slopbrake init . --update. Kit-owned files (scripts/slopbrake/, .claude/hooks/, .githooks/, skills, the reviewer) are refreshed. Repo-owned files are kept and listed as kept; merge these by hand (the kit's versions are what slopbrake init writes into a fresh scratch repo of the same stack):
    • scripts/check: STAGES=(lint types boundaries tests smoke test-quality mutation pr), with smoke; a fixed export MUTATION_FLOOR=60 and a fixed TEST_CMD instead of the ${…:-…} forms; a types stage that runs your type checker or returns 78 (skip); a smoke stage that returns 78 without scripts/smoke.sh.
    • .claude/door-rules.yml: the new one_way paths (gate configs, .claude/agents/**, .claude/skills/**, CLAUDE.md, secrets, more infra), the new content_patterns, the ignore list and the extra content_ignore entries. Keep each key once.
    • .github/workflows/check.yml: the setup and install steps for your package manager.
  3. Commit on the branch: the update touches the gate, which is a one-way door, so the gate refuses it on the default branch. A human reviews and merges it.
  4. slopbrake user-hooks install, so the hooks fire in sessions started outside the repo.
  5. slopbrake status . should list no gaps, and slopbrake verify . should report 16 proofs holding.

Slopbrake 0.1.0

Choose a tag to compare

@GreyforgeLabs GreyforgeLabs released this 26 Sep 05:26

Brakes for the software factory. Slopbrake puts Matt Pocock's agentic-engineering ideas into a repo as checks that fail a build, and proves each one bites.

uv tool install git+https://github.com/GreyforgeLabs/slopbrake@v0.1.0
slopbrake init path/to/repo && slopbrake verify path/to/repo

In 0.1.0

  • One gate (scripts/check): lint, types, import boundaries, tests, smoke, a tautological-test check, and a mutation floor on changed lines. Run by pre-commit, pre-push, CI and a Claude Code Stop hook.
  • Tautology check for Python (AST) and TypeScript (ESLint rule slopbrake/no-tautological-test).
  • Mutation floor on changed lines only: a stdlib mutator for Python, StrykerJS ranges for TypeScript.
  • Deep-module boundaries: a stdlib Python port of Pocock's dependency-cruiser rules, and his config for TypeScript.
  • Door triage: path and content rules compute a one-way/two-way floor the agent can raise but never lower; the PR-body check enforces Door and Blast Radius.
  • A reviewer agent in fresh context: two parallel axes (Standards, Spec), then a single fix-mode pass that commits review: fixes.
  • Git guardrails and a Stop-only-when-green hook for Claude Code.
  • slopbrake verify: 15 proofs in a throwaway worktree. Each rule must fail on a planted violation, name itself, and pass again once it's removed.
  • Pocock's skills vendored unmodified at c55ee46 (MIT), with credits for every idea in the README.

See docs/RULES.md for all 33 rules.