<?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: Biprayan Choudhuri</title>
    <description>The latest articles on DEV Community by Biprayan Choudhuri (@code_neuron58).</description>
    <link>https://dev.to/code_neuron58</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%2F3971945%2F7bad2bc5-e9d9-400f-b61d-799469fb706f.jpeg</url>
      <title>DEV Community: Biprayan Choudhuri</title>
      <link>https://dev.to/code_neuron58</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/code_neuron58"/>
    <language>en</language>
    <item>
      <title>Poltergit: I held a séance with a line of code, and it told me why it exists</title>
      <dc:creator>Biprayan Choudhuri</dc:creator>
      <pubDate>Sun, 01 Nov 2026 01:19:19 +0000</pubDate>
      <link>https://dev.to/code_neuron58/poltergit-i-held-a-seance-with-a-line-of-code-and-it-told-me-why-it-exists-57bh</link>
      <guid>https://dev.to/code_neuron58/poltergit-i-held-a-seance-with-a-line-of-code-and-it-told-me-why-it-exists-57bh</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;p&gt;Every codebase is haunted.&lt;/p&gt;

&lt;p&gt;There's a &lt;code&gt;time.sleep(3)&lt;/code&gt; nobody dares delete. A retry loop set to seven for a reason nobody remembers. A constant changed by a commit titled "update". The people who knew why have moved on, and &lt;code&gt;git blame&lt;/code&gt; only tells you who and when. Never why.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Poltergit&lt;/strong&gt;, a séance for your git history. You point at a line of code, and the line answers in its own voice, citing the commits and discussions behind it. Every ghost brings receipts. And when the record has nothing to say, it says so.&lt;/p&gt;

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

&lt;p&gt;Poltergit is an open-source tool that runs entirely on your laptop, as a command line and a small local web "séance room".&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Summon a line.&lt;/strong&gt; &lt;code&gt;poltergit why src/client.py:13&lt;/code&gt; walks that line's history with &lt;code&gt;git log -L&lt;/code&gt;: every commit that touched it, with its diff and message. If you opt in, it also pulls the GitHub pull requests, reviews and issues around those commits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The line speaks.&lt;/strong&gt; Gemma 4, running locally, answers in the first person, as the code and never as the person who wrote it. Every claim carries a citation and an exact quote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The receipts are checked.&lt;/strong&gt; Before you see an answer, every quote is matched word for word against its source. If one doesn't match, the whole answer is thrown away and you get the labelled source excerpts instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silence is an answer.&lt;/strong&gt; If the history doesn't explain the line, Poltergit says &lt;em&gt;the record is silent&lt;/em&gt; instead of guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hear the spirit.&lt;/strong&gt; Kokoro, an open text-to-speech model, reads the answer aloud with a quiet séance-room echo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An ofrenda for deleted code.&lt;/strong&gt; &lt;code&gt;poltergit ofrenda&lt;/code&gt; builds a Día de los Muertos memorial page. Every deleted file and Python function gets an obituary: when it arrived, how long it served, and the commit that retired it.&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%2Fqch50ts40s2bovzjxqy2.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%2Fqch50ts40s2bovzjxqy2.png" alt="The ofrenda: memorial cards for legacy_ping() with 401 days of service and old_cache.py with 366 days of service, each showing the commit that retired it" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I read the theme:&lt;/strong&gt; the ghosts in the machine are the reasons behind code, still there long after the people who wrote them have left. The séance is the spooky half: a voice from the other side, answering questions. The ofrenda is the honouring half: it remembers what code did before it was removed.&lt;/p&gt;

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

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/mfBNnNZ7Z2I" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The 44-second video walks through a séance: Gemma's answer, the commit behind it, the answer spoken aloud, and the ofrenda. The wait for CPU inference is cut, and the video says so.&lt;/p&gt;

&lt;p&gt;The demo repository is fictional and clearly labelled. &lt;code&gt;poltergit demo&lt;/code&gt; creates it with six real commits, so anyone can reproduce every answer below. Here is line 13, &lt;code&gt;time.sleep(3)&lt;/code&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%2Fphm5uwq14yujzd6rd2hu.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%2Fphm5uwq14yujzd6rd2hu.png" alt="The séance room: line 13, time.sleep(3), answers that its existence is tied to preventing a reconnect storm, with a citation, 1,503 tokens, $0 inference cost and &amp;quot;quotes matched&amp;quot;" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"My existence is tied to preventing a reconnect storm, as a three-second delay gives that reservation time to expire." [1]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Source [1] is the commit "fix: wait three seconds before reconnecting", whose message explains that the gateway keeps a closed session reserved for two seconds. The footer shows what the answer cost: 1,503 tokens, $0 in inference, and every quote matched.&lt;/p&gt;

&lt;p&gt;Line 5, &lt;code&gt;MAGIC_NUMBER = 43&lt;/code&gt;, was changed by a commit titled "update". The honest answer is &lt;em&gt;the record is silent&lt;/em&gt;, and that's what Poltergit says. Once, in live testing, Gemma tried to give a reason anyway, with a citation that didn't match its source. The check caught it, threw the answer away, and showed the commit text instead, clearly labelled.&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/CodeNeuron58" rel="noopener noreferrer"&gt;
        CodeNeuron58
      &lt;/a&gt; / &lt;a href="https://github.com/CodeNeuron58/Poltergit" rel="noopener noreferrer"&gt;
        Poltergit
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A séance for your git history. Every ghost brings receipts.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The code is MIT-licensed Python. CI runs the tests on Ubuntu and Windows, including a pgvector integration test against a real Postgres database.&lt;/p&gt;

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

&lt;p&gt;Everything that thinks runs on my laptop through &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;. There's no API key and no cloud model.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The history walk: git
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;git log -L&lt;/code&gt; follows one line through every commit that changed it, even when its line number moved. Poltergit reads the committed &lt;code&gt;HEAD&lt;/code&gt; only, warns you about uncommitted changes, and trims the prompt without dropping the line's oldest history.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The voice: Gemma 4 E4B
&lt;/h3&gt;

&lt;p&gt;Gemma gets the line, the code around it and its history, and must answer in structured JSON: a list of claims, each with a source ID and an exact quote, or &lt;code&gt;silent: true&lt;/code&gt;. Two rules are enforced in code, not just requested in the prompt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every quote must be at least 12 characters long and appear word for word in its source;&lt;/li&gt;
&lt;li&gt;the answer must speak as the code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Break either rule and the answer is discarded. Matching quotes can't prove the reasoning is right, but it guarantees that every sentence you see points at words that really exist in the record.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The memory: Tiger Data, pgvector and EmbeddingGemma
&lt;/h3&gt;

&lt;p&gt;Line history alone misses things. The retry loop on line 9 was only ever touched by the commit that introduced the client, so on its own the séance has nothing to say. The real reason lives in a different commit: the one that raised &lt;code&gt;MAX_RETRIES&lt;/code&gt; from 3 to 7.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;poltergit index&lt;/code&gt; embeds the repository's history with &lt;strong&gt;EmbeddingGemma&lt;/strong&gt; (768 dimensions, also local) and stores it in Postgres with &lt;strong&gt;pgvector&lt;/strong&gt; on a free &lt;strong&gt;Tiger Cloud&lt;/strong&gt; service. A single SQL query runs two searches and merges them with Reciprocal Rank Fusion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;vector similarity over the embeddings, using an HNSW cosine index;&lt;/li&gt;
&lt;li&gt;keyword ranking over Postgres full-text search.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every search is scoped to one repository, revision and embedding model, so histories never mix.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line 9: &lt;code&gt;for attempt in range(MAX_RETRIES):&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;What the line says&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Line history only&lt;/td&gt;
&lt;td&gt;&lt;em&gt;The record is silent&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With hybrid search on Tiger Data&lt;/td&gt;
&lt;td&gt;"I exist to tolerate the gateway maintenance window, as seven attempts cover the gateway's eighteen-second maintenance window…", with a citation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  4. The receipts for the receipts: Sentry Agent Tracing
&lt;/h3&gt;

&lt;p&gt;Every séance is a Sentry transaction (&lt;code&gt;gen_ai.invoke_agent&lt;/code&gt;, agent name "Poltergit") with a span for each step. Prompts and source code are never sent: &lt;code&gt;send_default_pii&lt;/code&gt; is off, so Sentry sees timings, the model and token counts, not my code.&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%2Fcbjra4luvs3cn769jg1b.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%2Fcbjra4luvs3cn769jg1b.png" alt="A Sentry trace of one séance: gen_ai.invoke_agent for 2.90 minutes, with git.history at 677 ms, gen_ai.chat at 2.89 minutes and evidence.validate at 0.12 ms" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One trace tells the whole story. Walking the history took 677 ms and checking the quotes took 0.12 ms. Gemma took 2.89 minutes on my CPU-only laptop, for 728 tokens in and 834 out. Inference cost: $0.&lt;/p&gt;

&lt;p&gt;How the traces helped me debug:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A timeout that looked like a broken model.&lt;/strong&gt; My first cold run failed with a model error at 181 seconds. The trace showed the model was working, just slowly (6 to 11 tokens a second without a GPU), and my 180-second timeout was the real problem. It's now configurable, and the setup guide says to raise it on CPU-only machines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing AI spans.&lt;/strong&gt; A test of the exact data sent to Sentry showed the AI child spans weren't inside the transaction, because the SDK streams them separately by default. I turned that off, so each séance arrives as one complete trace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traces that never arrived.&lt;/strong&gt; The command-line tool exited before the SDK sent anything. I'd disabled its default integrations, including the one that flushes on exit. One explicit &lt;code&gt;flush()&lt;/code&gt; before exiting fixed it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. The rest
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kokoro&lt;/strong&gt;, an open 82M-parameter text-to-speech model run through ONNX, reads the answer slightly slowed, with quiet delayed reflections for the séance-room echo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI, Typer and Jinja2&lt;/strong&gt; power the local séance room, the CLI and the static ofrenda export. The server only listens on localhost and uses anti-CSRF tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ofrenda's birth dates are real.&lt;/strong&gt; Function obituaries come from comparing Python syntax trees across up to 200 revisions of a file, so each function's birth date is when it actually appeared.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your history stays yours.&lt;/strong&gt; A repository's history is a company's diary: messages, reviews, internal reasons. Gemma, EmbeddingGemma and Kokoro all run on the laptop, so no closed AI API ever reads it. The only things that leave are what you choose: embeddings in your own database, and traces with no content in them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The safety net needs the whole loop.&lt;/strong&gt; Checking every quote word for word only works because I control everything: the prompt, the structured output, and what happens when an answer fails. I could test that the net holds, and it did, catching a made-up citation in live testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to ask.&lt;/strong&gt; Every séance is $0 in inference. A tool you consult on every confusing line in a legacy codebase can't charge per question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The past should outlive the vendor.&lt;/strong&gt; A tool for reading history shouldn't depend on an API that can be retired. The open weights on my disk will still be there in ten years.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Spirit of It
&lt;/h2&gt;

&lt;p&gt;The moment it landed wasn't on my demo repository. It was on &lt;code&gt;request&lt;/code&gt;, the npm library that was deprecated in 2020.&lt;/p&gt;

&lt;p&gt;I asked line 3 of its README why it exists. That's the line that reads "As of Feb 11th 2020, request is fully deprecated." Poltergit found the commit "doc: note full deprecation (#3267)" and the pull request behind it, whose entire description is two words: "It's time." There wasn't enough written down for Gemma to say more, so the séance showed me the excerpts, labelled, and fell silent. That felt right.&lt;/p&gt;

&lt;p&gt;Then I asked line 49 of its &lt;code&gt;package.json&lt;/code&gt;, the &lt;code&gt;uuid&lt;/code&gt; dependency. Its history holds a 2016 commit: "using defunctzombie fork of uuid because node-uuid has been abandoned by developer". Gemma answered by citing that commit. A deprecated library, still carrying a line that only exists because another library was abandoned before it. That's exactly the kind of ghost I built this for.&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 E4B writes every answer locally through Ollama, as structured JSON that's checked quote by quote, and EmbeddingGemma creates the 768-dimension vectors behind the hybrid search. Two Gemma models, both local, $0 in inference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Tiger Data:&lt;/strong&gt; the repository's history is embedded into pgvector on a free Tiger Cloud service, and one SQL query merges vector and keyword search with Reciprocal Rank Fusion, scoped by repository, revision and model. It turns line 9 from "the record is silent" into a cited answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Sentry Agent Tracing:&lt;/strong&gt; every séance is a &lt;code&gt;gen_ai.invoke_agent&lt;/code&gt; transaction with spans for the history walk, the Gemma call (model, tokens, latency) and quote verification, and no prompts or code are sent. The traces found a timeout, missing AI spans and a bug where traces were lost on exit.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Thanks for reading. Which line in your codebase would you summon first? 🕯️&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Tortoise: a slow agent that reads your council's planning applications so you don't have to</title>
      <dc:creator>Biprayan Choudhuri</dc:creator>
      <pubDate>Mon, 26 Oct 2026 02:39:58 +0000</pubDate>
      <link>https://dev.to/code_neuron58/tortoise-a-slow-agent-that-reads-your-councils-planning-applications-so-you-dont-have-to-5akk</link>
      <guid>https://dev.to/code_neuron58/tortoise-a-slow-agent-that-reads-your-councils-planning-applications-so-you-dont-have-to-5akk</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;p&gt;Most people find out what's being built next door when the scaffolding goes up.&lt;/p&gt;

&lt;p&gt;It was never a secret. In the UK, every planning application is published on the council's website, and neighbours get a few weeks to comment. But "published" means a weekly list of twenty-odd entries in planning shorthand, each linking to a pile of PDFs, with the comment deadline tucked away in a separate tab. Nobody reads that website. I wouldn't either.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Tortoise&lt;/strong&gt;, an agent that reads it for you: slowly, once a week, one page at a time.&lt;/p&gt;

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

&lt;p&gt;Every Monday at 07:00 UK time, Tortoise:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Opens the council's weekly list&lt;/strong&gt; of new planning applications in a real browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keeps only the ones near you.&lt;/strong&gt; You give it street names and postcode sectors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reads their documents:&lt;/strong&gt; the application form, the design and access statement, the planning statement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emails you one card per application:&lt;/strong&gt; what's proposed, why a neighbour might care, the date you can comment by, and a link to the council's page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the whole product. One email you can read in a minute, instead of a website you would never open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it's for:&lt;/strong&gt; anyone who'd like a say in what gets built on their street but will never read a planning portal. That includes neighbours, residents' associations, local councillors and the local paper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why UK councils, when I live in India?&lt;/strong&gt; More than 150 of them run the same planning software, Idox Public Access. An agent that understands one portal understands them all. The catch is that those portals block traffic from outside Europe, so Tortoise's browser runs in Frankfurt (more on that below).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it rewards laziness:&lt;/strong&gt; you do nothing. Tortoise does the reading, and it's deliberately slow about it: 5 to 10 seconds between pages, 8 seconds between applications, and only last week's list. In a normal week only one step costs money, and this week that step cost about a fifth of a cent.&lt;/p&gt;

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

&lt;p&gt;Tortoise has no website to click around. It's a Monday-morning job that runs on my machine, so the demo is what it sends you.&lt;/p&gt;

&lt;p&gt;Here is a real week: West Suffolk Council's list for 19 to 23 October 2026, with the York Road area of Bury St Edmunds as the watch zone. This is that run's brief and numbers in the current email design. I've removed house numbers; the real email has them.&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%2Fm80p7bvuu60ar5ckdzpb.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%2Fm80p7bvuu60ar5ckdzpb.png" alt="The weekly email: one card per application, with what is proposed, why a neighbour might care, the comment deadline with a countdown, and a link to the council's page" width="800" height="1571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Look at the York Road card. The council's listing says "single storey rear extension". The application form, the PDF nobody opens, says "rear and side extension". If you live on that side, that's the one sentence you needed, and Tortoise found it because it actually read the form.&lt;/p&gt;

&lt;p&gt;On the tree card, the documents don't give a reason for cutting five metres off the crown, so the brief says "not stated" instead of inventing one.&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/CodeNeuron58" rel="noopener noreferrer"&gt;
        CodeNeuron58
      &lt;/a&gt; / &lt;a href="https://github.com/CodeNeuron58/tortoise" rel="noopener noreferrer"&gt;
        tortoise
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A slow, steady agent that reads a UK council's weekly planning list and the PDFs behind it, then emails you a one-page brief: what's being built near your street, and the date to object by.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The code is MIT-licensed Python. CI runs 110 offline tests on every push, with no network, no browser and no model. The real run and all six failure tests have their evidence in &lt;code&gt;docs/evidence/&lt;/code&gt; (exported Temporal histories, plus CLI output for the duplicate-run test), so every number in this post has a file behind it.&lt;/p&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;Temporal workflow: WeeklyWatch (one per council, every Monday 07:00 UK time)
  └─ crawl_list ──▶ zone filter ──▶ fetch_application (+8 s pause) per app
        └─▶ gemma_filter (local, free) ──▶ write_brief (paid, counted)
              └─▶ send_email ──▶ record_run (the Lazy Payoff numbers)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. The browser: Stagehand on Browserbase
&lt;/h3&gt;

&lt;p&gt;The crawl is a &lt;a href="https://github.com/browserbase/stagehand" rel="noopener noreferrer"&gt;Stagehand&lt;/a&gt; script running in a &lt;a href="https://www.browserbase.com" rel="noopener noreferrer"&gt;Browserbase&lt;/a&gt; session pinned to &lt;code&gt;eu-central-1&lt;/code&gt;, because UK planning portals refuse traffic from other regions. The PDFs download through the same session, so every request comes from the same place.&lt;/p&gt;

&lt;p&gt;Idox portals share one page structure, so the stable parts are read with direct selectors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the weekly-list form,&lt;/li&gt;
&lt;li&gt;the results table,&lt;/li&gt;
&lt;li&gt;the Documents tab,&lt;/li&gt;
&lt;li&gt;the Important Dates tab, where the real comment deadline lives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stagehand's plain-English &lt;code&gt;act&lt;/code&gt;, &lt;code&gt;extract&lt;/code&gt; and &lt;code&gt;observe&lt;/code&gt; are the fallback for councils whose layout differs, with instructions like "set the list period to last week" and "click the Next page button". In this week's run the selectors covered everything, so the model that drives Stagehand was never called and the crawl cost nothing but browser minutes.&lt;/p&gt;

&lt;p&gt;That driving model is pluggable. I wrote an adapter (&lt;code&gt;crawl/llm_adapter.py&lt;/code&gt;) that sends Stagehand's model calls to any OpenAI-compatible endpoint: local Gemma through Ollama, or Ultramarine.&lt;/p&gt;

&lt;p&gt;The politeness rules are in code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It never opens the tabs that list applicants' names.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At most three text documents per application.&lt;/strong&gt; Drawings, plans and photos are skipped by name, and scanned PDFs are detected and skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every session is released the moment its work is done.&lt;/strong&gt; The whole week took &lt;strong&gt;1.71 browser minutes across three sessions&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. The slow part: Temporal
&lt;/h3&gt;

&lt;p&gt;The whole agent is one &lt;a href="https://temporal.io" rel="noopener noreferrer"&gt;Temporal&lt;/a&gt; workflow, &lt;code&gt;WeeklyWatch&lt;/code&gt;, run per council per week by a schedule (&lt;code&gt;0 7 * * MON&lt;/code&gt;, Europe/London). Temporal is what lets a slow agent also be a reliable one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Crawling resumes mid-list.&lt;/strong&gt; &lt;code&gt;crawl_list&lt;/code&gt; heartbeats after every results page, and each page's rows are saved before the heartbeat. If the worker dies, the retry reads the last heartbeat and jumps straight to that page, since Idox page links carry the page number. Rows are upserted by council and reference, so nothing duplicates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The politeness survives crashes.&lt;/strong&gt; The 8-second pause between applications is a &lt;code&gt;workflow.sleep&lt;/code&gt;, so a crash can't skip it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failures are handled honestly.&lt;/strong&gt; Activities retry three times with backoff. A bad API key or empty credit is non-retryable, and the email says the brief was skipped and why. If Ollama stays down, every application is kept and the run is marked "unfiltered". Tortoise never silently drops an application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No double emails.&lt;/strong&gt; Manual runs use the workflow id &lt;code&gt;watch-&amp;lt;council&amp;gt;-&amp;lt;week&amp;gt;&lt;/code&gt; with a reject-duplicate policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I broke it on purpose, six ways, against the live stack. Each test's evidence is in the repo. Here is test 1 in Temporal's timeline: the crawl's first attempt (red) died with the worker after page 2, and the second attempt picked up at page 2 on a new worker.&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%2Fds0iqs9zjx8yb4qblu06.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%2Fds0iqs9zjx8yb4qblu06.png" alt="Temporal timeline: crawl_list attempt 1 killed with the worker after page 2, attempt 2 resumed at page 2 on a new worker, then fetch_application with the 8-second pause" width="800" height="600"&gt;&lt;/a&gt;&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;What I broke&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Killed the worker after results page 2&lt;/td&gt;
&lt;td&gt;Heartbeat timed out after 2 minutes; the new worker logged &lt;code&gt;resuming crawl from page 2&lt;/code&gt;. 23 rows, no duplicates.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Ended the Browserbase session mid-crawl&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CDP connection closed&lt;/code&gt;, then a retry in a fresh session from page 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;The brief failing&lt;/td&gt;
&lt;td&gt;Three attempts, then an email saying the brief was skipped, and why. The exported run is a real failure: Ultramarine returned an empty reply.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Stopped Ollama&lt;/td&gt;
&lt;td&gt;Gemma failed three times; both applications were kept and the run marked unfiltered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Ran the same week twice&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Workflow execution already started&lt;/code&gt;, and one email&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Stalled the crawl for 150 seconds, like a slow site&lt;/td&gt;
&lt;td&gt;The first attempt was cancelled at 2:00; the retry crawled all 3 pages and finished&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  3. The free filter: Gemma 4, on my laptop
&lt;/h3&gt;

&lt;p&gt;First, a plain-Python filter matches street names and postcode sectors. That took this week's list from 23 applications to 2 before a single application page was opened.&lt;/p&gt;

&lt;p&gt;Then &lt;strong&gt;Gemma 4 E4B&lt;/strong&gt; runs locally through &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;'s OpenAI-compatible endpoint, at temperature 0 in JSON mode. For each application it returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a type: extension, new homes, change of use, demolition, trees, mast or antenna, shopfront or advert, or other,&lt;/li&gt;
&lt;li&gt;likely street impacts, such as parking, noise or daylight,&lt;/li&gt;
&lt;li&gt;a relevance score from 0 (elsewhere) to 3 (on your street),&lt;/li&gt;
&lt;li&gt;and a one-line summary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tortoise keeps 2 and 3. The JSON parsing is strict and tested against broken replies.&lt;/p&gt;

&lt;p&gt;It took 82 seconds for two applications on my laptop. That's slow, and it's fine: it's a Monday-morning job, nobody is waiting, and it's free.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The one paid step: Ultramarine 1 Flash
&lt;/h3&gt;

&lt;p&gt;Paper Instruments' &lt;strong&gt;Ultramarine 1 Flash&lt;/strong&gt; is called through the standard OpenAI Python SDK; only the base URL, key and model name change. It receives each kept application's council listing plus the text of its documents, trimmed to a 60,000-token budget (the model accepts 131,072).&lt;/p&gt;

&lt;p&gt;The prompt's rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use facts from the listing and documents only.&lt;/li&gt;
&lt;li&gt;Write "not stated" when a fact is missing.&lt;/li&gt;
&lt;li&gt;Never name applicants, agents or any other private individual.&lt;/li&gt;
&lt;li&gt;Reply in JSON.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The comment deadline and the link are filled in from the council's own pages, never from the model.&lt;/p&gt;

&lt;p&gt;Every call is costed to the token and logged in SQLite, and the total goes in the email's footer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cost (USD) = ( 0.15 × uncached input
             + 0.03 × cached input
             + 0.50 × output ) / 1,000,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This week's brief used 4,187 tokens in and 2,852 out, for &lt;strong&gt;$0.0021&lt;/strong&gt;. Every brief in the cost log, nine of them from my first test run to the last failure test, cost &lt;strong&gt;$0.012&lt;/strong&gt; together, out of the $5 Hacktoberfest credit.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The email: Brevo
&lt;/h3&gt;

&lt;p&gt;The email goes out as designed HTML and plain text through &lt;a href="https://www.brevo.com" rel="noopener noreferrer"&gt;Brevo&lt;/a&gt;'s free plan. Cards are sorted by deadline, each with a countdown, and the run's numbers are in the footer. If the brief was skipped, the email says so instead of going quiet.&lt;/p&gt;

&lt;h3&gt;
  
  
  What broke on the way
&lt;/h3&gt;

&lt;p&gt;A few things I got wrong before the run above, all fixed now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The in-session PDF download built invalid JavaScript, so early runs read zero PDFs and their briefs had only the council's one-line listing to go on.&lt;/li&gt;
&lt;li&gt;Comment deadlines came from the summary tab. The Important Dates tab is the one that's right.&lt;/li&gt;
&lt;li&gt;The brief prompt never actually included the proposal text.&lt;/li&gt;
&lt;li&gt;Browser sessions stayed open until Browserbase's timeout instead of being released.&lt;/li&gt;
&lt;li&gt;Cached tokens were billed twice in my cost formula.&lt;/li&gt;
&lt;li&gt;Ultramarine once returned an empty brief because its reasoning used up a small output cap. The cap is now 8,000 tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The robots.txt lesson
&lt;/h3&gt;

&lt;p&gt;After my live runs, I checked robots.txt properly. Every Idox portal I could reach, West Suffolk and Sevenoaks plus twelve more, says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-agent: *
Disallow: /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I should have checked before the first run, not after. A run reads about seven pages in four minutes, with pauses between them. But I ran it many times while testing, and a polite crawler is still a crawler.&lt;/p&gt;

&lt;p&gt;So Tortoise ships with both example councils &lt;strong&gt;disabled&lt;/strong&gt;, and a council with &lt;code&gt;enabled = false&lt;/code&gt; cannot start a run. The README says to switch one on only with that council's permission. For a project whose whole point is helping people take part in local decisions, asking first is the only version that makes sense.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The boring 90% costs nothing.&lt;/strong&gt; The crawl logic, the zone filter, Gemma and the scheduler are all open source or open-weight and run on my laptop; only the browser itself is rented, by the minute. If every step went through a hosted model, I'd be paying to read every page of every council's list, every week. Here the only metered step is one short brief about the few applications that matter, which is what makes "every council, every week" thinkable for a residents' group with no budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I could fix the framework instead of working around it.&lt;/strong&gt; Stagehand is open source, so I could read exactly how it calls its model and write an adapter that sends those calls to a local model instead. A closed browser agent would have made that choice for me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most of the data stays at home.&lt;/strong&gt; Filtering the whole list, by street and then by Gemma, happens on my machine. Only the few kept applications' documents go to a hosted model, and the brief is forbidden to name anyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rules are inspectable.&lt;/strong&gt; Politeness and privacy are code, not a promise on a terms page: the pauses, the three-document cap, never opening the contacts tab. A council's IT team can read exactly what Tortoise will and won't do before saying yes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing is locked in.&lt;/strong&gt; Gemma, Stagehand's driving model and the brief writer all sit behind OpenAI-compatible endpoints, switched with environment variables. Temporal runs locally with a database file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&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%2Fdptpwmvuqpqdovkuv34y.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%2Fdptpwmvuqpqdovkuv34y.png" alt="The email's footer: 23 applications listed, 2 in your zone, 2 PDFs read for you, 7 pages read, 1.71 browser minutes, $0.0021 total cost" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The real week: West Suffolk, 19 to 23 October 2026&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Applications on the weekly list&lt;/td&gt;
&lt;td&gt;23, over 3 results pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On the watched streets&lt;/td&gt;
&lt;td&gt;2 (York Road and Springfield Road)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documents read for you&lt;/td&gt;
&lt;td&gt;2 application forms, 13 pages; drawings skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comment deadlines found&lt;/td&gt;
&lt;td&gt;10 and 14 November 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser time&lt;/td&gt;
&lt;td&gt;1.71 minutes, 3 sessions, all released&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma, running locally&lt;/td&gt;
&lt;td&gt;82 seconds, $0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ultramarine&lt;/td&gt;
&lt;td&gt;7,039 tokens, $0.0021&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The whole run&lt;/td&gt;
&lt;td&gt;about 4 minutes, from start to email&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time a human spent reading&lt;/td&gt;
&lt;td&gt;about a minute, on the email&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Doing this by hand means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;opening the weekly list,&lt;/li&gt;
&lt;li&gt;checking 23 addresses against your streets,&lt;/li&gt;
&lt;li&gt;opening two applications and finding each deadline in its own tab,&lt;/li&gt;
&lt;li&gt;and reading 13 pages of forms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Call it half an hour a week. Mostly, though, people don't do it at all, and that's the real cost: the deadline passes without them.&lt;/p&gt;

&lt;p&gt;To be honest about usage: so far, the only inbox Tortoise has emailed is mine.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Browserbase:&lt;/strong&gt; Stagehand drives each council's live planning portal in a Browserbase session pinned to &lt;code&gt;eu-central-1&lt;/code&gt; (UK portals block other regions). It downloads the PDFs through that same session and releases it the moment it's done, so a full week took 1.71 browser minutes. Temporal turns that agent run into a repeatable Monday workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Ultramarine:&lt;/strong&gt; Ultramarine 1 Flash, called through the OpenAI SDK, reads the kept applications' documents and writes the brief under strict "not stated, never guess, never name anyone" rules. Every call's tokens and dollars are logged: this week's run cost $0.0021, and every brief in the cost log, tests included, cost $0.012.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Temporal:&lt;/strong&gt; the whole agent is one durable &lt;code&gt;WeeklyWatch&lt;/code&gt; workflow on a Monday schedule. It has per-page heartbeats so a crash resumes mid-crawl, a durable 8-second politeness sleep, non-retryable auth errors and duplicate-run rejection, proven by six live failure tests with their evidence in the repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Gemma 4 E4B runs locally through Ollama and labels every in-zone application (type, street impact, relevance 0 to 3) for free, so only the applications that matter reach a paid model. If Ollama is down, nothing is silently dropped.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Thanks for reading. If you live in one of those 150-odd council areas, I'd love to know what you'd want the Monday email to tell you that it doesn't yet. And if you work at a council, I'd love your permission to point a very polite tortoise at your weekly list. 🐢&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Turnaround: an open-source hiking buddy that knows when you should turn back, and tells someone if you don't come home</title>
      <dc:creator>Biprayan Choudhuri</dc:creator>
      <pubDate>Mon, 12 Oct 2026 03:12:09 +0000</pubDate>
      <link>https://dev.to/code_neuron58/turnaround-an-open-source-hiking-buddy-that-knows-when-you-should-turn-back-and-tells-someone-if-1pha</link>
      <guid>https://dev.to/code_neuron58/turnaround-an-open-source-hiking-buddy-that-knows-when-you-should-turn-back-and-tells-someone-if-1pha</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;Most hiking trouble isn't dramatic. Nobody falls off a cliff. Someone simply keeps going a little too long because the summit is "just around the corner", the sun sets earlier than they thought, and the only person who knows where they went is themselves.&lt;/p&gt;

&lt;p&gt;Two things would fix most of that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Knowing, before you leave, the time you should turn around&lt;/strong&gt;, based on how fast &lt;em&gt;you&lt;/em&gt; actually walk rather than a sign at the trailhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Someone you trust finding out automatically if you don't come back&lt;/strong&gt;, without you having to remember to tell them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I built &lt;strong&gt;Turnaround&lt;/strong&gt;. You spend about two minutes with it before a hike, then put your phone in your pocket.&lt;/p&gt;

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

&lt;p&gt;Turnaround is a hiking companion that runs entirely on your own laptop. Before a hike it gives you three times:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Turn around by:&lt;/strong&gt; not at the top or the far end by this time? Head back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back by:&lt;/strong&gt; 9 in 10 hikes like yours finish before this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sunset margin:&lt;/strong&gt; how much daylight you have to spare.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then it does four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Predicts&lt;/strong&gt; how long &lt;em&gt;you&lt;/em&gt; will take, as a range rather than a single guess. An open tabular foundation model (TabPFN) learns from 7,186 real recorded hikes and adjusts to your pace after every hike you finish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Briefs&lt;/strong&gt; you out loud. Gemma 4, running locally, turns the numbers into a short spoken briefing that sounds like a friend who hikes. It's saved to your phone before you leave, so it plays with no signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watches.&lt;/strong&gt; When you tap Start, a durable safety timer begins waiting for your check-in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tells someone.&lt;/strong&gt; If you're well past your back-by time and still haven't checked in, your emergency contact gets an email with your plan, your route and a map link to where you set off. If you then check in late, they get a second email saying you're safe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No GPX file? You just enter the three numbers every trail page lists (distance, elevation gain and trailhead), and Turnaround plans from those.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it's for:&lt;/strong&gt; anyone who hikes alone or is new to it, and anyone with a parent, partner or friend who worries when they head out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it gets you off the screen:&lt;/strong&gt; the screen part is the beginning and the end of the trip. Plan, listen, pocket the phone, walk. The next time you look at it is to tap &lt;em&gt;Hike complete&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&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%2F4158exc2ygmupx8hbvn0.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%2F4158exc2ygmupx8hbvn0.png" alt="How it works: plan, go, check in" width="800" height="642"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/eOzc_RNBzMk" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The video walks through the whole flow: planning a hike, the spoken briefing, starting the trip and the safety timer, and checking in.&lt;/p&gt;

&lt;p&gt;Turnaround runs on my laptop, by design (more on that below), so there's no public link. The repo has one-command-per-terminal setup if you want to run it yourself.&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/CodeNeuron58" rel="noopener noreferrer"&gt;
        CodeNeuron58
      &lt;/a&gt; / &lt;a href="https://github.com/CodeNeuron58/Turnaround" rel="noopener noreferrer"&gt;
        Turnaround
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      An open-source hiking companion
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;MIT-licensed. CI runs typechecks, the app build and two unit-test suites on every push. Everything I claim below has a file in the repo backing it.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Everything runs on one laptop:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phone or browser&lt;/strong&gt; → the &lt;strong&gt;app&lt;/strong&gt; (React + Vite), reached over Wi-Fi or Tailscale&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App&lt;/strong&gt; → the &lt;strong&gt;prediction service&lt;/strong&gt; (FastAPI + TabPFN + SQLite)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prediction service&lt;/strong&gt; → &lt;strong&gt;Gemma 4&lt;/strong&gt; (via Ollama) for the briefing, &lt;strong&gt;Kokoro&lt;/strong&gt; for the voice, and the &lt;strong&gt;Temporal server&lt;/strong&gt; to arm the timer and send check-ins&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporal worker&lt;/strong&gt; → the alert and all-clear &lt;strong&gt;emails&lt;/strong&gt; to your contact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only things that leave it are an anonymous weather lookup (coordinates only) and, if you're overdue, the alert email.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Predicting &lt;em&gt;your&lt;/em&gt; hike time: TabPFN
&lt;/h3&gt;

&lt;p&gt;The classic hiking-time rule is &lt;strong&gt;Naismith's rule&lt;/strong&gt;: roughly 12 minutes per kilometre plus 1 minute per 10 m of climbing. It's on half the trail signs in the world, and it gives the same answer for everyone.&lt;/p&gt;

&lt;p&gt;I wanted a range, and I wanted it to fit the person. &lt;a href="https://github.com/PriorLabs/TabPFN" rel="noopener noreferrer"&gt;TabPFN&lt;/a&gt; is an open tabular foundation model: you give it a table of examples and it predicts new rows &lt;em&gt;in context&lt;/em&gt;, with no training loop. Crucially for a safety tool, it returns a full predictive distribution rather than one number.&lt;/p&gt;

&lt;p&gt;The data is &lt;strong&gt;7,186 GPS-recorded hikes from hikr.org&lt;/strong&gt; (a public Kaggle dataset, CC0). The features are distance, climb, descent, highest point and the SAC difficulty grade (T1–T6). I held out &lt;strong&gt;1,438 hikes the model never saw&lt;/strong&gt; and compared it with Naismith:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;On 1,438 unseen hikes&lt;/th&gt;
&lt;th&gt;Naismith's rule&lt;/th&gt;
&lt;th&gt;TabPFN v3.5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Average error&lt;/td&gt;
&lt;td&gt;44.1 min&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34.8 min&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RMSE&lt;/td&gt;
&lt;td&gt;69.1 min&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;54.5 min&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real times inside the predicted 90% band&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;89.5%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hikers back before the "back by" time (P90)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;90.4%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the one I care about most. A safety tool has to be &lt;strong&gt;honest about its uncertainty&lt;/strong&gt;, not just accurate on average. When Turnaround says "9 in 10 hikes like this are done by 4:40", that turns out to be true for 90.4% of hikes. Average error being 21% lower is a nice bonus.&lt;/p&gt;

&lt;p&gt;On a laptop CPU I serve TabPFN v2 by default. It's about 3x faster with nearly identical calibration (89.7%). The v3.5 checkpoint is one environment variable away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How the prediction becomes decisions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Back by&lt;/strong&gt; = the 90th percentile + your planned breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn around by&lt;/strong&gt; = the share of that time you should spend heading &lt;em&gt;out&lt;/em&gt;, assuming you retrace your steps. Climbing happens on the way out, so steep routes front-load it (Naismith's split, clamped between 50% and 70%).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert&lt;/strong&gt; = the 95th percentile + breaks + 30 minutes of grace. I deliberately didn't make it the back-by time: your contact should only be emailed when you're &lt;em&gt;genuinely&lt;/em&gt; late, not five minutes slow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Learning your pace.&lt;/strong&gt; The crowd isn't you. Every hike you finish compares your actual moving time with the model's median for the same route, and the ratios combine into a &lt;strong&gt;personal pace factor&lt;/strong&gt;. It's shrunk toward 1.0 on purpose, so one bad day can't swing the safety timer. A hike that was 40% slower than predicted moves your next estimate up about 12%, not 40%. Hikes where you tapped &lt;em&gt;I turned back early&lt;/em&gt; don't count, because a turned-back time says nothing fair about the whole route. All of these rules are in a small, unit-tested module.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A promise that survives a crash: Temporal
&lt;/h3&gt;

&lt;p&gt;The scariest bug a safety app can have is a timer that silently dies. If the alert depends on a Node process staying alive for six hours on a laptop, it's not a safety feature, it's a hope.&lt;/p&gt;

&lt;p&gt;So the safety timer is a &lt;a href="https://temporal.io" rel="noopener noreferrer"&gt;Temporal&lt;/a&gt; workflow. One &lt;code&gt;tripWorkflow&lt;/code&gt; per hike races a &lt;strong&gt;check-in signal&lt;/strong&gt; against a &lt;strong&gt;durable timer&lt;/strong&gt; set to the alert moment. Sending the email is an activity with retries. A late check-in after the alert triggers the all-clear. I run Temporal's free local dev server with an on-disk database, so timers survive a restart of the server itself, not just the worker.&lt;/p&gt;

&lt;p&gt;Then I tried to break it. Each scenario has its workflow history or logs committed in &lt;code&gt;docs/evidence/&lt;/code&gt;:&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;What I did&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Checked in on time&lt;/td&gt;
&lt;td&gt;Timer closed, nobody emailed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Missed the check-in while the mail server failed&lt;/td&gt;
&lt;td&gt;Activity retried; alert went out on attempt 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Killed the worker mid-trip&lt;/strong&gt;, restarted it 25 s later&lt;/td&gt;
&lt;td&gt;Alert still fired &lt;strong&gt;exactly on the deadline&lt;/strong&gt;: 90.5 s after start, despite the worker being dead from second 20 to 45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Drill: missed check-in, then a late check-in&lt;/td&gt;
&lt;td&gt;Alert on the deadline, then an all-clear&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Killed the Temporal server mid-trip&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Timer survived on disk; a check-in sent during the outage reported the failure honestly and landed when re-sent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Scenario 3 is the whole reason Temporal is here. The promise didn't notice the outage.&lt;/p&gt;

&lt;p&gt;This is what your contact actually receives. Both emails below were delivered to my own phone through Brevo, using the same templates and code path the worker uses (sent with the preview command, which is why they're marked &lt;code&gt;[PREVIEW]&lt;/code&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%2Fuggzden7hzwcpt9138x2.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%2Fuggzden7hzwcpt9138x2.png" alt="The alert email and the all-clear email, as they arrived on my phone" width="800" height="803"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The alert says who, which route, when they set off and when they should have been back, with a one-tap map link to the starting point. The all-clear tells your contact they can stand down. Nobody has to decode a log file at 4 a.m.&lt;/p&gt;

&lt;p&gt;The app is equally honest when something is wrong. If the timer fails to arm, the trail screen shows a red "safety timer NOT armed" banner with a retry button rather than pretending you're covered. If you check in with no signal, the check-in is saved on the phone and sent the moment it can reach the laptop, and re-sending is idempotent, so a flaky connection can't double anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A briefing that sounds like a friend: Gemma 4 + Kokoro
&lt;/h3&gt;

&lt;p&gt;Numbers on a screen are easy to ignore. A voice in your ear saying &lt;em&gt;"If you're not at the top by 1:40, turn around. The view will still be there next weekend"&lt;/em&gt; is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemma 4 (E4B)&lt;/strong&gt; runs locally through Ollama's OpenAI-compatible endpoint. The service hands it the trip's facts (distance, climb, turn-around and back-by times, sunset, rain chance, trailhead) and asks for 4 to 6 short sentences "like a friend who hikes, not a robot", always including the turn-around rule. &lt;strong&gt;Kokoro&lt;/strong&gt;, an open 82M-parameter text-to-speech model, reads it aloud on the same machine. The text and audio are generated once, stored, and saved to your phone while you still have signal.&lt;/p&gt;

&lt;p&gt;Gemma is the voice, never the source of truth: every number it speaks comes from the prediction service, and if the sunset time is unknown, the prompt tells it to say so rather than invent one.&lt;/p&gt;

&lt;h3&gt;
  
  
  The app
&lt;/h3&gt;

&lt;p&gt;React + Vite, three screens: &lt;strong&gt;Plan&lt;/strong&gt;, &lt;strong&gt;On trail&lt;/strong&gt; and &lt;strong&gt;Summary&lt;/strong&gt;. Your phone reaches the laptop over the same Wi-Fi, or over &lt;a href="https://tailscale.com" rel="noopener noreferrer"&gt;Tailscale&lt;/a&gt; on mobile data, so nothing is exposed to the public internet. Weather and sunset come from Open-Meteo, with no API key and coordinates only. Alert emails go through a free Brevo account, and every email is also written to a local outbox as a record.&lt;/p&gt;

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

&lt;p&gt;Think about what a hiking plan reveals: &lt;strong&gt;where you'll be, that you'll be alone, and until when.&lt;/strong&gt; I don't think that belongs on someone else's server, and with open models it doesn't have to.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your location stays on your machine.&lt;/strong&gt; Gemma 4, TabPFN and Kokoro all run on my laptop. A closed AI API would see every trailhead I start from and every hour I'm out alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; No subscription, no per-request bill, no key that expires mid-hike. A hiking club could host it for its members on one old laptop or a Raspberry Pi-class box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The uncertainty is mine to inspect.&lt;/strong&gt; With TabPFN's weights and a public dataset, I could measure calibration on 1,438 unseen hikes myself. A closed "estimated time" API gives you a number and asks for trust. For a feature that decides when your family gets an alarming email, I wanted the evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything is swappable.&lt;/strong&gt; The checkpoint is one environment variable (v2 for speed, v3.5 for accuracy), Gemma could be any model behind an OpenAI-compatible endpoint, and Piper is a drop-in fallback for Kokoro. Nothing in the stack can be deprecated out from under it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The data is a commons.&lt;/strong&gt; The model learns from thousands of hikers who shared their tracks publicly, and gives each new hiker something back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where open was genuinely &lt;em&gt;better&lt;/em&gt; for me, not just cheaper: calibration. I needed to know that "back by" means 90%, and I could only know that because I could run the model myself on data I could see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest limits.&lt;/strong&gt; So far I've tested Turnaround at home, with sample routes and missed-check-in drills; taking it on a real trail is next. It's a prototype, not a replacement for telling someone your plan or carrying a satellite messenger in real backcountry. The laptop has to stay awake and online for the alert to send. The model knows hikr.org's mostly Alpine hikers best, and the pace factor exists precisely because you are not them.&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 predicts each hike's moving time as a calibrated distribution. Its P90 becomes the back-by time, its P95 sets when your contact is alerted, and its median feeds your personal pace factor. On 1,438 unseen hikes it beat Naismith's rule (34.8 vs 44.1 min average error) with 90.4% of hikers back before P90.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Temporal&lt;/strong&gt;: the safety timer is a durable Temporal workflow that races a check-in signal against the alert deadline, retries the alert email, and sends an all-clear on a late check-in. Tested by killing the worker and the Temporal server mid-trip, with evidence in the repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 4 runs locally via Ollama and turns each trip's numbers into a short spoken briefing (voiced by Kokoro) that's saved to your phone before you lose signal.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Thanks for reading. If you hike alone, I'd love to hear what would make you actually use something like this, and what time &lt;em&gt;you'd&lt;/em&gt; want to be told to turn around. 🥾&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>He asked for "fully secure" note sharing. Here's what I built instead.</title>
      <dc:creator>Biprayan Choudhuri</dc:creator>
      <pubDate>Sun, 04 Oct 2026 23:23:42 +0000</pubDate>
      <link>https://dev.to/code_neuron58/he-asked-for-fully-secure-note-sharing-heres-what-i-built-instead-4fdi</link>
      <guid>https://dev.to/code_neuron58/he-asked-for-fully-secure-note-sharing-heres-what-i-built-instead-4fdi</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;He asked for "fully secure" note sharing. He wouldn't stop asking, honestly. So before writing a single line of code, I had to be honest with him about what that sentence actually means.&lt;/p&gt;

&lt;p&gt;Fully secure note sharing is not possible. Not on the web, not anywhere. No website can stop someone from taking a screenshot, pointing a phone camera at the monitor, or retyping what they see. Anyone who promises otherwise is selling something.&lt;/p&gt;

&lt;p&gt;But "fully secure" being impossible doesn't mean "secure" is. So I built the most controlled version of the idea I could, and named it SecureNote.&lt;/p&gt;

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

&lt;p&gt;SecureNote is end-to-end encrypted note and file sharing, and I built it for my friend. His notes aren't text, by the way: they're PDFs, scanned documents, photos of whiteboards. So files are first-class citizens here, and they're encrypted exactly like everything else.&lt;/p&gt;

&lt;p&gt;What it actually gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notes and files (PDFs, docs, images, up to 10 MB) are encrypted in your browser with AES-256-GCM before upload. The database stores ciphertext: it cannot read your content, your titles, or even your filenames.&lt;/li&gt;
&lt;li&gt;You share with a person, not a link. Pick someone by their account, set an expiry and a view limit. The note's key gets wrapped for their public key alone.&lt;/li&gt;
&lt;li&gt;Revocation is instant. One click and their key stops working, even if they already read the note. Their open tab locks itself the next time it gets focus.&lt;/li&gt;
&lt;li&gt;Every read is on the record. Each view is watermarked with the reader's email and written to an audit log: who, when, from which IP, on what device. Even pressing the PrintScreen key is logged as an attempt.&lt;/li&gt;
&lt;li&gt;The AI never leaves your machine. Semantic search and "chat with your notes" powered by Gemma 3 (Google's open-weight model) running in the browser via WebGPU. Open the network tab while using it: nothing goes out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So what about screenshots? Like I said: I can't stop them. Nobody can. What I can do is make sure a screenshot isn't anonymous. Every page a reader opens is tiled with their identity and a timestamp. If they leak it, they leaked a page with their own name on it, at a logged time, from a logged IP. You can also cap the views or expire the share in an hour.&lt;/p&gt;

&lt;p&gt;My friend's response was something like "okay, fair."&lt;/p&gt;

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

&lt;p&gt;Live app: &lt;a href="https://securenote-j8b0.onrender.com" rel="noopener noreferrer"&gt;https://securenote-j8b0.onrender.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's hosted on Render's free tier, so the very first load can take about 50 seconds to wake up. Every load after that is instant. Sign up with two accounts: write a note as A, share it to B with a 2-view limit, open it as B, then check A's audit log. Press PrintScreen as B and watch it land there too.&lt;/p&gt;

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

&lt;p&gt;The full source is on GitHub, MIT licensed: &lt;a href="https://github.com/CodeNeuron58/SecureNote" rel="noopener noreferrer"&gt;https://github.com/CodeNeuron58/SecureNote&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The README covers the whole security model, including the threat model and the things no website can honestly promise.&lt;/p&gt;

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

&lt;p&gt;No crypto libraries, just the browser's built-in WebCrypto:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signup generates an ECDH P-256 keypair in your browser. The private key is encrypted with a key derived from your password (PBKDF2-SHA-256, 310,000 iterations) before it's ever uploaded. The server stores a bcrypt hash, a salt, your public key, and your sealed private key. It never sees your password.&lt;/li&gt;
&lt;li&gt;Every note gets a random content key. Title, body, and uploaded files are encrypted client-side; the content key is wrapped to your own public key.&lt;/li&gt;
&lt;li&gt;Sharing unwraps that key in your browser and wraps it for the reader's public key (ephemeral ECDH + HKDF). The server relays sealed blobs it cannot open, while still enforcing expiry and view limits.&lt;/li&gt;
&lt;li&gt;The AI is the part I'm proudest of. Private notes plus a cloud AI API is a contradiction, so the AI runs in the browser: MiniLM embeddings and Gemma 3 1B (open weight) via transformers.js on WebGPU, with the vector store in IndexedDB. After the one-time model download, searching and chatting make zero network calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stack: Next.js 15, TypeScript, Tailwind, MongoDB Atlas, hosted on Render.&lt;/p&gt;

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

&lt;p&gt;My friend's whole point was "my notes stay mine." Sending that data to a closed cloud AI would betray the entire premise, so an open-weight model running on-device wasn't a nice-to-have. It was the only architecture that made sense.&lt;/p&gt;

&lt;p&gt;It also cost exactly nothing to run, it works with pluggable models (swap Gemma for any ONNX model without asking anyone's permission), and he can clone the repo and host the whole thing himself if he ever stops trusting me. The open approach didn't just work better than a closed one here. It was the difference between using software and owning it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 3 1B (open weight) powers the in-browser "chat with your notes" and semantic search, running locally via WebGPU&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render&lt;/strong&gt;: the whole app is hosted on Render's free tier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of MongoDB Atlas&lt;/strong&gt;: Atlas is the data layer, deliberately storing only ciphertext, grants and audit events&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;It's built for one friend, but if you've ever needed to send something sensitive without losing control of it, it's for you too. Roast it, break it, tell me what you'd add.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>I Built Yumii — An Open-Source AI Companion</title>
      <dc:creator>Biprayan Choudhuri</dc:creator>
      <pubDate>Sun, 07 Jun 2026 14:45:43 +0000</pubDate>
      <link>https://dev.to/code_neuron58/i-built-yumii-an-open-source-ai-companion-3692</link>
      <guid>https://dev.to/code_neuron58/i-built-yumii-an-open-source-ai-companion-3692</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Yumii&lt;/strong&gt; is an open-source, locally-run AI companion with a Live2D avatar, real-time voice, and six distinct personalities — and she now actually &lt;em&gt;remembers you&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;She listens to your voice. She responds through an animated avatar. Her face reacts to what you say. And as of this sprint, she builds a long-term memory of who you are — across every conversation, every session, every restart.&lt;/p&gt;

&lt;p&gt;Beyond the core AI experience, Yumii focuses heavily on accessibility and ease of use. The project features a dedicated website, comprehensive documentation, a one-line installation command, and a streamlined CLI onboarding process that helps users get started quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/CodeNeuron58/Yumi" rel="noopener noreferrer"&gt;https://github.com/CodeNeuron58/Yumi&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2F64y3pl78k3qh8yx1g9z9.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.amazonaws.com%2Fuploads%2Farticles%2F64y3pl78k3qh8yx1g9z9.png" alt=" " width="799" height="274"&gt;&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2F02n328xahu8w5gl8a9ky.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.amazonaws.com%2Fuploads%2Farticles%2F02n328xahu8w5gl8a9ky.png" alt=" " width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before vs After — at a glance:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Forgot you on every restart&lt;/td&gt;
&lt;td&gt;Persistent memory across sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One shared conversation, no history&lt;/td&gt;
&lt;td&gt;Full session management — create, resume, rename&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No user facts&lt;/td&gt;
&lt;td&gt;Auto-extracts facts from every conversation turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No CLI beyond launch&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/memory&lt;/code&gt;, &lt;code&gt;/sessions&lt;/code&gt;, &lt;code&gt;/resume&lt;/code&gt;, &lt;code&gt;/forget&lt;/code&gt; commands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wiped on exit&lt;/td&gt;
&lt;td&gt;Survives restarts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No dedicated website&lt;/td&gt;
&lt;td&gt;Full documentation website&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic audio capture&lt;/td&gt;
&lt;td&gt;Active VAD — only speaks when you actually do&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual setup&lt;/td&gt;
&lt;td&gt;One-line installation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Comeback Story
&lt;/h2&gt;

&lt;p&gt;I saw Project Ava and a few other AI companion demos and thought — I could build that.&lt;/p&gt;

&lt;p&gt;I enjoy anime. The idea of an anime-style AI that actually talks and reacts to you genuinely excited me. So I built a scrappy version that barely held together, it worked, I was happy, and I dropped it. I mean i just build it for fun so i didn't care that much . &lt;/p&gt;

&lt;p&gt;Months later I saw the same concept being sold as a subscription product. That bothered me. I'd already built this, yeah a scrappy one , but still i built it. And i thought to myself that I could build it properly — and make it free or opensource . &lt;/p&gt;

&lt;p&gt;So I restarted. Proper CLI. Web interface. Clean architecture. VAD, STT, LLM, TTS — the full real-time voice loop. And one hard rule: it has to run on a CPU. I don't have a GPU. Most people don't. If it needs one, it's not really for everyone.&lt;/p&gt;

&lt;p&gt;That's what you're looking at now. Still alpha, still rough in places, not yet user-tested beyond myself. But it works, it's free, and it runs locally on your machine.&lt;/p&gt;

&lt;p&gt;Note on the demo: I'm skipping a video walkthrough — the Live2D model I use locally has redistribution restrictions, so I can't share footage with it publicly. Voice, memory, and everything else works fine as much as i can tell , it works without an avatar if you want to try it yourself.  &lt;/p&gt;

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

&lt;p&gt;Making her actually do things.&lt;/p&gt;

&lt;p&gt;Right now Yumii talks. What I want is for her to act — search the web, read your emails, help you shop, browse on your behalf. A real assistant, not just a companion. &lt;/p&gt;

&lt;p&gt;It would be done by giving her tool access via mcp. &lt;/p&gt;

&lt;h2&gt;
  
  
  Contribute
&lt;/h2&gt;

&lt;p&gt;Yumii is fully open source — MIT license. If you're a developer who builds with LangGraph, vector databases, voice pipelines, or just finds this kind of project interesting — contributions are genuinely welcome.&lt;/p&gt;

&lt;p&gt;I'd love to hear what other developers think of the architecture, the approach, or what they'd build on top of it. Open an issue, start a discussion, or just drop a comment here. Every perspective helps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small Note
&lt;/h2&gt;

&lt;p&gt;Yumii is still in a very early stage and actively being developed, so expect rough edges here and there.&lt;/p&gt;

&lt;p&gt;This is my first time properly sharing the project outside LinkedIn, and I’d genuinely love feedback, ideas, or contributions from fellow developers while building it &lt;/p&gt;

&lt;h2&gt;
  
  
  My Experience with GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;Copilot was running throughout this sprint mostly through tab completion — and that's where it genuinely helped.&lt;/p&gt;

&lt;p&gt;The repetitive async patterns across the session and memory modules are where it earned its place. Once it saw the structure of the first method, it completed the rest almost entirely — correct SQL, correct await placement, correct error handling. What would've taken an hour took fifteen minutes.&lt;/p&gt;

&lt;p&gt;The SQLite schema was another moment. I typed CREATE TABLE sessions ( and it suggested the full schema, including columns I hadn't thought of yet but ended up needing.&lt;/p&gt;

&lt;p&gt;It didn't make any decisions. The architecture, the three-database design, the fire-and-forget fact extraction, the async queue pipeline — all of that was thought through manually. But for translating decisions into correct boilerplate fast, it was genuinely useful.&lt;/p&gt;

&lt;p&gt;I also used Claude for cross-checking integration points — particularly verifying the LangChain tool interface when I added web search. Both tools, different jobs.&lt;/p&gt;

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