Five cards instead of forty notes: a local model that refuses to guess
This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady What I Built A notes folder grows faster than anyone reads it. I open mine on a Saturday, see a list of files, read
This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady
What I Built
A notes folder grows faster than anyone reads it. I open mine on a Saturday,
see a list of files, read the two with the newest dates, and close it. The work
I actually wanted was to find the five that matter this week. That is a
twenty-minute job I keep not doing.
five-things does that job in one command:
five-things brief ~/notes --budget 5 --minutes 2
It prints five cards. Each card says what the note is, the single next action,
and one sentence copied out of the note as proof. Below the cards it prints
everything it did not pick, with the reason. Nothing is silently dropped.
Two rules make it different from a summariser:
The model is rationed. A 2B model on a laptop CPU runs at under one token
per second on my machine. Every call is felt, so the tool spends the model only
on the handful of notes that a rule cannot already rank, and it caps that spend
twice: at most N notes, and at most M minutes of wall clock. When the clock
runs out, the rest are deferred with the reason time budget spent. It does not
speed up, it stops.
A card without a receipt is not a card. The prompt requires a quote field:
an exact sentence from the note. The tool checks that quote against the source,
ignoring whitespace and case. If the quote is not there, the card is marked
unverified, printed with an empty body, and the note stays in your queue. A
confident wrong summary is worse than no summary, and a 2B model produces
confident wrong summaries for a living.
Who gets to be lazier: anyone with a folder of half-finished notes, a tax pile,
or forty markdown files from a project that ended in March.
Demo
No hosted link. A local-only tool cannot be demoed with a URL, and that is the
point of it. Here is a real run instead, on the 14-note sample folder that ships
with the repo, with the model fully offline.
1. !renewal-domains.md
what : Domain renewals
next : decide whether the .dev one is worth another year.
proof: The two domains expire on 2026-10-30.
signals: pinned by filename, touched in the last 3 days, open markers: DECIDE, TODO
2. bills-october.md
what : October bills
next : pay the electricity before the late fee kicks in.
proof: DUE 2026-10-25: pay the electricity before the late fee kicks in.
signals: touched in the last 3 days, open markers: DUE, dated 2026-10-25 (within a week)
3. client-call-2026-10-23.md [unverified]
what : -
next : -
why : the model echoed the instructions instead of reading the note
signals: touched today, open markers: DECIDE, NEXT
4. apartment-move.md
what : Moving out checklist
next : DECIDE: sell the desk or take it.
proof: DECIDE: sell the desk or take it.
signals: touched in the last 2 weeks, open markers: BLOCKED, DECIDE, TODO, WAITING
not today (8)
- sourdough-timer.md <ranked #6, below the budget of 5>
- project-ideas-2024.md <ranked #11, below the budget of 5>
- wifi-and-codes.md <ranked #12, below the budget of 5>
filtered out (1)
- already-done.md <marked done>
run: 13 notes, 5 cards, 5 model calls (0 from cache), 1100+299 tokens, 394.9s, 0.8 tok/s
read: 3250 chars in the folder, 292 chars in your cards (9.0% of the folder)
card status: {"ok": 4, "unverified": 1}
Card 3 is the part I would point a judge at. The model answered with
"what": "what this note is about", which is the field description copied back
at me instead of a reading of the note. The tool caught it, printed no card, and
left the note in the pile. The alternative, a smooth sentence that looks like a
summary, is exactly the failure this design exists to avoid.
Second run on the same folder, cache warm: 0 model calls, 0.0s.
Code
fivethings on GitHub (MIT)
fivethings/scan.py deterministic scan: age, markers, dates, sections
fivethings/brief.py budget allocator, prompt, JSON parse, quote gate
fivethings/engine.py llama-server client: one endpoint, no SDK, no retries
fivethings/cli.py brief / doctor / start
tests/run_tests.py 20 assertions, no pytest, no network
sample/make_sample.py rebuilds the demo folder with realistic ages
No dependencies outside the Python standard library. The only external piece is
llama-server from llama.cpp.
How I Built It
1. Rank with rules, spend the model on wording
The model is bad at ranking and good at compressing, so I gave it only the
second job. scan.py walks the folder and scores each note on signals a person
could check: days since it was touched, how many TODO / WAITING / DECIDE /
BLOCKED markers survive in it, how many sections it has, and whether it names
a date inside the next seven days. Every point added produces a printable
reason, which is what ends up in the signals: line of a card and in the
not today list.
That has a cost I want to state: rules cannot see meaning. A note that matters
but has no marker and no recent edit will rank low, and the tool will leave it
out. The deferred list is the mitigation, not a fix. You can pin a file by
prefixing its name with !, which is a blunt escape hatch on purpose.
2. Two budgets, both enforced in code
--budget 5 is the count. --minutes 2 is the clock. Before each call the
allocator asks whether the last call's duration still fits in the remaining
time; if it does not, the remaining notes are deferred with the elapsed time in
the reason. A test breaks the clock deliberately and asserts that the run stops
early instead of pretending it finished. Without that test the clock would be
decoration.
The cache is the third saving: cards are keyed on note text, not path, so
renaming a file does not burn another call. The second run above cost nothing.
3. The quote gate
This is the whole idea in one function:
def quote_gate(quote, text):
q = _norm(quote)
if len(q) < 12:
return False
return q in _norm(text)
_norm collapses whitespace and lowercases. A quote shorter than twelve
characters fails, because "the" is always in the note. A paraphrase fails. On
failure the card keeps its status but loses its body, so the UI cannot show a
summary that was never supported.
The gate also gave me the second guard almost for free. When the model echoed
the schema instead of reading the note, the what field was literally
what this note is about. Those phrases are now rejected by name, and there is
a test for each.
4. Local inference, measured
Gemma 2 2B IT, Q4_K_M, served by llama.cpp build b11620, talked to over the
OpenAI-compatible endpoint. Nothing is imported beyond urllib, so the whole
tool is one process and one socket, and stopping the server stops the AI.
On this laptop, CPU only:
| What | Measured |
|---|---|
| Generation, single stream | 0.8 to 1.1 tokens per second |
| Prompt processing | 8.4 to 18.5 tokens per second |
| One card | about 60 to 90 seconds |
| Five cards, cold | 394.9 s |
| Five cards, warm cache | 0.0 s |
That speed is why the budgets exist, and it is also the honest argument against
using a 2B model for this: it is not fast enough to sit and watch. The tool is
built to run from a scheduler while you are asleep, and to be boring when you
open it.
5. What I would change next
The ranking should learn from the one signal I already have for free: which
cards you actually open. Right now nothing is recorded, so the rules stay
frozen at my guesses. The second change is language. The prompt is English and
the sample is English; on my own Chinese notes the cards were noticeably worse,
and I would rather tune that than pretend it works.
Why Does Open Innovation Matter?
A notes folder is not public data. Mine has a router password, a neighbour's
flat number, a client's billing habits and the accountant's deadline in it.
Sending that to a summarisation API means uploading it, and the usual product
answer is "we do not train on your data", which is a promise about a pipeline I
cannot inspect. Running the model myself makes the promise unnecessary: the
socket is 127.0.0.1, and grep on the source shows there is no other URL in
the tool.
The second reason is the guard. A quote check only makes sense if I control the
whole path from prompt to rendering. When a card fails, I can say exactly which
rule rejected it, because that rule is twelve lines long and in my repo. With a
hosted endpoint I would be debugging someone else's tokenizer and someone else's
safety filter, and I would have no way to prove that a failure was mine.
The third reason is cost over time. This tool has no key, no quota and no bill.
The expensive part is a 1.7 GB file that never changes. If Gemma 2 is replaced
by something better next year, that is one environment variable, not a
migration: FIVE_THINGS_MODEL points somewhere else and the code does not know
the difference.
My Agent Session
Not included. Saving a session with DevRelay means putting a third-party gateway
between my notes and the model, which contradicts the only claim this project
makes. The last submission I wrote for this challenge skipped it for the same
reason, and I would rather say that plainly than ship a demo that undercuts the
argument.
The build itself was written with AI assistance, with the tests and every number
in this post produced by runs I executed on this machine.
The Lazy Payoff
The arithmetic, on the sample folder: 3,250 characters of notes became 292
characters of card text, 9.0 percent of what was there. Six and a half minutes
of machine time bought that, and the second run cost nothing because the cache
held.
What I cannot claim: I have not run this on my real notes folder for a week and
counted the minutes. The sample folder is invented, with modification dates
spread over weeks so the ranking has something to rank. I built the tool and I
used it on this folder, repeatedly, while fixing the two guards. The saving
claim is arithmetic, not a diary entry, and the honest next step is to point it
at a real folder and see whether five cards is the right number or whether three
is.
One thing the run did show: of five cards, one was rejected. If the tool had
been a plain summariser, that note would have come back with a plausible
sentence and I would have believed it.
Prize Categories
Best Use of Gemma. The entire card path is Gemma 2 2B IT Q4_K_M through
llama.cpp, on CPU, offline. The model choice is also what makes the budgets
legitimate: a 2B model is the only size that fits the "runs on the laptop you
already own" claim, and being slow is what forces the design to choose instead
of summarise everything.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.