Every patch verb outside the series work is real. check reads the working tree through checkpatch's stdin, which is the only way it can read one, since --git with no revision dies; format puts --fix-inplace's output back into the tree after setting the uncommitted change aside, and restores it if the fix does not apply; commit and create sit on git; create writes a mail-formatted patch, because git send-email refuses a bare diff with "No subject line"; submit splits get_maintainer.pl's roles into To and Cc, prints the command line it is about to run, and only sends when --dry-run is off; update re-rolls a patch by renaming it and does nothing at the revision it is already at. git.rs grows the plumbing those verbs stand on: diff_unstaged, format_patch, apply, commit, rev_parse. apply feeds the patch in on stdin, so a fixed patch stays in memory until it is known to apply. tests/ holds a fixture kernel tree, so the suite needs no kernel tree, no network, and no configured git send-email. The two tests that do want the host's own checkpatch and get_maintainer are ignored unless asked for, and one of them earned its place immediately: checkpatch's --file means "this argument is source code", so checking a patch with it reported a dirty patch as clean, 8004 lines and no errors. Patchfile mode is checkpatch's default and the path now stands alone. patch check also takes --rev, which is how a committed range gets checked. Checked with cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test, and cargo doc --no-deps.
spectral
A monolithic kernel work wrapper, to make it easy.
Status: scaffold. The command surface parses, every verb is wired through
mainto its module, and the paths resolve. Every body is atodo!()stub. Nothing builds a kernel, boots qemu, scrapes bugzilla, or sends mail yet. What exists is the shape of the tool and the seams to fill in, and Where to pitch in below is the list.
Why
The kernel patch lifecycle is a dozen commands you retype every time. Find
something to fix, write it, run checkpatch, run it again after --fix,
commit with the right trailer, build, boot it under qemu, diff against
master, work out who maintains those files, send. Then a week later do it
all again as v2, remembering to thread it off the original.
None of those steps are hard. They are just easy to get subtly wrong, and
the ones that get a patch ignored rather than reviewed are the boring
details: a missing Signed-off-by, a v2 that does not thread, a maintainer
who never got CC'd.
spectral is a thin wrapper over that loop, and the thinness is the point. Every verb shells out to a tool you already have and already trust.
What it wraps
What each verb actually ends up running:
| spectral | actually runs |
|---|---|
patch check / patch format |
scripts/checkpatch.pl from your tree |
patch commit |
git commit |
patch create |
git diff <base> |
patch submit |
scripts/get_maintainer.pl from your tree, then git send-email |
kernel test |
make, then qemu-system-x86_64 |
kernel quest |
HTTP against bugzilla.kernel.org |
checkpatch.pl and get_maintainer.pl are worth calling out. They come from the tree you are working in, so spectral cannot drift out of date with the kernel's own style rules or maintainer map. Upgrade your tree and the new rules apply.
Because it is thin, you can drop the wrapper at any point. patch submit --dry-run prints the recipients and the exact git send-email command line
instead of sending anything. Read it, then either drop --dry-run or run
the command yourself.
The loop
What the finished tool should feel like. None of this runs yet:
$ spectral kernel quest
12345 usb: xhci: device does not enumerate after resume
https://bugzilla.kernel.org/show_bug.cgi?id=12345
# ... go and fix it, in the tree ...
$ spectral patch check
$ spectral patch format # checkpatch --fix, then check again
$ spectral patch commit "usb: xhci: re-arm the port after resume" --signoff
$ spectral kernel test # build, then boot it under qemu
$ spectral patch create 000-xhci-port-rearm
~/.spectral/patches/000-xhci-port-rearm.patch
$ spectral patch submit 000-xhci-port-rearm.patch --dry-run
# happy? drop --dry-run and it goes out
# ... wait for review ...
$ spectral patch update 000-xhci-port-rearm.patch -v 2
~/.spectral/patches/v2-000-xhci-port-rearm.patch
$ spectral patch submit v2-000-xhci-port-rearm.patch --in-reply-to '<msg-id-of-v1>'
One deviation from the obvious design: patch update is a single subcommand
taking -v N rather than a generated update-v2 / update-v3. clap
subcommands are static, and a flag beats a subcommand you cannot tab-complete.
Leave -v off and it infers the next revision from the file's current vN-
prefix.
Install
Needs a recent stable Rust: edition 2024, so 1.85 or newer. Developed against 1.98.1.
$ git clone https://git.spectoria.dev/huntedbytheirs/spectral.git
$ cd spectral
$ cargo build --release
$ install -Dm755 target/release/spectral ~/.local/bin/spectral
Not on crates.io, so cargo install spectral will get you something else.
cargo install --path . from a clone works too.
You also need:
- a kernel tree, for
scripts/checkpatch.plandscripts/get_maintainer.pl. Both ship with the kernel, so spectral does not carry its own. - git with
send-emailconfigured.git send-emailhas to work from a plain shell first. If it does not, spectral cannot fix it for you and is not meant to try. - qemu, so that
qemu-system-x86_64is on$PATH, forkernel test.
Configuration
There is no config file yet. Two paths matter, and both come from the
environment with $HOME-relative defaults:
| what | resolution |
|---|---|
| kernel tree | $SPECTRAL_KERNEL, else ~/.spectral/linux |
| patch dir | ~/.spectral/patches |
$ export SPECTRAL_KERNEL=$HOME/src/linux
A tree is accepted only if it has scripts/checkpatch.pl. Otherwise you get
not a kernel source tree rather than a confusing failure three steps later.
A missing tree reports the path and the variable that would have set it. The
tree is not needed for kernel quest, which only wants the network.
~/.config/spectral/config.toml and a spectral init to clone the tree are
planned, not built. src/config.rs is the only file that has to change to
add either.
Commands
$ spectral --help
A monolithic kernel work wrapper to make it easy.
Usage: spectral <COMMAND>
Commands:
kernel Find work, build it, boot it
patch Carry a change from working tree to mailing list
help Print this message or the help of the given subcommand(s)
spectral kernel quest · kernel test
spectral patch check · format · commit · create · submit · update
Every one of them has --help that says more than this README does, and for
now a body that panics with a description of what it is supposed to do.
Where to pitch in
Every stub is a small function that already has its signature, its doc
comment, and its caller in main. Pick one and you only need to read the
file it lives in.
Roughly in order of how much they unblock.
patch submit
The most self-contained win, and the only one that needs no network. It does
need a kernel tree and a working git send-email.
patch/maintainers.rs·lookuprunsget_maintainer.pl --gitover the patch, splits the output intoTo:andCc:, and collects the files the patch touches.patch/maintainers.rs·add_ccfolds--ccflags in without duplicates.patch/mod.rs·submitties those two together withgit send-email, with--dry-runstopping one step short.
patch check, format, commit, create
The everyday verbs. patch/checkpatch.rs · run already has the Target
enum (working tree, a revision, or a patch file) and Report with
is_clean. What is missing is the process call and pulling the error and
warning counts out of checkpatch's output.
patch update
Small, and worth writing a test for. patch/mod.rs · reroll_path holds the
naming rule in one pure function: strip one leading vN-, then prepend
v<revision>-. Running it twice at the same revision should do nothing,
which is easier to assert than to explain.
kernel quest
The fun one if you like HTML. kernel/quest.rs · Bugzilla::fetch_open is
the only place reqwest and scraper earn their place in Cargo.toml, and
run then applies --filter and picks one issue. The QuestSource trait is
where you would add syzbot or a lore.kernel.org thread later.
kernel test
Needs a machine you are willing to boot kernels on. kernel/qemu.rs ·
build then boot. Streaming serial output as it arrives beats buffering it
until qemu exits, and it is the difference between a boot hang you can
diagnose and one you cannot.
git plumbing
git.rs · current_branch, diff_against, commit, rev_parse. Each is a
few lines over the run that is already written, which makes these the
easiest place to start.
There are no tests yet. The naming rule in reroll_path and the recipient
split in lookup are the two that most want one.
Longer-term work, and the things spectral deliberately will not do, are in
ROADMAP.md.
Development
$ cargo fmt --all
$ cargo clippy --all-targets -- -D warnings
$ cargo test
All three are clean on main and are the bar for a change.
A few conventions:
src/cli.rsholds the whole command surface. One file on purpose: the CLI is the specification, and reading it top to bottom should tell you what the tool does.- Command modules return what they produced and
mainprints it, so output formatting lives in one place. - Errors go through the one
Errorenum and?. Nothing in the command paths callsunwrap. - A new stub gets a
todo!()naming the command it will end up running. A module that is unreachable until its caller exists carries a single#[allow(dead_code)]with the reason attached. There are no crate-wide allows, so the warnings come back as the stubs get filled in. - No
unsafeand no FFI.
License
MIT. See LICENSE, © 2026 huntedbytheirs.