Bot the Builder: A Mac mini that works through our tickets at night
At Pale Blue, we build and maintain quite a few mobile apps. Like most small teams, we have a backlog full of small tickets that are never urgent enough, and upkeep that nobody gets around to.
So we gave a Mac mini its own Jira account. Assign a ticket to it, and overnight it tries to build it, test it on an emulator and open a pull request for us to review in the morning.
It has been running for about twelve weeks. It doesn't always work, and it has given us a few new problems. But it's now part of how we build, so here is how it works and what we've learned.
Why mobile is harder
Coding agents can write code. What's harder on mobile is knowing that the code works. A passing build tells you very little about whether a screen looks right, or whether a flow still works once you tap through it.
So we wanted the agent to do what we would do ourselves: run the app on a device, use it and show us what happened.
How a ticket moves through it
Ticket assigned to the bot Recurring tasks board
│ │
▼ │ when due
Can it be built unattended? ──no──▶ Questions │
│ yes to the team │
│◀────── answered ────────┘ │
▼ │
Overnight queue ◀──────────────────────────┘
│
▼
┌─ Orchestrator (Python) ──────────────┐
│ Jira, git, devices, locks, timeouts │
└─────┬──────────────────────▲─────────┘
│ prompt │ verdict file, evidence
▼ │
┌─ Agent engine (replaceable) ─────────┐
│ Today: Claude Code │
│ Writes code, builds, tests on device │
└─────┬────────────────────────────────┘
▼
PR with evidence ──▶ Human review ──▶ Merge
▲ │
└── review fixes ◀───┘
During the day, the bot reads new tickets and decides whether each one can be built with nobody around. If a ticket is too vague, it asks on the ticket instead of guessing.
At midnight, it works through the ready tickets one at a time, highest priority first. After the PR opens, it handles review comments and failing CI, and the ticket moves along the board with the PR. It can't merge ticket work. That part stays with us.
A deliberately boring architecture
Python handles everything mechanical: Jira, git, devices, locks and timeouts. The agent handles everything that needs judgment.
They communicate through a single file that the agent writes at the end of each run. If the file is missing, the run counts as blocked. The bot never assumes success.
That's about 11,000 lines of Python and 750 lines of prompts, running on one 8GB Mac mini.
Not tied to one AI engine
Today the engine is Claude Code, but it sits behind one replaceable piece. This space moves too quickly to build everything around a single tool, so the workflow, the device handling and the evidence rules all live outside it. The model is a setting too, and recurring tasks can each choose their own.
To be fair, we haven't switched yet, so this is still untested. Moving to another harness would mean writing a new adapter and reworking the parts of our prompts that mention Claude Code directly. That's work, but it's far from a rewrite.
The hard part was the emulators
We expected the AI to be the difficult part. In fact, our emulator code ended up more than twice the size of our Jira client.
With 8GB of memory, only one device can run at a time. A ticket covering Android and iOS does one platform, shuts the device down, then boots the other. There's no display either, so there's no window to click on.
Then there's everything that goes wrong on a machine nobody is watching. Crash dialogs, emulators that boot with no network, full disks and starved memory each ended up as a fix in the code.
The failure we didn't see coming was an agent starting a long build in the background and waiting to be notified. In a headless run, nothing ever notifies it. The run ends, the build dies and no verdict gets written. Now the agent has to wait for everything in the foreground.
Showing the work
Every visible change comes with two things: a short demo video, and one screenshot with a row for each changed screen and a column for each platform. If nothing visible changed, the tests are the evidence.
This is what makes the PRs practical to review. You can see what the bot thinks it built before you read a single line of the diff.
Memory, without a memory system
Every run starts fresh. There's no special memory system, because what the bot needs to remember lives where the rest of our work lives:
- The repo. When a run loses time on something the next run would also hit, it adds a line to the repo's agent notes, in the same PR.
- The ticket. A recurring task gets its last run's status and the ticket's recent comments, and it checks its earlier PRs before starting.
- Our skills. Our team conventions are pulled daily, so each run reads the current version.
We review the agent's memory the same way we review its code. We only added the repo notes recently, so it's too early to say how much they help. But we like that a wrong note has to get past a person first.
The work nobody files a ticket for
Nobody raises a ticket that says "run the security audit again". So this work has its own Jira board, where the columns are the state: To Do hasn't started, In Progress is active and Done is stopped.
Each task is a ticket with a small block at the top:
schedule: weekly thu
repos: all
actions: [pr, attach]
model: opus
effort: high
YAML
The brief goes below it, in plain words. actions says what the task is allowed to do, and the default is to only comment. This is mostly an instruction to the agent and not a hard sandbox, so we keep it in the same category of trust as the rest of the prompts.
We now run bug hunts, security audits, ASO keyword research, version bumps and even LinkedIn post suggestions this way. Most of them stop at a PR. Two can merge their own, and one of those takes a build all the way to production: a weekly ASO release containing the latest release plus a version bump, and nothing else.
The weekly bug hunt surprised us the most. It looks for logic errors nobody has reported, like a calculation off by a day when the clocks change, or a percentage that loses a hundredth when rounding. So far, 21 of its 22 closed PRs were merged, and 10 more are waiting for us.
One rule matters more than it looks: a run that finds nothing is a successful run. A task that invents work to justify itself quickly teaches everyone to ignore it.
The numbers so far
| Since mid-August | |
|---|---|
| Pull requests opened | 174 |
| Merged | 124 |
| Closed without merging | 26 |
| Still open | 24 |
| From recurring tasks | 69 |
It also hasn't replaced anyone. Our own PRs went from 68 in July to 175 in October, and we wouldn't credit the bot for that.
What doesn't work yet
Review is becoming the bottleneck. The bot's merged PRs took a median of 22.5 hours to merge, against 7.2 for ours, at a similar size. Opening overnight explains some of that, but not all. Writing code got cheaper, and reading it didn't.
Merged doesn't mean perfect. Some of those 124 PRs picked up commits from us before landing.
It will argue with other bots. We also run an AI code reviewer, and the two can go back and forth for a long time. So the bot stops after three rounds and asks for a person. Anything a human says resets the count.
It needs real trust. Ticket text reaches an agent running with permission checks off. So we treat the ability to assign it a ticket like the ability to run commands on that machine, and we keep that group small.
Looking ahead
None of this is magic. It's a small machine, a lot of unglamorous error handling, and a team that still reads every ticket PR.
If you run a mobile team, we'd be curious to hear whether you would run something like this, and what would stop you. We don't intend to open-source it since it's tailor-made for us, but if you are interested, please get in touch ([email protected]).
Happy coding!