Vibecode SkedPal
track this build4 phases, 12 steps, beginner friendly0%The visible automatic scheduling loop is buildable, but a credible replacement needs more than the first screen. SkedPal earns its keep through polish, OS integration, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of SkedPal. 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 ===== # SkedPal · indie build An automatic scheduler whose hard problems are reading a calendar as busy time without learning what is in it, producing a plan that is stable and explainable rather than freshly shuffled every hour, and writing blocks back without ever touching an event it did not create. 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 | Tauri 2 with React and TypeScript, SQLite beneath | a desktop tool that holds your task list locally | | Calendar | Free and busy intervals only, through the provider's own busy query | the scheduler needs to know when you are busy and never why | | Solver | A deterministic pass over an explicit priority order with a recorded tie-break | a plan you cannot explain is a plan you will not follow | | Time | Instants stored, rules written in a named zone, expanded per date | the clocks change and a scheduler that ignores that books an hour that does not exist | | Write-back | Only events the tool created, tagged and tracked by their stored identifier | the calendar belongs to the person, not to the scheduler | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The Rust toolchain for Tauri 2** · free - Why: Builds the desktop shell that holds the task list locally. - Get it: rustup.rs, then the Tauri prerequisites for your platform. - Verify: cargo tauri info reports no missing dependencies - [ ] **A calendar account with a spare calendar for the tool's blocks** · free - Why: Writing into a dedicated calendar keeps every mistake visible and reversible, which matters more here than anywhere else in the build. - Get it: Create an empty calendar in your account and register an application with the narrowest scope that reads busy time and writes events. - Verify: The busy query returns intervals and a test event can be created in the new calendar - [ ] **Your rule for the clock change days** · free - Why: Both transitions are asserted in the suite, and deciding after seeing failures means choosing whatever passes. - Get it: Write down what a rule landing in the missing hour does and what happens on the repeated one. - Verify: Both are named in the README and in the tests - [ ] **Two weeks of your own real tasks and commitments** · free - Why: Determinism and stability only look like features against a messy real week; a tidy fixture hides both. - Get it: Put your actual flexible work into the tool with honest durations and due dates. - Verify: A plan over the real week reports its shortfall rather than fitting suspiciously well ## 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: - Reading event titles, attendees or locations. - Cross-device sync. - Team and shared scheduling. - Any promise about what you will finish. - calendar-provider edge cases and timezone correctness - cross-device sync - deep operating-system edge-case coverage - polished shortcuts and accessibility - vendor-maintained updates If one of those is essential to you, that is the reason to keep paying for SkedPal, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · SkedPal 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 an auto-scheduler that fits flexible tasks into the gaps in your calendar and can explain every placement to replace SkedPal. 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: Tauri 2 with React and TypeScript, SQLite beneath · a desktop tool that holds your task list locally - Calendar: Free and busy intervals only, through the provider's own busy query · the scheduler needs to know when you are busy and never why - Solver: A deterministic pass over an explicit priority order with a recorded tie-break · a plan you cannot explain is a plan you will not follow - Time: Instants stored, rules written in a named zone, expanded per date · the clocks change and a scheduler that ignores that books an hour that does not exist - Write-back: Only events the tool created, tagged and tracked by their stored identifier · the calendar belongs to the person, not to the scheduler ### Data model (create this before Phase 1) - tasks(id, title, total_minutes, remaining_minutes, due_at, earliest_at, priority, min_chunk_minutes, max_chunk_minutes, time_map_id, state) - time_maps(id, name, timezone, rules_json) · when a kind of work is allowed, written in the map's own zone - busy(id, calendar_id, starts_at, ends_at, external_id, is_ours) · start and end and nothing else, because nothing else is fetched - plans(id, computed_at, horizon_days, inputs_sha256, solver_version, unplaceable_json) · the hash makes an identical rerun provable - blocks(id, plan_id, task_id, starts_at, ends_at, placed_by_rule, state, external_event_id, pinned_at) - calendars(id, provider, account_ref, scopes_json, last_synced_at, revoked_at) - The busy table has no title column, so there is nothing for the scheduler, a log or a crash report to leak. ### Phase 1 · Busy time, and not one word more Build: Connect one calendar with the narrowest scope that reads busy time and writes the tool's own events. Use the provider's free and busy query, which returns only start and end times per calendar; the Google Calendar version caps a single request at fifty calendars and a hundred identifiers per group, so page rather than assume. Store the intervals and nothing else, and treat a calendar that has not synced within a stated window as busier than it looks rather than free. Trap: Reading the full event list because it is the easier endpoint puts titles, attendees and locations into your database, your logs and eventually a crash report, and none of that is needed to know that three o'clock is taken. Use the busy query, and assert in the suite that no title, attendee or location column exists anywhere. Done when: No title, description, attendee or location appears in the database or the logs, asserted by a test that scans both. Busy intervals from a calendar with more than the per-request limit are fetched by paging, tested against a fake provider. A calendar not synced within the configured window causes the scheduler to narrow availability rather than plan optimistically. Revoking access is detected and the tool stops planning rather than planning against stale intervals. Do not build yet: No time maps, no solver, no write-back, no notifications. ### Phase 2 · Time maps across a clock change Build: Define time maps as rules in a named zone: deep work on weekday mornings, errands on Saturday, and so on. Expand them per date into instants using a timezone library, subtract busy intervals and buffers, and produce the free slots for the horizon. Decide and document what a rule landing in the missing hour does and what happens on the repeated one. Trap: Generating slots by adding minutes to a local midnight offers a nine o'clock slot that does not exist on the spring transition and two of them on the autumn one, which is rare enough to pass every test written in June and common enough to wreck a week twice a year. Expand from the zone's rules for that specific date, and put both transition days in the suite. Done when: Slots on both clock change days match hand-computed expectations, asserted for a zone that observes them. Buffers and existing busy intervals remove slots correctly, including a busy interval that partially overlaps a slot. A task whose minimum chunk exceeds every remaining slot is reported rather than placed in a slot too small for it. Changing the machine's timezone does not change the plan, because the rules carry their own zone. Do not build yet: No solver, write-back, reminders or export. ### Phase 3 · A plan that is the same plan tomorrow Build: Sort candidate tasks by an explicit ordering of due date, priority and identifier, so there is no tie the machine breaks differently on two runs. Place each into the earliest slot that satisfies its chunk rules, recording which rule placed it. Hash the inputs into the plan. Treat a block the person pinned or already started as fixed, and plan around it rather than through it. Trap: A solver that replans from scratch every hour moves this afternoon's work to Thursday because a new task arrived, and after the third time nobody believes the calendar; worse, an unstable tie-break makes two runs over identical inputs disagree, so the person cannot tell a real change from noise. Order deterministically, hash the inputs, and hold pinned and started blocks still. Done when: Two solver runs over identical inputs produce byte-identical plans, asserted by comparing the plan hash. Every block records the rule that placed it and the interface shows it. A pinned block and a block whose start time has passed survive a replan unchanged, tested. Adding one task moves only the blocks that had to move, measured as a count in the suite and reported to the person. Do not build yet: No calendar write-back, no reminders, no export. ### Phase 4 · Writing blocks back without stomping Build: Create one calendar event per block, store its identifier, and mark it so it is recognisable. Update and delete only events whose identifier the tool stored. Reconcile before each replan: an event the person moved by hand becomes a pin, and one they deleted becomes a task with its time returned. Write into a dedicated calendar rather than the main one where that is available. Trap: Clearing the tool's blocks by deleting every event in a time range removes the dentist appointment that happened to fall inside it, and there is no undo worth the name. Delete only by stored identifier, and if an identifier is missing, leave the event alone and report it. Done when: An attempt to modify or delete an event with no stored identifier fails, asserted by a test that tries. Moving a block in the calendar by hand turns it into a pin on the next reconcile rather than being moved back. Deleting a block in the calendar returns its minutes to the task rather than losing them. Undoing a whole plan removes exactly the events that plan created, tested by counting before and after. Do not build yet: No reminders, no reports, no export. ### Phase 5 · Telling the truth when it does not fit Build: Report every unplaceable task with the reason: not enough slots, a chunk that fits nowhere, a due date already passed. Never silently shorten a task, drop one, or schedule past its due date without saying so. Show how much committed time the horizon holds against how much free time exists. Export tasks, time maps and plans as files, and pause planning when the calendar connection is revoked. Trap: Quietly trimming a three hour task into the ninety minutes that happen to be free makes the plan look achievable and the week fail, and because the trim was never reported the person blames themselves. Report the shortfall in hours, refuse to shorten a task without being asked, and show the committed-against-free figure before anyone plans their week around it. Done when: A task that cannot fit appears in an unplaceable list with a specific reason, never silently shortened, asserted in the suite. The horizon shows committed minutes against available minutes and flags a deficit before the plan is accepted. Export produces tasks, time maps and plans in files that reimport into an empty install. A revoked calendar pauses planning within one sync interval and says so on the main screen. Do not build yet: Nothing after this. ### Out of scope (and why) - Reading event details for any reason, including cleverness about meeting types; the tool needs to know when you are busy and never why. - Cross-device sync, which turns a local plan into a distributed one and needs a conflict model before it needs anything else. - Team scheduling and shared availability, which multiply every privacy and timezone question here. - Any claim that following the plan means the work gets done, which is a promise no scheduler can keep. ### README must contain - That the calendar is read as busy intervals only, asserted by a test that scans the schema and the logs - That time map rules carry their own zone and both clock change days are in the suite - That an identical rerun produces an identical plan, provable by the recorded input hash - That the tool only ever modifies events whose identifier it stored, and never clears a time range - That tasks which do not fit are reported rather than shortened, with the shortfall in hours ===== AGENTS.md ===== # Agent instructions · SkedPal indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, SQLite beneath, Free and busy intervals only, through the provider's own busy query, A deterministic pass over an explicit priority order with a recorded tie-break, Instants stored, rules written in a named zone, expanded per date, Only events the tool created, tagged and tracked by their stored identifier. 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 - Reading the full event list because it is the easier endpoint puts titles, attendees and locations into your database, your logs and eventually a crash report, and none of that is needed to know that three o'clock is taken. Use the busy query, and assert in the suite that no title, attendee or location column exists anywhere. - Generating slots by adding minutes to a local midnight offers a nine o'clock slot that does not exist on the spring transition and two of them on the autumn one, which is rare enough to pass every test written in June and common enough to wreck a week twice a year. Expand from the zone's rules for that specific date, and put both transition days in the suite. - A solver that replans from scratch every hour moves this afternoon's work to Thursday because a new task arrived, and after the third time nobody believes the calendar; worse, an unstable tie-break makes two runs over identical inputs disagree, so the person cannot tell a real change from noise. Order deterministically, hash the inputs, and hold pinned and started blocks still. - Clearing the tool's blocks by deleting every event in a time range removes the dentist appointment that happened to fall inside it, and there is no undo worth the name. Delete only by stored identifier, and if an identifier is missing, leave the event alone and report it. ===== BUILD_PLAN.md ===== # Build plan · SkedPal An automatic scheduler whose hard problems are reading a calendar as busy time without learning what is in it, producing a plan that is stable and explainable rather than freshly shuffled every hour, and writing blocks back without ever touching an event it did not create. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Busy time, and not one word more The calendar is read as intervals with no event content anywhere in the system. ### Steps 1. Use the busy query, not the event list The endpoint that returns less is the endpoint that leaks less. Files: `src/calendar/freebusy.ts` 2. Page rather than assume the limit The per-request calendar cap is published and it is not large. Files: `src/calendar/freebusy.ts`, `tests/paging.test.ts` 3. Assert no event content is stored A schema test is the only version of this promise that stays true. Files: `tests/no-event-content.test.ts` ### Done when - [ ] No event content exists in the database or logs - [ ] Busy fetching pages past the per-request limit - [ ] A stale calendar narrows availability - [ ] A revoked calendar stops planning ### Watch out - Reading the full event list because it is the easier endpoint puts titles, attendees and locations into your database, your logs and eventually a crash report, and none of that is needed to know that three o'clock is taken. Use the busy query, and assert in the suite that no title, attendee or location column exists anywhere. ## Phase 2 · Time maps across a clock change Available slots are computed from rules in a named zone and are right on the two awkward days. ### Steps 1. Expand rules per date with a timezone library Never by arithmetic on a local midnight. Files: `src/schedule/expand.ts`, `tests/dst.test.ts` 2. Subtract busy time and buffers to get free slots A partial overlap is the case that hand-rolled interval code gets wrong. Files: `src/schedule/slots.ts`, `tests/overlap.test.ts` 3. Keep the zone on the rule, not on the machine Travelling should not silently rewrite when you work. Files: `src/schedule/timemap.ts` ### Done when - [ ] Both transition days match hand-computed slots - [ ] Partial overlaps remove the right part of a slot - [ ] A chunk that fits nowhere is reported - [ ] Changing the machine zone does not change the plan ### Watch out - Generating slots by adding minutes to a local midnight offers a nine o'clock slot that does not exist on the spring transition and two of them on the autumn one, which is rare enough to pass every test written in June and common enough to wreck a week twice a year. Expand from the zone's rules for that specific date, and put both transition days in the suite. ## Phase 3 · A plan that is the same plan tomorrow The solver is deterministic, records why each block landed where it did, and does not reshuffle work already begun. ### Steps 1. Order candidates with an explicit, total ordering Including an identifier at the end so nothing is ever a real tie. Files: `src/solver/order.ts`, `tests/determinism.test.ts` 2. Hash the inputs into the plan It turns 'nothing changed' from a feeling into a comparison. Files: `src/solver/plan.ts` 3. Hold pinned and started blocks still A scheduler that moves what you already began is a scheduler you stop opening. Files: `src/solver/pins.ts`, `tests/stability.test.ts` ### Done when - [ ] Identical inputs produce an identical plan hash - [ ] Every block names the rule that placed it - [ ] Pinned and started blocks survive a replan - [ ] A new task moves a reported, minimal set of blocks ### Watch out - A solver that replans from scratch every hour moves this afternoon's work to Thursday because a new task arrived, and after the third time nobody believes the calendar; worse, an unstable tie-break makes two runs over identical inputs disagree, so the person cannot tell a real change from noise. Order deterministically, hash the inputs, and hold pinned and started blocks still. ## Phase 4 · Writing blocks back without stomping The calendar gains the tool's blocks and loses nothing else. ### Steps 1. Write only into a calendar the tool owns It makes the blast radius of any mistake visible and small. Files: `src/calendar/write.ts` 2. Touch only events with a stored identifier The range delete is the mistake that removes a real appointment. Files: `src/calendar/write.ts`, `tests/never-touch-foreign.test.ts` 3. Reconcile hand edits into pins A person moving a block is giving you information, not making an error. Files: `src/calendar/reconcile.ts` ### Done when - [ ] Foreign events cannot be modified or deleted - [ ] A hand-moved block becomes a pin - [ ] A deleted block returns its minutes - [ ] Undoing a plan removes exactly its own events ### Watch out - Clearing the tool's blocks by deleting every event in a time range removes the dentist appointment that happened to fall inside it, and there is no undo worth the name. Delete only by stored identifier, and if an identifier is missing, leave the event alone and report it. ## Not in this build - Reading event titles, attendees or locations. - Cross-device sync. - Team and shared scheduling. - Any promise about what you will finish. ## After v1, if you want it - A second calendar as a read-only busy source, which the busy query already supports - Recurring tasks that recompute their remaining minutes each period, built on the existing task model - A weekly review that compares planned blocks against what the calendar says actually happened ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Tasks, time maps, plans and busy intervals. DATABASE_PATH=~/.local/share/scheduler/tasks.db # Required. The registered application with the narrow scope. CALENDAR_CLIENT_ID=your-application-id # Required · secret. Stored in the OS keychain where available, never in the database. CALENDAR_CLIENT_SECRET=redacted # Required. The dedicated calendar. The tool never writes anywhere else. WRITE_CALENDAR_ID=scheduler@group.calendar.google.com # Required. How far ahead to plan. Longer horizons make the capacity figure more honest and the plan less stable. PLANNING_HORIZON_DAYS=14 # Required. Older than this and availability narrows rather than assuming free time. STALE_SYNC_MINUTES=30 # Required. Recorded on every plan, so a plan change caused by a code change is distinguishable from one caused by your week. SOLVER_VERSION=1
You are building a lean indie version of SkedPal. 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 ===== # SkedPal · indie build An automatic scheduler whose hard problems are reading a calendar as busy time without learning what is in it, producing a plan that is stable and explainable rather than freshly shuffled every hour, and writing blocks back without ever touching an event it did not create. 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 | Tauri 2 with React and TypeScript, SQLite beneath | a desktop tool that holds your task list locally | | Calendar | Free and busy intervals only, through the provider's own busy query | the scheduler needs to know when you are busy and never why | | Solver | A deterministic pass over an explicit priority order with a recorded tie-break | a plan you cannot explain is a plan you will not follow | | Time | Instants stored, rules written in a named zone, expanded per date | the clocks change and a scheduler that ignores that books an hour that does not exist | | Write-back | Only events the tool created, tagged and tracked by their stored identifier | the calendar belongs to the person, not to the scheduler | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The Rust toolchain for Tauri 2** · free - Why: Builds the desktop shell that holds the task list locally. - Get it: rustup.rs, then the Tauri prerequisites for your platform. - Verify: cargo tauri info reports no missing dependencies - [ ] **A calendar account with a spare calendar for the tool's blocks** · free - Why: Writing into a dedicated calendar keeps every mistake visible and reversible, which matters more here than anywhere else in the build. - Get it: Create an empty calendar in your account and register an application with the narrowest scope that reads busy time and writes events. - Verify: The busy query returns intervals and a test event can be created in the new calendar - [ ] **Your rule for the clock change days** · free - Why: Both transitions are asserted in the suite, and deciding after seeing failures means choosing whatever passes. - Get it: Write down what a rule landing in the missing hour does and what happens on the repeated one. - Verify: Both are named in the README and in the tests - [ ] **Two weeks of your own real tasks and commitments** · free - Why: Determinism and stability only look like features against a messy real week; a tidy fixture hides both. - Get it: Put your actual flexible work into the tool with honest durations and due dates. - Verify: A plan over the real week reports its shortfall rather than fitting suspiciously well ## 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: - Reading event titles, attendees or locations. - Cross-device sync. - Team and shared scheduling. - Any promise about what you will finish. - calendar-provider edge cases and timezone correctness - cross-device sync - deep operating-system edge-case coverage - polished shortcuts and accessibility - vendor-maintained updates If one of those is essential to you, that is the reason to keep paying for SkedPal, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · SkedPal 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 an auto-scheduler that fits flexible tasks into the gaps in your calendar and can explain every placement to replace SkedPal. 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: Tauri 2 with React and TypeScript, SQLite beneath · a desktop tool that holds your task list locally - Calendar: Free and busy intervals only, through the provider's own busy query · the scheduler needs to know when you are busy and never why - Solver: A deterministic pass over an explicit priority order with a recorded tie-break · a plan you cannot explain is a plan you will not follow - Time: Instants stored, rules written in a named zone, expanded per date · the clocks change and a scheduler that ignores that books an hour that does not exist - Write-back: Only events the tool created, tagged and tracked by their stored identifier · the calendar belongs to the person, not to the scheduler ### Data model (create this before Phase 1) - tasks(id, title, total_minutes, remaining_minutes, due_at, earliest_at, priority, min_chunk_minutes, max_chunk_minutes, time_map_id, state) - time_maps(id, name, timezone, rules_json) · when a kind of work is allowed, written in the map's own zone - busy(id, calendar_id, starts_at, ends_at, external_id, is_ours) · start and end and nothing else, because nothing else is fetched - plans(id, computed_at, horizon_days, inputs_sha256, solver_version, unplaceable_json) · the hash makes an identical rerun provable - blocks(id, plan_id, task_id, starts_at, ends_at, placed_by_rule, state, external_event_id, pinned_at) - calendars(id, provider, account_ref, scopes_json, last_synced_at, revoked_at) - The busy table has no title column, so there is nothing for the scheduler, a log or a crash report to leak. ### Phase 1 · Busy time, and not one word more Build: Connect one calendar with the narrowest scope that reads busy time and writes the tool's own events. Use the provider's free and busy query, which returns only start and end times per calendar; the Google Calendar version caps a single request at fifty calendars and a hundred identifiers per group, so page rather than assume. Store the intervals and nothing else, and treat a calendar that has not synced within a stated window as busier than it looks rather than free. Trap: Reading the full event list because it is the easier endpoint puts titles, attendees and locations into your database, your logs and eventually a crash report, and none of that is needed to know that three o'clock is taken. Use the busy query, and assert in the suite that no title, attendee or location column exists anywhere. Done when: No title, description, attendee or location appears in the database or the logs, asserted by a test that scans both. Busy intervals from a calendar with more than the per-request limit are fetched by paging, tested against a fake provider. A calendar not synced within the configured window causes the scheduler to narrow availability rather than plan optimistically. Revoking access is detected and the tool stops planning rather than planning against stale intervals. Do not build yet: No time maps, no solver, no write-back, no notifications. ### Phase 2 · Time maps across a clock change Build: Define time maps as rules in a named zone: deep work on weekday mornings, errands on Saturday, and so on. Expand them per date into instants using a timezone library, subtract busy intervals and buffers, and produce the free slots for the horizon. Decide and document what a rule landing in the missing hour does and what happens on the repeated one. Trap: Generating slots by adding minutes to a local midnight offers a nine o'clock slot that does not exist on the spring transition and two of them on the autumn one, which is rare enough to pass every test written in June and common enough to wreck a week twice a year. Expand from the zone's rules for that specific date, and put both transition days in the suite. Done when: Slots on both clock change days match hand-computed expectations, asserted for a zone that observes them. Buffers and existing busy intervals remove slots correctly, including a busy interval that partially overlaps a slot. A task whose minimum chunk exceeds every remaining slot is reported rather than placed in a slot too small for it. Changing the machine's timezone does not change the plan, because the rules carry their own zone. Do not build yet: No solver, write-back, reminders or export. ### Phase 3 · A plan that is the same plan tomorrow Build: Sort candidate tasks by an explicit ordering of due date, priority and identifier, so there is no tie the machine breaks differently on two runs. Place each into the earliest slot that satisfies its chunk rules, recording which rule placed it. Hash the inputs into the plan. Treat a block the person pinned or already started as fixed, and plan around it rather than through it. Trap: A solver that replans from scratch every hour moves this afternoon's work to Thursday because a new task arrived, and after the third time nobody believes the calendar; worse, an unstable tie-break makes two runs over identical inputs disagree, so the person cannot tell a real change from noise. Order deterministically, hash the inputs, and hold pinned and started blocks still. Done when: Two solver runs over identical inputs produce byte-identical plans, asserted by comparing the plan hash. Every block records the rule that placed it and the interface shows it. A pinned block and a block whose start time has passed survive a replan unchanged, tested. Adding one task moves only the blocks that had to move, measured as a count in the suite and reported to the person. Do not build yet: No calendar write-back, no reminders, no export. ### Phase 4 · Writing blocks back without stomping Build: Create one calendar event per block, store its identifier, and mark it so it is recognisable. Update and delete only events whose identifier the tool stored. Reconcile before each replan: an event the person moved by hand becomes a pin, and one they deleted becomes a task with its time returned. Write into a dedicated calendar rather than the main one where that is available. Trap: Clearing the tool's blocks by deleting every event in a time range removes the dentist appointment that happened to fall inside it, and there is no undo worth the name. Delete only by stored identifier, and if an identifier is missing, leave the event alone and report it. Done when: An attempt to modify or delete an event with no stored identifier fails, asserted by a test that tries. Moving a block in the calendar by hand turns it into a pin on the next reconcile rather than being moved back. Deleting a block in the calendar returns its minutes to the task rather than losing them. Undoing a whole plan removes exactly the events that plan created, tested by counting before and after. Do not build yet: No reminders, no reports, no export. ### Phase 5 · Telling the truth when it does not fit Build: Report every unplaceable task with the reason: not enough slots, a chunk that fits nowhere, a due date already passed. Never silently shorten a task, drop one, or schedule past its due date without saying so. Show how much committed time the horizon holds against how much free time exists. Export tasks, time maps and plans as files, and pause planning when the calendar connection is revoked. Trap: Quietly trimming a three hour task into the ninety minutes that happen to be free makes the plan look achievable and the week fail, and because the trim was never reported the person blames themselves. Report the shortfall in hours, refuse to shorten a task without being asked, and show the committed-against-free figure before anyone plans their week around it. Done when: A task that cannot fit appears in an unplaceable list with a specific reason, never silently shortened, asserted in the suite. The horizon shows committed minutes against available minutes and flags a deficit before the plan is accepted. Export produces tasks, time maps and plans in files that reimport into an empty install. A revoked calendar pauses planning within one sync interval and says so on the main screen. Do not build yet: Nothing after this. ### Out of scope (and why) - Reading event details for any reason, including cleverness about meeting types; the tool needs to know when you are busy and never why. - Cross-device sync, which turns a local plan into a distributed one and needs a conflict model before it needs anything else. - Team scheduling and shared availability, which multiply every privacy and timezone question here. - Any claim that following the plan means the work gets done, which is a promise no scheduler can keep. ### README must contain - That the calendar is read as busy intervals only, asserted by a test that scans the schema and the logs - That time map rules carry their own zone and both clock change days are in the suite - That an identical rerun produces an identical plan, provable by the recorded input hash - That the tool only ever modifies events whose identifier it stored, and never clears a time range - That tasks which do not fit are reported rather than shortened, with the shortfall in hours ===== AGENTS.md ===== # Agent instructions · SkedPal indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, SQLite beneath, Free and busy intervals only, through the provider's own busy query, A deterministic pass over an explicit priority order with a recorded tie-break, Instants stored, rules written in a named zone, expanded per date, Only events the tool created, tagged and tracked by their stored identifier. 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 - Reading the full event list because it is the easier endpoint puts titles, attendees and locations into your database, your logs and eventually a crash report, and none of that is needed to know that three o'clock is taken. Use the busy query, and assert in the suite that no title, attendee or location column exists anywhere. - Generating slots by adding minutes to a local midnight offers a nine o'clock slot that does not exist on the spring transition and two of them on the autumn one, which is rare enough to pass every test written in June and common enough to wreck a week twice a year. Expand from the zone's rules for that specific date, and put both transition days in the suite. - A solver that replans from scratch every hour moves this afternoon's work to Thursday because a new task arrived, and after the third time nobody believes the calendar; worse, an unstable tie-break makes two runs over identical inputs disagree, so the person cannot tell a real change from noise. Order deterministically, hash the inputs, and hold pinned and started blocks still. - Clearing the tool's blocks by deleting every event in a time range removes the dentist appointment that happened to fall inside it, and there is no undo worth the name. Delete only by stored identifier, and if an identifier is missing, leave the event alone and report it. ===== BUILD_PLAN.md ===== # Build plan · SkedPal An automatic scheduler whose hard problems are reading a calendar as busy time without learning what is in it, producing a plan that is stable and explainable rather than freshly shuffled every hour, and writing blocks back without ever touching an event it did not create. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Busy time, and not one word more The calendar is read as intervals with no event content anywhere in the system. ### Steps 1. Use the busy query, not the event list The endpoint that returns less is the endpoint that leaks less. Files: `src/calendar/freebusy.ts` 2. Page rather than assume the limit The per-request calendar cap is published and it is not large. Files: `src/calendar/freebusy.ts`, `tests/paging.test.ts` 3. Assert no event content is stored A schema test is the only version of this promise that stays true. Files: `tests/no-event-content.test.ts` ### Done when - [ ] No event content exists in the database or logs - [ ] Busy fetching pages past the per-request limit - [ ] A stale calendar narrows availability - [ ] A revoked calendar stops planning ### Watch out - Reading the full event list because it is the easier endpoint puts titles, attendees and locations into your database, your logs and eventually a crash report, and none of that is needed to know that three o'clock is taken. Use the busy query, and assert in the suite that no title, attendee or location column exists anywhere. ## Phase 2 · Time maps across a clock change Available slots are computed from rules in a named zone and are right on the two awkward days. ### Steps 1. Expand rules per date with a timezone library Never by arithmetic on a local midnight. Files: `src/schedule/expand.ts`, `tests/dst.test.ts` 2. Subtract busy time and buffers to get free slots A partial overlap is the case that hand-rolled interval code gets wrong. Files: `src/schedule/slots.ts`, `tests/overlap.test.ts` 3. Keep the zone on the rule, not on the machine Travelling should not silently rewrite when you work. Files: `src/schedule/timemap.ts` ### Done when - [ ] Both transition days match hand-computed slots - [ ] Partial overlaps remove the right part of a slot - [ ] A chunk that fits nowhere is reported - [ ] Changing the machine zone does not change the plan ### Watch out - Generating slots by adding minutes to a local midnight offers a nine o'clock slot that does not exist on the spring transition and two of them on the autumn one, which is rare enough to pass every test written in June and common enough to wreck a week twice a year. Expand from the zone's rules for that specific date, and put both transition days in the suite. ## Phase 3 · A plan that is the same plan tomorrow The solver is deterministic, records why each block landed where it did, and does not reshuffle work already begun. ### Steps 1. Order candidates with an explicit, total ordering Including an identifier at the end so nothing is ever a real tie. Files: `src/solver/order.ts`, `tests/determinism.test.ts` 2. Hash the inputs into the plan It turns 'nothing changed' from a feeling into a comparison. Files: `src/solver/plan.ts` 3. Hold pinned and started blocks still A scheduler that moves what you already began is a scheduler you stop opening. Files: `src/solver/pins.ts`, `tests/stability.test.ts` ### Done when - [ ] Identical inputs produce an identical plan hash - [ ] Every block names the rule that placed it - [ ] Pinned and started blocks survive a replan - [ ] A new task moves a reported, minimal set of blocks ### Watch out - A solver that replans from scratch every hour moves this afternoon's work to Thursday because a new task arrived, and after the third time nobody believes the calendar; worse, an unstable tie-break makes two runs over identical inputs disagree, so the person cannot tell a real change from noise. Order deterministically, hash the inputs, and hold pinned and started blocks still. ## Phase 4 · Writing blocks back without stomping The calendar gains the tool's blocks and loses nothing else. ### Steps 1. Write only into a calendar the tool owns It makes the blast radius of any mistake visible and small. Files: `src/calendar/write.ts` 2. Touch only events with a stored identifier The range delete is the mistake that removes a real appointment. Files: `src/calendar/write.ts`, `tests/never-touch-foreign.test.ts` 3. Reconcile hand edits into pins A person moving a block is giving you information, not making an error. Files: `src/calendar/reconcile.ts` ### Done when - [ ] Foreign events cannot be modified or deleted - [ ] A hand-moved block becomes a pin - [ ] A deleted block returns its minutes - [ ] Undoing a plan removes exactly its own events ### Watch out - Clearing the tool's blocks by deleting every event in a time range removes the dentist appointment that happened to fall inside it, and there is no undo worth the name. Delete only by stored identifier, and if an identifier is missing, leave the event alone and report it. ## Not in this build - Reading event titles, attendees or locations. - Cross-device sync. - Team and shared scheduling. - Any promise about what you will finish. ## After v1, if you want it - A second calendar as a read-only busy source, which the busy query already supports - Recurring tasks that recompute their remaining minutes each period, built on the existing task model - A weekly review that compares planned blocks against what the calendar says actually happened ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Tasks, time maps, plans and busy intervals. DATABASE_PATH=~/.local/share/scheduler/tasks.db # Required. The registered application with the narrow scope. CALENDAR_CLIENT_ID=your-application-id # Required · secret. Stored in the OS keychain where available, never in the database. CALENDAR_CLIENT_SECRET=redacted # Required. The dedicated calendar. The tool never writes anywhere else. WRITE_CALENDAR_ID=scheduler@group.calendar.google.com # Required. How far ahead to plan. Longer horizons make the capacity figure more honest and the plan less stable. PLANNING_HORIZON_DAYS=14 # Required. Older than this and availability narrows rather than assuming free time. STALE_SYNC_MINUTES=30 # Required. Recorded on every plan, so a plan change caused by a code change is distinguishable from one caused by your week. SOLVER_VERSION=1
You are building a production product version of SkedPal. 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 ===== # SkedPal · product brief ## Problem The visible automatic scheduling loop is buildable, but a credible replacement needs more than the first screen. SkedPal earns its keep through polish, OS integration, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A scheduler that knows when you are busy and not what you are doing, produces the same plan twice from the same week, never touches an event it did not create, and tells you in hours when the week does not fit. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS, Windows, or Linux development machine - Node.js 22 and Rust toolchain - Local filesystem access and OS permissions ## Explicit non-goals for v1 - Reading event titles, attendees or locations. - Cross-device sync. - Team and shared scheduling. - Any promise about what you will finish. - calendar-provider edge cases and timezone correctness - cross-device sync - deep operating-system edge-case coverage - polished shortcuts and accessibility - vendor-maintained updates ## Success criteria - No event content is confirmed present in the database or logs - Both clock change days are confirmed to match hand-computed slots - Identical inputs are confirmed to produce an identical plan hash - A foreign event is confirmed impossible to modify or delete - An unfittable task is confirmed reported rather than shortened ===== BRIEF.md ===== # Build brief · SkedPal 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 an auto-scheduler that fits flexible tasks into the gaps in your calendar and can explain every placement to replace SkedPal. 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: Tauri 2 with React and TypeScript, SQLite beneath · a desktop tool that holds your task list locally - Calendar: Free and busy intervals only, through the provider's own busy query · the scheduler needs to know when you are busy and never why - Solver: A deterministic pass over an explicit priority order with a recorded tie-break · a plan you cannot explain is a plan you will not follow - Time: Instants stored, rules written in a named zone, expanded per date · the clocks change and a scheduler that ignores that books an hour that does not exist - Write-back: Only events the tool created, tagged and tracked by their stored identifier · the calendar belongs to the person, not to the scheduler ### Data model (create this before Phase 1) - tasks(id, title, total_minutes, remaining_minutes, due_at, earliest_at, priority, min_chunk_minutes, max_chunk_minutes, time_map_id, state) - time_maps(id, name, timezone, rules_json) · when a kind of work is allowed, written in the map's own zone - busy(id, calendar_id, starts_at, ends_at, external_id, is_ours) · start and end and nothing else, because nothing else is fetched - plans(id, computed_at, horizon_days, inputs_sha256, solver_version, unplaceable_json) · the hash makes an identical rerun provable - blocks(id, plan_id, task_id, starts_at, ends_at, placed_by_rule, state, external_event_id, pinned_at) - calendars(id, provider, account_ref, scopes_json, last_synced_at, revoked_at) - The busy table has no title column, so there is nothing for the scheduler, a log or a crash report to leak. ### Phase 1 · Busy time, and not one word more Build: Connect one calendar with the narrowest scope that reads busy time and writes the tool's own events. Use the provider's free and busy query, which returns only start and end times per calendar; the Google Calendar version caps a single request at fifty calendars and a hundred identifiers per group, so page rather than assume. Store the intervals and nothing else, and treat a calendar that has not synced within a stated window as busier than it looks rather than free. Trap: Reading the full event list because it is the easier endpoint puts titles, attendees and locations into your database, your logs and eventually a crash report, and none of that is needed to know that three o'clock is taken. Use the busy query, and assert in the suite that no title, attendee or location column exists anywhere. Done when: No title, description, attendee or location appears in the database or the logs, asserted by a test that scans both. Busy intervals from a calendar with more than the per-request limit are fetched by paging, tested against a fake provider. A calendar not synced within the configured window causes the scheduler to narrow availability rather than plan optimistically. Revoking access is detected and the tool stops planning rather than planning against stale intervals. Do not build yet: No time maps, no solver, no write-back, no notifications. ### Phase 2 · Time maps across a clock change Build: Define time maps as rules in a named zone: deep work on weekday mornings, errands on Saturday, and so on. Expand them per date into instants using a timezone library, subtract busy intervals and buffers, and produce the free slots for the horizon. Decide and document what a rule landing in the missing hour does and what happens on the repeated one. Trap: Generating slots by adding minutes to a local midnight offers a nine o'clock slot that does not exist on the spring transition and two of them on the autumn one, which is rare enough to pass every test written in June and common enough to wreck a week twice a year. Expand from the zone's rules for that specific date, and put both transition days in the suite. Done when: Slots on both clock change days match hand-computed expectations, asserted for a zone that observes them. Buffers and existing busy intervals remove slots correctly, including a busy interval that partially overlaps a slot. A task whose minimum chunk exceeds every remaining slot is reported rather than placed in a slot too small for it. Changing the machine's timezone does not change the plan, because the rules carry their own zone. Do not build yet: No solver, write-back, reminders or export. ### Phase 3 · A plan that is the same plan tomorrow Build: Sort candidate tasks by an explicit ordering of due date, priority and identifier, so there is no tie the machine breaks differently on two runs. Place each into the earliest slot that satisfies its chunk rules, recording which rule placed it. Hash the inputs into the plan. Treat a block the person pinned or already started as fixed, and plan around it rather than through it. Trap: A solver that replans from scratch every hour moves this afternoon's work to Thursday because a new task arrived, and after the third time nobody believes the calendar; worse, an unstable tie-break makes two runs over identical inputs disagree, so the person cannot tell a real change from noise. Order deterministically, hash the inputs, and hold pinned and started blocks still. Done when: Two solver runs over identical inputs produce byte-identical plans, asserted by comparing the plan hash. Every block records the rule that placed it and the interface shows it. A pinned block and a block whose start time has passed survive a replan unchanged, tested. Adding one task moves only the blocks that had to move, measured as a count in the suite and reported to the person. Do not build yet: No calendar write-back, no reminders, no export. ### Phase 4 · Writing blocks back without stomping Build: Create one calendar event per block, store its identifier, and mark it so it is recognisable. Update and delete only events whose identifier the tool stored. Reconcile before each replan: an event the person moved by hand becomes a pin, and one they deleted becomes a task with its time returned. Write into a dedicated calendar rather than the main one where that is available. Trap: Clearing the tool's blocks by deleting every event in a time range removes the dentist appointment that happened to fall inside it, and there is no undo worth the name. Delete only by stored identifier, and if an identifier is missing, leave the event alone and report it. Done when: An attempt to modify or delete an event with no stored identifier fails, asserted by a test that tries. Moving a block in the calendar by hand turns it into a pin on the next reconcile rather than being moved back. Deleting a block in the calendar returns its minutes to the task rather than losing them. Undoing a whole plan removes exactly the events that plan created, tested by counting before and after. Do not build yet: No reminders, no reports, no export. ### Phase 5 · Telling the truth when it does not fit Build: Report every unplaceable task with the reason: not enough slots, a chunk that fits nowhere, a due date already passed. Never silently shorten a task, drop one, or schedule past its due date without saying so. Show how much committed time the horizon holds against how much free time exists. Export tasks, time maps and plans as files, and pause planning when the calendar connection is revoked. Trap: Quietly trimming a three hour task into the ninety minutes that happen to be free makes the plan look achievable and the week fail, and because the trim was never reported the person blames themselves. Report the shortfall in hours, refuse to shorten a task without being asked, and show the committed-against-free figure before anyone plans their week around it. Done when: A task that cannot fit appears in an unplaceable list with a specific reason, never silently shortened, asserted in the suite. The horizon shows committed minutes against available minutes and flags a deficit before the plan is accepted. Export produces tasks, time maps and plans in files that reimport into an empty install. A revoked calendar pauses planning within one sync interval and says so on the main screen. Do not build yet: Nothing after this. ### Out of scope (and why) - Reading event details for any reason, including cleverness about meeting types; the tool needs to know when you are busy and never why. - Cross-device sync, which turns a local plan into a distributed one and needs a conflict model before it needs anything else. - Team scheduling and shared availability, which multiply every privacy and timezone question here. - Any claim that following the plan means the work gets done, which is a promise no scheduler can keep. ### README must contain - That the calendar is read as busy intervals only, asserted by a test that scans the schema and the logs - That time map rules carry their own zone and both clock change days are in the suite - That an identical rerun produces an identical plan, provable by the recorded input hash - That the tool only ever modifies events whose identifier it stored, and never clears a time range - That tasks which do not fit are reported rather than shortened, with the shortfall in hours ===== ARCHITECTURE.md ===== # Architecture · SkedPal ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Tauri 2 with React and TypeScript, SQLite beneath | a desktop tool that holds your task list locally | | Calendar | Free and busy intervals only, through the provider's own busy query | the scheduler needs to know when you are busy and never why | | Solver | A deterministic pass over an explicit priority order with a recorded tie-break | a plan you cannot explain is a plan you will not follow | | Time | Instants stored, rules written in a named zone, expanded per date | the clocks change and a scheduler that ignores that books an hour that does not exist | | Write-back | Only events the tool created, tagged and tracked by their stored identifier | the calendar belongs to the person, not to the scheduler | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Calendar read | Busy intervals, paging and staleness. | Any provider with a busy query, provided no event content is stored. | | Time maps | Zone-aware rule expansion and free slots. | Any rule format, provided both transition days stay in the suite. | | Solver | Deterministic ordering, plan hashing and pins. | Any algorithm, provided identical inputs still give an identical plan. | | Calendar write | Owned events only, reconciliation into pins. | Not swappable for range deletion, which removes real appointments. | | Reporting | Unplaceable tasks, capacity and export. | Any presentation, provided nothing is silently shortened. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Tasks, time maps, plans and busy intervals. - `CALENDAR_CLIENT_ID` · required · The registered application with the narrow scope. - `CALENDAR_CLIENT_SECRET` · required, secret · Stored in the OS keychain where available, never in the database. - `WRITE_CALENDAR_ID` · required · The dedicated calendar. The tool never writes anywhere else. - `PLANNING_HORIZON_DAYS` · required · How far ahead to plan. Longer horizons make the capacity figure more honest and the plan less stable. - `STALE_SYNC_MINUTES` · required · Older than this and availability narrows rather than assuming free time. - `SOLVER_VERSION` · required · Recorded on every plan, so a plan change caused by a code change is distinguishable from one caused by your week. ## 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 · SkedPal product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, SQLite beneath, Free and busy intervals only, through the provider's own busy query, A deterministic pass over an explicit priority order with a recorded tie-break, Instants stored, rules written in a named zone, expanded per date, Only events the tool created, tagged and tracked by their stored identifier. - 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 - Reading the full event list because it is the easier endpoint puts titles, attendees and locations into your database, your logs and eventually a crash report, and none of that is needed to know that three o'clock is taken. Use the busy query, and assert in the suite that no title, attendee or location column exists anywhere. - Generating slots by adding minutes to a local midnight offers a nine o'clock slot that does not exist on the spring transition and two of them on the autumn one, which is rare enough to pass every test written in June and common enough to wreck a week twice a year. Expand from the zone's rules for that specific date, and put both transition days in the suite. - A solver that replans from scratch every hour moves this afternoon's work to Thursday because a new task arrived, and after the third time nobody believes the calendar; worse, an unstable tie-break makes two runs over identical inputs disagree, so the person cannot tell a real change from noise. Order deterministically, hash the inputs, and hold pinned and started blocks still. - Clearing the tool's blocks by deleting every event in a time range removes the dentist appointment that happened to fall inside it, and there is no undo worth the name. Delete only by stored identifier, and if an identifier is missing, leave the event alone and report it. - Quietly trimming a three hour task into the ninety minutes that happen to be free makes the plan look achievable and the week fail, and because the trim was never reported the person blames themselves. Report the shortfall in hours, refuse to shorten a task without being asked, and show the committed-against-free figure before anyone plans their week around it. ===== MILESTONES.md ===== # Delivery milestones · SkedPal Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Busy time, and not one word more The calendar is read as intervals with no event content anywhere in the system. ### Steps 1. Use the busy query, not the event list The endpoint that returns less is the endpoint that leaks less. Files: `src/calendar/freebusy.ts` 2. Page rather than assume the limit The per-request calendar cap is published and it is not large. Files: `src/calendar/freebusy.ts`, `tests/paging.test.ts` 3. Assert no event content is stored A schema test is the only version of this promise that stays true. Files: `tests/no-event-content.test.ts` ### Done when - [ ] No event content exists in the database or logs - [ ] Busy fetching pages past the per-request limit - [ ] A stale calendar narrows availability - [ ] A revoked calendar stops planning ### Watch out - Reading the full event list because it is the easier endpoint puts titles, attendees and locations into your database, your logs and eventually a crash report, and none of that is needed to know that three o'clock is taken. Use the busy query, and assert in the suite that no title, attendee or location column exists anywhere. ## M2 · Time maps across a clock change Available slots are computed from rules in a named zone and are right on the two awkward days. ### Steps 1. Expand rules per date with a timezone library Never by arithmetic on a local midnight. Files: `src/schedule/expand.ts`, `tests/dst.test.ts` 2. Subtract busy time and buffers to get free slots A partial overlap is the case that hand-rolled interval code gets wrong. Files: `src/schedule/slots.ts`, `tests/overlap.test.ts` 3. Keep the zone on the rule, not on the machine Travelling should not silently rewrite when you work. Files: `src/schedule/timemap.ts` ### Done when - [ ] Both transition days match hand-computed slots - [ ] Partial overlaps remove the right part of a slot - [ ] A chunk that fits nowhere is reported - [ ] Changing the machine zone does not change the plan ### Watch out - Generating slots by adding minutes to a local midnight offers a nine o'clock slot that does not exist on the spring transition and two of them on the autumn one, which is rare enough to pass every test written in June and common enough to wreck a week twice a year. Expand from the zone's rules for that specific date, and put both transition days in the suite. ## M3 · A plan that is the same plan tomorrow The solver is deterministic, records why each block landed where it did, and does not reshuffle work already begun. ### Steps 1. Order candidates with an explicit, total ordering Including an identifier at the end so nothing is ever a real tie. Files: `src/solver/order.ts`, `tests/determinism.test.ts` 2. Hash the inputs into the plan It turns 'nothing changed' from a feeling into a comparison. Files: `src/solver/plan.ts` 3. Hold pinned and started blocks still A scheduler that moves what you already began is a scheduler you stop opening. Files: `src/solver/pins.ts`, `tests/stability.test.ts` ### Done when - [ ] Identical inputs produce an identical plan hash - [ ] Every block names the rule that placed it - [ ] Pinned and started blocks survive a replan - [ ] A new task moves a reported, minimal set of blocks ### Watch out - A solver that replans from scratch every hour moves this afternoon's work to Thursday because a new task arrived, and after the third time nobody believes the calendar; worse, an unstable tie-break makes two runs over identical inputs disagree, so the person cannot tell a real change from noise. Order deterministically, hash the inputs, and hold pinned and started blocks still. ## M4 · Writing blocks back without stomping The calendar gains the tool's blocks and loses nothing else. ### Steps 1. Write only into a calendar the tool owns It makes the blast radius of any mistake visible and small. Files: `src/calendar/write.ts` 2. Touch only events with a stored identifier The range delete is the mistake that removes a real appointment. Files: `src/calendar/write.ts`, `tests/never-touch-foreign.test.ts` 3. Reconcile hand edits into pins A person moving a block is giving you information, not making an error. Files: `src/calendar/reconcile.ts` ### Done when - [ ] Foreign events cannot be modified or deleted - [ ] A hand-moved block becomes a pin - [ ] A deleted block returns its minutes - [ ] Undoing a plan removes exactly its own events ### Watch out - Clearing the tool's blocks by deleting every event in a time range removes the dentist appointment that happened to fall inside it, and there is no undo worth the name. Delete only by stored identifier, and if an identifier is missing, leave the event alone and report it. ## M5 · Telling the truth when it does not fit (production only) The tool says plainly what cannot be done and lets you take everything with you. ### Steps 1. Report unplaceable tasks with reasons The shortfall is the most useful output this tool produces. Files: `src/solver/unplaceable.ts`, `src/views/report.tsx` 2. Show committed against available minutes One number that stops a week being planned on arithmetic that never worked. Files: `src/views/capacity.tsx` 3. Export everything and pause on revocation Planning against a calendar you can no longer read is worse than not planning. Files: `src/export/run.ts`, `src/calendar/revoked.ts` ### Done when - [ ] Unplaceable tasks are listed with reasons - [ ] No task is silently shortened - [ ] Capacity shows committed against available - [ ] A revoked calendar pauses planning and says so ### Watch out - Quietly trimming a three hour task into the ninety minutes that happen to be free makes the plan look achievable and the week fail, and because the trim was never reported the person blames themselves. Report the shortfall in hours, refuse to shorten a task without being asked, and show the committed-against-free figure before anyone plans their week around it. ===== OPERATIONS.md ===== # Operations · SkedPal ## Backup The SQLite file, which holds the task list and the pins. The calendar holds the blocks and can be rebuilt from a plan, so the task list is the part that is genuinely yours and genuinely irreplaceable. ## Restore Restore the database, reconnect the calendar, then reconcile before planning. Reconciling first means hand edits made while the tool was down become pins rather than being overwritten by a stale plan. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the checks are at startup and in the suite: the age of the last sync, a revoked connection, blocks in the calendar with no matching row, and rows with no matching event. ## Incident checklist A missing appointment means something deleted an event it did not own, and the write log names it. A plan that changed for no reason is diagnosed by comparing the two plans' input hashes and solver versions before looking at anything else. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No event content is confirmed present in the database or logs - [ ] Both clock change days are confirmed to match hand-computed slots - [ ] Identical inputs are confirmed to produce an identical plan hash - [ ] A foreign event is confirmed impossible to modify or delete - [ ] An unfittable task is confirmed reported rather than shortened ## Launch constraint Do not market omitted SkedPal 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. Tasks, time maps, plans and busy intervals. DATABASE_PATH=~/.local/share/scheduler/tasks.db # Required. The registered application with the narrow scope. CALENDAR_CLIENT_ID=your-application-id # Required · secret. Stored in the OS keychain where available, never in the database. CALENDAR_CLIENT_SECRET=redacted # Required. The dedicated calendar. The tool never writes anywhere else. WRITE_CALENDAR_ID=scheduler@group.calendar.google.com # Required. How far ahead to plan. Longer horizons make the capacity figure more honest and the plan less stable. PLANNING_HORIZON_DAYS=14 # Required. Older than this and availability narrows rather than assuming free time. STALE_SYNC_MINUTES=30 # Required. Recorded on every plan, so a plan change caused by a code change is distinguishable from one caused by your week. SOLVER_VERSION=1
# SkedPal · indie build An automatic scheduler whose hard problems are reading a calendar as busy time without learning what is in it, producing a plan that is stable and explainable rather than freshly shuffled every hour, and writing blocks back without ever touching an event it did not create. 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 | Tauri 2 with React and TypeScript, SQLite beneath | a desktop tool that holds your task list locally | | Calendar | Free and busy intervals only, through the provider's own busy query | the scheduler needs to know when you are busy and never why | | Solver | A deterministic pass over an explicit priority order with a recorded tie-break | a plan you cannot explain is a plan you will not follow | | Time | Instants stored, rules written in a named zone, expanded per date | the clocks change and a scheduler that ignores that books an hour that does not exist | | Write-back | Only events the tool created, tagged and tracked by their stored identifier | the calendar belongs to the person, not to the scheduler | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The Rust toolchain for Tauri 2** · free - Why: Builds the desktop shell that holds the task list locally. - Get it: rustup.rs, then the Tauri prerequisites for your platform. - Verify: cargo tauri info reports no missing dependencies - [ ] **A calendar account with a spare calendar for the tool's blocks** · free - Why: Writing into a dedicated calendar keeps every mistake visible and reversible, which matters more here than anywhere else in the build. - Get it: Create an empty calendar in your account and register an application with the narrowest scope that reads busy time and writes events. - Verify: The busy query returns intervals and a test event can be created in the new calendar - [ ] **Your rule for the clock change days** · free - Why: Both transitions are asserted in the suite, and deciding after seeing failures means choosing whatever passes. - Get it: Write down what a rule landing in the missing hour does and what happens on the repeated one. - Verify: Both are named in the README and in the tests - [ ] **Two weeks of your own real tasks and commitments** · free - Why: Determinism and stability only look like features against a messy real week; a tidy fixture hides both. - Get it: Put your actual flexible work into the tool with honest durations and due dates. - Verify: A plan over the real week reports its shortfall rather than fitting suspiciously well ## 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: - Reading event titles, attendees or locations. - Cross-device sync. - Team and shared scheduling. - Any promise about what you will finish. - calendar-provider edge cases and timezone correctness - cross-device sync - deep operating-system edge-case coverage - polished shortcuts and accessibility - vendor-maintained updates If one of those is essential to you, that is the reason to keep paying for SkedPal, and the README should say so rather than pretend.
# Build brief · SkedPal 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 an auto-scheduler that fits flexible tasks into the gaps in your calendar and can explain every placement to replace SkedPal. 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: Tauri 2 with React and TypeScript, SQLite beneath · a desktop tool that holds your task list locally - Calendar: Free and busy intervals only, through the provider's own busy query · the scheduler needs to know when you are busy and never why - Solver: A deterministic pass over an explicit priority order with a recorded tie-break · a plan you cannot explain is a plan you will not follow - Time: Instants stored, rules written in a named zone, expanded per date · the clocks change and a scheduler that ignores that books an hour that does not exist - Write-back: Only events the tool created, tagged and tracked by their stored identifier · the calendar belongs to the person, not to the scheduler ### Data model (create this before Phase 1) - tasks(id, title, total_minutes, remaining_minutes, due_at, earliest_at, priority, min_chunk_minutes, max_chunk_minutes, time_map_id, state) - time_maps(id, name, timezone, rules_json) · when a kind of work is allowed, written in the map's own zone - busy(id, calendar_id, starts_at, ends_at, external_id, is_ours) · start and end and nothing else, because nothing else is fetched - plans(id, computed_at, horizon_days, inputs_sha256, solver_version, unplaceable_json) · the hash makes an identical rerun provable - blocks(id, plan_id, task_id, starts_at, ends_at, placed_by_rule, state, external_event_id, pinned_at) - calendars(id, provider, account_ref, scopes_json, last_synced_at, revoked_at) - The busy table has no title column, so there is nothing for the scheduler, a log or a crash report to leak. ### Phase 1 · Busy time, and not one word more Build: Connect one calendar with the narrowest scope that reads busy time and writes the tool's own events. Use the provider's free and busy query, which returns only start and end times per calendar; the Google Calendar version caps a single request at fifty calendars and a hundred identifiers per group, so page rather than assume. Store the intervals and nothing else, and treat a calendar that has not synced within a stated window as busier than it looks rather than free. Trap: Reading the full event list because it is the easier endpoint puts titles, attendees and locations into your database, your logs and eventually a crash report, and none of that is needed to know that three o'clock is taken. Use the busy query, and assert in the suite that no title, attendee or location column exists anywhere. Done when: No title, description, attendee or location appears in the database or the logs, asserted by a test that scans both. Busy intervals from a calendar with more than the per-request limit are fetched by paging, tested against a fake provider. A calendar not synced within the configured window causes the scheduler to narrow availability rather than plan optimistically. Revoking access is detected and the tool stops planning rather than planning against stale intervals. Do not build yet: No time maps, no solver, no write-back, no notifications. ### Phase 2 · Time maps across a clock change Build: Define time maps as rules in a named zone: deep work on weekday mornings, errands on Saturday, and so on. Expand them per date into instants using a timezone library, subtract busy intervals and buffers, and produce the free slots for the horizon. Decide and document what a rule landing in the missing hour does and what happens on the repeated one. Trap: Generating slots by adding minutes to a local midnight offers a nine o'clock slot that does not exist on the spring transition and two of them on the autumn one, which is rare enough to pass every test written in June and common enough to wreck a week twice a year. Expand from the zone's rules for that specific date, and put both transition days in the suite. Done when: Slots on both clock change days match hand-computed expectations, asserted for a zone that observes them. Buffers and existing busy intervals remove slots correctly, including a busy interval that partially overlaps a slot. A task whose minimum chunk exceeds every remaining slot is reported rather than placed in a slot too small for it. Changing the machine's timezone does not change the plan, because the rules carry their own zone. Do not build yet: No solver, write-back, reminders or export. ### Phase 3 · A plan that is the same plan tomorrow Build: Sort candidate tasks by an explicit ordering of due date, priority and identifier, so there is no tie the machine breaks differently on two runs. Place each into the earliest slot that satisfies its chunk rules, recording which rule placed it. Hash the inputs into the plan. Treat a block the person pinned or already started as fixed, and plan around it rather than through it. Trap: A solver that replans from scratch every hour moves this afternoon's work to Thursday because a new task arrived, and after the third time nobody believes the calendar; worse, an unstable tie-break makes two runs over identical inputs disagree, so the person cannot tell a real change from noise. Order deterministically, hash the inputs, and hold pinned and started blocks still. Done when: Two solver runs over identical inputs produce byte-identical plans, asserted by comparing the plan hash. Every block records the rule that placed it and the interface shows it. A pinned block and a block whose start time has passed survive a replan unchanged, tested. Adding one task moves only the blocks that had to move, measured as a count in the suite and reported to the person. Do not build yet: No calendar write-back, no reminders, no export. ### Phase 4 · Writing blocks back without stomping Build: Create one calendar event per block, store its identifier, and mark it so it is recognisable. Update and delete only events whose identifier the tool stored. Reconcile before each replan: an event the person moved by hand becomes a pin, and one they deleted becomes a task with its time returned. Write into a dedicated calendar rather than the main one where that is available. Trap: Clearing the tool's blocks by deleting every event in a time range removes the dentist appointment that happened to fall inside it, and there is no undo worth the name. Delete only by stored identifier, and if an identifier is missing, leave the event alone and report it. Done when: An attempt to modify or delete an event with no stored identifier fails, asserted by a test that tries. Moving a block in the calendar by hand turns it into a pin on the next reconcile rather than being moved back. Deleting a block in the calendar returns its minutes to the task rather than losing them. Undoing a whole plan removes exactly the events that plan created, tested by counting before and after. Do not build yet: No reminders, no reports, no export. ### Phase 5 · Telling the truth when it does not fit Build: Report every unplaceable task with the reason: not enough slots, a chunk that fits nowhere, a due date already passed. Never silently shorten a task, drop one, or schedule past its due date without saying so. Show how much committed time the horizon holds against how much free time exists. Export tasks, time maps and plans as files, and pause planning when the calendar connection is revoked. Trap: Quietly trimming a three hour task into the ninety minutes that happen to be free makes the plan look achievable and the week fail, and because the trim was never reported the person blames themselves. Report the shortfall in hours, refuse to shorten a task without being asked, and show the committed-against-free figure before anyone plans their week around it. Done when: A task that cannot fit appears in an unplaceable list with a specific reason, never silently shortened, asserted in the suite. The horizon shows committed minutes against available minutes and flags a deficit before the plan is accepted. Export produces tasks, time maps and plans in files that reimport into an empty install. A revoked calendar pauses planning within one sync interval and says so on the main screen. Do not build yet: Nothing after this. ### Out of scope (and why) - Reading event details for any reason, including cleverness about meeting types; the tool needs to know when you are busy and never why. - Cross-device sync, which turns a local plan into a distributed one and needs a conflict model before it needs anything else. - Team scheduling and shared availability, which multiply every privacy and timezone question here. - Any claim that following the plan means the work gets done, which is a promise no scheduler can keep. ### README must contain - That the calendar is read as busy intervals only, asserted by a test that scans the schema and the logs - That time map rules carry their own zone and both clock change days are in the suite - That an identical rerun produces an identical plan, provable by the recorded input hash - That the tool only ever modifies events whose identifier it stored, and never clears a time range - That tasks which do not fit are reported rather than shortened, with the shortfall in hours
# Agent instructions · SkedPal indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, SQLite beneath, Free and busy intervals only, through the provider's own busy query, A deterministic pass over an explicit priority order with a recorded tie-break, Instants stored, rules written in a named zone, expanded per date, Only events the tool created, tagged and tracked by their stored identifier. 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 - Reading the full event list because it is the easier endpoint puts titles, attendees and locations into your database, your logs and eventually a crash report, and none of that is needed to know that three o'clock is taken. Use the busy query, and assert in the suite that no title, attendee or location column exists anywhere. - Generating slots by adding minutes to a local midnight offers a nine o'clock slot that does not exist on the spring transition and two of them on the autumn one, which is rare enough to pass every test written in June and common enough to wreck a week twice a year. Expand from the zone's rules for that specific date, and put both transition days in the suite. - A solver that replans from scratch every hour moves this afternoon's work to Thursday because a new task arrived, and after the third time nobody believes the calendar; worse, an unstable tie-break makes two runs over identical inputs disagree, so the person cannot tell a real change from noise. Order deterministically, hash the inputs, and hold pinned and started blocks still. - Clearing the tool's blocks by deleting every event in a time range removes the dentist appointment that happened to fall inside it, and there is no undo worth the name. Delete only by stored identifier, and if an identifier is missing, leave the event alone and report it.
# Build plan · SkedPal An automatic scheduler whose hard problems are reading a calendar as busy time without learning what is in it, producing a plan that is stable and explainable rather than freshly shuffled every hour, and writing blocks back without ever touching an event it did not create. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Busy time, and not one word more The calendar is read as intervals with no event content anywhere in the system. ### Steps 1. Use the busy query, not the event list The endpoint that returns less is the endpoint that leaks less. Files: `src/calendar/freebusy.ts` 2. Page rather than assume the limit The per-request calendar cap is published and it is not large. Files: `src/calendar/freebusy.ts`, `tests/paging.test.ts` 3. Assert no event content is stored A schema test is the only version of this promise that stays true. Files: `tests/no-event-content.test.ts` ### Done when - [ ] No event content exists in the database or logs - [ ] Busy fetching pages past the per-request limit - [ ] A stale calendar narrows availability - [ ] A revoked calendar stops planning ### Watch out - Reading the full event list because it is the easier endpoint puts titles, attendees and locations into your database, your logs and eventually a crash report, and none of that is needed to know that three o'clock is taken. Use the busy query, and assert in the suite that no title, attendee or location column exists anywhere. ## Phase 2 · Time maps across a clock change Available slots are computed from rules in a named zone and are right on the two awkward days. ### Steps 1. Expand rules per date with a timezone library Never by arithmetic on a local midnight. Files: `src/schedule/expand.ts`, `tests/dst.test.ts` 2. Subtract busy time and buffers to get free slots A partial overlap is the case that hand-rolled interval code gets wrong. Files: `src/schedule/slots.ts`, `tests/overlap.test.ts` 3. Keep the zone on the rule, not on the machine Travelling should not silently rewrite when you work. Files: `src/schedule/timemap.ts` ### Done when - [ ] Both transition days match hand-computed slots - [ ] Partial overlaps remove the right part of a slot - [ ] A chunk that fits nowhere is reported - [ ] Changing the machine zone does not change the plan ### Watch out - Generating slots by adding minutes to a local midnight offers a nine o'clock slot that does not exist on the spring transition and two of them on the autumn one, which is rare enough to pass every test written in June and common enough to wreck a week twice a year. Expand from the zone's rules for that specific date, and put both transition days in the suite. ## Phase 3 · A plan that is the same plan tomorrow The solver is deterministic, records why each block landed where it did, and does not reshuffle work already begun. ### Steps 1. Order candidates with an explicit, total ordering Including an identifier at the end so nothing is ever a real tie. Files: `src/solver/order.ts`, `tests/determinism.test.ts` 2. Hash the inputs into the plan It turns 'nothing changed' from a feeling into a comparison. Files: `src/solver/plan.ts` 3. Hold pinned and started blocks still A scheduler that moves what you already began is a scheduler you stop opening. Files: `src/solver/pins.ts`, `tests/stability.test.ts` ### Done when - [ ] Identical inputs produce an identical plan hash - [ ] Every block names the rule that placed it - [ ] Pinned and started blocks survive a replan - [ ] A new task moves a reported, minimal set of blocks ### Watch out - A solver that replans from scratch every hour moves this afternoon's work to Thursday because a new task arrived, and after the third time nobody believes the calendar; worse, an unstable tie-break makes two runs over identical inputs disagree, so the person cannot tell a real change from noise. Order deterministically, hash the inputs, and hold pinned and started blocks still. ## Phase 4 · Writing blocks back without stomping The calendar gains the tool's blocks and loses nothing else. ### Steps 1. Write only into a calendar the tool owns It makes the blast radius of any mistake visible and small. Files: `src/calendar/write.ts` 2. Touch only events with a stored identifier The range delete is the mistake that removes a real appointment. Files: `src/calendar/write.ts`, `tests/never-touch-foreign.test.ts` 3. Reconcile hand edits into pins A person moving a block is giving you information, not making an error. Files: `src/calendar/reconcile.ts` ### Done when - [ ] Foreign events cannot be modified or deleted - [ ] A hand-moved block becomes a pin - [ ] A deleted block returns its minutes - [ ] Undoing a plan removes exactly its own events ### Watch out - Clearing the tool's blocks by deleting every event in a time range removes the dentist appointment that happened to fall inside it, and there is no undo worth the name. Delete only by stored identifier, and if an identifier is missing, leave the event alone and report it. ## Not in this build - Reading event titles, attendees or locations. - Cross-device sync. - Team and shared scheduling. - Any promise about what you will finish. ## After v1, if you want it - A second calendar as a read-only busy source, which the busy query already supports - Recurring tasks that recompute their remaining minutes each period, built on the existing task model - A weekly review that compares planned blocks against what the calendar says actually happened
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Tasks, time maps, plans and busy intervals. DATABASE_PATH=~/.local/share/scheduler/tasks.db # Required. The registered application with the narrow scope. CALENDAR_CLIENT_ID=your-application-id # Required · secret. Stored in the OS keychain where available, never in the database. CALENDAR_CLIENT_SECRET=redacted # Required. The dedicated calendar. The tool never writes anywhere else. WRITE_CALENDAR_ID=scheduler@group.calendar.google.com # Required. How far ahead to plan. Longer horizons make the capacity figure more honest and the plan less stable. PLANNING_HORIZON_DAYS=14 # Required. Older than this and availability narrows rather than assuming free time. STALE_SYNC_MINUTES=30 # Required. Recorded on every plan, so a plan change caused by a code change is distinguishable from one caused by your week. SOLVER_VERSION=1
# SkedPal · product brief ## Problem The visible automatic scheduling loop is buildable, but a credible replacement needs more than the first screen. SkedPal earns its keep through polish, OS integration, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A scheduler that knows when you are busy and not what you are doing, produces the same plan twice from the same week, never touches an event it did not create, and tells you in hours when the week does not fit. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS, Windows, or Linux development machine - Node.js 22 and Rust toolchain - Local filesystem access and OS permissions ## Explicit non-goals for v1 - Reading event titles, attendees or locations. - Cross-device sync. - Team and shared scheduling. - Any promise about what you will finish. - calendar-provider edge cases and timezone correctness - cross-device sync - deep operating-system edge-case coverage - polished shortcuts and accessibility - vendor-maintained updates ## Success criteria - No event content is confirmed present in the database or logs - Both clock change days are confirmed to match hand-computed slots - Identical inputs are confirmed to produce an identical plan hash - A foreign event is confirmed impossible to modify or delete - An unfittable task is confirmed reported rather than shortened
# Build brief · SkedPal 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 an auto-scheduler that fits flexible tasks into the gaps in your calendar and can explain every placement to replace SkedPal. 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: Tauri 2 with React and TypeScript, SQLite beneath · a desktop tool that holds your task list locally - Calendar: Free and busy intervals only, through the provider's own busy query · the scheduler needs to know when you are busy and never why - Solver: A deterministic pass over an explicit priority order with a recorded tie-break · a plan you cannot explain is a plan you will not follow - Time: Instants stored, rules written in a named zone, expanded per date · the clocks change and a scheduler that ignores that books an hour that does not exist - Write-back: Only events the tool created, tagged and tracked by their stored identifier · the calendar belongs to the person, not to the scheduler ### Data model (create this before Phase 1) - tasks(id, title, total_minutes, remaining_minutes, due_at, earliest_at, priority, min_chunk_minutes, max_chunk_minutes, time_map_id, state) - time_maps(id, name, timezone, rules_json) · when a kind of work is allowed, written in the map's own zone - busy(id, calendar_id, starts_at, ends_at, external_id, is_ours) · start and end and nothing else, because nothing else is fetched - plans(id, computed_at, horizon_days, inputs_sha256, solver_version, unplaceable_json) · the hash makes an identical rerun provable - blocks(id, plan_id, task_id, starts_at, ends_at, placed_by_rule, state, external_event_id, pinned_at) - calendars(id, provider, account_ref, scopes_json, last_synced_at, revoked_at) - The busy table has no title column, so there is nothing for the scheduler, a log or a crash report to leak. ### Phase 1 · Busy time, and not one word more Build: Connect one calendar with the narrowest scope that reads busy time and writes the tool's own events. Use the provider's free and busy query, which returns only start and end times per calendar; the Google Calendar version caps a single request at fifty calendars and a hundred identifiers per group, so page rather than assume. Store the intervals and nothing else, and treat a calendar that has not synced within a stated window as busier than it looks rather than free. Trap: Reading the full event list because it is the easier endpoint puts titles, attendees and locations into your database, your logs and eventually a crash report, and none of that is needed to know that three o'clock is taken. Use the busy query, and assert in the suite that no title, attendee or location column exists anywhere. Done when: No title, description, attendee or location appears in the database or the logs, asserted by a test that scans both. Busy intervals from a calendar with more than the per-request limit are fetched by paging, tested against a fake provider. A calendar not synced within the configured window causes the scheduler to narrow availability rather than plan optimistically. Revoking access is detected and the tool stops planning rather than planning against stale intervals. Do not build yet: No time maps, no solver, no write-back, no notifications. ### Phase 2 · Time maps across a clock change Build: Define time maps as rules in a named zone: deep work on weekday mornings, errands on Saturday, and so on. Expand them per date into instants using a timezone library, subtract busy intervals and buffers, and produce the free slots for the horizon. Decide and document what a rule landing in the missing hour does and what happens on the repeated one. Trap: Generating slots by adding minutes to a local midnight offers a nine o'clock slot that does not exist on the spring transition and two of them on the autumn one, which is rare enough to pass every test written in June and common enough to wreck a week twice a year. Expand from the zone's rules for that specific date, and put both transition days in the suite. Done when: Slots on both clock change days match hand-computed expectations, asserted for a zone that observes them. Buffers and existing busy intervals remove slots correctly, including a busy interval that partially overlaps a slot. A task whose minimum chunk exceeds every remaining slot is reported rather than placed in a slot too small for it. Changing the machine's timezone does not change the plan, because the rules carry their own zone. Do not build yet: No solver, write-back, reminders or export. ### Phase 3 · A plan that is the same plan tomorrow Build: Sort candidate tasks by an explicit ordering of due date, priority and identifier, so there is no tie the machine breaks differently on two runs. Place each into the earliest slot that satisfies its chunk rules, recording which rule placed it. Hash the inputs into the plan. Treat a block the person pinned or already started as fixed, and plan around it rather than through it. Trap: A solver that replans from scratch every hour moves this afternoon's work to Thursday because a new task arrived, and after the third time nobody believes the calendar; worse, an unstable tie-break makes two runs over identical inputs disagree, so the person cannot tell a real change from noise. Order deterministically, hash the inputs, and hold pinned and started blocks still. Done when: Two solver runs over identical inputs produce byte-identical plans, asserted by comparing the plan hash. Every block records the rule that placed it and the interface shows it. A pinned block and a block whose start time has passed survive a replan unchanged, tested. Adding one task moves only the blocks that had to move, measured as a count in the suite and reported to the person. Do not build yet: No calendar write-back, no reminders, no export. ### Phase 4 · Writing blocks back without stomping Build: Create one calendar event per block, store its identifier, and mark it so it is recognisable. Update and delete only events whose identifier the tool stored. Reconcile before each replan: an event the person moved by hand becomes a pin, and one they deleted becomes a task with its time returned. Write into a dedicated calendar rather than the main one where that is available. Trap: Clearing the tool's blocks by deleting every event in a time range removes the dentist appointment that happened to fall inside it, and there is no undo worth the name. Delete only by stored identifier, and if an identifier is missing, leave the event alone and report it. Done when: An attempt to modify or delete an event with no stored identifier fails, asserted by a test that tries. Moving a block in the calendar by hand turns it into a pin on the next reconcile rather than being moved back. Deleting a block in the calendar returns its minutes to the task rather than losing them. Undoing a whole plan removes exactly the events that plan created, tested by counting before and after. Do not build yet: No reminders, no reports, no export. ### Phase 5 · Telling the truth when it does not fit Build: Report every unplaceable task with the reason: not enough slots, a chunk that fits nowhere, a due date already passed. Never silently shorten a task, drop one, or schedule past its due date without saying so. Show how much committed time the horizon holds against how much free time exists. Export tasks, time maps and plans as files, and pause planning when the calendar connection is revoked. Trap: Quietly trimming a three hour task into the ninety minutes that happen to be free makes the plan look achievable and the week fail, and because the trim was never reported the person blames themselves. Report the shortfall in hours, refuse to shorten a task without being asked, and show the committed-against-free figure before anyone plans their week around it. Done when: A task that cannot fit appears in an unplaceable list with a specific reason, never silently shortened, asserted in the suite. The horizon shows committed minutes against available minutes and flags a deficit before the plan is accepted. Export produces tasks, time maps and plans in files that reimport into an empty install. A revoked calendar pauses planning within one sync interval and says so on the main screen. Do not build yet: Nothing after this. ### Out of scope (and why) - Reading event details for any reason, including cleverness about meeting types; the tool needs to know when you are busy and never why. - Cross-device sync, which turns a local plan into a distributed one and needs a conflict model before it needs anything else. - Team scheduling and shared availability, which multiply every privacy and timezone question here. - Any claim that following the plan means the work gets done, which is a promise no scheduler can keep. ### README must contain - That the calendar is read as busy intervals only, asserted by a test that scans the schema and the logs - That time map rules carry their own zone and both clock change days are in the suite - That an identical rerun produces an identical plan, provable by the recorded input hash - That the tool only ever modifies events whose identifier it stored, and never clears a time range - That tasks which do not fit are reported rather than shortened, with the shortfall in hours
# Architecture · SkedPal ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Tauri 2 with React and TypeScript, SQLite beneath | a desktop tool that holds your task list locally | | Calendar | Free and busy intervals only, through the provider's own busy query | the scheduler needs to know when you are busy and never why | | Solver | A deterministic pass over an explicit priority order with a recorded tie-break | a plan you cannot explain is a plan you will not follow | | Time | Instants stored, rules written in a named zone, expanded per date | the clocks change and a scheduler that ignores that books an hour that does not exist | | Write-back | Only events the tool created, tagged and tracked by their stored identifier | the calendar belongs to the person, not to the scheduler | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Calendar read | Busy intervals, paging and staleness. | Any provider with a busy query, provided no event content is stored. | | Time maps | Zone-aware rule expansion and free slots. | Any rule format, provided both transition days stay in the suite. | | Solver | Deterministic ordering, plan hashing and pins. | Any algorithm, provided identical inputs still give an identical plan. | | Calendar write | Owned events only, reconciliation into pins. | Not swappable for range deletion, which removes real appointments. | | Reporting | Unplaceable tasks, capacity and export. | Any presentation, provided nothing is silently shortened. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Tasks, time maps, plans and busy intervals. - `CALENDAR_CLIENT_ID` · required · The registered application with the narrow scope. - `CALENDAR_CLIENT_SECRET` · required, secret · Stored in the OS keychain where available, never in the database. - `WRITE_CALENDAR_ID` · required · The dedicated calendar. The tool never writes anywhere else. - `PLANNING_HORIZON_DAYS` · required · How far ahead to plan. Longer horizons make the capacity figure more honest and the plan less stable. - `STALE_SYNC_MINUTES` · required · Older than this and availability narrows rather than assuming free time. - `SOLVER_VERSION` · required · Recorded on every plan, so a plan change caused by a code change is distinguishable from one caused by your week. ## 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 · SkedPal product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, SQLite beneath, Free and busy intervals only, through the provider's own busy query, A deterministic pass over an explicit priority order with a recorded tie-break, Instants stored, rules written in a named zone, expanded per date, Only events the tool created, tagged and tracked by their stored identifier. - 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 - Reading the full event list because it is the easier endpoint puts titles, attendees and locations into your database, your logs and eventually a crash report, and none of that is needed to know that three o'clock is taken. Use the busy query, and assert in the suite that no title, attendee or location column exists anywhere. - Generating slots by adding minutes to a local midnight offers a nine o'clock slot that does not exist on the spring transition and two of them on the autumn one, which is rare enough to pass every test written in June and common enough to wreck a week twice a year. Expand from the zone's rules for that specific date, and put both transition days in the suite. - A solver that replans from scratch every hour moves this afternoon's work to Thursday because a new task arrived, and after the third time nobody believes the calendar; worse, an unstable tie-break makes two runs over identical inputs disagree, so the person cannot tell a real change from noise. Order deterministically, hash the inputs, and hold pinned and started blocks still. - Clearing the tool's blocks by deleting every event in a time range removes the dentist appointment that happened to fall inside it, and there is no undo worth the name. Delete only by stored identifier, and if an identifier is missing, leave the event alone and report it. - Quietly trimming a three hour task into the ninety minutes that happen to be free makes the plan look achievable and the week fail, and because the trim was never reported the person blames themselves. Report the shortfall in hours, refuse to shorten a task without being asked, and show the committed-against-free figure before anyone plans their week around it.
# Delivery milestones · SkedPal Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Busy time, and not one word more The calendar is read as intervals with no event content anywhere in the system. ### Steps 1. Use the busy query, not the event list The endpoint that returns less is the endpoint that leaks less. Files: `src/calendar/freebusy.ts` 2. Page rather than assume the limit The per-request calendar cap is published and it is not large. Files: `src/calendar/freebusy.ts`, `tests/paging.test.ts` 3. Assert no event content is stored A schema test is the only version of this promise that stays true. Files: `tests/no-event-content.test.ts` ### Done when - [ ] No event content exists in the database or logs - [ ] Busy fetching pages past the per-request limit - [ ] A stale calendar narrows availability - [ ] A revoked calendar stops planning ### Watch out - Reading the full event list because it is the easier endpoint puts titles, attendees and locations into your database, your logs and eventually a crash report, and none of that is needed to know that three o'clock is taken. Use the busy query, and assert in the suite that no title, attendee or location column exists anywhere. ## M2 · Time maps across a clock change Available slots are computed from rules in a named zone and are right on the two awkward days. ### Steps 1. Expand rules per date with a timezone library Never by arithmetic on a local midnight. Files: `src/schedule/expand.ts`, `tests/dst.test.ts` 2. Subtract busy time and buffers to get free slots A partial overlap is the case that hand-rolled interval code gets wrong. Files: `src/schedule/slots.ts`, `tests/overlap.test.ts` 3. Keep the zone on the rule, not on the machine Travelling should not silently rewrite when you work. Files: `src/schedule/timemap.ts` ### Done when - [ ] Both transition days match hand-computed slots - [ ] Partial overlaps remove the right part of a slot - [ ] A chunk that fits nowhere is reported - [ ] Changing the machine zone does not change the plan ### Watch out - Generating slots by adding minutes to a local midnight offers a nine o'clock slot that does not exist on the spring transition and two of them on the autumn one, which is rare enough to pass every test written in June and common enough to wreck a week twice a year. Expand from the zone's rules for that specific date, and put both transition days in the suite. ## M3 · A plan that is the same plan tomorrow The solver is deterministic, records why each block landed where it did, and does not reshuffle work already begun. ### Steps 1. Order candidates with an explicit, total ordering Including an identifier at the end so nothing is ever a real tie. Files: `src/solver/order.ts`, `tests/determinism.test.ts` 2. Hash the inputs into the plan It turns 'nothing changed' from a feeling into a comparison. Files: `src/solver/plan.ts` 3. Hold pinned and started blocks still A scheduler that moves what you already began is a scheduler you stop opening. Files: `src/solver/pins.ts`, `tests/stability.test.ts` ### Done when - [ ] Identical inputs produce an identical plan hash - [ ] Every block names the rule that placed it - [ ] Pinned and started blocks survive a replan - [ ] A new task moves a reported, minimal set of blocks ### Watch out - A solver that replans from scratch every hour moves this afternoon's work to Thursday because a new task arrived, and after the third time nobody believes the calendar; worse, an unstable tie-break makes two runs over identical inputs disagree, so the person cannot tell a real change from noise. Order deterministically, hash the inputs, and hold pinned and started blocks still. ## M4 · Writing blocks back without stomping The calendar gains the tool's blocks and loses nothing else. ### Steps 1. Write only into a calendar the tool owns It makes the blast radius of any mistake visible and small. Files: `src/calendar/write.ts` 2. Touch only events with a stored identifier The range delete is the mistake that removes a real appointment. Files: `src/calendar/write.ts`, `tests/never-touch-foreign.test.ts` 3. Reconcile hand edits into pins A person moving a block is giving you information, not making an error. Files: `src/calendar/reconcile.ts` ### Done when - [ ] Foreign events cannot be modified or deleted - [ ] A hand-moved block becomes a pin - [ ] A deleted block returns its minutes - [ ] Undoing a plan removes exactly its own events ### Watch out - Clearing the tool's blocks by deleting every event in a time range removes the dentist appointment that happened to fall inside it, and there is no undo worth the name. Delete only by stored identifier, and if an identifier is missing, leave the event alone and report it. ## M5 · Telling the truth when it does not fit (production only) The tool says plainly what cannot be done and lets you take everything with you. ### Steps 1. Report unplaceable tasks with reasons The shortfall is the most useful output this tool produces. Files: `src/solver/unplaceable.ts`, `src/views/report.tsx` 2. Show committed against available minutes One number that stops a week being planned on arithmetic that never worked. Files: `src/views/capacity.tsx` 3. Export everything and pause on revocation Planning against a calendar you can no longer read is worse than not planning. Files: `src/export/run.ts`, `src/calendar/revoked.ts` ### Done when - [ ] Unplaceable tasks are listed with reasons - [ ] No task is silently shortened - [ ] Capacity shows committed against available - [ ] A revoked calendar pauses planning and says so ### Watch out - Quietly trimming a three hour task into the ninety minutes that happen to be free makes the plan look achievable and the week fail, and because the trim was never reported the person blames themselves. Report the shortfall in hours, refuse to shorten a task without being asked, and show the committed-against-free figure before anyone plans their week around it.
# Operations · SkedPal ## Backup The SQLite file, which holds the task list and the pins. The calendar holds the blocks and can be rebuilt from a plan, so the task list is the part that is genuinely yours and genuinely irreplaceable. ## Restore Restore the database, reconnect the calendar, then reconcile before planning. Reconciling first means hand edits made while the tool was down become pins rather than being overwritten by a stale plan. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the checks are at startup and in the suite: the age of the last sync, a revoked connection, blocks in the calendar with no matching row, and rows with no matching event. ## Incident checklist A missing appointment means something deleted an event it did not own, and the write log names it. A plan that changed for no reason is diagnosed by comparing the two plans' input hashes and solver versions before looking at anything else. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No event content is confirmed present in the database or logs - [ ] Both clock change days are confirmed to match hand-computed slots - [ ] Identical inputs are confirmed to produce an identical plan hash - [ ] A foreign event is confirmed impossible to modify or delete - [ ] An unfittable task is confirmed reported rather than shortened ## Launch constraint Do not market omitted SkedPal 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. Tasks, time maps, plans and busy intervals. DATABASE_PATH=~/.local/share/scheduler/tasks.db # Required. The registered application with the narrow scope. CALENDAR_CLIENT_ID=your-application-id # Required · secret. Stored in the OS keychain where available, never in the database. CALENDAR_CLIENT_SECRET=redacted # Required. The dedicated calendar. The tool never writes anywhere else. WRITE_CALENDAR_ID=scheduler@group.calendar.google.com # Required. How far ahead to plan. Longer horizons make the capacity figure more honest and the plan less stable. PLANNING_HORIZON_DAYS=14 # Required. Older than this and availability narrows rather than assuming free time. STALE_SYNC_MINUTES=30 # Required. Recorded on every plan, so a plan change caused by a code change is distinguishable from one caused by your week. SOLVER_VERSION=1
$ choose a build depth, inspect the files, then open the complete pack in your agent
SkedPal: People keep paying because a utility used dozens of times a day must be instant, predictable, and compatible with every OS update.
xcalendar-provider edge cases and timezone correctness
xcross-device sync
xdeep operating-system edge-case coverage
xpolished shortcuts and accessibility
xvendor-maintained updates
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
SkedPal pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| core | — | $9.95/user | 21-day automatic-scheduling window and 1 connected calendar account. |
| pro | — | $14.95/user | 60-day automatic-scheduling window, unlimited calendar accounts, and time tracking. |
free tierno free tier
billingmonthly + annual; only annual numeric prices were publicly exposed; 14-day trial with no card
hidden costsThe $9.95 figure on file is the annual-billing equivalent, not a verified month-to-month price. Core is capped at a 21-day scheduling horizon and 1 calendar account; Pro raises the horizon to 60 days and removes the calendar-account cap.
verified 2026-08-14 · source ↗
Vibecode SkedPal
Kinda. The core of SkedPal is buildable in a weekend with the prompt on this page, but there are real gaps: calendar-provider edge cases and timezone correctness, cross-device sync. Read the honest list above before committing.
How much does SkedPal cost?
SkedPal costs about $9.95/month (Core, checked 2026-07-31), which is $119.39999999999999 per year.
What do I lose by replacing SkedPal?
Honestly: calendar-provider edge cases and timezone correctness; cross-device sync; deep operating-system edge-case coverage; polished shortcuts and accessibility; vendor-maintained updates. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to SkedPal?
Yes: FlowSavvy Free (It puts tasks around calendar events for free; the rolling scheduling horizon stops two weeks out.) Reclaim Lite (A free one-week planning horizon for tasks and focus time; long-range plans remain billable.) The prompt is for when you want it exactly your way.