All notable changes documented here. Format per Keep a Changelog, semver adherence.
See docs/plan.md for the active improvement plan.
- Bump
idna3.13 → 3.16 to close GHSA-65pc-fj4g-8rjx (CVE-2026-45409). The vulnerability is a denial-of-service inidna.encode()reachable only via theanthropicSDK inbench/dispatch.py(dev-only, not runtime), but the bump is free and the audit had to land somewhere. Surfaced by a full OSV scan of the resolved dependency tree — the other 38 packages came back clean.
- CI now installs dependencies via
uv sync --locked --all-extrasinstead ofpip install -e ".[dev]". The lockfile was previously decorative on CI (pip re-resolved from PyPI every run), so a malicious release of any dev dep would have landed in CI within hours of publish. With this change, dev-dep bumps require an explicit lockfile update that flows through review. actions/checkoutand the Python toolchain action are now pinned to commit SHAs (actions/checkout@34e1148 # v4,astral-sh/setup-uv@0880764 # v8.1.0) instead of floating@v4/@v5tags. This mitigates the tag-mutation supply chain attack class — see thetj-actions/changed-filesSept 2024 incident for what happens to consumers who don't pin.- Replaced
actions/setup-pythonwithastral-sh/setup-uv.uvprovisions the Python interpreter from the lockfile'srequires-python, so the separate setup-python step is redundant. - Added a top-level
permissions: contents: readto the CI workflow. The jobs only read, so the default token scope (which can becontents: writeonpush) was wider than needed. pre-commithook revs (ruff-pre-commit,shellcheck-py) are now frozen to commit SHAs with the version in a trailing comment. Same tag-mutation mitigation as the Actions pins.
ast-grep-cli>=0.39to the[dev]extra. It was previously installed unpinned by a dedicated CI step (pip install ast-grep-cli), which auto-pulled latest on every run. Moving it into[dev]means it's locked and hashed inuv.lockalongside the rest of the dev tree..github/dependabot.ymlcoveringgithub-actions,pip(dev group), andpre-commiton a weekly cadence. SHA pins are a frozen snapshot — without an updater they rot. Dependabot reads the# vX.Y.Zcomments on the pinned SHAs and opens PRs for the next release.
bullyis now reliably on$PATHfor plugin users via a newbin/bullywrapper. Claude Code auto-prepends each installed plugin'sbin/directory to$PATH, but bully wasn't shipping one — so when the skill ranbully --log-verdict ...to record semantic-evaluation telemetry, the shell answered withcommand not found: bully(exit 127) in every plugin-using repo. The documented fallback (python3 .../pipeline/pipeline.py) was also broken: that path was deleted in the 0.8.3 src/ restructure.bin/bullymirrors the PYTHONPATH-shim logic already used byhooks/hook.shand the repo-root./bullywrapper (PYTHONPATH=<plugin>/src python3 -m bully "$@"), so the primary path now works for plugin installs, editable installs, and manual symlink installs alike — with nopip installstep. The bully, bully-init, and bully-author skills and the README dropped the deadpipeline/pipeline.pyfallback and now documentPYTHONPATH=.../src python3 -m bullyas the form to use whencommand -v bullyfails (very old caches withoutbin/bully).
See docs/plan.md for the active improvement plan.
- BREAKING:
engine: semanticrules now require a non-emptydescriptionat parse time. The semantic engine sendsdescriptionto the LLM evaluator as the rule prompt, so a missing or empty description meant the rule silently no-op'd — the user thought they were enforcing something but the evaluator received no policy to evaluate against.parse_confignow raisesConfigErrorfor semantic rules wheredescriptionis missing,"", or whitespace-only (caught atbully --validateand at every hook invocation). Non-semantic engines (script,ast,session) still tolerate missing descriptions because their behaviour comes fromscript:/pattern:/when:require:. If you have a semantic rule failing to parse after this update, fill in thedescription— that is the prompt the evaluator was previously running blind.
- Script-engine rules now run with
cwdanchored to the directory containing.bully.yml(the config root) instead of inheriting the bully process's current directory. Affects both the subprocesscwd=and thecapabilities: { writes: cwd-only }HOME/TMPDIR confinement, which previously based its sandbox onos.getcwd(). The PostToolUse hook always chdir's to the config root before invoking bully, so this was a no-op there; the bug surfaced whenbully lint /path/to/file --config /elsewhere/.bully.ymlwas invoked from a third directory — scripts likepnpm lint {file}would resolve project-relative tooling against the wrong directory, andwrites: cwd-onlywould create.bully/tmpoutside the project. Regression coverage intests/test_script_engine.py. bully trustnow surfaces a clear, user-facing diagnostic when the trust store at~/.bully-trust.json(or$BULLY_TRUST_STORE) cannot be written — read-only home, disk full, locked file, or a path that's actually a directory. Previously the unwrappedsave_trust_store(store)call let the underlyingOSError(PermissionError,IsADirectoryError,ENOSPC, etc.) bubble out as a Python traceback, which is hostile UX for a CLI command.cmd_trustnow wraps the save intry/except OSError, printscannot write trust store to <path>: <err>to stderr, and returns rc=1 (matching the other failure paths in the function). The lower-levelsave_trust_storekeeps its stdlib raising semantics. Coverage intests/test_trust_gate.py::test_cmd_trust_unwritable_store_errors_cleanly.
See docs/plan.md for the active improvement plan.
- Repository restructured to a proper
src/layout. The 3,745-linepipeline/pipeline.pymonolith is gone; bully now lives undersrc/bully/and is organized by concern into eight subpackages:config/(parser, loader, scope, skip),engines/(script, ast_grep, output adapters),diff/(context builder, hunk analysis),semantic/(payload assembly, rule-health analyzer),state/(baseline, trust gate, telemetry),runtime/(orchestrator, rule executor, hook I/O),cli/(one file per subcommand), andbench/(fixtures, timing, dispatch, modes). No file in the new tree exceeds 600 lines, so Claude Code can pull in just the relevant area for a task instead of loading the whole pipeline. Tests moved to a top-leveltests/directory (flat); the 34-filesys.path.inserthack in test files is gone. The hook script moved frompipeline/hook.shtohooks/hook.shand now invokespython3 -m bullywithPYTHONPATHset, which lets the plugin run without an editable install. The console-script entry point is nowbully = "bully.cli:main"(waspipeline.pipeline:main). External references —.bully.ymlrule scopes,hooks/hooks.jsonpaths,scripts/lint.sh,scripts/dogfood.sh,.github/workflows/ci.yml, the./bullywrapper — were all updated to match.CLAUDE.mdgained a top-level table of contents covering each path. Backwards compatibility:bully/__init__.pyre-exports the public surface plus underscore-prefixed compat aliases (_parse_scalar,_cmd_trust, etc.) so any out-of-tree code that imported from the oldpipelinenamespace keeps working with a one-linepipeline → bullyrewrite. All 432 tests continue to pass; ruff and shellcheck stay clean; bully still dogfoods itself with no findings.
- Hook semantic-eval contract drift.
_hook_modewas re-renderingadditionalContextfrom the outer (excerpt-stripped)evaluatearray, so<EXCERPT_FOR_RULE>blocks declared withcontext: { lines: N }were silently dropped on the wire and the payload shape didn't match whatskills/bully/SKILL.md(§ "When semantic eval requested") instructs the skill to parse. The dispatcher now forwards the dict thatrun_pipelinealready produced — headerAGENTIC LINT SEMANTIC EVALUATION REQUIRED:followed by JSON withfile/diff/passed_checks/evaluate/_evaluator_input— preserving the per-rule excerpts inside_evaluator_input. New e2e testtest_semantic_eval_emits_dict_payload_with_excerptpins the contract: header prefix, JSON-parseable body, all required keys, survivingcontext.lines, and<EXCERPT_FOR_RULE rule="…">present in_evaluator_input. - Trust gate now fires inside all four hook-handler entry points:
cmd_session_record,cmd_stop,cmd_session_start, andcmd_subagent_stop. Previously, an untrusted.bully.ymlreaching the Stop hook would parse rules, evaluate session-rule violations, write.bully/session.jsonl, and could return exit 2 — a config the user had not yet reviewed could block their session-end. The session-start and subagent-stop paths similarly wrote telemetry and emitted banners on untrusted configs. The PostToolUse path was already gated; the four session-lifecycle subcommands were not. Now every entry point checkstrust_status()before any rule work or filesystem write.cmd_stopemits the standard untrusted stderr message and returns 0; the other three return 0 silently (the hook engine treats non-zero exits as failures, so silent skip is the right shape). Regression coverage intests/test_trust_gate_stop.py(18 tests across all four entry points). - Tightened
rule_add_perspectivefrom substring matching to module-level word-boundary regex (\b(?:avoid|ban|forbid|don'?t)\b|\bno\b(?!-), case-insensitive). With thelen(added) < 2floor removed in the prior change, this filter became more load-bearing; the substring matcher would false-flag descriptions like"banner placement","avoidance count", and"no-op pattern detection"as add-perspective rules and silently skip pure-deletion diffs. Coverage intests/test_cant_match_filters.pyfor false-positive substrings, true-positive whole-word matches, case-insensitivity, and end-to-end throughcan_match_diff. - Documented
pure-deletion-add-perspective-ruleas the fourth live skip reason inskills/bully-author/SKILL.md. - Analyzer's
_read_logis now per-line resilient. The previous list-comprehension[json.loads(line) for line in f if line.strip()]would crash the entirebully reviewinvocation on any single corrupt line — the very tool a user reaches for when telemetry is suspect. Replaced with a per-linetry/except json.JSONDecodeErrorplus anisinstance(rec, dict)filter (mirrors the pattern incmd_stop's session.jsonl reader). Coverage intests/test_analyzer.pyfor truncated, garbage, and valid-JSON-but-non-dict (42,null,[1,2,3]) lines. - Warning-only Stop now resets
.bully/session.jsonl. Previously the unlink was inside thenot violationsbranch, so a session rule withseverity: warningwould emit the same warning on every subsequent Stop until a clean stop occurred. Moved the unlink into thenot blockingbranch — error-severity violations still preserve the file (the user must clear them deliberately) but warnings reset. Coverage intests/test_stop_session_rules.pypinning the clean / warning-only / error matrix. Doc updated atdocs/rule-config.md:40. - Removed the
len(added) < 2floor from the semantic can-match filter. The floor was originally added to suppress noise from unrelated tweaks, but a one-line change likeresult = eval(user_input)is exactly the violation a semantic rule should catch — and the surviving gates (empty-diff, whitespace-only, comment-only, add-perspective pure-deletion) already cover the meaningful zero-signal cases. Updatedtests/test_cant_match_filters.py,test_pipeline.py, andtest_hook_shell.pyto verify single-line semantic violations dispatch. SKILL.md prose atskills/bully-author/SKILL.md:162updated.
- NEW:
bench/eval harness for the four bundled skills (bully,bully-author,bully-init,bully-review). Two eval modes:triggers(does Claude consult the skill given a query?) andexecute(once triggered, is the output correct?), with results graded against assertion lists by an LLM grader and an orthogonal-quality grader (1-5 scoring ontool_efficiency,faithfulness,calibration,instruction_adherence). Workspace layout follows Anthropic'sskill-creatorschemas so artifacts are compatible with the upstream eval-viewer. Multi-turn evals supported via--session-id/--resumeto test skills whose protocol spans multiple user turns. README atbench/README.md. - NEW: per-skill
evals/triggers.json,evals/evals.json, and fixture trees seeded for all four bundled skills. Includes one multi-turnbully-authoreval that exercises the full ask-then-author-then-confirm conversation. - README updates clarifying the "hybrid agent-harness sensor" framing.
skills/bully-author/SKILL.md: fixture-testing protocol now starts with a$BULLYbinary-resolution one-liner (mirrorsbully-init) so the protocol still works whenbullyis not onPATH(the common case for plugin installs that haven't been symlinked). All in-skillbully …invocations updated to$BULLY ….skills/bully-author/SKILL.md: added a MANDATORY step to the fixture-testing protocol —$BULLY --trust --config /tmp/bully-draft.ymlbefore running any lint against the draft. Untrusted configs returnstatus: untrustedwith rules silently skipped, so the lint result was non-load-bearing without this step. Invariants line updated to list trust as a precondition.
- NEW:
session_inittelemetry record. SessionStart writes one stamp per Claude Code session:{"type": "session_init", "bully_version": "0.8.1", "schema_version": 1, "ts": ...}. Lets the analyzer (and forensic readers of older logs) attribute later records back to the bully release that produced them. Cheaper than tagging every record with the version. marketplace.jsonversions and descriptions caught up with the v0.8.0 release (was lagging at 0.7.0 — a v0.8.0 release oversight). Existing installs at <=0.7.0 now see the update notification.
Harness-engineering elevation. Bully is now framed as a hybrid agent-harness sensor (computational + inferential lanes, subagent context firewall, scoped feedforward, session-aware Stop, self-pruning telemetry, capability-scoped script execution) rather than just a "PostToolUse linter".
- License changed from MIT to Apache 2.0. Adds explicit patent grant, requires attribution preservation, and requires marking modifications. Still permissive (no copyleft). New
NOTICEfile ships alongsideLICENSE.
- Analyzer now consumes
semantic_verdictandsemantic_skippedrecords (previously emitted but ignored). Closes the live coherence drift betweendocs/telemetry.md,pipeline/analyzer.py, andskills/bully-review/SKILL.md. format_reportadds askipped=column.bully-reviewSKILL.md no longer claims semantic rules are unobservable.README.mdcorrects the bench description (--fulldoes make real model calls).
- Semantic evaluation payload wraps rule descriptions in
<TRUSTED_POLICY>and the file/diff in<UNTRUSTED_EVIDENCE>, with explicit instructions to the evaluator to treat the latter as data, not directives. - Boundary tags in
_evaluator_inputare sanitized — diffs containing literal</UNTRUSTED_EVIDENCE>or other closing tags are neutralized so user-controlled content cannot break out of the untrusted block. - The
_evaluator_inputfield in the hook payload is now a pre-formatted string (was: dict). The bully skill passes it directly to the evaluator subagent without re-serialization. Breaking for skill consumers: bully harness ≥0.8.0 must be paired with bully skill ≥0.8.0; older skill versions would JSON-encode the string, producing escaped tags. The bundled SKILL.md is updated. - BREAKING (subagent capability): the bully-evaluator subagent no longer has
Read,Grep, orGlobtools. The diff is the only evidence by default. - NEW: rule-level
context: { lines: N }field. When set, the parent harness reads N lines around each diff hunk and bundles them in the payload as<EXCERPT_FOR_RULE>(the substitute mechanism for the removed tools). agents/bully-evaluator.mdrewritten to consume the new boundaries and metadata.- Renamed the dict-returning
build_semantic_payloadtobuild_semantic_payload_dictto disambiguate from the new string-returning helper.pipeline/bench.py'scount_tokensandfull_dispatchnow acceptstr | dictpayloads. - Synthetic line-anchor metadata moves into the
<TRUSTED_POLICY>block.
- NEW:
bully guide <file>andbully explain <file>for scoped feedforward — show rules that apply to a specific file on demand, no generated manual. - NEW:
bully session-startand wiredSessionStarthook entry — agents see a tiny "bully active, N rules" banner at session boot.
- NEW: session-scope rules (
engine: session) — fire at Stop time over the cumulative changed-set instead of per edit. - NEW:
bully stop,bully subagent-stop,bully session-recordsubcommands and matching hook entries (Stop,SubagentStop). - NEW:
.bully/session.jsonlcumulative changed-set (append-only JSONL; race-safe under parallel PostToolUse). - New telemetry record:
{"type": "subagent_stop"}for sub-agent run accounting.
- NEW:
bully coverage [--json]— per-file rule-scope coverage over telemetry. Surfaces uncovered files (zero rules match) and per-file rule lists. - NEW:
agents/bully-scheduler.md— background entropy agent. Wire via/scheduleto run periodically; opens at most one rule-retirement PR per run.
- NEW:
bully debt [--strict]— summarize disable-line markers across the repo, grouped by rule, with optional strict mode that fails on too-short reasons. - NEW: rule-level
capabilities:block (network: false,writes: cwd-only). Declarative, env-based: strips proxy vars, redirects HOME/TMPDIR. Best-effort tripwire, not kernel sandboxing.
Rules that relied on the evaluator using Read/Grep to pull surrounding context will now see only the diff. Add context: { lines: N } to those rules. Audit candidates: rules whose descriptions reference "callsite", "imports", "surrounding code", or anything beyond the literal hunk.
bully harness ≥0.8.0 must be paired with bully skill ≥0.8.0 — older skill versions would JSON-encode the new string _evaluator_input and break dispatch.
- Script output parser swallowed errors from tools with columnar or wrapped output. A live user report on a Laravel package showed phpstan's indented table produced a single
line ?:violation whose description was 500 chars of separator noise truncated mid-identifier ("🪪 mis"). Root cause:parse_script_outputmatched regexes without per-line lstrip, so indented rows fell into the unmatched bucket, then the entire unmatched output was joined and head-truncated to 500 chars — eating the preamble and dropping the signal.- Per-line lstrip before regex match so
11 Method Foo::bar()...matches_LINE_CONTENT. - Stateful continuation-joining: a numbered line opens a violation; following unnumbered, non-separator lines concatenate onto its description (captures phpstan/pest/psalm wrapped messages).
- Table separator rows (
----,====,____) dropped before parsing. - When nothing parses, the tail of unmatched lines is preserved as up to 20 individual violations (each capped at 500 chars) instead of a single head-truncated blob.
- Stderr is now parsed too. Numbered results from either stream are preferred; tails are combined across streams as a last resort.
- End-to-end: phpstan output that previously produced
1 violation, line=None, description="------ ... Illuminate\\Database\\El"now produces3 violations, line=11, line=11, line=18, description=<full error text>.
- Per-line lstrip before regex match so
output: passthroughrule field. Escape hatch for tools whose output format defies the continuation heuristic. Skips structured parsing and emits one violation carrying the tail of stdout+stderr. Opt-in per rule; default unchanged.bully lint --strictflag. For CI callers. Exit non-zero on any non-pass status (untrusted, config error). Default posture stays advisory (exit 0 on untrusted) so the PostToolUse hook never blocks edits on infra issues. Exit codes: 0 pass, 2 blocked, 3 strict-only non-pass.release-bullyskill. Codifies the version-bump + tag + publish flow so future release sessions don't have to rederive which fields move together.
- Violation rendering in blocked stderr drops the
line ?:placeholder. When a violation has no line number, the header is- [rule]: descriptioninstead of- [rule] line ?: description. Removes a rough edge on script rules that run whole-file tools and can't attribute a specific line.
bully-inithardened against real-world config hallucinations. Based on a live install transcript where the skill wrote a.bully.ymlwith a hallucinatedtelemetry:top-level key andexclude:list (the real key isskip), then spent output budget explaining a PostToolUse[FAIL]line that's a known false positive for plugin installs.- New mandatory draft-then-validate protocol: write the proposed
.bully.ymlto a scratch path, runbully --validate --config <path>, onlymvonto.bully.ymlon exit-0. Invalid configs never land on the user's filesystem. - New stack-aware default skip globs table (Python / Node / PHP / Go / Rust / Ruby) so the LLM has a reference instead of inventing per-session.
- New linter precedence guidance: when ruff AND black+isort are installed, default to ruff; when biome AND eslint+prettier, pick one stack -- wiring conflicting tools causes noise.
- Expanded doctor false-positive list to cover (a) PostToolUse-hook-not-in-
settings.json(plugins loadhooks/hooks.jsondynamically) and (b) stale-cache skill/agent version mismatches. Skill now notes them instead of attempting to "fix" them. - New binary-resolution one-liner (
command -v bully || ls -d ~/.claude/plugins/cache/*/bully/*/bully | sort -V | tail -1) that prefers$PATHand falls back to the newest plugin-cache version, avoiding version-pinned paths. - Telemetry is on by default.
/bully-initnow creates.bully/and adds it to.gitignoreunless the user explicitly opts out./bully-reviewhas data to read the first time it runs, instead of reporting "empty log" weeks later.
- New mandatory draft-then-validate protocol: write the proposed
- README and design docs realigned with linter-passthrough routing. The config, engine choice, and init-workflow docs were still pushing the pre-v0.5.0 framing (grep-first, external linters as opt-in). Lead README config example is now a
ruff-checkpassthrough. Added a "Where rules live" section, a cop-vs-lawmakers framing line, and updated/bully-initdescription to mention linter detection and install-on-approval behavior.docs/rule-authoring.mdpriority order rewritten as passthrough → ast → script → semantic.docs/design.mdMigration/Baselines subsections rewritten to treat linter routing as the default, not an opt-in.
- Skills now route rules through installed linters before falling back to bully-owned enforcement. Reframes bully as the cop and native linters (ruff, biome, eslint, tsc, phpstan, rubocop, clippy, …) as the lawmakers: the PostToolUse hook always enforces, but rule definitions should live wherever they express best.
bully-author: replaces the old script/ast/semantic split with a four-option decision tree — linter passthrough → ast → grep → semantic — plus a linter installed-vs-missing pre-flight and an enforcement-guarantee line the skill must say once when recommending a linter, so users don't assume moving a rule into a linter removes it from bully's scope. Review-recommendation table gains a row for "grep rule a linter could cover → passthrough."bully-init: Step 2 rebuilt into three sub-steps — (2) detect installed linters and offer passthrough rules, (2b) offer (never silently install) missing conventional linters with explicit approval, (2c) route project-specific rules (CLAUDE.md sections, arch tests, team conventions) through the same four-option tree.bully-review: two new FYI rows flagging mis-routed rules — grep matching a structural pattern → proposeengine: ast; grep matching something an installed linter covers → propose moving the rule into the linter's config and replacing the bully rule with a passthrough.
- No schema, engine, or hook contract changes. Existing
.bully.ymlconfigs continue to work unchanged — only the skill guidance shifts going forward.
- Critical regression: every rule stopped running on 0.4.0. 0.4.0's hand-rolled
_scope_glob_matches(added inedb362fto provide recursive**on Python 3.10-3.12) anchored the first pattern segment atparts[0], which is"/"for absolute paths. The PostToolUse hook always passes absolute paths, so every**scope missed andfilter_rulesreturned[]for every file. The symptom in a user's.bully/log.jsonlwasrules: []on every entry -- telemetry kept writing"status": "pass"rows but no script / AST / semantic rule ever executed. The fix restores the 0.3.x right-anchored semantic (matchingPurePath.match) by trying the first segment at every path-parts offset while keeping recursive**intact. Reproduced against a real Laravel + Inertia config (pipeline/tests/fixtures/groups4.bully.yml) and pinned with 6 regression tests, including negative cases that guard against over-matching (notapp/,appetite/, wrong extensions).
ruff-format-cleanrule in the dogfood.bully.yml. Closes a gap whereruff check(run by the existingruff-cleanrule) andruff format --checkare separate subcommands: files passing lint but needing reformatting slipped through the in-session hook and tripped CI'sruff format --check .step after push. Now blocks unformatted Python edits before commit.
- Parallel execution of script and AST rules within a single file (
execution.max_workersconfig,BULLY_MAX_WORKERSenv). Defaultmin(8, os.cpu_count() or 4). Setmax_workers: 1to restore serial execution if a rule script needs exclusive access to a resource. Single-rule phases skip the pool and run inline. - Bench (
bench/fixtures): warmwall_p50~38ms vs ~40ms on the prior release (~−5% on the mixed fixture suite). The big-extends-chain fixture goes ~12ms → ~8ms (−37%). Cold start is within noise. - Per-rule
error: truetelemetry field inrule_records(seedocs/telemetry.md), emitted when the rule's evaluator raised a Python exception.
- Behaviour change: a Python exception inside a rule's evaluator (not its shell exit code — an exception from inside bully's rule dispatch) no longer crashes the pipeline. It now becomes a blocking
severity=errorviolation with description"internal error: <ExcType>: <msg>". Exit code and gate behaviour are unchanged (the edit/commit is still blocked), but the output format for this case is different. Shell-level non-zero exits from script rules are unaffected.
bully validate --execute-dry-runruns each script rule against/dev/nullat config time, surfacing shell and regex syntax errors before a hook ever fires.bullyshell wrapper at the repo root so plugin-installed users can alias or symlink it withoutpip install.- New test suites covering YAML escape handling, recursive
**globs, Python version gating, plugin-cache path resolution, and dry-run execution. docs/rule-authoring.mdsection documenting YAML escapes,**compatibility notes, and the--explain/--execute-dry-runflags.
bully-initStep 6 now runs trust, doctor, telemetry-dir setup, and per-rule smoke tests before handing off — previously it dropped users at a config that might silently fail.doctorresolves skills and the semantic-evaluator agent from either the legacy~/.claude/skills/...path or the plugin cache (~/.claude/plugins/cache/*/bully/*/{skills,agents}/...).ruff-cleanrule in the dogfood config prefersruffonPATHbut now falls back to.venv/bin/ruffso Claude Code hooks (which don't inherit the activated venv) catch violations locally instead of only in CI.
- YAML parser silently miscompiled escapes.
_parse_scalarnow processes double-quoted escapes (\\,\",\n,\t,\r,\/,\0) and single-quoted''. Previously"\\."stayed as two backslashes, so every shippedexamples/rules/*.ymlpattern exited 0 on grep's "parentheses not balanced" error instead of matching. filter_rulesnow handles**recursively on Python 3.10–3.12 via a custom_scope_matches.PurePath.matchonly gained recursive**in 3.13, so older versions were silently skipping deep paths.doctoractually comparessys.version_infoagainst 3.10 instead of unconditionally printing[OK] Python X.Y >= 3.10.- Three
ruffSIM violations (SIM110,SIM102,SIM108) inpipeline.pythat CI caught but the hook'sruff-cleanrule missed becauseruffwasn't onPATHin the hook env. pipeline/tests/test_validate_cli.pyformatting to satisfyruff format --checkin CI.
bully benchcommand for measuring rule-suite performance: Mode A runs a fixture suite and writes per-run history, Mode B analyzes configured cost,--comparediffs the last two runs, and--fullcalls realmessages.createto record output tokens and actual dollar cost.- 8 bench fixtures covering script, ast, and semantic engines.
phase_timerhook onrun_pipelineso callers can observe per-phase latency without patching internals.ruff-cleanrule in the dogfood.bully.yml(tolerates a missingruffbinary rather than erroring).- CI runs on every push, not just PRs.
- README tagline: "Bully doesn't" → "Bully enforces".
bully bench --configand--compareare now mutually exclusive.- Semantic-evaluator pipeline short-circuits token counting when no semantic rules match.
- Dogfooded new ast rules across the repo.
- ruff
F841and formatting violations on P1 epic test files that were blocking CI.
- Two-phase lint pipeline (script + semantic rules).
- PostToolUse hook integration for Claude Code.
- Four skills: bully, bully-init, bully-author, bully-review.
- Semantic evaluator subagent with strict VIOLATIONS/NO_VIOLATIONS output contract.
- Opt-in JSONL telemetry (
.bully/log.jsonl). - Laravel example rule pack.
- Dogfood config (
.bully.yml) enforcing stdlib-only runtime, strict bash mode, and more.