diff --git a/.editorconfig b/.editorconfig
index 7add053..bfa467e 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -10,5 +10,9 @@ max_line_length = 120
[*.py]
indent_size = 4
+[*.rs]
+indent_size = 4
+max_line_length = off
+
[{CLAUDE.md,.editorconfig,super-linter.env,lychee.toml,renovate.json5,default.json,mise.toml}]
max_line_length = 300
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6313b56
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
diff --git a/.github/agents/knowledge/README.md b/.github/agents/knowledge/README.md
new file mode 100644
index 0000000..978136e
--- /dev/null
+++ b/.github/agents/knowledge/README.md
@@ -0,0 +1,14 @@
+# Knowledge Index
+
+Reusable repository guidance for agents working on flint v2.
+
+Load only files relevant to the current scope.
+
+## Topics
+
+| File | Load when |
+| ----------------- | --------------------------------------------------------------------------------- |
+| `architecture.md` | Navigating the codebase; understanding module roles or check kinds |
+| `linters.md` | Adding, modifying, or debugging a linter; `registry.rs` changes; config injection |
+| `design.md` | Questioning why something works the way it does; avoiding known pitfalls |
+| `testing.md` | Writing or updating tests; adding fixture cases; regenerating snapshots |
diff --git a/.github/agents/knowledge/architecture.md b/.github/agents/knowledge/architecture.md
new file mode 100644
index 0000000..6248d42
--- /dev/null
+++ b/.github/agents/knowledge/architecture.md
@@ -0,0 +1,38 @@
+# Architecture
+
+## Module Map
+
+- **`src/registry.rs`**: Static linter registry. Defines
+ `Check` (builder pattern) and `builtin()` which returns
+ the full list of built-in checks. This is where new
+ linters are added.
+- **`src/runner.rs`**: Executes checks against a file list.
+ Handles parallel execution (check mode) and serial
+ execution (fix mode, to avoid concurrent writes).
+- **`src/config.rs`**: Loads `flint.toml` from the project
+ root. All fields have defaults — the file is optional.
+- **`src/files.rs`**: Git-aware file discovery. Returns
+ changed files relative to the merge base, or all files
+ with `--full`.
+- **`src/linters/`**: Custom logic for special checks that
+ can't be expressed as a simple command template:
+ - `lychee.rs`: Link checking orchestration
+ - `renovate_deps.rs`: Renovate snapshot verification
+- **`src/main.rs`**: CLI parsing (clap), orchestration,
+ output formatting.
+- **`tests/e2e.rs`**: End-to-end tests. Spin up a temp git
+ repo, write files, run the flint binary, assert on
+ stdout/stderr and exit code.
+
+## Check Kinds
+
+A `Check` is either a `Template` (a command string with
+`{FILE}`, `{FILES}`, or `{MERGE_BASE}` placeholders) or a
+`Special` (custom Rust logic in `src/linters/`).
+
+Template scopes:
+
+- `File` — invoked once per matched file (`{FILE}`)
+- `Files` — invoked once with all matched files (`{FILES}`)
+- `Project` — invoked once with no file args; skipped
+ entirely if no matching files changed
diff --git a/.github/agents/knowledge/design.md b/.github/agents/knowledge/design.md
new file mode 100644
index 0000000..e5568ad
--- /dev/null
+++ b/.github/agents/knowledge/design.md
@@ -0,0 +1,48 @@
+# Key Design Decisions
+
+1. **Activation via `mise.toml`**: A check is active when
+ its tool (or `mise_tool_name` override) is declared in
+ the consuming repo's `mise.toml`. No PATH probing —
+ mise guarantees declared tools are on PATH.
+
+2. **`editorconfig-checker` deference**: `editorconfig-checker`
+ (binary: `ec`) runs on all files but skips file types owned
+ by active line-length-enforcing formatters (`cargo-fmt`,
+ `ruff-format`, `biome-format`, `prettier`). Implemented
+ via `.defer_to_formatters()` on the `editorconfig-checker`
+ entry. This avoids its `max_line_length` check conflicting
+ with formatter output.
+
+3. **markdownlint + prettier on `*.md`**: Both checkers are
+ active when their tools are installed. They cover
+ different concerns (markdownlint: structural rules;
+ prettier: formatting). To avoid MD013 (line length)
+ conflicting with prettier's line wrapping, consuming
+ repos must disable MD013 in `.markdownlint.json`:
+
+ ```json
+ { "MD013": false }
+ ```
+
+4. **Fix mode runs serially**: `runner.rs` runs checks in
+ parallel in check mode, but serially in fix mode to
+ avoid concurrent writes to the same file.
+
+5. **Version ranges**: When a `bin_name` has any
+ `version_range` entries, every entry for that binary
+ must have one (enforced by a registry unit test). This
+ prevents ambiguous activation when ranges don't cover
+ all versions.
+
+6. **Special checks**: `links` and `renovate-deps` have
+ custom orchestration logic that doesn't fit the command
+ template model. Their implementations live in
+ `src/linters/`.
+
+7. **Built-in file exclusions**: `src/files.rs` has a
+ `BUILTIN_EXCLUDES` slice of paths that are always removed
+ from the file list before any linter sees it. Currently
+ contains `.github/renovate-tracked-deps.json` (a
+ generated file that should never be linted by prettier,
+ ec, etc.). Add entries here — not in user-facing `exclude`
+ docs — when a file is managed by flint itself.
diff --git a/.github/agents/knowledge/linters.md b/.github/agents/knowledge/linters.md
new file mode 100644
index 0000000..ae139fb
--- /dev/null
+++ b/.github/agents/knowledge/linters.md
@@ -0,0 +1,80 @@
+# Adding a New Linter
+
+Add an entry to `builtin()` in `src/registry.rs` using the
+builder pattern:
+
+```rust
+// File scope — invoked per file
+Check::file("mytool", "mytool --check {FILE}", &["*.ext"])
+ .fix("mytool --fix {FILE}"),
+
+// Files scope — invoked once with all matched files (absolute paths)
+Check::files("mytool", "mytool {FILES}", &["*.ext"])
+ .fix("mytool --fix {FILES}"),
+
+// Files scope — invoked once with all matched files (relative to project root)
+// Use {RELFILES} when the tool requires paths relative to the project root
+// (e.g. dotnet format --include).
+Check::files("mytool", "mytool --include {RELFILES}", &["*.ext"])
+ .fix("mytool --fix --include {RELFILES}"),
+
+// Project scope — invoked once, skipped if no *.ext changed
+Check::project("mytool", "mytool run", &["*.ext"]),
+```
+
+Available builder modifiers:
+
+| Method | Purpose |
+| ---------------------------- | ----------------------------------------------------------------------------- |
+| `.fix(cmd)` | Enable `--fix` mode with this command |
+| `.bin(name)` | Override binary name (when check name ≠ binary) |
+| `.mise_tool(name)` | Look up availability under a different mise key (e.g. `rust` for `cargo-fmt`) |
+| `.version_req(range)` | Restrict to a semver range (e.g. `">=1.0.0"`) |
+| `.excludes(names)` | Skip files already owned by these active checks |
+| `.slow()` | Mark as slow — skipped by `--fast-only` |
+| `.linter_config(file, flag)` | Inject a config flag when `FLINT_CONFIG_DIR/` exists (see below) |
+
+## Config File Injection (`.linter_config`)
+
+Use `.linter_config(filename, flag)` when the tool supports an explicit config
+file path via a CLI flag. At runtime, if `FLINT_CONFIG_DIR/` exists,
+flint injects `flag ` right after the binary name in the command.
+If the file is absent the flag is silently omitted — native config discovery
+remains in effect.
+
+```rust
+// Example: markdownlint accepts --config
+Check::file("markdownlint", "markdownlint {FILE}", &["*.md"])
+ .fix("markdownlint --fix {FILE}")
+ .linter_config(".markdownlint.json", "--config"),
+// → markdownlint --config /repo/.github/config/.markdownlint.json
+```
+
+**When NOT to use it:**
+
+- The tool has no explicit `--config`/`--rcfile`/equivalent flag (e.g. `shfmt`)
+- The flag accepts a **directory** rather than a file (e.g. biome's
+ `--config-path `) — a different injection shape is needed. For biome,
+ check for `biome.json` existence but pass `config_dir` itself as the arg:
+ `biome --config-path check `. This requires a variant of
+ `.linter_config` that injects the directory rather than the full file path
+ (not yet implemented)
+- The tool is project-scoped and its config must live at the project root to
+ function (no explicit `--config` flag exists)
+
+Look up the tool's `--help` or man page for the config flag name and expected
+argument type before adding `.linter_config`.
+
+For checks that need custom logic (not a simple command template), add a module
+under `src/linters/` and use `CheckKind::Special`.
+
+## Changed-files scoping
+
+Most linters use `file` or `files` scope, so they naturally receive only changed
+files as arguments. `golangci-lint` uses `project` scope but scopes internally via
+`--new-from-rev={MERGE_BASE}`.
+
+**`cargo-clippy` cannot scope to changed files.** Cargo has no git-aware flag
+equivalent to `--new-from-rev`. It still skips entirely when no `*.rs` files
+changed, but when it does run it checks the whole project. Workspace support
+(`-p --no-deps` per changed package) would be a future improvement.
diff --git a/.github/agents/knowledge/testing.md b/.github/agents/knowledge/testing.md
new file mode 100644
index 0000000..f5c90e7
--- /dev/null
+++ b/.github/agents/knowledge/testing.md
@@ -0,0 +1,60 @@
+# Testing
+
+Run all tests with:
+
+```bash
+cargo test
+```
+
+## Unit Tests
+
+In-module `#[cfg(test)]` blocks in `src/`. Notable:
+
+- `src/registry.rs`: enforces version-range consistency
+- `src/runner.rs`: config injection, scope filtering
+- `src/linters/renovate_deps.rs`: log parsing, snapshot
+ read/write, diff output
+
+## Fixture-based E2E Tests
+
+`tests/cases/` holds one directory per scenario. Each
+contains:
+
+- `files/` — files copied verbatim into a temp git repo
+ and staged before the run
+- `test.toml` — test spec:
+
+```toml
+[expected]
+args = "--full shellcheck"
+exit = 1 # optional, default 0
+stderr = """
+...golden output...
+"""
+
+[expected.files] # optional: assert files written by --fix
+".github/renovate-tracked-deps.json" = """
+{...}
+"""
+
+[env] # optional extra env vars
+FOO = "bar"
+
+[fake_bins] # optional fake binaries (Unix only)
+renovate = '''
+#!/bin/sh
+echo '...'
+'''
+```
+
+The `cases` test in `tests/e2e.rs` runs all of them.
+Set `UPDATE_SNAPSHOTS=1` to regenerate `[expected].exit`/
+`stderr`/`stdout` in place. `[expected.files]` and `[fake_bins]`
+are always preserved by the snapshot writer.
+
+Use fixture cases for any check — including ones that require
+fake external binaries (via `[fake_bins]`). The fixture runner
+writes each binary into a tempdir and prepends it to `PATH`.
+
+When adding a new check, cover at least: clean pass, failure
+with correct diff/output, and fix mode if supported.
diff --git a/.github/config/flint.toml b/.github/config/flint.toml
new file mode 100644
index 0000000..b3f85fc
--- /dev/null
+++ b/.github/config/flint.toml
@@ -0,0 +1,5 @@
+[settings]
+exclude = ["CHANGELOG.md", "tests/cases/**"]
+
+[checks.renovate-deps]
+exclude_managers = ["github-actions", "github-runners", "cargo"]
diff --git a/.github/renovate-tracked-deps.json b/.github/renovate-tracked-deps.json
index 212d103..c579bc9 100644
--- a/.github/renovate-tracked-deps.json
+++ b/.github/renovate-tracked-deps.json
@@ -4,19 +4,40 @@
"mise"
]
},
- "README.md": {
+ ".github/workflows/test.yml": {
"regex": [
- "grafana/flint"
+ "mise"
]
},
"mise.toml": {
"mise": [
+ "actionlint",
+ "cargo:xmloxide",
+ "dotnet",
+ "editorconfig-checker",
+ "github:google/google-java-format",
+ "github:koalaman/shellcheck",
+ "github:mvdan/sh",
+ "github:pinterest/ktlint",
+ "go",
+ "golangci-lint",
+ "hadolint",
"lychee",
"node",
- "npm:renovate"
- ],
+ "npm:@biomejs/biome",
+ "npm:markdownlint-cli2",
+ "npm:prettier",
+ "npm:renovate",
+ "pipx:codespell",
+ "pipx:ruff",
+ "rust"
+ ]
+ },
+ "src/init/generation.rs": {
"regex": [
- "ghcr.io/super-linter/super-linter"
+ "actions/checkout",
+ "jdx/mise-action",
+ "mise"
]
}
}
diff --git a/.github/renovate.json5 b/.github/renovate.json5
index 90c7bf0..2ba5ba0 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -5,7 +5,7 @@
dependencyDashboard: true,
platformCommit: "enabled",
automerge: true,
- ignorePaths: [],
+ ignorePaths: ["tests/"],
ignorePresets: [":ignoreModulesAndTests"],
ignoreUnstable: true,
vulnerabilityAlerts: {
@@ -38,6 +38,33 @@
depNameTemplate: "mise",
matchStrings: ["jdx/mise-action.*\\n\\s*with:\\s*\\n\\s*version: [\"']?(?v[.\\d]+)[\"']?\\s*\\n\\s*sha256: [\"']?(?\\w+)[\"']?"],
},
+ {
+ customType: "regex",
+ description: "Update GitHub Action SHA pins embedded in the generated workflow template (src/init/generation.rs)",
+ managerFilePatterns: ["/^src/init/generation\\.rs$/"],
+ matchStrings: ["uses: (?[^@\\\\]+)@(?[a-f0-9]{40})\\s*#\\s*(?v[^\\s'\"\\\\]+)"],
+ datasourceTemplate: "github-tags",
+ },
+ {
+ customType: "regex",
+ description: "Update mise per-platform sha256 hashes in test.yml matrix",
+ managerFilePatterns: ["/.github/workflows/test.yml/"],
+ datasourceTemplate: "github-release-attachments",
+ packageNameTemplate: "jdx/mise",
+ depNameTemplate: "mise",
+ matchStrings: [
+ "mise_version: [\"']?(?v[.\\d]+)[\"']?\\s*\\n\\s*mise_sha256: [\"']?(?[a-f0-9]{64})[\"']?",
+ ],
+ },
+ {
+ customType: "regex",
+ description: "Update mise version embedded in the generated workflow template (src/init/generation.rs)",
+ managerFilePatterns: ["/^src/init/generation\\.rs$/"],
+ datasourceTemplate: "github-release-attachments",
+ packageNameTemplate: "jdx/mise",
+ depNameTemplate: "mise",
+ matchStrings: ["jdx/mise-action.*\\n\\s*with:\\s*\\n\\s*version: [\"']?(?v[.\\d]+)[\"']?\\s*\\n\\s*sha256: [\"']?(?\\w+)[\"']?"],
+ },
],
packageRules: [
{
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 62ff2f1..101e60e 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -2,6 +2,8 @@
name: Lint
on:
+ push:
+ branches: [main] # warms the Rust cache so PR branches get a cache hit
pull_request:
permissions: {}
@@ -26,6 +28,13 @@ jobs:
version: v2026.4.1
sha256: c597fa1e4da76d1ea1967111d150a6a655ca51a72f4cd17fdc584be2b9eaa8bd
+ # mise may activate an existing Rust toolchain without adding missing components.
+ - name: Install Rust lint components
+ run: rustup component add clippy rustfmt
+
+ - name: Restore cache
+ uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
+
- name: Lint
env:
GITHUB_TOKEN: ${{ github.token }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..a92bb35
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,58 @@
+---
+name: Release
+
+on:
+ push:
+ tags:
+ - "v*"
+
+permissions: {}
+
+jobs:
+ build:
+ name: Build ${{ matrix.target }}
+ runs-on: ${{ matrix.runner }}
+ permissions:
+ contents: write
+ id-token: write
+ attestations: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - target: x86_64-unknown-linux-gnu
+ runner: ubuntu-24.04
+ - target: aarch64-unknown-linux-gnu
+ runner: ubuntu-24.04
+ build-tool: cross
+ - target: x86_64-apple-darwin
+ runner: macos-15-intel
+ - target: aarch64-apple-darwin
+ runner: macos-latest
+ - target: x86_64-pc-windows-msvc
+ runner: windows-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+
+ - name: Build and upload
+ uses: taiki-e/upload-rust-binary-action@10c1cf6a3da113ad4e60018e386570529aa5f1d3 # v1.30.0
+ with:
+ bin: flint
+ target: ${{ matrix.target }}
+ archive: flint-$target
+ build-tool: ${{ matrix.build-tool }}
+ tar: unix
+ zip: windows
+ checksum: sha256
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+
+ - name: Attest build provenance
+ uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2
+ with:
+ subject-path: flint-${{ matrix.target }}.*
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..023cbba
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,52 @@
+---
+name: Test
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+permissions: {}
+
+jobs:
+ test:
+ name: Test (${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: ubuntu-24.04
+ mise_version: v2026.4.1
+ mise_sha256: c597fa1e4da76d1ea1967111d150a6a655ca51a72f4cd17fdc584be2b9eaa8bd
+ - os: macos-15
+ mise_version: v2026.4.1
+ mise_sha256: c85b387148d478dec754ded31d01798e2f4e4e9448f75682dcc6bb7c16c9a4f5
+ - os: windows-2025
+ mise_version: v2026.4.1
+ mise_sha256: "" # not published for .exe — https://github.com/jdx/mise/pull/8997
+
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ fetch-depth: 0 # needed for merge-base in e2e tests
+
+ - name: Setup mise
+ uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
+ with:
+ version: ${{ matrix.mise_version }}
+ sha256: ${{ matrix.mise_sha256 }}
+
+ - name: Install Rust lint components
+ run: rustup component add clippy rustfmt
+
+ - name: Restore cache
+ uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
+
+ - name: Test
+ run: cargo test
diff --git a/.gitignore b/.gitignore
index 1933a30..549fd40 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
.idea
.mise.super-linter-*.toml
+/target
diff --git a/AGENTS-V1.md b/AGENTS-V1.md
new file mode 100644
index 0000000..e414eab
--- /dev/null
+++ b/AGENTS-V1.md
@@ -0,0 +1,158 @@
+# AGENTS-V1.md
+
+Guidance for working on flint v1 — the bash task scripts.
+For v2 (Rust binary), see [AGENTS-V2.md](AGENTS-V2.md).
+
+## Repository Overview
+
+The v1 scripts live under `tasks/lint/`. They are designed to
+be consumed as HTTP remote tasks in other repositories'
+`mise.toml` files, not run directly in this repository.
+
+## Architecture
+
+### Task Script Design Pattern
+
+All task scripts follow these conventions:
+
+- **Environment**: Scripts expect `MISE_PROJECT_ROOT` to be
+ set (automatically provided by mise)
+- **Metadata**: Shell scripts use `#MISE` comments for
+ metadata; Python scripts use `# [MISE]` comments
+- **Usage args**: Shell scripts use `#USAGE` comments to
+ define CLI arguments that mise parses
+- **Exit behavior**: Scripts exit with non-zero on errors
+ for CI integration
+- **AUTOFIX mode**: All lint scripts check the `AUTOFIX`
+ environment variable. When `AUTOFIX=true`, linters that
+ support fixing issues will automatically apply fixes;
+ linters without fix capabilities silently ignore it.
+ This allows consuming repos to run all lints with
+ `AUTOFIX=true` via a single task (e.g., `mise run fix`)
+ without needing per-linter configuration
+
+### Script Categories
+
+**`tasks/lint/`** - Linting validators:
+
+- `super-linter.sh`: Runs Super-Linter via Docker/Podman,
+ auto-detects runtime, handles SELinux on Fedora.
+ `--native` flag runs a **subset** of linters directly
+ on the host for fast local feedback (not a full
+ replacement for the container — CI uses the full set).
+ `--full` flag lints all files instead of only changed
+ files (applies to both native and container modes)
+- `links.sh`: Runs lychee link checker with two default
+ checks (all links in modified files + local links in all
+ files) and a `--full` flag for comprehensive checking
+- `renovate-deps.py`: Verifies
+ `.github/renovate-tracked-deps.json` is up to date by
+ running Renovate locally and parsing its debug logs.
+ With `AUTOFIX=true`, automatically regenerates and
+ updates the file
+
+### Key Design Decisions
+
+1. **Container runtime detection**: `super-linter.sh` tries
+ podman first (with SELinux "z" mount flag),
+ falls back to Docker. With `--native`, the container
+ runtime is bypassed entirely and linters run directly
+ on the host
+2. **AUTOFIX mode**: Lint scripts that support fixing accept
+ `--autofix` flag and `AUTOFIX` env var for unified fix
+ workflows:
+ - `super-linter.sh`: Filters out `FIX_*` env vars
+ unless autofix is enabled
+ - `renovate-deps.py`: Automatically regenerates and
+ updates `.github/renovate-tracked-deps.json`
+ when autofix is enabled
+ - `links.sh`: Silently ignores the `AUTOFIX` env var
+ (lychee has no autofix capability;
+ no `--autofix` flag is exposed)
+ - The `AUTOFIX` env var is how the `fix` meta-task
+ propagates autofix through the dependency chain
+3. **Diff-based link checking**: `links.sh` runs two checks
+ by default (all links in modified files + local links in
+ all files), use `--full` to check all links in all files;
+ falls back to `--full` when config changes
+4. **Renovate exclusions**: `RENOVATE_TRACKED_DEPS_EXCLUDE`
+ allows skipping managers like
+ `github-actions,github-runners`
+5. **Consuming repos provide config**: Scripts reference
+ config files (`.github/config/super-linter.env`,
+ `.github/config/lychee.toml`) that consuming repos
+ must provide
+
+## Testing Changes
+
+Since these are remote task scripts consumed by other repos:
+
+1. Test changes by pointing a consuming repo's `mise.toml`
+ to a local file path or Git branch
+2. Verify scripts work with both Docker and Podman
+3. Test with and without `AUTOFIX=true`:
+ - `super-linter.sh`: Verify `FIX_*` vars are filtered
+ correctly
+ - `renovate-deps.py`: Verify it regenerates and updates
+ the file
+ - Link linters: Verify they run normally and don't
+ output warnings
+4. For Renovate scripts, ensure they handle missing deps
+ gracefully
+
+## Native Mode Tips
+
+For faster native linting, consider switching
+`super-linter.env` from a deny-list
+(`VALIDATE_X=false` for each unwanted linter) to an
+allow-list (only `VALIDATE_X=true` for linters you
+want). Super-linter's logic — and native mode — treats
+any explicit `VALIDATE_*=true` as "only run these".
+This avoids noise from linters like `golangci-lint`
+running on non-Go repos.
+
+After updating the super-linter version in `mise.toml`,
+run `mise run setup:native-lint-tools` on the host to
+install matching tool versions. Native mode fails if
+enabled tools are missing.
+
+**Config files:** Native mode requires linter configs at
+standard locations (project root), not in
+`.github/linters/` (super-linter's convention). The
+script errors if `.github/linters/` exists. All
+supported linters auto-discover their config:
+`textlint`→`.textlintrc`,
+`shellcheck`→`.shellcheckrc`,
+`markdownlint`→`.markdownlint.json`,
+`ec` (editorconfig-checker)→`.ecrc`,
+`actionlint`→`.github/actionlint.yml`,
+`hadolint`→`.hadolint.yaml`,
+`golangci-lint`→`.golangci.yml`,
+`ruff`→`ruff.toml`/`pyproject.toml`,
+`codespell`→`.codespellrc`/`pyproject.toml`,
+`biome`→`biome.json`,
+`prettier`→`.prettierrc`,
+`shfmt`→`.editorconfig`.
+
+## Adding New Linters
+
+When adding new lint scripts, follow these patterns:
+
+1. **Add AUTOFIX support**: Check for `AUTOFIX` env var and
+ implement fix behavior if the underlying tool supports it
+2. **Silent fallback**: If the tool doesn't support autofix,
+ silently ignore `AUTOFIX` (no warnings or errors)
+3. **Consistent behavior**: Ensure the script works the same
+ whether `AUTOFIX` is set or not for check-only tools
+4. **Document support**: Update README.md table to show
+ whether AUTOFIX is supported
+
+## Script Conventions
+
+- Shell scripts use `set -euo pipefail` for safety
+- Python scripts check for `MISE_PROJECT_ROOT` and exit
+ with clear error if missing
+- Use `# shellcheck disable=` with justification when
+ intentionally violating shellcheck rules
+- Python scripts use `sys.exit(1)` on errors, print errors
+ to stderr
diff --git a/AGENTS-V2.md b/AGENTS-V2.md
new file mode 100644
index 0000000..8e541ec
--- /dev/null
+++ b/AGENTS-V2.md
@@ -0,0 +1,45 @@
+# flint v2
+
+## Scope
+
+Guidance for working on flint v2 — the Rust binary.
+For v1 (bash task scripts), see [AGENTS-V1.md](AGENTS-V1.md).
+
+## Repository Layout
+
+- Usage documentation: `README.md`
+- Agent knowledge index: `.github/agents/knowledge/README.md`
+
+## Repository Overview
+
+v2 is a single Rust binary (`flint`) that discovers linting
+tools from the consuming repo's `mise.toml`, runs them
+against changed files in parallel, and produces identical
+output locally and in CI.
+
+## Knowledge Loading
+
+For coding, fix, and refactoring tasks, consult `.github/agents/knowledge/README.md`
+before making substantial changes.
+
+Use the knowledge index to load only the article(s) relevant to the current task.
+Do not load the entire knowledge folder by default.
+
+## Execution Rules
+
+Run tests with `cargo test`. Tests spin up temporary git repos and run the real
+`flint` binary — they are integration tests, not unit tests, so they can be slow.
+
+The `cases` test runs all fixture cases under `tests/cases/` in parallel by
+top-level directory (linter group). Two env vars control its behaviour:
+
+- `FLINT_CASES=` — run only cases matching that prefix, e.g.
+ `FLINT_CASES=shellcheck` or `FLINT_CASES=shellcheck/clean`.
+- `UPDATE_SNAPSHOTS=1` — regenerate golden stdout/stderr/exit in `test.toml`
+ instead of asserting. Always review the diff before committing.
+
+On failure the test prints a rerun hint, e.g.:
+`FLINT_CASES=shellcheck/clean cargo test cases`
+
+Always run `mise run lint:fix` before committing and review auto-fixed files —
+auto-fixes may produce unexpected results.
diff --git a/AGENTS.md b/AGENTS.md
index 446aac8..ae28cfd 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -3,107 +3,18 @@
This file provides guidance to AI coding agents when working
with code in this repository.
-## Repository Overview
+## Versions
-This repository contains reusable mise task scripts for
-linting. These scripts are designed to be consumed as HTTP
-remote tasks in other repositories' `mise.toml` files, not
-run directly in this repository.
+This repository contains two generations of flint:
-## Architecture
-
-### Task Script Design Pattern
-
-All task scripts follow these conventions:
-
-- **Environment**: Scripts expect `MISE_PROJECT_ROOT` to be
- set (automatically provided by mise)
-- **Metadata**: Shell scripts use `#MISE` comments for
- metadata; Python scripts use `# [MISE]` comments
-- **Usage args**: Shell scripts use `#USAGE` comments to
- define CLI arguments that mise parses
-- **Exit behavior**: Scripts exit with non-zero on errors
- for CI integration
-- **AUTOFIX mode**: All lint scripts check the `AUTOFIX`
- environment variable. When `AUTOFIX=true`, linters that
- support fixing issues will automatically apply fixes;
- linters without fix capabilities silently ignore it.
- This allows consuming repos to run all lints with
- `AUTOFIX=true` via a single task (e.g., `mise run fix`)
- without needing per-linter configuration
-
-### Script Categories
-
-**`tasks/lint/`** - Linting validators:
-
-- `super-linter.sh`: Runs Super-Linter via Docker/Podman,
- auto-detects runtime, handles SELinux on Fedora.
- `--native` flag runs a **subset** of linters directly
- on the host for fast local feedback (not a full
- replacement for the container — CI uses the full set).
- `--full` flag lints all files instead of only changed
- files (applies to both native and container modes)
-- `links.sh`: Runs lychee link checker with two default
- checks (all links in modified files + local links in all
- files) and a `--full` flag for comprehensive checking
-- `renovate-deps.py`: Verifies
- `.github/renovate-tracked-deps.json` is up to date by
- running Renovate locally and parsing its debug logs.
- With `AUTOFIX=true`, automatically regenerates and
- updates the file
-
-### Key Design Decisions
-
-1. **Container runtime detection**: `super-linter.sh` tries
- podman first (with SELinux "z" mount flag),
- falls back to Docker. With `--native`, the container
- runtime is bypassed entirely and linters run directly
- on the host
-2. **AUTOFIX mode**: Lint scripts that support fixing accept
- `--autofix` flag and `AUTOFIX` env var for unified fix
- workflows:
- - `super-linter.sh`: Filters out `FIX_*` env vars
- unless autofix is enabled
- - `renovate-deps.py`: Automatically regenerates and
- updates `.github/renovate-tracked-deps.json`
- when autofix is enabled
- - `links.sh`: Silently ignores the `AUTOFIX` env var
- (lychee has no autofix capability;
- no `--autofix` flag is exposed)
- - The `AUTOFIX` env var is how the `fix` meta-task
- propagates autofix through the dependency chain
-3. **Diff-based link checking**: `links.sh` runs two checks
- by default (all links in modified files + local links in
- all files), use `--full` to check all links in all files;
- falls back to `--full` when config changes
-4. **Renovate exclusions**: `RENOVATE_TRACKED_DEPS_EXCLUDE`
- allows skipping managers like
- `github-actions,github-runners`
-5. **Consuming repos provide config**: Scripts reference
- config files (`.github/config/super-linter.env`,
- `.github/config/lychee.toml`) that consuming repos
- must provide
-
-## Testing Changes
-
-Since these are remote task scripts consumed by other repos:
-
-1. Test changes by pointing a consuming repo's `mise.toml`
- to a local file path or Git branch
-2. Verify scripts work with both Docker and Podman
-3. Test with and without `AUTOFIX=true`:
- - `super-linter.sh`: Verify `FIX_*` vars are filtered
- correctly
- - `renovate-deps.py`: Verify it regenerates and updates
- the file
- - Link linters: Verify they run normally and don't
- output warnings
-4. For Renovate scripts, ensure they handle missing deps
- gracefully
+- **v1** (stable): reusable bash task scripts consumed as
+ HTTP remote tasks. See [AGENTS-V1.md](AGENTS-V1.md).
+- **v2** (in development, `feat/flint-v2` branch): a single
+ Rust binary. See [AGENTS-V2.md](AGENTS-V2.md).
## Linting
-**Always run `mise run fix` before committing changes.**
+**Always run `mise run lint:fix` before committing changes.**
This ensures all files pass CI linting (Biome formatting,
shellcheck, etc.). Review the auto-fixed files before
committing — auto-fixes may produce unexpected results.
@@ -115,62 +26,15 @@ workflow — both are optional. To install the Git hook:
```bash
# Auto-fix and verify (recommended dev workflow)
-mise run fix
+mise run lint:fix
# Verify only (same command used in CI)
mise run lint
# Install git pre-commit hook (one-time, opt-in)
-mise run setup:pre-commit-hook
+flint hook install
```
-## Native Mode Tips
-
-For faster native linting, consider switching
-`super-linter.env` from a deny-list
-(`VALIDATE_X=false` for each unwanted linter) to an
-allow-list (only `VALIDATE_X=true` for linters you
-want). Super-linter's logic — and native mode — treats
-any explicit `VALIDATE_*=true` as "only run these".
-This avoids noise from linters like `golangci-lint`
-running on non-Go repos.
-
-After updating the super-linter version in `mise.toml`,
-run `mise run setup:native-lint-tools` on the host to
-install matching tool versions. Native mode fails if
-enabled tools are missing.
-
-**Config files:** Native mode requires linter configs at
-standard locations (project root), not in
-`.github/linters/` (super-linter's convention). The
-script errors if `.github/linters/` exists. All
-supported linters auto-discover their config:
-`textlint`→`.textlintrc`,
-`shellcheck`→`.shellcheckrc`,
-`markdownlint`→`.markdownlint.json`,
-`ec` (editorconfig-checker)→`.ecrc`,
-`actionlint`→`.github/actionlint.yml`,
-`hadolint`→`.hadolint.yaml`,
-`golangci-lint`→`.golangci.yml`,
-`ruff`→`ruff.toml`/`pyproject.toml`,
-`codespell`→`.codespellrc`/`pyproject.toml`,
-`biome`→`biome.json`,
-`prettier`→`.prettierrc`,
-`shfmt`→`.editorconfig`.
-
-## Adding New Linters
-
-When adding new lint scripts, follow these patterns:
-
-1. **Add AUTOFIX support**: Check for `AUTOFIX` env var and
- implement fix behavior if the underlying tool supports it
-2. **Silent fallback**: If the tool doesn't support autofix,
- silently ignore `AUTOFIX` (no warnings or errors)
-3. **Consistent behavior**: Ensure the script works the same
- whether `AUTOFIX` is set or not for check-only tools
-4. **Document support**: Update README.md table to show
- whether AUTOFIX is supported
-
## Commit Messages
This repository uses
@@ -196,13 +60,3 @@ affect consumers (documentation, CI workflows, repository
config).
Misusing `fix:` for non-functional changes creates
unnecessary releases.
-
-## Script Conventions
-
-- Shell scripts use `set -euo pipefail` for safety
-- Python scripts check for `MISE_PROJECT_ROOT` and exit
- with clear error if missing
-- Use `# shellcheck disable=` with justification when
- intentionally violating shellcheck rules
-- Python scripts use `sys.exit(1)` on errors, print errors
- to stderr
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..7c52da5
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1092 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "atomic"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+
+[[package]]
+name = "bstr"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "bytemuck"
+version = "1.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "clap"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "crossterm"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "mio",
+ "parking_lot",
+ "rustix 0.38.44",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "figment"
+version = "0.10.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
+dependencies = [
+ "atomic",
+ "pear",
+ "serde",
+ "toml",
+ "uncased",
+ "version_check",
+]
+
+[[package]]
+name = "flint"
+version = "0.20.0-alpha.1"
+dependencies = [
+ "anyhow",
+ "clap",
+ "crossterm",
+ "dunce",
+ "figment",
+ "globset",
+ "regex",
+ "semver",
+ "serde",
+ "serde_json",
+ "similar",
+ "tempfile",
+ "tokio",
+ "toml",
+ "toml_edit",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "globset"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "indexmap"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "inlinable_string"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libc"
+version = "0.2.184"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "mio"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "pear"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
+dependencies = [
+ "inlinable_string",
+ "pear_codegen",
+ "yansi",
+]
+
+[[package]]
+name = "pear_codegen"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147"
+dependencies = [
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.12.1",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "signal-hook"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "similar"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom",
+ "once_cell",
+ "rustix 1.1.4",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio"
+version = "1.50.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "uncased"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.2+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..2b9f596
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+name = "flint"
+version = "0.20.0-alpha.1"
+edition = "2024"
+description = "mise-native lint orchestrator"
+license = "Apache-2.0"
+repository = "https://github.com/grafana/flint"
+
+[dependencies]
+anyhow = "1"
+crossterm = "0.28"
+clap = { version = "4", features = ["derive", "env"] }
+figment = { version = "0.10", features = ["toml", "env"] }
+serde = { version = "1", features = ["derive"] }
+toml = "0.8"
+tokio = { version = "1", features = ["full"] }
+semver = "1"
+regex = "1"
+globset = "0.4"
+serde_json = "1"
+similar = "2"
+toml_edit = "0.22"
+dunce = "1.0.5"
+
+[dev-dependencies]
+tempfile = "3"
+regex = "1"
diff --git a/MIGRATION.md b/MIGRATION.md
new file mode 100644
index 0000000..3f6833b
--- /dev/null
+++ b/MIGRATION.md
@@ -0,0 +1,37 @@
+# Migration Guide
+
+## Migrating from flint v1 (bash tasks) to flint v2 (binary)
+
+flint v2 replaces the HTTP remote tasks with a single `flint` binary that
+discovers linters from your `mise.toml` and runs them against changed files.
+
+### 1. Add `flint` as a tool
+
+```toml
+[tools]
+"ubi:grafana/flint" = "0.20.0-alpha.1"
+```
+
+### 2. Run `flint init`
+
+After installing flint (`mise install`), run `flint init`. It automatically:
+
+- removes v1 HTTP task entries from `[tasks]`
+- removes `RENOVATE_TRACKED_DEPS_EXCLUDE` from `[env]` and migrates the manager list to `flint.toml` (when a v1 renovate-deps task is present)
+- replaces `npm:markdownlint-cli` with `npm:markdownlint-cli2` in `[tools]`
+- adds the missing linters to `[tools]` based on your tracked files
+- adds `[env] FLINT_CONFIG_DIR` and standard `lint*` / `setup:pre-commit-hook` tasks
+- writes a `flint.toml` skeleton in your chosen config dir
+- generates `.github/workflows/lint.yml`
+- patches `renovate.json5` to add the flint preset
+
+Then run `mise install` to install the new tools and
+`mise run setup:pre-commit-hook` to install the git hook.
+
+Finally, run `flint run --fix renovate-deps` to regenerate
+`renovate-tracked-deps.json` with all the new tools included.
+
+### 3. Verify active linters
+
+Run `flint linters` to confirm flint detects all the tools declared in your
+`mise.toml`. Any tool listed as `missing` is not declared and will be skipped.
diff --git a/README-V1.md b/README-V1.md
new file mode 100644
index 0000000..7f88929
--- /dev/null
+++ b/README-V1.md
@@ -0,0 +1,544 @@
+# flint v1 (legacy)
+
+> [!NOTE]
+> **This is the legacy v1 documentation** (bash task scripts consumed as
+> mise HTTP remote tasks). The current version is [flint v2](README.md) —
+> a single Rust binary.
+
+A toolbox of reusable [mise](https://mise.jdx.dev/) lint task scripts.
+Pick the ones you need — each task is independent and can be adopted
+on its own.
+
+**Available tasks:**
+
+| Task | Tool |
+| -------------------- | ------------------------------------------------------------- |
+| `lint:super-linter` | [Super-Linter](https://github.com/super-linter/super-linter) |
+| `lint:links` | [lychee](https://lychee.cli.rs/) |
+| `lint:renovate-deps` | [Renovate](https://docs.renovatebot.com/) dependency tracking |
+
+## How it works
+
+Flint relies on two tools that each play a distinct role:
+
+### mise — the task runner
+
+[mise](https://mise.jdx.dev/) is a polyglot dev tool manager and task
+runner. In the context of flint, mise serves two purposes:
+
+1. **Installing tools.** mise's `[tools]` section pins exact versions
+ of the linters each task needs (e.g., `lychee`, `node`,
+ `"npm:renovate"`). Running `mise install` gives every developer and
+ CI runner the same versions, so local runs are consistent with CI.
+
+2. **Running tasks.** mise downloads task scripts from this repository
+ via HTTP, wires them into your project as local commands
+ (`mise run lint`, `mise run fix`), and passes flags and environment
+ variables through to each script. You don't need to clone flint —
+ mise fetches the scripts directly from GitHub URLs pinned in your
+ `mise.toml`.
+
+### Renovate — the dependency updater
+
+[Renovate](https://docs.renovatebot.com/) is an automated dependency update bot.
+Extending the flint [Renovate preset](#automatic-version-updates-with-renovate)
+(`default.json`) is essential for any repository that uses flint — without it,
+SHA-pinned flint URLs and `_VERSION` variables in `mise.toml` would never get
+updated. The preset ships custom managers that detect these patterns and open
+PRs to bump both flint itself and the tools it runs
+(e.g., Super-Linter, lychee).
+
+Optionally, the [`lint:renovate-deps`](#lintrenovate-deps) task adds a second
+layer: it runs Renovate locally to detect which dependencies Renovate is
+tracking, compares this against a committed snapshot, and fails if they
+diverge — catching cases where a dependency silently falls off Renovate's
+radar.
+
+## Usage
+
+> [!WARNING]
+> Always pin to a specific version, never use `main`.
+> The main branch may contain breaking changes.
+> See [CHANGELOG.md](CHANGELOG.md) for version history.
+
+Add whichever tasks you need as HTTP remote tasks in your `mise.toml`,
+pinned to the commit SHA of a release tag with a version comment:
+
+
+
+```toml
+# Pick the tasks you need from flint (https://github.com/grafana/flint)
+[tasks."lint:super-linter"]
+description = "Run Super-Linter on the repository"
+file = "https://raw.githubusercontent.com/grafana/flint/8a39d96e17327c54974623b252eb5260d67ed68a/tasks/lint/super-linter.sh" # v0.9.1
+[tasks."lint:links"]
+description = "Check for broken links in changed files + all local links"
+file = "https://raw.githubusercontent.com/grafana/flint/8a39d96e17327c54974623b252eb5260d67ed68a/tasks/lint/links.sh" # v0.9.1
+[tasks."lint:renovate-deps"]
+description = "Verify renovate-tracked-deps.json is up to date"
+file = "https://raw.githubusercontent.com/grafana/flint/8a39d96e17327c54974623b252eb5260d67ed68a/tasks/lint/renovate-deps.py" # v0.9.1
+```
+
+
+
+The SHA pin ensures the URL is immutable (tag-based URLs can change
+if a tag is force-pushed), and the `# v0.3.0` comment tells Renovate
+which version is currently pinned.
+
+Then wire up top-level `lint` and `fix` tasks that reference whichever tasks
+you adopted (add any project-specific subtasks to the `depends` list):
+
+```toml
+[tasks."lint:fast"]
+description = "Run fast lints (no Renovate)"
+depends = ["lint:super-linter", "lint:links"]
+
+[tasks.lint]
+description = "Run all lints"
+depends = ["lint:fast", "lint:renovate-deps"]
+
+[tasks.fix]
+description = "Auto-fix lint issues and regenerate tracked deps"
+run = "AUTOFIX=true mise run lint"
+
+[tasks.native-lint]
+description = "Run lints natively (no container)"
+run = "NATIVE=true mise run lint:fast"
+```
+
+Finally, extend the flint [Renovate preset](#automatic-version-updates-with-renovate)
+in your `renovate.json5` to keep flint and its tools up to date:
+
+```json5
+{
+ extends: ["github>grafana/flint"],
+}
+```
+
+Without this, SHA-pinned flint URLs and tool versions (e.g.,
+`SUPER_LINTER_VERSION`) in `mise.toml` will never receive automated
+updates.
+
+## Example
+
+See [grafana/docker-otel-lgtm][example-repo] for a real-world example
+of a repository using flint. Its [CONTRIBUTING.md][example-contributing]
+describes the developer workflow, and its [mise.toml][example-mise]
+shows how the tasks are wired up.
+
+[example-repo]: https://github.com/grafana/docker-otel-lgtm
+[example-contributing]: https://github.com/grafana/docker-otel-lgtm/blob/main/CONTRIBUTING.md
+[example-mise]: https://github.com/grafana/docker-otel-lgtm/blob/main/mise.toml
+
+## Tasks
+
+### `lint:super-linter`
+
+Runs [Super-Linter](https://github.com/super-linter/super-linter)
+via Docker or Podman. Auto-detects the container runtime (prefers
+Podman, falls back to Docker) and handles SELinux bind-mount flags
+on Fedora.
+
+**mise** fetches this script from the SHA-pinned URL in `mise.toml`
+and runs it as `mise run lint:super-linter`. The
+`SUPER_LINTER_VERSION` environment variable (set in `mise.toml`)
+controls which Super-Linter image is pulled. **Renovate**, via the
+flint preset, opens PRs to bump both the flint script URL and the
+`SUPER_LINTER_VERSION` value when new versions are available.
+
+**Slim vs full image:** Super-Linter publishes a slim image
+(`slim-v8.4.0`) that is ~2 GB smaller than the full image. The slim
+image excludes Rust, .NET/C#, PowerShell, and ARM template linters.
+Flint defaults to the slim image. To use the full image instead, set
+`SUPER_LINTER_VERSION` to the non-prefixed tag (e.g.
+`v8.4.0@sha256:...`) and update the Renovate `depName` comment
+accordingly (drop the `versioning` override so Renovate uses standard
+Docker versioning).
+
+**Flags:**
+
+| Flag | Description |
+| ----------- | ------------------------------------------------------------ |
+| `--autofix` | Enable autofix mode (enables `FIX_*` vars from the env file) |
+| `--native` | Run linters natively instead of via container |
+| `--full` | Lint all files instead of only changed files |
+
+`--autofix` and `--native` can also be set via the `AUTOFIX=true`
+and `NATIVE=true` environment variables respectively. This is how
+the `fix` and `native-lint` meta-tasks propagate them through the
+`depends` chain.
+
+When autofix is not enabled, all `FIX_*` lines are filtered out of
+the env file before running Super-Linter.
+
+**Native mode:**
+
+The `--native` flag runs a **subset** of linters directly on
+the host for fast local feedback. It is not a full replacement
+for the Super-Linter container — CI should always use the
+container for comprehensive checks.
+
+Native mode reads the same `super-linter.env` file and follows
+Super-Linter's default logic for determining which linters are
+enabled: if any `VALIDATE_*=true` is set, only those linters run;
+otherwise all linters run unless explicitly `VALIDATE_*=false`.
+`FILTER_REGEX_EXCLUDE` is respected. `FIX_*` variables are honored
+when `--autofix` is also set.
+
+Supported native linters (subset of super-linter):
+
+- `actionlint`
+- `biome`
+- `codespell`
+- `editorconfig-checker`
+- `golangci-lint`
+- `hadolint`
+- `markdownlint`
+- `prettier`
+- `ruff`
+- `shellcheck`
+- `shfmt`
+
+Tools must be installed separately (e.g., via
+`mise run setup:native-lint-tools`). Missing tools and unsupported
+`VALIDATE_*` flags are skipped with a warning. Linter configs must
+be at standard project-root locations (not `.github/linters/`).
+
+**Environment variables:**
+
+
+
+| Variable | Default | Required | Description |
+| ----------------------- | --------------------------------- | -------- | --------------------------------------------------------------------------------------------- |
+| `SUPER_LINTER_VERSION` | — | yes | Super-Linter image tag (e.g. `slim-v8.4.0@sha256:...` for slim, `v8.4.0@sha256:...` for full) |
+| `SUPER_LINTER_ENV_FILE` | `.github/config/super-linter.env` | no | Path to the Super-Linter env file |
+
+
+
+### `lint:links`
+
+Checks links with [lychee](https://lychee.cli.rs/). By default, it
+runs two checks: **all links (local + remote) in modified files** and
+**local file links in all files**. This keeps CI fast while catching
+both broken remote links in changed content and broken internal links
+across the whole repository.
+
+**mise** fetches this script and runs it as `mise run lint:links`.
+Lychee is installed via mise's `[tools]` section — add
+`lychee = ""` to your `mise.toml`. **Renovate**, via the
+flint preset, opens PRs to bump the flint script URL when a new
+version is available.
+
+**Flags:**
+
+
+
+| Flag | Description |
+| ---------------------- | ------------------------------------------------------------------------------------ |
+| `--full` | Check all links (local + remote) in all files (single run) |
+| `--base ` | Base branch to compare against (default: `origin/$GITHUB_BASE_REF` or `origin/main`) |
+| `--head ` | Head commit to compare against (default: `$GITHUB_HEAD_SHA` or `HEAD`) |
+| `--lychee-args ` | Extra arguments to pass to lychee |
+| `...` | Files to check (default: `.`; only used with `--full`) |
+
+
+
+When running in default mode, if a config change is detected
+(matching `LYCHEE_CONFIG_CHANGE_PATTERN` or lychee-related changes
+in `mise.toml`), the script falls back to `--full` behavior.
+Changes to `mise.toml` are content-aware: only lychee-related
+lines (e.g. version or task config) trigger a full check, not
+unrelated tool version bumps.
+
+**GitHub URL remaps:**
+
+When running on a PR branch, the script automatically remaps GitHub
+`/blob//` and `/tree//` URLs so that links
+to the base branch resolve against the PR branch instead. This
+ensures that links like `/blob/main/README.md` don't break when
+the file was added or moved in the PR.
+
+For `/blob/` URLs, four ordered remap rules are applied
+(lychee uses first-match-wins):
+
+1. **Line-number anchors** (`#L123`, `#L10-L20`): GitHub renders
+ these with JavaScript, so lychee can never verify the fragment.
+ The anchor is stripped and the file is checked on the PR branch.
+2. **[Scroll to Text Fragment][stf] anchors** (`#:~:text=...`):
+ Browser-only feature, not present in static HTML. The anchor
+ is stripped and the file is checked on the PR branch.
+3. **Other fragment URLs** (`#section`): Remapped to
+ `raw.githubusercontent.com` where lychee can verify the fragment
+ in the raw file content (workaround for
+ [lychee#1729](https://github.com/lycheeverse/lychee/issues/1729)).
+4. **Non-fragment URLs**: Remapped from the base branch to the PR
+ branch (the original behavior).
+
+For `/tree/` URLs, rules 1 and 4 apply (no raw remap needed).
+
+**Global GitHub URL handling:**
+
+In addition to the PR-specific remaps above, the script handles
+two patterns that affect ALL GitHub URLs (any repository):
+
+- **Line-number anchors** (`#L123`, `#L10-L20`): Stripped from
+ any GitHub `/blob/` URL. The file is still checked, but the
+ JS-rendered line-number fragment is skipped. This means
+ consuming repos don't need to exclude these in their
+ `lychee.toml`.
+- **Scroll to Text Fragment anchors** (`#:~:text=...`): Stripped
+ from any GitHub `/blob/` URL. These are a browser-only feature
+ not present in static HTML.
+- **Issue comment anchors** (`#issuecomment-*`): The fragment
+ is stripped so the issue/PR page is still checked, but the
+ JS-rendered comment anchor is skipped.
+
+Set `LYCHEE_SKIP_GITHUB_REMAPS=true` to disable all GitHub-specific
+remaps as an escape hatch if they cause unexpected behavior.
+
+**Lychee config cleanup:**
+
+When adopting `lint:links`, you can remove the following entries
+from your `lychee.toml` because flint handles them at runtime
+via `--remap` arguments:
+
+- **GitHub blob/fragment remap for
+ [lychee#1729](https://github.com/lycheeverse/lychee/issues/1729)**
+ — flint remaps fragment URLs to `raw.githubusercontent.com`
+ for the current PR's head branch, and strips line-number
+ and Scroll to Text Fragment anchors globally.
+- **`#issuecomment-*` excludes** — flint strips the fragment
+ via remap so the issue/PR page is still checked.
+- **`#L\d+` / `#L\d+-L\d+` line-number excludes** — flint strips
+ the fragment via remap so the file is still checked.
+- **`#:~:text=...` [Scroll to Text Fragment][stf] excludes** —
+ flint strips the fragment via remap so the file is still
+ checked.
+
+Note: flint uses `--remap` (not `--exclude`) for these because
+lychee's CLI `--exclude` flags override config file excludes
+rather than merging with them.
+
+**Environment variables:**
+
+
+
+| Variable | Default | Description |
+| ------------------------------ | -------------------------------------------------------- | ------------------------------------------------------------------------------ |
+| `LYCHEE_CONFIG` | `.github/config/lychee.toml` | Path to the lychee config file |
+| `LYCHEE_CONFIG_CHANGE_PATTERN` | `^(\.github/config/lychee\.toml\|\.mise/tasks/lint/.*)$` | Files whose change triggers a full link check (`mise.toml` checked separately) |
+| `LYCHEE_SKIP_GITHUB_REMAPS` | unset | Set to `true` to disable all GitHub URL remaps |
+
+
+
+**Examples:**
+
+```bash
+mise run lint:links # All links in modified + local links in all files (default)
+mise run lint:links --full # All links in all files
+```
+
+### `lint:renovate-deps`
+
+Verifies `.github/renovate-tracked-deps.json` is up to date by
+running Renovate locally and parsing its debug logs.
+
+**mise** fetches this script and runs it as `mise run lint:renovate-deps`.
+The Renovate CLI is installed via mise's `[tools]` section — add
+`node = ""` and `"npm:renovate" = ""` to your
+`mise.toml`. **Renovate** plays a dual role here: the flint preset
+keeps the script URL up to date, while the script itself runs Renovate
+locally in `--platform=local` mode to discover which dependencies
+Renovate is tracking and compares them against a committed snapshot.
+
+**Flags:**
+
+| Flag | Description |
+| ----------- | ------------------------------------------------------ |
+| `--autofix` | Automatically regenerate and update the committed file |
+
+**Environment variables:**
+
+
+
+| Variable | Default | Description |
+| ------------------------------- | ------- | ----------------------------------------------------------------------------------- |
+| `RENOVATE_TRACKED_DEPS_EXCLUDE` | unset | Comma-separated Renovate managers to exclude (e.g. `github-actions,github-runners`) |
+
+
+
+#### Why this exists
+
+Renovate silently stops tracking a dependency when it can no longer
+parse the version reference (typo in a comment annotation,
+unsupported syntax, moved file, etc.). When that happens, the
+dependency freezes in place with no PR and no dashboard entry — it
+simply disappears from Renovate's radar.
+
+The Dependency Dashboard catches _known_ dependencies that are
+pending or in error, but it cannot show you a dependency that
+Renovate no longer sees at all. This linter closes that gap by
+keeping a committed snapshot of every dependency Renovate tracks
+and failing CI when the two diverge.
+
+#### How the linter works
+
+The `lint:renovate-deps` task runs Renovate locally in
+`--platform=local` mode, parses its debug log for the
+`packageFiles with updates` message, and generates a dependency
+list (grouped by file and manager). It then diffs this against the
+committed `.github/renovate-tracked-deps.json`:
+
+- If they match → linter passes
+- If they differ → linter fails with a unified diff showing which
+ dependencies were added or removed
+- With `--autofix` flag (or `AUTOFIX=true` env var) → automatically
+ regenerates and updates the committed file
+
+#### Typical workflow
+
+- **A dependency disappears** (e.g., someone removes a
+ `# renovate:` comment or changes a file that Renovate was
+ matching) → CI fails, showing the removed dependency in the diff.
+ The author can then decide whether the removal was intentional or
+ accidental.
+
+- **A new dependency is added** → CI fails because the committed
+ snapshot is stale. Run `mise run fix` (or
+ `AUTOFIX=true mise run lint:renovate-deps`) to regenerate and
+ update the file, then commit.
+
+- **Routine regeneration** → After any change to `renovate.json5`,
+ Dockerfiles, `go.mod`, `package.json`, or other files Renovate
+ scans, the linter will detect the change and require
+ regeneration.
+
+## How AUTOFIX and NATIVE Work
+
+`lint:super-linter` accepts `--autofix` and `--native` flags.
+Both can also be set as environment variables (`AUTOFIX=true`,
+`NATIVE=true`), which is how the `fix` and `native-lint`
+meta-tasks propagate them — mise's `depends` cannot forward CLI
+flags, but env vars flow through naturally. Tasks that don't
+recognize these variables simply ignore them.
+
+**Check mode** (default):
+
+```bash
+mise run lint # Check all linters, fail on issues
+mise run lint:super-linter # Check code style, fail on issues
+mise run lint:renovate-deps # Verify tracked deps, fail if out of date
+```
+
+**Fix mode:**
+
+```bash
+mise run fix # Auto-fix all fixable issues
+# Or run individual linters:
+mise run lint:super-linter --autofix # Apply code fixes
+mise run lint:renovate-deps --autofix # Regenerate tracked deps
+```
+
+Linters that don't support autofix (like lychee link checker)
+silently ignore the `AUTOFIX` environment variable.
+
+**Native mode:**
+
+```bash
+mise run native-lint # Fast lints, natively (no container)
+# Or run directly:
+NATIVE=true mise run lint:fast # Same effect
+mise run lint:super-linter --native # Single task with CLI flag
+```
+
+Native mode is useful in environments where Docker/Podman is
+unavailable (e.g., inside containers, CI hooks). `native-lint`
+targets `lint:fast` (super-linter + links), skipping
+`lint:renovate-deps` which requires the Renovate CLI. Tasks
+that don't use a container (like `lint:links`) ignore the
+`NATIVE` variable.
+
+## Pre-commit hook
+
+Flint provides a `pre-commit` task that runs native linters on
+every commit — fast feedback without the container overhead. To
+set it up:
+
+```bash
+mise run setup:pre-commit-hook
+```
+
+This generates a `.git/hooks/pre-commit` that runs
+`mise run pre-commit`, which uses native mode for fast checks
+without requiring a container.
+
+**For consuming repos**, add these tasks to your `mise.toml`:
+
+```toml
+[tasks.pre-commit]
+description = "Pre-commit hook: native lint"
+depends = ["setup:native-lint-tools"]
+run = "NATIVE=true mise run lint:fast"
+
+[tasks."setup:pre-commit-hook"]
+description = "Install git pre-commit hook"
+run = "mise generate git-pre-commit --write --task=pre-commit"
+```
+
+Then run `mise run setup:pre-commit-hook` once per clone.
+
+## Automatic version updates with Renovate
+
+Flint provides a [Renovate shareable preset](https://docs.renovatebot.com/config-presets/)
+with custom managers that automatically update:
+
+- **SHA-pinned flint versions** in `mise.toml`
+ (`raw.githubusercontent.com` URLs with commit SHA and version
+ comment)
+- **`_VERSION` variables** in `mise.toml` (e.g., `SUPER_LINTER_VERSION`)
+
+Add this to your `renovate.json5`:
+
+```json5
+{
+ extends: ["github>grafana/flint"],
+}
+```
+
+## Per-repo configuration
+
+Each task expects certain config files that your repository must
+provide. You only need the files for the tasks you adopt:
+
+- **`lint:super-linter`** — Super-Linter env file
+ (`.github/config/super-linter.env`) to select which validators
+ to enable and which `FIX_*` vars to set, plus any linter config
+ files (`.golangci.yaml`, `.markdownlint.yaml`, `.yaml-lint.yml`,
+ `.editorconfig`, etc.)
+- **`lint:links`** — Lychee config
+ (`.github/config/lychee.toml`) for exclusions, timeouts,
+ remappings
+- **`lint:renovate-deps`** — Renovate config
+ (`.github/renovate.json5`) and committed snapshot
+ (`.github/renovate-tracked-deps.json`)
+- **Renovate preset** — Add `"github>grafana/flint"` to your
+ `renovate.json5` `extends` array to enable automatic updates of
+ flint URLs and tool versions
+
+## Versioning
+
+This project uses [Semantic Versioning](https://semver.org/).
+Breaking changes will be documented in [CHANGELOG.md](CHANGELOG.md)
+and will result in a major version bump.
+
+**Always pin to a specific commit SHA** in your `mise.toml` file
+URLs with a version comment (e.g., `# v0.6.0`). Never reference
+`main` directly as it may contain unreleased breaking changes. To
+find the commit SHA for a release tag, run
+`git rev-parse v0.6.0`.
+
+## Releasing
+
+See [RELEASING.md](RELEASING.md).
+
+[stf]: https://developer.mozilla.org/en-US/docs/Web/URI/Fragment/Text_fragments
diff --git a/README.md b/README.md
index 5f67a3b..259f1b6 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-
flint
+
flint — fast lint
@@ -13,524 +13,605 @@
-A toolbox of reusable [mise](https://mise.jdx.dev/) lint task scripts.
-Pick the ones you need — each task is independent and can be adopted
-on its own.
+Linter runner built for speed and consistency:
-**Available tasks:**
+- **Fast** — native execution (no Docker), parallel, diff-aware (changed files only), opt-in (undeclared tools don't run), small binary cached by mise
+- **Local == CI** — one binary, one config, identical behavior
+- **AI-friendly** — fix silently, surface only what needs review
+- **Cross-platform** — Linux, macOS, Windows
+- **Autofix** — `--fix` fixes what's fixable; reports what still needs review
-| Task | Tool |
-| -------------------- | ------------------------------------------------------------- |
-| `lint:super-linter` | [Super-Linter](https://github.com/super-linter/super-linter) |
-| `lint:links` | [lychee](https://lychee.cli.rs/) |
-| `lint:renovate-deps` | [Renovate](https://docs.renovatebot.com/) dependency tracking |
+See [Why / Principles](#why) for background.
-## How it works
+> [!TIP]
+> **Legacy v1** (bash task scripts): see [README-V1.md](README-V1.md).
-Flint relies on two tools that each play a distinct role:
+---
-### mise — the task runner
+## Getting Started
-[mise](https://mise.jdx.dev/) is a polyglot dev tool manager and task
-runner. In the context of flint, mise serves two purposes:
+### Installation
-1. **Installing tools.** mise's `[tools]` section pins exact versions
- of the linters each task needs (e.g., `lychee`, `node`,
- `"npm:renovate"`). Running `mise install` gives every developer and
- CI runner the same versions, so local runs are consistent with CI.
+Add `flint` to your repo's `mise.toml` (once published):
-2. **Running tasks.** mise downloads task scripts from this repository
- via HTTP, wires them into your project as local commands
- (`mise run lint`, `mise run fix`), and passes flags and environment
- variables through to each script. You don't need to clone flint —
- mise fetches the scripts directly from GitHub URLs pinned in your
- `mise.toml`.
-
-### Renovate — the dependency updater
-
-[Renovate](https://docs.renovatebot.com/) is an automated dependency update bot.
-Extending the flint [Renovate preset](#automatic-version-updates-with-renovate)
-(`default.json`) is essential for any repository that uses flint — without it,
-SHA-pinned flint URLs and `_VERSION` variables in `mise.toml` would never get
-updated. The preset ships custom managers that detect these patterns and open
-PRs to bump both flint itself and the tools it runs
-(e.g., Super-Linter, lychee).
+```toml
+[tools]
+flint = "0.x.y"
+```
-Optionally, the [`lint:renovate-deps`](#lintrenovate-deps) task adds a second
-layer: it runs Renovate locally to detect which dependencies Renovate is
-tracking, compares this against a committed snapshot, and fails if they
-diverge — catching cases where a dependency silently falls off Renovate's
-radar.
+Until the first published release, build from source:
-## Usage
+```bash
+git clone https://github.com/grafana/flint
+cd flint
+cargo build --release
+# Binary at target/release/flint
+```
-⚠️ **Important**: Always pin to a specific version, never use `main`.
-The main branch may contain breaking changes.
-See [CHANGELOG.md](CHANGELOG.md) for version history.
+### mise.toml setup
-Add whichever tasks you need as HTTP remote tasks in your `mise.toml`,
-pinned to the commit SHA of a release tag with a version comment:
+Flint reads your `[tools]` section to discover which linters to run — declaring
+a tool is the opt-in. No separate configuration needed to activate a check: if
+`shellcheck` is in `[tools]`, flint runs shellcheck; if it isn't, that check is
+skipped. `mise install` puts all declared tools on PATH; flint picks up whatever
+is there.
-
+Add the linting tools your project needs alongside the `flint` binary itself:
```toml
-# Pick the tasks you need from flint (https://github.com/grafana/flint)
-[tasks."lint:super-linter"]
-description = "Run Super-Linter on the repository"
-file = "https://raw.githubusercontent.com/grafana/flint/df7b637ee421a491ca5c34b5be4122d8360f57c1/tasks/lint/super-linter.sh" # v0.9.2
-[tasks."lint:links"]
-description = "Check for broken links in changed files + all local links"
-file = "https://raw.githubusercontent.com/grafana/flint/df7b637ee421a491ca5c34b5be4122d8360f57c1/tasks/lint/links.sh" # v0.9.2
-[tasks."lint:renovate-deps"]
-description = "Verify renovate-tracked-deps.json is up to date"
-file = "https://raw.githubusercontent.com/grafana/flint/df7b637ee421a491ca5c34b5be4122d8360f57c1/tasks/lint/renovate-deps.py" # v0.9.2
+[tools]
+flint = "0.x.y"
+
+# Add whichever linters apply to your repo:
+shellcheck = "v0.11.0"
+shfmt = "v3.12.0"
+actionlint = "1.7.10"
+"npm:markdownlint-cli2" = "0.47.0"
+"npm:prettier" = "3.5.0"
+rust = "1.87.0" # activates cargo-fmt + cargo-clippy
+go = "1.24.0" # activates gofmt
+lychee = "0.18.0" # activates links check
+"npm:renovate" = "39.0.0" # activates renovate-deps check (slow)
```
-
-
-The SHA pin ensures the URL is immutable (tag-based URLs can change
-if a tag is force-pushed), and the `# v0.3.0` comment tells Renovate
-which version is currently pinned.
-
-Then wire up top-level `lint` and `fix` tasks that reference whichever tasks
-you adopted (add any project-specific subtasks to the `depends` list):
+Then wire up lint tasks:
```toml
-[tasks."lint:fast"]
-description = "Run fast lints (no Renovate)"
-depends = ["lint:super-linter", "lint:links"]
-
[tasks.lint]
description = "Run all lints"
-depends = ["lint:fast", "lint:renovate-deps"]
+run = "flint run"
-[tasks.fix]
-description = "Auto-fix lint issues and regenerate tracked deps"
-run = "AUTOFIX=true mise run lint"
+[tasks."lint:pre-commit"]
+description = "Fast auto-fix lint pass — for pre-push hooks and agentic pipelines"
+run = "flint run --fix --fast-only"
-[tasks.native-lint]
-description = "Run lints natively (no container)"
-run = "NATIVE=true mise run lint:fast"
+[tasks."lint:fix"]
+description = "Auto-fix lint issues"
+run = "flint run --fix"
```
-Finally, extend the flint [Renovate preset](#automatic-version-updates-with-renovate)
-in your `renovate.json5` to keep flint and its tools up to date:
+### CI setup
-```json5
-{
- extends: ["github>grafana/flint"],
-}
+```yaml
+- name: Checkout code
+ uses: actions/checkout@...
+ with:
+ fetch-depth: 0 # needed for merge-base detection
+
+- name: Setup mise
+ uses: jdx/mise-action@...
+
+- name: Lint
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ run: mise run lint
```
-Without this, SHA-pinned flint URLs and tool versions (e.g.,
-`SUPER_LINTER_VERSION`) in `mise.toml` will never receive automated
-updates.
-
-## Example
-
-See [grafana/docker-otel-lgtm][example-repo] for a real-world example
-of a repository using flint. Its [CONTRIBUTING.md][example-contributing]
-describes the developer workflow, and its [mise.toml][example-mise]
-shows how the tasks are wired up.
-
-[example-repo]: https://github.com/grafana/docker-otel-lgtm
-[example-contributing]: https://github.com/grafana/docker-otel-lgtm/blob/main/CONTRIBUTING.md
-[example-mise]: https://github.com/grafana/docker-otel-lgtm/blob/main/mise.toml
-
-## Tasks
-
-### `lint:super-linter`
-
-Runs [Super-Linter](https://github.com/super-linter/super-linter)
-via Docker or Podman. Auto-detects the container runtime (prefers
-Podman, falls back to Docker) and handles SELinux bind-mount flags
-on Fedora.
-
-**mise** fetches this script from the SHA-pinned URL in `mise.toml`
-and runs it as `mise run lint:super-linter`. The
-`SUPER_LINTER_VERSION` environment variable (set in `mise.toml`)
-controls which Super-Linter image is pulled. **Renovate**, via the
-flint preset, opens PRs to bump both the flint script URL and the
-`SUPER_LINTER_VERSION` value when new versions are available.
-
-**Slim vs full image:** Super-Linter publishes a slim image
-(`slim-v8.4.0`) that is ~2 GB smaller than the full image. The slim
-image excludes Rust, .NET/C#, PowerShell, and ARM template linters.
-Flint defaults to the slim image. To use the full image instead, set
-`SUPER_LINTER_VERSION` to the non-prefixed tag (e.g.
-`v8.4.0@sha256:...`) and update the Renovate `depName` comment
-accordingly (drop the `versioning` override so Renovate uses standard
-Docker versioning).
-
-**Flags:**
-
-| Flag | Description |
-| ----------- | ------------------------------------------------------------ |
-| `--autofix` | Enable autofix mode (enables `FIX_*` vars from the env file) |
-| `--native` | Run linters natively instead of via container |
-| `--full` | Lint all files instead of only changed files |
-
-`--autofix` and `--native` can also be set via the `AUTOFIX=true`
-and `NATIVE=true` environment variables respectively. This is how
-the `fix` and `native-lint` meta-tasks propagate them through the
-`depends` chain.
-
-When autofix is not enabled, all `FIX_*` lines are filtered out of
-the env file before running Super-Linter.
-
-**Native mode:**
-
-The `--native` flag runs a **subset** of linters directly on
-the host for fast local feedback. It is not a full replacement
-for the Super-Linter container — CI should always use the
-container for comprehensive checks.
-
-Native mode reads the same `super-linter.env` file and follows
-Super-Linter's default logic for determining which linters are
-enabled: if any `VALIDATE_*=true` is set, only those linters run;
-otherwise all linters run unless explicitly `VALIDATE_*=false`.
-`FILTER_REGEX_EXCLUDE` is respected. `FIX_*` variables are honored
-when `--autofix` is also set.
-
-Supported native linters (subset of super-linter):
-
-- `actionlint`
-- `biome`
-- `codespell`
-- `editorconfig-checker`
-- `golangci-lint`
-- `hadolint`
-- `markdownlint`
-- `prettier`
-- `ruff`
-- `shellcheck`
-- `shfmt`
-
-Tools must be installed separately (e.g., via
-`mise run setup:native-lint-tools`). Missing tools and unsupported
-`VALIDATE_*` flags are skipped with a warning. Linter configs must
-be at standard project-root locations (not `.github/linters/`).
-
-**Environment variables:**
+`GITHUB_HEAD_SHA` tells flint which commit is the PR head when running in CI.
+`fetch-depth: 0` is required for merge-base detection.
-
+---
-| Variable | Default | Required | Description |
-| ----------------------- | --------------------------------- | -------- | --------------------------------------------------------------------------------------------- |
-| `SUPER_LINTER_VERSION` | — | yes | Super-Linter image tag (e.g. `slim-v8.4.0@sha256:...` for slim, `v8.4.0@sha256:...` for full) |
-| `SUPER_LINTER_ENV_FILE` | `.github/config/super-linter.env` | no | Path to the Super-Linter env file |
+## Reference
-
+### CLI
-### `lint:links`
+```text
+flint run [OPTIONS] [LINTERS...]
+flint linters
+flint version
+```
-Checks links with [lychee](https://lychee.cli.rs/). By default, it
-runs two checks: **all links (local + remote) in modified files** and
-**local file links in all files**. This keeps CI fast while catching
-both broken remote links in changed content and broken internal links
-across the whole repository.
+Commands and flags follow [golangci-lint](https://golangci-lint.run/) conventions — teams already using it don't need to re-learn the interface.
-**mise** fetches this script and runs it as `mise run lint:links`.
-Lychee is installed via mise's `[tools]` section — add
-`lychee = ""` to your `mise.toml`. **Renovate**, via the
-flint preset, opens PRs to bump the flint script URL when a new
-version is available.
+`flint run` flags:
-**Flags:**
+| Flag | Description |
+| -------------------- | ---------------------------------------------------------------------------------------------- |
+| `--fix` | Fix what's fixable, report what still needs review; exit 1 if anything changed or needs review |
+| `--full` | Lint all files instead of only changed files |
+| `--fast-only` | Skip slow checks (e.g. `renovate-deps`). Overridden by explicit linter names. |
+| `--short` | Compact summary output, no per-check noise |
+| `--verbose` | Show all linter output, not just failures |
+| `--new-from-rev REV` | Diff base (default: merge base with base branch) |
+| `--to-ref REF` | Diff head (default: HEAD) |
-
+Every flag has an env var equivalent: `FLINT_FIX`, `FLINT_FULL`, `FLINT_FAST_ONLY`,
+`FLINT_VERBOSE`, `FLINT_SHORT`, `FLINT_NEW_FROM_REV`, `FLINT_TO_REF`.
-| Flag | Description |
-| ---------------------- | ------------------------------------------------------------------------------------ |
-| `--full` | Check all links (local + remote) in all files (single run) |
-| `--base ` | Base branch to compare against (default: `origin/$GITHUB_BASE_REF` or `origin/main`) |
-| `--head ` | Head commit to compare against (default: `$GITHUB_HEAD_SHA` or `HEAD`) |
-| `--lychee-args ` | Extra arguments to pass to lychee |
-| `...` | Files to check (default: `.`; only used with `--full`) |
+#### Intended use by context
-
+| Context | Command | Why |
+| ---------------------------- | -------------------------------------- | ----------------------------------------------------------------- |
+| Interactive development | `flint run` or `flint run --fast-only` | Full output so you can read the details |
+| Human wanting a summary | `flint run --short` | Compact output, no per-check noise |
+| Pre-push hook (CC / agentic) | `flint run --fix --fast-only` | Fixes what it can silently, surfaces only what needs human review |
+| CI | `flint run` | Full output for humans reading CI logs |
-When running in default mode, if a config change is detected
-(matching `LYCHEE_CONFIG_CHANGE_PATTERN` or lychee-related changes
-in `mise.toml`), the script falls back to `--full` behavior.
-Changes to `mise.toml` are content-aware: only lychee-related
-lines (e.g. version or task config) trigger a full check, not
-unrelated tool version bumps.
-
-**GitHub URL remaps:**
-
-When running on a PR branch, the script automatically remaps GitHub
-`/blob//` and `/tree//` URLs so that links
-to the base branch resolve against the PR branch instead. This
-ensures that links like `/blob/main/README.md` don't break when
-the file was added or moved in the PR.
-
-For `/blob/` URLs, four ordered remap rules are applied
-(lychee uses first-match-wins):
-
-1. **Line-number anchors** (`#L123`, `#L10-L20`): GitHub renders
- these with JavaScript, so lychee can never verify the fragment.
- The anchor is stripped and the file is checked on the PR branch.
-2. **[Scroll to Text Fragment][stf] anchors** (`#:~:text=...`):
- Browser-only feature, not present in static HTML. The anchor
- is stripped and the file is checked on the PR branch.
-3. **Other fragment URLs** (`#section`): Remapped to
- `raw.githubusercontent.com` where lychee can verify the fragment
- in the raw file content (workaround for
- [lychee#1729](https://github.com/lycheeverse/lychee/issues/1729)).
-4. **Non-fragment URLs**: Remapped from the base branch to the PR
- branch (the original behavior).
-
-For `/tree/` URLs, rules 1 and 4 apply (no raw remap needed).
-
-**Global GitHub URL handling:**
-
-In addition to the PR-specific remaps above, the script handles
-two patterns that affect ALL GitHub URLs (any repository):
-
-- **Line-number anchors** (`#L123`, `#L10-L20`): Stripped from
- any GitHub `/blob/` URL. The file is still checked, but the
- JS-rendered line-number fragment is skipped. This means
- consuming repos don't need to exclude these in their
- `lychee.toml`.
-- **Scroll to Text Fragment anchors** (`#:~:text=...`): Stripped
- from any GitHub `/blob/` URL. These are a browser-only feature
- not present in static HTML.
-- **Issue comment anchors** (`#issuecomment-*`): The fragment
- is stripped so the issue/PR page is still checked, but the
- JS-rendered comment anchor is skipped.
-
-Set `LYCHEE_SKIP_GITHUB_REMAPS=true` to disable all GitHub-specific
-remaps as an escape hatch if they cause unexpected behavior.
-
-**Lychee config cleanup:**
-
-When adopting `lint:links`, you can remove the following entries
-from your `lychee.toml` because flint handles them at runtime
-via `--remap` arguments:
-
-- **GitHub blob/fragment remap for
- [lychee#1729](https://github.com/lycheeverse/lychee/issues/1729)**
- — flint remaps fragment URLs to `raw.githubusercontent.com`
- for the current PR's head branch, and strips line-number
- and Scroll to Text Fragment anchors globally.
-- **`#issuecomment-*` excludes** — flint strips the fragment
- via remap so the issue/PR page is still checked.
-- **`#L\d+` / `#L\d+-L\d+` line-number excludes** — flint strips
- the fragment via remap so the file is still checked.
-- **`#:~:text=...` [Scroll to Text Fragment][stf] excludes** —
- flint strips the fragment via remap so the file is still
- checked.
-
-Note: flint uses `--remap` (not `--exclude`) for these because
-lychee's CLI `--exclude` flags override config file excludes
-rather than merging with them.
-
-**Environment variables:**
+**`--short` output** — failed checks partitioned by fixability, fixable ones
+expressed as the exact command to run:
-
+```text
+flint: 2 checks failed — flint run --fix prettier cargo-fmt | review: shellcheck
+```
-| Variable | Default | Description |
-| ------------------------------ | -------------------------------------------------------- | ------------------------------------------------------------------------------ |
-| `LYCHEE_CONFIG` | `.github/config/lychee.toml` | Path to the lychee config file |
-| `LYCHEE_CONFIG_CHANGE_PATTERN` | `^(\.github/config/lychee\.toml\|\.mise/tasks/lint/.*)$` | Files whose change triggers a full link check (`mise.toml` checked separately) |
-| `LYCHEE_SKIP_GITHUB_REMAPS` | unset | Set to `true` to disable all GitHub URL remaps |
+**`--fix` output** — fixes what's fixable, then prints the full output of
+any checks that still need review, followed by a summary line. Exits 1 if
+anything was fixed (so the caller commits the fixes before pushing) or if
+anything still needs review. Exits 0 only if everything was already clean:
-
+```text
+[shellcheck]
-**Examples:**
+In bad.sh line 2:
+echo $1
+ ^-- SC2086 (info): Double quote to prevent globbing and word splitting.
+...
+flint: fixed: cargo-fmt — commit before pushing | review: shellcheck
+```
+
+Pass one or more linter names to run only those:
```bash
-mise run lint:links # All links in modified + local links in all files (default)
-mise run lint:links --full # All links in all files
+flint run shellcheck shfmt # run only shellcheck and shfmt
+flint run --fix prettier # fix only prettier
```
-### `lint:renovate-deps`
+`flint linters` shows every check with its status:
-Verifies `.github/renovate-tracked-deps.json` is up to date by
-running Renovate locally and parsing its debug logs.
+```text
+NAME BINARY STATUS SPEED PATTERNS
+-------------------------------------------------------------------
+shellcheck shellcheck installed fast *.sh *.bash *.bats
+cargo-fmt cargo-fmt missing fast *.rs
+renovate-deps renovate installed slow
+...
+```
-**mise** fetches this script and runs it as `mise run lint:renovate-deps`.
-The Renovate CLI is installed via mise's `[tools]` section — add
-`node = ""` and `"npm:renovate" = ""` to your
-`mise.toml`. **Renovate** plays a dual role here: the flint preset
-keeps the script URL up to date, while the script itself runs Renovate
-locally in `--platform=local` mode to discover which dependencies
-Renovate is tracking and compares them against a committed snapshot.
+### Config (`flint.toml`)
-**Flags:**
+Optional. Place in the repo root (or in `FLINT_CONFIG_DIR` — see below). All settings have defaults.
-| Flag | Description |
-| ----------- | ------------------------------------------------------ |
-| `--autofix` | Automatically regenerate and update the committed file |
+```toml
+[settings]
+base_branch = "main" # branch to diff against
+exclude = ["CHANGELOG.md", "vendor/**"] # glob patterns — exclude matching files
-**Environment variables:**
+[checks.links]
+config = ".github/config/lychee.toml" # lychee config path
+check_all_local = true # second pass: local links in all files
-
+[checks.renovate-deps]
+exclude_managers = ["github-actions", "cargo"] # skip these Renovate managers
+```
-| Variable | Default | Description |
-| ------------------------------- | ------- | ----------------------------------------------------------------------------------- |
-| `RENOVATE_TRACKED_DEPS_EXCLUDE` | unset | Comma-separated Renovate managers to exclude (e.g. `github-actions,github-runners`) |
+### `FLINT_CONFIG_DIR`
-
+Set this env var to consolidate config files in one directory (e.g. `.github/config`):
-#### Why this exists
+```toml
+# mise.toml
+[env]
+FLINT_CONFIG_DIR = ".github/config"
+```
-Renovate silently stops tracking a dependency when it can no longer
-parse the version reference (typo in a comment annotation,
-unsupported syntax, moved file, etc.). When that happens, the
-dependency freezes in place with no PR and no dashboard entry — it
-simply disappears from Renovate's radar.
+When set, `flint.toml` is loaded from that directory, and each linter that supports
+an explicit config file path via a CLI flag will have it injected automatically when
+the corresponding file exists there (see the "Config file" column in the table below).
+Files that are absent are silently skipped — existing project-root configs remain in
+effect.
-The Dependency Dashboard catches _known_ dependencies that are
-pending or in error, but it cannot show you a dependency that
-Renovate no longer sees at all. This linter closes that gap by
-keeping a committed snapshot of every dependency Renovate tracks
-and failing CI when the two diverge.
+**Note:** `editorconfig-checker`'s config file (`.editorconfig-checker.json`) controls its own settings,
+not `.editorconfig` itself — editorconfig discovery always walks up from the file
+being linted and cannot be redirected via a flag.
-#### How the linter works
+### Built-in linter registry
-The `lint:renovate-deps` task runs Renovate locally in
-`--platform=local` mode, parses its debug log for the
-`packageFiles with updates` message, and generates a dependency
-list (grouped by file and manager). It then diffs this against the
-committed `.github/renovate-tracked-deps.json`:
+
+
+
+
+| Name | Description | Fix |
+| ---------------------- | ------------------------------------------------------------------- | --- |
+| `actionlint` | Lint GitHub Actions workflow files | — |
+| `biome` | Lint JS/TS/JSON files | yes |
+| `biome-format` | Format JS/TS/JSON files | yes |
+| `cargo-clippy` | Lint Rust code; runs on all .rs files, not just changed | yes |
+| `cargo-fmt` | Format Rust code; runs on all .rs files, not just changed | yes |
+| `codespell` | Check for common spelling mistakes | yes |
+| `dotnet-format` | Format C# code | yes |
+| `editorconfig-checker` | Check files comply with EditorConfig settings | — |
+| `gofmt` | Format Go code | yes |
+| `golangci-lint` | Lint Go code; uses --new-from-rev to scope analysis to changed code | — |
+| `google-java-format` | Format Java code | yes |
+| `hadolint` | Lint Dockerfiles | — |
+| `ktlint` | Lint and format Kotlin code | yes |
+| `license-header` | Check source files have the required license header | — |
+| `lychee` | Check for broken links | — |
+| `markdownlint-cli2` | Lint Markdown files for style and consistency | yes |
+| `prettier` | Format Markdown and YAML files | yes |
+| `renovate-deps` | Verify Renovate dependency snapshot is up to date | yes |
+| `ruff` | Lint Python code | yes |
+| `ruff-format` | Format Python code | yes |
+| `shellcheck` | Lint shell scripts for common mistakes | — |
+| `shfmt` | Format shell scripts | yes |
+| `xmllint` | Validate XML files are well-formed | — |
+
+#### `actionlint`
+
+| | |
+| ----------- | -------------------------------------------------- |
+| Description | Lint GitHub Actions workflow files |
+| Fix | no |
+| Binary | `actionlint` |
+| Scope | [file](#scopes) |
+| Patterns | `.github/workflows/*.yml .github/workflows/*.yaml` |
+| Config | `actionlint.yml` |
+
+#### `biome`
+
+| | |
+| ----------- | -------------------------------------- |
+| Description | Lint JS/TS/JSON files |
+| Fix | yes |
+| Binary | `biome` |
+| Scope | [file](#scopes) |
+| Patterns | `*.json *.jsonc *.js *.ts *.jsx *.tsx` |
+
+#### `biome-format`
+
+| | |
+| ----------- | -------------------------------------- |
+| Description | Format JS/TS/JSON files |
+| Fix | yes |
+| Binary | `biome` |
+| Scope | [file](#scopes) |
+| Patterns | `*.json *.jsonc *.js *.ts *.jsx *.tsx` |
+
+#### `cargo-clippy`
+
+| | |
+| ----------- | ------------------------------------------------------- |
+| Description | Lint Rust code; runs on all .rs files, not just changed |
+| Fix | yes |
+| Binary | `cargo-clippy` |
+| Scope | [project](#scopes) |
+| Patterns | `*.rs` |
+
+#### `cargo-fmt`
+
+| | |
+| ----------- | --------------------------------------------------------- |
+| Description | Format Rust code; runs on all .rs files, not just changed |
+| Fix | yes |
+| Binary | `rustfmt` |
+| Scope | [project](#scopes) |
+| Patterns | `*.rs` |
+
+#### `codespell`
+
+| | |
+| ----------- | ---------------------------------- |
+| Description | Check for common spelling mistakes |
+| Fix | yes |
+| Binary | `codespell` |
+| Scope | [files](#scopes) |
+| Patterns | `*` |
+| Config | `.codespellrc` |
+
+#### `dotnet-format`
+
+| | |
+| ----------- | ---------------- |
+| Description | Format C# code |
+| Fix | yes |
+| Binary | `dotnet` |
+| Scope | [files](#scopes) |
+| Patterns | `*.cs` |
+
+#### `editorconfig-checker`
+
+| | |
+| ----------- | --------------------------------------------- |
+| Description | Check files comply with EditorConfig settings |
+| Fix | no |
+| Binary | `ec` |
+| Scope | [files](#scopes) |
+| Patterns | `*` |
+| Config | `.editorconfig-checker.json` |
+
+#### `gofmt`
+
+| | |
+| ----------- | --------------- |
+| Description | Format Go code |
+| Fix | yes |
+| Binary | `gofmt` |
+| Scope | [file](#scopes) |
+| Patterns | `*.go` |
+
+#### `golangci-lint`
+
+| | |
+| ----------- | ------------------------------------------------------------------- |
+| Description | Lint Go code; uses --new-from-rev to scope analysis to changed code |
+| Fix | no |
+| Binary | `golangci-lint` |
+| Scope | [project](#scopes) |
+| Patterns | `*.go` |
+| Config | `.golangci.yml` |
+
+#### `google-java-format`
+
+| | |
+| ----------- | -------------------- |
+| Description | Format Java code |
+| Fix | yes |
+| Binary | `google-java-format` |
+| Scope | [files](#scopes) |
+| Patterns | `*.java` |
+
+#### `hadolint`
+
+| | |
+| ----------- | -------------------------------------- |
+| Description | Lint Dockerfiles |
+| Fix | no |
+| Binary | `hadolint` |
+| Scope | [file](#scopes) |
+| Patterns | `Dockerfile Dockerfile.* *.dockerfile` |
+| Config | `.hadolint.yaml` |
+
+#### `ktlint`
+
+| | |
+| ----------- | --------------------------- |
+| Description | Lint and format Kotlin code |
+| Fix | yes |
+| Binary | `ktlint` |
+| Scope | [files](#scopes) |
+| Patterns | `*.kt *.kts` |
+
+#### `license-header`
+
+| | |
+| ----------- | --------------------------------------------------- |
+| Description | Check source files have the required license header |
+| Fix | no |
+| Binary | (built-in) |
+| Scope | [special](#scopes) |
+
+#### `lychee`
+
+| | |
+| ----------- | ---------------------------------- |
+| Description | Check for broken links |
+| Fix | no |
+| Binary | `lychee` |
+| Scope | [special](#scopes) |
+| Config | via `[checks.links]` in flint.toml |
+
+Orchestrates [lychee](https://lychee.cli.rs/) for link checking. Requires `lychee` in `[tools]`.
+
+Default behavior: checks all links in changed files. When `check_all_local = true` in `flint.toml`, adds a second pass over local links in all files — useful when broken internal links from unchanged files also matter.
+
+Configure via `flint.toml`:
-- If they match → linter passes
-- If they differ → linter fails with a unified diff showing which
- dependencies were added or removed
-- With `--autofix` flag (or `AUTOFIX=true` env var) → automatically
- regenerates and updates the committed file
+```toml
+[checks.links]
+config = ".github/config/lychee.toml"
+check_all_local = true
+```
-#### Typical workflow
+#### `markdownlint-cli2`
-- **A dependency disappears** (e.g., someone removes a
- `# renovate:` comment or changes a file that Renovate was
- matching) → CI fails, showing the removed dependency in the diff.
- The author can then decide whether the removal was intentional or
- accidental.
+| | |
+| ----------- | --------------------------------------------- |
+| Description | Lint Markdown files for style and consistency |
+| Fix | yes |
+| Binary | `markdownlint-cli2` |
+| Scope | [file](#scopes) |
+| Patterns | `*.md` |
+| Config | `.markdownlint.jsonc` |
-- **A new dependency is added** → CI fails because the committed
- snapshot is stale. Run `mise run fix` (or
- `AUTOFIX=true mise run lint:renovate-deps`) to regenerate and
- update the file, then commit.
+#### `prettier`
-- **Routine regeneration** → After any change to `renovate.json5`,
- Dockerfiles, `go.mod`, `package.json`, or other files Renovate
- scans, the linter will detect the change and require
- regeneration.
+| | |
+| ----------- | ------------------------------ |
+| Description | Format Markdown and YAML files |
+| Fix | yes |
+| Binary | `prettier` |
+| Scope | [files](#scopes) |
+| Patterns | `*.md *.yml *.yaml` |
+| Config | `.prettierrc` |
-## How AUTOFIX and NATIVE Work
+#### `renovate-deps`
-`lint:super-linter` accepts `--autofix` and `--native` flags.
-Both can also be set as environment variables (`AUTOFIX=true`,
-`NATIVE=true`), which is how the `fix` and `native-lint`
-meta-tasks propagate them — mise's `depends` cannot forward CLI
-flags, but env vars flow through naturally. Tasks that don't
-recognize these variables simply ignore them.
+| | |
+| ----------- | -------------------------------------------------------------------------------------------------------------------------- |
+| Description | Verify Renovate dependency snapshot is up to date |
+| Fix | yes |
+| Binary | `renovate` |
+| Scope | [special](#scopes) |
+| Patterns | `renovate.json renovate.json5 .github/renovate.json .github/renovate.json5 .renovaterc .renovaterc.json .renovaterc.json5` |
-**Check mode** (default):
+Verifies `.github/renovate-tracked-deps.json` is up to date by running Renovate locally and comparing its output against the committed snapshot. Requires `renovate` in `[tools]`.
-```bash
-mise run lint # Check all linters, fail on issues
-mise run lint:super-linter # Check code style, fail on issues
-mise run lint:renovate-deps # Verify tracked deps, fail if out of date
-```
+With `--fix`, automatically regenerates and commits the snapshot.
-**Fix mode:**
+Configure via `flint.toml`:
-```bash
-mise run fix # Auto-fix all fixable issues
-# Or run individual linters:
-mise run lint:super-linter --autofix # Apply code fixes
-mise run lint:renovate-deps --autofix # Regenerate tracked deps
+```toml
+[checks.renovate-deps]
+exclude_managers = ["github-actions", "github-runners"]
```
-Linters that don't support autofix (like lychee link checker)
-silently ignore the `AUTOFIX` environment variable.
+#### `ruff`
+
+| | |
+| ----------- | ---------------- |
+| Description | Lint Python code |
+| Fix | yes |
+| Binary | `ruff` |
+| Scope | [file](#scopes) |
+| Patterns | `*.py` |
+| Config | `ruff.toml` |
+
+#### `ruff-format`
+
+| | |
+| ----------- | ------------------ |
+| Description | Format Python code |
+| Fix | yes |
+| Binary | `ruff` |
+| Scope | [file](#scopes) |
+| Patterns | `*.py` |
+| Config | `ruff.toml` |
+
+#### `shellcheck`
+
+| | |
+| ----------- | -------------------------------------- |
+| Description | Lint shell scripts for common mistakes |
+| Fix | no |
+| Binary | `shellcheck` |
+| Scope | [file](#scopes) |
+| Patterns | `*.sh *.bash *.bats` |
+| Config | `.shellcheckrc` |
+
+#### `shfmt`
+
+| | |
+| ----------- | -------------------- |
+| Description | Format shell scripts |
+| Fix | yes |
+| Binary | `shfmt` |
+| Scope | [file](#scopes) |
+| Patterns | `*.sh *.bash` |
+
+#### `xmllint`
+
+| | |
+| ----------- | ---------------------------------- |
+| Description | Validate XML files are well-formed |
+| Fix | no |
+| Binary | `xmllint` |
+| Scope | [files](#scopes) |
+| Patterns | `*.xml` |
+
+
+
-**Native mode:**
+**Note:** Biome's config flag (`--config-path`) takes a directory, not a file path —
+config injection for `biome` and `biome-format` is not yet implemented.
-```bash
-mise run native-lint # Fast lints, natively (no container)
-# Or run directly:
-NATIVE=true mise run lint:fast # Same effect
-mise run lint:super-linter --native # Single task with CLI flag
-```
+#### Scopes
-Native mode is useful in environments where Docker/Podman is
-unavailable (e.g., inside containers, CI hooks). `native-lint`
-targets `lint:fast` (super-linter + links), skipping
-`lint:renovate-deps` which requires the Renovate CLI. Tasks
-that don't use a container (like `lint:links`) ignore the
-`NATIVE` variable.
+- `file` — invoked once per matched file
+- `files` — invoked once with all matched files as args; only changed files are passed
+- `project` — invoked once with no file args; for checks with patterns set
+ (e.g. `cargo-clippy`), skipped entirely if no matching files changed, but runs on
+ the whole project when it does run. `golangci-lint` is the exception — it uses
+ `--new-from-rev` to scope analysis to changed code even within the project run.
-## Pre-commit hook
+**Slow checks** (Slow = yes) are skipped by `--fast-only`. Use `--fast-only` for
+local/pre-push feedback and the full set in CI.
-Flint provides a `pre-commit` task that runs native linters on
-every commit — fast feedback without the container overhead. To
-set it up:
+**`editorconfig-checker` deference**: `editorconfig-checker` runs on all files, but
+automatically skips file types owned by an active line-length-enforcing
+formatter. When `cargo-fmt`, `ruff-format`, `biome-format`, or `prettier`
+are active, their file types are excluded from `editorconfig-checker` — those formatters
+already enforce line length and would conflict with `editorconfig-checker`'s
+`max_line_length` editorconfig check. If none of those formatters are
+installed, `editorconfig-checker` checks those files itself.
-```bash
-mise run setup:pre-commit-hook
-```
+## Why
-This generates a `.git/hooks/pre-commit` that runs
-`mise run pre-commit`, which uses native mode for fast checks
-without requiring a container.
+The bash task scripts (v1) have two problems:
-**For consuming repos**, add these tasks to your `mise.toml`:
+**Local ≠ CI**: `--native` runs a subset of linters; CI runs full super-linter
+in Docker. Different tools, different behavior. Passing locally does not mean
+passing in CI.
-```toml
-[tasks.pre-commit]
-description = "Pre-commit hook: native lint"
-depends = ["setup:native-lint-tools"]
-run = "NATIVE=true mise run lint:fast"
-
-[tasks."setup:pre-commit-hook"]
-description = "Install git pre-commit hook"
-run = "mise generate git-pre-commit --write --task=pre-commit"
-```
+**Bash has limits**: the registry pattern was already at the edge of what bash
+does cleanly. Adding built-in checks (links, renovate) would make it worse.
-Then run `mise run setup:pre-commit-hook` once per clone.
+### Why not pre-commit?
-## Automatic version updates with Renovate
+pre-commit adds a parallel tool management system on top of mise. Consuming repos
+already declare their tools in `mise.toml` — pre-commit would require maintaining
+a second inventory of the same tools in `.pre-commit-config.yaml`, with its own
+versioning and install lifecycle. That's friction without benefit for repos that
+are already mise-first.
-Flint provides a [Renovate shareable preset](https://docs.renovatebot.com/config-presets/)
-with custom managers that automatically update:
+### Why not Spotless (or other Maven formatter plugins)?
-- **SHA-pinned flint versions** in `mise.toml`
- (`raw.githubusercontent.com` URLs with commit SHA and version
- comment)
-- **`_VERSION` variables** in `mise.toml` (e.g., `SUPER_LINTER_VERSION`)
+Spotless runs `google-java-format` as a Maven build phase, which means format
+failures block compilation and test runs — that's the wrong place for a style
+check. flint's `google-java-format` check runs as a separate lint step, only on
+changed files, and is fast.
-Add this to your `renovate.json5`:
+To migrate: remove `spotless-maven-plugin` from your `pom.xml` (and any
+`spotless.skip` properties), add `"github:google/google-java-format"` to
+`[tools]` in `mise.toml`, and run `flint run --fix` once to confirm the repo is
+clean.
-```json5
-{
- extends: ["github>grafana/flint"],
-}
-```
+### Why not MegaLinter / super-linter?
+
+Container-based linters (super-linter, MegaLinter) ship their own tool versions,
+independent of what the repo pins in `mise.toml`. This breaks the "declare once,
+use everywhere" promise of mise. Container startup also adds latency to every run.
+
+## Principles
+
+1. **Fast** — the primary goal; everything else serves it:
+ - Native execution only (no Docker); linters run in parallel (Rust binary, short startup)
+ - Small binary, cached by mise — fast install, near-zero overhead between runs
+ - Diff-aware: only changed files are linted by default; `--full` to check everything
+ - Opt-in via `mise.toml`: undeclared tools are skipped entirely
+ - Slow checks (e.g. `renovate-deps`) tagged and skippable with `--fast-only`
+
+2. **Local same as CI** — one binary, one config, identical behavior.
+ No "native mode subset" distinction. If it passes locally, it passes in CI.
-## Per-repo configuration
-
-Each task expects certain config files that your repository must
-provide. You only need the files for the tasks you adopt:
-
-- **`lint:super-linter`** — Super-Linter env file
- (`.github/config/super-linter.env`) to select which validators
- to enable and which `FIX_*` vars to set, plus any linter config
- files (`.golangci.yaml`, `.markdownlint.yaml`, `.yaml-lint.yml`,
- `.editorconfig`, etc.)
-- **`lint:links`** — Lychee config
- (`.github/config/lychee.toml`) for exclusions, timeouts,
- remappings
-- **`lint:renovate-deps`** — Renovate config
- (`.github/renovate.json5`) and committed snapshot
- (`.github/renovate-tracked-deps.json`)
-- **Renovate preset** — Add `"github>grafana/flint"` to your
- `renovate.json5` `extends` array to enable automatic updates of
- flint URLs and tool versions
+3. **AI-friendly** — `--fix` fixes what's fixable silently, prints output
+ only for issues needing review, and exits with a structured summary:
+
+ ```text
+ [shellcheck]
+ ...
+ flint: fixed: cargo-fmt — commit before pushing | review: shellcheck
+ ```
+
+ Only unfixable issues surface for review — no reasoning step required.
+
+4. **Cross-platform** — runs on Linux, macOS, and Windows. The built-in
+ registry accounts for platform differences (e.g. binary names, path quoting).
+
+5. **Autofix where possible** — `--fix` checks first, fixes what's fixable,
+ reports what needs review. Fix mode runs serially to avoid concurrent writes.
+ Pass specific linter names to limit which fixers run (`flint run --fix prettier shfmt`).
## Versioning
@@ -538,14 +619,6 @@ This project uses [Semantic Versioning](https://semver.org/).
Breaking changes will be documented in [CHANGELOG.md](CHANGELOG.md)
and will result in a major version bump.
-**Always pin to a specific commit SHA** in your `mise.toml` file
-URLs with a version comment (e.g., `# v0.6.0`). Never reference
-`main` directly as it may contain unreleased breaking changes. To
-find the commit SHA for a release tag, run
-`git rev-parse v0.6.0`.
-
## Releasing
See [RELEASING.md](RELEASING.md).
-
-[stf]: https://developer.mozilla.org/en-US/docs/Web/URI/Fragment/Text_fragments
diff --git a/mise.toml b/mise.toml
index e4096e9..8742528 100644
--- a/mise.toml
+++ b/mise.toml
@@ -1,55 +1,50 @@
+[env]
+FLINT_CONFIG_DIR = ".github/config"
+
[tools]
lychee = "0.22.0"
node = "24.14.1"
-"npm:renovate" = "43.104.1"
-
-[env]
-RENOVATE_TRACKED_DEPS_EXCLUDE="github-actions,github-runners"
-# renovate: datasource=docker depName=ghcr.io/super-linter/super-linter
-SUPER_LINTER_VERSION="slim-v8.5.0@sha256:857dcc3f0bf5dd065fdeed1ace63394bb2004238a5ef02910ea23d9bcd8fd2b8"
-
-# Dogfood our own lint tasks - use local file paths
-[tasks."lint:super-linter"]
-description = "Run Super-Linter on the repository"
-file = "tasks/lint/super-linter.sh"
-
-[tasks."setup:native-lint-tools"]
-description = "Install native lint tools matching the pinned super-linter version"
-file = "tasks/setup/native-lint-tools.sh"
+"npm:renovate" = "43.92.1"
+"github:koalaman/shellcheck" = "v0.11.0"
+"github:mvdan/sh" = "v3.12.0"
+actionlint = "1.7.10"
+editorconfig-checker = "v3.6.1"
+"npm:markdownlint-cli2" = "0.17.2"
+"npm:prettier" = "3.8.1"
+"npm:@biomejs/biome" = "2.3.14"
+"pipx:ruff" = "0.15.0"
+"pipx:codespell" = "2.4.1"
+rust = { version = "1.94.1", components = "clippy,rustfmt" }
+
+# Tools not used to lint this repo, but required for e2e tests
+go = "1.26.2"
+hadolint = "2.14.0"
+"github:pinterest/ktlint" = "1.8.0"
+dotnet = "10.0.201"
+"cargo:xmloxide" = "0.4.1"
+golangci-lint = "2.11.4"
+"github:google/google-java-format" = "1.35.0"
[tasks."setup:update-super-linter-versions"]
description = "Generate super-linter version mapping from the super-linter repo"
file = "tasks/setup/update-super-linter-versions.sh"
-[tasks."lint:links"]
-description = "Check for broken links in changed files + all local links"
-file = "tasks/lint/links.sh"
-
-[tasks."lint:renovate-deps"]
-description = "Verify renovate-tracked-deps.json is up to date"
-file = "tasks/lint/renovate-deps.py"
-
-[tasks."lint:fast"]
-description = "Run fast lints (no Renovate)"
-depends = ["lint:super-linter", "lint:links"]
-
-[tasks."lint"]
+[tasks.lint]
description = "Run all lints"
-depends = ["lint:fast", "lint:renovate-deps"]
+run = "cargo run -q -- run"
-[tasks.fix]
-description = "Auto-fix lint issues and regenerate tracked deps"
-run = "AUTOFIX=true mise run lint"
+[tasks."lint:fix"]
+description = "Auto-fix lint issues"
+run = "cargo run -q -- run --fix"
-[tasks.native-lint]
-description = "Run lints natively (no container)"
-run = "NATIVE=true mise run lint:fast"
+[tasks."lint:pre-commit"]
+description = "Fast auto-fix lint pass (skips slow checks like renovate) — intended for pre-commit/pre-push hooks"
+run = "cargo run -q -- run --fix --fast-only"
-[tasks.pre-commit]
-description = "Pre-commit hook: native lint"
-depends = ["setup:native-lint-tools"]
-run = "NATIVE=true mise run lint:fast"
+[tasks.build]
+description = "Build the project"
+run = "cargo build"
-[tasks."setup:pre-commit-hook"]
-description = "Install git pre-commit hook that runs native linting"
-run = "mise generate git-pre-commit --write --task=pre-commit"
+[tasks.test]
+description = "Run tests"
+run = "cargo test -q"
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..4685804
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,123 @@
+use anyhow::Result;
+use figment::{
+ Figment,
+ providers::{Env, Format, Toml},
+};
+use serde::Deserialize;
+use std::path::Path;
+
+use crate::registry;
+
+#[derive(Debug, Deserialize, Clone)]
+#[serde(default)]
+#[derive(Default)]
+pub struct Config {
+ pub settings: Settings,
+ pub checks: ChecksConfig,
+}
+
+#[derive(Debug, Deserialize, Clone)]
+#[serde(default)]
+pub struct Settings {
+ pub base_branch: String,
+ pub exclude: Vec,
+}
+
+impl Default for Settings {
+ fn default() -> Self {
+ Self {
+ base_branch: "main".to_string(),
+ exclude: vec![],
+ }
+ }
+}
+
+#[derive(Debug, Default, Deserialize, Clone)]
+#[serde(default)]
+pub struct ChecksConfig {
+ pub lychee: LycheeConfig,
+ // The alias allows the underscore form used in env var keys alongside the
+ // hyphenated form used in flint.toml.
+ #[serde(rename = "renovate-deps", alias = "renovate_deps")]
+ pub renovate_deps: RenovateDepsConfig,
+ #[serde(rename = "license-header", alias = "license_header")]
+ pub license_header: LicenseHeaderConfig,
+}
+
+#[derive(Debug, Default, Deserialize, Clone)]
+#[serde(default)]
+pub struct LycheeConfig {
+ pub config: Option,
+ pub check_all_local: bool,
+}
+
+#[derive(Debug, Default, Deserialize, Clone)]
+#[serde(default)]
+pub struct RenovateDepsConfig {
+ // Env var: FLINT_RENOVATE_DEPS_EXCLUDE_MANAGERS (JSON array, e.g. '["npm"]')
+ pub exclude_managers: Vec,
+}
+
+#[derive(Debug, Deserialize, Clone)]
+#[serde(default)]
+pub struct LicenseHeaderConfig {
+ /// The text that must appear within the first `lines_to_check` lines of each file.
+ /// When empty (default), the check is disabled.
+ pub text: String,
+ /// Glob patterns for files to check (e.g. `["*.java", "*.kt"]`).
+ pub patterns: Vec,
+ /// How many lines from the top of each file to search. Default: 5.
+ pub lines_to_check: usize,
+}
+
+impl Default for LicenseHeaderConfig {
+ fn default() -> Self {
+ Self {
+ text: String::new(),
+ patterns: vec![],
+ lines_to_check: 5,
+ }
+ }
+}
+
+/// Builds env-var prefix → figment key-path mappings for every check in the registry.
+/// e.g. "lychee" → ("lychee_", "checks.lychee.")
+/// "renovate-deps" → ("renovate_deps_", "checks.renovate_deps.")
+/// "ruff-format" → ("ruff_format_", "checks.ruff_format.")
+/// Sorted longest-prefix-first so "ruff_format_" is matched before "ruff_".
+fn check_env_sections() -> Vec<(String, String)> {
+ let mut sections: Vec<(String, String)> = registry::builtin()
+ .into_iter()
+ .map(|c| {
+ let n = c.name.replace('-', "_");
+ (format!("{n}_"), format!("checks.{n}."))
+ })
+ .collect();
+ // Dedup by prefix (multiple checks can share a name after normalisation is unlikely,
+ // but be safe) then sort longest-first to avoid short prefixes shadowing longer ones.
+ sections.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
+ sections.dedup_by(|a, b| a.0 == b.0);
+ sections
+}
+
+pub fn load(config_dir: &Path) -> Result {
+ let sections = check_env_sections();
+ let cfg: Config = Figment::new()
+ .merge(Toml::file(config_dir.join("flint.toml")))
+ // Flat FLINT_ env vars, no double-underscore separators:
+ // FLINT_BASE_BRANCH, FLINT_EXCLUDE → settings.*
+ // FLINT_LYCHEE_CONFIG, FLINT_LYCHEE_* → checks.lychee.*
+ // FLINT_RENOVATE_DEPS_EXCLUDE_MANAGERS → checks.renovate_deps.*
+ // New Special checks added to the registry get env support automatically.
+ .merge(Env::prefixed("FLINT_").map(move |k| {
+ let k = k.as_str();
+ for (prefix, namespace) in §ions {
+ if let Some(rest) = k.strip_prefix(prefix.as_str()) {
+ return format!("{namespace}{rest}").into();
+ }
+ }
+ format!("settings.{k}").into()
+ }))
+ .extract()?;
+ Ok(cfg)
+}
diff --git a/src/files.rs b/src/files.rs
new file mode 100644
index 0000000..9d786fe
--- /dev/null
+++ b/src/files.rs
@@ -0,0 +1,175 @@
+use anyhow::{Context, Result};
+use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
+use std::path::{Path, PathBuf};
+use std::process::Command;
+
+use crate::config::Config;
+use crate::linters::renovate_deps::COMMITTED_PATHS;
+
+/// Files managed by flint itself — always excluded from generic linter checks.
+const BUILTIN_EXCLUDES: &[&str] = COMMITTED_PATHS;
+
+#[derive(Clone)]
+pub struct FileList {
+ pub files: Vec,
+ /// The merge base ref, used by project-scoped checks (e.g. golangci-lint).
+ pub merge_base: Option,
+ /// True when the file list contains all project files (explicit --full or no merge base).
+ /// Used by checks with a `full_cmd` to switch to a project-wide command.
+ pub full: bool,
+}
+
+pub fn changed(
+ project_root: &Path,
+ cfg: &Config,
+ full: bool,
+ from_ref: Option<&str>,
+ to_ref: Option<&str>,
+) -> Result {
+ let exclude = build_exclude_set(cfg);
+
+ if full {
+ return all_files(project_root, &exclude);
+ }
+
+ let merge_base = resolve_merge_base(project_root, cfg, from_ref)?;
+
+ let files = if let Some(ref base) = merge_base {
+ let to = to_ref.unwrap_or("HEAD");
+ collect_changed_files(project_root, &exclude, base, to)?
+ } else {
+ // No merge base (shallow clone etc.) — fall back to all files.
+ return all_files(project_root, &exclude);
+ };
+
+ Ok(FileList {
+ files,
+ merge_base,
+ full: false,
+ })
+}
+
+fn build_exclude_set(cfg: &Config) -> GlobSet {
+ let mut builder = GlobSetBuilder::new();
+ for pattern in &cfg.settings.exclude {
+ match GlobBuilder::new(pattern).literal_separator(true).build() {
+ Ok(glob) => {
+ builder.add(glob);
+ }
+ Err(e) => {
+ eprintln!("flint: invalid exclude pattern {pattern:?}: {e}");
+ }
+ }
+ }
+ builder.build().unwrap_or_default()
+}
+
+fn resolve_merge_base(
+ project_root: &Path,
+ cfg: &Config,
+ from_ref: Option<&str>,
+) -> Result