DEV Community

Cover image for My hours now write themselves every 15 minutes, from a 4B model that never leaves my Mac
Prateek Rai
Prateek Rai

Posted on

My hours now write themselves every 15 minutes, from a 4B model that never leaves my Mac

Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady Submission 🦥

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady

What I Built

A munshi is the clerk who keeps the register: spectacles, a cap, a ledger, and a quiet record of what happened today. Munshi is that clerk as a small buddy on my Mac's desktop.

At the end of every week I couldn't say where my hours went. My timesheet was guesswork, and I didn't want a cloud app watching my screen to fix that.

It saves three chores I am bad at: writing down what I did, rebuilding yesterday from memory, and guessing where my hours went. Anyone who keeps a log, bills by the hour, studies, writes or ships side projects gets to be lazier. So does anyone who sits down after lunch and wonders what they were doing before it.

The deal is that I do nothing. Every 10 seconds Munshi notes the front app and its window title. Every 5 minutes it reads the git branch, commit subjects and changed file names, only in folders I listed. Every 15 minutes Gemma 4 e4b, running in Ollama on the same Mac, turns each finished slot into blocks of work: a project, a topic and a short summary. When I click the munshi, one card answers four questions: where was I, what did I do today, what did I do yesterday, and how many hours went where. Copy for timesheet puts lines like tidepool · 2.5 h · tide-chart caching, CI fix on the clipboard. When a day ends, it writes a Markdown journal page and an hours CSV.

No screenshots, no keystrokes, no clipboard reading, no browser history, no file contents. Nothing leaves my Mac.

Demo

The munshi sits on the desktop at its real size, the munshi, and one click opens the card beside it:

The card window: where was I, today's blocks and the actions

The card, on the made-up demo data (the projects tidepool and lantern do not exist):

The Munshi card: where was I, today, yesterday and hours

Paused, with yesterday's hours:

The card while paused, showing yesterday's hours

Code

GitHub logo prateek11rai / munshi

A desktop buddy for your Mac that keeps your daily register with a local open-weight model (Gemma 4 via Ollama). No screenshots, no keystrokes, nothing leaves your Mac.

Munshi: your daily register, kept by a 4B model on your Mac

Munshi

Your daily register, kept by a 4B model on your Mac.

A small munshi (a clerk with spectacles, a cap and a ledger) sits on your Mac's desktop and keeps your register for you. It notes which app and window are in front and, for folders you choose, the git branch, commit subjects and changed file names. Every 15 minutes a local open-weight model (Gemma 4 e4b through Ollama) turns that into plain lines, and code checks every one. Click the munshi and one card tells you where you were, what you did today and yesterday, and your hours per project, ready to paste wherever you need them. Personal projects, study, writing, freelancing, open source: you never keep a log by hand or rebuild yesterday from memory.

Demo

The munshi sits on your desktop (actual size: the munshi). Click it and the card opens beside it.

The card window on a Mac: where was I, today's blocks and the actions

All…

The demo records nothing, so you can try it in a minute:

ollama pull gemma4:e4b
git clone https://github.com/prateek11rai/munshi && cd munshi
uv sync
uv run munshi demo load
uv run munshi start --demo
Enter fullscreen mode Exit fullscreen mode

How I Built It

It is one Python 3.13 project managed with uv: pywebview for the two windows (the 88-pixel buddy and the card), PyObjC for the macOS calls, SQLite for the store, and Ollama with gemma4:e4b for the words. Dependencies are pinned exactly, and uv's exclude-newer keeps new releases out until they have aged.

How Munshi works: collector, events, writer with Gemma 4 and code checks, blocks, notes, card, journal and hours, all inside the Mac

The collector reads the front app from NSWorkspace and the focused window's title through the Accessibility API. Three minutes without input starts an "away" stretch. Two details needed care. macOS only refreshes the front app while the main run loop runs, and pywebview owns the main thread, so the read is posted to the main thread and falls back to the window server's list when that is busy. And a terminal titled just "tmux" tells you nothing, so Munshi asks tmux for the active pane's command and repo name (never the path), only inside the folders I listed.

The writer takes each finished 15-minute slot, merges repeats, drops away time and gives every item a stable id (e1, e2, ...). It sends them to Gemma with the list of known projects and existing topics, asks for JSON under a schema at temperature 0, and gets back blocks of evidence, project, topic and summary.

Then code checks every block. The evidence ids must exist in that slot. The project must be one I have, and when code already detected the project from an editor title, a terminal or a git event, code wins. The topic is at most four words, must name a subject rather than an app, and must share a word with the evidence or the summary. The summary is at most 14 words and must use words that appear in the evidence. Digits are stripped from anything the model writes.

One writer run: compact the slot, prompt Gemma, check each block, accept or fall back, then the where-note

A block that fails, a model error or a timeout becomes a plain code-made line such as Preview · tide-tables-2026.pdf, so the timeline is never empty. Start, end and minutes always come from the evidence. Every number on the card comes from code: durations, hours, counts, "updated 6 min ago". The model only names things.

The privacy rules are code too, and they run before anything is written to disk:

Where private things are dropped: pause, own window, ignored apps, title words, process words, private windows, redaction, allowed roots, local-only model

I run it on the Mac I use for everything, so those gates are not theory. One chat app is on my ignore list. A word I never want in my register is a title word, so any window whose title contains it is never recorded, and repos, branches, commits or file names containing it are dropped. One of my two browser profiles is kept out by a process word: Munshi checks the browser's command line and, for Firefox, the profile folder it has open, so that profile stays out while the other one is recorded.

The last piece was measuring without looking. munshi stats and munshi eval --real print numbers only: blocks accepted, rejected by which check, fallback rate, seconds per slot, memory. The eval reruns a real day in a scratch copy of the store. The rule for the agent tuning Munshi on my real days was the same: numbers only, never a window title.

The tables are small and plain; the data model diagram shows how blocks cite events and notes cite blocks.

Why Does Open Innovation Matter?

A log of every window you open is the most private data on your computer. It holds who you talk to, what you read, what you are worried about and what you are paid to do.

Rewind, the Mac app that remembered everything, stopped capturing on 19 December 2025 after Meta bought its maker. The record it kept depended on a company, and the company changed hands. That is the risk whenever this kind of record lives on someone else's server or inside someone else's business.

With an open model on my own Mac, the record and the model both stay mine. I can read every line that decides what is recorded, and I can add the rules my situation needs, like a title word for something I never want recorded. No company can change the terms later.

I can also swap the model. Any Ollama model works: MUNSHI_MODEL=<name>, then munshi eval --demo --model <name> to compare. The checks are the same for every model, so a weaker model means more code-made lines, never wrong numbers.

My Agent Session

Munshi was built in two halves.

Session A is the base: collector, writer, card, journal, tests and docs, built in one evening with made-up demo data and fakes only. Real collection never ran in that session.

Munshi, session A: building a desktop buddy that keeps a local work journal (work laptop, demo data only)
You

Read ~/github/prateek11rai/munshi/docs/design/build-brief.md and do everything in it, in order. If anything is unclear, ask in this window and keep going on the parts that do not depend on the answer.

Agent
Agent
Agent
Agent
Agent
Agent
Agent

Other Step 0 checks pass (noreply email, leak-check clean, uv 0.12.3, Python 3.13.12, gemma4:e4b present). Looking at what the leak check enforces, then starting the skeleton.

Agent
Agent

Setup is fine apart from the DevRelay item above. Starting step 1, the skeleton: pyproject, uv config and a resolve to find the exact versions to pin.

Agent
Agent

Resolved within the 7-day cooldown: pydantic 2.14.1, pywebview 6.2.1, pyobjc 12.2.2, pytest 9.1.1, ruff 0.17.1. Pinning them exactly and syncing.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

First commit is in (66d7511). Moving to step 2: config, the SQLite store, and redaction, with their tests.

Agent
Agent
Agent
Agent
Agent
Agent
Agent

Step 2 done: config, store and redaction pass 31 tests. Committing, then the collector (step 3).

Agent
Agent

Now the collector: the loop with an injectable clock and source, the real macOS/git/Spotlight scanners (tested only with mocks), and a scripted fake.

Agent
Agent
Agent
Agent
Agent
Agent
Agent

Git and file scanners written. Now the macOS window/idle source with the main-thread fix for frontmostApplication.

Agent
Agent
Agent
Agent

11 of 12 collector tests pass; the last one needs the notes module from step 4. Now mock-only tests for the git, Spotlight and macOS sources.

Agent
Agent
Agent

Collector and source tests pass (30, with the redaction tweak). Before writing the model client, a quick check of how the local gemma4:e4b handles a JSON-schema request (localhost only).

Agent
Agent

The model answers valid schema-shaped JSON: about 5 s per slot warm, plus ~17 s to load the first time. Writing the writer: slots, model client, block checks, notes.

Agent
Agent
Agent
Agent
Agent

Writing notes (the "where" and "day" notes, the code-made facts line, and the fallbacks).

Agent
Agent
Agent

Hours and journal done. Now the writer run itself, which ties slots, model, checks, fallbacks, notes and day close together.

Agent
Agent
Agent
Agent

Block checks pass (13 tests, including the bad-id, invented-project, long-summary, digits and prompt-injection cases). Writing the demo data next, since the writer, journal and UI tests build on it.

Agent
Agent
Agent

The demo's yesterday journal matches the spec format exactly. Fixing two lint errors, then tests for the writer, demo, hours and journal.

Agent
Agent
Agent
Agent
Agent
Agent
Agent

Writer tests are close. Fixing topic casing (keep the model's casing, match case-insensitively) and two test fixtures.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

Card service written. Now the static UI files (theme, SVG, buddy, card HTML/JS); everything is rendered with textContent, since window titles are untrusted.

Agent
Agent
Agent

Now the asset inliner, the preview server ([REDACTED] only, with Host/Origin checks so other sites can't call it), and the pywebview app.

Agent
Agent
Agent

Now the CLI with every command from the spec. --demo is also accepted on pause/resume/forget so nothing on this laptop touches ~/.munshi.

Agent
Agent
Agent

Lint is clean and ~/.munshi doesn't exist. Running the demo commands for real now (demo home only).

Agent
Agent

Demo, doctor, hours and journal all behave. Now write --demo against the real local model (3 unwritten demo slots).

Agent
Agent
Agent
Agent

Back to green (88 passed). Writing tests for the service, preview server (including the cross-site protections), CLI, Ollama client, UI placement, and the one real-model test.

Agent
Agent

108 pass, including the real-model test. One genuine bug: doctor ignored MUNSHI_OLLAMA_URL/MUNSHI_MODEL when no config file exists. Fixing that, plus allowing S310 (urlopen) in tests only.

Agent
Agent

Unit suite green. Now the real app check: start --demo --self-test opens the windows, calls the JS bridge from inside the card, prints the result and quits. No screenshots.

Agent
Agent
Agent
Agent

All seven app checks pass against the real windows. The click/drag split from a real mouse can't be driven from here, so it goes on the owner's hand-check list. Committing the UI, CLI and tests.

Agent
Agent

Committed b41f060. Next, acceptance item 3: munshi preview --demo driven with Playwright. Reloading fresh demo data and starting the preview server in the background.

Agent
Agent
Agent
Agent
Agent
Agent

Only a missing favicon (I'll point it at the SVG). Reading the page structure:

Agent
Agent

All four parts render with the demo data. Saving card.png (the Playwright page only).

Agent
Agent
Agent
Agent

The card looks right. Two small preview fixes first: the favicon, and a note when Close is pressed in the browser (it's a no-op there).

Agent
Agent
Agent

Favicon error gone. Clicking through the controls: Hours → Yesterday, then reload to check it sticks.

Agent
Agent
Agent
Agent

The Hours tab survives a reload (stored in the demo store). Now Copy for timesheet:

Agent
Agent
Agent
Agent
Agent
Agent

The note clears after 3 s, so I'll click once more and read the note immediately (this writes the made-up demo timesheet to the clipboard again).

Agent
Agent

Copy shows "Copied". Next: Open journal and Close (both should show their preview notes).

Agent
Agent
Agent

Now Pause → 30 min:

Agent
Agent
Agent
Agent
Agent
Agent

Pause survives a reload ("Paused until 16:38" on the demo clock, Resume shown). Saving card-paused.png:

Agent
Agent
Agent

Store confirms both. Now Resume:

Agent
Agent
Agent
Agent

Resume persists. Now Refresh now, which runs the real model over the 3 unwritten demo slots (~30 s):

Agent
Agent

Refresh works: 6 blocks written, and the timeline now ends at 15:40–16:00. But the where-note fell back to the code version, so the model's note was rejected. Checking why:

Agent
Agent
Agent

The where-note now passes. Last Playwright control: Forget last hour (two clicks to confirm), then a reload.

Agent
Agent
Agent
Agent
Agent

Every control works and survives a reload. Closing the browser, stopping the preview server, and checking the paused screenshot.

Agent
Agent
Agent
Agent

Both screenshots look right. Committing the preview fixes, the topic check, the prompt tweaks and the screenshots (full suite including the model test).

Agent
Agent
Agent

The source change landed, but my test edit didn't match because ruff had reformatted those lines. Editing the tests directly:

Agent
Agent
Agent
Agent

Committed d693f82. Step 8: running munshi eval --demo over all demo slots with gemma4:e4b in the background (several minutes). Meanwhile I'll write CI and docs.

Agent
Agent
Agent
Agent
Agent
Agent

Eval is about halfway (~7 s per slot). Doing the file-size refactor meanwhile: eval logic goes to evals.py, the record dataclasses to records.py (re-exported from store).

Agent
Agent
Agent
Agent
Agent
Agent
Agent

A careless find-and-replace in the split renamed add_event to addevent_from and similar. Fixing those names:

Agent
Agent
Agent

Tests are back to 110 passing. Eval is at 51/53; I'll wait for it to finish before measuring the app's memory and CPU so the two don't overlap.

Agent
Agent

I'll be notified when eval completes. Drafting docs/BUILD_LOG.md meanwhile; eval and memory numbers go in once measured.

Agent
Agent
Agent
Agent
Agent

While that samples, I'll estimate the collector's own footprint: the real loop with the pyobjc frameworks imported but only the scripted fake source (no macOS calls), polling 30 times.

Agent
Agent

Collector estimate: about 0.6 ms of CPU per poll and 67 MB peak RSS with the AppKit, Quartz and ApplicationServices bindings loaded. Waiting for the app measurement to finish.

Agent
Agent

I'll get a notification when the measurement ends. Meanwhile, filling in the eval numbers in the build log.

Agent
Agent
Agent
Agent
Agent

App process: 61 MB physical footprint, 0.0% CPU when idle. Writing that into the build log, then committing the refactor, CI and docs.

Agent
Agent

Committed dcafef7. Final acceptance pass: a fresh clone in the scratchpad with uv sync --frozen and the full checks, including the model test.

Agent
Agent

Fresh clone: frozen sync works, lint and format clean, 111 tests pass (model test included). Re-running the window self-test and the demo commands on the final code, plus the repo hygiene checks.

Agent
Agent
Agent
Agent
Agent
Agent

Vault notes saved. Gathering the commit list for the report.

Agent
Agent

The Munshi base is built: everything in the spec, checked against the brief's acceptance list. 111 tests pass, including the one that calls the real local gemma4:e4b, and a fresh clone with uv sync --frozen passes too. The leak check is clean, history has only the noreply address, git status is clean, and ~/.munshi was never created.

Still open from Step 0: BLOCKED-setup stands. ~/.claude/skills and ~/.agents/skills both hold the nine devrelay-* skills, created at 22:17, after the repo-scoped copies at 22:08. I didn't touch them and never called DevRelay. The build doesn't depend on them, so I carried on as your prompt allowed.

Commits
- 66d7511 Skeleton: uv project, doctor stub, design docs
- e01f27b Config, SQLite store and redaction
- 96b40eb Collector, writer, notes, hours, journal and demo data. This one went in with one failing test because my command chain didn't stop on the pytest failure. I fixed it in the next commit rather than rewriting history, and every later commit was gated on pytest's exit code.
- b8d19b1 Fix the day-note fallback test's expected bullets
- b41f060 Buddy and card windows, preview server, CLI commands
- d693f82 Card screenshots, grounded topics, clearer where-note prompt
- dcafef7 Eval module, CI workflow, README, AGENTS.md and build log

Files by area
- src/munshi: cli, config, store, records, redact, control (pause/forget), timefmt, demo, evals, doctor.
- collect/: the loop, macOS source, git and Spotlight scanners, scripted fakes.
- write/: slots, model client, blocks, notes, hours, journal, writer.
- ui/: service, preview server, pywebview app, assets, plus the five static files.
- tests/: 15 files. Also the CI workflow, README, AGENTS.md, the build log and two screenshots.

Checks
- Real windows: munshi start --demo --self-test drives the windows through the JS bridge without screenshots. All seven checks pass: card data loads, a click opens the card filled, Esc hides it, a move saves the position, a click right after a drag is ignored, and there's no Dock icon.
- Preview in Playwright: every part of the card and every button worked and survived a reload, with the store showing each change. The ✓ table is in docs/BUILD_LOG.md; the screenshots are docs/img/card.png and card-paused.png.
- Eval on all 53 demo slots with gemma4:e4b:

| Result | Value |
|---|---|
| Model blocks accepted | 64 |
| Rejected | 0 |
| Fallback blocks | 9 (window time the model left out of every block, which code then labelled) |
| Seconds per slot | 3.8 mean, 8.1 max (plus about 17 s to load the model cold) |

  • Memory and CPU in demo mode: the app process uses 61 MB and 0% CPU when idle. The collector, estimated with a fake source, costs about 0.6 ms of CPU per 10-second poll.

Decisions I made alone (all written up in the build log)
- Freshness gotcha: the collector reads the front app on the main thread and falls back to the window server's on-screen window list if the main thread is busy. A test with a fake proves the choice.
- No pywebview server: the UI files are inlined into the windows, because pywebview's own local server has an any-origin API endpoint. The preview server listens on [REDACTED] only and refuses other hosts, other origins and non-JSON posts.
- Demo clock: the demo always opens at 16:05 on its day. Its last three slots are left unwritten, so Refresh has real work for the model.
- One check beyond the spec: a reused topic must share a word with the evidence or summary. The model had filed a PDF under "docs navigation".
- Forget: it cuts a long event at the one-hour mark instead of deleting it whole.
- Extra flags: pause, resume and forget take --demo, eval takes --slots, and start has a hidden --self-test.

Flagged for review
- .github/workflows/test.yml is security-relevant. Actions are pinned by tag, not commit SHA, because I couldn't look SHAs up with network limited to PyPI and Ollama.
- The new preview server, and subprocess calls to git, mdfind and open (list arguments, no shell).
- During the Copy check, the made-up demo timesheet text went onto this laptop's clipboard twice.
- I also wrongly tried to read the clipboard in the browser. It hung on a permission prompt and I stopped it, so nothing was read.
- Per your global rules I added two notes to the vault's knowledge/ folder: one on the front-app freshness fix, one on pywebview's local server.

Untested on real macOS (full list in the build log)
- The window, Accessibility and idle reads against real apps.
- Git and Spotlight scans on real folders.
- Long runs with the app open, and a day closing at midnight.
- A real mouse click and drag, the transparent buddy on a real desktop, clipboard and Finder from the app.
- Which binary needs the Accessibility grant.

The personal-laptop agent should first run uv sync, munshi doctor and start --demo --self-test. Then it should allow one throwaway folder with a test repo, set apps.mode = "only" with two apps, run munshi start for ten minutes, and compare the recorded events with what happened.

Open questions (my recommendation in each)
1. Pin the CI actions to commit SHAs before the repo goes public. Recommended: yes.
2. The CI model job pulls a multi-GB model on every run. Recommended: keep it, but limit it to changes under src/munshi/write/ or run it on a schedule if CI minutes matter.
3. The global DevRelay skills above. Recommended: the planner removes the home-folder copies.

DONE-munshi-base

You

Read ~/github/prateek11rai/munshi/docs/design/review-1.md and do everything in it, in order. If anything is unclear, ask in this window and keep going on the parts that do not depend on the answer.

Agent
Agent

Four tasks. Reading the planner's handover first, since I need to check it against what I built.

Agent
Agent

The handover is mostly consistent with the build, but I already see one wrong claim. Checking which tracked files its proposed leak-check rules would trip on (/Users/ and non-noreply emails):

Agent
Agent
Agent

No YAML parser in Python's stdlib; trying macOS's bundled Ruby to validate the workflow syntax:

Agent
Agent

Both parse. Task 3: the AGENTS.md pointer. Task 4: the quickest safe split of store.py is moving the schema SQL into its own module, which needs no change to any query.

Agent
Agent

store.py is now 371 lines. Updating the build log (file sizes, the resolved CI questions), then running all checks.

Agent
Agent

111 pass, leak check clean. Committing in two parts: CI first, then the store split, docs and the planner's two design files (so git status ends clean).

Agent
Agent

I did all four review items in two commits. Ruff, format and the full test suite pass (111, including the real-model test), the leak check is clean, history has only the noreply address, and git status is clean.

Commits
- 748fd94 CI: the actions are pinned to the SHAs you looked up, with the tag kept as a comment. The model job moved to .github/workflows/model.yml, which runs only when the model code, its test, pyproject.toml, uv.lock or the workflow itself change, or when started by hand. test.yml keeps the unit job. Both files parse as YAML; they haven't run on GitHub yet.
- 2c85fa2 store.py is split: the SQL schema moved to schema.py, which was quick and safe because no query changed, so it's now 371 lines and every file is under 400. AGENTS.md gets the one line pointing the next agent at the handover, and the build log is updated. This commit also adds your handover.md and review-1.md unedited; that's how git status ends clean.

Fixes needed in handover.md
1. Step 2.5 is wrong about the symlink. .claude/skills is not in the repo; it's in .gitignore, so a fresh clone won't have it. The fix is to add a step that creates it (ln -s ../.agents/skills .claude/skills). Being ignored, it won't show in git status.
2. Step 1.3's leak check would fail on four test files. The rule rejecting non-noreply email addresses catches made-up addresses used to test redaction:
- tests/test_redact.py:9 ([REDACTED])
- tests/test_redact.py:11 ([REDACTED])
- tests/test_collect_loop.py:71 ([REDACTED])
- tests/test_collect_scanners.py:88 ([REDACTED])

The fix is to also allow the reserved example domains (example.org, example.com, example.net). The /Users/ and private-key rules find nothing in the repo today.
3. Optional addition to Step 3.3: uv run munshi start --demo --self-test checks the card data, click, card fill, Esc, drag-saves-position, click-after-drag and no-Dock-icon behaviour on the real windows before you're asked to look.

Everything else in the handover matches the build: the commands and flags, file names, doctor, the Accessibility step, the demo commands, and the stop-line style.

DONE-munshi-review1

Session B turned on real collection behind the privacy filters above and tuned the prompt and the checks, looking only at numbers.

Munshi, session B: real collection behind privacy filters, tuned on numbers only
You

Read ~/github/prateek11rai/munshi/docs/design/phase-b.md and do everything in it, in order. If anything is unclear, ask in this window and keep going on the parts that do not depend on the answer.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

Now writing step 1. First the config: the two new word lists, unknown keys refused (so a misspelt privacy key fails loudly), and config errors that never echo values.

Agent
Agent
Agent

Now the store: a per-day counters table and slot columns for check reasons and model seconds, with an in-place migration for existing databases.

Agent
Agent

store.py is now 432 lines, over the ~400 limit. I'll keep the writes (bump, mark_slot) in the store and move the stats reads into a new stats.py.

Agent
Agent

Now the collector: skip reasons with per-day counters, the title and process word checks (before redaction), and dropping git/file names that contain a title word.

Agent
Agent

Only the expected schema-version test fails. Next, the model client remembers the 501.

Agent
Agent
Agent
Agent
Agent

Tidying two awkward formatting spots in stats.py, then wiring stats and the status thread into the CLI.

Agent
Agent

cli.py is now 440 lines. I'll move the background-thread builders into their own module, background.py.

Agent
Agent

Now the step 1 tests. First a small fake extension so a scripted window can carry its own pid.

Agent
Agent
Agent

The scripted events start one second after the only scan in the test window. Moving them to T0:

Agent
Agent

Collector tests pass. Next, tests for config, procs.usage, the store migration, stats, writer slot records and the CLI.

Agent
Agent
Agent
Agent
Agent
Agent

Adding a writer test for the stored slot records and a CLI test for stats --demo, then the full checks.

Agent
Agent

All 133 tests pass. Updating the spec (source of truth) for the new keys and the stats command before committing:

Agent
Agent

Running the full check set (including the real-model test) and the leak check before committing:

Agent
Agent

Everything passes: ruff, 134 tests including the real-model one, and the leak check. Committing in two pieces.

Agent
Agent

Step 1 is committed (ae71ce6, f92b34e). Step 2: checking that the real config loads. I'll print only booleans and list lengths.

Agent
Agent
Agent
Agent
Agent
Agent

No answer yet, so I'm continuing with work that doesn't depend on it. Step 4.1 is munshi eval --real: rerun the writer on a scratch copy of the real store and print numbers only.

Agent
Agent
Agent

Now wiring --real and --day into the eval command:

Agent
Agent

Now tests for eval --real: it must leave the real store untouched and print numbers only.

Agent
Agent
Agent

Tests pass. Running the full checks and committing eval --real:

Agent
Agent
Agent

Still no answer. Next: the demo click script (step 5.2) and the midnight-split unit test from step 3, which needs no real data.

Agent
Agent
Agent
Agent

The midnight test passes as written: the block is split at midnight, the first run after midnight closes the day, and the journal and CSV are written. Committing it:

Agent
Agent

Next is the demo click script (step 5.2). First I'll check what the demo journal folder holds, so the "Open journal" beat has a page to show.

Agent
Agent
Agent
Agent

demo load writes no journal page, so "Open journal" in the demo would open an empty folder. I'll have demo load write yesterday's made-up page as well (a small change), then write the script.

Agent
Agent
Agent

Now the click script:

Agent
Agent
Agent

Still no answer. Next, the README parts that don't need real numbers: privacy rules, commands, config and the Cursor credit.

Agent
Agent
Agent
Agent

While waiting, I'm drafting the phase-B section of docs/BUILD_LOG.md with the decisions made so far. Numbers come later.

Agent
Agent

Now a background watcher, so I'm woken if the planner adds an answer to phase-b.md:

Agent
Agent

I've done everything I can without your answers. Real collection hasn't started: the Firefox work-profile rule doesn't work on this laptop yet, and the brief says to stop and ask in that case.

Done so far (all checks green each time: ruff, 137 tests including the real-model one, leak check clean):
- Step 0: all checks passed, nothing blocked.
- Step 1 (ae71ce6, f92b34e):
- A window whose title or app name contains a title word is not recorded at all. Git and file names with the word are dropped too, which goes slightly beyond the brief.
- The process-word check reads each app's command line once with ps and keeps only the yes/no answer.
- A rejected model format (Ollama's 501) is now remembered for the client's lifetime.
- Skip counters per day, and munshi stats (numbers only).
- The config now refuses unknown keys, so a misspelt privacy key fails loudly, and its errors never repeat values.
- Step 4.1 (f50b0a1): munshi eval --real [--day D] reruns a real day in a throwaway copy of the store and prints numbers only.
- Ahead of order:
- A midnight-split test (ac7524d).
- The demo click script docs/demo/script.md, and demo load now writes yesterday's journal page (c7f3e49).
- README privacy rules, new commands and the Cursor credit (f9d8a00).
- A phase-B build-log draft, not committed yet.
- Step 2.1/2.3: munshi doctor loads the real config, and Accessibility is already granted for this terminal.

Question 1 (blocking real collection). Firefox has 2 app processes running and neither matches the process word. Neither was started with a profile flag, and the profile folder each one has open doesn't contain the word either. So the rule can't tell your work profile apart yet.
- How many work-profile Firefox windows do you have open right now?
- How do you open the work profile: the new profile switcher, about:profiles, or -P <name>?
- Which fix do you want?
- (a) Open the work profile with the word in its command line, e.g. open -n -a Firefox --args -P "<profile name containing the word>", and I re-run the check.
- (b) Ignore Firefox entirely. This loses personal Firefox time too.
- (c) Something else.

Question 2 (step 5.1). May I take one screenshot of the buddy window alone, in demo mode, by window id (screencapture -l <id> -o), for docs/img/buddy.png? Optionally a second one of the demo card window?

I'm also watching the Answers section of phase-b.md, so the planner can answer there instead.

Note: AGENTS.md still says "Never run real collection on a work machine". Phase B overrides that on your instruction. Do you want that line updated once the real run starts?

Agent

The watcher fired, so the Answers section of phase-b.md has changed. Reading it:

Agent
Agent
Agent
Agent

Ruff reformatted that line. The one failing test is expected: its fake only knows ps, and Firefox now also calls lsof. Extending the fake and adding profile-folder tests:

Agent
Agent

That's the designed behaviour, not a bug: pid 12 had no profile folder in the fake, so the check retries every poll. A real Firefox always holds its profile open, so I'll give pid 12 one in the test.

Agent
Agent

Adding a macOS-source test for the bundle lookup:

Agent
Agent

All 140 tests pass. Updating the spec and the build-log draft for the profile-folder rule, then committing:

Agent
Agent
Agent
Agent

The Firefox profile check now passes. Real collection still hasn't started: I'm waiting for your confirmation and the planner's go, as the planner's answer requires.

What changed (a3b7e8a): for a Firefox process, the process-word check now also looks at the name of the profile folder the process has open (via lsof). This was needed because your profiles don't show up in the command line. As before, only the yes/no answer is kept; if lsof fails, it counts as no match and is tried again. The full test suite (140 tests, including the real-model one) and the leak check pass.

The re-run check (booleans and counts only):

process words configured: 2
firefox pid-hidden: matches-word=true
firefox pid-hidden: matches-word=false
total firefox app processes: 2 · matching: 1 · answers cached: 2

Needed before I start recording:
1. Owner: please confirm that exactly one of your two Firefox instances is the work profile, with the other one personal.
2. Planner: the go for step 2.4, in the Answers section. I'm watching it.

Once both are in, I'll open mlh:munshi, run uv run munshi start there, and check munshi stats after 2 minutes.

Still open:
- Question 2: may I screenshot the buddy window alone, in demo mode, by window id, for docs/img/buddy.png? Optionally the demo card window too?
- AGENTS.md: it still says never to run real collection on a work machine. Do you want that line updated?

Agent
Agent
Agent

While the app runs for its first 2 minutes, I'm updating AGENTS.md as the owner approved:

Agent
Agent
Agent

Checking that the app started cleanly (its own messages only):

Agent
Agent
Agent
Agent
Agent

The window names may come back empty without Screen Recording permission, so the helper will also print sizes (88×88 buddy, 380×560 card) for that pid only:

Agent
Agent
Agent
Agent
Agent

The background job is still sleeping. It's past the 2-minute mark, so I'll run stats directly:

Agent
Agent
Agent

The fix works against the real ps. Committing it, plus the hidden --show-card flag as its own commit, after the full checks:

Agent
Agent

Restarting mlh:munshi to pick up the fix (Ctrl-C, then start again). I'll only check whether the app is running, without capturing the pane:

Agent
Agent
Agent
Agent

The demo instance is up: buddy is window 74468 (88×88), card is window 74477 (380×560), both on screen. Capturing each by window id, without shadow:

Agent
Agent
Agent

Recording has resumed, and real window events are now being recorded (3 polls so far).

Agent
Agent

While I wait, checking the Spotlight query against the real allowed root, through the same FileScanner code. Only counts are printed:

Agent
Agent
Agent

All 13 repos read fine. The gap may be my scrub rule dropping one repo whose name or branch contains a title word. Checking with a count only:

Agent
Agent
Agent
Agent
Agent

The fix is committed (b7c908f) and the app is running again (RSS 91 MB). Waiting on the owner now. The watcher on the Answers section is running.

Done since the last update:
- Started the real app in mlh:munshi. The first stats turned up two bugs, both fixed, tested and committed:
- af24a68: memory and CPU weren't showing because of a header line in the ps output.
- b7c908f: a repo under your root whose name contains the work word was still offered to the model as a project name. Its git events were already being dropped. The writer hadn't written any real slot yet, so nothing was stored through this.
- Real-run checks so far, all from stats or counts:
- Accessibility is granted. Window events are being recorded, and away detection fired.
- Git: 13 repos read, 12 stored; the one with the work word is dropped.
- Spotlight: the query works; 8 recent files were found, all correctly skipped because they're inside git repos.
- AGENTS.md updated as approved (13c7a7f).

Screenshots failed. On this macOS, screencapture -l needs Screen Recording permission for WezTerm ("could not create image from window"). Nothing was captured. During the attempt, real recording was paused for about 2 minutes; it has resumed and the demo instance is stopped. Your options:
- (a) Grant WezTerm Screen Recording for a minute (System Settings → Privacy & Security → Screen & System Audio Recording). I capture both windows by id, then you revoke it.
- (b) Take them yourself with ⌘⇧4, then Space, then ⌥-click the window, and save as docs/img/buddy.png and docs/img/card-window.png. I'll start the demo instance for you.
- (c) Skip them.

Hand checks (step 2.5), on the real buddy:
1. The buddy appears.
2. Clicking it opens the card with your real data. "Today" fills after the first writer run, around 01:03.
3. Esc hides the card.
4. Dragging moves the buddy without opening the card.
5. There is no Munshi icon in the Dock.
6. No Slack or work-Firefox time appears anywhere in the card.

Step 3 actions. I'll check each one with stats counters, numbers only:
- Switch between a few apps for 2 minutes.
- Use Slack for about 30 seconds.
- Use work Firefox for about 30 seconds.
- In personal Firefox, open a page with the work word in its title.
- Open a private window for about 20 seconds.
- Leave the laptop untouched for 4 minutes.
- Pause from the card, then Resume.
- Optionally, last: Forget last hour. This deletes that hour of your real record.

Reply here or through the planner.

Agent
Agent
Agent
Agent
Agent
Agent
Agent

Starting with Fix 1. The brief allows Playwright on munshi preview --demo pages, so I'll reproduce the overlap at the card's real size (380×560). Starting the demo preview in the background:

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

Fixing the card footer issue the owner reported. The layout measures clean in both the browser and the app window, so I'm checking a screenshot of the preview's bottom to see what they saw.

Agent
Agent
Agent

Simplifying: html { height: 100% } is harmless for the buddy page too. I'll also merge the two #copy rules, then re-check in the preview and the app window:

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

Fixed in the preview: the Copy button now has the accent colour and clear space, and the footer is its own panel. Committing Fix 1 after the checks:

Agent
Agent

Fix 1 is committed (82644ba). Now Fix 2. First, the word helpers in blocks.py, which the "topic is not an app name" check will reuse:

Agent
Agent
Agent

Only one test checks the "Mostly " prefix, so I'll use the planner's wording, e.g. "Mostly tidepool for 3 h 30 min, then in WezTerm for 2 min." Writing the 2b changes:

Agent
Agent

All tests pass. The code day bullets would still list fallback labels such as "WezTerm · tmux" as topics, so I'll have by_project label them "in WezTerm" too:

Agent
Agent
Agent
Agent
Agent
Agent
Agent

Fix 2b (topics and notes) is in place and the existing tests pass. Next: tests for it, then the tmux pane context (2a).

Agent
Agent

Committing 2b:

Agent
Agent

Fix 2b is committed (0828bef). For 2a, I'll first check how tmux answers here, printing booleans only:

Agent
Agent
Agent
Agent
Agent

The tmux pane is wired into the real collector. Now its tests, all with fakes:

Agent
Agent

All 7 tmux tests pass. Updating the spec, committing, and restarting the real app:

Agent
Agent
Agent
Agent

While the owner does that, step 4: a baseline eval --real on today's real slots (numbers only):

Agent
Agent
Agent
Agent

Committed (86975f2). I'll restart the app for this after the owner's Slack, Firefox and away pass, so as not to interrupt it. Checking the watcher:

Agent
Agent
Agent
Agent

My expected numbers were wrong, not the code: 12 + 10 minutes are model-labelled and 14 minutes are code-labelled, which gives 61% and 39%. Fixing the test:

Agent
Agent

Committed (12bf70a). For a before/after comparison, I'm running the demo eval in the background (about 3–4 minutes), which also shows whether the new app-topic check changes the demo numbers:

Agent
Agent

Waiting on two background jobs: the demo eval, and the watcher for the owner's Slack, work-Firefox and away pass.

So far in this stretch, all fixes from the owner's hand checks are committed:
- 82644ba: the card footer no longer covers anything, and the Copy button is visibly part of Hours.
- 0828bef: a topic can't be just an app name. When only app names are known, the note says "Mostly in WezTerm for 2 min".
- eef3ab3, 86975f2: terminals titled tmux now record the pane's command and repo, never its path, and stats counts how often that happens.
- 12bf70a: the eval now reports the share of time the model labelled.

The real app is running. I'll restart it once the owner's pass is done.

Agent
Agent
Agent

The build log now has the first-run findings (not committed yet; it'll be committed with the final numbers). The real app keeps recording, and the watcher wakes me when the work-Firefox counter appears, the planner answers, or 25 minutes pass (about 01:30).

Agent
Agent
Agent
Agent
Agent
Agent

Starting the 15-minute probe in the background. It prints only Firefox/other and true/false.

Agent
Agent
Agent
Agent

The probe hasn't printed its first line yet. Checking whether it's alive and how much CPU it uses:

Agent
Agent

The probe is asleep at 0% CPU and has printed nothing, so it's blocked somewhere. I'll stop it and run a 10-second version in the foreground with a traceback dump to find where:

Agent
Agent
Agent

The probe works in the foreground; under the background-task runner it blocked. I'll run it detached with nohup, writing to a scratch log, the way the demo instance ran earlier:

Agent
Agent

The probe is running now, until about 01:38. A watcher will wake me when it finishes or the planner answers:

Agent
Agent

While that runs, a fresh eval --real on today's slots so far (numbers only):

Agent
Agent
Agent

No warnings at all. I'll call a fresh GitScanner directly, as the app does at each start, and count what it would emit (counts only, nothing stored):

Agent
Agent

A fresh scanner emits 13 events, yet the restarts at 00:44, 00:47 and 01:04 added almost none. Searching the pane's history again, this time for munshi: anywhere in a line, printing counts only:

Agent
Agent
Agent
Agent
Agent

The test passes: after a restart, only repos that really changed are reported. Committing; I'll restart the app after the probe window closes, so the owner's check isn't disturbed.

Agent
Agent
Agent
Agent

Existing tests pass. Adding a test for the new schema shape and for extra ids counting as evidence:

Agent
Agent
Agent
Agent
Agent

The check passed; only my count was off. The first window item merges two events, so the block has 3 event ids, not 2:

Agent
Agent

Now the real eval with the split schema, and the demo eval in the background to check for regressions:

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

It's 01:35; the probe runs until 01:38. Committing the tuning in two pieces: the prompt and schema, then the git-noise filter.

Agent
Agent

Splitting the test file by hunks didn't work cleanly, so this goes in as one tuning commit:

Agent
Agent

Committed (0d40c7b). Running the demo eval on the final writer in the background:

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

The tuning table is in the build log. Running the full suite, including the real-model test, against the tuned writer:

Agent
Agent
Agent

The checklist is in the build log. Checking the watcher and the current numbers for the README proposal:

Agent
Agent
Agent

Committed (e3107ef). That leaves the owner's work-Firefox check and README numbers the owner approves (step 5.3). Proposed README numbers, to be refreshed at the end of the run:

First night on a real (work) laptop, 25 October 2026. Recording from 00:40: about 27 min of active window time so far, every app except Slack and the work browser profile. On the real slots the model labels 65% of active time and code labels the rest (demo days: 95%). 6.7 s per 15-minute slot on real data (demo: 3.1 s) with gemma4:e4b. The app uses 32 MB RSS and 1.1% CPU at rest (80–90 MB and about 40% CPU for the first seconds after start).

Owner/planner:
1. Do you approve these numbers for the README, refreshed with the totals at the end of the run? Or would you rather leave out "work laptop" and the per-app details?
2. Please do the 30-second work-Firefox check above. That's the last open item before I write the README section and print the final report.

The watcher wakes me when the process word counter moves or an answer arrives, or after 30 minutes (about 02:10).

Agent
Agent
Agent
Agent
Agent

Waiting on the watcher, which reports at about 01:57 or as soon as the work-Firefox counter moves. Then I'll add the final numbers to the build log, commit it with phase-b.md, and report.

Agent
Agent
Agent

Main now includes the docs merge, and my working tree is clean apart from phase-b.md. Making both strings generic and committing them together with phase-b.md:

Agent
Agent

Both strings are generic, the lock file is still valid, and the checks pass. Committing them with phase-b.md, as asked:

Agent
Agent

Committed (046bac8). One last eval --real over all 4 real slots for the build log's final numbers:

Agent
Agent
Agent

Committed (8c703fa). Final checks, one last look at the work-Firefox counter and the Answers section, and the list of my commits:

Agent
Agent

Phase B is done except one check: the work-profile Firefox has never been in front while the app was recording, so the rule that skips it hasn't fired live yet. Everything else ran on real data here. All 158 tests pass, including the real-model test, and the leak check is clean. The real app is still running in mlh:munshi; stop it with Ctrl-C there.

Commits (21, nothing pushed):
- Privacy rules and stats:
- f92b34e title and process words, skip counters, munshi stats
- a3b7e8a process words also match the open Firefox profile folder
- b7c908f repo names containing a blocked word are kept out of the project list
- af24a68 stats now shows the app's memory and CPU (a ps parsing bug)
- Writer and tuning:
- ae71ce6 Ollama's 501 is remembered for the client's lifetime
- 0828bef a topic can never be just an app name
- 14ed4c3 the git scanner remembers state across restarts
- 0d40c7b window ids are the evidence; git and file ids are optional extras; unrelated git items are dropped
- f50b0a1 eval --real
- 12bf70a the eval reports the share of time the model labelled
- Terminals: eef3ab3, 86975f2: a terminal titled tmux records the pane's command and repo (never the path), and stats counts it.
- Card and demo:
- 82644ba card footer layout
- cabdac7 hidden start --show-card flag
- c7f3e49 demo click script; demo load writes yesterday's journal page
- ac7524d midnight-split test
- Docs:
- 13c7a7f AGENTS.md rule
- f9d8a00 README; superseded by the docs agent
- e3107ef, 8c703fa build log
- 046bac8 generic descriptions, plus phase-b.md

stats at 02:00 (the run started 00:40):
- 36 min of active window time, 28 min away.
- 315 polls recorded, 125 of them using the tmux context.
- Skipped polls: ignored app 6, Munshi's own window 59, paused 6, away 100.
- 35 git events, 0 file events.
- Writer: 4 slots; 7 blocks accepted, 19 rejected, 6 fallback (3 of the slots were written before tuning).
- App at rest: 43 MB RSS, 0.0% CPU.

Tuning, before → after (code checks unchanged):

Time labelled by the model Seconds per slot
Real slots 19% → 80% 10.7 → 9.0
Demo slots 96% → 95% 3.2 → 3.1

The "after" real number is over 4 slots. Live slots take about 24 s on average because the model unloads 5 minutes after each run and the writer runs every 15 minutes, so most runs start with a 17-second load. I kept that on purpose: the alternative holds 6.6 GB in memory all day.

Owner confirmed by hand: the buddy, click opens the card, Pause greys the buddy, and the Slack and 4-minute-away checks. The counters agree for Slack and away. Screenshots were skipped: capturing by window id needs Screen Recording permission for the terminal.

Still rough:
- Live work-Firefox skip: the process word counter is still 0 (see top). The matcher marks exactly 1 of the 2 Firefox instances, which the owner confirmed is the work one; the only Firefox time recorded was the other instance.
- Card footer: the overlap the owner saw never reproduced in the browser or the app window. I made the layout defensive and the self-test now measures it.
- Fakes only: title words and private windows were tested only with fakes on this run.
- Session B curation:
- One of my tool outputs (about 00:40:38) captured the shell prompt with the account email, including the work domain; please remove it before publishing.
- My earlier README commits added "employer"/"work profile" wording. The docs agent's README has since replaced the README, so that's only in history.

DONE-munshi-phaseB

In both halves one agent session acted as planner, writing the spec and the briefs and reviewing every diff, while builder sessions in other tmux windows wrote the code.

One fix came from elsewhere: commit cba1f1e, which retries without constrained JSON when Homebrew's Ollama returns 501, was made by Cursor with Grok on a second Mac.

The Lazy Payoff

In its first night Munshi journaled 40 minutes of my active time, and 31 minutes away, in 13 blocks. After tuning, Gemma labels about 80 % of the active time and code labels the rest. With the model loaded a slot takes about 9 seconds; the app itself sits at 16 to 45 MB of memory and close to 0 % CPU.

What I did not do: write a note about what I did, scroll back through chat to rebuild Friday, or round my hours to whatever felt right. On Monday I click the munshi and read.

Prize Categories

Best Use of Gemma. Gemma 4 e4b runs locally through Ollama and does the one job a model is good at here: reading a messy slot of window titles, commits and file names and naming what I worked on. Everything around it is deliberately strict. It answers JSON under a schema at temperature 0, it has no tools, window titles are treated as data and never as instructions, and code checks every word it writes before anything reaches the card. A 4B model is enough to label a workday, and small enough that it never has to leave my Mac.

Top comments (0)