Vibecode Reclaim.ai
track this build4 phases, 12 steps, beginner friendly0%A personal auto-blocker is buildable, but two-way calendar sync, scheduling links, team features, and conflict-safe automation are upkeep-heavy.
You are building a lean indie version of Reclaim.ai. 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 ===== # Reclaim.ai · indie build A scheduling assistant whose core is a constraint solver over your calendar: fit tasks and recurring habits into free time, respect deadlines and working hours, and rearrange when a meeting lands on top of something without producing a different plan every time it runs. 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 with Fastify and PostgreSQL | the work is solving, syncing and reconciling | | Solver | A deterministic greedy placement with a stability preference | a full optimiser produces better plans and rearranges your week every time it runs | | Calendar | Two-way sync with events the tool created marked as its own | you must never delete a real meeting, and the tool must know which events are its | | Time | Instants in UTC with the user's zone and working hours declared | everything here is time arithmetic and it is where these tools break | | Reconciliation | Detecting external changes before every solve | someone moving one of your placed blocks means something, and it is not that they were wrong | ## 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 server, the solver and the sync workers. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Placements, solves and busy time with correct interval queries. - Get it: Docker or your package manager. - Verify: psql connects and a range overlap query returns correctly - [ ] **A calendar account with write access** · free - Why: Phase 3 writes real events into a real calendar, and the failure modes only appear against a live account with existing meetings. - Get it: A test calendar with a month of realistic meetings, including recurring series with exceptions and some declined invitations. - Verify: Events can be created and the tool's own events are identifiable by stored id - [ ] **A realistic week of your own commitments** · free - Why: Solver behaviour is entirely determined by how fragmented a real calendar is, and a test calendar with three meetings will make any solver look excellent. - Get it: Use your actual calendar, or reconstruct a genuinely busy week including back-to-back meetings and a day with almost no free time. - Verify: A week exists with at least fifteen meetings and one nearly full day - [ ] **Your rules for each event status** · free - Why: Declined, tentative, all-day and out-of-office events each need an answer before the solver exists, and changing the answer later silently changes every plan. - Get it: Write down whether each status blocks time, and what working hours actually mean for you including whether evenings count. - Verify: A written note gives a rule for every status your calendar produces ## 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-person meeting scheduling. - Integrations with external task tools. - Machine-learned duration estimation. - Team-wide scheduling policies. - team coordination - smart rescheduling polish - scheduling links - calendar edge cases - uptime If one of those is essential to you, that is the reason to keep paying for Reclaim.ai, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Reclaim.ai 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 places your tasks and habits into the gaps in your calendar to replace Reclaim.ai. 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 with Fastify and PostgreSQL · the work is solving, syncing and reconciling - Solver: A deterministic greedy placement with a stability preference · a full optimiser produces better plans and rearranges your week every time it runs - Calendar: Two-way sync with events the tool created marked as its own · you must never delete a real meeting, and the tool must know which events are its - Time: Instants in UTC with the user's zone and working hours declared · everything here is time arithmetic and it is where these tools break - Reconciliation: Detecting external changes before every solve · someone moving one of your placed blocks means something, and it is not that they were wrong ### Data model (create this before Phase 1) - tasks(id, title, duration_minutes, due_at, min_chunk_minutes, max_chunk_minutes, priority, completed_at) - habits(id, title, cadence, duration_minutes, window_start, window_end, days_json, priority) - placements(id, source_kind, source_id, calendar_event_id, starts_at, ends_at, solve_id, pinned, user_moved) - solves(id, at, reason, placements_changed, inputs_hash) · so you can explain why the plan changed - busy(id, calendar_id, starts_at, ends_at, external_event_id, is_ours) · the calendar as the solver sees it - working_hours(day_of_week, start_local, end_local, zone) - A placement records whether the user moved it, because a block someone dragged somewhere is a decision the solver must respect rather than undo. ### Phase 1 · Knowing when you are actually free Build: Sync calendars read only. Expand recurring events with their exceptions. Apply working hours per day in the user's zone. Handle all-day events, declined invitations and overlapping meetings. Trap: Treating a declined invitation or an all-day event as busy time removes most of the week from consideration, and treating tentative events as free schedules work on top of meetings people are probably attending. Decide the rule for each status explicitly, and expand recurrence with exceptions rather than assuming the series. Done when: Free time is computed correctly across a real calendar with recurring events, exceptions, declined invitations and all-day events. Both daylight saving transitions produce correct free time, tested on dates either side. Overlapping meetings do not double-count busy time. Working hours apply in the user's zone rather than the server's. Do not build yet: No tasks, solving or writing to calendars. ### Phase 2 · Placing work into the gaps Build: A deterministic solver placing tasks by deadline and priority, splitting them into chunks within their minimum and maximum sizes, and placing habits within their windows. Report what could not be placed and why. Trap: A solver that finds a slightly better arrangement each run will move your Tuesday afternoon every time it executes, and a plan that changes constantly is one nobody can work to. Prefer keeping existing placements over marginal improvements, and make identical inputs produce an identical plan. Done when: Identical inputs produce an identical plan, asserted in a test by solving twice. Tasks split into chunks respecting minimum and maximum sizes. A task that cannot be placed before its deadline is reported with the reason rather than dropped. A week with too much work reports what did not fit rather than overfilling. Do not build yet: No calendar writing or rescheduling yet. ### Phase 3 · Writing to a calendar you did not create Build: Create events for placements, marked identifiably as belonging to the tool. Never modify or delete an event the tool did not create. Handle the sync token expiring and a full resync without duplicating everything. Trap: Identifying your own events by title or colour means a user renaming a block makes it invisible to the tool, which then creates a duplicate, or worse, a coincidence of naming makes the tool delete a real meeting. Store the created event identifier and treat anything not in that set as untouchable. Done when: Placed blocks appear in the real calendar and are identifiable by stored identifier rather than by title. A real meeting is never modified or deleted, verified by a test that attempts it. A full resync after token expiry does not duplicate placements. Deleting the tool's events from the calendar is detected rather than causing duplicates. Do not build yet: No rescheduling or conflict handling yet. ### Phase 4 · When a meeting lands on your work Build: Detect external calendar changes and re-solve, preferring to move only what is affected. Respect blocks the user pinned or dragged. Never move a block that has already started or finished. Trap: Re-solving the whole week when one meeting appears rearranges work the user has already mentally committed to, and doing that a few times teaches them to ignore the calendar entirely. Move the displaced block and its dependents, leave everything else, and treat a block the user dragged as pinned. Done when: A new meeting moves only the displaced block, verified by comparing plans before and after. A block the user dragged is not moved by a later solve. A block in the past is never moved. A meeting cancelled frees the time without rearranging blocks that were already settled. Do not build yet: No reporting or preferences yet. ### Phase 5 · Explaining the plan, and living with it Build: Record a reason per placement and per solve. Show what changed since the last solve and why. Compare planned against completed to show whether the schedule is realistic. Handle a habit that is repeatedly skipped. Trap: A scheduler that keeps placing a habit you never do produces a calendar full of fiction, and the user stops trusting every other block as well. Detect a habit or task type that is consistently not completed, surface it, and offer to reduce or remove it rather than continuing to place it. Done when: Every placement has a stated reason and every solve records what changed. A habit skipped repeatedly is surfaced with an offer to change it. A comparison of planned against completed time is available for a period. The plan history explains a past week rather than only the current one. Do not build yet: Nothing after this. ### Out of scope (and why) - Meeting scheduling across several people's calendars, which is a substantially harder problem with a different failure mode. - Integrations with task tools, which is where the hosted product's practical value largely sits. - Machine-learned duration estimation, which needs far more completion data than a personal tool accumulates. - Team-wide scheduling policies, which turn a personal tool into a workplace one with different consent questions. ### README must contain - That the solver is deterministic so identical inputs give an identical plan, and why that matters more than optimality - That the tool identifies its own events by stored identifier and never touches anything else - That a block you drag is treated as pinned - That a new meeting re-solves minimally rather than rearranging the week - That a persistently skipped habit is surfaced rather than scheduled forever ===== AGENTS.md ===== # Agent instructions · Reclaim.ai indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, A deterministic greedy placement with a stability preference, Two-way sync with events the tool created marked as its own, Instants in UTC with the user's zone and working hours declared, Detecting external changes before every solve. 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 - Treating a declined invitation or an all-day event as busy time removes most of the week from consideration, and treating tentative events as free schedules work on top of meetings people are probably attending. Decide the rule for each status explicitly, and expand recurrence with exceptions rather than assuming the series. - A solver that finds a slightly better arrangement each run will move your Tuesday afternoon every time it executes, and a plan that changes constantly is one nobody can work to. Prefer keeping existing placements over marginal improvements, and make identical inputs produce an identical plan. - Identifying your own events by title or colour means a user renaming a block makes it invisible to the tool, which then creates a duplicate, or worse, a coincidence of naming makes the tool delete a real meeting. Store the created event identifier and treat anything not in that set as untouchable. - Re-solving the whole week when one meeting appears rearranges work the user has already mentally committed to, and doing that a few times teaches them to ignore the calendar entirely. Move the displaced block and its dependents, leave everything else, and treat a block the user dragged as pinned. ===== BUILD_PLAN.md ===== # Build plan · Reclaim.ai A scheduling assistant whose core is a constraint solver over your calendar: fit tasks and recurring habits into free time, respect deadlines and working hours, and rearrange when a meeting lands on top of something without producing a different plan every time it runs. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Knowing when you are actually free A correct picture of free time, including the awkward cases. ### Steps 1. Expand recurrence with exceptions The series minus its cancellations. Assuming the series is the common bug. Files: `src/recurrence.ts`, `src/recurrence.test.ts` 2. Decide a rule per event status Declined, tentative, all-day, out of office. Each needs an explicit answer. Files: `src/busy.ts` 3. Apply working hours in the user's zone Not the server's, and not a fixed offset. Files: `src/hours.ts` ### Done when - [ ] Free time is correct across a real calendar - [ ] Both daylight transitions produce correct results - [ ] Overlapping meetings do not double-count - [ ] Event statuses follow their declared rules ### Watch out - Treating a declined invitation or an all-day event as busy time removes most of the week from consideration, and treating tentative events as free schedules work on top of meetings people are probably attending. Decide the rule for each status explicitly, and expand recurrence with exceptions rather than assuming the series. ## Phase 2 · Placing work into the gaps Tasks and habits get placed sensibly and the same inputs give the same plan. ### Steps 1. Solve deterministically Same inputs, same plan, asserted in the suite. This is what makes it usable. Files: `src/solve.ts`, `src/solve.test.ts` 2. Respect chunk sizes Twenty minutes of deep work is not useful. The minimum exists for a reason. Files: `src/chunk.ts` 3. Report what did not fit and why Silently dropping a task before its deadline is the worst possible behaviour. Files: `src/report.ts` ### Done when - [ ] Identical inputs produce an identical plan - [ ] Chunk sizes are respected - [ ] Unplaceable tasks are reported with reasons - [ ] An overfull week reports overflow rather than overfilling ### Watch out - A solver that finds a slightly better arrangement each run will move your Tuesday afternoon every time it executes, and a plan that changes constantly is one nobody can work to. Prefer keeping existing placements over marginal improvements, and make identical inputs produce an identical plan. ## Phase 3 · Writing to a calendar you did not create Placed blocks appear in your real calendar and nothing else is ever touched. ### Steps 1. Identify your events by stored identifier Never by title or colour. A rename must not orphan a block. Files: `src/calendar.ts` 2. Refuse to touch foreign events In the write path, with a test that attempts it and expects a refusal. Files: `src/calendar.ts`, `tests/foreign-events.test.ts` 3. Handle full resync idempotently Sync tokens expire. A resync that duplicates a week is very visible. Files: `src/sync.ts` ### Done when - [ ] Placed blocks appear and are identified by stored id - [ ] A foreign event cannot be modified or deleted - [ ] A full resync does not duplicate placements - [ ] Externally deleted blocks are detected ### Watch out - Identifying your own events by title or colour means a user renaming a block makes it invisible to the tool, which then creates a duplicate, or worse, a coincidence of naming makes the tool delete a real meeting. Store the created event identifier and treat anything not in that set as untouchable. ## Phase 4 · When a meeting lands on your work A new meeting moves the affected block and nothing else. ### Steps 1. Re-solve minimally The displaced block and its dependents. Not the week. Files: `src/resolve.ts` 2. Treat a user move as a pin Someone dragging a block has expressed a preference. Undoing it is insulting. Files: `src/placements.ts` 3. Never move the past A block that started is history, whether or not the work happened. Files: `src/resolve.ts` ### Done when - [ ] A new meeting moves only what it displaced - [ ] A user-moved block is not moved again - [ ] Past blocks are never moved - [ ] A cancellation frees time without rearranging ### Watch out - Re-solving the whole week when one meeting appears rearranges work the user has already mentally committed to, and doing that a few times teaches them to ignore the calendar entirely. Move the displaced block and its dependents, leave everything else, and treat a block the user dragged as pinned. ## Not in this build - Multi-person meeting scheduling. - Integrations with external task tools. - Machine-learned duration estimation. - Team-wide scheduling policies. ## After v1, if you want it - Buffer time around meetings, which most people need more than more work blocks - Travel time between events with locations, once the busy model is trustworthy - Focus mode integration, so a placed block actually protects itself ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Placements, tasks and a copy of your calendar's busy time. DATABASE_URL=postgres://localhost:5432/schedule # Required · secret. Calendar access. Request the narrowest scope that allows creating and updating your own events. CALENDAR_CLIENT_ID=a provider client id # Required. Working hours and habit windows are local. Everything stored is in UTC. USER_TIMEZONE=Europe/London # Required. The smallest useful block. Below this a placement is an interruption rather than work. MIN_CHUNK_MINUTES=45 # Required. How strongly to prefer keeping existing placements over a marginally better plan. SOLVE_STABILITY_BIAS=high # Required. Present as a constant so the answer is visible in configuration. There is no other value. TOUCH_FOREIGN_EVENTS=never
You are building a lean indie version of Reclaim.ai. 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 ===== # Reclaim.ai · indie build A scheduling assistant whose core is a constraint solver over your calendar: fit tasks and recurring habits into free time, respect deadlines and working hours, and rearrange when a meeting lands on top of something without producing a different plan every time it runs. 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 with Fastify and PostgreSQL | the work is solving, syncing and reconciling | | Solver | A deterministic greedy placement with a stability preference | a full optimiser produces better plans and rearranges your week every time it runs | | Calendar | Two-way sync with events the tool created marked as its own | you must never delete a real meeting, and the tool must know which events are its | | Time | Instants in UTC with the user's zone and working hours declared | everything here is time arithmetic and it is where these tools break | | Reconciliation | Detecting external changes before every solve | someone moving one of your placed blocks means something, and it is not that they were wrong | ## 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 server, the solver and the sync workers. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Placements, solves and busy time with correct interval queries. - Get it: Docker or your package manager. - Verify: psql connects and a range overlap query returns correctly - [ ] **A calendar account with write access** · free - Why: Phase 3 writes real events into a real calendar, and the failure modes only appear against a live account with existing meetings. - Get it: A test calendar with a month of realistic meetings, including recurring series with exceptions and some declined invitations. - Verify: Events can be created and the tool's own events are identifiable by stored id - [ ] **A realistic week of your own commitments** · free - Why: Solver behaviour is entirely determined by how fragmented a real calendar is, and a test calendar with three meetings will make any solver look excellent. - Get it: Use your actual calendar, or reconstruct a genuinely busy week including back-to-back meetings and a day with almost no free time. - Verify: A week exists with at least fifteen meetings and one nearly full day - [ ] **Your rules for each event status** · free - Why: Declined, tentative, all-day and out-of-office events each need an answer before the solver exists, and changing the answer later silently changes every plan. - Get it: Write down whether each status blocks time, and what working hours actually mean for you including whether evenings count. - Verify: A written note gives a rule for every status your calendar produces ## 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-person meeting scheduling. - Integrations with external task tools. - Machine-learned duration estimation. - Team-wide scheduling policies. - team coordination - smart rescheduling polish - scheduling links - calendar edge cases - uptime If one of those is essential to you, that is the reason to keep paying for Reclaim.ai, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Reclaim.ai 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 places your tasks and habits into the gaps in your calendar to replace Reclaim.ai. 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 with Fastify and PostgreSQL · the work is solving, syncing and reconciling - Solver: A deterministic greedy placement with a stability preference · a full optimiser produces better plans and rearranges your week every time it runs - Calendar: Two-way sync with events the tool created marked as its own · you must never delete a real meeting, and the tool must know which events are its - Time: Instants in UTC with the user's zone and working hours declared · everything here is time arithmetic and it is where these tools break - Reconciliation: Detecting external changes before every solve · someone moving one of your placed blocks means something, and it is not that they were wrong ### Data model (create this before Phase 1) - tasks(id, title, duration_minutes, due_at, min_chunk_minutes, max_chunk_minutes, priority, completed_at) - habits(id, title, cadence, duration_minutes, window_start, window_end, days_json, priority) - placements(id, source_kind, source_id, calendar_event_id, starts_at, ends_at, solve_id, pinned, user_moved) - solves(id, at, reason, placements_changed, inputs_hash) · so you can explain why the plan changed - busy(id, calendar_id, starts_at, ends_at, external_event_id, is_ours) · the calendar as the solver sees it - working_hours(day_of_week, start_local, end_local, zone) - A placement records whether the user moved it, because a block someone dragged somewhere is a decision the solver must respect rather than undo. ### Phase 1 · Knowing when you are actually free Build: Sync calendars read only. Expand recurring events with their exceptions. Apply working hours per day in the user's zone. Handle all-day events, declined invitations and overlapping meetings. Trap: Treating a declined invitation or an all-day event as busy time removes most of the week from consideration, and treating tentative events as free schedules work on top of meetings people are probably attending. Decide the rule for each status explicitly, and expand recurrence with exceptions rather than assuming the series. Done when: Free time is computed correctly across a real calendar with recurring events, exceptions, declined invitations and all-day events. Both daylight saving transitions produce correct free time, tested on dates either side. Overlapping meetings do not double-count busy time. Working hours apply in the user's zone rather than the server's. Do not build yet: No tasks, solving or writing to calendars. ### Phase 2 · Placing work into the gaps Build: A deterministic solver placing tasks by deadline and priority, splitting them into chunks within their minimum and maximum sizes, and placing habits within their windows. Report what could not be placed and why. Trap: A solver that finds a slightly better arrangement each run will move your Tuesday afternoon every time it executes, and a plan that changes constantly is one nobody can work to. Prefer keeping existing placements over marginal improvements, and make identical inputs produce an identical plan. Done when: Identical inputs produce an identical plan, asserted in a test by solving twice. Tasks split into chunks respecting minimum and maximum sizes. A task that cannot be placed before its deadline is reported with the reason rather than dropped. A week with too much work reports what did not fit rather than overfilling. Do not build yet: No calendar writing or rescheduling yet. ### Phase 3 · Writing to a calendar you did not create Build: Create events for placements, marked identifiably as belonging to the tool. Never modify or delete an event the tool did not create. Handle the sync token expiring and a full resync without duplicating everything. Trap: Identifying your own events by title or colour means a user renaming a block makes it invisible to the tool, which then creates a duplicate, or worse, a coincidence of naming makes the tool delete a real meeting. Store the created event identifier and treat anything not in that set as untouchable. Done when: Placed blocks appear in the real calendar and are identifiable by stored identifier rather than by title. A real meeting is never modified or deleted, verified by a test that attempts it. A full resync after token expiry does not duplicate placements. Deleting the tool's events from the calendar is detected rather than causing duplicates. Do not build yet: No rescheduling or conflict handling yet. ### Phase 4 · When a meeting lands on your work Build: Detect external calendar changes and re-solve, preferring to move only what is affected. Respect blocks the user pinned or dragged. Never move a block that has already started or finished. Trap: Re-solving the whole week when one meeting appears rearranges work the user has already mentally committed to, and doing that a few times teaches them to ignore the calendar entirely. Move the displaced block and its dependents, leave everything else, and treat a block the user dragged as pinned. Done when: A new meeting moves only the displaced block, verified by comparing plans before and after. A block the user dragged is not moved by a later solve. A block in the past is never moved. A meeting cancelled frees the time without rearranging blocks that were already settled. Do not build yet: No reporting or preferences yet. ### Phase 5 · Explaining the plan, and living with it Build: Record a reason per placement and per solve. Show what changed since the last solve and why. Compare planned against completed to show whether the schedule is realistic. Handle a habit that is repeatedly skipped. Trap: A scheduler that keeps placing a habit you never do produces a calendar full of fiction, and the user stops trusting every other block as well. Detect a habit or task type that is consistently not completed, surface it, and offer to reduce or remove it rather than continuing to place it. Done when: Every placement has a stated reason and every solve records what changed. A habit skipped repeatedly is surfaced with an offer to change it. A comparison of planned against completed time is available for a period. The plan history explains a past week rather than only the current one. Do not build yet: Nothing after this. ### Out of scope (and why) - Meeting scheduling across several people's calendars, which is a substantially harder problem with a different failure mode. - Integrations with task tools, which is where the hosted product's practical value largely sits. - Machine-learned duration estimation, which needs far more completion data than a personal tool accumulates. - Team-wide scheduling policies, which turn a personal tool into a workplace one with different consent questions. ### README must contain - That the solver is deterministic so identical inputs give an identical plan, and why that matters more than optimality - That the tool identifies its own events by stored identifier and never touches anything else - That a block you drag is treated as pinned - That a new meeting re-solves minimally rather than rearranging the week - That a persistently skipped habit is surfaced rather than scheduled forever ===== AGENTS.md ===== # Agent instructions · Reclaim.ai indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, A deterministic greedy placement with a stability preference, Two-way sync with events the tool created marked as its own, Instants in UTC with the user's zone and working hours declared, Detecting external changes before every solve. 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 - Treating a declined invitation or an all-day event as busy time removes most of the week from consideration, and treating tentative events as free schedules work on top of meetings people are probably attending. Decide the rule for each status explicitly, and expand recurrence with exceptions rather than assuming the series. - A solver that finds a slightly better arrangement each run will move your Tuesday afternoon every time it executes, and a plan that changes constantly is one nobody can work to. Prefer keeping existing placements over marginal improvements, and make identical inputs produce an identical plan. - Identifying your own events by title or colour means a user renaming a block makes it invisible to the tool, which then creates a duplicate, or worse, a coincidence of naming makes the tool delete a real meeting. Store the created event identifier and treat anything not in that set as untouchable. - Re-solving the whole week when one meeting appears rearranges work the user has already mentally committed to, and doing that a few times teaches them to ignore the calendar entirely. Move the displaced block and its dependents, leave everything else, and treat a block the user dragged as pinned. ===== BUILD_PLAN.md ===== # Build plan · Reclaim.ai A scheduling assistant whose core is a constraint solver over your calendar: fit tasks and recurring habits into free time, respect deadlines and working hours, and rearrange when a meeting lands on top of something without producing a different plan every time it runs. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Knowing when you are actually free A correct picture of free time, including the awkward cases. ### Steps 1. Expand recurrence with exceptions The series minus its cancellations. Assuming the series is the common bug. Files: `src/recurrence.ts`, `src/recurrence.test.ts` 2. Decide a rule per event status Declined, tentative, all-day, out of office. Each needs an explicit answer. Files: `src/busy.ts` 3. Apply working hours in the user's zone Not the server's, and not a fixed offset. Files: `src/hours.ts` ### Done when - [ ] Free time is correct across a real calendar - [ ] Both daylight transitions produce correct results - [ ] Overlapping meetings do not double-count - [ ] Event statuses follow their declared rules ### Watch out - Treating a declined invitation or an all-day event as busy time removes most of the week from consideration, and treating tentative events as free schedules work on top of meetings people are probably attending. Decide the rule for each status explicitly, and expand recurrence with exceptions rather than assuming the series. ## Phase 2 · Placing work into the gaps Tasks and habits get placed sensibly and the same inputs give the same plan. ### Steps 1. Solve deterministically Same inputs, same plan, asserted in the suite. This is what makes it usable. Files: `src/solve.ts`, `src/solve.test.ts` 2. Respect chunk sizes Twenty minutes of deep work is not useful. The minimum exists for a reason. Files: `src/chunk.ts` 3. Report what did not fit and why Silently dropping a task before its deadline is the worst possible behaviour. Files: `src/report.ts` ### Done when - [ ] Identical inputs produce an identical plan - [ ] Chunk sizes are respected - [ ] Unplaceable tasks are reported with reasons - [ ] An overfull week reports overflow rather than overfilling ### Watch out - A solver that finds a slightly better arrangement each run will move your Tuesday afternoon every time it executes, and a plan that changes constantly is one nobody can work to. Prefer keeping existing placements over marginal improvements, and make identical inputs produce an identical plan. ## Phase 3 · Writing to a calendar you did not create Placed blocks appear in your real calendar and nothing else is ever touched. ### Steps 1. Identify your events by stored identifier Never by title or colour. A rename must not orphan a block. Files: `src/calendar.ts` 2. Refuse to touch foreign events In the write path, with a test that attempts it and expects a refusal. Files: `src/calendar.ts`, `tests/foreign-events.test.ts` 3. Handle full resync idempotently Sync tokens expire. A resync that duplicates a week is very visible. Files: `src/sync.ts` ### Done when - [ ] Placed blocks appear and are identified by stored id - [ ] A foreign event cannot be modified or deleted - [ ] A full resync does not duplicate placements - [ ] Externally deleted blocks are detected ### Watch out - Identifying your own events by title or colour means a user renaming a block makes it invisible to the tool, which then creates a duplicate, or worse, a coincidence of naming makes the tool delete a real meeting. Store the created event identifier and treat anything not in that set as untouchable. ## Phase 4 · When a meeting lands on your work A new meeting moves the affected block and nothing else. ### Steps 1. Re-solve minimally The displaced block and its dependents. Not the week. Files: `src/resolve.ts` 2. Treat a user move as a pin Someone dragging a block has expressed a preference. Undoing it is insulting. Files: `src/placements.ts` 3. Never move the past A block that started is history, whether or not the work happened. Files: `src/resolve.ts` ### Done when - [ ] A new meeting moves only what it displaced - [ ] A user-moved block is not moved again - [ ] Past blocks are never moved - [ ] A cancellation frees time without rearranging ### Watch out - Re-solving the whole week when one meeting appears rearranges work the user has already mentally committed to, and doing that a few times teaches them to ignore the calendar entirely. Move the displaced block and its dependents, leave everything else, and treat a block the user dragged as pinned. ## Not in this build - Multi-person meeting scheduling. - Integrations with external task tools. - Machine-learned duration estimation. - Team-wide scheduling policies. ## After v1, if you want it - Buffer time around meetings, which most people need more than more work blocks - Travel time between events with locations, once the busy model is trustworthy - Focus mode integration, so a placed block actually protects itself ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Placements, tasks and a copy of your calendar's busy time. DATABASE_URL=postgres://localhost:5432/schedule # Required · secret. Calendar access. Request the narrowest scope that allows creating and updating your own events. CALENDAR_CLIENT_ID=a provider client id # Required. Working hours and habit windows are local. Everything stored is in UTC. USER_TIMEZONE=Europe/London # Required. The smallest useful block. Below this a placement is an interruption rather than work. MIN_CHUNK_MINUTES=45 # Required. How strongly to prefer keeping existing placements over a marginally better plan. SOLVE_STABILITY_BIAS=high # Required. Present as a constant so the answer is visible in configuration. There is no other value. TOUCH_FOREIGN_EVENTS=never
You are building a production product version of Reclaim.ai. 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 ===== # Reclaim.ai · product brief ## Problem A personal auto-blocker is buildable, but two-way calendar sync, scheduling links, team features, and conflict-safe automation are upkeep-heavy. ## Product outcome A scheduler that fills your gaps with the work you actually have, gives the same answer twice, moves only what a new meeting displaced, and cannot touch a real meeting. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Google Calendar OAuth - scheduling logic - hosted backend - background workers - notifications ## Explicit non-goals for v1 - Multi-person meeting scheduling. - Integrations with external task tools. - Machine-learned duration estimation. - Team-wide scheduling policies. - team coordination - smart rescheduling polish - scheduling links - calendar edge cases - uptime ## Success criteria - Identical inputs are confirmed to produce an identical plan - A foreign event is confirmed impossible to modify or delete - A full resync is confirmed not to duplicate placements - A user-moved block is confirmed not to be moved again - Both daylight transitions are confirmed to produce correct free time ===== BRIEF.md ===== # Build brief · Reclaim.ai 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 places your tasks and habits into the gaps in your calendar to replace Reclaim.ai. 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 with Fastify and PostgreSQL · the work is solving, syncing and reconciling - Solver: A deterministic greedy placement with a stability preference · a full optimiser produces better plans and rearranges your week every time it runs - Calendar: Two-way sync with events the tool created marked as its own · you must never delete a real meeting, and the tool must know which events are its - Time: Instants in UTC with the user's zone and working hours declared · everything here is time arithmetic and it is where these tools break - Reconciliation: Detecting external changes before every solve · someone moving one of your placed blocks means something, and it is not that they were wrong ### Data model (create this before Phase 1) - tasks(id, title, duration_minutes, due_at, min_chunk_minutes, max_chunk_minutes, priority, completed_at) - habits(id, title, cadence, duration_minutes, window_start, window_end, days_json, priority) - placements(id, source_kind, source_id, calendar_event_id, starts_at, ends_at, solve_id, pinned, user_moved) - solves(id, at, reason, placements_changed, inputs_hash) · so you can explain why the plan changed - busy(id, calendar_id, starts_at, ends_at, external_event_id, is_ours) · the calendar as the solver sees it - working_hours(day_of_week, start_local, end_local, zone) - A placement records whether the user moved it, because a block someone dragged somewhere is a decision the solver must respect rather than undo. ### Phase 1 · Knowing when you are actually free Build: Sync calendars read only. Expand recurring events with their exceptions. Apply working hours per day in the user's zone. Handle all-day events, declined invitations and overlapping meetings. Trap: Treating a declined invitation or an all-day event as busy time removes most of the week from consideration, and treating tentative events as free schedules work on top of meetings people are probably attending. Decide the rule for each status explicitly, and expand recurrence with exceptions rather than assuming the series. Done when: Free time is computed correctly across a real calendar with recurring events, exceptions, declined invitations and all-day events. Both daylight saving transitions produce correct free time, tested on dates either side. Overlapping meetings do not double-count busy time. Working hours apply in the user's zone rather than the server's. Do not build yet: No tasks, solving or writing to calendars. ### Phase 2 · Placing work into the gaps Build: A deterministic solver placing tasks by deadline and priority, splitting them into chunks within their minimum and maximum sizes, and placing habits within their windows. Report what could not be placed and why. Trap: A solver that finds a slightly better arrangement each run will move your Tuesday afternoon every time it executes, and a plan that changes constantly is one nobody can work to. Prefer keeping existing placements over marginal improvements, and make identical inputs produce an identical plan. Done when: Identical inputs produce an identical plan, asserted in a test by solving twice. Tasks split into chunks respecting minimum and maximum sizes. A task that cannot be placed before its deadline is reported with the reason rather than dropped. A week with too much work reports what did not fit rather than overfilling. Do not build yet: No calendar writing or rescheduling yet. ### Phase 3 · Writing to a calendar you did not create Build: Create events for placements, marked identifiably as belonging to the tool. Never modify or delete an event the tool did not create. Handle the sync token expiring and a full resync without duplicating everything. Trap: Identifying your own events by title or colour means a user renaming a block makes it invisible to the tool, which then creates a duplicate, or worse, a coincidence of naming makes the tool delete a real meeting. Store the created event identifier and treat anything not in that set as untouchable. Done when: Placed blocks appear in the real calendar and are identifiable by stored identifier rather than by title. A real meeting is never modified or deleted, verified by a test that attempts it. A full resync after token expiry does not duplicate placements. Deleting the tool's events from the calendar is detected rather than causing duplicates. Do not build yet: No rescheduling or conflict handling yet. ### Phase 4 · When a meeting lands on your work Build: Detect external calendar changes and re-solve, preferring to move only what is affected. Respect blocks the user pinned or dragged. Never move a block that has already started or finished. Trap: Re-solving the whole week when one meeting appears rearranges work the user has already mentally committed to, and doing that a few times teaches them to ignore the calendar entirely. Move the displaced block and its dependents, leave everything else, and treat a block the user dragged as pinned. Done when: A new meeting moves only the displaced block, verified by comparing plans before and after. A block the user dragged is not moved by a later solve. A block in the past is never moved. A meeting cancelled frees the time without rearranging blocks that were already settled. Do not build yet: No reporting or preferences yet. ### Phase 5 · Explaining the plan, and living with it Build: Record a reason per placement and per solve. Show what changed since the last solve and why. Compare planned against completed to show whether the schedule is realistic. Handle a habit that is repeatedly skipped. Trap: A scheduler that keeps placing a habit you never do produces a calendar full of fiction, and the user stops trusting every other block as well. Detect a habit or task type that is consistently not completed, surface it, and offer to reduce or remove it rather than continuing to place it. Done when: Every placement has a stated reason and every solve records what changed. A habit skipped repeatedly is surfaced with an offer to change it. A comparison of planned against completed time is available for a period. The plan history explains a past week rather than only the current one. Do not build yet: Nothing after this. ### Out of scope (and why) - Meeting scheduling across several people's calendars, which is a substantially harder problem with a different failure mode. - Integrations with task tools, which is where the hosted product's practical value largely sits. - Machine-learned duration estimation, which needs far more completion data than a personal tool accumulates. - Team-wide scheduling policies, which turn a personal tool into a workplace one with different consent questions. ### README must contain - That the solver is deterministic so identical inputs give an identical plan, and why that matters more than optimality - That the tool identifies its own events by stored identifier and never touches anything else - That a block you drag is treated as pinned - That a new meeting re-solves minimally rather than rearranging the week - That a persistently skipped habit is surfaced rather than scheduled forever ===== ARCHITECTURE.md ===== # Architecture · Reclaim.ai ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | the work is solving, syncing and reconciling | | Solver | A deterministic greedy placement with a stability preference | a full optimiser produces better plans and rearranges your week every time it runs | | Calendar | Two-way sync with events the tool created marked as its own | you must never delete a real meeting, and the tool must know which events are its | | Time | Instants in UTC with the user's zone and working hours declared | everything here is time arithmetic and it is where these tools break | | Reconciliation | Detecting external changes before every solve | someone moving one of your placed blocks means something, and it is not that they were wrong | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Busy | Recurrence expansion, statuses and working hours. | Any implementation, provided exceptions and zones are handled rather than assumed. | | Solver | Deterministic placement with a stability preference. | Any algorithm, provided identical inputs give an identical plan. | | Calendar | Writing only the tool's own events. | Any provider, provided ownership is by stored identifier rather than by title. | | Reconciliation | Minimal re-solving after external changes. | Any strategy that moves what was displaced rather than the week. | | Explanation | Reasons, diffs and adherence. | Any presentation. The reasons are what make a plan arguable rather than imposed. | ## 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 · Placements, tasks and a copy of your calendar's busy time. - `CALENDAR_CLIENT_ID` · required, secret · Calendar access. Request the narrowest scope that allows creating and updating your own events. - `USER_TIMEZONE` · required · Working hours and habit windows are local. Everything stored is in UTC. - `MIN_CHUNK_MINUTES` · required · The smallest useful block. Below this a placement is an interruption rather than work. - `SOLVE_STABILITY_BIAS` · required · How strongly to prefer keeping existing placements over a marginally better plan. - `TOUCH_FOREIGN_EVENTS` · required · Present as a constant so the answer is visible in configuration. There is no other value. ## 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 · Reclaim.ai product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, A deterministic greedy placement with a stability preference, Two-way sync with events the tool created marked as its own, Instants in UTC with the user's zone and working hours declared, Detecting external changes before every solve. - 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 - Treating a declined invitation or an all-day event as busy time removes most of the week from consideration, and treating tentative events as free schedules work on top of meetings people are probably attending. Decide the rule for each status explicitly, and expand recurrence with exceptions rather than assuming the series. - A solver that finds a slightly better arrangement each run will move your Tuesday afternoon every time it executes, and a plan that changes constantly is one nobody can work to. Prefer keeping existing placements over marginal improvements, and make identical inputs produce an identical plan. - Identifying your own events by title or colour means a user renaming a block makes it invisible to the tool, which then creates a duplicate, or worse, a coincidence of naming makes the tool delete a real meeting. Store the created event identifier and treat anything not in that set as untouchable. - Re-solving the whole week when one meeting appears rearranges work the user has already mentally committed to, and doing that a few times teaches them to ignore the calendar entirely. Move the displaced block and its dependents, leave everything else, and treat a block the user dragged as pinned. - A scheduler that keeps placing a habit you never do produces a calendar full of fiction, and the user stops trusting every other block as well. Detect a habit or task type that is consistently not completed, surface it, and offer to reduce or remove it rather than continuing to place it. ===== MILESTONES.md ===== # Delivery milestones · Reclaim.ai Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Knowing when you are actually free A correct picture of free time, including the awkward cases. ### Steps 1. Expand recurrence with exceptions The series minus its cancellations. Assuming the series is the common bug. Files: `src/recurrence.ts`, `src/recurrence.test.ts` 2. Decide a rule per event status Declined, tentative, all-day, out of office. Each needs an explicit answer. Files: `src/busy.ts` 3. Apply working hours in the user's zone Not the server's, and not a fixed offset. Files: `src/hours.ts` ### Done when - [ ] Free time is correct across a real calendar - [ ] Both daylight transitions produce correct results - [ ] Overlapping meetings do not double-count - [ ] Event statuses follow their declared rules ### Watch out - Treating a declined invitation or an all-day event as busy time removes most of the week from consideration, and treating tentative events as free schedules work on top of meetings people are probably attending. Decide the rule for each status explicitly, and expand recurrence with exceptions rather than assuming the series. ## M2 · Placing work into the gaps Tasks and habits get placed sensibly and the same inputs give the same plan. ### Steps 1. Solve deterministically Same inputs, same plan, asserted in the suite. This is what makes it usable. Files: `src/solve.ts`, `src/solve.test.ts` 2. Respect chunk sizes Twenty minutes of deep work is not useful. The minimum exists for a reason. Files: `src/chunk.ts` 3. Report what did not fit and why Silently dropping a task before its deadline is the worst possible behaviour. Files: `src/report.ts` ### Done when - [ ] Identical inputs produce an identical plan - [ ] Chunk sizes are respected - [ ] Unplaceable tasks are reported with reasons - [ ] An overfull week reports overflow rather than overfilling ### Watch out - A solver that finds a slightly better arrangement each run will move your Tuesday afternoon every time it executes, and a plan that changes constantly is one nobody can work to. Prefer keeping existing placements over marginal improvements, and make identical inputs produce an identical plan. ## M3 · Writing to a calendar you did not create Placed blocks appear in your real calendar and nothing else is ever touched. ### Steps 1. Identify your events by stored identifier Never by title or colour. A rename must not orphan a block. Files: `src/calendar.ts` 2. Refuse to touch foreign events In the write path, with a test that attempts it and expects a refusal. Files: `src/calendar.ts`, `tests/foreign-events.test.ts` 3. Handle full resync idempotently Sync tokens expire. A resync that duplicates a week is very visible. Files: `src/sync.ts` ### Done when - [ ] Placed blocks appear and are identified by stored id - [ ] A foreign event cannot be modified or deleted - [ ] A full resync does not duplicate placements - [ ] Externally deleted blocks are detected ### Watch out - Identifying your own events by title or colour means a user renaming a block makes it invisible to the tool, which then creates a duplicate, or worse, a coincidence of naming makes the tool delete a real meeting. Store the created event identifier and treat anything not in that set as untouchable. ## M4 · When a meeting lands on your work A new meeting moves the affected block and nothing else. ### Steps 1. Re-solve minimally The displaced block and its dependents. Not the week. Files: `src/resolve.ts` 2. Treat a user move as a pin Someone dragging a block has expressed a preference. Undoing it is insulting. Files: `src/placements.ts` 3. Never move the past A block that started is history, whether or not the work happened. Files: `src/resolve.ts` ### Done when - [ ] A new meeting moves only what it displaced - [ ] A user-moved block is not moved again - [ ] Past blocks are never moved - [ ] A cancellation frees time without rearranging ### Watch out - Re-solving the whole week when one meeting appears rearranges work the user has already mentally committed to, and doing that a few times teaches them to ignore the calendar entirely. Move the displaced block and its dependents, leave everything else, and treat a block the user dragged as pinned. ## M5 · Explaining the plan, and living with it (production only) You can ask why something is where it is, and see whether the plan matched reality. ### Steps 1. Record a reason per placement Deadline, priority, habit window. So the plan can be questioned. Files: `src/reasons.ts` 2. Show the diff between solves What moved and why. Silent rearrangement is what erodes trust. Files: `src/diff.ts` 3. Detect persistently skipped work Continuing to schedule fiction makes the whole calendar untrustworthy. Files: `src/adherence.ts` ### Done when - [ ] Every placement has a stated reason - [ ] Solve diffs show what moved and why - [ ] A persistently skipped habit is surfaced - [ ] Planned against completed is available per period ### Watch out - A scheduler that keeps placing a habit you never do produces a calendar full of fiction, and the user stops trusting every other block as well. Detect a habit or task type that is consistently not completed, surface it, and offer to reduce or remove it rather than continuing to place it. ===== OPERATIONS.md ===== # Operations · Reclaim.ai ## Backup Tasks and habits are the irreplaceable data. Placements regenerate from a solve, and the calendar itself is the provider's responsibility, so back up the definitions rather than the plan. ## Restore Restore and confirm placements reconcile against the real calendar rather than duplicating, since a restore followed by a solve is exactly the situation that creates a second copy of the week. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Solve stability is the metric that matters: the number of placements changed per solve should be near zero when nothing external changed. A rising number means the solver is not preferring the existing plan. ## Incident checklist Duplicated blocks in the calendar mean ownership tracking failed, usually after a token expiry and full resync. A real meeting being modified is the most serious possible failure and the write path refusal is the control. 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 - [ ] Identical inputs are confirmed to produce an identical plan - [ ] A foreign event is confirmed impossible to modify or delete - [ ] A full resync is confirmed not to duplicate placements - [ ] A user-moved block is confirmed not to be moved again - [ ] Both daylight transitions are confirmed to produce correct free time ## Launch constraint Do not market omitted Reclaim.ai 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. Placements, tasks and a copy of your calendar's busy time. DATABASE_URL=postgres://localhost:5432/schedule # Required · secret. Calendar access. Request the narrowest scope that allows creating and updating your own events. CALENDAR_CLIENT_ID=a provider client id # Required. Working hours and habit windows are local. Everything stored is in UTC. USER_TIMEZONE=Europe/London # Required. The smallest useful block. Below this a placement is an interruption rather than work. MIN_CHUNK_MINUTES=45 # Required. How strongly to prefer keeping existing placements over a marginally better plan. SOLVE_STABILITY_BIAS=high # Required. Present as a constant so the answer is visible in configuration. There is no other value. TOUCH_FOREIGN_EVENTS=never
# Reclaim.ai · indie build A scheduling assistant whose core is a constraint solver over your calendar: fit tasks and recurring habits into free time, respect deadlines and working hours, and rearrange when a meeting lands on top of something without producing a different plan every time it runs. 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 with Fastify and PostgreSQL | the work is solving, syncing and reconciling | | Solver | A deterministic greedy placement with a stability preference | a full optimiser produces better plans and rearranges your week every time it runs | | Calendar | Two-way sync with events the tool created marked as its own | you must never delete a real meeting, and the tool must know which events are its | | Time | Instants in UTC with the user's zone and working hours declared | everything here is time arithmetic and it is where these tools break | | Reconciliation | Detecting external changes before every solve | someone moving one of your placed blocks means something, and it is not that they were wrong | ## 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 server, the solver and the sync workers. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Placements, solves and busy time with correct interval queries. - Get it: Docker or your package manager. - Verify: psql connects and a range overlap query returns correctly - [ ] **A calendar account with write access** · free - Why: Phase 3 writes real events into a real calendar, and the failure modes only appear against a live account with existing meetings. - Get it: A test calendar with a month of realistic meetings, including recurring series with exceptions and some declined invitations. - Verify: Events can be created and the tool's own events are identifiable by stored id - [ ] **A realistic week of your own commitments** · free - Why: Solver behaviour is entirely determined by how fragmented a real calendar is, and a test calendar with three meetings will make any solver look excellent. - Get it: Use your actual calendar, or reconstruct a genuinely busy week including back-to-back meetings and a day with almost no free time. - Verify: A week exists with at least fifteen meetings and one nearly full day - [ ] **Your rules for each event status** · free - Why: Declined, tentative, all-day and out-of-office events each need an answer before the solver exists, and changing the answer later silently changes every plan. - Get it: Write down whether each status blocks time, and what working hours actually mean for you including whether evenings count. - Verify: A written note gives a rule for every status your calendar produces ## 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-person meeting scheduling. - Integrations with external task tools. - Machine-learned duration estimation. - Team-wide scheduling policies. - team coordination - smart rescheduling polish - scheduling links - calendar edge cases - uptime If one of those is essential to you, that is the reason to keep paying for Reclaim.ai, and the README should say so rather than pretend.
# Build brief · Reclaim.ai 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 places your tasks and habits into the gaps in your calendar to replace Reclaim.ai. 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 with Fastify and PostgreSQL · the work is solving, syncing and reconciling - Solver: A deterministic greedy placement with a stability preference · a full optimiser produces better plans and rearranges your week every time it runs - Calendar: Two-way sync with events the tool created marked as its own · you must never delete a real meeting, and the tool must know which events are its - Time: Instants in UTC with the user's zone and working hours declared · everything here is time arithmetic and it is where these tools break - Reconciliation: Detecting external changes before every solve · someone moving one of your placed blocks means something, and it is not that they were wrong ### Data model (create this before Phase 1) - tasks(id, title, duration_minutes, due_at, min_chunk_minutes, max_chunk_minutes, priority, completed_at) - habits(id, title, cadence, duration_minutes, window_start, window_end, days_json, priority) - placements(id, source_kind, source_id, calendar_event_id, starts_at, ends_at, solve_id, pinned, user_moved) - solves(id, at, reason, placements_changed, inputs_hash) · so you can explain why the plan changed - busy(id, calendar_id, starts_at, ends_at, external_event_id, is_ours) · the calendar as the solver sees it - working_hours(day_of_week, start_local, end_local, zone) - A placement records whether the user moved it, because a block someone dragged somewhere is a decision the solver must respect rather than undo. ### Phase 1 · Knowing when you are actually free Build: Sync calendars read only. Expand recurring events with their exceptions. Apply working hours per day in the user's zone. Handle all-day events, declined invitations and overlapping meetings. Trap: Treating a declined invitation or an all-day event as busy time removes most of the week from consideration, and treating tentative events as free schedules work on top of meetings people are probably attending. Decide the rule for each status explicitly, and expand recurrence with exceptions rather than assuming the series. Done when: Free time is computed correctly across a real calendar with recurring events, exceptions, declined invitations and all-day events. Both daylight saving transitions produce correct free time, tested on dates either side. Overlapping meetings do not double-count busy time. Working hours apply in the user's zone rather than the server's. Do not build yet: No tasks, solving or writing to calendars. ### Phase 2 · Placing work into the gaps Build: A deterministic solver placing tasks by deadline and priority, splitting them into chunks within their minimum and maximum sizes, and placing habits within their windows. Report what could not be placed and why. Trap: A solver that finds a slightly better arrangement each run will move your Tuesday afternoon every time it executes, and a plan that changes constantly is one nobody can work to. Prefer keeping existing placements over marginal improvements, and make identical inputs produce an identical plan. Done when: Identical inputs produce an identical plan, asserted in a test by solving twice. Tasks split into chunks respecting minimum and maximum sizes. A task that cannot be placed before its deadline is reported with the reason rather than dropped. A week with too much work reports what did not fit rather than overfilling. Do not build yet: No calendar writing or rescheduling yet. ### Phase 3 · Writing to a calendar you did not create Build: Create events for placements, marked identifiably as belonging to the tool. Never modify or delete an event the tool did not create. Handle the sync token expiring and a full resync without duplicating everything. Trap: Identifying your own events by title or colour means a user renaming a block makes it invisible to the tool, which then creates a duplicate, or worse, a coincidence of naming makes the tool delete a real meeting. Store the created event identifier and treat anything not in that set as untouchable. Done when: Placed blocks appear in the real calendar and are identifiable by stored identifier rather than by title. A real meeting is never modified or deleted, verified by a test that attempts it. A full resync after token expiry does not duplicate placements. Deleting the tool's events from the calendar is detected rather than causing duplicates. Do not build yet: No rescheduling or conflict handling yet. ### Phase 4 · When a meeting lands on your work Build: Detect external calendar changes and re-solve, preferring to move only what is affected. Respect blocks the user pinned or dragged. Never move a block that has already started or finished. Trap: Re-solving the whole week when one meeting appears rearranges work the user has already mentally committed to, and doing that a few times teaches them to ignore the calendar entirely. Move the displaced block and its dependents, leave everything else, and treat a block the user dragged as pinned. Done when: A new meeting moves only the displaced block, verified by comparing plans before and after. A block the user dragged is not moved by a later solve. A block in the past is never moved. A meeting cancelled frees the time without rearranging blocks that were already settled. Do not build yet: No reporting or preferences yet. ### Phase 5 · Explaining the plan, and living with it Build: Record a reason per placement and per solve. Show what changed since the last solve and why. Compare planned against completed to show whether the schedule is realistic. Handle a habit that is repeatedly skipped. Trap: A scheduler that keeps placing a habit you never do produces a calendar full of fiction, and the user stops trusting every other block as well. Detect a habit or task type that is consistently not completed, surface it, and offer to reduce or remove it rather than continuing to place it. Done when: Every placement has a stated reason and every solve records what changed. A habit skipped repeatedly is surfaced with an offer to change it. A comparison of planned against completed time is available for a period. The plan history explains a past week rather than only the current one. Do not build yet: Nothing after this. ### Out of scope (and why) - Meeting scheduling across several people's calendars, which is a substantially harder problem with a different failure mode. - Integrations with task tools, which is where the hosted product's practical value largely sits. - Machine-learned duration estimation, which needs far more completion data than a personal tool accumulates. - Team-wide scheduling policies, which turn a personal tool into a workplace one with different consent questions. ### README must contain - That the solver is deterministic so identical inputs give an identical plan, and why that matters more than optimality - That the tool identifies its own events by stored identifier and never touches anything else - That a block you drag is treated as pinned - That a new meeting re-solves minimally rather than rearranging the week - That a persistently skipped habit is surfaced rather than scheduled forever
# Agent instructions · Reclaim.ai indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, A deterministic greedy placement with a stability preference, Two-way sync with events the tool created marked as its own, Instants in UTC with the user's zone and working hours declared, Detecting external changes before every solve. 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 - Treating a declined invitation or an all-day event as busy time removes most of the week from consideration, and treating tentative events as free schedules work on top of meetings people are probably attending. Decide the rule for each status explicitly, and expand recurrence with exceptions rather than assuming the series. - A solver that finds a slightly better arrangement each run will move your Tuesday afternoon every time it executes, and a plan that changes constantly is one nobody can work to. Prefer keeping existing placements over marginal improvements, and make identical inputs produce an identical plan. - Identifying your own events by title or colour means a user renaming a block makes it invisible to the tool, which then creates a duplicate, or worse, a coincidence of naming makes the tool delete a real meeting. Store the created event identifier and treat anything not in that set as untouchable. - Re-solving the whole week when one meeting appears rearranges work the user has already mentally committed to, and doing that a few times teaches them to ignore the calendar entirely. Move the displaced block and its dependents, leave everything else, and treat a block the user dragged as pinned.
# Build plan · Reclaim.ai A scheduling assistant whose core is a constraint solver over your calendar: fit tasks and recurring habits into free time, respect deadlines and working hours, and rearrange when a meeting lands on top of something without producing a different plan every time it runs. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Knowing when you are actually free A correct picture of free time, including the awkward cases. ### Steps 1. Expand recurrence with exceptions The series minus its cancellations. Assuming the series is the common bug. Files: `src/recurrence.ts`, `src/recurrence.test.ts` 2. Decide a rule per event status Declined, tentative, all-day, out of office. Each needs an explicit answer. Files: `src/busy.ts` 3. Apply working hours in the user's zone Not the server's, and not a fixed offset. Files: `src/hours.ts` ### Done when - [ ] Free time is correct across a real calendar - [ ] Both daylight transitions produce correct results - [ ] Overlapping meetings do not double-count - [ ] Event statuses follow their declared rules ### Watch out - Treating a declined invitation or an all-day event as busy time removes most of the week from consideration, and treating tentative events as free schedules work on top of meetings people are probably attending. Decide the rule for each status explicitly, and expand recurrence with exceptions rather than assuming the series. ## Phase 2 · Placing work into the gaps Tasks and habits get placed sensibly and the same inputs give the same plan. ### Steps 1. Solve deterministically Same inputs, same plan, asserted in the suite. This is what makes it usable. Files: `src/solve.ts`, `src/solve.test.ts` 2. Respect chunk sizes Twenty minutes of deep work is not useful. The minimum exists for a reason. Files: `src/chunk.ts` 3. Report what did not fit and why Silently dropping a task before its deadline is the worst possible behaviour. Files: `src/report.ts` ### Done when - [ ] Identical inputs produce an identical plan - [ ] Chunk sizes are respected - [ ] Unplaceable tasks are reported with reasons - [ ] An overfull week reports overflow rather than overfilling ### Watch out - A solver that finds a slightly better arrangement each run will move your Tuesday afternoon every time it executes, and a plan that changes constantly is one nobody can work to. Prefer keeping existing placements over marginal improvements, and make identical inputs produce an identical plan. ## Phase 3 · Writing to a calendar you did not create Placed blocks appear in your real calendar and nothing else is ever touched. ### Steps 1. Identify your events by stored identifier Never by title or colour. A rename must not orphan a block. Files: `src/calendar.ts` 2. Refuse to touch foreign events In the write path, with a test that attempts it and expects a refusal. Files: `src/calendar.ts`, `tests/foreign-events.test.ts` 3. Handle full resync idempotently Sync tokens expire. A resync that duplicates a week is very visible. Files: `src/sync.ts` ### Done when - [ ] Placed blocks appear and are identified by stored id - [ ] A foreign event cannot be modified or deleted - [ ] A full resync does not duplicate placements - [ ] Externally deleted blocks are detected ### Watch out - Identifying your own events by title or colour means a user renaming a block makes it invisible to the tool, which then creates a duplicate, or worse, a coincidence of naming makes the tool delete a real meeting. Store the created event identifier and treat anything not in that set as untouchable. ## Phase 4 · When a meeting lands on your work A new meeting moves the affected block and nothing else. ### Steps 1. Re-solve minimally The displaced block and its dependents. Not the week. Files: `src/resolve.ts` 2. Treat a user move as a pin Someone dragging a block has expressed a preference. Undoing it is insulting. Files: `src/placements.ts` 3. Never move the past A block that started is history, whether or not the work happened. Files: `src/resolve.ts` ### Done when - [ ] A new meeting moves only what it displaced - [ ] A user-moved block is not moved again - [ ] Past blocks are never moved - [ ] A cancellation frees time without rearranging ### Watch out - Re-solving the whole week when one meeting appears rearranges work the user has already mentally committed to, and doing that a few times teaches them to ignore the calendar entirely. Move the displaced block and its dependents, leave everything else, and treat a block the user dragged as pinned. ## Not in this build - Multi-person meeting scheduling. - Integrations with external task tools. - Machine-learned duration estimation. - Team-wide scheduling policies. ## After v1, if you want it - Buffer time around meetings, which most people need more than more work blocks - Travel time between events with locations, once the busy model is trustworthy - Focus mode integration, so a placed block actually protects itself
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Placements, tasks and a copy of your calendar's busy time. DATABASE_URL=postgres://localhost:5432/schedule # Required · secret. Calendar access. Request the narrowest scope that allows creating and updating your own events. CALENDAR_CLIENT_ID=a provider client id # Required. Working hours and habit windows are local. Everything stored is in UTC. USER_TIMEZONE=Europe/London # Required. The smallest useful block. Below this a placement is an interruption rather than work. MIN_CHUNK_MINUTES=45 # Required. How strongly to prefer keeping existing placements over a marginally better plan. SOLVE_STABILITY_BIAS=high # Required. Present as a constant so the answer is visible in configuration. There is no other value. TOUCH_FOREIGN_EVENTS=never
# Reclaim.ai · product brief ## Problem A personal auto-blocker is buildable, but two-way calendar sync, scheduling links, team features, and conflict-safe automation are upkeep-heavy. ## Product outcome A scheduler that fills your gaps with the work you actually have, gives the same answer twice, moves only what a new meeting displaced, and cannot touch a real meeting. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Google Calendar OAuth - scheduling logic - hosted backend - background workers - notifications ## Explicit non-goals for v1 - Multi-person meeting scheduling. - Integrations with external task tools. - Machine-learned duration estimation. - Team-wide scheduling policies. - team coordination - smart rescheduling polish - scheduling links - calendar edge cases - uptime ## Success criteria - Identical inputs are confirmed to produce an identical plan - A foreign event is confirmed impossible to modify or delete - A full resync is confirmed not to duplicate placements - A user-moved block is confirmed not to be moved again - Both daylight transitions are confirmed to produce correct free time
# Build brief · Reclaim.ai 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 places your tasks and habits into the gaps in your calendar to replace Reclaim.ai. 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 with Fastify and PostgreSQL · the work is solving, syncing and reconciling - Solver: A deterministic greedy placement with a stability preference · a full optimiser produces better plans and rearranges your week every time it runs - Calendar: Two-way sync with events the tool created marked as its own · you must never delete a real meeting, and the tool must know which events are its - Time: Instants in UTC with the user's zone and working hours declared · everything here is time arithmetic and it is where these tools break - Reconciliation: Detecting external changes before every solve · someone moving one of your placed blocks means something, and it is not that they were wrong ### Data model (create this before Phase 1) - tasks(id, title, duration_minutes, due_at, min_chunk_minutes, max_chunk_minutes, priority, completed_at) - habits(id, title, cadence, duration_minutes, window_start, window_end, days_json, priority) - placements(id, source_kind, source_id, calendar_event_id, starts_at, ends_at, solve_id, pinned, user_moved) - solves(id, at, reason, placements_changed, inputs_hash) · so you can explain why the plan changed - busy(id, calendar_id, starts_at, ends_at, external_event_id, is_ours) · the calendar as the solver sees it - working_hours(day_of_week, start_local, end_local, zone) - A placement records whether the user moved it, because a block someone dragged somewhere is a decision the solver must respect rather than undo. ### Phase 1 · Knowing when you are actually free Build: Sync calendars read only. Expand recurring events with their exceptions. Apply working hours per day in the user's zone. Handle all-day events, declined invitations and overlapping meetings. Trap: Treating a declined invitation or an all-day event as busy time removes most of the week from consideration, and treating tentative events as free schedules work on top of meetings people are probably attending. Decide the rule for each status explicitly, and expand recurrence with exceptions rather than assuming the series. Done when: Free time is computed correctly across a real calendar with recurring events, exceptions, declined invitations and all-day events. Both daylight saving transitions produce correct free time, tested on dates either side. Overlapping meetings do not double-count busy time. Working hours apply in the user's zone rather than the server's. Do not build yet: No tasks, solving or writing to calendars. ### Phase 2 · Placing work into the gaps Build: A deterministic solver placing tasks by deadline and priority, splitting them into chunks within their minimum and maximum sizes, and placing habits within their windows. Report what could not be placed and why. Trap: A solver that finds a slightly better arrangement each run will move your Tuesday afternoon every time it executes, and a plan that changes constantly is one nobody can work to. Prefer keeping existing placements over marginal improvements, and make identical inputs produce an identical plan. Done when: Identical inputs produce an identical plan, asserted in a test by solving twice. Tasks split into chunks respecting minimum and maximum sizes. A task that cannot be placed before its deadline is reported with the reason rather than dropped. A week with too much work reports what did not fit rather than overfilling. Do not build yet: No calendar writing or rescheduling yet. ### Phase 3 · Writing to a calendar you did not create Build: Create events for placements, marked identifiably as belonging to the tool. Never modify or delete an event the tool did not create. Handle the sync token expiring and a full resync without duplicating everything. Trap: Identifying your own events by title or colour means a user renaming a block makes it invisible to the tool, which then creates a duplicate, or worse, a coincidence of naming makes the tool delete a real meeting. Store the created event identifier and treat anything not in that set as untouchable. Done when: Placed blocks appear in the real calendar and are identifiable by stored identifier rather than by title. A real meeting is never modified or deleted, verified by a test that attempts it. A full resync after token expiry does not duplicate placements. Deleting the tool's events from the calendar is detected rather than causing duplicates. Do not build yet: No rescheduling or conflict handling yet. ### Phase 4 · When a meeting lands on your work Build: Detect external calendar changes and re-solve, preferring to move only what is affected. Respect blocks the user pinned or dragged. Never move a block that has already started or finished. Trap: Re-solving the whole week when one meeting appears rearranges work the user has already mentally committed to, and doing that a few times teaches them to ignore the calendar entirely. Move the displaced block and its dependents, leave everything else, and treat a block the user dragged as pinned. Done when: A new meeting moves only the displaced block, verified by comparing plans before and after. A block the user dragged is not moved by a later solve. A block in the past is never moved. A meeting cancelled frees the time without rearranging blocks that were already settled. Do not build yet: No reporting or preferences yet. ### Phase 5 · Explaining the plan, and living with it Build: Record a reason per placement and per solve. Show what changed since the last solve and why. Compare planned against completed to show whether the schedule is realistic. Handle a habit that is repeatedly skipped. Trap: A scheduler that keeps placing a habit you never do produces a calendar full of fiction, and the user stops trusting every other block as well. Detect a habit or task type that is consistently not completed, surface it, and offer to reduce or remove it rather than continuing to place it. Done when: Every placement has a stated reason and every solve records what changed. A habit skipped repeatedly is surfaced with an offer to change it. A comparison of planned against completed time is available for a period. The plan history explains a past week rather than only the current one. Do not build yet: Nothing after this. ### Out of scope (and why) - Meeting scheduling across several people's calendars, which is a substantially harder problem with a different failure mode. - Integrations with task tools, which is where the hosted product's practical value largely sits. - Machine-learned duration estimation, which needs far more completion data than a personal tool accumulates. - Team-wide scheduling policies, which turn a personal tool into a workplace one with different consent questions. ### README must contain - That the solver is deterministic so identical inputs give an identical plan, and why that matters more than optimality - That the tool identifies its own events by stored identifier and never touches anything else - That a block you drag is treated as pinned - That a new meeting re-solves minimally rather than rearranging the week - That a persistently skipped habit is surfaced rather than scheduled forever
# Architecture · Reclaim.ai ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | the work is solving, syncing and reconciling | | Solver | A deterministic greedy placement with a stability preference | a full optimiser produces better plans and rearranges your week every time it runs | | Calendar | Two-way sync with events the tool created marked as its own | you must never delete a real meeting, and the tool must know which events are its | | Time | Instants in UTC with the user's zone and working hours declared | everything here is time arithmetic and it is where these tools break | | Reconciliation | Detecting external changes before every solve | someone moving one of your placed blocks means something, and it is not that they were wrong | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Busy | Recurrence expansion, statuses and working hours. | Any implementation, provided exceptions and zones are handled rather than assumed. | | Solver | Deterministic placement with a stability preference. | Any algorithm, provided identical inputs give an identical plan. | | Calendar | Writing only the tool's own events. | Any provider, provided ownership is by stored identifier rather than by title. | | Reconciliation | Minimal re-solving after external changes. | Any strategy that moves what was displaced rather than the week. | | Explanation | Reasons, diffs and adherence. | Any presentation. The reasons are what make a plan arguable rather than imposed. | ## 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 · Placements, tasks and a copy of your calendar's busy time. - `CALENDAR_CLIENT_ID` · required, secret · Calendar access. Request the narrowest scope that allows creating and updating your own events. - `USER_TIMEZONE` · required · Working hours and habit windows are local. Everything stored is in UTC. - `MIN_CHUNK_MINUTES` · required · The smallest useful block. Below this a placement is an interruption rather than work. - `SOLVE_STABILITY_BIAS` · required · How strongly to prefer keeping existing placements over a marginally better plan. - `TOUCH_FOREIGN_EVENTS` · required · Present as a constant so the answer is visible in configuration. There is no other value. ## 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 · Reclaim.ai product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, A deterministic greedy placement with a stability preference, Two-way sync with events the tool created marked as its own, Instants in UTC with the user's zone and working hours declared, Detecting external changes before every solve. - 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 - Treating a declined invitation or an all-day event as busy time removes most of the week from consideration, and treating tentative events as free schedules work on top of meetings people are probably attending. Decide the rule for each status explicitly, and expand recurrence with exceptions rather than assuming the series. - A solver that finds a slightly better arrangement each run will move your Tuesday afternoon every time it executes, and a plan that changes constantly is one nobody can work to. Prefer keeping existing placements over marginal improvements, and make identical inputs produce an identical plan. - Identifying your own events by title or colour means a user renaming a block makes it invisible to the tool, which then creates a duplicate, or worse, a coincidence of naming makes the tool delete a real meeting. Store the created event identifier and treat anything not in that set as untouchable. - Re-solving the whole week when one meeting appears rearranges work the user has already mentally committed to, and doing that a few times teaches them to ignore the calendar entirely. Move the displaced block and its dependents, leave everything else, and treat a block the user dragged as pinned. - A scheduler that keeps placing a habit you never do produces a calendar full of fiction, and the user stops trusting every other block as well. Detect a habit or task type that is consistently not completed, surface it, and offer to reduce or remove it rather than continuing to place it.
# Delivery milestones · Reclaim.ai Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Knowing when you are actually free A correct picture of free time, including the awkward cases. ### Steps 1. Expand recurrence with exceptions The series minus its cancellations. Assuming the series is the common bug. Files: `src/recurrence.ts`, `src/recurrence.test.ts` 2. Decide a rule per event status Declined, tentative, all-day, out of office. Each needs an explicit answer. Files: `src/busy.ts` 3. Apply working hours in the user's zone Not the server's, and not a fixed offset. Files: `src/hours.ts` ### Done when - [ ] Free time is correct across a real calendar - [ ] Both daylight transitions produce correct results - [ ] Overlapping meetings do not double-count - [ ] Event statuses follow their declared rules ### Watch out - Treating a declined invitation or an all-day event as busy time removes most of the week from consideration, and treating tentative events as free schedules work on top of meetings people are probably attending. Decide the rule for each status explicitly, and expand recurrence with exceptions rather than assuming the series. ## M2 · Placing work into the gaps Tasks and habits get placed sensibly and the same inputs give the same plan. ### Steps 1. Solve deterministically Same inputs, same plan, asserted in the suite. This is what makes it usable. Files: `src/solve.ts`, `src/solve.test.ts` 2. Respect chunk sizes Twenty minutes of deep work is not useful. The minimum exists for a reason. Files: `src/chunk.ts` 3. Report what did not fit and why Silently dropping a task before its deadline is the worst possible behaviour. Files: `src/report.ts` ### Done when - [ ] Identical inputs produce an identical plan - [ ] Chunk sizes are respected - [ ] Unplaceable tasks are reported with reasons - [ ] An overfull week reports overflow rather than overfilling ### Watch out - A solver that finds a slightly better arrangement each run will move your Tuesday afternoon every time it executes, and a plan that changes constantly is one nobody can work to. Prefer keeping existing placements over marginal improvements, and make identical inputs produce an identical plan. ## M3 · Writing to a calendar you did not create Placed blocks appear in your real calendar and nothing else is ever touched. ### Steps 1. Identify your events by stored identifier Never by title or colour. A rename must not orphan a block. Files: `src/calendar.ts` 2. Refuse to touch foreign events In the write path, with a test that attempts it and expects a refusal. Files: `src/calendar.ts`, `tests/foreign-events.test.ts` 3. Handle full resync idempotently Sync tokens expire. A resync that duplicates a week is very visible. Files: `src/sync.ts` ### Done when - [ ] Placed blocks appear and are identified by stored id - [ ] A foreign event cannot be modified or deleted - [ ] A full resync does not duplicate placements - [ ] Externally deleted blocks are detected ### Watch out - Identifying your own events by title or colour means a user renaming a block makes it invisible to the tool, which then creates a duplicate, or worse, a coincidence of naming makes the tool delete a real meeting. Store the created event identifier and treat anything not in that set as untouchable. ## M4 · When a meeting lands on your work A new meeting moves the affected block and nothing else. ### Steps 1. Re-solve minimally The displaced block and its dependents. Not the week. Files: `src/resolve.ts` 2. Treat a user move as a pin Someone dragging a block has expressed a preference. Undoing it is insulting. Files: `src/placements.ts` 3. Never move the past A block that started is history, whether or not the work happened. Files: `src/resolve.ts` ### Done when - [ ] A new meeting moves only what it displaced - [ ] A user-moved block is not moved again - [ ] Past blocks are never moved - [ ] A cancellation frees time without rearranging ### Watch out - Re-solving the whole week when one meeting appears rearranges work the user has already mentally committed to, and doing that a few times teaches them to ignore the calendar entirely. Move the displaced block and its dependents, leave everything else, and treat a block the user dragged as pinned. ## M5 · Explaining the plan, and living with it (production only) You can ask why something is where it is, and see whether the plan matched reality. ### Steps 1. Record a reason per placement Deadline, priority, habit window. So the plan can be questioned. Files: `src/reasons.ts` 2. Show the diff between solves What moved and why. Silent rearrangement is what erodes trust. Files: `src/diff.ts` 3. Detect persistently skipped work Continuing to schedule fiction makes the whole calendar untrustworthy. Files: `src/adherence.ts` ### Done when - [ ] Every placement has a stated reason - [ ] Solve diffs show what moved and why - [ ] A persistently skipped habit is surfaced - [ ] Planned against completed is available per period ### Watch out - A scheduler that keeps placing a habit you never do produces a calendar full of fiction, and the user stops trusting every other block as well. Detect a habit or task type that is consistently not completed, surface it, and offer to reduce or remove it rather than continuing to place it.
# Operations · Reclaim.ai ## Backup Tasks and habits are the irreplaceable data. Placements regenerate from a solve, and the calendar itself is the provider's responsibility, so back up the definitions rather than the plan. ## Restore Restore and confirm placements reconcile against the real calendar rather than duplicating, since a restore followed by a solve is exactly the situation that creates a second copy of the week. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Solve stability is the metric that matters: the number of placements changed per solve should be near zero when nothing external changed. A rising number means the solver is not preferring the existing plan. ## Incident checklist Duplicated blocks in the calendar mean ownership tracking failed, usually after a token expiry and full resync. A real meeting being modified is the most serious possible failure and the write path refusal is the control. 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 - [ ] Identical inputs are confirmed to produce an identical plan - [ ] A foreign event is confirmed impossible to modify or delete - [ ] A full resync is confirmed not to duplicate placements - [ ] A user-moved block is confirmed not to be moved again - [ ] Both daylight transitions are confirmed to produce correct free time ## Launch constraint Do not market omitted Reclaim.ai 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. Placements, tasks and a copy of your calendar's busy time. DATABASE_URL=postgres://localhost:5432/schedule # Required · secret. Calendar access. Request the narrowest scope that allows creating and updating your own events. CALENDAR_CLIENT_ID=a provider client id # Required. Working hours and habit windows are local. Everything stored is in UTC. USER_TIMEZONE=Europe/London # Required. The smallest useful block. Below this a placement is an interruption rather than work. MIN_CHUNK_MINUTES=45 # Required. How strongly to prefer keeping existing placements over a marginally better plan. SOLVE_STABILITY_BIAS=high # Required. Present as a constant so the answer is visible in configuration. There is no other value. TOUCH_FOREIGN_EVENTS=never
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay because calendar automation is only valuable if it never makes embarrassing mistakes.
xteam coordination
xsmart rescheduling polish
xscheduling links
xcalendar edge cases
xuptime
Reclaim.ai pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| lite | $0/user | $0/user | 5 AI agents; 1-user team; 1-week scheduling range; 1 calendar sync; 1 scheduling link. |
| starter | $12/user | $10/user | Up to 10 seats; 10 AI agents/seat; 8-week scheduling range; 3 calendar syncs; 3 scheduling links. |
| business | $18/user | $15/user | Up to 100 seats; 100 AI agents/seat; 12-week range; unlimited calendar syncs and scheduling links. |
| enterprise | — | $22/user | 100+ seats; unlimited AI agents; 12-week scheduling range. |
free tier5 AI agents; 1-user team; 1-week scheduling range; 1 calendar sync; 1 scheduling link.
billingLite free; Starter/Business monthly + annual; Enterprise annual only
hidden costsCumulative attendee users are billed separately: annual Starter packs cost $8/$16/$24/$32 per month for 3/6/9/12 attendee users; annual Business packs cost $12/$24/$36/$48 for 6/12/18/24. Monthly packs cost more. Each pack includes one free paid seat.
verified 2026-08-14 · source ↗
Vibecode Reclaim.ai
Kinda. The core of Reclaim.ai is buildable in a weekend with the prompt on this page, but there are real gaps: team coordination, smart rescheduling polish. Read the honest list above before committing.
How much does Reclaim.ai cost?
Reclaim.ai costs about $12/month (Starter, checked 2026-07-30), which is $144 per year.
What do I lose by replacing Reclaim.ai?
Honestly: team coordination; smart rescheduling polish; scheduling links; calendar edge cases; uptime. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Reclaim.ai?
Yes: Cal.com (Open-source scheduling platform; useful prior art for calendar OAuth and scheduling workfl). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.