huntedbytheirs 1c668c3c1e kernel, patch, doctor, config: fill in the rest of Now and Next
kernel quest reads bugzilla's REST API and picks one bug through a seeded
xorshift, so a pick is reproducible in a test and varied in a person's hands.
kernel test builds under spawn_blocking and boots under qemu with the serial
console streamed line by line, and it reports what qemu did instead of
flattening a failed boot into a success.

patch create writes a whole series when --range is given. git format-patch
does the naming and the numbering because that is already its job, the cover
letter comes from --cover-letter, and a series.toml beside the patches records
the range with both ends as hashes, the revision, the files and the blurb.
patch submit takes a directory or several files and sends them in one
git send-email invocation, and patch update re-rolls a directory through
--reroll-count, deleting the superseded revision only once the new one is
complete.

doctor runs its checks in the order a first run hits them, prints one line and
a fix hint for each, and names the first hard failure in the summary. A missing
qemu or a missing mail route warns and keeps exit 0, because --dry-run is where
the loop actually stops on a machine without an SMTP route.

~/.config/spectral/config.toml holds the tree path, the patch directory,
always-CC addresses and an identity, with the environment winning over the file
and the file over the built-in default. spectral init verifies or clones a tree
and writes it once: a second run with the same tree says so and writes nothing,
and a run that would change an existing answer is refused until --force.

tests/ grew a fixture for each of those. The harness controls PATH and git's
configuration, and its PATH can hold a git that answers send-email either way,
so a host without that support still passes rather than deciding the result.

Two review lanes ran over this work and this commit carries their fixes. A
re-roll regenerated base..HEAD, so a commit that landed after create would have
silently joined the set and been mailed: the range is recorded now and a
re-roll regenerates exactly it. A patch file that was already gone made update
fail forever while the sidecar described a revision that was not on disk: a
missing file is the state the re-roll wanted. An empty blurb aborted after the
files were written: it is checked before anything is generated. A tree that is
not a git repository was reported as a missing revision. init cloned before it
refused. The bugzilla request had no timeout, a signal-ended child printed the
bare word "signal", CommandSpawn dropped the io reason, and the printed
send-email line quoted only arguments with spaces, so --in-reply-to <id@host>
pasted as a redirection.

Checked with cargo fmt --all -- --check, cargo clippy --all-targets --locked --
-D warnings, cargo test --locked (96 passed, 4 ignored) and cargo doc --no-deps.
The four ignored tests want the host's own checkpatch, get_maintainer and
bugzilla, and all four pass when asked for.
2026-09-16 21:52:05 -04:00
2026-09-14 20:32:10 -04:00
2026-09-14 20:28:01 -04:00

spectral

A monolithic kernel work wrapper, to make it easy.

Status: scaffold. The command surface parses, every verb is wired through main to its module, and the paths resolve. Every body is a todo!() 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.pl and scripts/get_maintainer.pl. Both ship with the kernel, so spectral does not carry its own.
  • git with send-email configured. git send-email has 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_64 is on $PATH, for kernel 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 · lookup runs get_maintainer.pl --git over the patch, splits the output into To: and Cc:, and collects the files the patch touches.
  • patch/maintainers.rs · add_cc folds --cc flags in without duplicates.
  • patch/mod.rs · submit ties those two together with git send-email, with --dry-run stopping 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.rs holds 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 main prints it, so output formatting lives in one place.
  • Errors go through the one Error enum and ?. Nothing in the command paths calls unwrap.
  • 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 unsafe and no FFI.

License

MIT. See LICENSE, © 2026 huntedbytheirs.

S
Description
A monolithic kernel work wrapper to make it easy.
Readme MIT
221 KiB
Languages
Rust 100%