Skip to main content

Quality enforcement for AI-assisted development — ruff + semgrep + incident-derived rules for Claude Code

Project description

Fettle

PyPI CI Python License: MIT

fettle (v.) — foundry term: to trim and clean a rough casting fresh from the mold. "In fine fettle" — in excellent condition.

The quality harness for AI-generated code — enforcement at the moment of creation, not days later in code review.

AI agents write code at machine speed; your review process still runs at human speed. Every linter, CI gate, and review bot you already have fires after the agent session that produced the bug has ended — the context is gone, and the same antipattern gets written again tomorrow. Fettle hooks the agent's own tool calls (Claude Code, OpenCode) and runs static analysis, process gates, and incident-derived LLM-antipattern rules on every edit, inside the session, where the agent can still fix it with full context.

  agent writes code ─▶ Fettle gate fires ─▶ finding lands in-session ─▶ agent fixes it
       (ms)              (≤400ms budget)        (same context)           (immediately)

60-Second Start

git clone https://github.com/MilindGaharwar/fettle ~/projects/fettle
cd ~/projects/fettle && python3 fettle/cli.py init --install-tools
fettle doctor        # verify — hooks are live in your next agent session

CLI-only (hooks need the checkout): pipx install finefettle

Status: v1.3.0 “Evidence Loop” — living specs with scenario-coverage gating, agent worktree infrastructure with work-item claims, agentic UAT with reconciled verdicts, an on-demand semantic link graph (req → scenario → test → verdict), functional-test verification, and a remote-CI verification gate — a pushed commit is not done until its remote verdict is green. On top of v1.2's real package namespace, agent abstraction, validated config schema, and CI parity across GitHub/GitLab/pre-commit. Roadmap: docs/ROADMAP.md.

What It Does

flowchart LR
    subgraph agents ["AI Agents"]
        CC[Claude Code]
        OC[OpenCode]
    end
    subgraph fettle ["Fettle"]
        T["fettle.agents<br/>translators"] --> D["Dispatcher<br/>latency-budgeted"]
        D --> G["30+ gates<br/>lint · TDD · complexity · lean<br/>destructive · MCP trust · coverage"]
        G --> A["Audit trail<br/>versioned JSONL"]
    end
    subgraph chokepoints ["Same policy everywhere"]
        P[pre-commit] ~~~ CI["GitHub / GitLab CI"] ~~~ L[LSP / editor]
    end
    CC -- "hook events" --> T
    OC -- "native events" --> T
    fettle -. "one .fettle.toml" .-> chokepoints
Layer Hook What runs
Per-edit lint PostToolUse (Write/Edit) ruff + semgrep on every Python edit
TDD ordering PreToolUse + PostToolUse Test-before-implementation enforcement (v0.9)
Complexity PostToolUse (Write/Edit) Cyclomatic + cognitive per modified function (v0.9)
Lean review PostToolUse (Write/Edit) Over-engineering detection: abstractions, wrappers, large additions (v0.8)
Pre-write gate PreToolUse (Write/Edit) Plan gate, config protection, UX spec gate
MCP trust PreToolUse (Bash) Package install allowlist
Artifact integrity PreToolUse (Bash) Destructive command guard
Doc freshness PostToolUse (Bash) Warns if implementation changed but no docs updated
Bash audit PostToolUse (Bash) Structured event logging, privacy-first (v0.8)
Cross-file Stop Import/contract resolution before response delivery
Coverage gate Stop Diff line + branch coverage from coverage.json (v0.8/v0.9)
Discipline link PostToolUse Injects skill reminders when loop/scope/lean gates fire (v0.8)

Why Fettle

Fettle occupies a gap none of the adjacent tool categories cover: enforcement at the moment AI generates code, not minutes or days later.

Category When it acts What it misses
Linters & SAST (ruff, semgrep, SonarLint) On demand / in editor Not wired into agent sessions; no process enforcement; you configure and run them yourself
Commit hooks (pre-commit, Husky) At commit time Bad code already sits in the working tree; agents iterate dozens of edits per commit
CI quality gates (SonarQube, CodeQL) At push/PR time Feedback arrives after the agent session ended — the context that produced the bug is gone
AI code-review bots At PR time Review comments, not enforcement; nothing stops the pattern from being written again

Fettle hooks the agent's own tool calls (PreToolUse/PostToolUse/Stop), so the finding lands inside the session that caused it, where the agent can still fix it with full context.

The governance model is Human in Control, not Human in the Loop: you set policy once (.fettle.toml, advisory → enforce per gate) and retain full authority over outcomes without approving every step. Gates are automatable, agent-assessable checks — never manual approval meetings. Fail-open design and strict latency budgets mean enforcement never becomes the bottleneck. (The AI-Native Large-Scale Agile Manifesto, arXiv:2605.07717, names exactly this model — Fettle is the assurance layer for it.)

What's genuinely different

  • Incident-derived rules. /fettle:learn turns a real production incident into a semgrep rule with test fixtures and an incident citation. The rules catalog isn't a generic style guide — every LLM-antipattern rule traces to something that actually broke.
  • Noise is a measured budget, not a hope. fettle bench tracks findings-per-KLOC against committed budgets; rules are promoted advisory → enforce (and demoted back) based on evidence via fettle ratchet. A quality tool that doesn't measure its own false-positive rate becomes ignored wallpaper.
  • Process gates, not just pattern matching. TDD ordering, plan-before-edit, diff coverage, complexity ceilings, over-engineering (lean) review, destructive-command guard, and an MCP package supply-chain gate — the engineering discipline around the code, not only the code.
  • One policy, every chokepoint. The same .fettle.toml drives agent hooks, the CLI, pre-commit, CI (GitHub Action + SARIF), and the LSP server. No drift between what the editor warns about and what CI blocks.
  • Fail-open by design. Hooks run under strict latency budgets and never crash or hang an agent session over an environment problem — enforcement degrades visibly (doctor, trace log) instead of breaking your flow.
  • Suppressions with expiry and owner. Every suppression carries a reason, an owner, and an expiry date — expired suppressions resurface as findings instead of rotting silently.
  • It polices its own development. Fettle's commit-time guards blocked two of Fettle's own commits during recent development (an intentional-fixture scan hit and a broad-except in new code). The harness that doesn't pass its own bar doesn't ship.

Enterprise Operations (v1.3 arc, shipping now)

Capability How
Central policy [extends] in .fettle.toml layers a digest-pinned org policy under repo config — content-addressed (sha256 verified on fetch and every cache read), cache-only in hooks (zero network in the hook path), offline-safe. fettle policy sync|status
Audit trail Every gate decision logged to versioned, append-only JSONL with repo attribution — prove what was enforced, when, where
Org reporting fettle report --org rolls up decisions/violations/blocks per repo for platform teams
CI dashboards SARIF (GitHub code scanning) + JUnit XML (fettle check --junit — GitLab, Jenkins, Azure DevOps)
Config governance Published JSON Schema, fettle config --validate with typo-catching unknown-key warnings — orgs review a schema, not source code
Supply-chain stance Tokenless releases (PyPI Trusted Publishing/OIDC), pinned tool installs only on explicit user action — hooks never install anything

Intelligence Layer (v0.3.0+)

Feature Command Description
Learn /fettle:learn Incident text → LLM-generated semgrep rule + fixtures + citation
Explain /fettle:explain Why did the last hook block? Human-readable trace
Baseline /fettle:baseline Snapshot violations for incremental adoption
Report /fettle:report Effectiveness metrics (pass/violation rates, top violations)

Rules Catalog (semgrep)

Rule Severity Catches
regex-llm-output ERROR Regex-parsing LLM output instead of structured tool use
bare-except-swallow ERROR except: pass swallowing all errors
broad-except-no-reraise ERROR except Exception without re-raise or logging
missing-httpx-timeout ERROR httpx clients without timeouts
sql-fstring ERROR SQL built with f-strings (injection)
health-score-inversion ERROR Health checks returning perfect on no data
orphaned-queue-flag ERROR Queue writes with no verified consumer
datetime-now-pipeline WARNING datetime.now() in pipeline code (breaks backfill)
non-atomic-write-output WARNING Non-atomic writes in pipeline output paths

Plus ruff: BLE001, S110, S608, S701 as errors; SIM*, UP* as warnings.

Installation

# Clone (agent hooks run from the checkout)
git clone https://github.com/MilindGaharwar/fettle ~/projects/fettle
cd ~/projects/fettle

# One command wires everything: repo config, Claude Code plugin symlink,
# OpenCode plugin registration, commit-time guards — idempotent.
python3 fettle/cli.py init --install-tools

# Verify
fettle doctor

fettle init detects which agents are installed and only wires those; add --dry-run to preview. Hooks auto-activate via hooks/hooks.json once the plugin symlink exists. OpenCode details: docs/OPENCODE.md.

The CLI is also on PyPI — the fettle name belongs to an unrelated project, so the package is finefettle (“in fine fettle”); the command is still fettle:

pipx install finefettle
fettle doctor

(Agent hooks require the git checkout — wheels ship the CLI and rules only; fettle init will tell you if a checkout is needed.)

CLI

fettle init [--install-tools] [--dry-run]
fettle check [--all] [--changed] [--json] [--fix] [--baseline] [--junit FILE]
fettle config --print-effective
fettle config --explain
fettle config --validate
fettle policy sync|status
fettle report [--org] [--days N]
fettle explain [--last N]
fettle spec [lint|list|coverage] [--json]
fettle worktree [create|list|remove] <item-id>
fettle work [list|claim|release] [item-id]
fettle uat doctor [--json]        # surface detection + capability probe
fettle uat run --surface S --yes  # agentic UAT session → verdicts
fettle uat report --worktree PATH # re-reconcile a past session
fettle uat manual                 # human walkthrough from spec scenarios
fettle uat attest <spec/Sn> --outcome ... --observed ...
fettle links <id> [--json]        # semantic layer: everything attached to an id
fettle links --orphans            # broken evidence chains (req→scenario→test/UAT)
fettle verify [--full] [--json]   # run the test suite, record verification stamp
fettle ci status [--json]         # remote CI verdict for HEAD (gh, REST fallback)
fettle ci wait [--json]           # poll remote CI to completion, record stamp
fettle baseline create|update
fettle doctor
fettle lsp

fettle check flags:

Flag Effect
--changed Scan only git-changed Python files (staged, unstaged, untracked)
--fix Apply safe ruff autofixes before scanning
--baseline Report only findings not in .fettle-baseline.json
--json Machine-readable output (same exit codes as text mode)
--all Scan the whole tree (default; conflicts with --changed)

Exit codes: 0 no error-severity findings · 1 error findings present · 2 usage or environment error. Identical for text and --json output — safe to gate CI on.

GitHub Actions

Use the composite Action at the same ref as your workflow:

- uses: MilindGaharwar/fettle@main
  with:
    mode: advisory

For centralized adoption, call .github/workflows/fettle-reusable.yml; both surfaces support SARIF and pull request annotations. Pin a release tag instead of main for stable CI.

Slash Commands (12)

Command Purpose
/fettle:quality Full project scan
/fettle:preflight Pre-deployment FMEA checklist
/fettle:ops-review Operational readiness review
/fettle:plan-activate Start a plan (required before edits in enforce mode)
/fettle:plan-complete Mark plan done
/fettle:mcp-approve Approve an MCP package
/fettle:mcp-revoke Revoke MCP package trust
/fettle:learn Generate rule from incident
/fettle:explain Explain last hook decision
/fettle:baseline Manage violation baselines
/fettle:report Effectiveness metrics

Configuration

.fettle.toml at project root. Full reference: docs/CONFIG.md.

[gates.lint]
enabled = true
mode = "advisory"   # advisory | soft | enforce

[gates.lean_review]
mode = "advisory"   # silent | advisory — surfaces over-engineering findings (v0.8)

[gates.complexity]
enabled = true
mode = "advisory"               # advisory | enforce (replaces the old `enforce` bool)
max_cyclomatic = 10
max_cognitive = 15

[gates.coverage]
enabled = false
threshold = 80                  # Line coverage % for changed lines
minimum_branch_percent = 0      # Branch coverage (0 = disabled)

[gates.tdd]
enabled = false
mode = "advisory"               # advisory only in v0.9
accept_preexisting_tests = true

[gates.bdd]
enabled = false
mode = "advisory"               # advisory | enforce — spec scenario coverage (Stage 3)

[gates.claims]
enabled = false
mode = "advisory"               # claim-before-work in fettle worktrees (Stage 4)

[gates.verify]
enabled = false
mode = "advisory"               # advisory | enforce — fresh green `fettle verify` stamp at Stop

[gates.ci]
enabled = false
mode = "advisory"               # advisory | enforce — pushed this session ⇒ remote CI verified green

[gates.plan]
enabled = false
threshold = 3                   # Files changed before plan required
risk_paths = []                 # Globs that auto-require plan (e.g. "**/auth/**")
module_threshold = null         # Distinct packages, null = disabled
line_threshold = null           # Added lines, null = disabled

[gates.bash_audit]
enabled = false                 # Privacy-first: opt-in only
capture_command = false         # If true, applies redaction before logging

[gates.advisory]
cooldown_seconds = 300
max_per_turn = 3

[gates.discipline_link]
enabled = true
cooldown_seconds = 300

[severity]
error_rules = ["BLE001", "S110", "S608", "S701"]
warning_prefixes = ["SIM", "UP"]

Architecture

Claude Code Tool Call
    │
    ▼
PreToolUse ──→ dispatcher.py selects checks by event + tool + extension:
             → quality_gate (plan, UX spec)
             → tdd_gate (test-first ordering)
             → config_protect, destructive_guard
             → mcp_trust_gate (Bash only)
    │
    ▼ (tool executes)
    │
PostToolUse ──→ dispatcher.py:
              → post_edit (ruff + semgrep on .py)
              → post_edit_ts, post_edit_go (language-specific)
              → complexity_check (cyclomatic + cognitive)
              → lean_sniffers (over-engineering detection)
              → bash_audit (structured event logging)
              → tdd_gate (records test/impl edits)
              → bdd_gate (spec scenario coverage)
              → claims_gate (claim-before-work in worktrees)
              → loop_detect + scope_creep + discipline_link
    │
    ▼
Stop ──→ dispatcher.py:
       → quality_gate (test freshness)
       → stop_quality_gate (imports + cargo check)
       → coverage_gate (line + branch coverage)

All checks route through dispatcher.py (single process, per-check budget, advisory cap). 17 checks registered, ordered by priority, fail-open on error.

Result Taxonomy

Every hook returns one of:

Status Meaning User action
PASS No issues None
VIOLATION Code quality issue Fix the code
TOOL_ERROR ruff/semgrep missing or crashed Run fettle doctor
CONFIG_ERROR Invalid .fettle.toml Fix config
SKIPPED File not in scope None

Key Design Principles

  1. Advisory by default — opinionated gates default off; lint is advisory; every block names its disable key
  2. Fail visible — tool crashes surface as warnings, never as silent passes
  3. Rules carry receipts — every rule has origin + citation; /fettle:learn rules cite their incident
  4. Single config source.fettle.toml, no scattered env vars
  5. No shared global state — per-session state dirs

Extensibility

Check Registry (fettle/dispatcher_registry.py)

Every gate is a CheckSpec (name, events, matcher, budget, run function) in one declarative table; the dispatcher selects and runs applicable checks per hook event under per-check and per-event latency budgets.

Agent Translators (fettle/agents/)

from fettle.agents import detect_agent, normalize
hook_input = normalize(payload, fallback_cwd=os.getcwd())
# Claude Code hook JSON and native OpenCode plugin events both normalize
# to the same event model — conformance-tested per agent (WP-140).

Language Adapters (fettle/adapters/)

Python, TypeScript, Rust, and Go adapters implement a common protocol (get_adapter(extension)); external tools (SonarQube, Black Duck, Pact) follow the IntegrationAdapter protocol with fail-open/fail-closed policy per integration.

Event Model (fettle/event.py)

event = FettleEvent.from_stdin(HookType.POST_TOOL_USE)
# → typed, normalized: event.is_python, event.file_extension, event.repo_root

Result Caching (fettle/cache.py)

Cache key = file content hash + config hash. Skips re-scanning unchanged files.

Testing

cd ~/projects/fettle
.venv/bin/python -m pytest tests/ fettle/tests/ -q

1150+ tests across 120+ test files covering all checks, adapters, agent translators, and infrastructure. All adapter tests use mocked tool outputs — no eslint, biome, tsc, cargo, or semgrep installation required to run the suite.

Roadmap

Version Theme Status
v0.2.0 Core lint gates Shipped
v0.3.0 Process gates + intelligence foundation Shipped
v0.4.0 TS/JS rules, cross-review, SARIF, caching, autofix, checker protocol Shipped
v0.5.0 Adaptive enforcement platform Shipped
v0.6.0 Trust and precision Shipped
v0.7.0 Action, LSP, policy layering, OpenCode adapter Shipped
v0.8.0 Discipline integration (advisory contract, link pilot, budget, audit, coverage) Shipped
v0.9.0 Engineering discipline enforcement (branch coverage, complexity, plan thresholds, TDD) Shipped
v1.0.0 Enterprise integration (security review, threat model, deploy safety, adapters, SWEBOK gaps) Shipped
v1.0.1 Trustworthy core (audit fixes D1–D9, exit-code contract, --version) Shipped
v1.0.2 finefettle on PyPI, Trusted Publishing, commit-time guards Shipped
v1.2.0 Independence: package restructure, agent abstraction, fettle init, config schema Shipped
v1.3.0 Evidence Loop: central policy, org reporting, living specs + BDD gate, agent worktrees, agentic UAT, semantic links, verify + remote-CI gates Shipped

See docs/ROADMAP.md for remaining governance and distribution work.

License

MIT (c) Milind Gaharwar

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

finefettle-1.3.0.tar.gz (396.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

finefettle-1.3.0-py3-none-any.whl (293.0 kB view details)

Uploaded Python 3

File details

Details for the file finefettle-1.3.0.tar.gz.

File metadata

  • Download URL: finefettle-1.3.0.tar.gz
  • Upload date:
  • Size: 396.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for finefettle-1.3.0.tar.gz
Algorithm Hash digest
SHA256 8453a828d306017a3452eb89d2a82f5a5fa77a5ee58b5dd8fa248d1b39259ded
MD5 27a8dc6d9c75de7c0f1788821d40d36f
BLAKE2b-256 86de78fa0e2932c6e3f24ebc89d306d2aa65f0157dd4479748495653270d9074

See more details on using hashes here.

Provenance

The following attestation bundles were made for finefettle-1.3.0.tar.gz:

Publisher: release.yml on MilindGaharwar/fettle

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file finefettle-1.3.0-py3-none-any.whl.

File metadata

  • Download URL: finefettle-1.3.0-py3-none-any.whl
  • Upload date:
  • Size: 293.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for finefettle-1.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c5b246e8618dc359fc2f7849fbced0252be11c1741c87f1757de17c4c7c48f2f
MD5 4b9e7d035516f12f7422d56d7c62b565
BLAKE2b-256 a99b3df0c973d8f005ace6aa0d311ec8be2ddc2ee297b0a1afca6b061c4c4158

See more details on using hashes here.

Provenance

The following attestation bundles were made for finefettle-1.3.0-py3-none-any.whl:

Publisher: release.yml on MilindGaharwar/fettle

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page