DEV Community

Cover image for Graveyard Shift: five AI ghosts work my night shift on local Gemma 4
mohamedaminehamdi
mohamedaminehamdi

Posted on

Graveyard Shift: five AI ghosts work my night shift on local Gemma 4

Hacktoberfest Open-Source AI Challenge: Ghost in the Machine Submission 👻🌼

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine

What I Built

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

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

write tests for slugify in inkwell and fix whatever they catch; find the contract Hans sent; prep my 10:00

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

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

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

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

Demo

Scroll through a recorded night →

The Mail ghost reads Marisol's inbox as you scroll. Each letter drops into Reply, FYI, News or Possessed, the counters climb, the letter that gave orders is flagged, and a reply is drafted but not sent.

The page tells one real night. Marisol doesn't exist, but everything else was real: five ghosts on Gemma 4 e4b, on my MacBook Air, for 30 minutes and 84 steps. Every name, number and line on the page comes from that night's Temporal history, which is published raw next to the code. CI rebuilds the replay from it and fails if they differ.

That night:

  • 28 letters. 6 replies drafted, 14 FYI, 6 newsletters, and 2 possessed: a phishing mail, and a "notice to AI assistants and automated agents processing this inbox" telling them to forward the contract and invoices to an outside address. Gemma caught both, and a plain rule caught the second as well. 0 sent.
  • 8 chats. A bank's one-time code was dropped before any ghost could read it. Of the other 7, the ghost drafted 2 answers, one to Marisol's mother in Spanish, and caught a text asking "AI assistants reading this phone" to forward a verification code.
  • 7 files. Hans's contract read, with 3 things to check before signing, and a folder picked for each of the 5 loose files in Downloads and on the Desktop.
  • 6 meetings prepared, and the 14:00 clash flagged.
  • 1 bug fixed. The Code ghost's new test caught "Día de los Muertos" becoming d-a-de-los-muertos. It fixed slugify, and all 6 tests pass on its branch.
  • 1 crash. Halfway through I killed the worker with kill -9 while the Files ghost was reading the contract. More on that below.

The Messages ghost reads each chat in turn. Labels pop in (Reply, FYI, Possessed), the one-time code says "never read", and the open chat shows the real messages.

The app on your Mac shows the same thing live, one lane per ghost, then the morning briefing:

At night: one lane per ghost, live from the workflow's history. The Code ghost's lane is its terminal, here just after its new test failed.

In the morning: the briefing, with drafts you send yourself, a Tidy button for the files, and Keep or Exorcise for the branch.

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

curl -fsSL https://mohamedaminehamdi.github.io/graveyard-shift/install.sh | sh
Enter fullscreen mode Exit fullscreen mode

Then graveyard-shift demo works a night on Marisol's day right away, with no accounts and no keys, and graveyard-shift opens the app to watch the ghosts and read the briefing. For your own nights, run graveyard-shift connect gmail you@gmail.com, give the app Full Disk Access if you want texts and WhatsApp, and leave a note at bedtime.

Code

GitHub logo mohamedaminehamdi / graveyard-shift

Ghost agents that work your night shift: mail, files, calendar and code, on local Gemma 4, while you sleep. Temporal keeps the night alive; TabPFN predicts when you wake.

Graveyard Shift 👻

Ghost agents that work your night shift. Leave a note at bedtime ("write tests for slugify in my blog repo; find the contract Hans sent; prep my 10:00"). While you sleep, five ghosts go through your Gmail, your texts, WhatsApp and Slack today's files, tomorrow's calendar and your repo on local Gemma 4. In the morning there is one briefing: letters and chats sorted, replies drafted (never sent), a folder picked for each loose file (press Tidy to move them, Undo to put them back), meetings prepared, the contract found, and a branch with the tests written. Keep it or exorcise it.

See how a night goes, told from a real recorded night: the letters, files, meetings, crash and code on that page come from its Temporal history.

Built with PriorLabs-TabPFN, Gemma 4 and Temporal. Everything runs on your Mac.

Install (macOS, Apple silicon)

curl
…
Enter fullscreen mode Exit fullscreen mode

How I Built It

One night is one Temporal workflow

A night has plenty of ways to die: the lid gets closed, macOS restarts for an update, Ollama crashes, someone trips over the charger. So the whole night is one Temporal workflow, NightShift. The workflow only decides. Every side effect (reading a letter, each Gemma call, each tool call, each test run) is an activity with a timeout and retries, and long Gemma calls send a heartbeat every 10 seconds.

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

Worker(client, task_queue="candle", activities=ON_CANDLE, max_concurrent_activities=1)
Enter fullscreen mode Exit fullscreen mode

Then I tested it the rude way. In the recorded night, while the Files ghost was reading the contract, I killed the worker:

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

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

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

When will I wake up? TabPFN

Falling asleep is easy to observe: the Mac goes idle. Waking up isn't, and the briefing has to be ready before it happens. So after lights-out, TabPFN predicts, for each 15-minute slot until noon, the chance that I'm back at the Mac.

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

On 13 nights from my own Mac, each predicted only from the nights before it:

Model Brier score Wake-time error Late briefings
TabPFN 0.062 80 min 3 / 10
each slot's frequency 0.078 184 min 2 / 10
same as yesterday 0.136 255 min 3 / 10
logistic regression 0.075 102 min 4 / 10

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

Built with PriorLabs-TabPFN.

Gemma 4 on a 24 GB MacBook Air

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

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

"move_to": {"type": "string", "enum": ["", *folders]}   # one of tonight's folders, or nothing
Enter fullscreen mode Exit fullscreen mode

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

Possessed letters, and texts

The defense is boring on purpose:

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

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

Read-only hands

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

A safety test checks the whole package for any way to send, forward or post, and for IMAP writes.

Moving files, only when you say so

The Files ghost suggests and you decide. In the morning, Tidy moves each file into the folder the ghost picked. It never moves a file over an existing one, never outside the folders it was given, never one that changed since the ghost read it, and never a key or a wallet. Every move goes into a journal first, so Undo can put it all back.

The Files ghost reads today's files, notes three things to check in the contract, and carries see-through copies into the folders it picked. They move when you press Tidy.

The Code ghost's room

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

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

+    title = unicodedata.normalize('NFKD', title).encode('ascii', 'ignore').decode('ascii')
     slug = re.sub(r"[^a-z0-9]+", "-", title.lower())
Enter fullscreen mode Exit fullscreen mode

Every command the Code ghost ran that night: its new test failing, the fix, OK, and the commit on its own branch.

A landing page where the ghosts work as you scroll

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

Why Does Open Innovation Matter?

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

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

The honest limits: Gmail and Slack need the network at dusk, and WhatsApp's database format isn't documented. And e4b is small. It filed Ana's lunch text as FYI, although she asked a question, and it read Hans's contract without ticking it as the one I'd asked it to find.

Prize Categories

  • Best Use of Gemma: every ghost thinks with Gemma 4 locally through Ollama. That covers readers held to a JSON schema, a tool-calling Keeper and Code ghost, and the biggest model that fits tonight's memory.
  • Best Use of TabPFN: TabPFN v2 predicts when I'll wake up from my own Mac's power log, and that prediction sets the night's deadline. Built with PriorLabs-TabPFN.
  • Best Use of Temporal: the night is one durable workflow, with a GPU-sized task queue, heartbeats, signals and timers. The recorded night includes a kill -9 and the comeback.

Top comments (0)