Vibecode Any.do
track this build4 phases, 12 steps, beginner friendly0%The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Any.do, manage personal tasks, lists, reminders, recurring items, and a daily planning flow. The hard boundary is mobile capture, widgets, family features, calendar integration, and sync, plus cross-device reliability and workflow polish.
You are building a lean indie version of Any.do. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Any.do · indie build A desktop task manager whose hard problems are storing scheduled times so they still mean the same thing after a government changes the clocks, firing reminders exactly once across sleep and restarts, and letting a recurring series be edited without rewriting what already happened. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A desktop application with a global capture shortcut and native notifications | capture that needs a window to be open is capture that does not happen | | Storage | SQLite beside the application, one file, backed up on a schedule | a task list that loses an entry is not a task list | | Time | Wall clock plus an IANA zone identifier, with instants derived at scheduling time | a future instant computed today is a guess about rules that have not been made yet | | Recurrence | A rule on the series, occurrences computed on read, rows written only when something happens | materialising ten years of a daily task is ten years of rows to migrate | | Delivery | A scheduler that records each fire and refuses to fire the same occurrence twice | waking a laptop should not produce forty notifications | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the test suite. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A desktop application shell with a global shortcut and native notifications** · free - Why: Both capture and delivery depend on working while the window is closed, which a web page cannot do dependably. - Get it: Any desktop shell for your stack that exposes a global shortcut and the system notification service. - Verify: A shortcut pressed in another application opens capture, and a notification appears with the window closed - [ ] **A time zone library backed by the IANA database, whose version you can read at runtime** · free - Why: The whole scheduling design depends on knowing which database version produced an instant, so a library that hides the version is unusable here. - Get it: Use your runtime's internationalisation support or a maintained library that exposes the version. - Verify: The application prints the database version at startup and a test can stub it - [ ] **What happens to a local time that does not exist or happens twice** · free - Why: Twice a year a clock change makes some wall clock times impossible and others ambiguous, and picking a rule in the debugger later means picking whatever the library happened to do. - Get it: Decide and write down both rules before implementing the scheduler. - Verify: docs/time-rules.md states both and each has a test ## Quick start ```sh npm run backup npm run restore -- --from=backups/latest.db ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Cloud sync between devices. - Two-way calendar synchronisation. - Shared lists and assignment. - A mobile application. - mobile capture, widgets, family features, calendar integration, and sync - perfect mobile sync - calendar and email integrations - shared workspaces - behavioral coaching polish If one of those is essential to you, that is the reason to keep paying for Any.do, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Any.do The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a task list whose reminders fire at the right local time years from now to replace Any.do. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: A desktop application with a global capture shortcut and native notifications · capture that needs a window to be open is capture that does not happen - Storage: SQLite beside the application, one file, backed up on a schedule · a task list that loses an entry is not a task list - Time: Wall clock plus an IANA zone identifier, with instants derived at scheduling time · a future instant computed today is a guess about rules that have not been made yet - Recurrence: A rule on the series, occurrences computed on read, rows written only when something happens · materialising ten years of a daily task is ten years of rows to migrate - Delivery: A scheduler that records each fire and refuses to fire the same occurrence twice · waking a laptop should not produce forty notifications ### Data model (create this before Phase 1) - tasks(id, title, notes, project_id, priority, starts_on, due_on, created_at, completed_at, deleted_at) - schedules(task_id, rule_text, local_time, zone_id, starts_on, ends_on) · local_time and zone_id together, never a stored instant for a future occurrence - occurrences(task_id, occurrence_date, state, completed_at, skipped_at, overridden_json) · a row exists only once something happened to that date - reminders(id, task_id, occurrence_date, fire_at_utc, tzdb_version, state, fired_at) · the instant is derived at scheduling time and recorded with the database version that produced it - projects(id, name, position) and tags(task_id, tag) · the two groupings that earn their keep - audit(id, entity, entity_id, action, at, payload_json) · what undo reads, and what tells you where a task went - The only UTC instants here are on reminders inside the scheduling horizon, and every one of them records which time zone database version computed it. ### Phase 1 · Capture before you lose the thought Build: A global shortcut opens a single input over whatever you are doing. Parse a leading or trailing date phrase into a due date while keeping the original text visible and correctable. Save on enter, confirm briefly, and close. Every create, edit and delete writes an audit row so undo is real. Trap: A capture box that parses 'friday' out of the title and silently rewrites what you typed means the day you write 'call about friday delivery' the title is mangled and the task is due at the wrong time. Show the parsed date as a separate chip you can reject, and never remove text from the title without the user seeing it happen. Done when: The capture window opens from the global shortcut while another application has focus and saves on enter, timed by hand. A parsed date appears as a separate element that can be dismissed, leaving the original title intact, tested. Undo restores the previous state of any create, edit, complete or delete, tested for each. Closing the window with unsaved text keeps a draft rather than discarding it. Do not build yet: No scheduling, recurrence, reminders or views yet. ### Phase 2 · Time stored the way a person meant it Build: Store local time and an IANA zone identifier on the schedule. Derive the instant only when putting a reminder into the scheduling horizon, and record the time zone database version used. On startup, compare the runtime's database version against the recorded one and recompute every pending reminder when it differs. Handle a local time that does not exist or occurs twice with a stated rule. Trap: Storing 'weekdays at 08:00' as a UTC instant bakes in the offset rules that happened to exist on the day you saved it, and those rules change: release 2026d of the time zone database, published on 11 September 2026, recorded that Canada's Northwest Territories moved to permanent minus six from 21 August 2026. Every future reminder stored in that zone as an instant was then wrong by an hour, and nobody finds out until somebody is late. Store the wall clock and the zone, derive the instant late, and recompute when the database version changes. Done when: No future reminder is stored as an instant beyond the configured scheduling horizon, asserted by querying the table. Changing the runtime's time zone database version causes every pending reminder to be recomputed, tested by stubbing the version. A local time that does not exist on a clock change day resolves by the documented rule and is covered by a test. A local time that occurs twice fires once, with the chosen occurrence documented and tested. Do not build yet: No delivery, recurrence editing or views yet. ### Phase 3 · Reminders that fire once Build: A scheduler that reads the next due reminders from storage rather than holding timers for everything. Mark a reminder fired before showing it. On wake or startup, deliver at most one catch-up notification summarising what was missed beyond a stated grace period, rather than one per reminder. Record every fire with its time. Trap: Computing missed reminders after a laptop has been shut for three days and firing them all produces forty notifications in one second, which the person dismisses in one gesture, losing the two that mattered along with the thirty eight that did not. Fire anything inside the grace period normally, collapse everything older into a single summary, and mark them all delivered. Done when: A reminder is marked fired before the notification is shown, so a crash mid-delivery cannot repeat it, tested by crashing between the two. Waking after a long sleep produces at most one catch-up summary plus anything inside the grace period, tested by moving the clock. The scheduler holds timers only for the next horizon, verified by inspecting its state with several thousand tasks. Notifications work with the main window closed, tested. Do not build yet: No recurrence editing, views, export or backups yet. ### Phase 4 · A series, its occurrences, and its history Build: Compute future occurrences from the rule on read. Write a row only when an occurrence is completed, skipped or overridden. Editing offers this occurrence, this and following, or the whole series, and the second one ends the old rule and starts a new schedule. Completed occurrences are never recomputed when a rule changes. Trap: Implementing 'edit this and following' by changing the rule on the existing series silently rewrites every completed occurrence's expected date, and the history of a habit you have kept for a year becomes wrong in a way you cannot reconstruct. End the old schedule on the day before the change and create a new one, so the past keeps the rule it was actually kept under. Done when: Completing one occurrence of a daily task leaves the rest of the series untouched and the next occurrence appears, tested. Editing this and following ends the old schedule and creates a new one, with completed occurrences keeping their original dates, tested. Skipping is distinguishable from completing everywhere it appears, including history. A rule change never alters a row that already has a completed_at value, asserted in the suite. Do not build yet: No export, backup, archive or deletion yet. ### Phase 5 · Backups, export and a day view that ends Build: Automatic local backups on a schedule with a retention count, and a restore exercised in the suite. A documented JSON export covering tasks, schedules, occurrences, reminders and audit rows, which reimports into an empty database. A today view capped at a stated number of items with the rest behind a deliberate action. An archive and a real delete that removes the rows. Trap: A today view that lists every overdue task stops being a plan after the first busy week and becomes a wall of failure people close and stop opening, which is how task applications die. Cap it, show the overdue count as a number with its own screen, and make moving something to today an action the person takes. Done when: Export and reimport into an empty database round-trip every table including audit rows, tested. Automatic backups run on the configured schedule with the stated retention and a restore is exercised in the suite rather than documented. The today view never shows more than the configured number of items and the overdue count is a separate figure, tested with a large fixture. Deleting a task removes its rows and its pending reminders together, tested. Do not build yet: Nothing after this. ### Out of scope (and why) - Cloud sync between devices, which is a conflict resolution problem rather than a storage one and is most of what the subscription actually pays for. - Two-way calendar synchronisation, which means implementing somebody else's event model and handling their conflicts as well as your own. - Shared lists and assignment, since the moment two people edit the same task you need identity, permissions and merge rules. - A mobile application, which is where capture actually happens for most people and is a different build with a different review process. ### README must contain - That scheduled times are stored as a wall clock time plus a named zone, and that instants are derived only inside the scheduling horizon - That every derived instant records the time zone database version, and that a version change recomputes pending reminders - What happens to a reminder set for a local time that does not exist or that occurs twice on a clock change day - That a long sleep produces one catch-up summary rather than one notification per missed reminder - Where the database and backups live, how many are kept, and the exact restore command ===== AGENTS.md ===== # Agent instructions · Any.do indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A desktop application with a global capture shortcut and native notifications, SQLite beside the application, one file, backed up on a schedule, Wall clock plus an IANA zone identifier, with instants derived at scheduling time, A rule on the series, occurrences computed on read, rows written only when something happens, A scheduler that records each fire and refuses to fire the same occurrence twice. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A capture box that parses 'friday' out of the title and silently rewrites what you typed means the day you write 'call about friday delivery' the title is mangled and the task is due at the wrong time. Show the parsed date as a separate chip you can reject, and never remove text from the title without the user seeing it happen. - Storing 'weekdays at 08:00' as a UTC instant bakes in the offset rules that happened to exist on the day you saved it, and those rules change: release 2026d of the time zone database, published on 11 September 2026, recorded that Canada's Northwest Territories moved to permanent minus six from 21 August 2026. Every future reminder stored in that zone as an instant was then wrong by an hour, and nobody finds out until somebody is late. Store the wall clock and the zone, derive the instant late, and recompute when the database version changes. - Computing missed reminders after a laptop has been shut for three days and firing them all produces forty notifications in one second, which the person dismisses in one gesture, losing the two that mattered along with the thirty eight that did not. Fire anything inside the grace period normally, collapse everything older into a single summary, and mark them all delivered. - Implementing 'edit this and following' by changing the rule on the existing series silently rewrites every completed occurrence's expected date, and the history of a habit you have kept for a year becomes wrong in a way you cannot reconstruct. End the old schedule on the day before the change and create a new one, so the past keeps the rule it was actually kept under. ===== BUILD_PLAN.md ===== # Build plan · Any.do A desktop task manager whose hard problems are storing scheduled times so they still mean the same thing after a government changes the clocks, firing reminders exactly once across sleep and restarts, and letting a recurring series be edited without rewriting what already happened. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capture before you lose the thought A task goes in with one keystroke from anywhere, and nothing typed is ever lost. ### Steps 1. Make the shortcut work without the app focused The thought arrives while you are doing something else. Files: `src/capture/window.ts` 2. Show parsed dates as a chip you can reject Guessing silently is worse than not guessing. Files: `src/capture/parse.ts`, `tests/parse.test.ts` 3. Write an audit row for every change Undo built on an audit trail also answers where a task went. Files: `src/store/audit.ts` ### Done when - [ ] The shortcut captures without the app focused - [ ] Parsed dates are visible and rejectable - [ ] Undo restores every kind of change - [ ] An unsaved capture survives as a draft ### Watch out - A capture box that parses 'friday' out of the title and silently rewrites what you typed means the day you write 'call about friday delivery' the title is mangled and the task is due at the wrong time. Show the parsed date as a separate chip you can reject, and never remove text from the title without the user seeing it happen. ## Phase 2 · Time stored the way a person meant it A scheduled time is a wall clock time in a named zone, and instants are derived rather than stored. ### Steps 1. Store local time and zone, never a future instant The rules for that instant have not all been made yet. Files: `src/time/schedule.ts`, `tests/zone.test.ts` 2. Record the database version with every derived instant It is how you know which reminders need recomputing. Files: `src/time/derive.ts` 3. Write down the rule for missing and doubled local times Both happen twice a year and both need a decision, not an exception. Files: `docs/time-rules.md`, `tests/dst.test.ts` ### Done when - [ ] No future instants beyond the horizon are stored - [ ] A database version change recomputes pending reminders - [ ] Non-existent local times resolve by a tested rule - [ ] Doubled local times fire exactly once ### Watch out - Storing 'weekdays at 08:00' as a UTC instant bakes in the offset rules that happened to exist on the day you saved it, and those rules change: release 2026d of the time zone database, published on 11 September 2026, recorded that Canada's Northwest Territories moved to permanent minus six from 21 August 2026. Every future reminder stored in that zone as an instant was then wrong by an hour, and nobody finds out until somebody is late. Store the wall clock and the zone, derive the instant late, and recompute when the database version changes. ## Phase 3 · Reminders that fire once Notifications survive sleep, restarts and a closed window, and never arrive in a burst. ### Steps 1. Mark fired before showing, not after A crash between the two should lose a notification, not repeat it forever. Files: `src/reminders/deliver.ts` 2. Collapse old misses into one summary Forty notifications is the same as none. Files: `src/reminders/catchup.ts`, `tests/catchup.test.ts` 3. Hold timers only for the next horizon Otherwise a few thousand tasks become a few thousand timers. Files: `src/reminders/scheduler.ts` ### Done when - [ ] Fired is recorded before the notification is shown - [ ] A long sleep produces one catch-up summary - [ ] Timers exist only for the next horizon - [ ] Reminders fire with the window closed ### Watch out - Computing missed reminders after a laptop has been shut for three days and firing them all produces forty notifications in one second, which the person dismisses in one gesture, losing the two that mattered along with the thirty eight that did not. Fire anything inside the grace period normally, collapse everything older into a single summary, and mark them all delivered. ## Phase 4 · A series, its occurrences, and its history Completing, skipping or editing one occurrence never rewrites the rest of the series or the past. ### Steps 1. Compute occurrences on read, write only on events Ten years of a daily task is a computation, not ten years of rows. Files: `src/recurrence/expand.ts`, `tests/expand.test.ts` 2. Split the series on this and following The past was kept under the old rule and should stay that way. Files: `src/recurrence/split.ts` 3. Keep skipped and completed as different things A skipped day is information, not a gap. Files: `src/recurrence/state.ts` ### Done when - [ ] Completing one occurrence leaves the series intact - [ ] This and following splits rather than rewrites - [ ] Skipped and completed stay distinguishable - [ ] No rule change touches a completed occurrence ### Watch out - Implementing 'edit this and following' by changing the rule on the existing series silently rewrites every completed occurrence's expected date, and the history of a habit you have kept for a year becomes wrong in a way you cannot reconstruct. End the old schedule on the day before the change and create a new one, so the past keeps the rule it was actually kept under. ## Not in this build - Cloud sync between devices. - Two-way calendar synchronisation. - Shared lists and assignment. - A mobile application. ## After v1, if you want it - A read-only view served on your own network, so a phone can see today without building a mobile app - Import from a calendar file for fixed appointments, kept separate from tasks you actually control - A weekly review screen built from the audit table, showing what you completed, skipped and moved ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Everything. This is the file to back up. DATABASE_PATH=~/.local/share/tasks/tasks.db # Required. Automatic copies. Restore is tested against these. BACKUP_DIR=~/.local/share/tasks/backups # Required. How many to retain. A task database is small, so keep plenty. BACKUP_KEEP=14 # Required. How far ahead reminders are converted into instants. Beyond this, only wall clock and zone exist. SCHEDULING_HORIZON_HOURS=48 # Required. Misses newer than this fire normally; older ones collapse into one summary. CATCHUP_GRACE_MINUTES=30 # Required. The day view stops here. Overdue is a count with its own screen. TODAY_VIEW_LIMIT=12
You are building a lean indie version of Any.do. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Any.do · indie build A desktop task manager whose hard problems are storing scheduled times so they still mean the same thing after a government changes the clocks, firing reminders exactly once across sleep and restarts, and letting a recurring series be edited without rewriting what already happened. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A desktop application with a global capture shortcut and native notifications | capture that needs a window to be open is capture that does not happen | | Storage | SQLite beside the application, one file, backed up on a schedule | a task list that loses an entry is not a task list | | Time | Wall clock plus an IANA zone identifier, with instants derived at scheduling time | a future instant computed today is a guess about rules that have not been made yet | | Recurrence | A rule on the series, occurrences computed on read, rows written only when something happens | materialising ten years of a daily task is ten years of rows to migrate | | Delivery | A scheduler that records each fire and refuses to fire the same occurrence twice | waking a laptop should not produce forty notifications | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the test suite. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A desktop application shell with a global shortcut and native notifications** · free - Why: Both capture and delivery depend on working while the window is closed, which a web page cannot do dependably. - Get it: Any desktop shell for your stack that exposes a global shortcut and the system notification service. - Verify: A shortcut pressed in another application opens capture, and a notification appears with the window closed - [ ] **A time zone library backed by the IANA database, whose version you can read at runtime** · free - Why: The whole scheduling design depends on knowing which database version produced an instant, so a library that hides the version is unusable here. - Get it: Use your runtime's internationalisation support or a maintained library that exposes the version. - Verify: The application prints the database version at startup and a test can stub it - [ ] **What happens to a local time that does not exist or happens twice** · free - Why: Twice a year a clock change makes some wall clock times impossible and others ambiguous, and picking a rule in the debugger later means picking whatever the library happened to do. - Get it: Decide and write down both rules before implementing the scheduler. - Verify: docs/time-rules.md states both and each has a test ## Quick start ```sh npm run backup npm run restore -- --from=backups/latest.db ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Cloud sync between devices. - Two-way calendar synchronisation. - Shared lists and assignment. - A mobile application. - mobile capture, widgets, family features, calendar integration, and sync - perfect mobile sync - calendar and email integrations - shared workspaces - behavioral coaching polish If one of those is essential to you, that is the reason to keep paying for Any.do, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Any.do The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a task list whose reminders fire at the right local time years from now to replace Any.do. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: A desktop application with a global capture shortcut and native notifications · capture that needs a window to be open is capture that does not happen - Storage: SQLite beside the application, one file, backed up on a schedule · a task list that loses an entry is not a task list - Time: Wall clock plus an IANA zone identifier, with instants derived at scheduling time · a future instant computed today is a guess about rules that have not been made yet - Recurrence: A rule on the series, occurrences computed on read, rows written only when something happens · materialising ten years of a daily task is ten years of rows to migrate - Delivery: A scheduler that records each fire and refuses to fire the same occurrence twice · waking a laptop should not produce forty notifications ### Data model (create this before Phase 1) - tasks(id, title, notes, project_id, priority, starts_on, due_on, created_at, completed_at, deleted_at) - schedules(task_id, rule_text, local_time, zone_id, starts_on, ends_on) · local_time and zone_id together, never a stored instant for a future occurrence - occurrences(task_id, occurrence_date, state, completed_at, skipped_at, overridden_json) · a row exists only once something happened to that date - reminders(id, task_id, occurrence_date, fire_at_utc, tzdb_version, state, fired_at) · the instant is derived at scheduling time and recorded with the database version that produced it - projects(id, name, position) and tags(task_id, tag) · the two groupings that earn their keep - audit(id, entity, entity_id, action, at, payload_json) · what undo reads, and what tells you where a task went - The only UTC instants here are on reminders inside the scheduling horizon, and every one of them records which time zone database version computed it. ### Phase 1 · Capture before you lose the thought Build: A global shortcut opens a single input over whatever you are doing. Parse a leading or trailing date phrase into a due date while keeping the original text visible and correctable. Save on enter, confirm briefly, and close. Every create, edit and delete writes an audit row so undo is real. Trap: A capture box that parses 'friday' out of the title and silently rewrites what you typed means the day you write 'call about friday delivery' the title is mangled and the task is due at the wrong time. Show the parsed date as a separate chip you can reject, and never remove text from the title without the user seeing it happen. Done when: The capture window opens from the global shortcut while another application has focus and saves on enter, timed by hand. A parsed date appears as a separate element that can be dismissed, leaving the original title intact, tested. Undo restores the previous state of any create, edit, complete or delete, tested for each. Closing the window with unsaved text keeps a draft rather than discarding it. Do not build yet: No scheduling, recurrence, reminders or views yet. ### Phase 2 · Time stored the way a person meant it Build: Store local time and an IANA zone identifier on the schedule. Derive the instant only when putting a reminder into the scheduling horizon, and record the time zone database version used. On startup, compare the runtime's database version against the recorded one and recompute every pending reminder when it differs. Handle a local time that does not exist or occurs twice with a stated rule. Trap: Storing 'weekdays at 08:00' as a UTC instant bakes in the offset rules that happened to exist on the day you saved it, and those rules change: release 2026d of the time zone database, published on 11 September 2026, recorded that Canada's Northwest Territories moved to permanent minus six from 21 August 2026. Every future reminder stored in that zone as an instant was then wrong by an hour, and nobody finds out until somebody is late. Store the wall clock and the zone, derive the instant late, and recompute when the database version changes. Done when: No future reminder is stored as an instant beyond the configured scheduling horizon, asserted by querying the table. Changing the runtime's time zone database version causes every pending reminder to be recomputed, tested by stubbing the version. A local time that does not exist on a clock change day resolves by the documented rule and is covered by a test. A local time that occurs twice fires once, with the chosen occurrence documented and tested. Do not build yet: No delivery, recurrence editing or views yet. ### Phase 3 · Reminders that fire once Build: A scheduler that reads the next due reminders from storage rather than holding timers for everything. Mark a reminder fired before showing it. On wake or startup, deliver at most one catch-up notification summarising what was missed beyond a stated grace period, rather than one per reminder. Record every fire with its time. Trap: Computing missed reminders after a laptop has been shut for three days and firing them all produces forty notifications in one second, which the person dismisses in one gesture, losing the two that mattered along with the thirty eight that did not. Fire anything inside the grace period normally, collapse everything older into a single summary, and mark them all delivered. Done when: A reminder is marked fired before the notification is shown, so a crash mid-delivery cannot repeat it, tested by crashing between the two. Waking after a long sleep produces at most one catch-up summary plus anything inside the grace period, tested by moving the clock. The scheduler holds timers only for the next horizon, verified by inspecting its state with several thousand tasks. Notifications work with the main window closed, tested. Do not build yet: No recurrence editing, views, export or backups yet. ### Phase 4 · A series, its occurrences, and its history Build: Compute future occurrences from the rule on read. Write a row only when an occurrence is completed, skipped or overridden. Editing offers this occurrence, this and following, or the whole series, and the second one ends the old rule and starts a new schedule. Completed occurrences are never recomputed when a rule changes. Trap: Implementing 'edit this and following' by changing the rule on the existing series silently rewrites every completed occurrence's expected date, and the history of a habit you have kept for a year becomes wrong in a way you cannot reconstruct. End the old schedule on the day before the change and create a new one, so the past keeps the rule it was actually kept under. Done when: Completing one occurrence of a daily task leaves the rest of the series untouched and the next occurrence appears, tested. Editing this and following ends the old schedule and creates a new one, with completed occurrences keeping their original dates, tested. Skipping is distinguishable from completing everywhere it appears, including history. A rule change never alters a row that already has a completed_at value, asserted in the suite. Do not build yet: No export, backup, archive or deletion yet. ### Phase 5 · Backups, export and a day view that ends Build: Automatic local backups on a schedule with a retention count, and a restore exercised in the suite. A documented JSON export covering tasks, schedules, occurrences, reminders and audit rows, which reimports into an empty database. A today view capped at a stated number of items with the rest behind a deliberate action. An archive and a real delete that removes the rows. Trap: A today view that lists every overdue task stops being a plan after the first busy week and becomes a wall of failure people close and stop opening, which is how task applications die. Cap it, show the overdue count as a number with its own screen, and make moving something to today an action the person takes. Done when: Export and reimport into an empty database round-trip every table including audit rows, tested. Automatic backups run on the configured schedule with the stated retention and a restore is exercised in the suite rather than documented. The today view never shows more than the configured number of items and the overdue count is a separate figure, tested with a large fixture. Deleting a task removes its rows and its pending reminders together, tested. Do not build yet: Nothing after this. ### Out of scope (and why) - Cloud sync between devices, which is a conflict resolution problem rather than a storage one and is most of what the subscription actually pays for. - Two-way calendar synchronisation, which means implementing somebody else's event model and handling their conflicts as well as your own. - Shared lists and assignment, since the moment two people edit the same task you need identity, permissions and merge rules. - A mobile application, which is where capture actually happens for most people and is a different build with a different review process. ### README must contain - That scheduled times are stored as a wall clock time plus a named zone, and that instants are derived only inside the scheduling horizon - That every derived instant records the time zone database version, and that a version change recomputes pending reminders - What happens to a reminder set for a local time that does not exist or that occurs twice on a clock change day - That a long sleep produces one catch-up summary rather than one notification per missed reminder - Where the database and backups live, how many are kept, and the exact restore command ===== AGENTS.md ===== # Agent instructions · Any.do indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A desktop application with a global capture shortcut and native notifications, SQLite beside the application, one file, backed up on a schedule, Wall clock plus an IANA zone identifier, with instants derived at scheduling time, A rule on the series, occurrences computed on read, rows written only when something happens, A scheduler that records each fire and refuses to fire the same occurrence twice. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A capture box that parses 'friday' out of the title and silently rewrites what you typed means the day you write 'call about friday delivery' the title is mangled and the task is due at the wrong time. Show the parsed date as a separate chip you can reject, and never remove text from the title without the user seeing it happen. - Storing 'weekdays at 08:00' as a UTC instant bakes in the offset rules that happened to exist on the day you saved it, and those rules change: release 2026d of the time zone database, published on 11 September 2026, recorded that Canada's Northwest Territories moved to permanent minus six from 21 August 2026. Every future reminder stored in that zone as an instant was then wrong by an hour, and nobody finds out until somebody is late. Store the wall clock and the zone, derive the instant late, and recompute when the database version changes. - Computing missed reminders after a laptop has been shut for three days and firing them all produces forty notifications in one second, which the person dismisses in one gesture, losing the two that mattered along with the thirty eight that did not. Fire anything inside the grace period normally, collapse everything older into a single summary, and mark them all delivered. - Implementing 'edit this and following' by changing the rule on the existing series silently rewrites every completed occurrence's expected date, and the history of a habit you have kept for a year becomes wrong in a way you cannot reconstruct. End the old schedule on the day before the change and create a new one, so the past keeps the rule it was actually kept under. ===== BUILD_PLAN.md ===== # Build plan · Any.do A desktop task manager whose hard problems are storing scheduled times so they still mean the same thing after a government changes the clocks, firing reminders exactly once across sleep and restarts, and letting a recurring series be edited without rewriting what already happened. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capture before you lose the thought A task goes in with one keystroke from anywhere, and nothing typed is ever lost. ### Steps 1. Make the shortcut work without the app focused The thought arrives while you are doing something else. Files: `src/capture/window.ts` 2. Show parsed dates as a chip you can reject Guessing silently is worse than not guessing. Files: `src/capture/parse.ts`, `tests/parse.test.ts` 3. Write an audit row for every change Undo built on an audit trail also answers where a task went. Files: `src/store/audit.ts` ### Done when - [ ] The shortcut captures without the app focused - [ ] Parsed dates are visible and rejectable - [ ] Undo restores every kind of change - [ ] An unsaved capture survives as a draft ### Watch out - A capture box that parses 'friday' out of the title and silently rewrites what you typed means the day you write 'call about friday delivery' the title is mangled and the task is due at the wrong time. Show the parsed date as a separate chip you can reject, and never remove text from the title without the user seeing it happen. ## Phase 2 · Time stored the way a person meant it A scheduled time is a wall clock time in a named zone, and instants are derived rather than stored. ### Steps 1. Store local time and zone, never a future instant The rules for that instant have not all been made yet. Files: `src/time/schedule.ts`, `tests/zone.test.ts` 2. Record the database version with every derived instant It is how you know which reminders need recomputing. Files: `src/time/derive.ts` 3. Write down the rule for missing and doubled local times Both happen twice a year and both need a decision, not an exception. Files: `docs/time-rules.md`, `tests/dst.test.ts` ### Done when - [ ] No future instants beyond the horizon are stored - [ ] A database version change recomputes pending reminders - [ ] Non-existent local times resolve by a tested rule - [ ] Doubled local times fire exactly once ### Watch out - Storing 'weekdays at 08:00' as a UTC instant bakes in the offset rules that happened to exist on the day you saved it, and those rules change: release 2026d of the time zone database, published on 11 September 2026, recorded that Canada's Northwest Territories moved to permanent minus six from 21 August 2026. Every future reminder stored in that zone as an instant was then wrong by an hour, and nobody finds out until somebody is late. Store the wall clock and the zone, derive the instant late, and recompute when the database version changes. ## Phase 3 · Reminders that fire once Notifications survive sleep, restarts and a closed window, and never arrive in a burst. ### Steps 1. Mark fired before showing, not after A crash between the two should lose a notification, not repeat it forever. Files: `src/reminders/deliver.ts` 2. Collapse old misses into one summary Forty notifications is the same as none. Files: `src/reminders/catchup.ts`, `tests/catchup.test.ts` 3. Hold timers only for the next horizon Otherwise a few thousand tasks become a few thousand timers. Files: `src/reminders/scheduler.ts` ### Done when - [ ] Fired is recorded before the notification is shown - [ ] A long sleep produces one catch-up summary - [ ] Timers exist only for the next horizon - [ ] Reminders fire with the window closed ### Watch out - Computing missed reminders after a laptop has been shut for three days and firing them all produces forty notifications in one second, which the person dismisses in one gesture, losing the two that mattered along with the thirty eight that did not. Fire anything inside the grace period normally, collapse everything older into a single summary, and mark them all delivered. ## Phase 4 · A series, its occurrences, and its history Completing, skipping or editing one occurrence never rewrites the rest of the series or the past. ### Steps 1. Compute occurrences on read, write only on events Ten years of a daily task is a computation, not ten years of rows. Files: `src/recurrence/expand.ts`, `tests/expand.test.ts` 2. Split the series on this and following The past was kept under the old rule and should stay that way. Files: `src/recurrence/split.ts` 3. Keep skipped and completed as different things A skipped day is information, not a gap. Files: `src/recurrence/state.ts` ### Done when - [ ] Completing one occurrence leaves the series intact - [ ] This and following splits rather than rewrites - [ ] Skipped and completed stay distinguishable - [ ] No rule change touches a completed occurrence ### Watch out - Implementing 'edit this and following' by changing the rule on the existing series silently rewrites every completed occurrence's expected date, and the history of a habit you have kept for a year becomes wrong in a way you cannot reconstruct. End the old schedule on the day before the change and create a new one, so the past keeps the rule it was actually kept under. ## Not in this build - Cloud sync between devices. - Two-way calendar synchronisation. - Shared lists and assignment. - A mobile application. ## After v1, if you want it - A read-only view served on your own network, so a phone can see today without building a mobile app - Import from a calendar file for fixed appointments, kept separate from tasks you actually control - A weekly review screen built from the audit table, showing what you completed, skipped and moved ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Everything. This is the file to back up. DATABASE_PATH=~/.local/share/tasks/tasks.db # Required. Automatic copies. Restore is tested against these. BACKUP_DIR=~/.local/share/tasks/backups # Required. How many to retain. A task database is small, so keep plenty. BACKUP_KEEP=14 # Required. How far ahead reminders are converted into instants. Beyond this, only wall clock and zone exist. SCHEDULING_HORIZON_HOURS=48 # Required. Misses newer than this fire normally; older ones collapse into one summary. CATCHUP_GRACE_MINUTES=30 # Required. The day view stops here. Overdue is a count with its own screen. TODAY_VIEW_LIMIT=12
You are building a production product version of Any.do. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Any.do · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Any.do, manage personal tasks, lists, reminders, recurring items, and a daily planning flow. The hard boundary is mobile capture, widgets, family features, calendar integration, and sync, plus cross-device reliability and workflow polish. ## Product outcome A task list whose reminders still fire at the right local time after the rules change, which never shows the same reminder twice or forty at once, and whose history of a kept habit stays true when the rule changes. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS, Windows, or Linux - notification permission - local SQLite database ## Explicit non-goals for v1 - Cloud sync between devices. - Two-way calendar synchronisation. - Shared lists and assignment. - A mobile application. - mobile capture, widgets, family features, calendar integration, and sync - perfect mobile sync - calendar and email integrations - shared workspaces - behavioral coaching polish ## Success criteria - No future instants beyond the horizon are confirmed stored - A time zone database version change is confirmed to recompute pending reminders - A crash between marking fired and showing is confirmed not to repeat a notification - A long sleep is confirmed to produce one catch-up summary - Export and reimport are confirmed to round-trip every table ===== BRIEF.md ===== # Build brief · Any.do The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a task list whose reminders fire at the right local time years from now to replace Any.do. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: A desktop application with a global capture shortcut and native notifications · capture that needs a window to be open is capture that does not happen - Storage: SQLite beside the application, one file, backed up on a schedule · a task list that loses an entry is not a task list - Time: Wall clock plus an IANA zone identifier, with instants derived at scheduling time · a future instant computed today is a guess about rules that have not been made yet - Recurrence: A rule on the series, occurrences computed on read, rows written only when something happens · materialising ten years of a daily task is ten years of rows to migrate - Delivery: A scheduler that records each fire and refuses to fire the same occurrence twice · waking a laptop should not produce forty notifications ### Data model (create this before Phase 1) - tasks(id, title, notes, project_id, priority, starts_on, due_on, created_at, completed_at, deleted_at) - schedules(task_id, rule_text, local_time, zone_id, starts_on, ends_on) · local_time and zone_id together, never a stored instant for a future occurrence - occurrences(task_id, occurrence_date, state, completed_at, skipped_at, overridden_json) · a row exists only once something happened to that date - reminders(id, task_id, occurrence_date, fire_at_utc, tzdb_version, state, fired_at) · the instant is derived at scheduling time and recorded with the database version that produced it - projects(id, name, position) and tags(task_id, tag) · the two groupings that earn their keep - audit(id, entity, entity_id, action, at, payload_json) · what undo reads, and what tells you where a task went - The only UTC instants here are on reminders inside the scheduling horizon, and every one of them records which time zone database version computed it. ### Phase 1 · Capture before you lose the thought Build: A global shortcut opens a single input over whatever you are doing. Parse a leading or trailing date phrase into a due date while keeping the original text visible and correctable. Save on enter, confirm briefly, and close. Every create, edit and delete writes an audit row so undo is real. Trap: A capture box that parses 'friday' out of the title and silently rewrites what you typed means the day you write 'call about friday delivery' the title is mangled and the task is due at the wrong time. Show the parsed date as a separate chip you can reject, and never remove text from the title without the user seeing it happen. Done when: The capture window opens from the global shortcut while another application has focus and saves on enter, timed by hand. A parsed date appears as a separate element that can be dismissed, leaving the original title intact, tested. Undo restores the previous state of any create, edit, complete or delete, tested for each. Closing the window with unsaved text keeps a draft rather than discarding it. Do not build yet: No scheduling, recurrence, reminders or views yet. ### Phase 2 · Time stored the way a person meant it Build: Store local time and an IANA zone identifier on the schedule. Derive the instant only when putting a reminder into the scheduling horizon, and record the time zone database version used. On startup, compare the runtime's database version against the recorded one and recompute every pending reminder when it differs. Handle a local time that does not exist or occurs twice with a stated rule. Trap: Storing 'weekdays at 08:00' as a UTC instant bakes in the offset rules that happened to exist on the day you saved it, and those rules change: release 2026d of the time zone database, published on 11 September 2026, recorded that Canada's Northwest Territories moved to permanent minus six from 21 August 2026. Every future reminder stored in that zone as an instant was then wrong by an hour, and nobody finds out until somebody is late. Store the wall clock and the zone, derive the instant late, and recompute when the database version changes. Done when: No future reminder is stored as an instant beyond the configured scheduling horizon, asserted by querying the table. Changing the runtime's time zone database version causes every pending reminder to be recomputed, tested by stubbing the version. A local time that does not exist on a clock change day resolves by the documented rule and is covered by a test. A local time that occurs twice fires once, with the chosen occurrence documented and tested. Do not build yet: No delivery, recurrence editing or views yet. ### Phase 3 · Reminders that fire once Build: A scheduler that reads the next due reminders from storage rather than holding timers for everything. Mark a reminder fired before showing it. On wake or startup, deliver at most one catch-up notification summarising what was missed beyond a stated grace period, rather than one per reminder. Record every fire with its time. Trap: Computing missed reminders after a laptop has been shut for three days and firing them all produces forty notifications in one second, which the person dismisses in one gesture, losing the two that mattered along with the thirty eight that did not. Fire anything inside the grace period normally, collapse everything older into a single summary, and mark them all delivered. Done when: A reminder is marked fired before the notification is shown, so a crash mid-delivery cannot repeat it, tested by crashing between the two. Waking after a long sleep produces at most one catch-up summary plus anything inside the grace period, tested by moving the clock. The scheduler holds timers only for the next horizon, verified by inspecting its state with several thousand tasks. Notifications work with the main window closed, tested. Do not build yet: No recurrence editing, views, export or backups yet. ### Phase 4 · A series, its occurrences, and its history Build: Compute future occurrences from the rule on read. Write a row only when an occurrence is completed, skipped or overridden. Editing offers this occurrence, this and following, or the whole series, and the second one ends the old rule and starts a new schedule. Completed occurrences are never recomputed when a rule changes. Trap: Implementing 'edit this and following' by changing the rule on the existing series silently rewrites every completed occurrence's expected date, and the history of a habit you have kept for a year becomes wrong in a way you cannot reconstruct. End the old schedule on the day before the change and create a new one, so the past keeps the rule it was actually kept under. Done when: Completing one occurrence of a daily task leaves the rest of the series untouched and the next occurrence appears, tested. Editing this and following ends the old schedule and creates a new one, with completed occurrences keeping their original dates, tested. Skipping is distinguishable from completing everywhere it appears, including history. A rule change never alters a row that already has a completed_at value, asserted in the suite. Do not build yet: No export, backup, archive or deletion yet. ### Phase 5 · Backups, export and a day view that ends Build: Automatic local backups on a schedule with a retention count, and a restore exercised in the suite. A documented JSON export covering tasks, schedules, occurrences, reminders and audit rows, which reimports into an empty database. A today view capped at a stated number of items with the rest behind a deliberate action. An archive and a real delete that removes the rows. Trap: A today view that lists every overdue task stops being a plan after the first busy week and becomes a wall of failure people close and stop opening, which is how task applications die. Cap it, show the overdue count as a number with its own screen, and make moving something to today an action the person takes. Done when: Export and reimport into an empty database round-trip every table including audit rows, tested. Automatic backups run on the configured schedule with the stated retention and a restore is exercised in the suite rather than documented. The today view never shows more than the configured number of items and the overdue count is a separate figure, tested with a large fixture. Deleting a task removes its rows and its pending reminders together, tested. Do not build yet: Nothing after this. ### Out of scope (and why) - Cloud sync between devices, which is a conflict resolution problem rather than a storage one and is most of what the subscription actually pays for. - Two-way calendar synchronisation, which means implementing somebody else's event model and handling their conflicts as well as your own. - Shared lists and assignment, since the moment two people edit the same task you need identity, permissions and merge rules. - A mobile application, which is where capture actually happens for most people and is a different build with a different review process. ### README must contain - That scheduled times are stored as a wall clock time plus a named zone, and that instants are derived only inside the scheduling horizon - That every derived instant records the time zone database version, and that a version change recomputes pending reminders - What happens to a reminder set for a local time that does not exist or that occurs twice on a clock change day - That a long sleep produces one catch-up summary rather than one notification per missed reminder - Where the database and backups live, how many are kept, and the exact restore command ===== ARCHITECTURE.md ===== # Architecture · Any.do ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A desktop application with a global capture shortcut and native notifications | capture that needs a window to be open is capture that does not happen | | Storage | SQLite beside the application, one file, backed up on a schedule | a task list that loses an entry is not a task list | | Time | Wall clock plus an IANA zone identifier, with instants derived at scheduling time | a future instant computed today is a guess about rules that have not been made yet | | Recurrence | A rule on the series, occurrences computed on read, rows written only when something happens | materialising ten years of a daily task is ten years of rows to migrate | | Delivery | A scheduler that records each fire and refuses to fire the same occurrence twice | waking a laptop should not produce forty notifications | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | Global shortcut, parsing and drafts. | Any front end, provided parsing never silently rewrites the title. | | Time | Wall clock storage and late derivation. | Any library, provided the database version is readable. | | Scheduler | Horizon timers, firing and catch-up. | Any scheduler, provided fired is recorded before delivery. | | Recurrence | Expansion, splitting and occurrence state. | Any rule format, provided the past is never recomputed. | | Portability | Backups, export and deletion. | Any format, provided it round-trips into an empty database. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Everything. This is the file to back up. - `BACKUP_DIR` · required · Automatic copies. Restore is tested against these. - `BACKUP_KEEP` · required · How many to retain. A task database is small, so keep plenty. - `SCHEDULING_HORIZON_HOURS` · required · How far ahead reminders are converted into instants. Beyond this, only wall clock and zone exist. - `CATCHUP_GRACE_MINUTES` · required · Misses newer than this fire normally; older ones collapse into one summary. - `TODAY_VIEW_LIMIT` · required · The day view stops here. Overdue is a count with its own screen. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Any.do product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A desktop application with a global capture shortcut and native notifications, SQLite beside the application, one file, backed up on a schedule, Wall clock plus an IANA zone identifier, with instants derived at scheduling time, A rule on the series, occurrences computed on read, rows written only when something happens, A scheduler that records each fire and refuses to fire the same occurrence twice. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - A capture box that parses 'friday' out of the title and silently rewrites what you typed means the day you write 'call about friday delivery' the title is mangled and the task is due at the wrong time. Show the parsed date as a separate chip you can reject, and never remove text from the title without the user seeing it happen. - Storing 'weekdays at 08:00' as a UTC instant bakes in the offset rules that happened to exist on the day you saved it, and those rules change: release 2026d of the time zone database, published on 11 September 2026, recorded that Canada's Northwest Territories moved to permanent minus six from 21 August 2026. Every future reminder stored in that zone as an instant was then wrong by an hour, and nobody finds out until somebody is late. Store the wall clock and the zone, derive the instant late, and recompute when the database version changes. - Computing missed reminders after a laptop has been shut for three days and firing them all produces forty notifications in one second, which the person dismisses in one gesture, losing the two that mattered along with the thirty eight that did not. Fire anything inside the grace period normally, collapse everything older into a single summary, and mark them all delivered. - Implementing 'edit this and following' by changing the rule on the existing series silently rewrites every completed occurrence's expected date, and the history of a habit you have kept for a year becomes wrong in a way you cannot reconstruct. End the old schedule on the day before the change and create a new one, so the past keeps the rule it was actually kept under. - A today view that lists every overdue task stops being a plan after the first busy week and becomes a wall of failure people close and stop opening, which is how task applications die. Cap it, show the overdue count as a number with its own screen, and make moving something to today an action the person takes. ===== MILESTONES.md ===== # Delivery milestones · Any.do Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Capture before you lose the thought A task goes in with one keystroke from anywhere, and nothing typed is ever lost. ### Steps 1. Make the shortcut work without the app focused The thought arrives while you are doing something else. Files: `src/capture/window.ts` 2. Show parsed dates as a chip you can reject Guessing silently is worse than not guessing. Files: `src/capture/parse.ts`, `tests/parse.test.ts` 3. Write an audit row for every change Undo built on an audit trail also answers where a task went. Files: `src/store/audit.ts` ### Done when - [ ] The shortcut captures without the app focused - [ ] Parsed dates are visible and rejectable - [ ] Undo restores every kind of change - [ ] An unsaved capture survives as a draft ### Watch out - A capture box that parses 'friday' out of the title and silently rewrites what you typed means the day you write 'call about friday delivery' the title is mangled and the task is due at the wrong time. Show the parsed date as a separate chip you can reject, and never remove text from the title without the user seeing it happen. ## M2 · Time stored the way a person meant it A scheduled time is a wall clock time in a named zone, and instants are derived rather than stored. ### Steps 1. Store local time and zone, never a future instant The rules for that instant have not all been made yet. Files: `src/time/schedule.ts`, `tests/zone.test.ts` 2. Record the database version with every derived instant It is how you know which reminders need recomputing. Files: `src/time/derive.ts` 3. Write down the rule for missing and doubled local times Both happen twice a year and both need a decision, not an exception. Files: `docs/time-rules.md`, `tests/dst.test.ts` ### Done when - [ ] No future instants beyond the horizon are stored - [ ] A database version change recomputes pending reminders - [ ] Non-existent local times resolve by a tested rule - [ ] Doubled local times fire exactly once ### Watch out - Storing 'weekdays at 08:00' as a UTC instant bakes in the offset rules that happened to exist on the day you saved it, and those rules change: release 2026d of the time zone database, published on 11 September 2026, recorded that Canada's Northwest Territories moved to permanent minus six from 21 August 2026. Every future reminder stored in that zone as an instant was then wrong by an hour, and nobody finds out until somebody is late. Store the wall clock and the zone, derive the instant late, and recompute when the database version changes. ## M3 · Reminders that fire once Notifications survive sleep, restarts and a closed window, and never arrive in a burst. ### Steps 1. Mark fired before showing, not after A crash between the two should lose a notification, not repeat it forever. Files: `src/reminders/deliver.ts` 2. Collapse old misses into one summary Forty notifications is the same as none. Files: `src/reminders/catchup.ts`, `tests/catchup.test.ts` 3. Hold timers only for the next horizon Otherwise a few thousand tasks become a few thousand timers. Files: `src/reminders/scheduler.ts` ### Done when - [ ] Fired is recorded before the notification is shown - [ ] A long sleep produces one catch-up summary - [ ] Timers exist only for the next horizon - [ ] Reminders fire with the window closed ### Watch out - Computing missed reminders after a laptop has been shut for three days and firing them all produces forty notifications in one second, which the person dismisses in one gesture, losing the two that mattered along with the thirty eight that did not. Fire anything inside the grace period normally, collapse everything older into a single summary, and mark them all delivered. ## M4 · A series, its occurrences, and its history Completing, skipping or editing one occurrence never rewrites the rest of the series or the past. ### Steps 1. Compute occurrences on read, write only on events Ten years of a daily task is a computation, not ten years of rows. Files: `src/recurrence/expand.ts`, `tests/expand.test.ts` 2. Split the series on this and following The past was kept under the old rule and should stay that way. Files: `src/recurrence/split.ts` 3. Keep skipped and completed as different things A skipped day is information, not a gap. Files: `src/recurrence/state.ts` ### Done when - [ ] Completing one occurrence leaves the series intact - [ ] This and following splits rather than rewrites - [ ] Skipped and completed stay distinguishable - [ ] No rule change touches a completed occurrence ### Watch out - Implementing 'edit this and following' by changing the rule on the existing series silently rewrites every completed occurrence's expected date, and the history of a habit you have kept for a year becomes wrong in a way you cannot reconstruct. End the old schedule on the day before the change and create a new one, so the past keeps the rule it was actually kept under. ## M5 · Backups, export and a day view that ends (production only) The data is portable and backed up, and the daily view is bounded rather than an infinite list of guilt. ### Steps 1. Automate backups and test the restore A backup nobody has restored is a hope. Files: `src/backup.ts`, `tests/restore.test.ts` ```sh npm run backup npm run restore -- --from=backups/latest.db ``` 2. Round-trip the export through an empty database That is what makes leaving this app possible. Files: `src/export/json.ts`, `tests/roundtrip.test.ts` 3. Cap the day view and move overdue behind a decision A list you cannot finish is a list you stop opening. Files: `src/views/today.ts` ### Done when - [ ] Export and reimport round-trip every table - [ ] Backups run automatically and restore is tested - [ ] The today view is capped with overdue separate - [ ] Deleting a task removes its pending reminders ### Watch out - A today view that lists every overdue task stops being a plan after the first busy week and becomes a wall of failure people close and stop opening, which is how task applications die. Cap it, show the overdue count as a number with its own screen, and make moving something to today an action the person takes. ===== OPERATIONS.md ===== # Operations · Any.do ## Backup One SQLite file, copied on a schedule with several generations kept. Small enough that keeping a fortnight of them costs nothing, and the restore path is exercised by the test suite rather than assumed. ## Restore Restore the file, start the application, and confirm the task count, the next reminder and that pending reminders recompute against the current time zone database version. A restore onto a machine with a newer database should show recomputation happening. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local application the checks live in the suite: no future instants beyond the horizon, no duplicate fires, catch-up collapsing, and no completed occurrence altered by a rule change. Watch the size of the audit table, which grows faster than the tasks do. ## Incident checklist The incident is a reminder that did not arrive or arrived at the wrong hour. The reminder rows carry the derived instant and the database version that produced it, which is exactly enough to tell a scheduler bug from a rules change. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No future instants beyond the horizon are confirmed stored - [ ] A time zone database version change is confirmed to recompute pending reminders - [ ] A crash between marking fired and showing is confirmed not to repeat a notification - [ ] A long sleep is confirmed to produce one catch-up summary - [ ] Export and reimport are confirmed to round-trip every table ## Launch constraint Do not market omitted Any.do capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Everything. This is the file to back up. DATABASE_PATH=~/.local/share/tasks/tasks.db # Required. Automatic copies. Restore is tested against these. BACKUP_DIR=~/.local/share/tasks/backups # Required. How many to retain. A task database is small, so keep plenty. BACKUP_KEEP=14 # Required. How far ahead reminders are converted into instants. Beyond this, only wall clock and zone exist. SCHEDULING_HORIZON_HOURS=48 # Required. Misses newer than this fire normally; older ones collapse into one summary. CATCHUP_GRACE_MINUTES=30 # Required. The day view stops here. Overdue is a count with its own screen. TODAY_VIEW_LIMIT=12
# Any.do · indie build A desktop task manager whose hard problems are storing scheduled times so they still mean the same thing after a government changes the clocks, firing reminders exactly once across sleep and restarts, and letting a recurring series be edited without rewriting what already happened. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A desktop application with a global capture shortcut and native notifications | capture that needs a window to be open is capture that does not happen | | Storage | SQLite beside the application, one file, backed up on a schedule | a task list that loses an entry is not a task list | | Time | Wall clock plus an IANA zone identifier, with instants derived at scheduling time | a future instant computed today is a guess about rules that have not been made yet | | Recurrence | A rule on the series, occurrences computed on read, rows written only when something happens | materialising ten years of a daily task is ten years of rows to migrate | | Delivery | A scheduler that records each fire and refuses to fire the same occurrence twice | waking a laptop should not produce forty notifications | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the test suite. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A desktop application shell with a global shortcut and native notifications** · free - Why: Both capture and delivery depend on working while the window is closed, which a web page cannot do dependably. - Get it: Any desktop shell for your stack that exposes a global shortcut and the system notification service. - Verify: A shortcut pressed in another application opens capture, and a notification appears with the window closed - [ ] **A time zone library backed by the IANA database, whose version you can read at runtime** · free - Why: The whole scheduling design depends on knowing which database version produced an instant, so a library that hides the version is unusable here. - Get it: Use your runtime's internationalisation support or a maintained library that exposes the version. - Verify: The application prints the database version at startup and a test can stub it - [ ] **What happens to a local time that does not exist or happens twice** · free - Why: Twice a year a clock change makes some wall clock times impossible and others ambiguous, and picking a rule in the debugger later means picking whatever the library happened to do. - Get it: Decide and write down both rules before implementing the scheduler. - Verify: docs/time-rules.md states both and each has a test ## Quick start ```sh npm run backup npm run restore -- --from=backups/latest.db ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Cloud sync between devices. - Two-way calendar synchronisation. - Shared lists and assignment. - A mobile application. - mobile capture, widgets, family features, calendar integration, and sync - perfect mobile sync - calendar and email integrations - shared workspaces - behavioral coaching polish If one of those is essential to you, that is the reason to keep paying for Any.do, and the README should say so rather than pretend.
# Build brief · Any.do The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a task list whose reminders fire at the right local time years from now to replace Any.do. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: A desktop application with a global capture shortcut and native notifications · capture that needs a window to be open is capture that does not happen - Storage: SQLite beside the application, one file, backed up on a schedule · a task list that loses an entry is not a task list - Time: Wall clock plus an IANA zone identifier, with instants derived at scheduling time · a future instant computed today is a guess about rules that have not been made yet - Recurrence: A rule on the series, occurrences computed on read, rows written only when something happens · materialising ten years of a daily task is ten years of rows to migrate - Delivery: A scheduler that records each fire and refuses to fire the same occurrence twice · waking a laptop should not produce forty notifications ### Data model (create this before Phase 1) - tasks(id, title, notes, project_id, priority, starts_on, due_on, created_at, completed_at, deleted_at) - schedules(task_id, rule_text, local_time, zone_id, starts_on, ends_on) · local_time and zone_id together, never a stored instant for a future occurrence - occurrences(task_id, occurrence_date, state, completed_at, skipped_at, overridden_json) · a row exists only once something happened to that date - reminders(id, task_id, occurrence_date, fire_at_utc, tzdb_version, state, fired_at) · the instant is derived at scheduling time and recorded with the database version that produced it - projects(id, name, position) and tags(task_id, tag) · the two groupings that earn their keep - audit(id, entity, entity_id, action, at, payload_json) · what undo reads, and what tells you where a task went - The only UTC instants here are on reminders inside the scheduling horizon, and every one of them records which time zone database version computed it. ### Phase 1 · Capture before you lose the thought Build: A global shortcut opens a single input over whatever you are doing. Parse a leading or trailing date phrase into a due date while keeping the original text visible and correctable. Save on enter, confirm briefly, and close. Every create, edit and delete writes an audit row so undo is real. Trap: A capture box that parses 'friday' out of the title and silently rewrites what you typed means the day you write 'call about friday delivery' the title is mangled and the task is due at the wrong time. Show the parsed date as a separate chip you can reject, and never remove text from the title without the user seeing it happen. Done when: The capture window opens from the global shortcut while another application has focus and saves on enter, timed by hand. A parsed date appears as a separate element that can be dismissed, leaving the original title intact, tested. Undo restores the previous state of any create, edit, complete or delete, tested for each. Closing the window with unsaved text keeps a draft rather than discarding it. Do not build yet: No scheduling, recurrence, reminders or views yet. ### Phase 2 · Time stored the way a person meant it Build: Store local time and an IANA zone identifier on the schedule. Derive the instant only when putting a reminder into the scheduling horizon, and record the time zone database version used. On startup, compare the runtime's database version against the recorded one and recompute every pending reminder when it differs. Handle a local time that does not exist or occurs twice with a stated rule. Trap: Storing 'weekdays at 08:00' as a UTC instant bakes in the offset rules that happened to exist on the day you saved it, and those rules change: release 2026d of the time zone database, published on 11 September 2026, recorded that Canada's Northwest Territories moved to permanent minus six from 21 August 2026. Every future reminder stored in that zone as an instant was then wrong by an hour, and nobody finds out until somebody is late. Store the wall clock and the zone, derive the instant late, and recompute when the database version changes. Done when: No future reminder is stored as an instant beyond the configured scheduling horizon, asserted by querying the table. Changing the runtime's time zone database version causes every pending reminder to be recomputed, tested by stubbing the version. A local time that does not exist on a clock change day resolves by the documented rule and is covered by a test. A local time that occurs twice fires once, with the chosen occurrence documented and tested. Do not build yet: No delivery, recurrence editing or views yet. ### Phase 3 · Reminders that fire once Build: A scheduler that reads the next due reminders from storage rather than holding timers for everything. Mark a reminder fired before showing it. On wake or startup, deliver at most one catch-up notification summarising what was missed beyond a stated grace period, rather than one per reminder. Record every fire with its time. Trap: Computing missed reminders after a laptop has been shut for three days and firing them all produces forty notifications in one second, which the person dismisses in one gesture, losing the two that mattered along with the thirty eight that did not. Fire anything inside the grace period normally, collapse everything older into a single summary, and mark them all delivered. Done when: A reminder is marked fired before the notification is shown, so a crash mid-delivery cannot repeat it, tested by crashing between the two. Waking after a long sleep produces at most one catch-up summary plus anything inside the grace period, tested by moving the clock. The scheduler holds timers only for the next horizon, verified by inspecting its state with several thousand tasks. Notifications work with the main window closed, tested. Do not build yet: No recurrence editing, views, export or backups yet. ### Phase 4 · A series, its occurrences, and its history Build: Compute future occurrences from the rule on read. Write a row only when an occurrence is completed, skipped or overridden. Editing offers this occurrence, this and following, or the whole series, and the second one ends the old rule and starts a new schedule. Completed occurrences are never recomputed when a rule changes. Trap: Implementing 'edit this and following' by changing the rule on the existing series silently rewrites every completed occurrence's expected date, and the history of a habit you have kept for a year becomes wrong in a way you cannot reconstruct. End the old schedule on the day before the change and create a new one, so the past keeps the rule it was actually kept under. Done when: Completing one occurrence of a daily task leaves the rest of the series untouched and the next occurrence appears, tested. Editing this and following ends the old schedule and creates a new one, with completed occurrences keeping their original dates, tested. Skipping is distinguishable from completing everywhere it appears, including history. A rule change never alters a row that already has a completed_at value, asserted in the suite. Do not build yet: No export, backup, archive or deletion yet. ### Phase 5 · Backups, export and a day view that ends Build: Automatic local backups on a schedule with a retention count, and a restore exercised in the suite. A documented JSON export covering tasks, schedules, occurrences, reminders and audit rows, which reimports into an empty database. A today view capped at a stated number of items with the rest behind a deliberate action. An archive and a real delete that removes the rows. Trap: A today view that lists every overdue task stops being a plan after the first busy week and becomes a wall of failure people close and stop opening, which is how task applications die. Cap it, show the overdue count as a number with its own screen, and make moving something to today an action the person takes. Done when: Export and reimport into an empty database round-trip every table including audit rows, tested. Automatic backups run on the configured schedule with the stated retention and a restore is exercised in the suite rather than documented. The today view never shows more than the configured number of items and the overdue count is a separate figure, tested with a large fixture. Deleting a task removes its rows and its pending reminders together, tested. Do not build yet: Nothing after this. ### Out of scope (and why) - Cloud sync between devices, which is a conflict resolution problem rather than a storage one and is most of what the subscription actually pays for. - Two-way calendar synchronisation, which means implementing somebody else's event model and handling their conflicts as well as your own. - Shared lists and assignment, since the moment two people edit the same task you need identity, permissions and merge rules. - A mobile application, which is where capture actually happens for most people and is a different build with a different review process. ### README must contain - That scheduled times are stored as a wall clock time plus a named zone, and that instants are derived only inside the scheduling horizon - That every derived instant records the time zone database version, and that a version change recomputes pending reminders - What happens to a reminder set for a local time that does not exist or that occurs twice on a clock change day - That a long sleep produces one catch-up summary rather than one notification per missed reminder - Where the database and backups live, how many are kept, and the exact restore command
# Agent instructions · Any.do indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A desktop application with a global capture shortcut and native notifications, SQLite beside the application, one file, backed up on a schedule, Wall clock plus an IANA zone identifier, with instants derived at scheduling time, A rule on the series, occurrences computed on read, rows written only when something happens, A scheduler that records each fire and refuses to fire the same occurrence twice. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A capture box that parses 'friday' out of the title and silently rewrites what you typed means the day you write 'call about friday delivery' the title is mangled and the task is due at the wrong time. Show the parsed date as a separate chip you can reject, and never remove text from the title without the user seeing it happen. - Storing 'weekdays at 08:00' as a UTC instant bakes in the offset rules that happened to exist on the day you saved it, and those rules change: release 2026d of the time zone database, published on 11 September 2026, recorded that Canada's Northwest Territories moved to permanent minus six from 21 August 2026. Every future reminder stored in that zone as an instant was then wrong by an hour, and nobody finds out until somebody is late. Store the wall clock and the zone, derive the instant late, and recompute when the database version changes. - Computing missed reminders after a laptop has been shut for three days and firing them all produces forty notifications in one second, which the person dismisses in one gesture, losing the two that mattered along with the thirty eight that did not. Fire anything inside the grace period normally, collapse everything older into a single summary, and mark them all delivered. - Implementing 'edit this and following' by changing the rule on the existing series silently rewrites every completed occurrence's expected date, and the history of a habit you have kept for a year becomes wrong in a way you cannot reconstruct. End the old schedule on the day before the change and create a new one, so the past keeps the rule it was actually kept under.
# Build plan · Any.do A desktop task manager whose hard problems are storing scheduled times so they still mean the same thing after a government changes the clocks, firing reminders exactly once across sleep and restarts, and letting a recurring series be edited without rewriting what already happened. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capture before you lose the thought A task goes in with one keystroke from anywhere, and nothing typed is ever lost. ### Steps 1. Make the shortcut work without the app focused The thought arrives while you are doing something else. Files: `src/capture/window.ts` 2. Show parsed dates as a chip you can reject Guessing silently is worse than not guessing. Files: `src/capture/parse.ts`, `tests/parse.test.ts` 3. Write an audit row for every change Undo built on an audit trail also answers where a task went. Files: `src/store/audit.ts` ### Done when - [ ] The shortcut captures without the app focused - [ ] Parsed dates are visible and rejectable - [ ] Undo restores every kind of change - [ ] An unsaved capture survives as a draft ### Watch out - A capture box that parses 'friday' out of the title and silently rewrites what you typed means the day you write 'call about friday delivery' the title is mangled and the task is due at the wrong time. Show the parsed date as a separate chip you can reject, and never remove text from the title without the user seeing it happen. ## Phase 2 · Time stored the way a person meant it A scheduled time is a wall clock time in a named zone, and instants are derived rather than stored. ### Steps 1. Store local time and zone, never a future instant The rules for that instant have not all been made yet. Files: `src/time/schedule.ts`, `tests/zone.test.ts` 2. Record the database version with every derived instant It is how you know which reminders need recomputing. Files: `src/time/derive.ts` 3. Write down the rule for missing and doubled local times Both happen twice a year and both need a decision, not an exception. Files: `docs/time-rules.md`, `tests/dst.test.ts` ### Done when - [ ] No future instants beyond the horizon are stored - [ ] A database version change recomputes pending reminders - [ ] Non-existent local times resolve by a tested rule - [ ] Doubled local times fire exactly once ### Watch out - Storing 'weekdays at 08:00' as a UTC instant bakes in the offset rules that happened to exist on the day you saved it, and those rules change: release 2026d of the time zone database, published on 11 September 2026, recorded that Canada's Northwest Territories moved to permanent minus six from 21 August 2026. Every future reminder stored in that zone as an instant was then wrong by an hour, and nobody finds out until somebody is late. Store the wall clock and the zone, derive the instant late, and recompute when the database version changes. ## Phase 3 · Reminders that fire once Notifications survive sleep, restarts and a closed window, and never arrive in a burst. ### Steps 1. Mark fired before showing, not after A crash between the two should lose a notification, not repeat it forever. Files: `src/reminders/deliver.ts` 2. Collapse old misses into one summary Forty notifications is the same as none. Files: `src/reminders/catchup.ts`, `tests/catchup.test.ts` 3. Hold timers only for the next horizon Otherwise a few thousand tasks become a few thousand timers. Files: `src/reminders/scheduler.ts` ### Done when - [ ] Fired is recorded before the notification is shown - [ ] A long sleep produces one catch-up summary - [ ] Timers exist only for the next horizon - [ ] Reminders fire with the window closed ### Watch out - Computing missed reminders after a laptop has been shut for three days and firing them all produces forty notifications in one second, which the person dismisses in one gesture, losing the two that mattered along with the thirty eight that did not. Fire anything inside the grace period normally, collapse everything older into a single summary, and mark them all delivered. ## Phase 4 · A series, its occurrences, and its history Completing, skipping or editing one occurrence never rewrites the rest of the series or the past. ### Steps 1. Compute occurrences on read, write only on events Ten years of a daily task is a computation, not ten years of rows. Files: `src/recurrence/expand.ts`, `tests/expand.test.ts` 2. Split the series on this and following The past was kept under the old rule and should stay that way. Files: `src/recurrence/split.ts` 3. Keep skipped and completed as different things A skipped day is information, not a gap. Files: `src/recurrence/state.ts` ### Done when - [ ] Completing one occurrence leaves the series intact - [ ] This and following splits rather than rewrites - [ ] Skipped and completed stay distinguishable - [ ] No rule change touches a completed occurrence ### Watch out - Implementing 'edit this and following' by changing the rule on the existing series silently rewrites every completed occurrence's expected date, and the history of a habit you have kept for a year becomes wrong in a way you cannot reconstruct. End the old schedule on the day before the change and create a new one, so the past keeps the rule it was actually kept under. ## Not in this build - Cloud sync between devices. - Two-way calendar synchronisation. - Shared lists and assignment. - A mobile application. ## After v1, if you want it - A read-only view served on your own network, so a phone can see today without building a mobile app - Import from a calendar file for fixed appointments, kept separate from tasks you actually control - A weekly review screen built from the audit table, showing what you completed, skipped and moved
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Everything. This is the file to back up. DATABASE_PATH=~/.local/share/tasks/tasks.db # Required. Automatic copies. Restore is tested against these. BACKUP_DIR=~/.local/share/tasks/backups # Required. How many to retain. A task database is small, so keep plenty. BACKUP_KEEP=14 # Required. How far ahead reminders are converted into instants. Beyond this, only wall clock and zone exist. SCHEDULING_HORIZON_HOURS=48 # Required. Misses newer than this fire normally; older ones collapse into one summary. CATCHUP_GRACE_MINUTES=30 # Required. The day view stops here. Overdue is a count with its own screen. TODAY_VIEW_LIMIT=12
# Any.do · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Any.do, manage personal tasks, lists, reminders, recurring items, and a daily planning flow. The hard boundary is mobile capture, widgets, family features, calendar integration, and sync, plus cross-device reliability and workflow polish. ## Product outcome A task list whose reminders still fire at the right local time after the rules change, which never shows the same reminder twice or forty at once, and whose history of a kept habit stays true when the rule changes. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS, Windows, or Linux - notification permission - local SQLite database ## Explicit non-goals for v1 - Cloud sync between devices. - Two-way calendar synchronisation. - Shared lists and assignment. - A mobile application. - mobile capture, widgets, family features, calendar integration, and sync - perfect mobile sync - calendar and email integrations - shared workspaces - behavioral coaching polish ## Success criteria - No future instants beyond the horizon are confirmed stored - A time zone database version change is confirmed to recompute pending reminders - A crash between marking fired and showing is confirmed not to repeat a notification - A long sleep is confirmed to produce one catch-up summary - Export and reimport are confirmed to round-trip every table
# Build brief · Any.do The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a task list whose reminders fire at the right local time years from now to replace Any.do. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: A desktop application with a global capture shortcut and native notifications · capture that needs a window to be open is capture that does not happen - Storage: SQLite beside the application, one file, backed up on a schedule · a task list that loses an entry is not a task list - Time: Wall clock plus an IANA zone identifier, with instants derived at scheduling time · a future instant computed today is a guess about rules that have not been made yet - Recurrence: A rule on the series, occurrences computed on read, rows written only when something happens · materialising ten years of a daily task is ten years of rows to migrate - Delivery: A scheduler that records each fire and refuses to fire the same occurrence twice · waking a laptop should not produce forty notifications ### Data model (create this before Phase 1) - tasks(id, title, notes, project_id, priority, starts_on, due_on, created_at, completed_at, deleted_at) - schedules(task_id, rule_text, local_time, zone_id, starts_on, ends_on) · local_time and zone_id together, never a stored instant for a future occurrence - occurrences(task_id, occurrence_date, state, completed_at, skipped_at, overridden_json) · a row exists only once something happened to that date - reminders(id, task_id, occurrence_date, fire_at_utc, tzdb_version, state, fired_at) · the instant is derived at scheduling time and recorded with the database version that produced it - projects(id, name, position) and tags(task_id, tag) · the two groupings that earn their keep - audit(id, entity, entity_id, action, at, payload_json) · what undo reads, and what tells you where a task went - The only UTC instants here are on reminders inside the scheduling horizon, and every one of them records which time zone database version computed it. ### Phase 1 · Capture before you lose the thought Build: A global shortcut opens a single input over whatever you are doing. Parse a leading or trailing date phrase into a due date while keeping the original text visible and correctable. Save on enter, confirm briefly, and close. Every create, edit and delete writes an audit row so undo is real. Trap: A capture box that parses 'friday' out of the title and silently rewrites what you typed means the day you write 'call about friday delivery' the title is mangled and the task is due at the wrong time. Show the parsed date as a separate chip you can reject, and never remove text from the title without the user seeing it happen. Done when: The capture window opens from the global shortcut while another application has focus and saves on enter, timed by hand. A parsed date appears as a separate element that can be dismissed, leaving the original title intact, tested. Undo restores the previous state of any create, edit, complete or delete, tested for each. Closing the window with unsaved text keeps a draft rather than discarding it. Do not build yet: No scheduling, recurrence, reminders or views yet. ### Phase 2 · Time stored the way a person meant it Build: Store local time and an IANA zone identifier on the schedule. Derive the instant only when putting a reminder into the scheduling horizon, and record the time zone database version used. On startup, compare the runtime's database version against the recorded one and recompute every pending reminder when it differs. Handle a local time that does not exist or occurs twice with a stated rule. Trap: Storing 'weekdays at 08:00' as a UTC instant bakes in the offset rules that happened to exist on the day you saved it, and those rules change: release 2026d of the time zone database, published on 11 September 2026, recorded that Canada's Northwest Territories moved to permanent minus six from 21 August 2026. Every future reminder stored in that zone as an instant was then wrong by an hour, and nobody finds out until somebody is late. Store the wall clock and the zone, derive the instant late, and recompute when the database version changes. Done when: No future reminder is stored as an instant beyond the configured scheduling horizon, asserted by querying the table. Changing the runtime's time zone database version causes every pending reminder to be recomputed, tested by stubbing the version. A local time that does not exist on a clock change day resolves by the documented rule and is covered by a test. A local time that occurs twice fires once, with the chosen occurrence documented and tested. Do not build yet: No delivery, recurrence editing or views yet. ### Phase 3 · Reminders that fire once Build: A scheduler that reads the next due reminders from storage rather than holding timers for everything. Mark a reminder fired before showing it. On wake or startup, deliver at most one catch-up notification summarising what was missed beyond a stated grace period, rather than one per reminder. Record every fire with its time. Trap: Computing missed reminders after a laptop has been shut for three days and firing them all produces forty notifications in one second, which the person dismisses in one gesture, losing the two that mattered along with the thirty eight that did not. Fire anything inside the grace period normally, collapse everything older into a single summary, and mark them all delivered. Done when: A reminder is marked fired before the notification is shown, so a crash mid-delivery cannot repeat it, tested by crashing between the two. Waking after a long sleep produces at most one catch-up summary plus anything inside the grace period, tested by moving the clock. The scheduler holds timers only for the next horizon, verified by inspecting its state with several thousand tasks. Notifications work with the main window closed, tested. Do not build yet: No recurrence editing, views, export or backups yet. ### Phase 4 · A series, its occurrences, and its history Build: Compute future occurrences from the rule on read. Write a row only when an occurrence is completed, skipped or overridden. Editing offers this occurrence, this and following, or the whole series, and the second one ends the old rule and starts a new schedule. Completed occurrences are never recomputed when a rule changes. Trap: Implementing 'edit this and following' by changing the rule on the existing series silently rewrites every completed occurrence's expected date, and the history of a habit you have kept for a year becomes wrong in a way you cannot reconstruct. End the old schedule on the day before the change and create a new one, so the past keeps the rule it was actually kept under. Done when: Completing one occurrence of a daily task leaves the rest of the series untouched and the next occurrence appears, tested. Editing this and following ends the old schedule and creates a new one, with completed occurrences keeping their original dates, tested. Skipping is distinguishable from completing everywhere it appears, including history. A rule change never alters a row that already has a completed_at value, asserted in the suite. Do not build yet: No export, backup, archive or deletion yet. ### Phase 5 · Backups, export and a day view that ends Build: Automatic local backups on a schedule with a retention count, and a restore exercised in the suite. A documented JSON export covering tasks, schedules, occurrences, reminders and audit rows, which reimports into an empty database. A today view capped at a stated number of items with the rest behind a deliberate action. An archive and a real delete that removes the rows. Trap: A today view that lists every overdue task stops being a plan after the first busy week and becomes a wall of failure people close and stop opening, which is how task applications die. Cap it, show the overdue count as a number with its own screen, and make moving something to today an action the person takes. Done when: Export and reimport into an empty database round-trip every table including audit rows, tested. Automatic backups run on the configured schedule with the stated retention and a restore is exercised in the suite rather than documented. The today view never shows more than the configured number of items and the overdue count is a separate figure, tested with a large fixture. Deleting a task removes its rows and its pending reminders together, tested. Do not build yet: Nothing after this. ### Out of scope (and why) - Cloud sync between devices, which is a conflict resolution problem rather than a storage one and is most of what the subscription actually pays for. - Two-way calendar synchronisation, which means implementing somebody else's event model and handling their conflicts as well as your own. - Shared lists and assignment, since the moment two people edit the same task you need identity, permissions and merge rules. - A mobile application, which is where capture actually happens for most people and is a different build with a different review process. ### README must contain - That scheduled times are stored as a wall clock time plus a named zone, and that instants are derived only inside the scheduling horizon - That every derived instant records the time zone database version, and that a version change recomputes pending reminders - What happens to a reminder set for a local time that does not exist or that occurs twice on a clock change day - That a long sleep produces one catch-up summary rather than one notification per missed reminder - Where the database and backups live, how many are kept, and the exact restore command
# Architecture · Any.do ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A desktop application with a global capture shortcut and native notifications | capture that needs a window to be open is capture that does not happen | | Storage | SQLite beside the application, one file, backed up on a schedule | a task list that loses an entry is not a task list | | Time | Wall clock plus an IANA zone identifier, with instants derived at scheduling time | a future instant computed today is a guess about rules that have not been made yet | | Recurrence | A rule on the series, occurrences computed on read, rows written only when something happens | materialising ten years of a daily task is ten years of rows to migrate | | Delivery | A scheduler that records each fire and refuses to fire the same occurrence twice | waking a laptop should not produce forty notifications | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | Global shortcut, parsing and drafts. | Any front end, provided parsing never silently rewrites the title. | | Time | Wall clock storage and late derivation. | Any library, provided the database version is readable. | | Scheduler | Horizon timers, firing and catch-up. | Any scheduler, provided fired is recorded before delivery. | | Recurrence | Expansion, splitting and occurrence state. | Any rule format, provided the past is never recomputed. | | Portability | Backups, export and deletion. | Any format, provided it round-trips into an empty database. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Everything. This is the file to back up. - `BACKUP_DIR` · required · Automatic copies. Restore is tested against these. - `BACKUP_KEEP` · required · How many to retain. A task database is small, so keep plenty. - `SCHEDULING_HORIZON_HOURS` · required · How far ahead reminders are converted into instants. Beyond this, only wall clock and zone exist. - `CATCHUP_GRACE_MINUTES` · required · Misses newer than this fire normally; older ones collapse into one summary. - `TODAY_VIEW_LIMIT` · required · The day view stops here. Overdue is a count with its own screen. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Any.do product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A desktop application with a global capture shortcut and native notifications, SQLite beside the application, one file, backed up on a schedule, Wall clock plus an IANA zone identifier, with instants derived at scheduling time, A rule on the series, occurrences computed on read, rows written only when something happens, A scheduler that records each fire and refuses to fire the same occurrence twice. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - A capture box that parses 'friday' out of the title and silently rewrites what you typed means the day you write 'call about friday delivery' the title is mangled and the task is due at the wrong time. Show the parsed date as a separate chip you can reject, and never remove text from the title without the user seeing it happen. - Storing 'weekdays at 08:00' as a UTC instant bakes in the offset rules that happened to exist on the day you saved it, and those rules change: release 2026d of the time zone database, published on 11 September 2026, recorded that Canada's Northwest Territories moved to permanent minus six from 21 August 2026. Every future reminder stored in that zone as an instant was then wrong by an hour, and nobody finds out until somebody is late. Store the wall clock and the zone, derive the instant late, and recompute when the database version changes. - Computing missed reminders after a laptop has been shut for three days and firing them all produces forty notifications in one second, which the person dismisses in one gesture, losing the two that mattered along with the thirty eight that did not. Fire anything inside the grace period normally, collapse everything older into a single summary, and mark them all delivered. - Implementing 'edit this and following' by changing the rule on the existing series silently rewrites every completed occurrence's expected date, and the history of a habit you have kept for a year becomes wrong in a way you cannot reconstruct. End the old schedule on the day before the change and create a new one, so the past keeps the rule it was actually kept under. - A today view that lists every overdue task stops being a plan after the first busy week and becomes a wall of failure people close and stop opening, which is how task applications die. Cap it, show the overdue count as a number with its own screen, and make moving something to today an action the person takes.
# Delivery milestones · Any.do Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Capture before you lose the thought A task goes in with one keystroke from anywhere, and nothing typed is ever lost. ### Steps 1. Make the shortcut work without the app focused The thought arrives while you are doing something else. Files: `src/capture/window.ts` 2. Show parsed dates as a chip you can reject Guessing silently is worse than not guessing. Files: `src/capture/parse.ts`, `tests/parse.test.ts` 3. Write an audit row for every change Undo built on an audit trail also answers where a task went. Files: `src/store/audit.ts` ### Done when - [ ] The shortcut captures without the app focused - [ ] Parsed dates are visible and rejectable - [ ] Undo restores every kind of change - [ ] An unsaved capture survives as a draft ### Watch out - A capture box that parses 'friday' out of the title and silently rewrites what you typed means the day you write 'call about friday delivery' the title is mangled and the task is due at the wrong time. Show the parsed date as a separate chip you can reject, and never remove text from the title without the user seeing it happen. ## M2 · Time stored the way a person meant it A scheduled time is a wall clock time in a named zone, and instants are derived rather than stored. ### Steps 1. Store local time and zone, never a future instant The rules for that instant have not all been made yet. Files: `src/time/schedule.ts`, `tests/zone.test.ts` 2. Record the database version with every derived instant It is how you know which reminders need recomputing. Files: `src/time/derive.ts` 3. Write down the rule for missing and doubled local times Both happen twice a year and both need a decision, not an exception. Files: `docs/time-rules.md`, `tests/dst.test.ts` ### Done when - [ ] No future instants beyond the horizon are stored - [ ] A database version change recomputes pending reminders - [ ] Non-existent local times resolve by a tested rule - [ ] Doubled local times fire exactly once ### Watch out - Storing 'weekdays at 08:00' as a UTC instant bakes in the offset rules that happened to exist on the day you saved it, and those rules change: release 2026d of the time zone database, published on 11 September 2026, recorded that Canada's Northwest Territories moved to permanent minus six from 21 August 2026. Every future reminder stored in that zone as an instant was then wrong by an hour, and nobody finds out until somebody is late. Store the wall clock and the zone, derive the instant late, and recompute when the database version changes. ## M3 · Reminders that fire once Notifications survive sleep, restarts and a closed window, and never arrive in a burst. ### Steps 1. Mark fired before showing, not after A crash between the two should lose a notification, not repeat it forever. Files: `src/reminders/deliver.ts` 2. Collapse old misses into one summary Forty notifications is the same as none. Files: `src/reminders/catchup.ts`, `tests/catchup.test.ts` 3. Hold timers only for the next horizon Otherwise a few thousand tasks become a few thousand timers. Files: `src/reminders/scheduler.ts` ### Done when - [ ] Fired is recorded before the notification is shown - [ ] A long sleep produces one catch-up summary - [ ] Timers exist only for the next horizon - [ ] Reminders fire with the window closed ### Watch out - Computing missed reminders after a laptop has been shut for three days and firing them all produces forty notifications in one second, which the person dismisses in one gesture, losing the two that mattered along with the thirty eight that did not. Fire anything inside the grace period normally, collapse everything older into a single summary, and mark them all delivered. ## M4 · A series, its occurrences, and its history Completing, skipping or editing one occurrence never rewrites the rest of the series or the past. ### Steps 1. Compute occurrences on read, write only on events Ten years of a daily task is a computation, not ten years of rows. Files: `src/recurrence/expand.ts`, `tests/expand.test.ts` 2. Split the series on this and following The past was kept under the old rule and should stay that way. Files: `src/recurrence/split.ts` 3. Keep skipped and completed as different things A skipped day is information, not a gap. Files: `src/recurrence/state.ts` ### Done when - [ ] Completing one occurrence leaves the series intact - [ ] This and following splits rather than rewrites - [ ] Skipped and completed stay distinguishable - [ ] No rule change touches a completed occurrence ### Watch out - Implementing 'edit this and following' by changing the rule on the existing series silently rewrites every completed occurrence's expected date, and the history of a habit you have kept for a year becomes wrong in a way you cannot reconstruct. End the old schedule on the day before the change and create a new one, so the past keeps the rule it was actually kept under. ## M5 · Backups, export and a day view that ends (production only) The data is portable and backed up, and the daily view is bounded rather than an infinite list of guilt. ### Steps 1. Automate backups and test the restore A backup nobody has restored is a hope. Files: `src/backup.ts`, `tests/restore.test.ts` ```sh npm run backup npm run restore -- --from=backups/latest.db ``` 2. Round-trip the export through an empty database That is what makes leaving this app possible. Files: `src/export/json.ts`, `tests/roundtrip.test.ts` 3. Cap the day view and move overdue behind a decision A list you cannot finish is a list you stop opening. Files: `src/views/today.ts` ### Done when - [ ] Export and reimport round-trip every table - [ ] Backups run automatically and restore is tested - [ ] The today view is capped with overdue separate - [ ] Deleting a task removes its pending reminders ### Watch out - A today view that lists every overdue task stops being a plan after the first busy week and becomes a wall of failure people close and stop opening, which is how task applications die. Cap it, show the overdue count as a number with its own screen, and make moving something to today an action the person takes.
# Operations · Any.do ## Backup One SQLite file, copied on a schedule with several generations kept. Small enough that keeping a fortnight of them costs nothing, and the restore path is exercised by the test suite rather than assumed. ## Restore Restore the file, start the application, and confirm the task count, the next reminder and that pending reminders recompute against the current time zone database version. A restore onto a machine with a newer database should show recomputation happening. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local application the checks live in the suite: no future instants beyond the horizon, no duplicate fires, catch-up collapsing, and no completed occurrence altered by a rule change. Watch the size of the audit table, which grows faster than the tasks do. ## Incident checklist The incident is a reminder that did not arrive or arrived at the wrong hour. The reminder rows carry the derived instant and the database version that produced it, which is exactly enough to tell a scheduler bug from a rules change. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No future instants beyond the horizon are confirmed stored - [ ] A time zone database version change is confirmed to recompute pending reminders - [ ] A crash between marking fired and showing is confirmed not to repeat a notification - [ ] A long sleep is confirmed to produce one catch-up summary - [ ] Export and reimport are confirmed to round-trip every table ## Launch constraint Do not market omitted Any.do capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Everything. This is the file to back up. DATABASE_PATH=~/.local/share/tasks/tasks.db # Required. Automatic copies. Restore is tested against these. BACKUP_DIR=~/.local/share/tasks/backups # Required. How many to retain. A task database is small, so keep plenty. BACKUP_KEEP=14 # Required. How far ahead reminders are converted into instants. Beyond this, only wall clock and zone exist. SCHEDULING_HORIZON_HOURS=48 # Required. Misses newer than this fire normally; older ones collapse into one summary. CATCHUP_GRACE_MINUTES=30 # Required. The day view stops here. Overdue is a count with its own screen. TODAY_VIEW_LIMIT=12
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Any.do because task software is sticky because trust, speed, and ubiquitous capture matter more than the database schema. The recurring cost buys recurrence edge cases, notifications, sync conflicts, calendar APIs, mobile capture, and backups, not just the visible interface.
xmobile capture, widgets, family features, calendar integration, and sync
xperfect mobile sync
xcalendar and email integrations
xshared workspaces
xbehavioral coaching polish
Don't feel like building it? These folks already made it free.
TTasks.orgA serious Android task list with recurrence and reminders; desktop and CalDAV sync are paidopen source↗all 4 free alternatives to Any.do →· no votes, no pay-to-list · just what's real
Any.do pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| personal | $0/user | $0/user | 1.5 MB attachment limit; 1 priority tag; 5 Any.do Moment sessions/month; no numeric task/list cap published. |
| premium | $7.99/user | $4.99/user | Unlimited recurring tasks, reminders, color tags, and Moment sessions; larger attachments. |
| family | $9.99 | $8.33 | Up to 4 family members; up to 4 shared family boards. |
| workspace | $7.99/user | $4.99/user | 1 to unlimited members; unlimited boards; 100+ workflow templates. |
free tier1.5 MB attachment limit; 1 priority tag; 5 Any.do Moment sessions/month; no numeric task/list cap published.
billingmonthly + annual; Family is flat for up to 4 members; Workspace is per member
hidden costsWorkspace bills each member; Family is capped at 4 members and 4 shared boards.
verified 2026-08-14 · source ↗
Vibecode Any.do
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Any.do replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Any.do cost?
Any.do costs about $7.99/month (Premium, checked 2026-07-31), which is $95.88 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Any.do?
Honestly: mobile capture, widgets, family features, calendar integration, and sync; perfect mobile sync; calendar and email integrations; shared workspaces; behavioral coaching polish. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Any.do?
Yes: Tasks.org (A serious Android task list with recurrence and reminders; desktop and CalDAV sync are paid) Vikunja (A capable Todoist replacement once you accept becoming its hosting department) Microsoft To Do (A polished free task manager, paid for with a Microsoft account rather than money) All 4 curated free alternatives are at vibecodeit.com/anydo/alternatives. The prompt is for when you want it exactly your way.