The drive out, and a new kind of desktop wallpaper.
Most software teams have a mountain on their desktop. A photograph of one. Pinned glass-clear behind the IDE, behind the terminal, behind the seventeen Slack notifications. The mountain is what we look at when we look away from the work. The mountain is decoration.
This Saturday, the mountain was the work.
We left Bhukum at five in the morning. One car, packed — every builder for the day in a single vehicle, headlamps and jackets and chargers and the road west to Mulshi already cool by the time we were on it. The phones in our laps were the development machines. Not metaphor — actual phones, with actual code on them, pulled down from a fresh repo on GitHub ready to be cooked once we arrive. By sunrise we'd be at a cliff edge in Tamhini. By breakfast we'd have a working agent. That was the plan.
The thesis we'd written into the brochure was this: most agentic AI demos happen at desks with full power and unlimited bandwidth. A real harness has to do real work in worse conditions than that. Ours rarely does, because we never test it there. So we'd go to the conditions. A ghat west of Pune, first light, patchy 4G, phones running local models against pi-driven scaffolds. Build something small that actually works on a 3-billion-parameter model on a phone on battery, and you'd have learned something the data centre cannot teach you.
The conditions, it turned out, taught us a great deal. Not always the lesson we'd shown up for.
New age of development. Not a mountain picture for a desktop image — the mountain itself as the backdrop. The terminal is in your hand. The wallpaper is everything else.
Base camp · Termux, Claude Code, and the first commit from a phone.
We pulled into the parking spot near Savalya around half past six. Cool air, the sky just beginning to do its colour-shift thing, ridges still half-lit and half not. The plan was to brief at the car, walk the fifteen minutes to the viewpoint, and then start.
The actual base camp was simpler than the brochure made it sound: a flat rock, three pods of builders sitting cross-legged with phones out, and the Python scaffold opened in the Termux text editor on each device. The night before, the harness had been bootstrapped from a phone — the very first commit on the repo was authored on a phone, with Claude Code's CLI running in Termux. A small thing, technically. A large thing, philosophically.
Here's the part that's worth pausing on. The repository — small, twelve files, two languages — was both the artifact and the destination. setup.sh had been run the night before on each phone with home Wi-Fi. verify.sh had printed READY FOR SAVALYA on every device by 9 PM Friday. The Ollama binary was sitting on the phones, the llama3.2:3b model already pulled. The Python harness — journal.py, tools.py, prompts.py — was on disk in /data/data/com.termux/files/home. Everything that was supposed to be there, was there. On paper.
On paper.
Each pod ran the same three commands. Start the Ollama server in the background. Sleep three seconds so the port could bind. Then python journal.py, and watch the agent's first What would you like to do? appear on a phone screen at six-thirty in the morning, two-and-a-half hours from any internet connection that anyone owned.
For five minutes everything looked like it would work.
Then it began to not work, in three slightly different ways at once.
What was actually pulled at the trailhead.
Before we get to what broke, it's worth a paragraph on what was being run. Because the whole point of the morning was that the audience could go home, clone the same repo, and trace the same lessons through the same code on their own phones the following weekend.
The harness is small on purpose. The README says it bluntly: "the smallest thing that shows you what an agent loop looks like when you write one yourself, and it's small on purpose so you can hold the whole thing in your head while you fork it." Three Python files do all the work:
Hands & ears.
One Python function per capability. capture_voice_note, transcribe, capture_photo, get_location, call_llm, append_to_journal. Plain functions, typed signatures, a single ToolError exception for the things a hiker can recover from.
The voice.
All the prose-shaping templates live here. The system prompt that asks the LLM to clean up a rough voice note. The summary template. The follow-up question template. Pods who wanted to change tone never had to open the agent loop.
The brain.
A planner picks the next step. An executor runs it. A few composites stitch together capabilities into things a human would call a "command": record a voice entry, capture a photo with a caption, write a morning summary, exit cleanly. About a hundred and fifty lines.
You'll notice what's missing. There's no LangChain. There's no LangGraph. There's no asyncio. There are no dataclasses, no abstract base classes, no Protocols, no Pydantic models. There are no tests. The deliberate absence of all this was the pedagogical point. You can write an agent loop in a hundred and fifty lines of plain Python, and you should know what it looks like at that size before you reach for a framework that abstracts the loop away from you.
Every tool, one shape.
Each tool function in tools.py follows the same shape: a typed signature, a single docstring describing what the tool does and what it returns, explicit subprocess invocations with capture_output=True and check=True, and a single ToolError raised whenever something went wrong in a way the human user could fix without restarting the agent. The error messages were written for hikers, not engineers. "microphone produced no audio — is Termux:API granted Mic?" is a real string in the codebase. That string did real work in the field on Saturday.
The agent loop catches ToolError, prints the message, and asks the user what to do next. No retries. No exponential backoff. No telemetry. The harness trusts the human to make the call.
One detail in tools.py deserves its own paragraph because it tells you something about the building style. The get_location function tries the network provider first, then falls back to GPS. The comment above it just says "network provider first (faster), then GPS as fallback". No fanfare. But this single decision — try the cheap signal-based fix before reaching for the expensive satellite one — turned out to be exactly the kind of pattern the whole morning was about. The harness was a little parable about graceful degradation, and we were about to live inside that parable for three hours.
Point 2 · Andharban valley and the first real run.
After the first pass at Savalya, where the agents technically launched but voice transcription crashed for two of the three pods, we walked deeper. The morning was lifting fast. Andharban valley opens out beyond Savalya in a way that the photographs do not prepare you for. The valley falls away in layers, the far ridges still in cloud, the near ones already in early sun. Even the most heads-down builder looked up.
Pod 2 found a flat rock with enough shade to read a screen. Pod 1, more ambitious, perched closer to the edge. Pod 3 stayed slightly back, near the trailhead, partly for safety and partly because their voice transcription had been the most stable at the first attempt and they wanted to extend it rather than start over.
The second run is when the harness started to do something. A photograph was captured by Pod 2 — the agent's capture_photo tool calling termux-camera-photo as a subprocess, the file landing in ~/storage/shared/savalya-photos/<timestamp>.jpg on the phone's shared storage. A short voice caption followed. The LLM took ten seconds to respond — a real, measurable, you-could-watch-the-cursor-blink ten seconds — and then it stitched the caption and the photo metadata into a paragraph that, when read aloud, sounded like it might have been written by a competent travel writer.
It was a real working entry. Photo, GPS, prose, all of it written to savalya-journal.md on disk, all of it generated by a model that had never seen an internet connection during the run. The pod stared at the screen for about ten seconds longer than strictly necessary. There was a quiet bit of clapping. The sort of clapping you do at a viewpoint, on a Saturday morning, when you don't want to scare the birds.
And then, naturally, three different things broke.
When the wilderness still had bars.
This is the part the brochure didn't predict and the marketing copy can't easily absorb, and is therefore the part most worth saying out loud.
We cheated. Twice. Sometimes more.
When a pod's local Ollama instance refused to load the model because the phone had run out of RAM, and a fresh restart looked like a ten-minute fix, the relevant builder did what builders do. They paused the offline run, opened Codex CLI on the side, and verified what the agent loop should have produced. Then resumed the offline run.
When a pod's voice transcription failed because whisper-cli couldn't find a model file and the path in tools.py had been hand-edited an hour earlier, the relevant builder did what builders do. They posted a screenshot to the WhatsApp group, the group debugged together, and yes — at one point, the screenshot resolved because Jio's signal had reached even Andharban. The Indian telecom industry, undefeated.
The point isn't to apologise for these moments. The point is that the boundary between "offline AI development" and "AI-assisted offline AI development" is the most interesting boundary in the whole morning, and it would be dishonest to pretend the boundary didn't move around. A pod with a cloud lifeline in their pocket is doing different work than a pod without one. We were doing both, sometimes within the same minute. That's the truth.
This was the moment one builder said the line that the rest of the group quoted at each other for the rest of the day. "In the wilderness, we are still on Jio." The wilderness was a relative term. Anyone who has hiked in the Sahyadris knows that connectivity in May, at altitude, in line of sight of any village — is unreliable but rarely zero. The phones found bars in places they had no business finding bars. The fallback to Codex when the local stack stumbled wasn't cheating per se; it was an honest read on what would actually happen in production. Real users don't sit at desks waiting for their offline-only agent to start working. Real users fall back to whatever's available.
Which is, on reflection, exactly what the agent harness should be designed for. Graceful degradation in both directions. The local LLM if it works. The remote LLM if you have signal and it's faster. The disk-based journal append either way, because nothing important is lost when either path fails.
This wasn't in the original Saturday plan, but it became the most useful conversation of the morning.
Point 3 · the agent asks its first question.
Half an hour after the confession, we'd moved again. The third stop was a smaller clearing, less photogenic, more practical — enough flat ground for three pods to sit, enough breeze that the phones didn't overheat from the LLM doing its quiet work, and enough quiet that we could hear each other when something worked and when something didn't.
By this point, something interesting was happening that nobody had planned. The pods had stopped working in isolation. A bug Pod 3 had hit twenty minutes earlier was now hitting Pod 1, and Pod 3 had a fix. Pod 2 had figured out a workaround for the camera not firing on a particular OEM ROM, and they were dictating it to Pod 1's lead-builder while another pod-mate took notes in the journal. The agent harness had become a shared problem. Three pods, six phones, one repo, one slowly-stabilising story about what works in Termux at 8:30 in the morning at 700 metres of altitude.
And then, the moment we'd come for. The agent asked a question.
One pod had implemented an extension to the basic scaffold — taken from the README's Pod ideas section, the second one — that had the LLM produce a single short follow-up question after each voice entry. The hiker could answer it or skip. The follow-up question was generated by llama3.2:3b, running on a phone, with the airplane-mode toggle on for verification. The transcript came in. The LLM thought for eight seconds. And then the screen displayed:
You mentioned the wind was steady from the west. Do you think it'll hold through the morning, or shift as the sun climbs?
It was not a profound question. It was, in a strict information-theoretic sense, a derivative question — a reasonable next thing to ask given the previous note. The point isn't that the small model produced a brilliant question. The point is that a small model, running on a phone, with no internet, asked a specific question that referenced a specific detail from the previous voice entry. Which is, when you stop and consider it, exactly what an agent is supposed to do.
The builder looked at it. Read it aloud to the pod. Said "yeah, I think it'll shift around nine". The agent dutifully transcribed the answer, called the LLM one more time to merge the answer into the original journal entry, and appended the merged paragraph to savalya-journal.md. A real conversation, in the field, with no signal, between a human and a small model running on a phone.
That was the moment the morning earned its title.
Voice failed. Photo failed. The permissions we missed.
Now for the part of every field report that gets written most reluctantly, because it isn't flattering, but is also where the most useful information for the next pod lives.
The first thing that failed was whisper.cpp. Not for one pod, for two. The voice notes were being recorded — Termux's termux-microphone-record was firing and the WAV files were on disk. But the transcription step, the part where whisper-cli was supposed to take the WAV and return text, was raising ToolError("whisper-cli missing at ...") on one phone and producing an empty string on the other.
The first failure mode turned out to be an install-script regression. The phone in question had run setup.sh the previous week, before a small fix had landed in the script, and the whisper binary had built into a path that the tools.py constants no longer matched. WHISPER_BIN = HOME / "whisper.cpp" / "build" / "bin" / "whisper-cli" in the current repo. The old build had put the binary one directory shallower. Five minutes to fix; the kind of regression that only manifests if you set up early and never re-pull.
The second failure mode was harder. The transcription ran, returned exit code zero, produced empty output. whisper.cpp with the tiny English model is small but not infallible. It turned out the voice notes were being recorded at a sample rate that the model handled, but with the phone's microphone gain set very low — by the OS, not by Termux — to suppress background hiss. The result was a recording that contained the speaker's voice but at an amplitude below whisper's silence threshold. The model heard nothing because, from its perspective, nothing was said. The model's failure mode was honesty.
The fix was a one-line addition: pipe the recording through ffmpeg with a normalisation filter before handing it to whisper. The pod that hit this didn't ship the fix during the session — they noted it for the post-event PR. The other pods, who'd been speaking louder by accident, never encountered the bug.
The second failure was the camera. termux-camera-photo wasn't installed on one pod's device. Specifically, the Termux:API APK was installed (the F-Droid one, from the night before's instructions) but the Termux:API package hadn't been installed inside Termux — the binary that exposes the camera command to the shell. A separate pkg install termux-api would have fixed it. The Friday instructions had said to do both. One pod, two devices, both forgot the second.
The third failure was permissions. Even on the phones where everything was installed, Android's runtime permission model meant that Termux had to be granted Camera, Microphone, Storage, and Location via the system settings panel, not via Termux itself. The Friday email had described this. Three of the six phones in the room had completed it. Three had not.
So at the end of the third build window, the score looked something like:
This is, on reflection, an extraordinary outcome for a half-day in the field. But in the moment it felt like the morning was going sideways. The thing that saved it was that we still had thirty minutes before we had to head back, and one of the pods had become unofficially expert in Termux:API gotchas by way of having had every single one of them.
The half-hour fix · Termux:API from F-Droid, on the rocks.
The last half-hour was a small, focused fix-it session that did more to teach the morning's lessons than the previous two hours combined.
Two builders worked on the permissions and APK installs across the three affected phones. One walked through the system Settings → Apps → Termux → Permissions flow with each device. A third sat with the pod whose whisper-cli path was wrong and patched the constants in tools.py by hand, in the Termux nano editor, on a phone screen, at a viewpoint, on a Saturday morning in May. The mountain was the desktop. The mountain wallpaper, but you could touch it.
By 10:00 — give or take — every pod had at least one fully working capability beyond what they'd come with. One pod had voice notes flowing end-to-end, the audio file landing on disk, whisper transcribing it, the LLM cleaning it up, the journal recording it. One pod had photos with captions stitched into journal entries. One pod had implemented the multi-hiker tagging feature from the README's Pod ideas section, the one that asks each note-author for a single-letter initial and produces a per-person sub-summary at the end of the morning.
The demo round was brief. Three pods, three phones held up, three journal files opened in Markor or the Files app or the Termux cat command, depending on the pod's aesthetic preferences. The journals were short. Each had four or five entries. Each entry had a timestamp, a GPS coordinate, a paragraph of cleaned-up prose, and at least one photo or one voice note pinned by file path. The journals were unmistakably written by an agent — slightly too polished in places, slightly over-fond of the phrase "the morning light was" — but they were also unmistakably built on real observation. Real wind. Real ridges. Real cliff edges that we'd just been near.
Then we walked back to the car. Drove back to Pune. Home by half past twelve, almost on schedule. Saturday afternoon was free, just as the brochure had promised. Most of the WhatsApp group was exchanging gists by lunchtime.
What the harness taught us.
Six things to take home from this morning. Six things that won't fit in a LinkedIn caption but will reward an extra five minutes of reading.
1. The harness is the product, not the model. This was the thesis from Session 01 and it survived Session 02 with renewed force. The local LLM did its job competently. The model wasn't what failed. What failed — and where the morning's energy went — was the layer around the model. Permissions. Paths. APIs. Audio levels. None of that is glamorous; all of it determines whether the agent actually works. The model is rented intelligence. The harness is the part you own.
2. Offline-first is a posture, not a switch. The honest version of "offline AI" is not a binary. It is a stack of fallbacks that degrade gracefully. The same agent that runs against llama3.2:3b on a phone should, if you ask it nicely, run against a Codex CLI session when you happen to have bars. The same agent should write to a local journal file regardless. The cloud is not the enemy; the cloud's absence is the test. Your harness either handles the test or it doesn't.
3. Small models are humble. They tell you when they don't know. Whisper-tiny returned empty strings when the audio was too quiet. llama3.2:3b produced a follow-up question that referenced a specific detail rather than wandering into hallucination. These are not properties of small models in general — but the ones we used had been trained well enough that their failures were honest failures. The mistake would have been to disguise the honesty by wrapping the small model in a confident-sounding chat interface. Our agent didn't disguise anything. The agent's voice sounded like a small model, because it was one.
4. The pod is the unit, not the individual. The morning's biggest accidental discovery was that pods of three or four debugged better than any individual builder. The bug that had stumped one pod for fifteen minutes got fixed in three when explained to another pod. By the end of the morning we had effectively rotated knowledge through the room, and three different builders had at some point been "the person who knew about the Termux:API APK gotcha." This is how engineering communities actually work. The session format accidentally reproduced it.
5. The new desktop wallpaper. This needs no exegesis but deserves its own line. The mountain on your wallpaper is decoration. The mountain at your back is data. What you build in the wild is informed by what's around you in a way that you cannot reproduce by setting a 4K image as your background. The view goes into the journal entries, not just behind them. The next time we sit at a desk and notice the wallpaper, we'll know what we're missing.
6. The repo is the take-home. Three pods went home. One repo got better. The README was updated with the install gotchas we'd hit. The setup.sh got a sample-rate normalisation patch. The TROUBLESHOOTING.md grew three real entries that came from real Saturday-morning pain. Anyone reading this now can clone the repo, run it on their own phone, and reproduce most of this morning's session at their own pace. The mountain is optional. The harness is not.
An agent without a journal is a conversation you forgot.
An agent without a harness is a model you rented.
An agent in the wild is the only one whose limits you actually know.
The application, running.
This is the part of the field report that, if we were doing it properly, would not need to be written at all. The repo is on GitHub. The setup script is reproducible. Anyone with an Android phone and an evening can have the same agent running on their own device by Sunday. Words about the morning are no substitute for the artifact the morning produced.
Still — for the record, and for the reader who skimmed straight to the end — here is what the agent looked like in the field on Saturday, and here is a short clip of it doing its actual job. Local LLM. No connectivity. Termux on a phone screen. A journal entry being written from a voice note, in front of the camera, in real time.
If the video doesn't say everything that needed saying about the morning, the absence of a loading spinner during the LLM call probably does. The agent is not waiting for the cloud. The cloud is not part of this conversation.