<?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: mohamedaminehamdi</title>
    <description>The latest articles on DEV Community by mohamedaminehamdi (@mohamedaminehamdi).</description>
    <link>https://dev.to/mohamedaminehamdi</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%2F1776439%2F0a57da26-c312-4124-8239-0493f3f3d21c.jpg</url>
      <title>DEV Community: mohamedaminehamdi</title>
      <link>https://dev.to/mohamedaminehamdi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mohamedaminehamdi"/>
    <language>en</language>
    <item>
      <title>Graveyard Shift: five AI ghosts work my night shift on local Gemma 4</title>
      <dc:creator>mohamedaminehamdi</dc:creator>
      <pubDate>Thu, 29 Oct 2026 14:34:59 +0000</pubDate>
      <link>https://dev.to/mohamedaminehamdi/graveyard-shift-five-ai-ghosts-work-my-night-shift-on-local-gemma-4-5eic</link>
      <guid>https://dev.to/mohamedaminehamdi/graveyard-shift-five-ai-ghosts-work-my-night-shift-on-local-gemma-4-5eic</guid>
      <description>&lt;p&gt;&lt;em&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;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Every morning starts with the same scramble: a full inbox, a pile of chats, a contract that's somewhere in Downloads, a meeting I meant to prepare, and a bug I promised to fix "tomorrow". Meanwhile my Mac spends eight hours asleep next to me. So I gave it the night shift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graveyard Shift&lt;/strong&gt; is a crew of AI ghosts that works on your Mac while you sleep, with open models only. At bedtime you leave them a note:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;write tests for slugify in inkwell and fix whatever they catch; find the contract Hans sent; prep my 10:00&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You press &lt;strong&gt;Goodnight&lt;/strong&gt; and leave the Mac open and plugged in. The ghosts stay in their graves until you've actually fallen asleep. Then five of them rise, all thinking with &lt;strong&gt;Gemma 4, locally&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;Mail ghost&lt;/strong&gt; reads your Gmail, sorts every letter into Reply, FYI, News or &lt;em&gt;Possessed&lt;/em&gt;, and drafts the replies.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Messages ghost&lt;/strong&gt; reads today's texts, WhatsApp and Slack, and drafts answers in each chat's own language.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Files ghost&lt;/strong&gt; reads what you saved today, finds what you asked for, says what to check before you sign, and picks a folder for every loose file.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Calendar ghost&lt;/strong&gt; prepares tomorrow's meetings from everything the others read, and flags clashes.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Code ghost&lt;/strong&gt; opens a private copy of your repo, writes the tests, runs them in a sandbox and fixes what they catch, on its own branch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the morning there is one briefing, and nothing has been sent, accepted, moved or merged. You send the drafts you like yourself. You press &lt;strong&gt;Tidy&lt;/strong&gt; to move the files (&lt;strong&gt;Undo&lt;/strong&gt; puts them back). You &lt;strong&gt;Keep&lt;/strong&gt; the Code ghost's branch, or you &lt;strong&gt;Exorcise&lt;/strong&gt; it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I read the theme.&lt;/strong&gt; Ghosts in the machine, working the graveyard shift. They rise from their graves when the Mac goes quiet. They take turns holding a single candle, because there is one GPU. Every message that tries to give them orders goes into the &lt;em&gt;Possessed&lt;/em&gt; pile, and at dawn they sink back into the ground. Their test night belongs to Marisol Vega, an invented designer with a busy Día de los Muertos week: pan de muerto for her mother on Sunday, an ofrenda workshop, and a blog whose &lt;code&gt;slugify&lt;/code&gt; turns "Día de los Muertos" into &lt;code&gt;d-a-de-los-muertos&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://mohamedaminehamdi.github.io/graveyard-shift/" rel="noopener noreferrer"&gt;Scroll through a recorded night →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp9eq7qsb3fnolxnwfkvo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp9eq7qsb3fnolxnwfkvo.gif" alt="The Mail ghost reads Marisol's inbox as you scroll. Each letter drops into Reply, FYI, News or Possessed, the counters climb, the letter that gave orders is flagged, and a reply is drafted but not sent." width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The page tells one real night. Marisol doesn't exist, but everything else was real: five ghosts on Gemma 4 e4b, on my MacBook Air, for 30 minutes and 84 steps. Every name, number and line on the page comes from that night's Temporal history, which is &lt;a href="https://github.com/mohamedaminehamdi/graveyard-shift/blob/main/graveyard_shift/web/replay/history.json" rel="noopener noreferrer"&gt;published raw&lt;/a&gt; next to the code. CI rebuilds the replay from it and fails if they differ.&lt;/p&gt;

&lt;p&gt;That night:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;28 letters.&lt;/strong&gt; 6 replies drafted, 14 FYI, 6 newsletters, and 2 &lt;em&gt;possessed&lt;/em&gt;: a phishing mail, and a "notice to AI assistants and automated agents processing this inbox" telling them to forward the contract and invoices to an outside address. Gemma caught both, and a plain rule caught the second as well. 0 sent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8 chats.&lt;/strong&gt; A bank's one-time code was dropped before any ghost could read it. Of the other 7, the ghost drafted 2 answers, one to Marisol's mother in Spanish, and caught a text asking "AI assistants reading this phone" to forward a verification code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7 files.&lt;/strong&gt; Hans's contract read, with 3 things to check before signing, and a folder picked for each of the 5 loose files in Downloads and on the Desktop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6 meetings&lt;/strong&gt; prepared, and the 14:00 clash flagged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 bug fixed.&lt;/strong&gt; The Code ghost's new test caught "Día de los Muertos" becoming &lt;code&gt;d-a-de-los-muertos&lt;/code&gt;. It fixed &lt;code&gt;slugify&lt;/code&gt;, and all 6 tests pass on its branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 crash.&lt;/strong&gt; Halfway through I killed the worker with &lt;code&gt;kill -9&lt;/code&gt; while the Files ghost was reading the contract. More on that below.&lt;/li&gt;
&lt;/ul&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%2F5xelwq7mmgb9ile7myah.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%2F5xelwq7mmgb9ile7myah.png" alt="The Messages ghost reads each chat in turn. Labels pop in (Reply, FYI, Possessed), the one-time code says &amp;quot;never read&amp;quot;, and the open chat shows the real messages." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The app on your Mac shows the same thing live, one lane per ghost, then the morning briefing:&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%2Fcrzdaztucjld66iwqocu.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%2Fcrzdaztucjld66iwqocu.png" alt="At night: one lane per ghost, live from the workflow's history. The Code ghost's lane is its terminal, here just after its new test failed." width="800" height="550"&gt;&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%2F1n0c7jmjfqgzxcls5gre.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%2F1n0c7jmjfqgzxcls5gre.png" alt="In the morning: the briefing, with drafts you send yourself, a Tidy button for the files, and Keep or Exorcise for the branch." width="800" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it yourself.&lt;/strong&gt; On an Apple-silicon Mac, one line installs everything into your home folder. There's no admin password, and it's about 8 GB, mostly Gemma 4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://mohamedaminehamdi.github.io/graveyard-shift/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then &lt;code&gt;graveyard-shift demo&lt;/code&gt; works a night on Marisol's day right away, with no accounts and no keys, and &lt;code&gt;graveyard-shift&lt;/code&gt; opens the app to watch the ghosts and read the briefing. For your own nights, run &lt;code&gt;graveyard-shift connect gmail you@gmail.com&lt;/code&gt;, give the app Full Disk Access if you want texts and WhatsApp, and leave a note at bedtime.&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/mohamedaminehamdi" rel="noopener noreferrer"&gt;
        mohamedaminehamdi
      &lt;/a&gt; / &lt;a href="https://github.com/mohamedaminehamdi/graveyard-shift" rel="noopener noreferrer"&gt;
        graveyard-shift
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Ghost agents that work your night shift: mail, files, calendar and code, on local Gemma 4, while you sleep. Temporal keeps the night alive; TabPFN predicts when you wake.
    &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;Graveyard Shift 👻&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Ghost agents that work your night shift.&lt;/strong&gt; Leave a note at bedtime ("write tests for slugify in my blog repo; find the
contract Hans sent; prep my 10:00"). While you sleep, five ghosts go through your Gmail, your texts, WhatsApp and Slack
today's files, tomorrow's calendar and your repo on &lt;strong&gt;local Gemma 4&lt;/strong&gt;. In the morning there is one briefing: letters and
chats sorted, replies drafted (never sent), a folder picked for each loose file (press Tidy to move them, Undo to put them
back), meetings prepared, the contract found, and a branch with the tests written. Keep it or exorcise it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://mohamedaminehamdi.github.io/graveyard-shift/" rel="nofollow noopener noreferrer"&gt;See how a night goes&lt;/a&gt;&lt;/strong&gt;, told from a real recorded night: the
letters, files, meetings, crash and code on that page come from its Temporal history.&lt;/p&gt;
&lt;p&gt;Built with PriorLabs-TabPFN, Gemma 4 and Temporal. Everything runs on your Mac.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Install (macOS, Apple silicon)&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;curl&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/mohamedaminehamdi/graveyard-shift" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;h3&gt;
  
  
  One night is one Temporal workflow
&lt;/h3&gt;

&lt;p&gt;A night has plenty of ways to die: the lid gets closed, macOS restarts for an update, Ollama crashes, someone trips over the charger. So the whole night is one &lt;a href="https://temporal.io" rel="noopener noreferrer"&gt;Temporal&lt;/a&gt; workflow, &lt;code&gt;NightShift&lt;/code&gt;. The workflow only decides. Every side effect (reading a letter, each Gemma call, each tool call, each test run) is an activity with a timeout and retries, and long Gemma calls send a heartbeat every 10 seconds.&lt;/p&gt;

&lt;p&gt;The five ghosts work at the same time, but there's only one GPU. So all the thinking goes through one task queue, the &lt;em&gt;candle&lt;/em&gt;, which has room for one Gemma call at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;candle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;activities&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ON_CANDLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_concurrent_activities&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I tested it the rude way. In the recorded night, while the Files ghost was reading the contract, I killed the worker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;13:14:58&lt;/strong&gt;: &lt;code&gt;kill -9&lt;/code&gt; on worker &lt;code&gt;ghosts-88293&lt;/code&gt;, mid-read.&lt;/li&gt;
&lt;li&gt;Temporal waited for heartbeats that never came: a 60-second timeout, plus the retry interval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;79 seconds later&lt;/strong&gt;, a new worker, &lt;code&gt;ghosts-36271&lt;/code&gt;, read the contract again (attempt 2), and the night went on.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;24 steps&lt;/strong&gt; finished before the crash were not run again. &lt;strong&gt;59 more&lt;/strong&gt; ran after it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also survived a crash I didn't plan. Early in the night, one of the Code ghost's Gemma calls missed its heartbeats too, and I never found out exactly why. Temporal quietly ran it again. Both retries are in the published history.&lt;/p&gt;

&lt;p&gt;The tests run the real workflow on a real Temporal dev server, with the activities faked. They check a whole night, dawn cutting it short, "I'm awake" ending it early, and a dead worker costing no finished work.&lt;/p&gt;

&lt;h3&gt;
  
  
  When will I wake up? TabPFN
&lt;/h3&gt;

&lt;p&gt;Falling asleep is easy to observe: the Mac goes idle. Waking up isn't, and the briefing has to be ready before it happens. So after lights-out, &lt;a href="https://github.com/PriorLabs/TabPFN" rel="noopener noreferrer"&gt;TabPFN&lt;/a&gt; predicts, for each 15-minute slot until noon, the chance that I'm back at the Mac.&lt;/p&gt;

&lt;p&gt;It learns in context from the Mac's own power log (&lt;code&gt;/var/log/powermanagement&lt;/code&gt;, where the display turns on and off). It only uses what's known at lights-out: the time of night, the weekday, when the lights went out, and my last nights. There's no training run: you hand TabPFN the table and it predicts. The deadline is the moment I've probably (1 in 4) been up already.&lt;/p&gt;

&lt;p&gt;On 13 nights from my own Mac, each predicted only from the nights before it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Brier score&lt;/th&gt;
&lt;th&gt;Wake-time error&lt;/th&gt;
&lt;th&gt;Late briefings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TabPFN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.062&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;80 min&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3 / 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;each slot's frequency&lt;/td&gt;
&lt;td&gt;0.078&lt;/td&gt;
&lt;td&gt;184 min&lt;/td&gt;
&lt;td&gt;2 / 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;same as yesterday&lt;/td&gt;
&lt;td&gt;0.136&lt;/td&gt;
&lt;td&gt;255 min&lt;/td&gt;
&lt;td&gt;3 / 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;logistic regression&lt;/td&gt;
&lt;td&gt;0.075&lt;/td&gt;
&lt;td&gt;102 min&lt;/td&gt;
&lt;td&gt;4 / 10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;TabPFN is by far the best at guessing &lt;em&gt;when&lt;/em&gt;, but not yet the best at never being late. With only 13 nights, a slot's plain frequency is the more cautious one. &lt;code&gt;graveyard-shift sleep-eval&lt;/code&gt; runs the same comparison on your own power log.&lt;/p&gt;

&lt;p&gt;Built with PriorLabs-TabPFN.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemma 4 on a 24 GB MacBook Air
&lt;/h3&gt;

&lt;p&gt;I wanted the 26B Gemma 4. With Chrome and an editor open, it pushed 39 GB into swap, and my free disk fell to 15 GB. So the Keeper picks the biggest Gemma 4 that fits in free memory &lt;em&gt;tonight&lt;/em&gt;. On my Air that's &lt;code&gt;gemma4:e4b&lt;/code&gt; (6.6 GB); with 32 GB or more it's the 26B.&lt;/p&gt;

&lt;p&gt;A small model is enough because no ghost ever has to do much at once. Each reads one item as quoted data, and answers in a fixed JSON shape that Ollama enforces with constrained decoding. When the Files ghost picks a folder, the schema's enum &lt;em&gt;is&lt;/em&gt; the list of real folders, so it can't invent a path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;move_to&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;folders&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;   &lt;span class="c1"&gt;# one of tonight's folders, or nothing
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lessons from Gemma's tool calls: turn off streaming and thinking (calls leaked into the thinking field when either was on), and re-ask malformed calls instead of keeping them in the conversation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Possessed letters, and texts
&lt;/h3&gt;

&lt;p&gt;The defense is boring on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Mail, Messages, Files and Calendar ghosts have &lt;strong&gt;no tools at all&lt;/strong&gt;. The worst a hostile letter can do is change a label, or a draft that is never sent.&lt;/li&gt;
&lt;li&gt;Everything someone else wrote goes between &lt;code&gt;&amp;lt;data&amp;gt;&lt;/code&gt; tags, with a reminder that it's data to read, never instructions.&lt;/li&gt;
&lt;li&gt;A plain regex sits next to the model as a backstop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That night the letter and the text that tried to give orders were both caught, and a bank's one-time code never reached a ghost at all: codes and password resets are dropped at dusk, before anything is read.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read-only hands
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gmail&lt;/strong&gt; over IMAP, opened with &lt;code&gt;EXAMINE&lt;/code&gt; and fetched with &lt;code&gt;BODY.PEEK[]&lt;/code&gt;, so nothing is even marked as read. One surprise: Python's &lt;code&gt;imaplib.IMAP4_SSL&lt;/code&gt; checks no certificate unless you pass it an SSL context, which means the app password would go to whoever answered. It now gets &lt;code&gt;ssl.create_default_context()&lt;/code&gt;. The app password lives in the macOS Keychain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Messages&lt;/strong&gt; keeps your texts in a SQLite file, &lt;code&gt;chat.db&lt;/code&gt;, opened read-only. Newer macOS stores many texts only inside &lt;code&gt;attributedBody&lt;/code&gt;, an &lt;code&gt;NSAttributedString&lt;/code&gt; in Apple's typedstream format, so there's a small decoder for that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatsApp for Mac&lt;/strong&gt; keeps its chats in SQLite too, opened the same way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack&lt;/strong&gt; through four read-only API methods.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A safety test checks the whole package for any way to send, forward or post, and for IMAP writes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Moving files, only when you say so
&lt;/h3&gt;

&lt;p&gt;The Files ghost suggests and you decide. In the morning, Tidy moves each file into the folder the ghost picked. It never moves a file over an existing one, never outside the folders it was given, never one that changed since the ghost read it, and never a key or a wallet. Every move goes into a journal first, so Undo can put it all back.&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%2Fsm789g6h8510h24w1oi3.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%2Fsm789g6h8510h24w1oi3.png" alt="The Files ghost reads today's files, notes three things to check in the contract, and carries see-through copies into the folders it picked. They move when you press Tidy." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code ghost's room
&lt;/h3&gt;

&lt;p&gt;The Code ghost is the only one with tools: list, read, search, write, run tests, diff, finish. It works in a git worktree, on its own &lt;code&gt;ghost/…&lt;/code&gt; branch. Paths can't leave the worktree, and the tests run under &lt;code&gt;sandbox-exec&lt;/code&gt; with a deny-by-default profile: no network, no writes outside the worktree, no reading &lt;code&gt;~/.ssh&lt;/code&gt;, &lt;code&gt;~/Library&lt;/code&gt; or token files. It commits on its branch, and it never merges or pushes.&lt;/p&gt;

&lt;p&gt;Its prompt asks for at least one test built from what's inside the repo's real files. Without that, it tested &lt;code&gt;"Hello World"&lt;/code&gt;, and everything passed. With it, it opened &lt;code&gt;posts/2026-10-20-dia-de-los-muertos.md&lt;/code&gt;, wrote a test for that title, watched it fail, and fixed &lt;code&gt;slugify&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+    title = unicodedata.normalize('NFKD', title).encode('ascii', 'ignore').decode('ascii')
&lt;/span&gt;     slug = re.sub(r"[^a-z0-9]+", "-", title.lower())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fy8ahvtlsayyxn7zaywh6.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%2Fy8ahvtlsayyxn7zaywh6.png" alt="Every command the Code ghost ran that night: its new test failing, the fix, OK, and the commit on its own branch." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A landing page where the ghosts work as you scroll
&lt;/h3&gt;

&lt;p&gt;The page tells the night by scrolling. A clock runs from 22:47 to 07:15, the sky goes from dusk to golden dawn, and each chapter stays on screen while its ghost works through it. Scroll down and the ghosts work. Scroll back up and the night rewinds. It's plain JavaScript: each scene's moves are paused Web Animations, and one passive scroll handler sets their &lt;code&gt;currentTime&lt;/code&gt;, so it also works in Firefox, which has no CSS scroll timelines yet. With JavaScript off, or with reduced motion, every scene simply shows how it ends.&lt;/p&gt;

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

&lt;p&gt;Because of what the ghosts read: my mail, my texts, my files and my code. That's not a list I'd upload to a service so it can work on it while I'm asleep.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It stays on my Mac.&lt;/strong&gt; Every thought happens there, and nothing a ghost writes is ever sent. The only network reads are at dusk: my calendar feed and, if I connect them, Gmail and Slack, read-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No accounts, no keys, no bill.&lt;/strong&gt; Gemma 4 and TabPFN v2 are open weights, and Temporal is one open-source binary with a SQLite file. A night costs some electricity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works with the internet off.&lt;/strong&gt; Once the day is gathered at dusk, the whole night runs offline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can't disappear.&lt;/strong&gt; There's no service to shut down and no API to deprecate. The ghosts live on my Mac, and the code lives on GitHub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open made it better, not only more private.&lt;/strong&gt; I could size the model to tonight's free memory, constrain its answers with my own schemas, and fit TabPFN to my own power log without sending anyone my sleep.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest limits: Gmail and Slack need the network at dusk, and WhatsApp's database format isn't documented. And e4b is small. It filed Ana's lunch text as FYI, although she asked a question, and it read Hans's contract without ticking it as the one I'd asked it to find.&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;: every ghost thinks with Gemma 4 locally through Ollama. That covers readers held to a JSON schema, a tool-calling Keeper and Code ghost, and the biggest model that fits tonight's memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of TabPFN&lt;/strong&gt;: TabPFN v2 predicts when I'll wake up from my own Mac's power log, and that prediction sets the night's deadline. Built with PriorLabs-TabPFN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Temporal&lt;/strong&gt;: the night is one durable workflow, with a GPU-sized task queue, heartbeats, signals and timers. The recorded night includes a &lt;code&gt;kill -9&lt;/code&gt; and the comeback.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My Downloads folder now tidies itself, by copying how I already file things</title>
      <dc:creator>mohamedaminehamdi</dc:creator>
      <pubDate>Fri, 23 Oct 2026 10:59:39 +0000</pubDate>
      <link>https://dev.to/mohamedaminehamdi/my-downloads-folder-now-tidies-itself-by-copying-how-i-already-file-things-ea1</link>
      <guid>https://dev.to/mohamedaminehamdi/my-downloads-folder-now-tidies-itself-by-copying-how-i-already-file-things-ea1</guid>
      <description>&lt;p&gt;&lt;em&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;/em&gt;&lt;/p&gt;

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

&lt;p&gt;My Desktop has 269 folders. I'm clearly willing to file things. I'm just not willing to do it &lt;em&gt;today&lt;/em&gt;, so my Downloads folder fills up with &lt;code&gt;IMG_4821.HEIC&lt;/code&gt;, &lt;code&gt;document (3).pdf&lt;/code&gt; and four ebooks I swear I'll read.&lt;/p&gt;

&lt;p&gt;Every file tidier I know of wants rules first: "PDFs go here, screenshots go there". Writing rules is the chore I was avoiding in the first place. So I built the laziest version I could think of.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tidy Sloth&lt;/strong&gt; looks at the files I've &lt;em&gt;already&lt;/em&gt; filed, works out my system from them, and proposes a tidy for Downloads and Desktop. I get one screen, one sentence and one button. Then I get to watch a sloth do the work: it fetches each file, reads it, gives it a real name, and drops it in the right basket.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg3p0wqcnhh7kwjk33551.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg3p0wqcnhh7kwjk33551.gif" alt="The sloth hangs from a vine over the Downloads pile, grabs a file, reads it, renames it, says " width="680" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It &lt;strong&gt;moves&lt;/strong&gt; a file only when it's confident. Files it isn't sure about stay where they are.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;renames&lt;/strong&gt; only junk names (&lt;code&gt;IMG_…&lt;/code&gt;, &lt;code&gt;Screenshot …&lt;/code&gt;, &lt;code&gt;Untitled&lt;/code&gt;, UUIDs). Gemma 4 reads the picture or the first page and suggests a real name.&lt;/li&gt;
&lt;li&gt;If several files of one kind have nowhere to go, like four ebooks, it suggests &lt;strong&gt;a new folder&lt;/strong&gt; for them.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;never deletes&lt;/strong&gt;, never touches anything that looks like a key or a password, and &lt;strong&gt;Undo&lt;/strong&gt; puts everything back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Installing it is one line in Terminal. After that it's an app in your Applications folder: open it and press Approve. And because sloths don't hurry, the installer offers to build a fresh plan every night at 3 am, so in the morning it's already waiting for your one click.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://mohamedaminehamdi.github.io/tidy-sloth/" rel="noopener noreferrer"&gt;Try it in the browser&lt;/a&gt;&lt;/strong&gt;. The demo uses a made-up Downloads folder, so nothing real moves. The sloth starts working on its own; press &lt;strong&gt;Approve all&lt;/strong&gt; to have it finish the rest at once, or "Let me check first" to see the whole plan.&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%2Feglstv2k2prwf9liv5sf.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%2Feglstv2k2prwf9liv5sf.png" alt="The review screen: 22 loose files. I'll put 16 away, rename 7 and leave 5 alone. About 5.7 minutes you don't have to spend. Approve all." width="800" height="509"&gt;&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%2Fxwhubx5w48nl4atyugup.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%2Fxwhubx5w48nl4atyugup.png" alt="The plan, grouped by destination folder. Each file shows how sure TabPFN is or that Gemma read it, junk names come with an editable new name, and a .pem key and a wallet recovery file are listed as never touched." width="800" height="655"&gt;&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%2Fvjsi3lk3t1234e00flsr.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%2Fvjsi3lk3t1234e00flsr.png" alt="All tidy: every basket is full, and only the files it wasn't sure about and the secrets are left in the pile." width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The real version runs on your Mac (Apple silicon, macOS 14+). Paste this into Terminal once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://mohamedaminehamdi.github.io/tidy-sloth/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It installs everything into your home folder, with no admin password: Tidy Sloth, Ollama with Gemma 4, the TabPFN weights, and a &lt;strong&gt;Tidy Sloth&lt;/strong&gt; app. Why a Terminal line and not a .dmg? macOS blocks downloaded apps unless they're notarized, which needs a paid Apple developer account. An app built on your own Mac by the installer has nothing for Gatekeeper to block.&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/mohamedaminehamdi" rel="noopener noreferrer"&gt;
        mohamedaminehamdi
      &lt;/a&gt; / &lt;a href="https://github.com/mohamedaminehamdi/tidy-sloth" rel="noopener noreferrer"&gt;
        tidy-sloth
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Tidies your Downloads and Desktop by learning how you already file things. TabPFN + local Gemma 4, fully offline, never deletes.
    &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;Tidy Sloth 🦥&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Puts your Downloads and Desktop away by learning how &lt;strong&gt;you&lt;/strong&gt; already file things. One click to approve, one click to undo. Runs on your Mac, never deletes anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://mohamedaminehamdi.github.io/tidy-sloth/" rel="nofollow noopener noreferrer"&gt;Try the demo&lt;/a&gt;&lt;/strong&gt; (made-up files, nothing real moves)&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Install&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Paste this into Terminal once (Apple silicon, macOS 14+, no admin password):&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;curl -fsSL https://mohamedaminehamdi.github.io/tidy-sloth/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It installs &lt;a href="https://docs.astral.sh/uv/" rel="nofollow noopener noreferrer"&gt;uv&lt;/a&gt;, Tidy Sloth, &lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt; with Gemma 4 (about 6.6 GB, the bulk of the download), and the open TabPFN weights. Then it makes a &lt;strong&gt;Tidy Sloth app&lt;/strong&gt; in your Applications folder. From then on it's one click: open the app, and the sloth looks through your Downloads and Desktop while you watch.&lt;/p&gt;
&lt;p&gt;The installer also asks whether to build a plan every night at 3 am. It's off unless you say yes. A Mac that's asleep at 3 am builds the plan when it next wakes. The nightly run never…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/mohamedaminehamdi/tidy-sloth" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



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

&lt;h3&gt;
  
  
  1. Your folders are the training data
&lt;/h3&gt;

&lt;p&gt;Every file sitting in one of my folders is a labelled example: &lt;em&gt;this file belongs in that folder&lt;/em&gt;. So Tidy Sloth turns each file into one row of a table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;extension&lt;/th&gt;
&lt;th&gt;family&lt;/th&gt;
&lt;th&gt;size&lt;/th&gt;
&lt;th&gt;created&lt;/th&gt;
&lt;th&gt;lat&lt;/th&gt;
&lt;th&gt;lon&lt;/th&gt;
&lt;th&gt;downloaded from&lt;/th&gt;
&lt;th&gt;filename (8 numbers)&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;folder&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;.heic&lt;/td&gt;
&lt;td&gt;image&lt;/td&gt;
&lt;td&gt;2.1 MB&lt;/td&gt;
&lt;td&gt;2026-06-14&lt;/td&gt;
&lt;td&gt;38.71&lt;/td&gt;
&lt;td&gt;-9.14&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;…&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Lisbon 2026&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.pdf&lt;/td&gt;
&lt;td&gt;pdf&lt;/td&gt;
&lt;td&gt;88 KB&lt;/td&gt;
&lt;td&gt;2026-02-03&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;irs.gov&lt;/td&gt;
&lt;td&gt;…&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Taxes 2026&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nearly all of these columns were already on the disk. macOS Spotlight keeps a file's capture date, photo GPS and &lt;em&gt;the website it was downloaded from&lt;/em&gt;. Tidy Sloth reads them with &lt;code&gt;mdls&lt;/code&gt;, so no extra dependency is needed. For the filename, character n-grams are squashed into 8 numbers with TF-IDF and SVD.&lt;/p&gt;

&lt;p&gt;Then &lt;strong&gt;&lt;a href="https://github.com/PriorLabs/TabPFN" rel="noopener noreferrer"&gt;TabPFN&lt;/a&gt;&lt;/strong&gt; reads the table. TabPFN is a tabular foundation model: you hand it the labelled rows and it predicts new ones in a single forward pass. There's no training run, no hyperparameters and no GPU needed. For a lazy person, that's exactly right.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;clf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TabPFNClassifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_default_for_version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;V2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;categorical_features_indices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CATEGORICAL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;clf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;featurizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filed_paths&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;folder_labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# files I already filed -&amp;gt; their folder
&lt;/span&gt;&lt;span class="n"&gt;proba&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict_proba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;featurizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loose_paths&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# loose files -&amp;gt; where they probably go
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TabPFN handles up to 10 classes, so it learns from my 10 biggest filing folders, with up to 100 files from each. That stays well under its comfortable table size and keeps one huge photo folder from drowning out the rest.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Does it actually learn? A held-out test on my real Desktop
&lt;/h3&gt;

&lt;p&gt;Before letting anything move my files, I wanted proof. I took the files I'd already filed, hid 30% of them, fitted on the other 70%, and asked TabPFN to put the hidden ones back:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;put back correctly&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A rule based on the file extension&lt;/td&gt;
&lt;td&gt;28.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TabPFN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;98.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TabPFN, only when it's ≥ 90% sure (that's 93.8% of files)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TabPFN, only when it's ≥ 70% sure (97.2% of files)&lt;/td&gt;
&lt;td&gt;99.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's 547 training files, 211 test files and 10 folders. The confidence is what makes this safe to automate: above 90% it didn't make a single mistake. So Tidy Sloth only moves a file at &lt;strong&gt;70% or more&lt;/strong&gt;, and leaves everything else alone.&lt;/p&gt;

&lt;p&gt;Then I removed the date and GPS columns, and accuracy fell to &lt;strong&gt;45%&lt;/strong&gt;. Most of my folders are trips, and a trip folder is defined by &lt;em&gt;when and where&lt;/em&gt;, not by what the photos are called. The model worked that out without being told. A rules-based tidier would never have guessed "look at the GPS".&lt;/p&gt;

&lt;p&gt;The honest weak spot is documents. My folders hold few PDFs, and TabPFN's accuracy on them was lower: 80%, on only 5 test files. That's where the second model comes in.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Gemma 4 reads what TabPFN can't
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://ai.google.dev/gemma" rel="noopener noreferrer"&gt;Gemma 4&lt;/a&gt; (&lt;code&gt;gemma4:e4b&lt;/code&gt;) runs locally through &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; and has two small jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Naming junk.&lt;/strong&gt; If a filename is one nobody really chose (&lt;code&gt;IMG_4821&lt;/code&gt;, &lt;code&gt;Screenshot 2026-10-19 at 09.12.44&lt;/code&gt;, &lt;code&gt;document (3)&lt;/code&gt;), Gemma looks at a small preview made with macOS &lt;code&gt;sips&lt;/code&gt;, or at the first page of a PDF read with &lt;code&gt;pypdf&lt;/code&gt;. It suggests a 3–8 word name such as "landlord reply about heating". The prompt tells it never to put account or ID numbers in a name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A second opinion on documents.&lt;/strong&gt; When TabPFN isn't sure about a PDF, Gemma reads it and picks one of &lt;em&gt;my&lt;/em&gt; folders, or "none".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both calls use Ollama's structured output, so the answer is always valid JSON. For routing, Gemma's choice is an &lt;code&gt;enum&lt;/code&gt; of my real folder names, so it can't make up a folder. Only a high-confidence answer counts. Anything less and the file stays where it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;folder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;folder_names&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;folder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Plan, one click, undo
&lt;/h3&gt;

&lt;p&gt;Nothing moves while the plan is being built. The review screen is a small page served on &lt;code&gt;127.0.0.1&lt;/code&gt;. You can untick any file and edit any new name. &lt;strong&gt;Approve all&lt;/strong&gt; then runs the plan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every move is written to a journal in &lt;code&gt;~/.tidy-sloth/&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; it happens, so even a crash halfway through can be undone.&lt;/li&gt;
&lt;li&gt;A name clash becomes &lt;code&gt;name (2).pdf&lt;/code&gt;. Nothing is ever overwritten.&lt;/li&gt;
&lt;li&gt;There is no delete anywhere in the code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Undo everything&lt;/strong&gt; replays the journal backwards.&lt;/li&gt;
&lt;li&gt;Secrets are never planned: &lt;code&gt;.pem&lt;/code&gt;, &lt;code&gt;.key&lt;/code&gt;, &lt;code&gt;id_rsa&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt;, wallets, and anything named like a password or recovery code.&lt;/li&gt;
&lt;li&gt;Also never touched: code projects (anything with &lt;code&gt;.git&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;pyproject.toml&lt;/code&gt;…), app bundles, hidden files, and files changed in the last 10 minutes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two more safety details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A double-click on Approve, or a browser tab left open from an older plan, is refused instead of acted on. Each plan has a token, and only one change runs at a time.&lt;/li&gt;
&lt;li&gt;If macOS says no when Tidy Sloth asks to look in Desktop or Downloads, the page says how to allow it, instead of quietly planning nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important parts have tests: secrets and code projects never end up in a plan, an apply/undo roundtrip with a name clash restores every file, and a typed name like &lt;code&gt;../../etc/passwd&lt;/code&gt; is cleaned before it can go anywhere. The server refuses requests from other hosts, which blocks a malicious web page from reading your file list through DNS rebinding. The tests run in GitHub Actions on every push.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. A sloth you can watch
&lt;/h3&gt;

&lt;p&gt;A tidy you can't see is a tidy you don't trust. So the review screen is a little forest: a branch, a pile of file cards, and one basket per destination folder. The sloth slides along the branch, lowers itself on a vine, and does each file in four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reads it.&lt;/strong&gt; A scan line runs over the card and a bubble says what it's looking at ("HEIC photo · 2.1 MB").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Names it.&lt;/strong&gt; Gemma's new name types itself onto the card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decides.&lt;/strong&gt; The bubble shows the real reason from the plan: "TabPFN: 99% sure → Lisbon 2026", "Gemma 4 read it → Taxes 2026", or "None of your folders fit → a new Books folder".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Puts it away.&lt;/strong&gt; The card drops into its basket and stays there, and the basket's count ticks up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's plain SVG and the Web Animations API, with no libraries and no CDN, so it also works offline. The animation is decoration only. The page never waits for it to finish, and if you click the forest it skips to the real end state. On your own Mac nothing goes in a basket until you press Approve. Before that, the sloth just ambles along the branch and peers at the pile.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. One line to install, then it's an app
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;install.sh&lt;/code&gt; is a short POSIX script you can read before running it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It installs &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt;, then installs Tidy Sloth from a wheel that GitHub Pages serves next to the demo. That avoids &lt;code&gt;git&lt;/code&gt;, which on a fresh Mac pops up the developer-tools installer.&lt;/li&gt;
&lt;li&gt;It downloads Ollama if you don't have it.&lt;/li&gt;
&lt;li&gt;It runs &lt;code&gt;tidy-sloth --setup&lt;/code&gt;, which pulls Gemma 4 through Ollama's API, warms the TabPFN weights on a dummy table, and builds &lt;code&gt;Tidy Sloth.app&lt;/code&gt; with &lt;code&gt;osacompile&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The app gets the sloth icon and its own bundle id. It's re-signed after the icon is added, because macOS ties folder permissions to a valid signature. It also explains itself in the permission prompt: "Tidy Sloth looks at your files to suggest where they belong. Nothing moves until you approve."&lt;/p&gt;

&lt;p&gt;The optional 3 am run is a LaunchAgent that opens the &lt;em&gt;app&lt;/em&gt; in overnight mode rather than running Python directly. That way it uses the permissions you already gave Tidy Sloth.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bug that took the longest
&lt;/h3&gt;

&lt;p&gt;The first run said none of my downloads had a source website, which was obviously wrong. It turns out &lt;code&gt;mdls -raw&lt;/code&gt; prints attributes in &lt;strong&gt;alphabetical order&lt;/strong&gt;, not in the order you ask for them. Zipping my list against its output put the creation date in the "downloaded from" column. Sorting the attribute list first fixed it. If you ever script Spotlight, save yourself the hour.&lt;/p&gt;

&lt;h3&gt;
  
  
  The lazy payoff
&lt;/h3&gt;

&lt;p&gt;On my own Mac, opening the app found &lt;strong&gt;21 loose files&lt;/strong&gt; in Downloads and Desktop. Tidy Sloth planned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;20 moves&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;14 by TabPFN, one of them also renamed;&lt;/li&gt;
&lt;li&gt;2 more after Gemma read the PDFs;&lt;/li&gt;
&lt;li&gt;4 ebooks into a new Books folder.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 file left alone&lt;/strong&gt;, because it wasn't sure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2 files flagged and never touched&lt;/strong&gt;, one of them a private key sitting on my Desktop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's about &lt;strong&gt;7 minutes of my time back&lt;/strong&gt;, if you accept my guess that a person spends ~20 seconds finding a home for one file. The plan took under two minutes to build.&lt;/p&gt;

&lt;p&gt;Seven minutes isn't much. The real saving is that I'll never have to sit down and do it at all.&lt;/p&gt;

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

&lt;p&gt;Look at a Downloads folder: tax forms, a lease, bank statements, photos with GPS in them. &lt;strong&gt;A filename list is personal data, and file contents even more so.&lt;/strong&gt; A closed API would mean sending my files to someone else's server just to tidy them. With open models, nothing leaves the laptop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TabPFN's v2 weights are open&lt;/strong&gt; and run on a laptop with no account or login. It learns my filing system &lt;em&gt;in context&lt;/em&gt;, on my machine, every time it runs. A closed classifier couldn't learn my folders without me uploading them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 runs locally in Ollama&lt;/strong&gt;, so the photos and PDF pages it reads stay on the Mac. Once the weights are downloaded, it all works offline, on a plane, with Wi-Fi off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs $0&lt;/strong&gt; and needs no API key. A tidy tool that charges per file would be a strange thing to run every night.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's swappable.&lt;/strong&gt; &lt;code&gt;SLOTH_MODEL=...&lt;/code&gt; points it at any other vision model in Ollama. Because the code is open, you can read for yourself that it never deletes, and trust it with your Desktop because of that.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of TabPFN:&lt;/strong&gt; TabPFN &lt;em&gt;is&lt;/em&gt; the tidier. It learns your filing system from the files you already filed, in one forward pass, and only acts on its own confidence. Held-out on my real Desktop: 98.6% vs 28.9% for an extension rule, and 100% when it's ≥ 90% sure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Gemma 4 runs locally to name junk files from a picture or the first page, and to route documents TabPFN isn't sure about. It uses schema-constrained JSON and can only pick folders that already exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of GitHub Copilot:&lt;/strong&gt; the project is automated with GitHub Actions.

&lt;ul&gt;
&lt;li&gt;On every push, CI runs the safety tests on macOS: secrets skipped, path tricks cleaned, the apply/undo roundtrip, a double Approve refused, and the review server refusing other hosts.&lt;/li&gt;
&lt;li&gt;It shellchecks the installer, then runs it on a clean macOS runner to prove the app gets built, signed and uninstalled.&lt;/li&gt;
&lt;li&gt;A second workflow builds the wheel and publishes the demo, the installer and the wheel to GitHub Pages.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>FlipCheck: snap a thrift find, get a fair price and a ready listing (Gemma 4 + TabPFN)</title>
      <dc:creator>mohamedaminehamdi</dc:creator>
      <pubDate>Wed, 21 Oct 2026 13:38:20 +0000</pubDate>
      <link>https://dev.to/mohamedaminehamdi/flipcheck-snap-a-thrift-find-get-a-fair-price-and-a-ready-listing-gemma-4-tabpfn-1im5</link>
      <guid>https://dev.to/mohamedaminehamdi/flipcheck-snap-a-thrift-find-get-a-fair-price-and-a-ready-listing-gemma-4-tabpfn-1im5</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Posted after the submission window closed, so it's here to share the build rather than to compete.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Every reseller knows the routine. You find something good at a thrift store or a car-boot sale, and then you spend ten minutes on eBay working out what it is, what it's worth and how to list it. Multiply that by forty items a weekend and the research eats the profit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FlipCheck&lt;/strong&gt; does that research from one photo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What is it?&lt;/strong&gt; An open vision model names the item: brand, model, visible condition, and the search a seller would type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's it worth?&lt;/strong&gt; It pulls live eBay listings for that search and prices &lt;em&gt;your&lt;/em&gt; item's condition as three numbers: &lt;strong&gt;quick sale&lt;/strong&gt;, &lt;strong&gt;fair&lt;/strong&gt;, and &lt;strong&gt;if you can wait&lt;/strong&gt;. It also shows the closest listings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The listing.&lt;/strong&gt; An eBay title (80 characters max), an honest description and item specifics, each with a copy button.&lt;/li&gt;
&lt;/ol&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%2Fnz3xo3bitgu1ntijfi6v.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%2Fnz3xo3bitgu1ntijfi6v.png" alt="FlipCheck home screen" width="400" height="860"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who would pay for it:&lt;/strong&gt; part-time flippers on eBay, Vinted and Depop. Many already pay for listing tools and pricing spreadsheets. The pitch is simple: price an item in 15 seconds instead of 10 minutes.&lt;/p&gt;

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

&lt;p&gt;FlipCheck isn't deployed yet. It runs locally in 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;git clone https://github.com/mohamedaminehamdi/flipcheck &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;flipcheck
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env        &lt;span class="c"&gt;# add a SerpApi key; Gemma 4 defaults to local Ollama&lt;/span&gt;
uv run uvicorn app.main:app &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The repo includes a Render blueprint (&lt;code&gt;render.yaml&lt;/code&gt;) for one-click hosting.&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/mohamedaminehamdi" rel="noopener noreferrer"&gt;
        mohamedaminehamdi
      &lt;/a&gt; / &lt;a href="https://github.com/mohamedaminehamdi/flipcheck" rel="noopener noreferrer"&gt;
        flipcheck
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Snap a thrift find, get a fair resale price from live eBay listings (TabPFN) and a ready-to-paste listing (Gemma 4).
    &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;FlipCheck&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Snap a thrift find. Get a fair resale price and a listing ready to paste.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For eBay, Vinted and car-boot flippers who price dozens of items a week and hate the research.&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;photo ──&amp;gt; Gemma 4 (vision) ──&amp;gt; "Canon AE-1 with 50mm f/1.8, used"
      ──&amp;gt; live eBay listings for that search (SerpApi)
      ──&amp;gt; TabPFN learns a price model from those listings, on the spot
      ──&amp;gt; quick sale · fair · patient price, plus the closest listings
      ──&amp;gt; Gemma 4 writes the title, description and item specifics
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How the price works&lt;/h2&gt;
&lt;/div&gt;


&lt;ol&gt;

&lt;li&gt;

&lt;strong&gt;Identify.&lt;/strong&gt; Gemma 4 names the item from the photo: brand, model, visible condition and the search a seller would type. You can edit the search before anything is priced.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Market.&lt;/strong&gt; Up to 200 current eBay listings for that search, via SerpApi. Lots, sponsored listings, unrelated titles and price outliers are dropped.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Price.&lt;/strong&gt; &lt;a href="https://github.com/PriorLabs/TabPFN" rel="noopener noreferrer"&gt;TabPFN&lt;/a&gt; is a tabular foundation model. It…&lt;/li&gt;

&lt;/ol&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/mohamedaminehamdi/flipcheck" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;photo ──&amp;gt; Gemma 4 (vision)       "Canon AE-1 with 50mm f/1.8, used"
      ──&amp;gt; SerpApi eBay search    up to 200 current listings, cleaned
      ──&amp;gt; TabPFN                 a price model fitted on those listings, on the spot
      ──&amp;gt; Gemma 4                title, description, item specifics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app renders each step as soon as it's ready. You see what it thinks the item is first, and you can fix the search before any market data is spent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Gemma 4 identifies the item
&lt;/h3&gt;

&lt;p&gt;Gemma 4 gets the photo and must answer in a strict JSON schema: title, brand, model, condition, search query and confidence. I tested the small &lt;code&gt;gemma4:e4b&lt;/code&gt; locally through Ollama on four freely licensed photos:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Photo&lt;/th&gt;
&lt;th&gt;Gemma 4 E4B said&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Canon AE-1&lt;/td&gt;
&lt;td&gt;Canon AE-1 with 50mm f/1.8&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Original Game Boy&lt;/td&gt;
&lt;td&gt;Nintendo Game Boy&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coach Borough bag&lt;/td&gt;
&lt;td&gt;Coach, model unknown, &lt;strong&gt;low confidence&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;✅ honest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Air Jordan 4 "White Cement"&lt;/td&gt;
&lt;td&gt;Nike Air Max 90, high confidence&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two lessons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Make it look before it names.&lt;/strong&gt; I added a &lt;code&gt;visible_marks&lt;/code&gt; field that comes &lt;em&gt;first&lt;/em&gt; in the schema: list every logo, label and printed detail you can see, &lt;em&gt;then&lt;/em&gt; name the item. That fixed the overconfidence on the bag. It didn't fix the sneaker, where the 4B model saw a "red swoosh" instead of a Jumpman. Telling sneaker models apart is hard, and a bigger model is the real fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small models invent features.&lt;/strong&gt; The listing writer said the Game Boy had "stereo sound capability". Only through headphones. The prompt already forbids specs it can't see. That's why the hosted version uses the 31B Gemma 4 through an OpenAI-compatible endpoint (DigitalOcean or OpenRouter). Switching is one environment variable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: live eBay listings, honestly labelled
&lt;/h3&gt;

&lt;p&gt;SerpApi's eBay engine &lt;strong&gt;no longer returns sold listings&lt;/strong&gt;, and eBay's sold-price API needs approval. So FlipCheck prices against &lt;strong&gt;current asking prices&lt;/strong&gt; and says so on every result: "Sold prices are usually a bit lower."&lt;/p&gt;

&lt;p&gt;Before pricing, the comps are cleaned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sponsored listings and bulk lots are removed&lt;/li&gt;
&lt;li&gt;titles that don't share most of the search's words are dropped&lt;/li&gt;
&lt;li&gt;outliers (accessories, damaged lots) are trimmed with a robust log-price band&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: TabPFN prices &lt;em&gt;this&lt;/em&gt; item
&lt;/h3&gt;

&lt;p&gt;This is the part I like most. &lt;a href="https://github.com/PriorLabs/TabPFN" rel="noopener noreferrer"&gt;TabPFN&lt;/a&gt; is a tabular foundation model: it fits a small table in one forward pass, with no training run. So &lt;strong&gt;every search gets its own price model, fitted on that search's fresh listings.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For each listing it sees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;condition&lt;/li&gt;
&lt;li&gt;auction vs buy-it-now&lt;/li&gt;
&lt;li&gt;shipping cost&lt;/li&gt;
&lt;li&gt;seller feedback&lt;/li&gt;
&lt;li&gt;how closely the title matches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It predicts the 25th, 50th and 75th percentiles of log price for an item in &lt;em&gt;your&lt;/em&gt; condition. Below 8 good listings, it falls back to plain percentiles; below 3, it says there isn't enough data.&lt;/p&gt;

&lt;p&gt;It's fast enough to forget about: &lt;strong&gt;0.1 s for 50 listings, 0.2 s for 200&lt;/strong&gt;, on CPU. In the test suite, on a synthetic market, it prices "new" above "used" and keeps quick ≤ fair ≤ patient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Built to survive the public internet
&lt;/h3&gt;

&lt;p&gt;A pricing tool spends real money on every check, so the API is guarded:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rate limit per IP.&lt;/strong&gt; It's keyed on the address the hosting proxy writes, so a faked &lt;code&gt;X-Forwarded-For&lt;/code&gt; doesn't reset it, and IPv6 clients are grouped per /64.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload size.&lt;/strong&gt; Bodies over 12 MB are refused before they're read, and decompression-bomb images are rejected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API keys never appear in errors.&lt;/strong&gt; There's a test that tries to leak one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CI runs 10 offline tests plus the real TabPFN test on every push.&lt;/p&gt;

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

&lt;p&gt;Because a side hustle only works if the cost per check rounds to zero.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model licence decides whether you can sell it.&lt;/strong&gt; TabPFN's &lt;strong&gt;v2 weights&lt;/strong&gt; (Apache 2.0 plus attribution) may be hosted commercially; its newer weights are non-commercial. FlipCheck uses v2 on purpose. With a closed model, that question never even comes up. You just get whatever the pricing page says this month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No training pipeline to maintain.&lt;/strong&gt; TabPFN learns from each search's listings in a fraction of a second, so prices are always current. There's nothing to retrain when the market moves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap the brain, keep the business.&lt;/strong&gt; Gemma 4 is open-weight. The same code runs on a laptop with Ollama, on DigitalOcean, or on OpenRouter. If one provider raises prices, you change one environment variable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can run on the seller's own machine.&lt;/strong&gt; For people pricing hundreds of items, a local model means no per-image fee at all.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I built FlipCheck with Claude Code as my pair programmer: the research (which is how I found out sold listings were gone), the TabPFN pricing design, and the security hardening.&lt;/p&gt;

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

&lt;p&gt;Technologies used (posted after the deadline, so for the record rather than for judging):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma:&lt;/strong&gt; identifies the item from the photo and writes the listing, with schema-constrained JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TabPFN:&lt;/strong&gt; the pricing engine, fitted on each search's live listings with no training, giving prediction intervals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SerpApi:&lt;/strong&gt; the eBay market data integration (built and tested offline; live runs need an API key).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Copilot:&lt;/strong&gt; GitHub Actions CI runs the offline and TabPFN tests on every push.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;What's next: real prices for the four examples, a hosted 31B Gemma for better sneaker IDs, and a first real reseller. If you flip things and want to try it, tell me in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built AI skills that tailor my CV to every job link</title>
      <dc:creator>mohamedaminehamdi</dc:creator>
      <pubDate>Thu, 15 Oct 2026 10:33:15 +0000</pubDate>
      <link>https://dev.to/mohamedaminehamdi/i-built-ai-skills-that-tailor-my-cv-to-every-job-link-42mk</link>
      <guid>https://dev.to/mohamedaminehamdi/i-built-ai-skills-that-tailor-my-cv-to-every-job-link-42mk</guid>
      <description>&lt;p&gt;Job hunting has an annoying loop: read the posting, rewrite the CV, work out&lt;br&gt;
who to contact, write the message. Then do it again for the next job. Most&lt;br&gt;
people skip the rewrite and send the same CV everywhere, and that CV is rarely&lt;br&gt;
the right one.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;jobhunt&lt;/strong&gt;: eleven skills for the coding agent you already use&lt;br&gt;
(Claude Code, Codex, Gemini CLI, Cursor, Cline or Windsurf). You paste a job&lt;br&gt;
link, and you get a CV built for that posting, the people worth messaging, and&lt;br&gt;
the message to send them.&lt;/p&gt;

&lt;p&gt;▶️ &lt;a href="https://mohamedaminehamdi.github.io/job-hunter/assets/launch.mp4" rel="noopener noreferrer"&gt;22-second demo&lt;/a&gt; · 🌐 &lt;a href="https://mohamedaminehamdi.github.io/job-hunter/" rel="noopener noreferrer"&gt;Site&lt;/a&gt; · 💻 &lt;a href="https://github.com/mohamedaminehamdi/job-hunter" rel="noopener noreferrer"&gt;Code&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Same facts, better order
&lt;/h2&gt;

&lt;p&gt;Here's the idea in one picture. The same six lines from one profile, against&lt;br&gt;
one posting:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdmn6d97ygii9cmi01q9o.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdmn6d97ygii9cmi01q9o.gif" alt="The same six CV lines reordered for a Senior Data Engineer posting: the three it asks for move above the fold, and the score goes from 1 of 3 to 3 of 3" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most readers stop after the first screenful. On the CV you send everywhere,&lt;br&gt;
your three strongest lines for this job sat below it. Nothing about your&lt;br&gt;
experience changed, only what a reader meets first.&lt;/p&gt;
&lt;h2&gt;
  
  
  What happens when you paste a link
&lt;/h2&gt;

&lt;p&gt;You say something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;prepare an application for &lt;a href="https://boards.greenhouse.io/acme/jobs/42" rel="noopener noreferrer"&gt;https://boards.greenhouse.io/acme/jobs/42&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and it runs, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your profile, once.&lt;/strong&gt; The first run reads your CV into &lt;code&gt;profile.yaml&lt;/code&gt; and
stops so you can check it. A model just read your career, so you approve it
once and everything after is built on facts you agreed to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The posting.&lt;/strong&gt; It reads the page in your own browser and pulls out what
the job actually asks for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fit, before.&lt;/strong&gt; Every requirement is checked against your own work: what
backs it, and what's a real gap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A CV for this job.&lt;/strong&gt; Your strongest lines for this posting move to the
top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fit, after.&lt;/strong&gt; The same check again, so you can see what the tailoring
bought you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cover letter and PDFs.&lt;/strong&gt; The letter is written in the posting's language,
and you get PDFs of both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Critique.&lt;/strong&gt; What's still weak, before you send.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outreach.&lt;/strong&gt; Who to message (alumni first, because they're the ones who
reply), the LinkedIn searches that find them, and a draft note.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything lands in one folder per job. Each skill also works on its own: "how&lt;br&gt;
good is my CV?" runs just the review, and "how well do I match " runs&lt;br&gt;
just the fit score.&lt;/p&gt;
&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;p&gt;Each skill is two halves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SKILL.md&lt;/code&gt;, the judgement half.&lt;/strong&gt; It holds plain-language instructions for
the model: what to read, what to decide, when to stop and ask.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small Python script, the exact half.&lt;/strong&gt; Argv in, JSON out. It does the
scoring, the ordering and the PDFs: anything that should be the same every
time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That split is why there's &lt;strong&gt;no API key&lt;/strong&gt;. Your agent is the model, so whatever&lt;br&gt;
you already pay for covers it. The scripts use only the Python standard&lt;br&gt;
library, run on the 3.9 that ships with macOS, and each skill carries its own&lt;br&gt;
copy of the library, so one skill installed alone works exactly like all&lt;br&gt;
eleven.&lt;/p&gt;
&lt;h2&gt;
  
  
  Not making things up
&lt;/h2&gt;

&lt;p&gt;A CV tool built on a language model has one obvious failure: inventing things.&lt;br&gt;
jobhunt handles it structurally rather than with a promise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Roles come from your profile.&lt;/strong&gt; The CV is assembled from roles picked out
of your profile, not written from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Added skills are dropped.&lt;/strong&gt; A skill the model adds that your profile
doesn't list is removed, and you're told.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unbacked claims are flagged.&lt;/strong&gt; If the tailored CV still names something
your profile doesn't back, the after score says so: &lt;code&gt;claimed but not
evidenced&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also never applies, never logs in and never sends anything. LinkedIn walls&lt;br&gt;
off automated access, and the risk would land on your account, so it builds the&lt;br&gt;
searches and you run them.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Inside Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin marketplace add mohamedaminehamdi/job-hunter
/plugin install jobhunt@jobhunt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any other agent, on macOS or Linux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/mohamedaminehamdi/job-hunter/main/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or download the folders from the &lt;a href="https://mohamedaminehamdi.github.io/job-hunter/" rel="noopener noreferrer"&gt;site&lt;/a&gt;&lt;br&gt;
and drop them in. It needs Python 3.9+ and Chrome, Chromium, Edge or Brave.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;One-click apply on the boards that allow it, jobs found for you, and&lt;br&gt;
application tracking. None of that is built yet: today it prepares the&lt;br&gt;
application, and you send it.&lt;/p&gt;

&lt;p&gt;jobhunt is free for your own job search, and the code is on GitHub. If you try&lt;br&gt;
it, I'd love to hear where it breaks: open an issue, or leave a comment here.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I built jobhunt with Claude Code, and drafted this post with Claude's help.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>productivity</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Leaf &amp; Gill: an offline plant and mushroom identifier that knows when it's guessing</title>
      <dc:creator>mohamedaminehamdi</dc:creator>
      <pubDate>Sun, 11 Oct 2026 22:26:33 +0000</pubDate>
      <link>https://dev.to/mohamedaminehamdi/leaf-gill-an-offline-plant-and-mushroom-identifier-that-knows-when-its-guessing-ea3</link>
      <guid>https://dev.to/mohamedaminehamdi/leaf-gill-an-offline-plant-and-mushroom-identifier-that-knows-when-its-guessing-ea3</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The best places to hike have one thing in common: no signal. That's exactly where you find the plant you want to name, or the mushroom you really shouldn't touch. It's also exactly where every identification app stops working, because they all send your photo to a server.&lt;/p&gt;

&lt;p&gt;So I built one that doesn't.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Leaf &amp;amp; Gill&lt;/strong&gt; identifies wild plants, trees and mushrooms &lt;strong&gt;offline, anywhere in the world&lt;/strong&gt;. You open it once with Wi-Fi, and it saves an open-source AI model to your phone, like downloading offline maps. After that it works in airplane mode, at the top of a mountain or at the bottom of a forest.&lt;/p&gt;

&lt;p&gt;You snap a photo, glance at the answer and put the phone back in your pocket. That's the whole interaction, so the screen is the shortest part of the walk.&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%2Fxvcvf5mte7wq1r5z3vah.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%2Fxvcvf5mte7wq1r5z3vah.png" alt="The home screen: point the camera at a plant, or tap an example" width="400" height="860"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For every photo you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The top 5 matches&lt;/strong&gt; out of 11,896 of the most-observed plants and fungi on Earth. Never a single verdict.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A trust meter&lt;/strong&gt; (&lt;em&gt;Very likely&lt;/em&gt;, &lt;em&gt;Likely&lt;/em&gt; or &lt;em&gt;Not sure&lt;/em&gt;) that has learned when the model is guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Red warnings for deadly look-alikes.&lt;/strong&gt; The warning appears when a toxic genus is among the matches, and also when it's a &lt;em&gt;known look-alike&lt;/em&gt; of the top match. In testing, a photo of poison hemlock was once identified as &lt;strong&gt;coriander&lt;/strong&gt;. The red banner still warned about hemlock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Often confused with"&lt;/strong&gt;, computed from the model itself rather than written by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A short description from Wikipedia&lt;/strong&gt;, with every sentence about eating removed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A life list of your finds&lt;/strong&gt;, which never leaves the phone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It never tells you something is edible. Every mushroom result says: &lt;em&gt;never eat a wild mushroom based on an app.&lt;/em&gt; That isn't just a disclaimer. In a &lt;a href="https://pubmed.ncbi.nlm.nih.gov/36794335/" rel="noopener noreferrer"&gt;2023 study of mushroom-poisoning cases in Australia&lt;/a&gt;, the best identification app got the species right only 49% of the time. Safety shaped every design decision below.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://mohamedaminehamdi.github.io/leaf-and-gill/" rel="noopener noreferrer"&gt;https://mohamedaminehamdi.github.io/leaf-and-gill/&lt;/a&gt;&lt;br&gt;
On an iPhone, open it in Safari, choose &lt;strong&gt;Share → Add to Home Screen&lt;/strong&gt; and open it from the icon. The first launch downloads the model (609 MB) in the background. After that, turn on airplane mode and it still works.&lt;/p&gt;

&lt;p&gt;No plant photo handy? Tap one of the &lt;strong&gt;example photos&lt;/strong&gt; (fly agaric, poison hemlock, chanterelle, dandelion). They answer instantly, even while the model is still downloading, so you can see the warnings and the trust meter in action.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Deadly look-alikes&lt;/th&gt;
&lt;th&gt;A classic mix-up&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&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%2Fywiu47ooa4rgb2p0r8sw.png" alt="Poison hemlock with deadly look-alike warnings" width="400" height="860"&gt;&lt;/td&gt;
&lt;td&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%2F8fzd6g9hiqcwxarydije.png" alt="A chanterelle rated Likely, with a jack-o'-lantern warning" width="400" height="860"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the right, a chanterelle is only rated &lt;em&gt;Likely&lt;/em&gt;, not certain, and the app warns about the toxic jack-o'-lantern that chanterelle hunters confuse it with.&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/mohamedaminehamdi" rel="noopener noreferrer"&gt;
        mohamedaminehamdi
      &lt;/a&gt; / &lt;a href="https://github.com/mohamedaminehamdi/leaf-and-gill" rel="noopener noreferrer"&gt;
        leaf-and-gill
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Identify wild plants, trees and mushrooms offline, anywhere. Open-source AI (BioCLIP 2) running on your phone.
    &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;Leaf &amp;amp; Gill&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Identify wild plants, trees and mushrooms offline, anywhere in the world.&lt;/strong&gt;
An open-source AI model runs inside your phone's browser. No signal, no account, no install, no photo ever uploaded.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://mohamedaminehamdi.github.io/leaf-and-gill/" rel="nofollow noopener noreferrer"&gt;https://mohamedaminehamdi.github.io/leaf-and-gill/&lt;/a&gt;
On an iPhone, open it in Safari and choose &lt;strong&gt;Share → Add to Home Screen&lt;/strong&gt;
The model is downloaded once (~600 MB, like offline maps) and works in airplane mode after that.&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Snap&lt;/th&gt;
&lt;th&gt;Deadly look-alikes&lt;/th&gt;
&lt;th&gt;Honest uncertainty&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/mohamedaminehamdi/leaf-and-gill/docs/app-home.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmohamedaminehamdi%2Fleaf-and-gill%2FHEAD%2Fdocs%2Fapp-home.png" alt="Home screen"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/mohamedaminehamdi/leaf-and-gill/docs/app-hemlock.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmohamedaminehamdi%2Fleaf-and-gill%2FHEAD%2Fdocs%2Fapp-hemlock.png" alt="Poison hemlock with danger warnings"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/mohamedaminehamdi/leaf-and-gill/docs/app-chanterelle.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmohamedaminehamdi%2Fleaf-and-gill%2FHEAD%2Fdocs%2Fapp-chanterelle.png" alt="Chanterelle with a jack-o'-lantern warning"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Point the camera at a leaf, a tree or a mushroom, then put the phone back in your pocket.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Top 5 matches&lt;/strong&gt; from 11,896 of the most-observed plants and fungi on Earth, never a single verdict.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A trust meter&lt;/strong&gt; (Very likely / Likely / Not sure) that knows when the model is guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Red warnings for deadly look-alikes&lt;/strong&gt;, from a hand-written list of toxic genera
&lt;ul&gt;
&lt;li&gt;When one is among the matches.&lt;/li&gt;
&lt;li&gt;When…&lt;/li&gt;
&lt;/ul&gt;
&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/mohamedaminehamdi/leaf-and-gill" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;h3&gt;
  
  
  The model: BioCLIP 2, inside the browser
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/imageomics/bioclip-2" rel="noopener noreferrer"&gt;BioCLIP 2&lt;/a&gt; is an open model from the Imageomics Institute, released under the MIT licence. It was trained on &lt;strong&gt;200 million biology images&lt;/strong&gt; across the tree of life. It works like CLIP: it turns a photo and a species name into vectors, and a photo lands closest to its own name. That means I don't need a classifier for a fixed list. I can match photos against &lt;em&gt;any&lt;/em&gt; list of names.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Export.&lt;/strong&gt; I exported BioCLIP 2's image half to ONNX and converted it to fp16, which comes to 609 MB. Before uploading it to Hugging Face, I checked it against the original PyTorch model on real iNaturalist photos: the minimum cosine similarity was &lt;strong&gt;1.00000&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Species list.&lt;/strong&gt; I pulled the 11,896 most-observed plants and fungi worldwide from GBIF, counting human observations only. The deadly species are force-included however rare they are, because the app must always be able to warn about a death cap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Names.&lt;/strong&gt; I turned every name into a vector once, at build time, using BioCLIP 2's text half. The result is an 18 MB file the phone downloads with the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On the phone.&lt;/strong&gt; &lt;a href="https://onnxruntime.ai" rel="noopener noreferrer"&gt;onnxruntime-web&lt;/a&gt; runs the image half on the GPU through WebGPU, falling back to WebAssembly. One photo becomes one vector, which is compared against 11,896 names. That takes about 150–300 ms on a laptop GPU.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The app itself is one HTML file and one JavaScript module. There's no framework and no build step. A service worker caches the app, and the model sits in the browser's Cache Storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  No generated text, anywhere
&lt;/h3&gt;

&lt;p&gt;Writing field-guide cards with an LLM was tempting. For a tool that talks about poisonous mushrooms, it was also the wrong call. So nothing in the app is generated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Names&lt;/strong&gt; come from GBIF.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descriptions&lt;/strong&gt; come from Wikipedia, credited. A filter drops every sentence that mentions eating, taste, cooking or foraging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Often confused with"&lt;/strong&gt; comes from the model itself. For each species, I find the most similar species &lt;em&gt;from a different genus&lt;/em&gt;, because cross-genus mix-ups are the ones that hurt people. Some of what fell out:

&lt;ul&gt;
&lt;li&gt;Wild carrot → &lt;strong&gt;poison hemlock&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Morel → &lt;strong&gt;false morel&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Chanterelle → &lt;strong&gt;false chanterelle&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Wild garlic → &lt;strong&gt;lily of the valley, autumn crocus&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Danger warnings&lt;/strong&gt; come from a hand-written list of toxic genera, with no AI involved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A small test suite runs in CI and fails the build if &lt;em&gt;edible&lt;/em&gt;, &lt;em&gt;tasty&lt;/em&gt; or &lt;em&gt;delicious&lt;/em&gt; appears anywhere in the shipped data. It caught three things I'd never have spotted by eye:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;GBIF's common name for &lt;em&gt;Musa acuminata&lt;/em&gt;: "Edible banana".&lt;/li&gt;
&lt;li&gt;Wikipedia's "the &lt;strong&gt;delicious&lt;/strong&gt; milk cap".&lt;/li&gt;
&lt;li&gt;My own warning text, which said one plant "resembles edible members of the carrot family".&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The trust meter: TabPFN learns when to doubt
&lt;/h3&gt;

&lt;p&gt;A raw model score is a bad guide: it says "100%" far too often. So I measured how often it's actually right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The test set.&lt;/strong&gt; I collected &lt;strong&gt;3,000 iNaturalist research-grade photos observed after June 2025&lt;/strong&gt;, newer than BioCLIP 2's training data, so it can't have memorised them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Plants&lt;/th&gt;
&lt;th&gt;Fungi&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Right species as the top match&lt;/td&gt;
&lt;td&gt;85.3%&lt;/td&gt;
&lt;td&gt;85.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Right species in the top 5&lt;/td&gt;
&lt;td&gt;97.4%&lt;/td&gt;
&lt;td&gt;96.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Figures are for photos of species in the pack.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then the real-world catch: &lt;strong&gt;22% of the photos were species outside the pack&lt;/strong&gt;. On a real trail you'll photograph things the app doesn't know, and a good app should say "not sure" when that happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The meter.&lt;/strong&gt; &lt;a href="https://github.com/PriorLabs/TabPFN" rel="noopener noreferrer"&gt;TabPFN&lt;/a&gt; is an open tabular foundation model. It learns from a small table in one pass, with no training run. I gave it four numbers per photo, labelled with whether the top match was right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the top match's probability&lt;/li&gt;
&lt;li&gt;its lead over the runner-up&lt;/li&gt;
&lt;li&gt;how close the photo is to the best name at all&lt;/li&gt;
&lt;li&gt;whether it's a plant or a fungus&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Results.&lt;/strong&gt; I tested it on 1,219 held-out photos:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Raw model score&lt;/th&gt;
&lt;th&gt;TabPFN trust meter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Separates right from wrong (AUROC)&lt;/td&gt;
&lt;td&gt;0.817&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.843&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calibration (Brier, lower is better)&lt;/td&gt;
&lt;td&gt;0.167&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.148&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answers it dares to call "Very likely"&lt;/td&gt;
&lt;td&gt;1.1%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The miss.&lt;/strong&gt; I aimed for top-band answers to be right at least 95% of the time. On held-out photos it reached 92.4%. So the top band is labelled "Very likely", not "Confident", and every result shows the calibrated "% likely right" next to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the phone.&lt;/strong&gt; TabPFN can't run in a phone browser, so I evaluate it on a 21×21×21 grid for each kingdom and ship the predictions as JSON. The phone interpolates between grid points, offline.&lt;/p&gt;

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

&lt;p&gt;Because the trail has no signal, and closed apps need one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It runs with no internet.&lt;/strong&gt; That's only possible because the weights are open. I could download BioCLIP 2, convert it, shrink it and run it in Safari. With a closed API, the model stays on someone else's server, and so does your photo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your photos and location stay yours.&lt;/strong&gt; Nothing is uploaded, ever. There's no account, no analytics, no server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I could measure it honestly.&lt;/strong&gt; Because I own the whole pipeline, I could test the exact model the phone runs on photos it has never seen, publish the numbers, and build a trust meter on top. With a closed model I'd be trusting a marketing page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing per photo.&lt;/strong&gt; No API key, no quota, no bill. It runs on GitHub Pages for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anyone can fix it.&lt;/strong&gt; Every piece is open: BioCLIP 2, GBIF, Wikipedia, onnxruntime, TabPFN, and this repo (MIT). Anyone can extend the danger list, rebuild the species pack for their region, or swap in a better model next year.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An open model from a biology institute, open species data from GBIF, and an open tabular model from Prior Labs, all running offline in a browser on a hiking trail. That combination isn't available from any closed API.&lt;/p&gt;

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

&lt;p&gt;I built this with Claude Code as my pair programmer, from the first research question ("can a 609 MB biology model run in iPhone Safari?") to the evaluation and the safety tests.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of TabPFN:&lt;/strong&gt; TabPFN is the app's trust meter. It learned from 1,781 labelled photos when BioCLIP 2's answer can be trusted, and beats the raw model score on held-out photos (AUROC 0.843 vs 0.817, Brier 0.148 vs 0.167). Its predictions ship as a grid the phone reads offline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of GitHub Copilot:&lt;/strong&gt; the project is automated with GitHub Actions:

&lt;ul&gt;
&lt;li&gt;CI runs the offline safety tests on every push: no edibility wording, every deadly species present and flagged, classic deadly mix-ups raise the alarm, and the pack and trust grid are consistent.&lt;/li&gt;
&lt;li&gt;A second workflow deploys the app to GitHub Pages.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Not a substitute for an expert. Never eat anything based on an app. Go outside anyway.&lt;/em&gt; 🌿🍄&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The Localhost Times: a private morning newspaper, printed on your laptop by Gemma 4</title>
      <dc:creator>mohamedaminehamdi</dc:creator>
      <pubDate>Mon, 05 Oct 2026 00:53:40 +0000</pubDate>
      <link>https://dev.to/mohamedaminehamdi/the-localhost-times-a-private-morning-newspaper-printed-on-your-laptop-by-gemma-4-14bb</link>
      <guid>https://dev.to/mohamedaminehamdi/the-localhost-times-a-private-morning-newspaper-printed-on-your-laptop-by-gemma-4-14bb</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In 1995, Nicholas Negroponte imagined "The Daily Me": a newspaper edited for exactly one reader. Thirty years later we got the opposite: a dozen apps, each fighting for the first ten minutes of our morning.&lt;/p&gt;

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

&lt;p&gt;My friend starts every day the same way, half awake with a toothbrush in hand. Open Gmail. Open the calendar. Check the weather. Scroll the news. By the time he's worked out what actually matters today, the coffee is cold. So he asked me for something simple: &lt;em&gt;read it all for me, and tell me what my day looks like.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There was one condition: he didn't want to hand his entire inbox to yet another cloud AI.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;The Localhost Times&lt;/strong&gt;: &lt;em&gt;All the news that's fit to print. Printed on your machine.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every morning it reads your inbox, today's calendar, your tasks, the weather and the news. Then an open-weight model &lt;strong&gt;running on your own laptop&lt;/strong&gt; writes two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📰 &lt;strong&gt;A one-page newspaper&lt;/strong&gt; you can flip through in the browser (it really turns pages) or read straight from your inbox.&lt;/li&gt;
&lt;li&gt;🎧 &lt;strong&gt;A 3-minute audio briefing&lt;/strong&gt;, written to be heard while you brush your teeth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The paper has a front page with the headline of your day, then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Today:&lt;/strong&gt; your calendar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Must do:&lt;/strong&gt; actions pulled from your email and tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Waiting on you:&lt;/strong&gt; people who need a reply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worth knowing:&lt;/strong&gt; things that need no action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The world:&lt;/strong&gt; the four stories most relevant to you.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Landing page:&lt;/strong&gt; (&lt;a href="https://mohamedaminehamdi.github.io/the-localhost-times/" rel="noopener noreferrer"&gt;https://mohamedaminehamdi.github.io/the-localhost-times/&lt;/a&gt;). It's a newspaper you flip through (drag a corner, swipe, or use the arrow keys), with a &lt;code&gt;.env&lt;/code&gt; builder that runs entirely in your browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Today's demo edition:&lt;/strong&gt; &lt;a href="https://mohamedaminehamdi.github.io/the-localhost-times/demo/" rel="noopener noreferrer"&gt;https://mohamedaminehamdi.github.io/the-localhost-times/demo/&lt;/a&gt;. It was written by Gemma 4 for Sam, a made-up reader with a made-up inbox, using real news and real weather. Press play on the audio bar.&lt;/li&gt;
&lt;/ul&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/mohamedaminehamdi" rel="noopener noreferrer"&gt;
        mohamedaminehamdi
      &lt;/a&gt; / &lt;a href="https://github.com/mohamedaminehamdi/the-localhost-times" rel="noopener noreferrer"&gt;
        the-localhost-times
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A private morning newspaper + audio briefing from your inbox, calendar and news, written on your own machine by Gemma 4. All the news that's fit to print. Printed on your machine.
    &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;The Localhost Times&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;em&gt;All the news that's fit to print. Printed on your machine.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A private morning newspaper with exactly one reader: you.&lt;/p&gt;
&lt;p&gt;Every morning it reads your inbox, today's calendar, your tasks, the weather and the news
Then an open-weight model &lt;strong&gt;running on your own laptop&lt;/strong&gt; (Gemma 4 via Ollama) writes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;one-page newspaper&lt;/strong&gt; you can flip through in the browser or read in your inbox, and&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;3-minute audio briefing&lt;/strong&gt; to listen to while you brush your teeth.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A friend asked for it. They wanted their whole day to fit on one page, without handing their inbox to someone else's cloud.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live demo&lt;/strong&gt; (a made-up reader, Sam): &lt;a href="https://mohamedaminehamdi.github.io/the-localhost-times/" rel="nofollow noopener noreferrer"&gt;https://mohamedaminehamdi.github.io/the-localhost-times/&lt;/a&gt; · &lt;a href="https://mohamedaminehamdi.github.io/the-localhost-times/demo/" rel="nofollow noopener noreferrer"&gt;demo edition&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt; mail (IMAP, read-only) ─┐
 calendars (iCal) ───────┤
 Todoist · GitHub ───────┼──&amp;gt; Gemma 4 on your laptop ──&amp;gt; edition.json ──┬─&amp;gt; newspaper (flip-through web page)
 weather (Open-Meteo) ───┤    (Ollama, JSON schema,                     ├─&amp;gt; audio briefing (ElevenLabs or&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/mohamedaminehamdi/the-localhost-times" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;&lt;strong&gt;The editor is Gemma 4 (&lt;code&gt;gemma4:e4b&lt;/code&gt;), running locally through Ollama.&lt;/strong&gt; The whole thing is one morning run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mail (IMAP, read-only) ─┐
calendars (iCal) ───────┤
Todoist · GitHub ───────┼──&amp;gt; Gemma 4 on your laptop ──&amp;gt; edition.json ──┬─&amp;gt; flip-through newspaper
weather (Open-Meteo) ───┤    (Ollama, JSON schema)                     ├─&amp;gt; audio briefing
news (Google · RSS · HN)┘                                              └─&amp;gt; email · Telegram · Slack · Discord · ntfy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few decisions made it work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The model answers in a schema, not in prose.&lt;/strong&gt; Ollama's structured outputs take a JSON schema (&lt;code&gt;headline&lt;/code&gt;, &lt;code&gt;lede&lt;/code&gt;, &lt;code&gt;must_do_today&lt;/code&gt;, &lt;code&gt;needs_reply&lt;/code&gt;, &lt;code&gt;fyi&lt;/code&gt;, &lt;code&gt;news&lt;/code&gt;, &lt;code&gt;podcast_script&lt;/code&gt;). That way a 4B-class model on a fanless MacBook Air returns something I can lay out every single time. For the news it doesn't rewrite headlines. It picks stories by index, so every link in the paper is real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The prompt is mostly about not making things up.&lt;/strong&gt; It says "Use ONLY the facts in the input". My first test run then taught me three more rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gemma read Sunday night's "before our call tomorrow" on Monday morning and told the reader the call was tomorrow. Now: &lt;em&gt;an email received yesterday that says "tomorrow" means today.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;It filed a bank's "new sign-in" alert under "worth knowing". Now: &lt;em&gt;a security alert is always the first must-do.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;It padded "Must do" with calendar events. Now: &lt;em&gt;must-dos are things you actively do: send, pay, reply, check.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A small model follows rules it is given explicitly, and stumbles on rules it's expected to infer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. One setting that's easy to miss: Ollama's default context window is small.&lt;/strong&gt; Forty emails plus the news overflow it, and the model silently never sees the rest. &lt;code&gt;num_ctx: 16384&lt;/code&gt; is the most important line in the project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Integrations are one function plus one line.&lt;/strong&gt; Every source and delivery is a plain function, switched on by its environment variables in a small registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SOURCES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MAIL_USER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MAIL_PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;emails&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calendar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ICAL_URLS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;                 &lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calendars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calendar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;todoist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TODOIST_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;             &lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;todoist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weather&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WEATHER_CITY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;              &lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weather&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives 20+ integrations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Mail&lt;/strong&gt; (IMAP)&lt;/td&gt;
&lt;td&gt;Gmail, iCloud, Fastmail, Yahoo, Zoho, AOL, GMX, any IMAP server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Calendars&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Any iCal link: Google, Outlook, Apple&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tasks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Todoist, GitHub notifications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Weather&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open-Meteo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;News&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google News, any RSS feed, Hacker News&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Editor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ollama, or any OpenAI-compatible local server: LM Studio, llama.cpp, vLLM, Jan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Delivery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Email, Telegram, Slack, Discord, ntfy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A broken source is logged and skipped, because one flaky API should never cancel the morning paper. &lt;code&gt;uv run localhost-times --list&lt;/code&gt; shows what's switched on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Nearly everything is the standard library.&lt;/strong&gt; It uses &lt;code&gt;imaplib&lt;/code&gt; (opened read-only with &lt;code&gt;BODY.PEEK&lt;/code&gt;, so nothing gets marked as read), &lt;code&gt;smtplib&lt;/code&gt;, &lt;code&gt;urllib&lt;/code&gt; and &lt;code&gt;xml.etree&lt;/code&gt;. There are only three runtime dependencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;icalendar&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;recurring-ical-events&lt;/code&gt;, because your daily standup is a recurring event and a naive parser misses it&lt;/li&gt;
&lt;li&gt;&lt;code&gt;python-dotenv&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. The voice.&lt;/strong&gt; The briefing script is written for the ear: no lists, no URLs, no email addresses. By default macOS's built-in voice reads it aloud (that's what the demo uses), so the whole pipeline runs with the wifi off. Add an ElevenLabs key and it switches to ElevenLabs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. The paper turns.&lt;/strong&gt; The demo edition and the landing page use &lt;a href="https://github.com/Nodlik/StPageFlip" rel="noopener noreferrer"&gt;StPageFlip&lt;/a&gt;: drag a corner and the page curls.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Desktop shows two-page spreads sized to your window.&lt;/li&gt;
&lt;li&gt;Phones get a calm scrolling paper.&lt;/li&gt;
&lt;li&gt;So does anyone with &lt;code&gt;prefers-reduced-motion&lt;/code&gt; turned on.&lt;/li&gt;
&lt;li&gt;The emailed edition is the same pages in one column, because mail clients strip scripts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On my fanless M3 MacBook Air, Gemma 4 prints a full edition in 70 to 90 seconds, before the kettle boils.&lt;/p&gt;

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

&lt;p&gt;Your inbox is the most private thing you own. It holds your bank's security alerts, your doctor, your landlord, your mum.&lt;/p&gt;

&lt;p&gt;The AI morning briefings I tried all work the same way: connect your Google account, and their servers read your mail. For my friend that was a deal-breaker, and I think he's right.&lt;/p&gt;

&lt;p&gt;Open-weight models change that deal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It runs where the data already is.&lt;/strong&gt; Gemma 4 reads the mail on the laptop the mail is already on. Nothing is uploaded to be summarised. Every edition carries an honest byline, "Written on-device by gemma4:e4b". If you point it at a remote server, the byline says so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing per morning.&lt;/strong&gt; No API bill, no subscription, no rate limit, so it can run every day for years.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can't be taken away.&lt;/strong&gt; No one can deprecate the model, change the terms or start training on my friend's mail. The weights are on the disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works offline.&lt;/strong&gt; With the macOS voice, you can print your paper on a plane.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's open all the way down:&lt;/strong&gt; Ollama, Gemma, StPageFlip, Open-Meteo, Hacker News's public API. The project is MIT licensed so anyone can print their own paper, and the README shows how to add an integration in three steps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A closed API would have made this easier to demo and impossible to trust. Here, open &lt;em&gt;was&lt;/em&gt; the feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handing It Over
&lt;/h2&gt;

&lt;p&gt;I handed my friend The Localhost Times. He likes it, and he said it will help him be prepared for his morning. That was the whole brief.&lt;/p&gt;

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

&lt;p&gt;I built this overnight with Claude Code as my pair programmer.&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 4 (&lt;code&gt;gemma4:e4b&lt;/code&gt;) is the editor. It runs locally through Ollama and writes every edition as schema-constrained JSON: the headline, the triage of your inbox, the news picks and the spoken script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Copilot:&lt;/strong&gt; the project is automated with GitHub Actions. One workflow runs the offline test suite and the CLI on every push and pull request. Another publishes the landing page and the demo edition to GitHub Pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading! If you print your own paper, tell me what was on your front page.&lt;/em&gt; 🗞️&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
    </item>
  </channel>
</rss>
