<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: James Ngandu</title>
    <description>The latest articles on DEV Community by James Ngandu (@mukuvi).</description>
    <link>https://dev.to/mukuvi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1177239%2Fccb72090-3f30-44df-b801-8aec157f5609.jpg</url>
      <title>DEV Community: James Ngandu</title>
      <link>https://dev.to/mukuvi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mukuvi"/>
    <language>en</language>
    <item>
      <title>Requiem: a crash post-mortem that speaks as the program that died</title>
      <dc:creator>James Ngandu</dc:creator>
      <pubDate>Tue, 27 Oct 2026 08:04:36 +0000</pubDate>
      <link>https://dev.to/mukuvi/requiem-a-crash-post-mortem-that-speaks-as-the-program-that-died-3df1</link>
      <guid>https://dev.to/mukuvi/requiem-a-crash-post-mortem-that-speaks-as-the-program-that-died-3df1</guid>
      <description>&lt;p&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open Source AI Challenge: Week 4 - Ghost in the Machine&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;Requiem is a CLI that gives every crashed program a funeral. When a command fails, Requiem produces an obituary card with the cause of death, a suggested fix, and a first person eulogy written as if the program itself were speaking.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mukuvi/requiem" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; python3 &lt;span class="nt"&gt;-m&lt;/span&gt; requiem run &lt;span class="nt"&gt;--&lt;/span&gt; python3 examples/lost_config.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─ REQUIEM ─────────────────────────────────────────────────────┐
│ requiem-733bae  python3 examples/lost_config.py  132ms       │
│ died of missing key 'bob' in config dictionary.              │
├──────────────────────────────────────────────────────────────┤
│ WHERE                                                        │
│   examples/lost_config.py:14 in load_user                   │
│     user = config["users"][user_id]                          │
│ CAUSE                                                        │
│   missing key 'bob' in config dictionary.                    │
│ FIX                                                          │
│   add 'bob' to config dictionary.                            │
│ EULOGY                                                       │
│   I was a program. I existed to process requests, to         │
│   organize data.                                             │
└──────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A segfault gets the same treatment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make examples/segv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; python3 &lt;span class="nt"&gt;-m&lt;/span&gt; requiem run &lt;span class="nt"&gt;--&lt;/span&gt; ./examples/segv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─ REQUIEM ─────────────────────────────────────────────────────┐
│ requiem-e9b362  ./examples/segv  1ms                         │
│ died of segmentation fault                                   │
├──────────────────────────────────────────────────────────────┤
│ WHERE                                                        │
│   killed by SIGSEGV                                          │
│ CAUSE                                                        │
│   segmentation fault                                         │
│ FIX                                                          │
│   null                                                       │
│ EULOGY                                                       │
│   I trusted a pointer I had no right to trust. Now I return  │
│   to /dev/null.                                              │
└──────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Requiem is a hybrid: code does what code must, the model gives the program a voice.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Capture runs the command with subprocess and records exit code, signal, duration, and the last chunk of stderr and stdout.&lt;/li&gt;
&lt;li&gt;Coroner parses Python tracebacks, maps signals to names, reads the failing source line off disk, and redacts secrets before anything leaves the process.&lt;/li&gt;
&lt;li&gt;Ghost talks to a local llama-server running Gemma 3 1B via llama.cpp. It asks for a terse diagnosis and a first person eulogy, caches results, and falls back to deterministic output if the model is offline.&lt;/li&gt;
&lt;li&gt;Graveyard stores every obituary in &lt;code&gt;data/obituaries.jsonl&lt;/code&gt;. Use &lt;code&gt;requiem list&lt;/code&gt; and &lt;code&gt;requiem show &amp;lt;id&amp;gt;&lt;/code&gt; to browse them. The terminal renders a clean obituary card.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of this is stdlib Python, about 400 lines. No API keys. No network calls when using &lt;code&gt;--no-model&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why open innovation matters
&lt;/h2&gt;

&lt;p&gt;Crash logs are sensitive. A traceback can contain file paths, hostnames, SQL, auth headers, customer data, or tokens. Sending that to a cloud crash explainer leaks private information. Requiem runs inference on your own machine and redacts secrets before the model ever sees them.&lt;/p&gt;

&lt;p&gt;It works offline. The model, prompt, logic, and examples are all local. Swap the GGUF, edit the prompt, read every line. With &lt;code&gt;--no-model&lt;/code&gt; it is fully deterministic and works in air gapped environments.&lt;/p&gt;

&lt;p&gt;It is inspectable. Redaction rules, traceback parsing, and the eulogy prompt are all visible and easy to change. Open tools let you reason about your failures without handing them to a third party.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize categories
&lt;/h2&gt;

&lt;p&gt;Best Use of Gemma - Gemma 3 1B runs locally through llama.cpp and llama-server to write the eulogy and diagnosis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/mukuvi/requiem
&lt;span class="nb"&gt;cd &lt;/span&gt;requiem
make demo
make &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every crash gets a funeral.&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Dozer: a folder tidier that works while you sleep</title>
      <dc:creator>James Ngandu</dc:creator>
      <pubDate>Thu, 22 Oct 2026 11:29:26 +0000</pubDate>
      <link>https://dev.to/mukuvi/dozer-a-folder-tidier-that-works-while-you-sleep-6d9</link>
      <guid>https://dev.to/mukuvi/dozer-a-folder-tidier-that-works-while-you-sleep-6d9</guid>
      <description>&lt;p&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frekd9qtu4ql19nzc0gjo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frekd9qtu4ql19nzc0gjo.png" alt="Dozer's plan view: 13 files to file, 6 left alone, 4 needed the model" width="800" height="975"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;My Downloads folder is not a storage problem. It is a stack of small decisions I keep postponing. Every file asks a small question: is this an invoice, a screenshot, something I should keep at all? Answering forty of them is not hard. It is just relentless.&lt;/p&gt;

&lt;p&gt;Dozer reads a folder one level deep, one file at a time, with Gemma 3 1B running on the same machine through &lt;code&gt;llama.cpp&lt;/code&gt;. Then it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Files each file into &lt;code&gt;Invoices/&lt;/code&gt;, &lt;code&gt;Documents/&lt;/code&gt;, &lt;code&gt;Reading/&lt;/code&gt;, &lt;code&gt;Photos/&lt;/code&gt;, &lt;code&gt;Screenshots/&lt;/code&gt;, &lt;code&gt;Installers/&lt;/code&gt;, &lt;code&gt;Archives/&lt;/code&gt;, &lt;code&gt;Code/&lt;/code&gt; or &lt;code&gt;Duplicates/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Renames only the files whose names carry no information. &lt;code&gt;a1b2c3d4e5f6a7b8.pdf&lt;/code&gt; became &lt;code&gt;2026-10-08_certificate-of-participation.pdf&lt;/code&gt;. &lt;code&gt;kasha-hardware-invoice.pdf&lt;/code&gt; was left exactly as it was.&lt;/li&gt;
&lt;li&gt;Leaves a file alone when it is not sure, when it is empty, when the type is unknown, or when you touched it in the last ten minutes.&lt;/li&gt;
&lt;li&gt;Never deletes and never overwrites. A name collision gets a &lt;code&gt;-2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Writes every move to &lt;code&gt;data/runs.jsonl&lt;/code&gt;, and &lt;code&gt;undo&lt;/code&gt; puts the whole run back.&lt;/li&gt;
&lt;li&gt;Works at a pace you choose, with a budget, so an interrupted night resumes where it stopped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is for anyone whose Downloads folder has become the place where decisions go to wait. You set it going before bed, and the only thing waiting for you in the morning is a short note.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The whole thing is three commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make models &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make llama
make demo     &lt;span class="c"&gt;# dashboard on a throwaway copy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or against your real folder:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make scan &lt;span class="nv"&gt;ROOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/Downloads
make night &lt;span class="nv"&gt;ROOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/Downloads
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is a real scan of the 19-file folder that ships in &lt;code&gt;samples/&lt;/code&gt;, trimmed to keep it readable:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dozer · /tmp/dozer-demo
19 files · 13 to file · 6 left alone · 9 folders
asked the local model about 4 of 19 files, 4 answered from cache

DOCUMENTS/
  2026-10-08_certificate-of-participation.pdf
    from a1b2c3d4e5f6a7b8.pdf · 95% · The text is a certificate, which is a document.
  2026-09-22_tenancy-agreement.pdf
    from download (3).pdf · 80% · the text reads like a tenancy agreement
  random_notes.txt
    80% · the text reads like meeting notes

INVOICES/
  2026-10-12_tax-invoice.pdf
    from scan_0042.pdf · 85% · the text reads like a tax invoice
  statement-oct.pdf
    85% · the text reads like an account statement

READING/
  doc_8831.pdf
    80% · the text reads like a book

LEFT ALONE
  data_dump.csv - I'm not sure what this is.
  empty.txt - empty file
  notes.txt - This file contains a list of vegetables and spices. It's not a standard document type.
  recent-scan.pdf - touched 1 minute ago, still in use
  ubuntu-notes.md - The text is too short to determine the category.
  weird.xyz - not a type Dozer knows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then the overnight pass:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make night ROOT=/tmp/dozer-demo PACE=2
[1/13] a1b2c3d4e5f6a7b8.pdf -&amp;gt; Documents/2026-10-08_certificate-of-participation.pdf
[2/13] doc_8831.pdf -&amp;gt; Reading/doc_8831.pdf
[3/13] download (3).pdf -&amp;gt; Documents/2026-09-22_tenancy-agreement.pdf
...
[13/13] VSCodeUserSetup-x64-1.94.2.exe -&amp;gt; Installers/VSCodeUserSetup-x64-1.94.2.exe

MORNING NOTE
  I left 6 files alone. I filed 13 files into 3 Documents, 2 Invoices, 2
  Reading. 4 of them needed the model and the rest were rules. The pass took 24
  seconds and nothing left this machine.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And the folder afterwards:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/tmp/dozer-demo
├── Archives/        node-v22.11.0-linux-x64.tar.xz
├── Code/            scrape.py
├── Documents/       2026-09-22_tenancy-agreement.pdf
│                    2026-10-08_certificate-of-participation.pdf
│                    random_notes.txt
├── Duplicates/      IMG_20260918_0712 (1).heic
├── Installers/      VSCodeUserSetup-x64-1.94.2.exe
├── Invoices/        2026-10-12_tax-invoice.pdf
│                    statement-oct.pdf
├── Photos/          IMG_20260918_0712.heic
├── Reading/         doc_8831.pdf
│                    thinking-fast-and-slow.epub
├── Screenshots/     Screenshot 2026-10-20 at 09.14.02.png
├── data_dump.csv
├── empty.txt
├── notes.txt
├── recent-scan.pdf
├── ubuntu-notes.md
└── weird.xyz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The six files still in the root are the point.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1kwnlh3hn5n8iboeo2ld.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1kwnlh3hn5n8iboeo2ld.png" alt="The morning note" width="799" height="612"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/mukuvi" rel="noopener noreferrer"&gt;
        mukuvi
      &lt;/a&gt; / &lt;a href="https://github.com/mukuvi/dozer" rel="noopener noreferrer"&gt;
        dozer
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A folder tidier that works while you sleep, with Gemma 3 1B running locally.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Dozer&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A folder tidier that works while you sleep, with an open-weight model at the core.&lt;/p&gt;

&lt;p&gt;Dozer reads your Downloads one file at a time with &lt;strong&gt;Gemma 3 1B&lt;/strong&gt; running through
&lt;code&gt;llama.cpp&lt;/code&gt; on your own machine. It files invoices, documents, installers, photos
duplicates and the rest into folders, renames the files that have useless names, and
leaves everything it is not sure about exactly where it is.&lt;/p&gt;
&lt;p&gt;No API key. No upload. No account.&lt;/p&gt;
&lt;p&gt;Write-up: &lt;a href="https://dev.to/mukuvi/dozer-a-folder-tidier-that-works-while-you-sleep-6d9" rel="nofollow"&gt;Dozer: a folder tidier that works while you sleep&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it actually does&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Files the mess in one folder, one level deep.&lt;/li&gt;
&lt;li&gt;Renames only files whose names carry no information (&lt;code&gt;scan_0042.pdf&lt;/code&gt;, &lt;code&gt;IMG_2301.HEIC&lt;/code&gt;
&lt;code&gt;download (3).pdf&lt;/code&gt;). A name a human chose is left alone.&lt;/li&gt;
&lt;li&gt;Keeps identical files in &lt;code&gt;Duplicates/&lt;/code&gt; instead of deleting anything.&lt;/li&gt;
&lt;li&gt;Leaves files alone when it is not sure, when the file is empty, when the type is
unknown, or…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/mukuvi/dozer" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;MIT licensed, 27 tests that run without a model. &lt;code&gt;make test&lt;/code&gt; on a clean checkout.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  A[Folder, one level deep] --&amp;gt; B[hash + short text preview]
  B --&amp;gt; C{extension or text rule?}
  C --&amp;gt;|yes| D[category + confidence]
  C --&amp;gt;|no| E[Gemma 3 1B in a warm llama-server]
  E --&amp;gt; D
  D --&amp;gt; F{confidence &amp;gt;= 0.75?}
  F --&amp;gt;|yes| G[folder + safe name]
  F --&amp;gt;|no| H[left alone, with the reason]
  G --&amp;gt; I[apply one file at a time]
  I --&amp;gt; J[(data/runs.jsonl)]
  J --&amp;gt; K[undo]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Three decisions make a 1B model trustworthy enough to move files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rules first.&lt;/strong&gt; An &lt;code&gt;.exe&lt;/code&gt; is an installer. A document with &lt;code&gt;Tax Invoice&lt;/code&gt; and &lt;code&gt;Amount due&lt;/code&gt; in the text is an invoice. In the scan above, 15 of 19 files never reached the model. Rules are free, instant and predictable, and they leave the model only the cases that are actually ambiguous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A confidence floor.&lt;/strong&gt; Below 0.75 Dozer does nothing and says why. &lt;code&gt;data_dump.csv&lt;/code&gt; is still in the folder because the model answered &lt;code&gt;other&lt;/code&gt; at 0.0 confidence and said "I'm not sure what this is." That is a correct answer. A tidier that guesses is worse than a folder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A warm server, not a subprocess per file.&lt;/strong&gt; Every call goes to one &lt;code&gt;llama-server&lt;/code&gt; on localhost, with the prompt prefix cached. On this 2015 dual-core laptop the difference is large. Loading the model per file costs more than the answer, and the shared prefix means the instructions are evaluated once per run instead of once per file. A fresh ambiguous file takes about thirty seconds here. The first one in a run also pays for loading 806 MB.&lt;/p&gt;

&lt;p&gt;Model answers are also cached by content hash, so a second pass over a folder you already scanned costs nothing. That is why the demo above says "4 needed the model, 4 answered from cache."&lt;/p&gt;

&lt;p&gt;The safety rules are simple. No symlinks, no subfolders, no deletion, no overwrite, and no rename of a name a person wrote. &lt;code&gt;undo&lt;/code&gt; reverses the last run in order and refuses to overwrite a file you put back yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it got wrong
&lt;/h3&gt;

&lt;p&gt;Three things, and they are the interesting part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It called a book chapter a document.&lt;/strong&gt; &lt;code&gt;doc_8831.pdf&lt;/code&gt; starts with "Chapter 3: Trees and Recursion", and the model filed it under Documents at 95% confidence. The fix was one rule, not a bigger model or a longer prompt: text with &lt;code&gt;Chapter N&lt;/code&gt;, &lt;code&gt;Table of Contents&lt;/code&gt; or an ISBN is reading. Rules are how you correct a small model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;kind&lt;/code&gt; field was noisy.&lt;/strong&gt; Asked about a certificate, it returned &lt;code&gt;kind: "tax invoice"&lt;/code&gt;. The prompt's examples were doing more work than the instruction, so I removed them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It invented file names in the morning note.&lt;/strong&gt; I wanted the model to write the note you see at 7am. It produced "I left 6 files, including the one named 'Project Alpha'". There is no Project Alpha in the folder. The note is a template now.&lt;/p&gt;

&lt;p&gt;That is the rule I ended up with. The model answers one narrow question, says how sure it is, and code checks the answer. It does not get to tell me what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The folder never leaves the machine.&lt;/strong&gt; Downloads is where bank statements, contracts, ID scans and photos of your kitchen end up. A closed sorter has to upload them to read them. Running the weights locally means there is no trust boundary to audit, because there is no boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works with no network.&lt;/strong&gt; After the one-time model download, the scanner, the rules, the model and the log are all on disk. On a metered or unreliable connection that matters more than any feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can change the sorter.&lt;/strong&gt; Swapping Gemma for a bigger model, a different quantisation, or a model fine-tuned on your own vendor names is a path and an environment variable. The prompt is a string in the repo. When it behaved badly I could read it and fix it the same afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing per file.&lt;/strong&gt; No per-document price, no rate limit, no key that expires. The run above cost nothing, which matters most for the person with a full folder and no budget for another subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I built this with an agent, and the session is saved. It includes the benchmark that pushed me from a subprocess per file to a warm server, and the moment the model invented a file name in the morning note:&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="OpenCode"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building Dozer: a local folder tidier for Hacktoberfest Week 3&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Proceed with the Hacktoberfest Week 3 challenge (Slow and Steady). Build something with open-source AI that rewards laziness.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Plan: a local folder tidier called Dozer. Gemma 3 1B through llama.cpp decides where a file belongs; deterministic rules handle the obvious cases; anything below 0.75 confidence is left exactly where it is. Python + FastAPI, JSONL log, undo, and a paced overnight mode.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-getchallenges"&gt;get_challenges&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;Week 3 details&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Week 3 details&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Week 3 runs Oct 19-25, theme 'Slow and Steady': build something that rewards laziness. Judging: writing quality weighted most, then theme relevance, creativity, technical execution.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;First I checked the hardware. An i5-5200U with 4 threads, and the machine was already loaded. A quick llama-bench run showed 3.81 t/s prompt and 0.19 t/s generation. Loading the 806 MB model per file with llama-cli would make the tool unusable, so the model layer became a warm llama-server with prefix caching instead.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-llamabench"&gt;llama-bench&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;gemma-3-1b-it-Q4_K_M, 4 threads, pp32/tg8&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;gemma-3-1b-it-Q4_K_M, 4 threads, pp32/tg8&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;pp32: 3.81 t/s, tg8: 0.19 t/s&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The core is plan.py. Extension and text rules run first, so most files never reach the model. Rules also produce the safe filenames: only names with no information (scan_0042, IMG_2301, a 16-character hash) get renamed, and only when the model or a rule has a title to offer. Duplicates are compared by SHA-256 and moved to Duplicates/ instead of being deleted.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-pytest"&gt;pytest&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;tests/test_plan.py&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;tests/test_plan.py&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Failed: two tests assumed a .pdf extension meant pdftotext could read plain text. Fixed by using the real sample PDFs, and an autouse fixture that keeps tests away from the local model.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Then the first real scan. The model called a book chapter (doc_8831.pdf, 'Chapter 3: Trees and Recursion') a Document at 95% confidence, and its 'why' text was boilerplate. I did not reach for a bigger model. I added one rule: text with 'Chapter N', 'Table of Contents' or an ISBN is Reading. That is the lesson the post is built around.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;I also tried letting the model write the morning note. It produced: "I left 6 files, including the one named 'Project Alpha'" - there is no Project Alpha in the folder. It invented file names. The note became a deterministic template, and the model is now trusted with exactly one decision: the category.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Final run on the 19-file sample folder: 13 filed, 6 left alone, 9 folders, 3 renames, 4 files needed the model. Paced at 2 seconds per file, the pass took 24 seconds. A fresh ambiguous file costs about 28 seconds on this laptop, and answers are cached by content hash so the next scan is instant.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-clipynight"&gt;cli.py night&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;night /tmp/dozer-demo --pace 2&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;night /tmp/dozer-demo --pace 2&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;13 files moved in 24 seconds. Morning note: I left 6 files alone. I filed 13 files into 3 Documents, 2 Invoices, 2 Reading. 4 of them needed the model and the rest were rules.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Shipped: 27 tests passing, dashboard screenshots from a real run, README, MIT license, render.yaml for the hosted demo. The repo is github.com/mukuvi/dozer and the Week 3 post is on DEV with the #hf26challenge tag.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        8 of 8 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;Thirteen files moved without me. Three names got better. One duplicate pair got separated, and neither copy was deleted.&lt;/p&gt;

&lt;p&gt;The numbers are less flattering than a pitch. The six files left alone are six decisions I still have to make. Dozer did not remove the work. It batched it and moved it to a moment when I might feel like doing it. What it did remove was the interruption: you open the folder for one file and leave twenty minutes later with nothing decided.&lt;/p&gt;

&lt;p&gt;Machine time: 24 seconds, paced at two seconds per file. Human time: one glance at a morning note.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 3 1B (&lt;code&gt;Q4_K_M&lt;/code&gt;, 806 MB) does every ambiguous classification, running locally through &lt;code&gt;llama.cpp&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render&lt;/strong&gt;: &lt;code&gt;render.yaml&lt;/code&gt; deploys the dashboard as a hosted demo. The free instance cannot hold a 0.8 GB model, so the hosted demo runs the rule engine only, on a throwaway copy of the sample folder, and the UI says so. The model path is the one you run at home.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built for Hacktoberfest 2026. No API key, no upload, no account, and nothing leaves the machine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Quiet Ledger: a receipt sorter that never uploads your receipts</title>
      <dc:creator>James Ngandu</dc:creator>
      <pubDate>Sun, 18 Oct 2026 17:37:15 +0000</pubDate>
      <link>https://dev.to/mukuvi/quiet-ledger-a-receipt-sorter-that-never-uploads-your-receipts-ion</link>
      <guid>https://dev.to/mukuvi/quiet-ledger-a-receipt-sorter-that-never-uploads-your-receipts-ion</guid>
      <description>&lt;p&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hf26"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg9hb3a22nlhd8inf0q9o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg9hb3a22nlhd8inf0q9o.png" alt="Quiet Ledger's ledger view" width="800" height="719"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Quiet Ledger is a receipt and invoice sorter for one-person businesses, and it runs entirely on your own machine.&lt;/p&gt;

&lt;p&gt;You drop a PDF invoice, a photo of a receipt, or paste the text. Gemma 3 1B reads it locally, drafts a bookkeeping entry, and shows it to you. You fix anything it got wrong, and then you export a CSV with the columns an accountant actually wants.&lt;/p&gt;

&lt;p&gt;That is the whole loop. It exists because I watched a friend who freelances spend the last Sunday of every month with a shoebox of receipts, a bank statement, and a spreadsheet. The work is not hard. It is just relentless, and every cloud tool wants the receipts uploaded to a server first.&lt;/p&gt;

&lt;p&gt;Three things make it different from the receipt apps I tried:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The document never leaves the laptop.&lt;/strong&gt; No upload, no account, no API key, no per-document fee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model drafts; you approve.&lt;/strong&gt; This is not a black box that files your taxes. It is a fast first pass you correct in a few seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The output is a file you own.&lt;/strong&gt; &lt;code&gt;data/receipts.jsonl&lt;/code&gt; on disk, and a CSV you can hand to anyone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It reads the things a freelancer's shoebox actually contains: supermarket receipts, a hardware store tax invoice, a design retainer from another country. It keeps currencies separate instead of quietly adding shillings to dollars, which is the kind of bug that ruins a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The demo is three commands, because the whole point is that it runs where the documents are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make models &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make llama
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LLAMA_BIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/.llama.cpp/build/bin/llama-cli"&lt;/span&gt;
make run            &lt;span class="c"&gt;# http://127.0.0.1:8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no hosted demo link, and that is deliberate: hosting it would mean uploading other people's receipts, which is the exact thing this project refuses to do. The repo ships three sample documents so the screenshot above is reproducible from a clean clone.&lt;/p&gt;

&lt;p&gt;What a real run looks like. I dropped the sample tax invoice PDF, and 43 seconds later on four CPU cores this came back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"vendor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"KASHA HARDWARE LTD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hardware"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4850.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"KES"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;620.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payment_method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mpesa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"invoice_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-2026-1044"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extracted_by"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gemma-3-1b-it"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the export:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;date,vendor,category,total,currency,tax,payment_method,invoice_number
2026-10-15,MAMA NJERI'S KITCHEN,meals,1400.00,KES,,cash,
2026-10-12,KASHA HARDWARE LTD,hardware,4850.00,KES,620.00,mpesa,INV-2026-1044
2026-10-09,Northwind Studio LLC,professional-services,2083.20,USD,163.20,bank transfer,NW-8841
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/mukuvi/quiet-ledger" rel="noopener noreferrer"&gt;https://github.com/mukuvi/quiet-ledger&lt;/a&gt; (MIT licensed).&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;A small FastAPI server, a single-page PWA, and the plumbing from my last offline project, collapsed into one app: Gemma 3 1B with &lt;code&gt;llama.cpp&lt;/code&gt;, plus &lt;code&gt;pdftotext&lt;/code&gt; and &lt;code&gt;tesseract&lt;/code&gt; for getting text out of files.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  A[PDF / photo / paste] --&amp;gt; B[FastAPI]
  B --&amp;gt; C[pdftotext or tesseract]
  C --&amp;gt; D[Gemma 3 1B via llama.cpp]
  D --&amp;gt; E[normalise + sanity checks]
  E --&amp;gt; F[you approve or fix]
  F --&amp;gt; G[(data/receipts.jsonl)]
  G --&amp;gt; H[CSV for the accountant]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The model gets a strict, JSON-only prompt at temperature 0. No chat, no markdown, no commentary, and an explicit instruction to return &lt;code&gt;null&lt;/code&gt; rather than invent a value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Return JSON only. No markdown, no explanation, no commentary.
- Use null when a value is not printed on the receipt. Never invent values.
- "total" is the final amount paid. If the receipt shows both a subtotal
  and a total or amount due, use the final amount due, never the subtotal.
- "tax" is the tax or VAT amount in money, not the tax rate.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last pair of rules exists because I watched the model get both wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The normaliser is where the real work is.&lt;/strong&gt; A 1B model is a good reader and a mediocre accountant. Three failure modes showed up in my own test runs, and each one is now handled in code rather than papered over:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It returned &lt;code&gt;16%&lt;/code&gt;, the VAT rate, in the &lt;code&gt;tax&lt;/code&gt; field. Money and rates are now parsed differently, and a tax amount smaller than one percent of the total is treated as a rate and replaced with the printed tax line.&lt;/li&gt;
&lt;li&gt;It returned the &lt;strong&gt;subtotal&lt;/strong&gt; as the total on an invoice that also had an &lt;code&gt;Amount due&lt;/code&gt; line. The printed amount due now wins over the model's answer.&lt;/li&gt;
&lt;li&gt;It labelled a hardware purchase &lt;code&gt;other&lt;/code&gt;. When the model has no useful opinion, a keyword pass over the receipt text gets a second chance, so &lt;code&gt;hardware&lt;/code&gt; wins over &lt;code&gt;other&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else is deliberately boring. Amounts lose their symbols and separators, dates become ISO, categories map onto a fixed list of twelve, and a payment method of &lt;code&gt;M-PESA&lt;/code&gt; becomes &lt;code&gt;mpesa&lt;/code&gt; so it does not split into three spellings. Files are deduplicated by SHA-256, so dragging the same receipt in twice does nothing.&lt;/p&gt;

&lt;p&gt;Storage is JSON Lines. Not Postgres, not SQLite, not a vector database. A freelancer has a few thousand receipts, not a data warehouse, and a plain file is easier to back up, grep, and explain.&lt;/p&gt;

&lt;p&gt;If no model is on disk, the app still runs and labels every entry &lt;code&gt;fallback-parser&lt;/code&gt;. I did that on purpose: someone should be able to clone the repo, see the workflow, and decide whether the model is worth the download before committing to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Would Pay For This
&lt;/h2&gt;

&lt;p&gt;The person I built it for is a freelancer or a two-person studio with client receipts in three currencies and an accountant who charges by the hour to sort them.&lt;/p&gt;

&lt;p&gt;The pitch is not "another subscription". It is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy that is real, not a policy page.&lt;/strong&gt; Client names, supplier prices, and where you were on a Tuesday never leave the machine. For anyone under a confidentiality clause, that is the product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero marginal cost.&lt;/strong&gt; No per-receipt pricing and no seat count. The download is 0.8 GB, once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works where the work happens.&lt;/strong&gt; A market trader with a laptop and no reliable connection can sort receipts in the evening. A cloud tool cannot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The accountant gets the same CSV either way.&lt;/strong&gt; The value is the hour back, not a new format to learn.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest price for this is low, and that is fine. A tool that costs nothing to run can be priced at what the saved time is worth, and the model can be swapped for a bigger one later without renegotiating a contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Every part of this that matters is open, and each one is a practical advantage rather than a slogan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The document stays private because inference is local.&lt;/strong&gt; A receipt is one of the most sensitive documents a small business owns. A closed sorter has to move it to a server to read it. Running the weights on the same laptop means there is no trust boundary to audit, because there is no boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It runs with no network.&lt;/strong&gt; The model, the prompt, and the parser are all on disk. The app is a PWA that opens offline. In a place where connectivity is metered or missing, the open version is not a worse version of the cloud one. It is the only version that works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I can change the accountant.&lt;/strong&gt; Gemma runs from a local GGUF file, so moving to a larger Gemma, a different quantisation, or a different model entirely is an environment variable, not a vendor conversation. A regional list of vendor names and tax labels is a fine-tuning run away. That kind of adaptation is impossible when the model is someone else's endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I can inspect the reasoning.&lt;/strong&gt; When the model put a tax rate in the tax field, I could read the prompt, change it, and re-run the same document. With a closed API I would be guessing at what changed between two versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost stays at zero.&lt;/strong&gt; No API key, no rate limit, no bill that grows with how diligent you are about keeping receipts. That matters most for the people who most need the tool.&lt;/p&gt;

&lt;p&gt;The open pieces are not a licensing detail here. They are the reason the product can promise privacy, offline use, and a price of nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 3 1B runs locally through &lt;code&gt;llama.cpp&lt;/code&gt; and does every extraction in the app. Its output is then validated by the normaliser, which is the part that makes a 1B model trustworthy enough for bookkeeping.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built for Hacktoberfest 2026. No API key, no upload, no outbound calls when you sort a receipt.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Field notes from running small services</title>
      <dc:creator>James Ngandu</dc:creator>
      <pubDate>Tue, 06 Oct 2026 12:46:34 +0000</pubDate>
      <link>https://dev.to/mukuvi/field-notes-from-running-small-services-52j6</link>
      <guid>https://dev.to/mukuvi/field-notes-from-running-small-services-52j6</guid>
      <description>&lt;p&gt;I keep a running list of the things I do on every small service I run. None of it is clever. All of it has saved me at least once. I wrote up the full setup in &lt;a href="https://dev.to/mukuvi/you-probably-do-not-need-kubernetes-36on"&gt;You probably do not need Kubernetes&lt;/a&gt;. These are the notes I actually come back to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Harden the box before anything else
&lt;/h2&gt;

&lt;p&gt;Before the app goes anywhere near a server, the server has to be safe to expose. No root login. Key-only SSH. A firewall open on 80, 443, and SSH, and nothing else. Something like fail2ban to cut down the noise.&lt;/p&gt;

&lt;p&gt;The step people skip is disabling password authentication. Do it only after you have confirmed key login works in a second session. I have locked myself out doing it the fast way, and it is a bad hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bind the app to localhost
&lt;/h2&gt;

&lt;p&gt;One line, worth more than most of the architecture decisions I make:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:3000:3000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app port is reachable only from the machine itself, so the reverse proxy is the only door in. If the proxy config breaks, you have a broken site, not an exposed service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the certificate now, not later
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;certbot renew &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five seconds. If it fails, I want to know today rather than the morning a certificate expires and the site goes red.&lt;/p&gt;

&lt;h2&gt;
  
  
  A deploy you can reverse is a deploy you will actually do
&lt;/h2&gt;

&lt;p&gt;If deploying feels dangerous, you avoid it, and then you ship one scary change. The fix is small and reversible deploys. On a push, run the tests, and if they pass, update the box. Keep secrets in the platform, never in the repo. Keep the previous version one step away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimum observability
&lt;/h2&gt;

&lt;p&gt;A server you cannot see is a server you are guessing about. For a small service that means a health check that reflects real readiness, an external monitor that pages you when it stops answering, and logs you can read. That is enough. Collecting more than that, before there is a question to answer, is procrastination wearing a dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reach for Kubernetes late
&lt;/h2&gt;

&lt;p&gt;Kubernetes earns its keep when there are several services that scale independently, or a team that deploys without you, or a scheduler with real work to do. Until then it is a second job you volunteered for. I would rather run one box I understand than a cluster I have to look up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that is not technical
&lt;/h2&gt;

&lt;p&gt;The same shape of work shows up outside infrastructure. Leading the IEEE student branch at university was the same problem: limited time, unclear requirements, people with different goals. The clarity is the job. The tools are secondary.&lt;/p&gt;

&lt;p&gt;None of these notes are original. They are just the ones that keep being true. If you run a small service, the boring version you understand will almost always beat the impressive version you do not.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>career</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Infrastructure was not where I started</title>
      <dc:creator>James Ngandu</dc:creator>
      <pubDate>Tue, 06 Oct 2026 12:43:45 +0000</pubDate>
      <link>https://dev.to/mukuvi/infrastructure-was-not-where-i-started-3713</link>
      <guid>https://dev.to/mukuvi/infrastructure-was-not-where-i-started-3713</guid>
      <description>&lt;p&gt;Infrastructure was not where I started.&lt;/p&gt;

&lt;p&gt;I started in C. Pointers, arrays, strings, then memory allocation, linked lists, and eventually a shell. For months the work was small and low level: read a line, parse it, fork, exec, wait. Debug a segfault until it finally made sense. Recompile.&lt;/p&gt;

&lt;p&gt;At the time it did not feel like infrastructure. It felt like fighting the machine. The compiler disagreed with me constantly. Memory had to be handed back by hand. Almost nothing was convenient.&lt;/p&gt;

&lt;p&gt;Then I moved into Linux administration, Docker, CI/CD, and running production systems. And I kept noticing that the hard part was familiar.&lt;/p&gt;

&lt;p&gt;When a container exits with code 137, that is the OOM killer, and it helps that I once had to think about how memory is actually used. When a process will not die, signals stopped being mysterious back in the shell project. When a service will not start, reading the log and following the failure chain is the same skill as tracing why a pointer wrote to the wrong place.&lt;/p&gt;

&lt;p&gt;The language changed. The habit did not. The habit is this: understand the layer below the one you work in, so that when something breaks you can reason about why instead of guessing.&lt;/p&gt;

&lt;p&gt;That is the thing ALX actually gave me, more than any single technology. It made being lost normal, and it made me comfortable opening the box.&lt;/p&gt;

&lt;p&gt;If you are in the middle of the low level work right now, and it feels disconnected from the jobs you want, this is what I would say. The specific skills transfer less than you expect. The instincts transfer more. And the instincts are the part nobody can take from you.&lt;/p&gt;

&lt;p&gt;Still learning. Still running small things on a single box more often than not.&lt;/p&gt;

</description>
      <category>career</category>
      <category>devops</category>
      <category>linux</category>
      <category>beginners</category>
    </item>
    <item>
      <title>You probably do not need Kubernetes</title>
      <dc:creator>James Ngandu</dc:creator>
      <pubDate>Tue, 06 Oct 2026 12:42:16 +0000</pubDate>
      <link>https://dev.to/mukuvi/you-probably-do-not-need-kubernetes-36on</link>
      <guid>https://dev.to/mukuvi/you-probably-do-not-need-kubernetes-36on</guid>
      <description>&lt;p&gt;Every few months I talk to someone who wants to put a small app on Kubernetes. Usually the app is one container, a database, and a handful of users. Often it is not even in production yet.&lt;/p&gt;

&lt;p&gt;Kubernetes is excellent at a specific job: running many services, across many machines, with a team that deploys independently. If that is not your situation, it is a large amount of complexity you are taking on for a problem you do not have.&lt;/p&gt;

&lt;p&gt;Most of my services run on a single VPS, and this is roughly how I set one up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Harden the box first
&lt;/h2&gt;

&lt;p&gt;Before anything runs, the server needs to be safe to expose. That means no root login, no password authentication, a firewall with only the ports I need, and something like fail2ban to cut down the noise. It is ten minutes of work, once.&lt;/p&gt;

&lt;p&gt;The step people skip is disabling password authentication, because it is the one that can lock you out. Confirm your key login works in a second session before you close the first. I have gotten that wrong, and it is a bad hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  The app runs in a container, bound to localhost
&lt;/h2&gt;

&lt;p&gt;I run almost everything in Docker. One detail matters more than the rest: I bind the app port to &lt;code&gt;127.0.0.1&lt;/code&gt; instead of exposing it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:3000:3000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single choice means the container is never directly reachable from the internet. Only the reverse proxy in front of it can talk to it. If I misconfigure the proxy, the worst case is a broken site, not an open service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nginx sits in front and terminates TLS
&lt;/h2&gt;

&lt;p&gt;Nginx does three jobs for me. It proxies to the app, it terminates HTTPS, and it is the layer I edit when routing changes or a new domain shows up. Certbot handles the certificates, including the renewal timer, so I am not the renewal timer.&lt;/p&gt;

&lt;p&gt;The one check I run on every new server is &lt;code&gt;certbot renew --dry-run&lt;/code&gt;. If that fails, I want to know today rather than the morning a certificate expires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploys should not need me
&lt;/h2&gt;

&lt;p&gt;The pipeline is short on purpose. On a push to main, GitHub Actions runs the tests, and if they pass it updates the box and restarts the container. Secrets live in GitHub, never in the repository, and the change is small enough that rolling back is just pointing at the previous commit.&lt;/p&gt;

&lt;p&gt;A deploy I can reverse in a minute is a deploy I am willing to run often. That matters more than any particular tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can I see it?
&lt;/h2&gt;

&lt;p&gt;A server I cannot observe is a server I am guessing about. I want a health check that reflects real readiness, an external monitor that pings it and shouts at me when it stops answering, and logs I can actually read. That is enough for a small service. Collecting more than that, before there is a question to answer, is usually procrastination wearing a dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I would actually reach for Kubernetes
&lt;/h2&gt;

&lt;p&gt;When there are several services that scale independently, or a team that needs to deploy without asking me, or a scheduler with real work to do. When the complexity is buying something I need.&lt;/p&gt;

&lt;p&gt;Until then: one box, a few containers, a proxy, and a pipeline I understand end to end. It runs, I can reason about it at 2am, and I can replace it when the requirements change.&lt;/p&gt;

&lt;p&gt;That is usually the right trade. The impressive setup can wait until the boring one stops being enough.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>linux</category>
      <category>nginx</category>
    </item>
    <item>
      <title>My ALX journey: from Hello World in C to a load balancer and my own shell</title>
      <dc:creator>James Ngandu</dc:creator>
      <pubDate>Tue, 06 Oct 2026 12:35:24 +0000</pubDate>
      <link>https://dev.to/mukuvi/my-alx-journey-from-hello-world-in-c-to-a-load-balancer-and-my-own-shell-47ci</link>
      <guid>https://dev.to/mukuvi/my-alx-journey-from-hello-world-in-c-to-a-load-balancer-and-my-own-shell-47ci</guid>
      <description>&lt;p&gt;ALX is the software engineering programme that half of tech Twitter in Africa either went through or is currently grinding through. It is run by ALX Africa, built on the Holberton School curriculum, sponsored through the Mastercard Foundation, and it does not cost the learner anything. Twelve months, no lectures, project after project, and a peer learning model that takes a few weeks to get used to.&lt;/p&gt;

&lt;p&gt;This is what that year looked like for me, and what is sitting in my repos to show for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the programme works
&lt;/h2&gt;

&lt;p&gt;It runs in sprints. You start with foundations, then move into a specialization. You advance by finishing projects, not by passing an exam. There is a completion cutoff around 80 percent of the projects across the sprints, with a minimum average in each one. Nobody stands in front of a class. You read the material, you attempt the project, you get stuck, you ask the people around you, and you submit.&lt;/p&gt;

&lt;p&gt;The curriculum is deliberately vague in places, and that is the point. The phrase they use is owning your own learning, and after a few months it stops sounding like a slogan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Foundations: Git, the command line, and C
&lt;/h2&gt;

&lt;p&gt;The first thing you learn is that the terminal is not scary. Git and command line basics come before any real code, then Bash, and then C, which is where the real work started.&lt;/p&gt;

&lt;p&gt;My &lt;a href="https://github.com/mukuvi/alx-low_level_programming" rel="noopener noreferrer"&gt;alx-low_level_programming&lt;/a&gt; repo is the record of that stretch. It starts with a Hello World and works through variables, conditions, loops, and functions. Then debugging. Then the thing that humbles everyone: pointers. Pointers, arrays, and strings get four separate project sets. After that come recursion, static and dynamic libraries, command line arguments, &lt;code&gt;malloc&lt;/code&gt; and &lt;code&gt;free&lt;/code&gt;, the preprocessor, structs, function pointers, variadic functions, singly and doubly linked lists, bit manipulation, file I/O, hash tables, Makefiles, and search algorithms.&lt;/p&gt;

&lt;p&gt;Somewhere in there you also reimplement formatted output, which is the moment variadic functions stop being a curiosity, and later you build a working shell: read a line, parse it, fork, exec, wait, and loop. That shell project is where all the small pieces suddenly have to work together in one program, and it is the closest the low level work gets to feeling like an actual system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Higher level: Python and JavaScript
&lt;/h2&gt;

&lt;p&gt;Then the language changes and the ideas stay the same. My &lt;a href="https://github.com/mukuvi/alx-higher_level_programming" rel="noopener noreferrer"&gt;alx-higher_level_programming&lt;/a&gt; repo goes from Python fundamentals (loops, imports, modules) into data structures, exceptions, classes, inheritance, test driven development, and a project called almost a circle that builds a small shape hierarchy from scratch. Later it moves into object relational mapping, network requests, and then JavaScript: the warm up, then objects, scopes, and closures, then web scraping and jQuery.&lt;/p&gt;

&lt;p&gt;The interesting part is not learning a second language. It is noticing how much of C follows you into Python. Memory, references, and what an object actually is stop being abstract once you have spent months managing all of it by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  System engineering and DevOps
&lt;/h2&gt;

&lt;p&gt;The third thread is the one that changed how I think about software the most. My &lt;a href="https://github.com/mukuvi/alx-system_engineering-devops" rel="noopener noreferrer"&gt;alx-system_engineering-devops&lt;/a&gt; repo covers shell basics, permissions, redirections, variables, loops, conditions and parsing, processes and signals, and regular expressions. From there it goes where most beginner courses never do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Networking, in two parts.&lt;/li&gt;
&lt;li&gt;Web infrastructure design, the diagrams that explain how a request actually reaches an application.&lt;/li&gt;
&lt;li&gt;SSH, and hardening it.&lt;/li&gt;
&lt;li&gt;A web server, then an application server.&lt;/li&gt;
&lt;li&gt;A load balancer, and later HTTPS and SSL certificates.&lt;/li&gt;
&lt;li&gt;MySQL, including primary and replica setups.&lt;/li&gt;
&lt;li&gt;APIs, and then advanced API work.&lt;/li&gt;
&lt;li&gt;Web stack debugging, four separate rounds of it.&lt;/li&gt;
&lt;li&gt;Webstack monitoring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The web stack debugging projects are brutal, and I mean that as a compliment. You get a broken server and no description of what is wrong, and you have to find it. That is closer to the real job than any tutorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ALX actually taught me
&lt;/h2&gt;

&lt;p&gt;Less about any single language, and more about process. How to read documentation, how to break a big problem into pieces, how to ask a good question, and how to keep going when a project makes no sense for the first two hours. The deadlines are real and the volume is high, so you also learn to ship something that works instead of something perfect.&lt;/p&gt;

&lt;p&gt;The other thing is people. The peer model is not decoration. A lot of what I know came from sitting with someone late at night tracing a segfault until one of us saw it. A lecture does not give you that.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you are in ALX right now
&lt;/h2&gt;

&lt;p&gt;A few things I would tell anyone in the middle of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not skip the low level work. It is the foundation everything else sits on.&lt;/li&gt;
&lt;li&gt;Write down what broke and how you fixed it. It becomes your own documentation.&lt;/li&gt;
&lt;li&gt;Ask for help earlier. Suffering for six hours before asking is not discipline, it is wasted time.&lt;/li&gt;
&lt;li&gt;Push your work to GitHub as you go. Those repos are a portfolio at the end, and they are the reason people believe you when you describe what you can do.&lt;/li&gt;
&lt;li&gt;Do not compare your progress to the person next to you. The sprints are hard for everyone, in different places.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The repos
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/mukuvi/alx-low_level_programming" rel="noopener noreferrer"&gt;alx-low_level_programming&lt;/a&gt;: C, from Hello World to a working shell.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mukuvi/alx-higher_level_programming" rel="noopener noreferrer"&gt;alx-higher_level_programming&lt;/a&gt;: Python and JavaScript.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mukuvi/alx-system_engineering-devops" rel="noopener noreferrer"&gt;alx-system_engineering-devops&lt;/a&gt;: shell, networking, servers, and the debugging that comes with them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are going through ALX, or thinking about starting, and you want to compare notes, the comments are open.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>career</category>
      <category>c</category>
      <category>devops</category>
    </item>
    <item>
      <title>I built ARN.IO, a reading companion that keeps your place and your notes</title>
      <dc:creator>James Ngandu</dc:creator>
      <pubDate>Tue, 06 Oct 2026 12:26:33 +0000</pubDate>
      <link>https://dev.to/mukuvi/i-built-a-reading-app-because-i-never-remembered-what-i-read-416m</link>
      <guid>https://dev.to/mukuvi/i-built-a-reading-app-because-i-never-remembered-what-i-read-416m</guid>
      <description>&lt;p&gt;Browsers are good at showing you a page and bad at remembering it. Read a book online, close the tab, and a week later you are scrolling to find your place and wondering what you highlighted. ARN.IO is a small attempt to fix that.&lt;/p&gt;

&lt;p&gt;It is a digital library and reading companion. You browse a collection, read chapter by chapter in the browser, and the app keeps track of where you are, so picking a book back up does not start with hunting for your place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A curated library you can search by title, author, or genre.&lt;/li&gt;
&lt;li&gt;Reading in the browser, one chapter at a time.&lt;/li&gt;
&lt;li&gt;Progress per book, with the current chapter and a percentage, plus reading streaks that count minutes and pages.&lt;/li&gt;
&lt;li&gt;Notes attached to a specific book and chapter, so the note lives next to the thing it is about instead of in a separate app.&lt;/li&gt;
&lt;li&gt;Upload your own documents. PDF, Word, TXT, Markdown, HTML, and RTF are accepted and split into chapters automatically.&lt;/li&gt;
&lt;li&gt;An AI reading assistant you can ask about themes, characters, summaries, or what to read next.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the AI part, honestly
&lt;/h2&gt;

&lt;p&gt;The assistant works out of the box. It ships with built-in keyword matched responses, so there is no API key to paste and nothing to pay for, and it still answers the common questions about a book. That was deliberate. I wanted someone to clone the repo, run it, and have the assistant actually reply instead of showing a configuration error. The chat is per book and the history is kept, so if you want to wire it to a real model later the interface is already there.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it is put together
&lt;/h2&gt;

&lt;p&gt;It is split into a client and a server. There is a &lt;code&gt;run.sh&lt;/code&gt; at the root if you want to start everything at once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x run.sh
./run.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a &lt;code&gt;docker-compose.yml&lt;/code&gt; with separate Dockerfiles for the client and the server, if you would rather not install anything locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notes on the upload
&lt;/h2&gt;

&lt;p&gt;Splitting a document into chapters across six formats is the fiddliest part of the feature. A PDF and a Markdown file do not agree on what a chapter even is, so the splitting is a best effort and you can always fix it by hand afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is not
&lt;/h2&gt;

&lt;p&gt;There is no feed, no recommendations pushing you toward the next purchase, and no social layer. It is a reading tool, and it tries to stay out of the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/mukuvi/ARN.IO" rel="noopener noreferrer"&gt;https://github.com/mukuvi/ARN.IO&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is JavaScript end to end and it runs locally in a couple of commands. If you read and you lose track like I do, or you just want a small project to poke at the document parsing, the issues are open.&lt;/p&gt;




&lt;p&gt;Also on DEV: if you are trying to make your first open source contribution, I wrote about &lt;a href="https://dev.to/mukuvi/i-built-a-site-where-your-first-open-source-contribution-is-adding-yourself-to-it-2bae"&gt;a repo where contributing means adding yourself to it&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a site where your first open source contribution is adding yourself to it</title>
      <dc:creator>James Ngandu</dc:creator>
      <pubDate>Tue, 06 Oct 2026 12:26:31 +0000</pubDate>
      <link>https://dev.to/mukuvi/i-built-a-site-where-your-first-open-source-contribution-is-adding-yourself-to-it-2bae</link>
      <guid>https://dev.to/mukuvi/i-built-a-site-where-your-first-open-source-contribution-is-adding-yourself-to-it-2bae</guid>
      <description>&lt;p&gt;Plenty of people never make their first pull request. The commands are rarely the reason. You can learn &lt;code&gt;git add&lt;/code&gt;, &lt;code&gt;git commit&lt;/code&gt;, and &lt;code&gt;git push&lt;/code&gt; in an afternoon. What stops people is that the repo they pick has no reason to care whether they showed up, and nothing visibly changes when they are finished.&lt;/p&gt;

&lt;p&gt;So I built FREE-OPEN. It is a small React site, and the contribution is literally you. You fork it, add yourself to one JSON file, and open a pull request. When it is merged, your card appears on the contributors page of the live site. That is the whole thing. No issue to claim, no build system to learn, no review gauntlet waiting for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What your first contribution looks like
&lt;/h2&gt;

&lt;p&gt;Fork the repo, clone your fork, and install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/&amp;lt;YOUR-USERNAME&amp;gt;/FREE-OPEN.git
&lt;span class="nb"&gt;cd &lt;/span&gt;FREE-OPEN
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a branch, open &lt;code&gt;src/data/contributors.json&lt;/code&gt;, and add yourself to the end of the array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your Full Name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"github"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-github-username"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"linkedin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-linkedin-slug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"twitter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-twitter-handle"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;npm run dev&lt;/code&gt;, check that your card shows up at &lt;code&gt;/contributors&lt;/code&gt;, then commit, push, and open the pull request. Once it is merged your card is on the live site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a JSON file
&lt;/h2&gt;

&lt;p&gt;I wanted the first contribution to be as small as possible. Adding one object to one file means you cannot really break anything, and if two people do it at the same time the conflict is a one-line fix. The point is not the JSON. The point is that you run the whole loop once, fork, branch, commit, push, pull request, merge, and then you look at the site and your name is there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;React 18, Vite, Tailwind CSS, and React Router. Nothing exotic. Vite because the dev server is fast enough that a newcomer does not sit around wondering if something is broken. Tailwind because the site has a dark mode and I did not want to hand-write a second stylesheet. React Router for the contributors and about pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the repo is for
&lt;/h2&gt;

&lt;p&gt;The repo has picked up 19 forks, and it has become a place to practise the boring, real parts of contributing: writing a branch name, writing a commit message that says what you did, writing a PR description that a maintainer can act on. Those are the things that get you through your first job, and you can practise them here without a deadline.&lt;/p&gt;

&lt;p&gt;The lesson I keep coming back to is simple. People will contribute if the barrier is one file and the reward is visible. A lot of projects get the first half right and forget the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  It is not finished
&lt;/h2&gt;

&lt;p&gt;There is a CONTRIBUTING.md, a code of conduct, and issue templates for bugs, features, and improvements, so there is more to do than just add a card. The site itself is plain on purpose. I would rather it stay readable than clever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Live site: &lt;a href="https://free-open.vercel.app/" rel="noopener noreferrer"&gt;https://free-open.vercel.app/&lt;/a&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/mukuvi/FREE-OPEN" rel="noopener noreferrer"&gt;https://github.com/mukuvi/FREE-OPEN&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have never opened a pull request, this is a good place to start and I will review it. If you maintain a repo and you are trying to get first-timers through the door, take the idea. Make the first step tiny and make the result visible.&lt;/p&gt;




&lt;p&gt;Also on DEV: I wrote about &lt;a href="https://dev.to/mukuvi/i-built-a-reading-app-because-i-never-remembered-what-i-read-416m"&gt;ARN.IO, a reading companion that keeps your place and your notes&lt;/a&gt;, if you want another look at how I build things end to end.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>github</category>
      <category>beginners</category>
      <category>git</category>
    </item>
    <item>
      <title>Touch Grass Birder: a bird call identifier that works with no signal</title>
      <dc:creator>James Ngandu</dc:creator>
      <pubDate>Tue, 06 Oct 2026 05:11:27 +0000</pubDate>
      <link>https://dev.to/mukuvi/touch-grass-birder-a-bird-call-identifier-that-works-with-no-signal-1hnp</link>
      <guid>https://dev.to/mukuvi/touch-grass-birder-a-bird-call-identifier-that-works-with-no-signal-1hnp</guid>
      <description>&lt;p&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hf26"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Touch Grass Birder identifies bird calls, and it does the whole thing on your own machine.&lt;/p&gt;

&lt;p&gt;You tap &lt;strong&gt;Listen&lt;/strong&gt;, hold your phone up while a bird sings for about six seconds, and it tells you what you are hearing. Then you put the phone away and go look for the bird. That is the point: the screen is only in the way for a moment.&lt;/p&gt;

&lt;p&gt;I built it for the places where phone apps tend to fall over. A trailhead with one bar. A tent before sunrise. A park where the warblers are moving through and you have no idea what you are listening to.&lt;/p&gt;

&lt;p&gt;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Records six seconds of audio in the browser and encodes it to WAV on the device.&lt;/li&gt;
&lt;li&gt;Identifies the species locally with &lt;strong&gt;BirdNET&lt;/strong&gt;, the open acoustic classifier from the Cornell Lab of Ornithology.&lt;/li&gt;
&lt;li&gt;Writes a short field note about the bird with &lt;strong&gt;Gemma 3 1B&lt;/strong&gt;, an open-weight model running through &lt;code&gt;llama.cpp&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Keeps a field log of everything you have heard, in a plain JSON Lines file you own.&lt;/li&gt;
&lt;li&gt;Installs as a PWA and keeps working with the network turned off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no API key, no audio upload, and no network round trip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Live demo: &lt;a href="https://touch-grass-birder.onrender.com" rel="noopener noreferrer"&gt;https://touch-grass-birder.onrender.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The hosted demo runs BirdNET, so you can point it at a recording right now. Gemma field notes only work when you run it yourself, because a free Render instance is too small for the 0.8 GB model. Run &lt;code&gt;make models &amp;amp;&amp;amp; make run&lt;/code&gt; for those.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/mukuvi/touch-grass-birder" rel="noopener noreferrer"&gt;https://github.com/mukuvi/touch-grass-birder&lt;/a&gt; (MIT licensed).&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;Two open models run on the device, wired together behind a small FastAPI server.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  A[Phone browser&amp;lt;br/&amp;gt;records 6s, encodes WAV] --&amp;gt; B[FastAPI]
  B --&amp;gt; C[ffmpeg&amp;lt;br/&amp;gt;decode to 16-bit mono WAV]
  C --&amp;gt; D[BirdNET acoustic model&amp;lt;br/&amp;gt;open, local inference]
  D --&amp;gt; E[species + confidence]
  E --&amp;gt; F[Gemma 3 1B&amp;lt;br/&amp;gt;open-weight via llama.cpp]
  F --&amp;gt; G[plain-language field note]
  E --&amp;gt; H[(data/sightings.jsonl&amp;lt;br/&amp;gt;your field log)]
  G --&amp;gt; A
  H --&amp;gt; A&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser side:&lt;/strong&gt; the MediaRecorder API captures six seconds, an &lt;code&gt;AudioContext&lt;/code&gt; decodes it, and a small function in &lt;code&gt;app.js&lt;/code&gt; writes a real 16-bit mono WAV &lt;code&gt;Blob&lt;/code&gt; before it is sent. The server never has to touch the audio to get a usable clip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BirdNET&lt;/strong&gt; does the acoustic work. It is trained on thousands of species for passive acoustic monitoring, and it is the reason this works offline at all. It loads through the LiteRT runtime and stays warm in memory after the first call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 3 1B&lt;/strong&gt; (Q4_K_M, about 0.8 GB) runs through the &lt;code&gt;llama.cpp&lt;/code&gt; CLI to write a two or three sentence field note: what the bird sounds like, where and when you would hear it, and one field mark to look for. If the model is not on disk, the app skips the note and still identifies the bird.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; ties it together with four endpoints (&lt;code&gt;/api/identify&lt;/code&gt;, &lt;code&gt;/api/sightings&lt;/code&gt;, &lt;code&gt;/api/health&lt;/code&gt;, and the static app). The field log is &lt;code&gt;data/sightings.jsonl&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is overridable with environment variables (&lt;code&gt;LLAMA_BIN&lt;/code&gt;, &lt;code&gt;GEMMA_MODEL&lt;/code&gt;, &lt;code&gt;MIN_CONF&lt;/code&gt;, &lt;code&gt;LLAMA_THREADS&lt;/code&gt;, &lt;code&gt;BIRDER_DATA_DIR&lt;/code&gt;), so the same code runs on a laptop, a Raspberry Pi, or something sitting next to a phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;The short version: this project only exists because the models are open.&lt;/p&gt;

&lt;p&gt;The best places to hear birds are the places with no signal, and a cloud API cannot run there at all. Every closed identifier needs a round trip to a server. Running BirdNET on the device means the app is not a worse version of a cloud tool in the backcountry. It is the only version that works there. The recording is six seconds because nothing has to be uploaded and there is no spinner to wait on. The answer is already on the phone.&lt;/p&gt;

&lt;p&gt;A rare sighting is sensitive. A closed app can learn where you were and when, which matters if you are near a nesting site or reporting a scarce species. Here the only record is a line in a file you own, on hardware you control. There is no server for it to go to.&lt;/p&gt;

&lt;p&gt;Open weights also mean I can change the model. Gemma runs from a local GGUF file, so moving to a larger Gemma, a different quantization, or a different model entirely is a path and an environment variable, not a conversation with a vendor. I can fine-tune on a regional species list later. The classifier itself, BirdNET, is open and inspectable, which feels important when the output is a claim about a living thing.&lt;/p&gt;

&lt;p&gt;And it costs nothing to run. No per-identification fee, no rate limit, no key that expires. A birder on a budget, a school group, or a citizen science project can run it as long as they want.&lt;/p&gt;

&lt;p&gt;The open parts are the point. For this app the offline, private, zero-cost version is simply the better one.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I built this with an agent. The session is saved, so you can see how it went:&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="OpenCode"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building Touch Grass Birder for the HF26 Week 1 challenge&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Pasted the Hacktoberfest Open-Source AI Challenge Week 1 (Touch Grass) announcement: build something with open-source AI at its core that gets people outdoors, due Oct 11. Can you do this?&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;I checked the workspace. There is already a project: Touch Grass Birder, a bird-call identifier (BirdNET + Gemma 3 1B) that runs fully locally. Tests pass. I loaded the challenge skill and confirmed Week 1 is active (id 79, due Oct 12 06:59 UTC).&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-getchallenges"&gt;get_challenges&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;list active DEV challenges&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;list active DEV challenges&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Week 1 (id 79) active: theme Touch Grass, $2450 across 17 winners, judging = writing quality, relevance, creativity, technical execution, partner tech. Requires tag #hf26challenge.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Scope: Draft the DEV post, repo polish + commit, deploy to Render, save + embed the agent session, do it all end-to-end.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Audited the code (identify.py via BirdNET/LiteRT, fieldnotes.py via llama.cpp Gemma 3 1B, FastAPI main.py, PWA static app, 3 passing tests). Drafted the full submission post to docs/dev-submission-draft.md and added an MIT LICENSE.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-pytest"&gt;pytest&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;uv run pytest -q&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;uv run pytest -q&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;3 passed in 2.09s&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-gitlsremote"&gt;git ls-remote&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;git@github.com:mukuvi/touch-grass-birder.git&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;git@github.com:mukuvi/touch-grass-birder.git&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Repository not found. Found the real repo at mukuvi/-touch-grass-birder (leading dash) with Apache-2.0 LICENSE; README/pyproject declared MIT. Local git history was an unrelated snapshot.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Decisions: rename repo to touch-grass-birder, use MIT license, deploy a BirdNET-only demo on Render free tier (Gemma notes run locally).&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Reconciled the local repo onto the real remote, replaced Apache-2.0 LICENSE with MIT (matching README/pyproject), committed the post draft + license, and pushed to GitHub.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-gitpush"&gt;git push&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;git push git@github.com:mukuvi/-touch-grass-birder.git main&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;git push git@github.com:mukuvi/-touch-grass-birder.git main&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;25ba8b6..40b587d main -&amp;gt; main&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;proceed bro, you got all the keys&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Verified the push landed, pointed local origin at the real repo, and set out the remaining steps: rename on GitHub (needs a one-click settings change from the user - SSH cannot rename repos), deploy the Render blueprint, embed this saved agent session in the post, then publish the draft.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        8 of 7 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 3 1B runs locally through &lt;code&gt;llama.cpp&lt;/code&gt; and writes the field notes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render&lt;/strong&gt;: the live demo runs on Render with the included &lt;code&gt;render.yaml&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built for Hacktoberfest 2026. Files, models, and data stay on the machine, with no key, no upload, and no network.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
