Status: Approved for implementation
Version: 1.0
This spec is the source of truth. Every requirement below has an ID.
Every ID must be traceable to an implementation location and a test —
see TRACEABILITY.md. If code exists that isn't traceable to an ID here,
that's scope creep and should be cut or the spec should be updated first.
Build a CLI alarm clock. No web UI, no database. Time-boxed exercise, no detailed spec was provided by the requester — so this document is the spec, written first, before implementation, to remove ambiguity and give a fixed target to build and test against.
- User: a person operating the tool from a terminal.
Each requirement has a unique ID, a description, and acceptance criteria
in Given/When/Then form. Acceptance criteria are what the tests in
tests/test_alarmclock.py are written against — not vibes.
The user can create an alarm with a time, an optional label, and a repeat mode.
- FR-1.1 — Given a time in 24-hour
HH:MMformat, when the user adds an alarm, then it is persisted with that time, zero-padded (e.g.7:5→07:05). - FR-1.2 — Given a time outside valid ranges (hour > 23, minute > 59) or a non-time string, when the user attempts to add it, then the system rejects it with a clear error and does not persist it.
- FR-1.3 — Given no
--repeatflag, when an alarm is created, then it defaults toonce. - FR-1.4 — Given
--repeatis provided, it must be eitheronceordaily; any other value is rejected. - FR-1.5 — Given a label is omitted, the alarm is stored with an empty label (not an error).
- FR-1.6 — Given a
oncealarm whose time has already passed today, when it is created, then the system accepts it (it will fire at that time tomorrow) and prints an informational note — this is not an error condition.
The user can view all alarms currently stored.
- FR-2.1 — Given zero alarms exist, when the user lists them, then a "no alarms" message is shown (not an empty table, not an error).
- FR-2.2 — Given one or more alarms exist, when listed, then each row shows: id, time, repeat mode, enabled/disabled status, and label.
- FR-2.3 — Alarms are listed sorted by time, ascending.
The user can delete an alarm by its id.
- FR-3.1 — Given a valid existing id, when removed, then it no longer
appears in subsequent
listcalls. - FR-3.2 — Given an id that does not exist, when removal is attempted, then the system reports an error and exits non-zero, without modifying the store.
The user can start a foreground process that watches for due alarms and rings them.
- FR-4.1 — Given the current time matches an enabled alarm's time
(same
HH:MM), when the watch loop polls, then that alarm fires. - FR-4.2 — Given an alarm has already fired once on the current calendar date, when the loop polls again within that same date, then it must not fire again (prevents re-firing every poll cycle within the same matching minute).
- FR-4.3 — Given an alarm's
repeatisdaily, when it fires and is dismissed, then it remains enabled and will fire again on the next matching date. - FR-4.4 — Given an alarm's
repeatisonce, when it fires and is dismissed, then it is disabled (will not fire again). - FR-4.5 — Given a disabled alarm, when the loop polls, then it is never considered due, regardless of time match.
- FR-4.6 — When the loop is interrupted with Ctrl+C, the process exits cleanly with a message, not a stack trace.
When an alarm fires, the user must be able to dismiss or snooze it.
- FR-5.1 — On firing, the system plays an audible/visual alert (bell + banner) and prompts for input.
- FR-5.2 — Given the user presses Enter (or types
d/dismiss), the alarm is dismissed: FR-4.3/FR-4.4 apply. - FR-5.3 — Given the user types
s/snooze, the alarm is rescheduled to fire again in 5 minutes, remains enabled, and is treated as a fresh one-shot fire (not double-counted against the original schedule). - FR-5.4 — Given unrecognized input, the system reprompts rather than crashing or silently dismissing.
- FR-5.5 — Given stdin is closed/unavailable mid-prompt (EOF), the system treats this as a safe auto-dismiss rather than crashing the process.
- NFR-1 (No external runtime dependencies) — the tool must run using only the Python standard library. Rationale: this is a timed exercise and a portable deliverable; audio/scheduling libraries add platform-specific install failure modes that aren't worth the risk.
- NFR-2 (No database) — explicitly excluded by the assignment. Persistence is a flat JSON file.
- NFR-3 (Crash safety of persisted state) — a corrupted or half-written store file must not crash the CLI; it should be treated as empty rather than raising an unhandled exception.
- NFR-4 (Atomic writes) — writes to the store must not leave a partially-written file if interrupted mid-write.
- NFR-5 (Testability) — every FR above must have at least one
corresponding automated test. See
TRACEABILITY.md.
Documented here rather than silently dropped, so scope decisions are visible and defensible:
- OS-1: Timezone handling — local system time only.
- OS-2: Weekday-specific repeat patterns (e.g. weekdays-only). Only
onceanddailyper FR-1.4. - OS-3: Custom/audio-file sounds — terminal bell only, per NFR-1.
- OS-4: Background/daemon mode (systemd,
nohup, etc.) —runis a foreground blocking process by design; backgrounding it is an OS-level concern outside this tool. - OS-5: Simultaneous ringing of multiple due alarms — they ring sequentially in the order encountered, not concurrently.
A requirement is "done" when:
- Implementation exists that satisfies its acceptance criteria.
- An automated test exists exercising that acceptance criteria and passes.
- It is listed in
TRACEABILITY.mdwith links to both.
This spec is deliberately written before PLAN.md and before any
implementation code, and is not edited retroactively to match whatever
got built — if implementation reveals the spec was wrong, the spec is
updated explicitly and that update is called out, not silently absorbed.