# AGENTS.md Instructions for coding agents working in this repository. Humans should start with [README.md](README.md). The tracker contract is [protocol.md](protocol.md). ## What this is Omapomodoro (`postman.omapomodoro`) is a third-party Omarchy plugin: a **service** that supervises a Rust tracker, and a **bar-widget** whose KeyboardPanel overlay shows the live cycle, today's sessions, week analytics, and streaks. The overlay does not own the clock. All timing, persistence, and analytics live in `omapomodoro-track`. Do not turn this into a standalone app, an Electron UI, or a first-party `omarchy.*` plugin. ## Hard constraints - **Never run the timer, persistence, or analytics from QML/JS.** No QML `Timer` as source of truth, no writing `history.json` from the overlay. - **Never walk `/proc`.** This plugin does not care which window is focused. - **Never write history outside the state dir.** `$XDG_STATE_HOME/omapomodoro` or `~/.local/state/omapomodoro` (`0700` / `0600`). - **Never edit `/usr/share/omarchy/`.** - **Do not change the plugin id** (`postman.omapomodoro`) unless the user explicitly asks. - **Do not commit** `DESIGN.md`, `PLAN.md`, live history, sockets, `.env`, or `target/`. - **`omarchy plugin add` does not compile.** After clone, `mise install && ./scripts/build.sh` must produce `target/release/omapomodoro-track`. ## Layout ``` manifest.json id, kinds: ["service","bar-widget"], settings schema service/Service.qml ensure the daemon every 60s bar/BarWidget.qml chip + KeyboardPanel host + PanelKeyCatcher bar/Model.js chip text / tooltip / parse overlay/Overlay.qml session, FileView, Process children, remaining tick overlay/OverlayModel.js parse, merge, actions, palette overlay/Hero.qml pinned title + Time / Day / Week / Streak overlay/TimerPanel.qml ring + start/pause/skip/reset overlay/RingCanvas.qml phase ring overlay/TodayPanel.qml today's sessions overlay/SessionList.qml session rows overlay/WeekPanel.qml 7-day strip + rows overlay/WeekStrip.qml last 7 local days overlay/StreaksPanel.qml goals + streak + heatmap overlay/Heatmap.qml keepDays cells overlay/Meter.qml goal meter overlay/StatusBar.qml running / paused / offline overlay/EmptyState.qml first-run / missing binary / error overlay/Format.js MM:SS, durations, words overlay/qmldir src/main.rs CLI dispatcher src/protocol.rs NDJSON events src/paths.rs dirs, flock, atomic write src/clock.rs three clocks, day keys src/timer.rs phase machine src/daemon.rs socket, snapshot, poll src/store.rs history / session / snapshot / config src/stats.rs sessions, week, heatmap, insights src/streaks.rs current/best streak src/goals.rs goals.json src/config.rs durations / flags src/session.rs loginctl IdleHint / LockedHint tests/ unit + CLI scripts/build.sh scripts/test.sh the verification gate scripts/dev-install.sh scripts/install-menu.sh scripts/publish.sh ``` ## Build and verify ```sh mise install ./scripts/test.sh ./scripts/build.sh ./scripts/dev-install.sh ``` After QML edits on a **symlink** install, force a reload with `omarchy-shell shell rescanPlugins` — inotify does not follow the plugin symlink. `./scripts/test.sh` is the gate: `cargo fmt --check`, clippy, unit + CLI tests, `omarchy plugin validate`, and a JSON parse of `manifest.json`. ## Protocol invariants Full schema: [protocol.md](protocol.md). - One JSON object per stdout line. `v` must be `1`. Unknown `type` → ignore. - stderr is human logs only. Overlay must not parse stderr. Exception: the daemon's first stderr line is a JSON `hello`. - `proto` creates the state dir and prints paths. It does not bind the socket. - Do **not** ship a field named `idle`. Session IdleHint is `sessionIdle`. - `label` is today's completed count, never the streak. - Omitted `config` / `goal` flags never reset last-seen values. - `stop` flushes the envelope; it does not abandon the open phase. ## Overlay invariants 1. **Never `Date.now()`.** Remaining on the ring is last snapshot `remainingMs` minus tick count (`tickInterval` 250 ms), floored to `remainingMs - 1000`. A QML `Timer` is animation only. Do not interpolate from wall clock — that completes a focus after a nap. 2. **`FailedToStart` + `onRunningChanged`.** Quickshell does not emit `exited` on `FailedToStart`. Missing binary: `onExited` code `!== 0`, **or** the process stops without a v1 `proto` / expected event (`onRunningChanged` and the matching `*SawExit` flag is still false). Reset those flags before every spawn. 3. **Do not put a role named `color` on a QML ListModel.** Use `fill`. Quickshell treats `color` as reserved. 4. **Duration edits go through `setDuration`.** Time-tab steppers call `overlay.applyDuration` → `BarWidget.setDuration` → `updateEntryInline` + `onSettingsChanged` `ensure`. Do not write `config.json` from QML. Config applies to the next phase. The Cycle card header toggles `showCycle` (`overlay.toggleCycle` → `BarWidget.setShowCycle`); that is a widget setting, not tracker config. 5. **`PanelKeyCatcher` signals only.** Wire `/usr/share/omarchy/shell/Ui/PanelKeyCatcher.qml` — do not invent a parallel `Keys.onPressed`. - Space / Enter → `onActivateRequested` (start / pause / resume; on Streak, cycle daily pomodoro goal). Never bind Space via `onTextKey`. - `x` / `X` → `onDeleteRequested` (reset). Never bind `x` via `onTextKey`. - arrows / `h` `j` `k` `l` → `onMoveRequested`. Do not invent a j/k-vs-arrows split. - `s` skip, `1`–`4` tabs, `r` refresh, `g`/`G` scroll → `onTextKey`. - Esc → `onCloseRequested`. Tab → `onTabRequested`. - Set `blocked` while a duration stepper or goal editor has focus (`pickerOpen`). 5. Overlay width is `Style.space(420)`, height fits content (cap `Style.space(560)`). Header stays **outside** the Flickable. 6. Chip and overlay `FileView` `snapshot.json`. No 1-second `Process` poll. Open = reload + `ensure` + one `view`. 7. Drop a `view` with `sessions.length > 80` or `heatmap.length > 400`. ## Tracker invariants - Focus remaining is monotonic elapsed. Sleep cannot complete a focus. - Break remaining is boottime elapsed. Sleep consumes the break. - Pause focus on `LockedHint` only. `IdleHint` is `sessionIdle` (informational). - Credit the open session to `session.dayKey` captured at start. - Skip of a focus does **not** increment `cycleIndex`; it always arms a short break. - Snapshot: immediate on transition; ~1s while running; relaxed (no fsync). History and session are `fsync`'d. ## Out of scope unless asked - Rewriting the tracker in another language. - Packaging a prebuilt binary for every arch. - Changing marketplace id, license, or install/remove docs without updating README and `manifest.json` together. - Tracking focused windows (`/proc`, Hyprland class). - Tasks / tags / projects on a session. - Cloud sync or a mobile companion.