{"slug": "diffscribe-the-commit-message-writes-itself-locally", "title": "DiffScribe: The Commit Message Writes Itself (Locally)", "summary": "A developer built DiffScribe, a command-line tool that reads a git diff and drafts conventional-commit messages and changelog entries entirely on the local machine using Qwen2.5-0.5B-Instruct served through llama.cpp, avoiding sending unreleased code to cloud services. The design principle is that the model only words things while deterministic code counts and validates them: it checks conventional-commit format, length caps and bullet counts, generates per-file stats itself, and labels which text came from the model. In a recorded demo the 0.5B model produced a well-formed subject and plausible bullets but mislabeled a new function as a docstring and a usage section as installation steps, which the author frames as the expected near-miss failure profile the tool is built to bound.", "body_md": "*This is a submission for the [Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine](https://dev.to/challenges/hacktoberfest-week4-2026-10-26)*\n\nEvery developer knows the moment. The work is done, the tests pass, and\n\nthen you face the emptiest, most judgmental input field in all of\n\nsoftware: the commit message box. So you type what we all type:\n\n```\nfix stuff\nupdates\nfinal version (2)\n```\n\nGit keeps perfect records of *what* changed. The *why* and the *what it means* live in commit messages and changelogs — the two texts written\n\n`git log` archaeology is a running joke\nThe fashionable fix is to paste your diff into a cloud AI and let it\n\nwrite the message. That works, and it also means shipping your\n\nunreleased code — every secret you forgot to gitignore, every internal\n\nhostname — to someone else's server, per commit, forever.\n\nI wanted the drafting help without the disclosure. So for this week's\n\nchallenge I built **DiffScribe**: a small command-line tool that reads a\n\ngit diff and drafts a conventional-commit message and a changelog entry,\n\nrunning entirely on my own machine. The AI at its core is an open-weight\n\nmodel small enough to run on a laptop CPU. The interesting part isn't\n\nthat it works — it's *how little of the output the model is trusted with*.\n\nStage some changes, run one command:\n\n```\npython3 -m diffscribe draft\n```\n\nIt parses the diff, builds a compact digest (files, exact +/- counts,\n\nand a few \"signal\" lines like new function definitions), and asks a\n\nlocal model — Qwen2.5-0.5B-Instruct (Apache 2.0), served by llama.cpp\n\n(MIT) — for two small wordings: a commit subject, and a handful of\n\nchangelog bullets. Then deterministic code takes over again: it\n\nvalidates both texts, generates the per-file stats body itself, and\n\nassembles the final draft with labels showing exactly which words came\n\nfrom the model and which came from rules.\n\nThe governing rule of the whole design: **the model words things; code counts things.** A language model — especially a half-billion-parameter\n\n`./scripts/demo.sh` drafts for a real staged diff: a README usage\n\nsection, a new test file, and a new `count_sentences` function added to\n\na toy text-statistics project. Recorded verbatim:\n\n```\nCOMMIT MESSAGE:\nfeat: add docstring for textstat/count.py\n\nFiles changed: 3 (+20 -1)\n- README.md: +5 -1 (modified)\n- tests/test_count.py: +9 -0 (added)\n- textstat/count.py: +6 -0 (modified)\n\nCHANGELOG ENTRY (Unreleased):\n- Updated README for easy navigation\n- Added new installation steps\n- Updated tests with new code\n- Improved textstat count functionality\n```\n\nNow the honest reading, because this output is good *and* imperfect in\n\ninstructive ways. The subject is well-formed, correctly typed, and\n\npoints at the right file — but the diff adds a *function*, not a\n\ndocstring. The bullets are reasonable paraphrases, except \"Added new\n\ninstallation steps\": the README gained a *usage* section, not install\n\nsteps. Nothing here is malformed, and nothing is quite fabricated —\n\nit's the confident near-miss of a very small model.\n\nThat is exactly the failure profile I designed for. DiffScribe cannot\n\nmake a 0.5B model wise. What it can do is bound the blast radius:\n\nvalidate the *shape* (conventional-commit format, length caps, bullet\n\ncounts), validate the *grounding* (more on that below), generate every\n\n*fact-shaped* part itself, and label whatever the model did write so a\n\nhuman editor — you, before you commit — knows what they're reviewing. A\n\ndrafter, supervised. Not an oracle.\n\n`def`, `class`, `export function`…).\nOne subtlety the tests cover: a content line can `---deleted text` inside a removed string), so parsing keys\noff the `diff --git` file boundaries, not vibes.`feat(auth):`, the string \"auth\" must appear in a changed file's\npath or signal lines. Bullets must be 1–6 lines, each ≤ 140\ncharacters. Either text can fail on its own and be replaced by a\nlabeled rule-based draft (type inferred from the change shape, exact\nstats included) without discarding the other.\nThere is also a `--no-llm` mode that skips the model entirely and emits\n\nthe pure deterministic draft — vaguer, and never wrong about the facts.\n\nThe full recorded transcript of both modes is `demo-output.txt` in the\n\nrepo.\n\nThe grounding check exists because of a specific, embarrassing run\n\nearly in development. My first one-shot example — meant only to teach\n\nthe output *format* — used an auth-flavored sample: a digest about\n\n`auth/login.py`, subject `fix(auth): reject expired tokens during`. I then fed DiffScribe the text-statistics diff, and the model\n\nlogin\n\nreturned, confidently and in perfect conventional-commit format:\n\n```\nfeat(auth): reject expired tokens during login\n```\n\nThere is no auth code in that diff. There is no login. The model hadn't\n\nread my diff at all; it had copied my example and changed one word.\n\nEvery format check passed. This is the tiny-model failure mode that\n\nmatters: not gibberish — gibberish is easy to catch — but fluent,\n\nwell-formed, entirely wrong.\n\nTwo fixes. The example is now deliberately bland and topically distant\n\n(a README edit), because an example a model might parrot should be one\n\nwhose parroting is obvious. And the deterministic grounding check: a\n\nscoped subject whose scope appears nowhere in the diff is rejected and\n\nreplaced by the fallback, no matter how pretty it is. There's a\n\nregression test named after the incident\n\n(`test_assemble_rejects_leaked_example_subject`). The subject in the\n\ndemo — slightly wrong about \"docstring\", but genuinely about *this*\n\ndiff — is what the model produces under those constraints.\n\nA second, quieter reuse bug deserves a sentence: I copied last week's\n\ndownload script for llama.cpp, and it kept failing with a polite\n\n\"Not Found\" before a `bash -x` trace showed why — the asset name in the\n\nURL had a doubled letter (`llama-bb11580…`), a latent typo that had\n\nbeen sitting in the old script all along. Inherited code inherits bugs.\n\nIt also inherits the ability to *find* them, because the script was\n\nmine to read. Hold that thought for the next section.\n\nThis is the third week I've built on the same openly licensed stack —\n\nopen weights, an MIT inference engine, a standard-library-only program —\n\nand DiffScribe is the week the compounding became visible.\n\n**Reuse is real when the licenses let it be.** The llama.cpp wrapper,\n\nthe greedy-decoding discipline, the labeled-fallback pattern, even the\n\ndownload scripts came straight from my Week 2 and Week 3 entries.\n\nNothing had to be re-negotiated, re-purchased, or re-permissioned; the\n\nlicenses (Apache 2.0, MIT) were the whole conversation. And when part\n\nof what I reused turned out to be buggy, openness was the debugging\n\ntool: the script was a file on my disk, readable to the last character,\n\nfixable in one line.\n\n**Open weights change who gets to build.** DiffScribe's model is a\n\n~490 MB file I downloaded once. No account, no API key, no per-commit\n\nbill, no terms-of-service roulette about whether my use case is allowed\n\nthis month. A student on a metered connection in Ahmedabad and a staff\n\nengineer at a Fortune 500 company get *the same artifact* — and,\n\nbecause it's just a file, the same ability to inspect what it is and\n\npin exactly which version produced an output. My demo reproduces\n\nbyte-for-byte because the weights, the engine version, and the decoding\n\nsettings are all fixed and named in the repo. That reproducibility is\n\nan open-artifact property; a hosted model that silently changes under\n\nyou cannot offer it.\n\n**Openness is also a quality strategy.** The most important line in\n\nthis project is a test that encodes a failure. Small open models fail\n\nin public, cheaply, on your own machine, where you can watch them do it\n\nand engineer around it — validation, grounding, fallbacks. That loop\n\n(watch it fail → bound the failure → write the test) is the actual\n\ncraft of building with AI right now, and open tooling makes the loop\n\ntight enough to run in an afternoon.\n\nOn this week's theme — **Ghost in the Machine** — DiffScribe fits on the honouring-the-past side of the brief, and I want to be plain about the limits of that fit: this is not a ghost-story generator, and it is not an ofrenda builder. What it is, is a keeper of a project's past. Every commit is a trace of work already done, and when those traces are written as \"fix stuff\", that past is effectively lost — the story of why a change was made fades, just as surely as an unrecorded memory does. DiffScribe tries to keep that history legible, drafting the commit message and changelog entry that let a future reader reconstruct what happened and why, from the diff itself. And there is a literal ghost in this machine, too: a half-billion-parameter open-weight model that lives entirely on my laptop, offline, drafting words in the dark with no server and no internet — a small, local presence that remembers how to phrase the past, and forgets nothing it was never sent.\n\n`auth` would also match a path containing `author`.```\nchore: update code\n(3 files, +20 -1)\n```\n) — correct about every fact it states, useless as\nprose. It is a safety net, not a writer.\nNone entered — no partner technology is used, by design (see Honest limitations above). This submission is entered for the overall prize only.\n\nRepo: [https://github.com/swarnnim/diffscribe](https://github.com/swarnnim/diffscribe)\n\n```\ngit clone https://github.com/swarnnim/diffscribe && cd diffscribe\n./scripts/download-llamacpp.sh && ./scripts/download-model.sh\n./scripts/demo.sh                       # recorded demo, both modes\npython3 -m unittest discover -s tests   # 50 tests\ngit add -A && python3 -m diffscribe draft   # draft for your own change\n```\n\nStandard library only; the downloads are the single network touch, and\n\nafter that DiffScribe will happily draft commit messages on a plane, in\n\na tunnel, or wherever your `git log` has been embarrassing you.\n\n*DiffScribe was built in the open, from open parts, for the most open-source month of the year. Week 1 taught me to ship small, Week 2\nto keep data local, Week 3 to build the guardrails first. Week 4's\nlesson: trust the model with the wording, never with the facts — and\nwrite the test for the day it confuses the two.*", "url": "https://wpnews.pro/news/diffscribe-the-commit-message-writes-itself-locally", "canonical_source": "https://dev.to/swarnnim/diffscribe-the-commit-message-writes-itself-locally-4k8", "published_at": "2026-10-26 20:40:28+00:00", "updated_at": "2026-10-26 20:42:31.878582+00:00", "lang": "en", "topics": ["ai-tools", "large-language-models", "developer-tools", "ai-agents"], "entities": ["DiffScribe", "Qwen2.5-0.5B-Instruct", "llama.cpp", "Git", "Hacktoberfest"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/diffscribe-the-commit-message-writes-itself-locally", "markdown": "https://wpnews.pro/news/diffscribe-the-commit-message-writes-itself-locally.md", "text": "https://wpnews.pro/news/diffscribe-the-commit-message-writes-itself-locally.txt", "jsonld": "https://wpnews.pro/news/diffscribe-the-commit-message-writes-itself-locally.jsonld"}}