A dependency-free, terminal-native alarm clock. No web UI, no database — alarms persist to a small JSON file on disk.
Given no detailed spec, I didn't jump straight to code. The order was:
SPEC.md— written first. Every behavior is a numbered, testable requirement (FR-1.1,NFR-3, etc.) with Given/When/Then acceptance criteria, plus an explicit "out of scope" section so scope cuts are visible decisions, not silent omissions.PLAN.md— derived from the spec, not the other way around. Tasks are sequenced by dependency (data model → storage → scheduling logic → CLI), and each task cites which requirement IDs it exists to satisfy.- Implementation — every non-trivial line in
alarmclock/*.pythat implements a requirement is tagged inline with its ID (e.g.# FR-4.2), so you can grep the code against the spec directly. TRACEABILITY.md— a matrix mapping every requirement to its implementation location and its test. Built by walking the spec line-by-line against the finished code, which is what surfaced two real, documented coverage gaps (see that file's "Known gaps" section) rather than assuming coverage was complete.
The point of this ordering: the spec is the thing under version control and review, not a single prompt's output. If a requirement turns out to be wrong or missing, that's a spec change with a visible diff — not a silent edit to whatever the AI happened to generate.
Scope. Given a 30-minute, no-spec exercise, I decided the core value is: add an alarm, see your alarms, remove one, and have the clock actually ring at the right time with a snooze/dismiss interaction. Everything else (custom sounds, weekday-only repeats, timezones) is explicitly out of scope — see "Not built (and why)" below.
No external dependencies. Sound and scheduling libraries (playsound,
schedule, etc.) add platform-specific fragility that isn't worth it for
this scope. The terminal bell (\a) plus a printed banner works everywhere
Python runs, with zero install steps.
JSON file, not a database. The exercise explicitly says no database.
Alarms are stored at ~/.alarmclock/alarms.json by default (overridable via
--store <path> or the ALARMCLOCK_STORE env var), written atomically via a
temp-file-then-rename to avoid partial writes if the process is killed
mid-save.
Separate add and run commands. A real alarm clock needs to accept new
alarms while it's not necessarily running the watch loop right now (e.g.
you set an alarm the night before). Splitting these means the JSON file is
the single source of truth shared between invocations.
| Decision | Reasoning |
|---|---|
Alarm times are HH:MM, 24-hour, validated via datetime.time() |
Fails fast on 25:99 etc. at add time, not at 3am when it silently never fires |
once alarms auto-disable after ringing; daily alarms don't |
Matches what a user actually expects from each mode |
last_triggered_date tracked per alarm |
The watch loop polls every second, so without this a daily alarm would ring 60 times in its matching minute |
Setting a once alarm for a time already past today |
Not treated as an error — it will correctly ring at that time tomorrow, since the loop matches on time-of-day only. A warning is printed at add time so the user isn't surprised |
Ctrl+C during run |
Caught explicitly, exits cleanly instead of a stack trace |
| Snooze | Reschedules the same alarm object to fire again in 5 minutes as a one-shot re-fire, rather than creating a duplicate alarm entry |
| Corrupted/unreadable JSON store | Loads as empty rather than crashing the CLI — a corrupted cache shouldn't brick the tool |
input() raising EOFError (stdin closed, e.g. piped/non-interactive run) |
Caught and treated as an auto-dismiss instead of crashing the whole process — found via live testing, see commit history |
Left out deliberately, given the time box — noted here rather than silently omitted:
- Timezones — assumes local system time throughout.
- Weekday-specific repeats (e.g. "weekdays only") — only
once/daily. - Custom/audio-file sounds — terminal bell only, to stay dependency-free.
- Background/daemon mode —
runis a foreground blocking process by design; wrapping it in a systemd unit ornohupis an OS-level concern, not this tool's. - Concurrent multi-alarm ringing — if two alarms are due in the same second, they ring one after another, not simultaneously. A queue, not a interruption.
No dependencies required to run the tool itself (only pytest for the test
suite).
git clone <this-repo>
cd alarmclock-cli
# Add an alarm
python -m alarmclock add --time 07:30 --label "Wake up" --repeat daily
# Add a one-off alarm
python -m alarmclock add --time 14:00 --label "Standup"
# See what's scheduled
python -m alarmclock list
# Remove one (id comes from `list`)
python -m alarmclock remove d05c361e
# Start the clock — blocking, watches every second, rings when due
python -m alarmclock runWhen an alarm rings, you'll see a banner and be prompted to press Enter to dismiss, or s to snooze 5 minutes. Ctrl+C stops the watcher entirely.
By default alarms are stored at ~/.alarmclock/alarms.json. Override with
--store /path/to/file.json on any command, or set ALARMCLOCK_STORE.
pip install pytest
python -m pytest tests/ -v15 tests covering: time/repeat validation, JSON persistence round-trips (including missing/corrupted files), the due-alarm detection logic (the actual scheduling correctness — same-time match, disabled alarms, no double-fire within a day, daily re-fire next day), and end-to-end CLI smoke tests via subprocess (add/list/remove, and rejecting invalid input).
alarmclock/
models.py — Alarm dataclass + validation
storage.py — JSON load/save (atomic writes)
engine.py — the watch loop: due-alarm detection, ringing, snooze/dismiss
cli.py — argparse subcommands: add / list / remove / run
__main__.py — `python -m alarmclock` entrypoint
tests/
test_alarmclock.py