Vibecode Motion
track this build5 phases, 15 steps, beginner friendly0%You can build an auto-scheduler for your own tasks, but robust calendar conflict handling, integrations, and productized rescheduling take real engineering.
You are building a lean indie version of Motion. 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 ===== # Motion · indie build Tasks with duration, deadline and priority, availability read from your real calendar, a solver that places them into free time respecting working hours and deadlines, time blocks written back as calendar events, and a rescheduling pass that explains what moved and why when a meeting appears. Estimated effort: **multi-day**. 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 | TypeScript on Node 22 | the work is reading calendars, solving a small problem and writing back | | Calendar | CalDAV, or a provider API, behind one interface | availability that does not reflect your real calendar produces a plan you cannot follow | | Solver | A greedy placement with a deterministic tie-break, not a constraint library | at a few dozen tasks a good heuristic is instant and explainable, and explainability matters more than optimality here | | Storage | PostgreSQL | tasks, schedules and the history of what moved | | Time | A library with a full time zone database | working hours are local and a scheduler that ignores that books you at three in the morning | ## 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 solver and the service. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A real calendar account you are willing to write to** · free with an existing account - Why: Availability from a test calendar tells you nothing, and writing blocks into a real calendar is the whole product. Use one you actually live by. - Get it: Set up the connection with your provider's credentials or CalDAV. Create a dedicated calendar for scheduled blocks so they are visually separable and easy to clear. - Verify: Busy time reads correctly and a test event writes to the blocks calendar - [ ] **PostgreSQL 16 or newer** · free - Why: Holds tasks, plans and the history of what moved. - Get it: Install locally or run it in Docker. - Verify: psql connects and reports 16 or higher - [ ] **Two weeks of your real tasks with honest durations** · free - Why: The solver is only interesting against real work, and honest durations are what reveal whether the plan is achievable. Optimistic estimates produce a plan that fits and a week that does not. - Get it: Write down what you actually intend to do for two weeks, with a duration you would defend and a real deadline. - Verify: At least twenty tasks with durations and deadlines are entered - [ ] **A second calendar that constrains you** (optional) · free - Why: Product phase only. Most people have a second calendar containing meetings that block time, and ignoring it produces a plan that collides with reality. - Get it: Connect it read-only. It constrains placement without receiving blocks. - Verify: Its events appear as busy time ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multi-provider calendar sync at commercial reliability. - Recurring work rules and accumulated scheduling patterns. - Mobile applications. - Team scheduling across several people. - multi-calendar sync - recurring work rules - mobile apps - team scheduling - polished rescheduling - support If one of those is essential to you, that is the reason to keep paying for Motion, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Motion 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 scheduler that fits your tasks into the gaps in your calendar and rebuilds the plan when the day changes to replace Motion. 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: TypeScript on Node 22 · the work is reading calendars, solving a small problem and writing back - Calendar: CalDAV, or a provider API, behind one interface · availability that does not reflect your real calendar produces a plan you cannot follow - Solver: A greedy placement with a deterministic tie-break, not a constraint library · at a few dozen tasks a good heuristic is instant and explainable, and explainability matters more than optimality here - Storage: PostgreSQL · tasks, schedules and the history of what moved - Time: A library with a full time zone database · working hours are local and a scheduler that ignores that books you at three in the morning ### Data model (create this before Phase 1) - tasks(id, title, duration_minutes, deadline_at, priority, min_block_minutes, max_block_minutes, state, project_id) - dependencies(task_id, depends_on_task_id) · a task cannot be scheduled before what it needs - availability(id, weekday, start_local, end_local, kind) · kind is work, deep or personal, in a stated time zone - busy(id, source, starts_at_utc, ends_at_utc, external_id) · pulled from real calendars, never invented - blocks(id, task_id, starts_at_utc, ends_at_utc, external_event_id, generation) · what was written back - schedule_runs(id, at, generation, tasks_considered, tasks_placed, tasks_unplaceable, reason_json) - A generation number on every block is what lets a rebuild replace the previous plan cleanly instead of accumulating duplicates. ### Phase 1 · Real availability Build: Connect a real calendar and pull busy time. Define working hours per weekday with an explicit time zone. Compute free intervals by subtracting busy time from working hours. Do not schedule anything yet. Trap: Working hours are local wall clock and busy time is instants, so subtracting one from the other without converting produces free intervals that shift by an hour twice a year. Convert working hours to instants for the specific dates being scheduled, using the zone, rather than treating them as fixed offsets. Done when: Free intervals for tomorrow match what you would say by looking at your calendar. An all day event does not consume the whole working day unless you configured it to. Free intervals computed across a daylight saving transition keep working hours at the right local times. An event added to the calendar changes the free intervals after a refresh. Do not build yet: No tasks and no scheduling. Availability only. ### Phase 2 · A solver that explains itself Build: Sort by deadline then priority, place each task into the earliest free interval that fits, splitting into blocks between the minimum and maximum block size. When a task cannot be placed, record the specific reason rather than dropping it. Trap: A solver that silently omits what it cannot fit produces a plan that looks achievable and is not, and you discover the missing work at the deadline. Every unplaced task must appear with a reason: not enough free time before the deadline, blocked by a dependency, or a minimum block size larger than any available gap. Done when: Tasks fit into free intervals without overlapping each other or busy time, verified by checking every pair. A task with insufficient time before its deadline is reported as unplaceable with that reason rather than omitted. Running the solver twice on unchanged input produces an identical plan. A task with a dependency is never placed before it. Do not build yet: Nothing is written to the calendar yet. ### Phase 3 · Writing blocks back Build: Write each block as a calendar event tagged with its generation. On a rebuild, remove the previous generation's events and write the new one. Never touch an event this application did not create. Trap: Identifying your own events by title or by time will eventually delete a real meeting, and that is an unrecoverable mistake in someone's calendar. Tag every event you create with an identifier you own, and refuse to delete anything without that tag, even during cleanup. Done when: A plan appears as calendar events on the right days. Rebuilding replaces the previous generation and leaves no orphan blocks, verified by counting events before and after. An event created by hand in the same calendar is never modified or deleted. Deleting a block in the calendar is noticed on the next run rather than silently recreated in the same slot. Do not build yet: No rescheduling on change yet. ### Phase 4 · Rescheduling when the day changes Build: Detect calendar changes and rerun the solver. Prefer stability: a block that can stay where it is should stay. Produce a diff naming what moved, what was displaced past its deadline, and what became unplaceable. Trap: A solver rerun that reshuffles everything because one meeting appeared makes the plan feel random and untrustworthy, even when each individual placement is optimal. Weight stability explicitly: keep blocks that still fit, and move only what the change forced. Done when: Adding a two hour meeting moves only the blocks it actually displaced, verified by comparing plans before and after. The diff names what moved and what is now at risk. A task pushed past its deadline is reported prominently rather than silently rescheduled to after it. Rerunning with no calendar change produces no moves at all. Do not build yet: No reporting or shared calendars yet. ### Phase 5 · Being honest about the plan Build: Report total committed hours against available hours for the period. Flag over commitment before it becomes a missed deadline. Track how much scheduled work actually got done, and use the gap to show that estimates are optimistic rather than to nag. Trap: A scheduler that always produces a full plan implies the work is achievable, and the honest failure mode is telling someone that it is not. If committed work exceeds available time, say so at the top rather than placing tasks past their deadlines and calling it a schedule. Done when: A period with more committed work than available time shows an over-commitment warning naming the shortfall in hours. Completion tracking compares scheduled against actual and reports the ratio without judgement language. A week that fits shows no warning. The report distinguishes work that slipped from work that was never placed. Do not build yet: Sharing and operations come last. ### Phase 6 · Multiple calendars and running it continuously Build: Read busy time from several calendars including ones you do not write to. Run the solver on a schedule as a service. Alert when a calendar connection fails, because a scheduler working from stale availability is worse than one that stopped. Trap: A silently failed calendar connection means the solver schedules over meetings it can no longer see, and the plan looks normal while being wrong. Track last successful sync per calendar and refuse to write a plan built on availability older than a threshold. Done when: Busy time from a second calendar constrains placement. A failed connection produces an alert and the solver refuses to write a plan from stale availability, tested by breaking a connection deliberately. The service runs on a schedule and survives a restart. Each calendar's last sync time is visible. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-calendar sync across providers with the reliability the paid product manages, which is a long tail of provider quirks. - Recurring work rules and the scheduling patterns teams accumulate, which are where the commercial product's depth lives. - Mobile applications, which matter a great deal for a tool you consult during the day. - Team scheduling that coordinates several people's availability, which is a substantially harder problem than one person's. ### README must contain - That working hours are local wall clock and busy time is instants, and how the two are reconciled - That every unplaceable task is reported with a reason rather than dropped - That created events are tagged and nothing untagged is ever modified - That rescheduling prefers stability, so only displaced blocks move - That the solver refuses to plan from stale calendar data ===== AGENTS.md ===== # Agent instructions · Motion indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript on Node 22, CalDAV, or a provider API, behind one interface, A greedy placement with a deterministic tie-break, not a constraint library, PostgreSQL, A library with a full time zone database. 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 - Working hours are local wall clock and busy time is instants, so subtracting one from the other without converting produces free intervals that shift by an hour twice a year. Convert working hours to instants for the specific dates being scheduled, using the zone, rather than treating them as fixed offsets. - A solver that silently omits what it cannot fit produces a plan that looks achievable and is not, and you discover the missing work at the deadline. Every unplaced task must appear with a reason: not enough free time before the deadline, blocked by a dependency, or a minimum block size larger than any available gap. - Identifying your own events by title or by time will eventually delete a real meeting, and that is an unrecoverable mistake in someone's calendar. Tag every event you create with an identifier you own, and refuse to delete anything without that tag, even during cleanup. - A solver rerun that reshuffles everything because one meeting appeared makes the plan feel random and untrustworthy, even when each individual placement is optimal. Weight stability explicitly: keep blocks that still fit, and move only what the change forced. - A scheduler that always produces a full plan implies the work is achievable, and the honest failure mode is telling someone that it is not. If committed work exceeds available time, say so at the top rather than placing tasks past their deadlines and calling it a schedule. ===== BUILD_PLAN.md ===== # Build plan · Motion Tasks with duration, deadline and priority, availability read from your real calendar, a solver that places them into free time respecting working hours and deadlines, time blocks written back as calendar events, and a rescheduling pass that explains what moved and why when a meeting appears. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Real availability The application knows exactly when you are free, from your actual calendar. ### Steps 1. Pull busy time from a real calendar Behind one interface so the provider can change. Store instants with the source and external identifier. Files: `src/calendar.ts` 2. Define working hours with a zone Per weekday, local wall clock, with the zone stated explicitly rather than inherited from the server. Files: `src/availability.ts` 3. Subtract correctly across zones Convert working hours to instants per date. Test across a clock change. Files: `src/availability.test.ts` ### Done when - [ ] Free intervals for tomorrow match your calendar by inspection - [ ] An all day event does not consume the day unless configured to - [ ] Working hours hold their local times across a clock change - [ ] A new calendar event changes free intervals after refresh ### Watch out - Working hours are local wall clock and busy time is instants, so subtracting one from the other without converting produces free intervals that shift by an hour twice a year. Convert working hours to instants for the specific dates being scheduled, using the zone, rather than treating them as fixed offsets. ## Phase 2 · A solver that explains itself Tasks are placed into free time, and every task that was not placed says why. ### Steps 1. Sort and place greedily Deadline first, then priority, with a deterministic tie-break so runs are reproducible. Files: `src/solve.ts` 2. Split into blocks within bounds Respect minimum and maximum block size. A task needing two hours of unbroken focus cannot be four half hours. Files: `src/solve.ts` 3. Report every failure with a reason Unplaceable is an output, not an omission. This is the difference between a plan and a wish. Files: `src/solve.ts` ### Done when - [ ] No placed block overlaps another or busy time - [ ] An unplaceable task is reported with a specific reason - [ ] Two runs on unchanged input produce identical plans - [ ] A dependent task is never placed before its dependency ### Watch out - A solver that silently omits what it cannot fit produces a plan that looks achievable and is not, and you discover the missing work at the deadline. Every unplaced task must appear with a reason: not enough free time before the deadline, blocked by a dependency, or a minimum block size larger than any available gap. ## Phase 3 · Writing blocks back The plan appears in your calendar, and rebuilding it does not leave debris. ### Steps 1. Tag every created event An identifier in a property you own. Nothing untagged is ever touched. Files: `src/write.ts` 2. Replace by generation Delete the previous generation's tagged events, then write the new one. Count before and after. Files: `src/write.ts` 3. Notice manual deletions A block whose event is gone is a signal the user rejected it, not an error to correct. Files: `src/sync.ts` ### Done when - [ ] Blocks appear as events on the right days - [ ] A rebuild leaves no orphan blocks - [ ] Hand-created events are never modified - [ ] A manually deleted block is noticed rather than recreated ### Watch out - Identifying your own events by title or by time will eventually delete a real meeting, and that is an unrecoverable mistake in someone's calendar. Tag every event you create with an identifier you own, and refuse to delete anything without that tag, even during cleanup. ## Phase 4 · Rescheduling when the day changes A meeting appearing at short notice reshuffles the plan, and you can see what moved. ### Steps 1. Detect changes and rerun On a schedule or on a change notification. A stale plan is worse than no plan. Files: `src/watch.ts` 2. Weight stability Keep placements that still fit. Only what the change displaced may move. Files: `src/solve.ts` 3. Produce a readable diff Moved, at risk, and newly unplaceable. This is what the user actually reads after a rerun. Files: `src/diff.ts` ### Done when - [ ] A new meeting moves only what it displaced - [ ] The diff names moved and at-risk tasks - [ ] A task pushed past its deadline is reported prominently - [ ] An unchanged calendar produces no moves ### Watch out - A solver rerun that reshuffles everything because one meeting appeared makes the plan feel random and untrustworthy, even when each individual placement is optimal. Weight stability explicitly: keep blocks that still fit, and move only what the change forced. ## Phase 5 · Being honest about the plan The application tells you when the plan is not achievable rather than presenting a full calendar. ### Steps 1. Compare committed to available Total task duration against total free time for the horizon. One number, shown prominently. Files: `src/capacity.ts` 2. Track completion honestly Scheduled against actually done. Report the ratio as information about your estimates. Files: `src/completion.ts` 3. Keep the language neutral An estimate that was optimistic is a fact about estimating, not a failing. Files: `src/report.ts` ### Done when - [ ] Over-commitment shows a warning with the shortfall in hours - [ ] A week that fits shows no warning - [ ] Completion ratio is reported without judgement - [ ] Slipped work is distinguished from unplaced work ### Watch out - A scheduler that always produces a full plan implies the work is achievable, and the honest failure mode is telling someone that it is not. If committed work exceeds available time, say so at the top rather than placing tasks past their deadlines and calling it a schedule. ## Not in this build - Multi-provider calendar sync at commercial reliability. - Recurring work rules and accumulated scheduling patterns. - Mobile applications. - Team scheduling across several people. ## After v1, if you want it - Energy-aware placement, putting demanding work in the hours you actually do it well - Task splitting suggestions when a task is too large for any available gap - A morning summary showing today's plan and what is at risk ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Your PostgreSQL connection string. DATABASE_URL=postgres://user:pass@localhost:5432/schedule # Required. The calendar that receives blocks. A dedicated one, so clearing it is safe. BLOCKS_CALENDAR_ID=scheduled-work # Required. The property value marking events this application created. Nothing without it is ever modified. EVENT_TAG=autoplan # Required. The zone working hours are expressed in. Not the server's zone. WORKING_TIMEZONE=Europe/London # Required. Default minimum block. Shorter blocks fit more easily and are less useful for real work. MIN_BLOCK_MINUTES=30 # Required. How old calendar data may be before the solver refuses to write a plan. AVAILABILITY_STALE_MINUTES=60 # Required. How far ahead to schedule. Longer plans are more fragile because more will change. PLAN_HORIZON_DAYS=14
You are building a lean indie version of Motion. 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 ===== # Motion · indie build Tasks with duration, deadline and priority, availability read from your real calendar, a solver that places them into free time respecting working hours and deadlines, time blocks written back as calendar events, and a rescheduling pass that explains what moved and why when a meeting appears. Estimated effort: **multi-day**. 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 | TypeScript on Node 22 | the work is reading calendars, solving a small problem and writing back | | Calendar | CalDAV, or a provider API, behind one interface | availability that does not reflect your real calendar produces a plan you cannot follow | | Solver | A greedy placement with a deterministic tie-break, not a constraint library | at a few dozen tasks a good heuristic is instant and explainable, and explainability matters more than optimality here | | Storage | PostgreSQL | tasks, schedules and the history of what moved | | Time | A library with a full time zone database | working hours are local and a scheduler that ignores that books you at three in the morning | ## 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 solver and the service. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A real calendar account you are willing to write to** · free with an existing account - Why: Availability from a test calendar tells you nothing, and writing blocks into a real calendar is the whole product. Use one you actually live by. - Get it: Set up the connection with your provider's credentials or CalDAV. Create a dedicated calendar for scheduled blocks so they are visually separable and easy to clear. - Verify: Busy time reads correctly and a test event writes to the blocks calendar - [ ] **PostgreSQL 16 or newer** · free - Why: Holds tasks, plans and the history of what moved. - Get it: Install locally or run it in Docker. - Verify: psql connects and reports 16 or higher - [ ] **Two weeks of your real tasks with honest durations** · free - Why: The solver is only interesting against real work, and honest durations are what reveal whether the plan is achievable. Optimistic estimates produce a plan that fits and a week that does not. - Get it: Write down what you actually intend to do for two weeks, with a duration you would defend and a real deadline. - Verify: At least twenty tasks with durations and deadlines are entered - [ ] **A second calendar that constrains you** (optional) · free - Why: Product phase only. Most people have a second calendar containing meetings that block time, and ignoring it produces a plan that collides with reality. - Get it: Connect it read-only. It constrains placement without receiving blocks. - Verify: Its events appear as busy time ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multi-provider calendar sync at commercial reliability. - Recurring work rules and accumulated scheduling patterns. - Mobile applications. - Team scheduling across several people. - multi-calendar sync - recurring work rules - mobile apps - team scheduling - polished rescheduling - support If one of those is essential to you, that is the reason to keep paying for Motion, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Motion 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 scheduler that fits your tasks into the gaps in your calendar and rebuilds the plan when the day changes to replace Motion. 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: TypeScript on Node 22 · the work is reading calendars, solving a small problem and writing back - Calendar: CalDAV, or a provider API, behind one interface · availability that does not reflect your real calendar produces a plan you cannot follow - Solver: A greedy placement with a deterministic tie-break, not a constraint library · at a few dozen tasks a good heuristic is instant and explainable, and explainability matters more than optimality here - Storage: PostgreSQL · tasks, schedules and the history of what moved - Time: A library with a full time zone database · working hours are local and a scheduler that ignores that books you at three in the morning ### Data model (create this before Phase 1) - tasks(id, title, duration_minutes, deadline_at, priority, min_block_minutes, max_block_minutes, state, project_id) - dependencies(task_id, depends_on_task_id) · a task cannot be scheduled before what it needs - availability(id, weekday, start_local, end_local, kind) · kind is work, deep or personal, in a stated time zone - busy(id, source, starts_at_utc, ends_at_utc, external_id) · pulled from real calendars, never invented - blocks(id, task_id, starts_at_utc, ends_at_utc, external_event_id, generation) · what was written back - schedule_runs(id, at, generation, tasks_considered, tasks_placed, tasks_unplaceable, reason_json) - A generation number on every block is what lets a rebuild replace the previous plan cleanly instead of accumulating duplicates. ### Phase 1 · Real availability Build: Connect a real calendar and pull busy time. Define working hours per weekday with an explicit time zone. Compute free intervals by subtracting busy time from working hours. Do not schedule anything yet. Trap: Working hours are local wall clock and busy time is instants, so subtracting one from the other without converting produces free intervals that shift by an hour twice a year. Convert working hours to instants for the specific dates being scheduled, using the zone, rather than treating them as fixed offsets. Done when: Free intervals for tomorrow match what you would say by looking at your calendar. An all day event does not consume the whole working day unless you configured it to. Free intervals computed across a daylight saving transition keep working hours at the right local times. An event added to the calendar changes the free intervals after a refresh. Do not build yet: No tasks and no scheduling. Availability only. ### Phase 2 · A solver that explains itself Build: Sort by deadline then priority, place each task into the earliest free interval that fits, splitting into blocks between the minimum and maximum block size. When a task cannot be placed, record the specific reason rather than dropping it. Trap: A solver that silently omits what it cannot fit produces a plan that looks achievable and is not, and you discover the missing work at the deadline. Every unplaced task must appear with a reason: not enough free time before the deadline, blocked by a dependency, or a minimum block size larger than any available gap. Done when: Tasks fit into free intervals without overlapping each other or busy time, verified by checking every pair. A task with insufficient time before its deadline is reported as unplaceable with that reason rather than omitted. Running the solver twice on unchanged input produces an identical plan. A task with a dependency is never placed before it. Do not build yet: Nothing is written to the calendar yet. ### Phase 3 · Writing blocks back Build: Write each block as a calendar event tagged with its generation. On a rebuild, remove the previous generation's events and write the new one. Never touch an event this application did not create. Trap: Identifying your own events by title or by time will eventually delete a real meeting, and that is an unrecoverable mistake in someone's calendar. Tag every event you create with an identifier you own, and refuse to delete anything without that tag, even during cleanup. Done when: A plan appears as calendar events on the right days. Rebuilding replaces the previous generation and leaves no orphan blocks, verified by counting events before and after. An event created by hand in the same calendar is never modified or deleted. Deleting a block in the calendar is noticed on the next run rather than silently recreated in the same slot. Do not build yet: No rescheduling on change yet. ### Phase 4 · Rescheduling when the day changes Build: Detect calendar changes and rerun the solver. Prefer stability: a block that can stay where it is should stay. Produce a diff naming what moved, what was displaced past its deadline, and what became unplaceable. Trap: A solver rerun that reshuffles everything because one meeting appeared makes the plan feel random and untrustworthy, even when each individual placement is optimal. Weight stability explicitly: keep blocks that still fit, and move only what the change forced. Done when: Adding a two hour meeting moves only the blocks it actually displaced, verified by comparing plans before and after. The diff names what moved and what is now at risk. A task pushed past its deadline is reported prominently rather than silently rescheduled to after it. Rerunning with no calendar change produces no moves at all. Do not build yet: No reporting or shared calendars yet. ### Phase 5 · Being honest about the plan Build: Report total committed hours against available hours for the period. Flag over commitment before it becomes a missed deadline. Track how much scheduled work actually got done, and use the gap to show that estimates are optimistic rather than to nag. Trap: A scheduler that always produces a full plan implies the work is achievable, and the honest failure mode is telling someone that it is not. If committed work exceeds available time, say so at the top rather than placing tasks past their deadlines and calling it a schedule. Done when: A period with more committed work than available time shows an over-commitment warning naming the shortfall in hours. Completion tracking compares scheduled against actual and reports the ratio without judgement language. A week that fits shows no warning. The report distinguishes work that slipped from work that was never placed. Do not build yet: Sharing and operations come last. ### Phase 6 · Multiple calendars and running it continuously Build: Read busy time from several calendars including ones you do not write to. Run the solver on a schedule as a service. Alert when a calendar connection fails, because a scheduler working from stale availability is worse than one that stopped. Trap: A silently failed calendar connection means the solver schedules over meetings it can no longer see, and the plan looks normal while being wrong. Track last successful sync per calendar and refuse to write a plan built on availability older than a threshold. Done when: Busy time from a second calendar constrains placement. A failed connection produces an alert and the solver refuses to write a plan from stale availability, tested by breaking a connection deliberately. The service runs on a schedule and survives a restart. Each calendar's last sync time is visible. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-calendar sync across providers with the reliability the paid product manages, which is a long tail of provider quirks. - Recurring work rules and the scheduling patterns teams accumulate, which are where the commercial product's depth lives. - Mobile applications, which matter a great deal for a tool you consult during the day. - Team scheduling that coordinates several people's availability, which is a substantially harder problem than one person's. ### README must contain - That working hours are local wall clock and busy time is instants, and how the two are reconciled - That every unplaceable task is reported with a reason rather than dropped - That created events are tagged and nothing untagged is ever modified - That rescheduling prefers stability, so only displaced blocks move - That the solver refuses to plan from stale calendar data ===== AGENTS.md ===== # Agent instructions · Motion indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript on Node 22, CalDAV, or a provider API, behind one interface, A greedy placement with a deterministic tie-break, not a constraint library, PostgreSQL, A library with a full time zone database. 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 - Working hours are local wall clock and busy time is instants, so subtracting one from the other without converting produces free intervals that shift by an hour twice a year. Convert working hours to instants for the specific dates being scheduled, using the zone, rather than treating them as fixed offsets. - A solver that silently omits what it cannot fit produces a plan that looks achievable and is not, and you discover the missing work at the deadline. Every unplaced task must appear with a reason: not enough free time before the deadline, blocked by a dependency, or a minimum block size larger than any available gap. - Identifying your own events by title or by time will eventually delete a real meeting, and that is an unrecoverable mistake in someone's calendar. Tag every event you create with an identifier you own, and refuse to delete anything without that tag, even during cleanup. - A solver rerun that reshuffles everything because one meeting appeared makes the plan feel random and untrustworthy, even when each individual placement is optimal. Weight stability explicitly: keep blocks that still fit, and move only what the change forced. - A scheduler that always produces a full plan implies the work is achievable, and the honest failure mode is telling someone that it is not. If committed work exceeds available time, say so at the top rather than placing tasks past their deadlines and calling it a schedule. ===== BUILD_PLAN.md ===== # Build plan · Motion Tasks with duration, deadline and priority, availability read from your real calendar, a solver that places them into free time respecting working hours and deadlines, time blocks written back as calendar events, and a rescheduling pass that explains what moved and why when a meeting appears. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Real availability The application knows exactly when you are free, from your actual calendar. ### Steps 1. Pull busy time from a real calendar Behind one interface so the provider can change. Store instants with the source and external identifier. Files: `src/calendar.ts` 2. Define working hours with a zone Per weekday, local wall clock, with the zone stated explicitly rather than inherited from the server. Files: `src/availability.ts` 3. Subtract correctly across zones Convert working hours to instants per date. Test across a clock change. Files: `src/availability.test.ts` ### Done when - [ ] Free intervals for tomorrow match your calendar by inspection - [ ] An all day event does not consume the day unless configured to - [ ] Working hours hold their local times across a clock change - [ ] A new calendar event changes free intervals after refresh ### Watch out - Working hours are local wall clock and busy time is instants, so subtracting one from the other without converting produces free intervals that shift by an hour twice a year. Convert working hours to instants for the specific dates being scheduled, using the zone, rather than treating them as fixed offsets. ## Phase 2 · A solver that explains itself Tasks are placed into free time, and every task that was not placed says why. ### Steps 1. Sort and place greedily Deadline first, then priority, with a deterministic tie-break so runs are reproducible. Files: `src/solve.ts` 2. Split into blocks within bounds Respect minimum and maximum block size. A task needing two hours of unbroken focus cannot be four half hours. Files: `src/solve.ts` 3. Report every failure with a reason Unplaceable is an output, not an omission. This is the difference between a plan and a wish. Files: `src/solve.ts` ### Done when - [ ] No placed block overlaps another or busy time - [ ] An unplaceable task is reported with a specific reason - [ ] Two runs on unchanged input produce identical plans - [ ] A dependent task is never placed before its dependency ### Watch out - A solver that silently omits what it cannot fit produces a plan that looks achievable and is not, and you discover the missing work at the deadline. Every unplaced task must appear with a reason: not enough free time before the deadline, blocked by a dependency, or a minimum block size larger than any available gap. ## Phase 3 · Writing blocks back The plan appears in your calendar, and rebuilding it does not leave debris. ### Steps 1. Tag every created event An identifier in a property you own. Nothing untagged is ever touched. Files: `src/write.ts` 2. Replace by generation Delete the previous generation's tagged events, then write the new one. Count before and after. Files: `src/write.ts` 3. Notice manual deletions A block whose event is gone is a signal the user rejected it, not an error to correct. Files: `src/sync.ts` ### Done when - [ ] Blocks appear as events on the right days - [ ] A rebuild leaves no orphan blocks - [ ] Hand-created events are never modified - [ ] A manually deleted block is noticed rather than recreated ### Watch out - Identifying your own events by title or by time will eventually delete a real meeting, and that is an unrecoverable mistake in someone's calendar. Tag every event you create with an identifier you own, and refuse to delete anything without that tag, even during cleanup. ## Phase 4 · Rescheduling when the day changes A meeting appearing at short notice reshuffles the plan, and you can see what moved. ### Steps 1. Detect changes and rerun On a schedule or on a change notification. A stale plan is worse than no plan. Files: `src/watch.ts` 2. Weight stability Keep placements that still fit. Only what the change displaced may move. Files: `src/solve.ts` 3. Produce a readable diff Moved, at risk, and newly unplaceable. This is what the user actually reads after a rerun. Files: `src/diff.ts` ### Done when - [ ] A new meeting moves only what it displaced - [ ] The diff names moved and at-risk tasks - [ ] A task pushed past its deadline is reported prominently - [ ] An unchanged calendar produces no moves ### Watch out - A solver rerun that reshuffles everything because one meeting appeared makes the plan feel random and untrustworthy, even when each individual placement is optimal. Weight stability explicitly: keep blocks that still fit, and move only what the change forced. ## Phase 5 · Being honest about the plan The application tells you when the plan is not achievable rather than presenting a full calendar. ### Steps 1. Compare committed to available Total task duration against total free time for the horizon. One number, shown prominently. Files: `src/capacity.ts` 2. Track completion honestly Scheduled against actually done. Report the ratio as information about your estimates. Files: `src/completion.ts` 3. Keep the language neutral An estimate that was optimistic is a fact about estimating, not a failing. Files: `src/report.ts` ### Done when - [ ] Over-commitment shows a warning with the shortfall in hours - [ ] A week that fits shows no warning - [ ] Completion ratio is reported without judgement - [ ] Slipped work is distinguished from unplaced work ### Watch out - A scheduler that always produces a full plan implies the work is achievable, and the honest failure mode is telling someone that it is not. If committed work exceeds available time, say so at the top rather than placing tasks past their deadlines and calling it a schedule. ## Not in this build - Multi-provider calendar sync at commercial reliability. - Recurring work rules and accumulated scheduling patterns. - Mobile applications. - Team scheduling across several people. ## After v1, if you want it - Energy-aware placement, putting demanding work in the hours you actually do it well - Task splitting suggestions when a task is too large for any available gap - A morning summary showing today's plan and what is at risk ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Your PostgreSQL connection string. DATABASE_URL=postgres://user:pass@localhost:5432/schedule # Required. The calendar that receives blocks. A dedicated one, so clearing it is safe. BLOCKS_CALENDAR_ID=scheduled-work # Required. The property value marking events this application created. Nothing without it is ever modified. EVENT_TAG=autoplan # Required. The zone working hours are expressed in. Not the server's zone. WORKING_TIMEZONE=Europe/London # Required. Default minimum block. Shorter blocks fit more easily and are less useful for real work. MIN_BLOCK_MINUTES=30 # Required. How old calendar data may be before the solver refuses to write a plan. AVAILABILITY_STALE_MINUTES=60 # Required. How far ahead to schedule. Longer plans are more fragile because more will change. PLAN_HORIZON_DAYS=14
You are building a production product version of Motion. 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 ===== # Motion · product brief ## Problem You can build an auto-scheduler for your own tasks, but robust calendar conflict handling, integrations, and productized rescheduling take real engineering. ## Product outcome A scheduler whose availability reflects your real calendar, which never silently drops work it cannot fit, which only moves what a change actually displaced, and which refuses to plan when its view of your calendar is stale. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Google/Microsoft calendar OAuth - scheduling algorithm - hosted backend - notifications - task database ## Explicit non-goals for v1 - Multi-provider calendar sync at commercial reliability. - Recurring work rules and accumulated scheduling patterns. - Mobile applications. - Team scheduling across several people. - multi-calendar sync - recurring work rules - mobile apps - team scheduling - polished rescheduling - support ## Success criteria - No placed block overlaps busy time or another block - Every unplaceable task carries a reason - Untagged calendar events are confirmed never modified - A stale calendar connection is confirmed to block planning - A rerun with no change is confirmed to move nothing ===== BRIEF.md ===== # Build brief · Motion 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 scheduler that fits your tasks into the gaps in your calendar and rebuilds the plan when the day changes to replace Motion. 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: TypeScript on Node 22 · the work is reading calendars, solving a small problem and writing back - Calendar: CalDAV, or a provider API, behind one interface · availability that does not reflect your real calendar produces a plan you cannot follow - Solver: A greedy placement with a deterministic tie-break, not a constraint library · at a few dozen tasks a good heuristic is instant and explainable, and explainability matters more than optimality here - Storage: PostgreSQL · tasks, schedules and the history of what moved - Time: A library with a full time zone database · working hours are local and a scheduler that ignores that books you at three in the morning ### Data model (create this before Phase 1) - tasks(id, title, duration_minutes, deadline_at, priority, min_block_minutes, max_block_minutes, state, project_id) - dependencies(task_id, depends_on_task_id) · a task cannot be scheduled before what it needs - availability(id, weekday, start_local, end_local, kind) · kind is work, deep or personal, in a stated time zone - busy(id, source, starts_at_utc, ends_at_utc, external_id) · pulled from real calendars, never invented - blocks(id, task_id, starts_at_utc, ends_at_utc, external_event_id, generation) · what was written back - schedule_runs(id, at, generation, tasks_considered, tasks_placed, tasks_unplaceable, reason_json) - A generation number on every block is what lets a rebuild replace the previous plan cleanly instead of accumulating duplicates. ### Phase 1 · Real availability Build: Connect a real calendar and pull busy time. Define working hours per weekday with an explicit time zone. Compute free intervals by subtracting busy time from working hours. Do not schedule anything yet. Trap: Working hours are local wall clock and busy time is instants, so subtracting one from the other without converting produces free intervals that shift by an hour twice a year. Convert working hours to instants for the specific dates being scheduled, using the zone, rather than treating them as fixed offsets. Done when: Free intervals for tomorrow match what you would say by looking at your calendar. An all day event does not consume the whole working day unless you configured it to. Free intervals computed across a daylight saving transition keep working hours at the right local times. An event added to the calendar changes the free intervals after a refresh. Do not build yet: No tasks and no scheduling. Availability only. ### Phase 2 · A solver that explains itself Build: Sort by deadline then priority, place each task into the earliest free interval that fits, splitting into blocks between the minimum and maximum block size. When a task cannot be placed, record the specific reason rather than dropping it. Trap: A solver that silently omits what it cannot fit produces a plan that looks achievable and is not, and you discover the missing work at the deadline. Every unplaced task must appear with a reason: not enough free time before the deadline, blocked by a dependency, or a minimum block size larger than any available gap. Done when: Tasks fit into free intervals without overlapping each other or busy time, verified by checking every pair. A task with insufficient time before its deadline is reported as unplaceable with that reason rather than omitted. Running the solver twice on unchanged input produces an identical plan. A task with a dependency is never placed before it. Do not build yet: Nothing is written to the calendar yet. ### Phase 3 · Writing blocks back Build: Write each block as a calendar event tagged with its generation. On a rebuild, remove the previous generation's events and write the new one. Never touch an event this application did not create. Trap: Identifying your own events by title or by time will eventually delete a real meeting, and that is an unrecoverable mistake in someone's calendar. Tag every event you create with an identifier you own, and refuse to delete anything without that tag, even during cleanup. Done when: A plan appears as calendar events on the right days. Rebuilding replaces the previous generation and leaves no orphan blocks, verified by counting events before and after. An event created by hand in the same calendar is never modified or deleted. Deleting a block in the calendar is noticed on the next run rather than silently recreated in the same slot. Do not build yet: No rescheduling on change yet. ### Phase 4 · Rescheduling when the day changes Build: Detect calendar changes and rerun the solver. Prefer stability: a block that can stay where it is should stay. Produce a diff naming what moved, what was displaced past its deadline, and what became unplaceable. Trap: A solver rerun that reshuffles everything because one meeting appeared makes the plan feel random and untrustworthy, even when each individual placement is optimal. Weight stability explicitly: keep blocks that still fit, and move only what the change forced. Done when: Adding a two hour meeting moves only the blocks it actually displaced, verified by comparing plans before and after. The diff names what moved and what is now at risk. A task pushed past its deadline is reported prominently rather than silently rescheduled to after it. Rerunning with no calendar change produces no moves at all. Do not build yet: No reporting or shared calendars yet. ### Phase 5 · Being honest about the plan Build: Report total committed hours against available hours for the period. Flag over commitment before it becomes a missed deadline. Track how much scheduled work actually got done, and use the gap to show that estimates are optimistic rather than to nag. Trap: A scheduler that always produces a full plan implies the work is achievable, and the honest failure mode is telling someone that it is not. If committed work exceeds available time, say so at the top rather than placing tasks past their deadlines and calling it a schedule. Done when: A period with more committed work than available time shows an over-commitment warning naming the shortfall in hours. Completion tracking compares scheduled against actual and reports the ratio without judgement language. A week that fits shows no warning. The report distinguishes work that slipped from work that was never placed. Do not build yet: Sharing and operations come last. ### Phase 6 · Multiple calendars and running it continuously Build: Read busy time from several calendars including ones you do not write to. Run the solver on a schedule as a service. Alert when a calendar connection fails, because a scheduler working from stale availability is worse than one that stopped. Trap: A silently failed calendar connection means the solver schedules over meetings it can no longer see, and the plan looks normal while being wrong. Track last successful sync per calendar and refuse to write a plan built on availability older than a threshold. Done when: Busy time from a second calendar constrains placement. A failed connection produces an alert and the solver refuses to write a plan from stale availability, tested by breaking a connection deliberately. The service runs on a schedule and survives a restart. Each calendar's last sync time is visible. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-calendar sync across providers with the reliability the paid product manages, which is a long tail of provider quirks. - Recurring work rules and the scheduling patterns teams accumulate, which are where the commercial product's depth lives. - Mobile applications, which matter a great deal for a tool you consult during the day. - Team scheduling that coordinates several people's availability, which is a substantially harder problem than one person's. ### README must contain - That working hours are local wall clock and busy time is instants, and how the two are reconciled - That every unplaceable task is reported with a reason rather than dropped - That created events are tagged and nothing untagged is ever modified - That rescheduling prefers stability, so only displaced blocks move - That the solver refuses to plan from stale calendar data ===== ARCHITECTURE.md ===== # Architecture · Motion ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript on Node 22 | the work is reading calendars, solving a small problem and writing back | | Calendar | CalDAV, or a provider API, behind one interface | availability that does not reflect your real calendar produces a plan you cannot follow | | Solver | A greedy placement with a deterministic tie-break, not a constraint library | at a few dozen tasks a good heuristic is instant and explainable, and explainability matters more than optimality here | | Storage | PostgreSQL | tasks, schedules and the history of what moved | | Time | A library with a full time zone database | working hours are local and a scheduler that ignores that books you at three in the morning | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Calendar | Reading busy time and writing tagged blocks. | Providers differ; the tag-and-never-touch-untagged rule survives any change and prevents the one unrecoverable mistake. | | Availability | Working hours, zones and free intervals. | Pure computation, testable with no network. The zone handling is where correctness lives. | | Solver | Placement, splitting and unplaceable reasons. | A constraint library would optimise better and explain worse. At this size, explainability wins. | | Diff | Saying what moved and why. | Presentation, but it is what makes a rerun trustworthy rather than random. | | Capacity | Telling you the plan does not fit. | The honest output. A scheduler that always produces a full plan is hiding the answer. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Your PostgreSQL connection string. - `BLOCKS_CALENDAR_ID` · required · The calendar that receives blocks. A dedicated one, so clearing it is safe. - `EVENT_TAG` · required · The property value marking events this application created. Nothing without it is ever modified. - `WORKING_TIMEZONE` · required · The zone working hours are expressed in. Not the server's zone. - `MIN_BLOCK_MINUTES` · required · Default minimum block. Shorter blocks fit more easily and are less useful for real work. - `AVAILABILITY_STALE_MINUTES` · required · How old calendar data may be before the solver refuses to write a plan. - `PLAN_HORIZON_DAYS` · required · How far ahead to schedule. Longer plans are more fragile because more will change. ## 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 · Motion product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript on Node 22, CalDAV, or a provider API, behind one interface, A greedy placement with a deterministic tie-break, not a constraint library, PostgreSQL, A library with a full time zone database. - 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 - Working hours are local wall clock and busy time is instants, so subtracting one from the other without converting produces free intervals that shift by an hour twice a year. Convert working hours to instants for the specific dates being scheduled, using the zone, rather than treating them as fixed offsets. - A solver that silently omits what it cannot fit produces a plan that looks achievable and is not, and you discover the missing work at the deadline. Every unplaced task must appear with a reason: not enough free time before the deadline, blocked by a dependency, or a minimum block size larger than any available gap. - Identifying your own events by title or by time will eventually delete a real meeting, and that is an unrecoverable mistake in someone's calendar. Tag every event you create with an identifier you own, and refuse to delete anything without that tag, even during cleanup. - A solver rerun that reshuffles everything because one meeting appeared makes the plan feel random and untrustworthy, even when each individual placement is optimal. Weight stability explicitly: keep blocks that still fit, and move only what the change forced. - A scheduler that always produces a full plan implies the work is achievable, and the honest failure mode is telling someone that it is not. If committed work exceeds available time, say so at the top rather than placing tasks past their deadlines and calling it a schedule. - A silently failed calendar connection means the solver schedules over meetings it can no longer see, and the plan looks normal while being wrong. Track last successful sync per calendar and refuse to write a plan built on availability older than a threshold. ===== MILESTONES.md ===== # Delivery milestones · Motion Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Real availability The application knows exactly when you are free, from your actual calendar. ### Steps 1. Pull busy time from a real calendar Behind one interface so the provider can change. Store instants with the source and external identifier. Files: `src/calendar.ts` 2. Define working hours with a zone Per weekday, local wall clock, with the zone stated explicitly rather than inherited from the server. Files: `src/availability.ts` 3. Subtract correctly across zones Convert working hours to instants per date. Test across a clock change. Files: `src/availability.test.ts` ### Done when - [ ] Free intervals for tomorrow match your calendar by inspection - [ ] An all day event does not consume the day unless configured to - [ ] Working hours hold their local times across a clock change - [ ] A new calendar event changes free intervals after refresh ### Watch out - Working hours are local wall clock and busy time is instants, so subtracting one from the other without converting produces free intervals that shift by an hour twice a year. Convert working hours to instants for the specific dates being scheduled, using the zone, rather than treating them as fixed offsets. ## M2 · A solver that explains itself Tasks are placed into free time, and every task that was not placed says why. ### Steps 1. Sort and place greedily Deadline first, then priority, with a deterministic tie-break so runs are reproducible. Files: `src/solve.ts` 2. Split into blocks within bounds Respect minimum and maximum block size. A task needing two hours of unbroken focus cannot be four half hours. Files: `src/solve.ts` 3. Report every failure with a reason Unplaceable is an output, not an omission. This is the difference between a plan and a wish. Files: `src/solve.ts` ### Done when - [ ] No placed block overlaps another or busy time - [ ] An unplaceable task is reported with a specific reason - [ ] Two runs on unchanged input produce identical plans - [ ] A dependent task is never placed before its dependency ### Watch out - A solver that silently omits what it cannot fit produces a plan that looks achievable and is not, and you discover the missing work at the deadline. Every unplaced task must appear with a reason: not enough free time before the deadline, blocked by a dependency, or a minimum block size larger than any available gap. ## M3 · Writing blocks back The plan appears in your calendar, and rebuilding it does not leave debris. ### Steps 1. Tag every created event An identifier in a property you own. Nothing untagged is ever touched. Files: `src/write.ts` 2. Replace by generation Delete the previous generation's tagged events, then write the new one. Count before and after. Files: `src/write.ts` 3. Notice manual deletions A block whose event is gone is a signal the user rejected it, not an error to correct. Files: `src/sync.ts` ### Done when - [ ] Blocks appear as events on the right days - [ ] A rebuild leaves no orphan blocks - [ ] Hand-created events are never modified - [ ] A manually deleted block is noticed rather than recreated ### Watch out - Identifying your own events by title or by time will eventually delete a real meeting, and that is an unrecoverable mistake in someone's calendar. Tag every event you create with an identifier you own, and refuse to delete anything without that tag, even during cleanup. ## M4 · Rescheduling when the day changes A meeting appearing at short notice reshuffles the plan, and you can see what moved. ### Steps 1. Detect changes and rerun On a schedule or on a change notification. A stale plan is worse than no plan. Files: `src/watch.ts` 2. Weight stability Keep placements that still fit. Only what the change displaced may move. Files: `src/solve.ts` 3. Produce a readable diff Moved, at risk, and newly unplaceable. This is what the user actually reads after a rerun. Files: `src/diff.ts` ### Done when - [ ] A new meeting moves only what it displaced - [ ] The diff names moved and at-risk tasks - [ ] A task pushed past its deadline is reported prominently - [ ] An unchanged calendar produces no moves ### Watch out - A solver rerun that reshuffles everything because one meeting appeared makes the plan feel random and untrustworthy, even when each individual placement is optimal. Weight stability explicitly: keep blocks that still fit, and move only what the change forced. ## M5 · Being honest about the plan The application tells you when the plan is not achievable rather than presenting a full calendar. ### Steps 1. Compare committed to available Total task duration against total free time for the horizon. One number, shown prominently. Files: `src/capacity.ts` 2. Track completion honestly Scheduled against actually done. Report the ratio as information about your estimates. Files: `src/completion.ts` 3. Keep the language neutral An estimate that was optimistic is a fact about estimating, not a failing. Files: `src/report.ts` ### Done when - [ ] Over-commitment shows a warning with the shortfall in hours - [ ] A week that fits shows no warning - [ ] Completion ratio is reported without judgement - [ ] Slipped work is distinguished from unplaced work ### Watch out - A scheduler that always produces a full plan implies the work is achievable, and the honest failure mode is telling someone that it is not. If committed work exceeds available time, say so at the top rather than placing tasks past their deadlines and calling it a schedule. ## M6 · Multiple calendars and running it continuously (production only) It reads every calendar that constrains you, and it keeps running without supervision. ### Steps 1. Read several calendars Read-only ones count as constraints. Write only to the one you own. Files: `src/calendar.ts` 2. Refuse to plan on stale data A freshness threshold per calendar. Refusing is safer than scheduling over an invisible meeting. Files: `src/freshness.ts` 3. Run as a service with alerts Scheduled runs, restart survival, and an alert on connection failure. Files: `src/service.ts` ### Done when - [ ] A second calendar's events constrain placement - [ ] A broken connection alerts and blocks planning on stale data - [ ] The service survives a restart - [ ] Last sync time per calendar is visible ### Watch out - A silently failed calendar connection means the solver schedules over meetings it can no longer see, and the plan looks normal while being wrong. Track last successful sync per calendar and refuse to write a plan built on availability older than a threshold. ===== OPERATIONS.md ===== # Operations · Motion ## Backup Tasks and plan history are in the database. The blocks in your calendar are regenerable, which is why they carry a generation tag rather than being treated as data. ## Restore Restore the database, clear the blocks calendar, and rerun. Because blocks are tagged and disposable, a restore rebuilds the plan rather than reconciling it. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Calendar sync freshness per connection and the count of unplaceable tasks. A rising unplaceable count is the signal that commitments have outgrown available time, which is information rather than a fault. ## Incident checklist If blocks stop appearing, check calendar connection freshness first, since the solver is designed to refuse rather than write from stale data. If a real meeting was deleted, the tag check failed and that is the highest priority bug in the application. 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 placed block overlaps busy time or another block - [ ] Every unplaceable task carries a reason - [ ] Untagged calendar events are confirmed never modified - [ ] A stale calendar connection is confirmed to block planning - [ ] A rerun with no change is confirmed to move nothing ## Launch constraint Do not market omitted Motion 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 · secret. Your PostgreSQL connection string. DATABASE_URL=postgres://user:pass@localhost:5432/schedule # Required. The calendar that receives blocks. A dedicated one, so clearing it is safe. BLOCKS_CALENDAR_ID=scheduled-work # Required. The property value marking events this application created. Nothing without it is ever modified. EVENT_TAG=autoplan # Required. The zone working hours are expressed in. Not the server's zone. WORKING_TIMEZONE=Europe/London # Required. Default minimum block. Shorter blocks fit more easily and are less useful for real work. MIN_BLOCK_MINUTES=30 # Required. How old calendar data may be before the solver refuses to write a plan. AVAILABILITY_STALE_MINUTES=60 # Required. How far ahead to schedule. Longer plans are more fragile because more will change. PLAN_HORIZON_DAYS=14
# Motion · indie build Tasks with duration, deadline and priority, availability read from your real calendar, a solver that places them into free time respecting working hours and deadlines, time blocks written back as calendar events, and a rescheduling pass that explains what moved and why when a meeting appears. Estimated effort: **multi-day**. 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 | TypeScript on Node 22 | the work is reading calendars, solving a small problem and writing back | | Calendar | CalDAV, or a provider API, behind one interface | availability that does not reflect your real calendar produces a plan you cannot follow | | Solver | A greedy placement with a deterministic tie-break, not a constraint library | at a few dozen tasks a good heuristic is instant and explainable, and explainability matters more than optimality here | | Storage | PostgreSQL | tasks, schedules and the history of what moved | | Time | A library with a full time zone database | working hours are local and a scheduler that ignores that books you at three in the morning | ## 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 solver and the service. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A real calendar account you are willing to write to** · free with an existing account - Why: Availability from a test calendar tells you nothing, and writing blocks into a real calendar is the whole product. Use one you actually live by. - Get it: Set up the connection with your provider's credentials or CalDAV. Create a dedicated calendar for scheduled blocks so they are visually separable and easy to clear. - Verify: Busy time reads correctly and a test event writes to the blocks calendar - [ ] **PostgreSQL 16 or newer** · free - Why: Holds tasks, plans and the history of what moved. - Get it: Install locally or run it in Docker. - Verify: psql connects and reports 16 or higher - [ ] **Two weeks of your real tasks with honest durations** · free - Why: The solver is only interesting against real work, and honest durations are what reveal whether the plan is achievable. Optimistic estimates produce a plan that fits and a week that does not. - Get it: Write down what you actually intend to do for two weeks, with a duration you would defend and a real deadline. - Verify: At least twenty tasks with durations and deadlines are entered - [ ] **A second calendar that constrains you** (optional) · free - Why: Product phase only. Most people have a second calendar containing meetings that block time, and ignoring it produces a plan that collides with reality. - Get it: Connect it read-only. It constrains placement without receiving blocks. - Verify: Its events appear as busy time ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multi-provider calendar sync at commercial reliability. - Recurring work rules and accumulated scheduling patterns. - Mobile applications. - Team scheduling across several people. - multi-calendar sync - recurring work rules - mobile apps - team scheduling - polished rescheduling - support If one of those is essential to you, that is the reason to keep paying for Motion, and the README should say so rather than pretend.
# Build brief · Motion 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 scheduler that fits your tasks into the gaps in your calendar and rebuilds the plan when the day changes to replace Motion. 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: TypeScript on Node 22 · the work is reading calendars, solving a small problem and writing back - Calendar: CalDAV, or a provider API, behind one interface · availability that does not reflect your real calendar produces a plan you cannot follow - Solver: A greedy placement with a deterministic tie-break, not a constraint library · at a few dozen tasks a good heuristic is instant and explainable, and explainability matters more than optimality here - Storage: PostgreSQL · tasks, schedules and the history of what moved - Time: A library with a full time zone database · working hours are local and a scheduler that ignores that books you at three in the morning ### Data model (create this before Phase 1) - tasks(id, title, duration_minutes, deadline_at, priority, min_block_minutes, max_block_minutes, state, project_id) - dependencies(task_id, depends_on_task_id) · a task cannot be scheduled before what it needs - availability(id, weekday, start_local, end_local, kind) · kind is work, deep or personal, in a stated time zone - busy(id, source, starts_at_utc, ends_at_utc, external_id) · pulled from real calendars, never invented - blocks(id, task_id, starts_at_utc, ends_at_utc, external_event_id, generation) · what was written back - schedule_runs(id, at, generation, tasks_considered, tasks_placed, tasks_unplaceable, reason_json) - A generation number on every block is what lets a rebuild replace the previous plan cleanly instead of accumulating duplicates. ### Phase 1 · Real availability Build: Connect a real calendar and pull busy time. Define working hours per weekday with an explicit time zone. Compute free intervals by subtracting busy time from working hours. Do not schedule anything yet. Trap: Working hours are local wall clock and busy time is instants, so subtracting one from the other without converting produces free intervals that shift by an hour twice a year. Convert working hours to instants for the specific dates being scheduled, using the zone, rather than treating them as fixed offsets. Done when: Free intervals for tomorrow match what you would say by looking at your calendar. An all day event does not consume the whole working day unless you configured it to. Free intervals computed across a daylight saving transition keep working hours at the right local times. An event added to the calendar changes the free intervals after a refresh. Do not build yet: No tasks and no scheduling. Availability only. ### Phase 2 · A solver that explains itself Build: Sort by deadline then priority, place each task into the earliest free interval that fits, splitting into blocks between the minimum and maximum block size. When a task cannot be placed, record the specific reason rather than dropping it. Trap: A solver that silently omits what it cannot fit produces a plan that looks achievable and is not, and you discover the missing work at the deadline. Every unplaced task must appear with a reason: not enough free time before the deadline, blocked by a dependency, or a minimum block size larger than any available gap. Done when: Tasks fit into free intervals without overlapping each other or busy time, verified by checking every pair. A task with insufficient time before its deadline is reported as unplaceable with that reason rather than omitted. Running the solver twice on unchanged input produces an identical plan. A task with a dependency is never placed before it. Do not build yet: Nothing is written to the calendar yet. ### Phase 3 · Writing blocks back Build: Write each block as a calendar event tagged with its generation. On a rebuild, remove the previous generation's events and write the new one. Never touch an event this application did not create. Trap: Identifying your own events by title or by time will eventually delete a real meeting, and that is an unrecoverable mistake in someone's calendar. Tag every event you create with an identifier you own, and refuse to delete anything without that tag, even during cleanup. Done when: A plan appears as calendar events on the right days. Rebuilding replaces the previous generation and leaves no orphan blocks, verified by counting events before and after. An event created by hand in the same calendar is never modified or deleted. Deleting a block in the calendar is noticed on the next run rather than silently recreated in the same slot. Do not build yet: No rescheduling on change yet. ### Phase 4 · Rescheduling when the day changes Build: Detect calendar changes and rerun the solver. Prefer stability: a block that can stay where it is should stay. Produce a diff naming what moved, what was displaced past its deadline, and what became unplaceable. Trap: A solver rerun that reshuffles everything because one meeting appeared makes the plan feel random and untrustworthy, even when each individual placement is optimal. Weight stability explicitly: keep blocks that still fit, and move only what the change forced. Done when: Adding a two hour meeting moves only the blocks it actually displaced, verified by comparing plans before and after. The diff names what moved and what is now at risk. A task pushed past its deadline is reported prominently rather than silently rescheduled to after it. Rerunning with no calendar change produces no moves at all. Do not build yet: No reporting or shared calendars yet. ### Phase 5 · Being honest about the plan Build: Report total committed hours against available hours for the period. Flag over commitment before it becomes a missed deadline. Track how much scheduled work actually got done, and use the gap to show that estimates are optimistic rather than to nag. Trap: A scheduler that always produces a full plan implies the work is achievable, and the honest failure mode is telling someone that it is not. If committed work exceeds available time, say so at the top rather than placing tasks past their deadlines and calling it a schedule. Done when: A period with more committed work than available time shows an over-commitment warning naming the shortfall in hours. Completion tracking compares scheduled against actual and reports the ratio without judgement language. A week that fits shows no warning. The report distinguishes work that slipped from work that was never placed. Do not build yet: Sharing and operations come last. ### Phase 6 · Multiple calendars and running it continuously Build: Read busy time from several calendars including ones you do not write to. Run the solver on a schedule as a service. Alert when a calendar connection fails, because a scheduler working from stale availability is worse than one that stopped. Trap: A silently failed calendar connection means the solver schedules over meetings it can no longer see, and the plan looks normal while being wrong. Track last successful sync per calendar and refuse to write a plan built on availability older than a threshold. Done when: Busy time from a second calendar constrains placement. A failed connection produces an alert and the solver refuses to write a plan from stale availability, tested by breaking a connection deliberately. The service runs on a schedule and survives a restart. Each calendar's last sync time is visible. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-calendar sync across providers with the reliability the paid product manages, which is a long tail of provider quirks. - Recurring work rules and the scheduling patterns teams accumulate, which are where the commercial product's depth lives. - Mobile applications, which matter a great deal for a tool you consult during the day. - Team scheduling that coordinates several people's availability, which is a substantially harder problem than one person's. ### README must contain - That working hours are local wall clock and busy time is instants, and how the two are reconciled - That every unplaceable task is reported with a reason rather than dropped - That created events are tagged and nothing untagged is ever modified - That rescheduling prefers stability, so only displaced blocks move - That the solver refuses to plan from stale calendar data
# Agent instructions · Motion indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript on Node 22, CalDAV, or a provider API, behind one interface, A greedy placement with a deterministic tie-break, not a constraint library, PostgreSQL, A library with a full time zone database. 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 - Working hours are local wall clock and busy time is instants, so subtracting one from the other without converting produces free intervals that shift by an hour twice a year. Convert working hours to instants for the specific dates being scheduled, using the zone, rather than treating them as fixed offsets. - A solver that silently omits what it cannot fit produces a plan that looks achievable and is not, and you discover the missing work at the deadline. Every unplaced task must appear with a reason: not enough free time before the deadline, blocked by a dependency, or a minimum block size larger than any available gap. - Identifying your own events by title or by time will eventually delete a real meeting, and that is an unrecoverable mistake in someone's calendar. Tag every event you create with an identifier you own, and refuse to delete anything without that tag, even during cleanup. - A solver rerun that reshuffles everything because one meeting appeared makes the plan feel random and untrustworthy, even when each individual placement is optimal. Weight stability explicitly: keep blocks that still fit, and move only what the change forced. - A scheduler that always produces a full plan implies the work is achievable, and the honest failure mode is telling someone that it is not. If committed work exceeds available time, say so at the top rather than placing tasks past their deadlines and calling it a schedule.
# Build plan · Motion Tasks with duration, deadline and priority, availability read from your real calendar, a solver that places them into free time respecting working hours and deadlines, time blocks written back as calendar events, and a rescheduling pass that explains what moved and why when a meeting appears. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Real availability The application knows exactly when you are free, from your actual calendar. ### Steps 1. Pull busy time from a real calendar Behind one interface so the provider can change. Store instants with the source and external identifier. Files: `src/calendar.ts` 2. Define working hours with a zone Per weekday, local wall clock, with the zone stated explicitly rather than inherited from the server. Files: `src/availability.ts` 3. Subtract correctly across zones Convert working hours to instants per date. Test across a clock change. Files: `src/availability.test.ts` ### Done when - [ ] Free intervals for tomorrow match your calendar by inspection - [ ] An all day event does not consume the day unless configured to - [ ] Working hours hold their local times across a clock change - [ ] A new calendar event changes free intervals after refresh ### Watch out - Working hours are local wall clock and busy time is instants, so subtracting one from the other without converting produces free intervals that shift by an hour twice a year. Convert working hours to instants for the specific dates being scheduled, using the zone, rather than treating them as fixed offsets. ## Phase 2 · A solver that explains itself Tasks are placed into free time, and every task that was not placed says why. ### Steps 1. Sort and place greedily Deadline first, then priority, with a deterministic tie-break so runs are reproducible. Files: `src/solve.ts` 2. Split into blocks within bounds Respect minimum and maximum block size. A task needing two hours of unbroken focus cannot be four half hours. Files: `src/solve.ts` 3. Report every failure with a reason Unplaceable is an output, not an omission. This is the difference between a plan and a wish. Files: `src/solve.ts` ### Done when - [ ] No placed block overlaps another or busy time - [ ] An unplaceable task is reported with a specific reason - [ ] Two runs on unchanged input produce identical plans - [ ] A dependent task is never placed before its dependency ### Watch out - A solver that silently omits what it cannot fit produces a plan that looks achievable and is not, and you discover the missing work at the deadline. Every unplaced task must appear with a reason: not enough free time before the deadline, blocked by a dependency, or a minimum block size larger than any available gap. ## Phase 3 · Writing blocks back The plan appears in your calendar, and rebuilding it does not leave debris. ### Steps 1. Tag every created event An identifier in a property you own. Nothing untagged is ever touched. Files: `src/write.ts` 2. Replace by generation Delete the previous generation's tagged events, then write the new one. Count before and after. Files: `src/write.ts` 3. Notice manual deletions A block whose event is gone is a signal the user rejected it, not an error to correct. Files: `src/sync.ts` ### Done when - [ ] Blocks appear as events on the right days - [ ] A rebuild leaves no orphan blocks - [ ] Hand-created events are never modified - [ ] A manually deleted block is noticed rather than recreated ### Watch out - Identifying your own events by title or by time will eventually delete a real meeting, and that is an unrecoverable mistake in someone's calendar. Tag every event you create with an identifier you own, and refuse to delete anything without that tag, even during cleanup. ## Phase 4 · Rescheduling when the day changes A meeting appearing at short notice reshuffles the plan, and you can see what moved. ### Steps 1. Detect changes and rerun On a schedule or on a change notification. A stale plan is worse than no plan. Files: `src/watch.ts` 2. Weight stability Keep placements that still fit. Only what the change displaced may move. Files: `src/solve.ts` 3. Produce a readable diff Moved, at risk, and newly unplaceable. This is what the user actually reads after a rerun. Files: `src/diff.ts` ### Done when - [ ] A new meeting moves only what it displaced - [ ] The diff names moved and at-risk tasks - [ ] A task pushed past its deadline is reported prominently - [ ] An unchanged calendar produces no moves ### Watch out - A solver rerun that reshuffles everything because one meeting appeared makes the plan feel random and untrustworthy, even when each individual placement is optimal. Weight stability explicitly: keep blocks that still fit, and move only what the change forced. ## Phase 5 · Being honest about the plan The application tells you when the plan is not achievable rather than presenting a full calendar. ### Steps 1. Compare committed to available Total task duration against total free time for the horizon. One number, shown prominently. Files: `src/capacity.ts` 2. Track completion honestly Scheduled against actually done. Report the ratio as information about your estimates. Files: `src/completion.ts` 3. Keep the language neutral An estimate that was optimistic is a fact about estimating, not a failing. Files: `src/report.ts` ### Done when - [ ] Over-commitment shows a warning with the shortfall in hours - [ ] A week that fits shows no warning - [ ] Completion ratio is reported without judgement - [ ] Slipped work is distinguished from unplaced work ### Watch out - A scheduler that always produces a full plan implies the work is achievable, and the honest failure mode is telling someone that it is not. If committed work exceeds available time, say so at the top rather than placing tasks past their deadlines and calling it a schedule. ## Not in this build - Multi-provider calendar sync at commercial reliability. - Recurring work rules and accumulated scheduling patterns. - Mobile applications. - Team scheduling across several people. ## After v1, if you want it - Energy-aware placement, putting demanding work in the hours you actually do it well - Task splitting suggestions when a task is too large for any available gap - A morning summary showing today's plan and what is at risk
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Your PostgreSQL connection string. DATABASE_URL=postgres://user:pass@localhost:5432/schedule # Required. The calendar that receives blocks. A dedicated one, so clearing it is safe. BLOCKS_CALENDAR_ID=scheduled-work # Required. The property value marking events this application created. Nothing without it is ever modified. EVENT_TAG=autoplan # Required. The zone working hours are expressed in. Not the server's zone. WORKING_TIMEZONE=Europe/London # Required. Default minimum block. Shorter blocks fit more easily and are less useful for real work. MIN_BLOCK_MINUTES=30 # Required. How old calendar data may be before the solver refuses to write a plan. AVAILABILITY_STALE_MINUTES=60 # Required. How far ahead to schedule. Longer plans are more fragile because more will change. PLAN_HORIZON_DAYS=14
# Motion · product brief ## Problem You can build an auto-scheduler for your own tasks, but robust calendar conflict handling, integrations, and productized rescheduling take real engineering. ## Product outcome A scheduler whose availability reflects your real calendar, which never silently drops work it cannot fit, which only moves what a change actually displaced, and which refuses to plan when its view of your calendar is stale. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Google/Microsoft calendar OAuth - scheduling algorithm - hosted backend - notifications - task database ## Explicit non-goals for v1 - Multi-provider calendar sync at commercial reliability. - Recurring work rules and accumulated scheduling patterns. - Mobile applications. - Team scheduling across several people. - multi-calendar sync - recurring work rules - mobile apps - team scheduling - polished rescheduling - support ## Success criteria - No placed block overlaps busy time or another block - Every unplaceable task carries a reason - Untagged calendar events are confirmed never modified - A stale calendar connection is confirmed to block planning - A rerun with no change is confirmed to move nothing
# Build brief · Motion 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 scheduler that fits your tasks into the gaps in your calendar and rebuilds the plan when the day changes to replace Motion. 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: TypeScript on Node 22 · the work is reading calendars, solving a small problem and writing back - Calendar: CalDAV, or a provider API, behind one interface · availability that does not reflect your real calendar produces a plan you cannot follow - Solver: A greedy placement with a deterministic tie-break, not a constraint library · at a few dozen tasks a good heuristic is instant and explainable, and explainability matters more than optimality here - Storage: PostgreSQL · tasks, schedules and the history of what moved - Time: A library with a full time zone database · working hours are local and a scheduler that ignores that books you at three in the morning ### Data model (create this before Phase 1) - tasks(id, title, duration_minutes, deadline_at, priority, min_block_minutes, max_block_minutes, state, project_id) - dependencies(task_id, depends_on_task_id) · a task cannot be scheduled before what it needs - availability(id, weekday, start_local, end_local, kind) · kind is work, deep or personal, in a stated time zone - busy(id, source, starts_at_utc, ends_at_utc, external_id) · pulled from real calendars, never invented - blocks(id, task_id, starts_at_utc, ends_at_utc, external_event_id, generation) · what was written back - schedule_runs(id, at, generation, tasks_considered, tasks_placed, tasks_unplaceable, reason_json) - A generation number on every block is what lets a rebuild replace the previous plan cleanly instead of accumulating duplicates. ### Phase 1 · Real availability Build: Connect a real calendar and pull busy time. Define working hours per weekday with an explicit time zone. Compute free intervals by subtracting busy time from working hours. Do not schedule anything yet. Trap: Working hours are local wall clock and busy time is instants, so subtracting one from the other without converting produces free intervals that shift by an hour twice a year. Convert working hours to instants for the specific dates being scheduled, using the zone, rather than treating them as fixed offsets. Done when: Free intervals for tomorrow match what you would say by looking at your calendar. An all day event does not consume the whole working day unless you configured it to. Free intervals computed across a daylight saving transition keep working hours at the right local times. An event added to the calendar changes the free intervals after a refresh. Do not build yet: No tasks and no scheduling. Availability only. ### Phase 2 · A solver that explains itself Build: Sort by deadline then priority, place each task into the earliest free interval that fits, splitting into blocks between the minimum and maximum block size. When a task cannot be placed, record the specific reason rather than dropping it. Trap: A solver that silently omits what it cannot fit produces a plan that looks achievable and is not, and you discover the missing work at the deadline. Every unplaced task must appear with a reason: not enough free time before the deadline, blocked by a dependency, or a minimum block size larger than any available gap. Done when: Tasks fit into free intervals without overlapping each other or busy time, verified by checking every pair. A task with insufficient time before its deadline is reported as unplaceable with that reason rather than omitted. Running the solver twice on unchanged input produces an identical plan. A task with a dependency is never placed before it. Do not build yet: Nothing is written to the calendar yet. ### Phase 3 · Writing blocks back Build: Write each block as a calendar event tagged with its generation. On a rebuild, remove the previous generation's events and write the new one. Never touch an event this application did not create. Trap: Identifying your own events by title or by time will eventually delete a real meeting, and that is an unrecoverable mistake in someone's calendar. Tag every event you create with an identifier you own, and refuse to delete anything without that tag, even during cleanup. Done when: A plan appears as calendar events on the right days. Rebuilding replaces the previous generation and leaves no orphan blocks, verified by counting events before and after. An event created by hand in the same calendar is never modified or deleted. Deleting a block in the calendar is noticed on the next run rather than silently recreated in the same slot. Do not build yet: No rescheduling on change yet. ### Phase 4 · Rescheduling when the day changes Build: Detect calendar changes and rerun the solver. Prefer stability: a block that can stay where it is should stay. Produce a diff naming what moved, what was displaced past its deadline, and what became unplaceable. Trap: A solver rerun that reshuffles everything because one meeting appeared makes the plan feel random and untrustworthy, even when each individual placement is optimal. Weight stability explicitly: keep blocks that still fit, and move only what the change forced. Done when: Adding a two hour meeting moves only the blocks it actually displaced, verified by comparing plans before and after. The diff names what moved and what is now at risk. A task pushed past its deadline is reported prominently rather than silently rescheduled to after it. Rerunning with no calendar change produces no moves at all. Do not build yet: No reporting or shared calendars yet. ### Phase 5 · Being honest about the plan Build: Report total committed hours against available hours for the period. Flag over commitment before it becomes a missed deadline. Track how much scheduled work actually got done, and use the gap to show that estimates are optimistic rather than to nag. Trap: A scheduler that always produces a full plan implies the work is achievable, and the honest failure mode is telling someone that it is not. If committed work exceeds available time, say so at the top rather than placing tasks past their deadlines and calling it a schedule. Done when: A period with more committed work than available time shows an over-commitment warning naming the shortfall in hours. Completion tracking compares scheduled against actual and reports the ratio without judgement language. A week that fits shows no warning. The report distinguishes work that slipped from work that was never placed. Do not build yet: Sharing and operations come last. ### Phase 6 · Multiple calendars and running it continuously Build: Read busy time from several calendars including ones you do not write to. Run the solver on a schedule as a service. Alert when a calendar connection fails, because a scheduler working from stale availability is worse than one that stopped. Trap: A silently failed calendar connection means the solver schedules over meetings it can no longer see, and the plan looks normal while being wrong. Track last successful sync per calendar and refuse to write a plan built on availability older than a threshold. Done when: Busy time from a second calendar constrains placement. A failed connection produces an alert and the solver refuses to write a plan from stale availability, tested by breaking a connection deliberately. The service runs on a schedule and survives a restart. Each calendar's last sync time is visible. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-calendar sync across providers with the reliability the paid product manages, which is a long tail of provider quirks. - Recurring work rules and the scheduling patterns teams accumulate, which are where the commercial product's depth lives. - Mobile applications, which matter a great deal for a tool you consult during the day. - Team scheduling that coordinates several people's availability, which is a substantially harder problem than one person's. ### README must contain - That working hours are local wall clock and busy time is instants, and how the two are reconciled - That every unplaceable task is reported with a reason rather than dropped - That created events are tagged and nothing untagged is ever modified - That rescheduling prefers stability, so only displaced blocks move - That the solver refuses to plan from stale calendar data
# Architecture · Motion ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript on Node 22 | the work is reading calendars, solving a small problem and writing back | | Calendar | CalDAV, or a provider API, behind one interface | availability that does not reflect your real calendar produces a plan you cannot follow | | Solver | A greedy placement with a deterministic tie-break, not a constraint library | at a few dozen tasks a good heuristic is instant and explainable, and explainability matters more than optimality here | | Storage | PostgreSQL | tasks, schedules and the history of what moved | | Time | A library with a full time zone database | working hours are local and a scheduler that ignores that books you at three in the morning | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Calendar | Reading busy time and writing tagged blocks. | Providers differ; the tag-and-never-touch-untagged rule survives any change and prevents the one unrecoverable mistake. | | Availability | Working hours, zones and free intervals. | Pure computation, testable with no network. The zone handling is where correctness lives. | | Solver | Placement, splitting and unplaceable reasons. | A constraint library would optimise better and explain worse. At this size, explainability wins. | | Diff | Saying what moved and why. | Presentation, but it is what makes a rerun trustworthy rather than random. | | Capacity | Telling you the plan does not fit. | The honest output. A scheduler that always produces a full plan is hiding the answer. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Your PostgreSQL connection string. - `BLOCKS_CALENDAR_ID` · required · The calendar that receives blocks. A dedicated one, so clearing it is safe. - `EVENT_TAG` · required · The property value marking events this application created. Nothing without it is ever modified. - `WORKING_TIMEZONE` · required · The zone working hours are expressed in. Not the server's zone. - `MIN_BLOCK_MINUTES` · required · Default minimum block. Shorter blocks fit more easily and are less useful for real work. - `AVAILABILITY_STALE_MINUTES` · required · How old calendar data may be before the solver refuses to write a plan. - `PLAN_HORIZON_DAYS` · required · How far ahead to schedule. Longer plans are more fragile because more will change. ## 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 · Motion product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript on Node 22, CalDAV, or a provider API, behind one interface, A greedy placement with a deterministic tie-break, not a constraint library, PostgreSQL, A library with a full time zone database. - 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 - Working hours are local wall clock and busy time is instants, so subtracting one from the other without converting produces free intervals that shift by an hour twice a year. Convert working hours to instants for the specific dates being scheduled, using the zone, rather than treating them as fixed offsets. - A solver that silently omits what it cannot fit produces a plan that looks achievable and is not, and you discover the missing work at the deadline. Every unplaced task must appear with a reason: not enough free time before the deadline, blocked by a dependency, or a minimum block size larger than any available gap. - Identifying your own events by title or by time will eventually delete a real meeting, and that is an unrecoverable mistake in someone's calendar. Tag every event you create with an identifier you own, and refuse to delete anything without that tag, even during cleanup. - A solver rerun that reshuffles everything because one meeting appeared makes the plan feel random and untrustworthy, even when each individual placement is optimal. Weight stability explicitly: keep blocks that still fit, and move only what the change forced. - A scheduler that always produces a full plan implies the work is achievable, and the honest failure mode is telling someone that it is not. If committed work exceeds available time, say so at the top rather than placing tasks past their deadlines and calling it a schedule. - A silently failed calendar connection means the solver schedules over meetings it can no longer see, and the plan looks normal while being wrong. Track last successful sync per calendar and refuse to write a plan built on availability older than a threshold.
# Delivery milestones · Motion Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Real availability The application knows exactly when you are free, from your actual calendar. ### Steps 1. Pull busy time from a real calendar Behind one interface so the provider can change. Store instants with the source and external identifier. Files: `src/calendar.ts` 2. Define working hours with a zone Per weekday, local wall clock, with the zone stated explicitly rather than inherited from the server. Files: `src/availability.ts` 3. Subtract correctly across zones Convert working hours to instants per date. Test across a clock change. Files: `src/availability.test.ts` ### Done when - [ ] Free intervals for tomorrow match your calendar by inspection - [ ] An all day event does not consume the day unless configured to - [ ] Working hours hold their local times across a clock change - [ ] A new calendar event changes free intervals after refresh ### Watch out - Working hours are local wall clock and busy time is instants, so subtracting one from the other without converting produces free intervals that shift by an hour twice a year. Convert working hours to instants for the specific dates being scheduled, using the zone, rather than treating them as fixed offsets. ## M2 · A solver that explains itself Tasks are placed into free time, and every task that was not placed says why. ### Steps 1. Sort and place greedily Deadline first, then priority, with a deterministic tie-break so runs are reproducible. Files: `src/solve.ts` 2. Split into blocks within bounds Respect minimum and maximum block size. A task needing two hours of unbroken focus cannot be four half hours. Files: `src/solve.ts` 3. Report every failure with a reason Unplaceable is an output, not an omission. This is the difference between a plan and a wish. Files: `src/solve.ts` ### Done when - [ ] No placed block overlaps another or busy time - [ ] An unplaceable task is reported with a specific reason - [ ] Two runs on unchanged input produce identical plans - [ ] A dependent task is never placed before its dependency ### Watch out - A solver that silently omits what it cannot fit produces a plan that looks achievable and is not, and you discover the missing work at the deadline. Every unplaced task must appear with a reason: not enough free time before the deadline, blocked by a dependency, or a minimum block size larger than any available gap. ## M3 · Writing blocks back The plan appears in your calendar, and rebuilding it does not leave debris. ### Steps 1. Tag every created event An identifier in a property you own. Nothing untagged is ever touched. Files: `src/write.ts` 2. Replace by generation Delete the previous generation's tagged events, then write the new one. Count before and after. Files: `src/write.ts` 3. Notice manual deletions A block whose event is gone is a signal the user rejected it, not an error to correct. Files: `src/sync.ts` ### Done when - [ ] Blocks appear as events on the right days - [ ] A rebuild leaves no orphan blocks - [ ] Hand-created events are never modified - [ ] A manually deleted block is noticed rather than recreated ### Watch out - Identifying your own events by title or by time will eventually delete a real meeting, and that is an unrecoverable mistake in someone's calendar. Tag every event you create with an identifier you own, and refuse to delete anything without that tag, even during cleanup. ## M4 · Rescheduling when the day changes A meeting appearing at short notice reshuffles the plan, and you can see what moved. ### Steps 1. Detect changes and rerun On a schedule or on a change notification. A stale plan is worse than no plan. Files: `src/watch.ts` 2. Weight stability Keep placements that still fit. Only what the change displaced may move. Files: `src/solve.ts` 3. Produce a readable diff Moved, at risk, and newly unplaceable. This is what the user actually reads after a rerun. Files: `src/diff.ts` ### Done when - [ ] A new meeting moves only what it displaced - [ ] The diff names moved and at-risk tasks - [ ] A task pushed past its deadline is reported prominently - [ ] An unchanged calendar produces no moves ### Watch out - A solver rerun that reshuffles everything because one meeting appeared makes the plan feel random and untrustworthy, even when each individual placement is optimal. Weight stability explicitly: keep blocks that still fit, and move only what the change forced. ## M5 · Being honest about the plan The application tells you when the plan is not achievable rather than presenting a full calendar. ### Steps 1. Compare committed to available Total task duration against total free time for the horizon. One number, shown prominently. Files: `src/capacity.ts` 2. Track completion honestly Scheduled against actually done. Report the ratio as information about your estimates. Files: `src/completion.ts` 3. Keep the language neutral An estimate that was optimistic is a fact about estimating, not a failing. Files: `src/report.ts` ### Done when - [ ] Over-commitment shows a warning with the shortfall in hours - [ ] A week that fits shows no warning - [ ] Completion ratio is reported without judgement - [ ] Slipped work is distinguished from unplaced work ### Watch out - A scheduler that always produces a full plan implies the work is achievable, and the honest failure mode is telling someone that it is not. If committed work exceeds available time, say so at the top rather than placing tasks past their deadlines and calling it a schedule. ## M6 · Multiple calendars and running it continuously (production only) It reads every calendar that constrains you, and it keeps running without supervision. ### Steps 1. Read several calendars Read-only ones count as constraints. Write only to the one you own. Files: `src/calendar.ts` 2. Refuse to plan on stale data A freshness threshold per calendar. Refusing is safer than scheduling over an invisible meeting. Files: `src/freshness.ts` 3. Run as a service with alerts Scheduled runs, restart survival, and an alert on connection failure. Files: `src/service.ts` ### Done when - [ ] A second calendar's events constrain placement - [ ] A broken connection alerts and blocks planning on stale data - [ ] The service survives a restart - [ ] Last sync time per calendar is visible ### Watch out - A silently failed calendar connection means the solver schedules over meetings it can no longer see, and the plan looks normal while being wrong. Track last successful sync per calendar and refuse to write a plan built on availability older than a threshold.
# Operations · Motion ## Backup Tasks and plan history are in the database. The blocks in your calendar are regenerable, which is why they carry a generation tag rather than being treated as data. ## Restore Restore the database, clear the blocks calendar, and rerun. Because blocks are tagged and disposable, a restore rebuilds the plan rather than reconciling it. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Calendar sync freshness per connection and the count of unplaceable tasks. A rising unplaceable count is the signal that commitments have outgrown available time, which is information rather than a fault. ## Incident checklist If blocks stop appearing, check calendar connection freshness first, since the solver is designed to refuse rather than write from stale data. If a real meeting was deleted, the tag check failed and that is the highest priority bug in the application. 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 placed block overlaps busy time or another block - [ ] Every unplaceable task carries a reason - [ ] Untagged calendar events are confirmed never modified - [ ] A stale calendar connection is confirmed to block planning - [ ] A rerun with no change is confirmed to move nothing ## Launch constraint Do not market omitted Motion 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 · secret. Your PostgreSQL connection string. DATABASE_URL=postgres://user:pass@localhost:5432/schedule # Required. The calendar that receives blocks. A dedicated one, so clearing it is safe. BLOCKS_CALENDAR_ID=scheduled-work # Required. The property value marking events this application created. Nothing without it is ever modified. EVENT_TAG=autoplan # Required. The zone working hours are expressed in. Not the server's zone. WORKING_TIMEZONE=Europe/London # Required. Default minimum block. Shorter blocks fit more easily and are less useful for real work. MIN_BLOCK_MINUTES=30 # Required. How old calendar data may be before the solver refuses to write a plan. AVAILABILITY_STALE_MINUTES=60 # Required. How far ahead to schedule. Longer plans are more fragile because more will change. PLAN_HORIZON_DAYS=14
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay because the calendar stays organized without manual drag-and-drop.
xmulti-calendar sync
xrecurring work rules
xmobile apps
xteam scheduling
xpolished rescheduling
xsupport
Don't feel like building it? These folks already made it free.
all 3 free alternatives to Motion →· no votes, no pay-to-list · just what's real
Motion pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| pro ai | — | $19/user | 7,500 AI credits/seat/month; unlimited storage; AI chat, projects, tasks, calendar, meetings, docs, wiki, and notes. |
| business ai | — | $29/user | 15,000 AI credits/seat/month; capacity planning, dashboards, Gantt, time tracking, permissions, and priority support. |
free tierno free tier
billingmonthly + annual (-33% advertised); exact monthly dollar amounts were not exposed; free trial
hidden costsAI overages cost $0.25 per 100 credits on Pro AI and $0.19 per 100 credits on Business AI.
verified 2026-08-14 · source ↗
Vibecode Motion
Kinda. The core of Motion is buildable in a weekend with the prompt on this page, but there are real gaps: multi-calendar sync, recurring work rules. Read the honest list above before committing.
How much does Motion cost?
Motion costs about $19/month (Pro AI, checked 2026-08-14), which is $228 per year.
What do I lose by replacing Motion?
Honestly: multi-calendar sync; recurring work rules; mobile apps; team scheduling; polished rescheduling; support. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Motion?
Yes: FlowSavvy Free (It auto-plans unlimited tasks for the next two weeks; priorities and dependencies are how the paid plan gets you.) Reclaim Lite (The free solo plan moves focus, habits, breaks and meetings around; the planning horizon is one week.) Trevor AI Free (One calendar gets task-to-time-block planning and some AI suggestions; multiple calendars and deeper automation cost extra.) All 3 curated free alternatives are at vibecodeit.com/motion/alternatives. The prompt is for when you want it exactly your way.