The faster run took three minutes. It produced zero drafts I could accept.
The slower run took almost twenty-seven minutes. It produced all four, then repeated the completed job in less than a second without asking the model anything.
That difference became a useful release test for PatchPitch 0.3.0, my open-source tool for turning development facts into announcement drafts for Telegram, X, DEV and GitHub. I shipped it on 24 October. Getting there meant looking beyond whether the model returned some text.
In my previous article, I ran the headless workflow on Windows and reviewed the results in the studio. The CLI already worked. For the release, I wanted to see what happened on a fresh Linux runner, outside the checkout, with stdin closed—and what remained usable when the model got things wrong.
One set of facts, four different writing jobs
Finishing a release leaves me with another small project: explaining it. The facts are the same, but a Telegram update, an X post, a DEV story and GitHub notes need different amounts of context.
PatchPitch starts with selected facts and a JSON or TOML job. Ollama supplies the local model; the CLI prepares the drafts and saves the evidence. The studio is available when I want to inspect or edit the results, but generation doesn't need its web server or a browser.
This GIF shows saved development drafts from the earlier Windows run. It illustrates review in the studio; it is not a recording of the Linux acceptance run below. The 42-second demo opens a player with sound, subtitles and playback controls.
The part I care about is what survives after generation: original responses, failed attempts, input snapshots and my editable publication files. If one target fails, I can inspect it without throwing away the other three. If I edit a draft, repeating the job preserves that edit.
The smaller model finished first—and failed the campaign
I used GitHub Actions for Linux because my Windows machine had neither WSL/Docker nor a running Ollama at the time. The runner installed PatchPitch into a fresh environment and invoked the installed CLI from outside the checkout.
I also tried the same campaign with gemma3:4b. It took 183.374 seconds, made four model requests and exited with code 4. All four targets were attempted; none produced an accepted output.
| Target | What went wrong |
|---|---|
| X | The topic plan narrowed itself to one fact. Only four catalog tags qualified; six were required, so writing stopped before another request. |
| Telegram | The Russian target received English prose with internal C01–C06 source IDs leaking into the text. |
| DEV | The article left selected facts out. |
| GitHub | The notes also missed selected facts. |
I kept the failed run and its evidence. Making those checks less strict would have produced a green workflow, but it would still have left me with the wrong language, missing information and an unusable X draft.
This result describes one campaign on one model. It doesn't establish that every 4B model will fail. It does mean I can't present this configuration as a cheaper equivalent to the one that passed.
The passing run had an eight-minute DEV request
With gemma4:12b, the Linux campaign finished in 1599.653 seconds: 26 minutes 40 seconds. Ubuntu 24.04, Python 3.12.15 and Ollama 0.40.10 were the recorded environment.
| Target | Model request time | Result |
|---|---|---|
| X, English | 124.834 s planning + 179.148 s writing | Accepted, six tags, estimated 272/280 characters |
| Telegram, Russian | 399.657 s | Accepted |
| DEV, English | 497.921 s | Accepted |
| GitHub, English | 397.662 s | Accepted |
Five requests produced four accepted drafts. Validation and session export passed too. The full Actions run records the experiment; these timings are an observation from that CPU runner, not a speed promise.
The DEV request exposed a practical setup detail: 497.921 seconds exceeds the normal 420-second timeout. The passing configuration explicitly used 600 seconds. From a prepared checkout, with Ollama running and gemma4:12b already installed, the reproduction command is:
PATCHPITCH_MODEL=gemma4:12b PATCHPITCH_TIMEOUT=600 python -m patchpitch run --job examples/headless/all-four.toml --out-root artifacts/campaigns --json </dev/null
That example reproduces the original experiment inputs. Final-release facts live separately in examples/headless/release.toml. The live evidence predates the final version label; final promotion left the inference and validation behavior unchanged.
Then came the pleasantly boring part. Repeating the completed run took 0.352 seconds. Explicit resume took 0.329 seconds. All campaign files stayed byte-identical, and neither operation made another model request.
Twenty-seven minutes is a long wait to repeat accidentally. Keeping completed work mattered more here than shaving a few words off a prompt.
A lost response shouldn't create a second article
The DEV adapter had a different kind of failure to handle. Suppose DEV saves my draft, but the response never reaches PatchPitch. Sending another create request could leave me with a duplicate.
There was an extra wrinkle: the owned-article response can omit the AI disclosure field. The old recovery logic treated an absent field as fully_autonomous. A draft sent with some_ai or no_ai could then fail reconciliation even when its content had been saved correctly.
Receipts now retain the exact outgoing article payload. Recovery checks the owned draft and its observable content, then sends the desired disclosure explicitly to that same draft. A missing field no longer becomes an invented value. Regression tests cover the recovery paths, including omitted disclosure.
Generation and delivery remain separate commands. The adapter saves drafts with published=false and protects articles that are already published. That gives me somewhere to review the actual DEV rendering before a public post.
Six tags, an explicit writing brief, and a release I can inspect
For X, I chose a product rule: exactly six relevant hashtags. PatchPitch selects them from dated observations, checks topic and audience fit, and reserves their space alongside the link before asking for prose. The passing Linux post used #DevTools #LocalAI #LLM #Ollama #OpenSource #Automation.
That catalog supports a topical choice; it doesn't prove a reach improvement. The failed 4B plan was useful precisely because it showed where the available evidence ran out.
For this article, I used PatchPitch with a Quillgrain editorial brief: first-person singular, a concrete experiment, short paragraphs and the failures kept in the story. I then edited the model draft, corrected its framing of the earlier demo and added the result tables and source links. The brief makes my preferences explicit; the edit still decides what earns a paragraph. The Linux acceptance sample itself used the clear preset.
The latest main CI passed the base suite, the pinned Quillgrain integration job and the package checks. Quillgrain ran 21 tests without skips. The wheel and source archive built successfully, and the wheel's CLI passed checks in a clean environment without UI dependencies.
PatchPitch 0.3.0 is available on GitHub, with the archives, checksums and source commit. The headless guide covers installation and job configuration. It isn't on PyPI yet.
I can now hand the CLI one maintained set of facts, let it prepare the drafts, and come back to both the results and the evidence behind them. Smaller-model acceptance and inline DEV video with sound and controls are still in the backlog.
If you run local models unattended, which failure has cost you more time: a rejected response, or a response that looked valid until you tried to use it?

Top comments (0)