DEV Community

Chestly Ace
Chestly Ace

Posted on

DuckWalk: a rubber duck that won't talk to you until you go outside

Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass Submission 🌿

This is a submission for the Hacktoberfest Open-Source AI Challenge: Week 1, Touch Grass

What I Built

I kept skipping breaks while coding, not out of willpower but because leaving the desk felt like it would cost me my mental context. So I built something that removes that cost on purpose.

DuckWalk watches for when you're stuck, writes down exactly where you are so there's nothing to lose by leaving, and pings your phone with one line. Then it makes you actually go outside before it'll say another word. Once you're walking, a Socratic duck talks you through the problem by voice, your screen off the whole time, asking questions and never handing you the fix.

A laptop daemon watches signals like time since your last break, failed test runs, how many times you've touched the same file, and undo ratio, and scores each 5-minute window with TabPFN. Cross a threshold, and it grabs a breadcrumb (your branch, diff, last terminal error) and compresses it into a 60-word note with Gemma. Your phone buzzes with that note, self-hosted so it never touches a third party's server. You tap "Walk," the screen goes black except for a stop button, and outside you talk it through while the duck asks questions back. When you're done, one screen tells you what you worked out and what to try next, readable in about ten seconds.

The rule I held myself to the whole build: minimize time looking at a screen. If a feature didn't push work onto the AI, it got cut.

Underneath, it's a durable Temporal workflow, every stage traced in Sentry, and the duck's voice is a 4B model I fine-tuned on Tinker until it matched a much larger cloud model on this specific job.

Demo

I'm writing this at 2am, and the whole point of the project is a real walk outside, so I wasn't going to fake that with a clip filmed at my desk. What I can show instead is something that doesn't need daylight: a real crash mid-conversation, and Temporal picking it back up without losing a word.

Duck: It's intermittent. What distinguishes the times it passes from the times it doesn't?
You: I've got an integration test and go that times out sometimes...
--- worker process killed here, restarted 5 seconds later ---
Duck: Sorry, I lost you for a moment. Where were we?
You: I've got an integration test and go that times out sometimes...
Duck: It's intermittent. When it only gets two events, is the third one missing...
Enter fullscreen mode Exit fullscreen mode

That's a real transcript from killing the worker process mid-walk and bringing it back up five seconds later. The other three failure drills (Ollama going down, the phone going offline, a malformed detector input) are in docs/traces/phase8-drills.md, and the actual outdoor walk-through is scripted shot by shot in demo-script.md for whenever the sun's up.

Code

GitHub logo chestlyace / DuckWalk

A local-first AI companion that notices when a developer is stuck, gets them away from the screen, and helps them think out loud while they walk. The screen is the shortest part of the experience.

DuckWalk

A local-first AI companion that notices when a developer is stuck, gets them away from the screen, and helps them think out loud while they walk. The screen is the shortest part of the experience.

Setup

Linux x86_64. Requires uv, Ollama, git, cmake, a C++ compiler and curl.

bash scripts/setup.sh
Enter fullscreen mode Exit fullscreen mode

This installs Python 3.12 and the dependencies, pulls gemma4:e4b (local default), and puts the Temporal CLI, whisper.cpp (base.en) and the Piper voice (en_US-lessac-medium) under vendor/ (gitignored). It also creates .env from .env.example. Add your SENTRY_DSN there. To use Ollama's cloud instead of a local model, set OLLAMA_MODEL=gemma4:31b-cloud (requires ollama signin and ollama pull gemma4:31b-cloud).

Then check everything:

vendor/bin/temporal server start-dev     # separate terminal; UI at http://localhost:8233
uv run python scripts/doctor.py          # pass/fail table for every service
uv run python scripts/doctor.py --sentry-test   # also sends a test error to Sentry
Enter fullscreen mode Exit fullscreen mode

The Flutter…

How I Built It

The stuck detector runs on TabPFN: six features per 5-minute window, no hyperparameter tuning, no feature engineering past picking honest signals. It's meant to be the thing that decides for you that it's time to stop, which only works if it's not annoying about it, so there's also flow protection (don't nudge during a high-edit-rate streak with nothing broken) and natural-break timing (hold the nudge for the next commit or green test instead of interrupting mid-thought).

Gemma writes the breadcrumb and the nudge. gemma4:e4b runs locally by default, and the same prompts work fine against gemma4:31b-cloud on machines that can't run it locally. The breadcrumb is really the whole trick that makes leaving the desk feel safe: the model writes down where you were so you don't have to.

The duck's own voice needed something narrower than general-purpose chat: always exactly one question, never the fix, plain and short. Gemma isn't available through Tinker, so I fine-tuned Qwen3.5-4B on filtered teacher dialogues instead. The untuned model passed a frozen 8-criteria rubric 68% of the time; after tuning, 98%. Measured against the deployed cloud Gemma duck (80%), I'd call that equal quality from a 4B model, not better. A second, independent judge put the two within noise of each other, and I wrote up that caveat along with the rest of the methodology in eval/results.md.

The whole thing runs as one Temporal workflow: breadcrumb, nudge, wait for movement, voice loop, brief, with retries on every LLM call. I needed this because a real walk can drop off wifi for ninety minutes and come back, and the workflow just waits it out. It's also what let the crash-and-resume above happen without losing the conversation.

Sentry traces every stage, tagged with model and token counts. That's how I found a real bottleneck instead of guessing at one: per-turn timings showed speech-to-text eating about 4 seconds a turn, way past the LLM call (0.76s) or the TTS (0.63s). Turned out whisper.cpp was running 5-beam search by default. Switching to greedy decoding cut it to about 2.6 seconds, 36% faster, with identical transcription text. Before-and-after numbers are in docs/traces/phase8-bottleneck.md.

The phone side is Flutter, three screens (nudge, walking, return brief), and it has to handle a walk going out of range: live-streamed to the laptop when reachable, an on-device Gemma conversation when it isn't, syncing the transcript back once it reconnects so the laptop still writes the final brief with the bigger model.

Why Does Open Innovation Matter?

Everything above runs on my own machine unless I deliberately opt out of that. Gemma through Ollama, whisper.cpp, Piper for text-to-speech, a self-hosted push server instead of someone else's, TabPFN doing inference on-device. No code, diffs, prompts, or transcripts leave the laptop. The only thing that goes out is Sentry trace metadata, and by design that's never the content of what you said, just how long each stage took.

Being able to swap the pieces is also what let me actually measure anything instead of trusting a vendor's claim. I pointed the duck's backend at Ollama, a local llama-server, and Tinker's cloud, one env var at a time, and compared them directly. The fine-tuning number above only exists because Tinker made training and serving a small open model cheap enough to run that comparison for real.

And it costs nothing to run every day, which matters for something meant to interrupt you several times a shift. A tool you hesitate to use because of a bill is a tool you turn off.

Results

The fine-tuning went from 68% to 98% on a frozen 50-prompt rubric, matching a much larger cloud model on a 4B budget. Full caveats in eval/results.md.

For reliability: a 2-hour run, 23 walk sessions back to back across 5 bug scenarios and 3 scripted conversations, zero failures and no manual intervention. My dev machine actually rebooted partway through the night, so I confirmed this straight from Temporal's own workflow history rather than a log file that didn't survive.

All four failure drills pass. Ollama going down makes the nudge and brief fall back to canned text instead of killing the walk. The worker restarting mid-conversation resumes without losing anything. The phone going offline just syncs later. A malformed detector input fails with a clear error instead of hanging.

What isn't done: the stuck detector has no real held-out precision or recall yet. Hand-labeling real work windows didn't get finished before the deadline, so I'm saying that plainly instead of papering over it with a synthetic number. ml/train_detector.py is ready to produce the real learning curve the moment there's labeled data to feed it.

Prize Categories

Best Use of Gemma. Writes the breadcrumb, the nudge, the walk-mode pick, and the return brief, and was also the teacher model behind the fine-tuning data and the eval judge.

Best Use of TabPFN. Classifies each 5-minute window as stuck or flow from six local signals with no feature engineering or tuning. I'll say the caveat up front: real held-out metrics aren't collected yet, for the reason above. The classifier, its Sentry instrumentation, and its failure handling are all real and tested regardless.

Best Use of Tinker. Took a 4B model from 68% to 98% on an 8-criteria behavioral rubric, with real before-and-after numbers and a second judge checking the first one's work, not just a vibe.

Best Use of Temporal. The crash-and-resume up top isn't a demo trick. It's the actual thing that turns a phone losing signal for an hour into a non-event instead of a risk of losing the whole walk.

Best Use of Sentry Agent Tracing. Full per-stage tracing is what turned a guess about the bottleneck into a measured 36% fix, and it's the backbone of all four failure drills.

Top comments (0)