Vibecode TimeHero
track this build4 phases, 12 steps, beginner friendly0%The visible automatic work planning loop is buildable, but a credible replacement needs more than the first screen. TimeHero 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 TimeHero. 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 ===== # TimeHero · indie build A local work planner whose hard problems are computing capacity that really exists, placing tasks by an algorithm you can explain, and refusing to hide the fact that a deadline is going to be missed. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, keyboard first | a planner opened twenty times a day has to answer instantly | | Scheduler | One named deterministic algorithm with a stated tie-break | a plan you cannot explain is a plan nobody follows | | Storage | SQLite where each plan is a version rather than an overwrite | this morning's reshuffle needs to be comparable with yesterday's | | Time | Instants in UTC, working hours as local rules plus a named zone | a working day is local, a deadline is an instant | | Calendar | Optional read-only import of busy intervals from an iCalendar file or URL | the planner must be useful with nothing connected | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 and the Rust toolchain** · free - Why: Tauri 2 needs both, and the desktop shell is what makes the keyboard-first behaviour possible. - Get it: nodejs.org and rustup.rs, then the Tauri prerequisites for your platform. - Verify: node --version prints v22 or higher and cargo --version succeeds - [ ] **An iCalendar file or subscription URL for your real commitments** (optional) · free - Why: Capacity computed without your actual meetings is fiction, and the whole build rests on capacity being true. - Get it: Most calendars publish a private subscription address; download the file or point the importer at it. - Verify: Imported busy intervals match a week you can check by eye - [ ] **Your scheduling rule and its tie-break** · free - Why: The plan is only arguable if the rule is written down, and picking the rule after seeing the output means picking whatever the code already did. - Get it: Choose one rule, such as earliest deadline first with a stated tie-break on priority then creation order, and write it into the documentation before implementing it. - Verify: The README names the rule and the tie-break, and the code refers to the same names - [ ] **Your near horizon, churn limit and working hours** · free - Why: Each is asserted by a test, and stability is a decision rather than an emergent property. - Get it: Write down how far ahead placements are pinned, how many minutes of movement is acceptable for a small change, and your working hours with their zone. - Verify: All three are in the configuration and named in tests - [ ] **A week of real tasks with honest estimates** · free - Why: Infeasibility is only testable against a week that genuinely does not fit, and a made-up one will not. - Get it: Take an actual busy week, enter the tasks and estimate them without flattering yourself. - Verify: The planner produces at least one miss with a shortfall you recognise ## 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: - Task creation from email and chat. - Team planning and workload balancing. - Time tracking. - Completion dates presented as promises. - deep operating-system edge-case coverage - polished shortcuts and accessibility - vendor-maintained updates - cross-device sync If one of those is essential to you, that is the reason to keep paying for TimeHero, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · TimeHero The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a planner that fits your tasks into the time you actually have and says out loud what will not fit to replace TimeHero. 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) - Shell: Tauri 2 with React and TypeScript, keyboard first · a planner opened twenty times a day has to answer instantly - Scheduler: One named deterministic algorithm with a stated tie-break · a plan you cannot explain is a plan nobody follows - Storage: SQLite where each plan is a version rather than an overwrite · this morning's reshuffle needs to be comparable with yesterday's - Time: Instants in UTC, working hours as local rules plus a named zone · a working day is local, a deadline is an instant - Calendar: Optional read-only import of busy intervals from an iCalendar file or URL · the planner must be useful with nothing connected ### Data model (create this before Phase 1) - tasks(id, title, estimate_minutes, remaining_minutes, deadline_at, earliest_start_at, priority, project_id, state, created_at) - dependencies(task_id, depends_on_task_id) · a cycle is rejected at write time, not discovered at plan time - hours(weekday, start_local, end_local, timezone) · when you can actually work, as local rules - busy_blocks(id, source, starts_at, ends_at, external_uid) · imported, times only, and tagged with where they came from - plans(id, computed_at, algorithm, params_json, horizon_end, feasible, churn_minutes) - placements(plan_id, task_id, starts_at, ends_at, ordinal, reason) · the reason names the rule that put it there - misses(plan_id, task_id, deadline_at, would_finish_at, shortfall_minutes) · the output that is never hidden - A plan is data with a reason attached to every placement, so 'why is this on Thursday' has an answer that is not a shrug. ### Phase 1 · Capacity that actually exists Build: Store working hours as local rules with a named zone and expand them per date from the zone's rules. Subtract imported busy intervals. Produce a list of free intervals over the horizon and show the total per day. Reject a dependency cycle when it is written rather than at plan time. Trap: Treating capacity as eight hours a day produces a plan built on time you do not have, and the first thing it does is quietly assume your Tuesday meetings are not happening. Subtract real busy intervals, expand hours from the zone's rules for each date so the two clock change days are right, and show the daily total so a fantasy week is visible before anything is scheduled. Done when: Daily capacity equals working hours minus busy intervals for that date, asserted against hand-computed examples including partial overlaps. Both clock change days produce the documented number of available minutes, tested with both dates. A dependency cycle is refused at write time with the cycle named. Capacity per day is visible before any plan exists. Do not build yet: No scheduling, misses, replanning or export. ### Phase 2 · A scheduler you can name Build: Implement one scheduling rule, name it in the code and the README, and state the tie-break explicitly. Walk free intervals in order, decrementing remaining capacity as each task is placed and respecting dependencies and earliest start. Record a reason string per placement. Trap: A greedy pass that sorts by deadline and assigns each task a start time without decrementing the capacity it just consumed puts eleven hours of work into a seven hour Tuesday, and nothing errors because nobody asked whether the day was full. Consume capacity as you place, and assert that the sum of placements in any day never exceeds that day's available minutes. Done when: The sum of placed minutes on any day never exceeds that day's capacity, asserted over generated inputs. Running the scheduler twice on identical inputs produces an identical plan, asserted by comparing serialised plans. Every placement has a non-empty reason naming the rule, and the reason is visible in the interface. A task cannot start before its dependencies finish or before its earliest start, tested. Do not build yet: No infeasibility reporting, stability or export yet. ### Phase 3 · Saying what will not fit Build: Place tasks without ever altering an estimate. When a task cannot finish before its deadline, write a miss row with the deadline, the projected finish and the shortfall in minutes. Show misses at the top of the plan, and offer the smallest changes that would resolve each one: hours to add, work to drop, or a deadline to move. Trap: A planner that shaves estimates to make the week fit is worse than a list, because it hands you a schedule that looks achievable, is not, and teaches you within a fortnight to ignore it. Never write to an estimate from the scheduler; produce a miss with a shortfall in minutes and let the person decide what gives. Done when: The scheduler never writes to a task's estimate, asserted by a test that fails if the field changes during planning. A week containing more work than capacity produces miss rows with shortfalls in minutes summing to the overflow, tested with a constructed example. Misses appear above the plan rather than inside it. Each miss lists at least one concrete resolution with its size. Do not build yet: No stability measures or export yet. ### Phase 4 · A plan that does not reshuffle every morning Build: Pin work already started and anything inside a near horizon you set. Replan on explicit triggers rather than continuously. Compute a churn figure against the previous plan in minutes moved, store it with the plan, and show what changed and why. Trap: Recomputing the whole plan whenever anything changes rearranges half your week at nine in the morning, and since each rearrangement is individually defensible you cannot argue with it, so by Wednesday you have stopped looking. Pin what has started and the near horizon, replan on triggers, and put a churn number on every plan so a restless scheduler is visible rather than merely annoying. Done when: Work already started keeps its slot across a replan, tested. Adding one small task moves less than a stated number of minutes of existing work, asserted over generated cases. Every plan stores a churn figure and a diff against the previous plan is viewable. Replanning happens on named triggers only, and the trigger is recorded with the plan. Do not build yet: No export or backup yet. ### Phase 5 · Getting the plan out without poisoning it Build: Export placements as iCalendar and CSV, tagging every exported event with an identifier that marks it as yours. On import, exclude anything carrying that tag. Export and import the whole task set as JSON. Snapshot the database and exercise a restore in the suite. Trap: Exporting placements to your calendar and then importing that calendar as busy intervals makes the plan block itself: every task appears as a commitment, capacity drops to nothing, and the next plan says the week is impossible. Tag exported events and filter them on import, and test the round trip specifically. Done when: A full export to calendar followed by an import leaves capacity unchanged, tested as an explicit round trip. Exported events carry an identifier marking their origin and imports exclude them. The JSON export re-imports into an empty instance with tasks, dependencies and history intact. A restore from snapshot is exercised in the suite rather than described. Do not build yet: Nothing after this. ### Out of scope (and why) - Automatic task creation from email and chat, which is a connector business and a stream of low quality tasks that make the plan worse. - Team planning and workload balancing across people, which turns one capacity calculation into a negotiation with its own politics. - Time tracking. Planning and tracking are different products, and bolting one onto the other usually ruins both. - A predicted completion date presented as a promise. The scheduler projects from estimates you typed, and the interface should say so rather than implying knowledge it does not have. ### README must contain - The name of the scheduling algorithm, its tie-break, and where to read it in the code - That capacity is working hours minus real commitments, computed per date from the zone's rules - That the scheduler never alters an estimate, and that a deadline it cannot meet becomes a miss with a shortfall in minutes - That replanning pins started work and reports churn, because a plan that reshuffles daily stops being followed - That exported calendar events are tagged and excluded on import, or the plan blocks itself ===== AGENTS.md ===== # Agent instructions · TimeHero indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, keyboard first, One named deterministic algorithm with a stated tie-break, SQLite where each plan is a version rather than an overwrite, Instants in UTC, working hours as local rules plus a named zone, Optional read-only import of busy intervals from an iCalendar file or URL. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Treating capacity as eight hours a day produces a plan built on time you do not have, and the first thing it does is quietly assume your Tuesday meetings are not happening. Subtract real busy intervals, expand hours from the zone's rules for each date so the two clock change days are right, and show the daily total so a fantasy week is visible before anything is scheduled. - A greedy pass that sorts by deadline and assigns each task a start time without decrementing the capacity it just consumed puts eleven hours of work into a seven hour Tuesday, and nothing errors because nobody asked whether the day was full. Consume capacity as you place, and assert that the sum of placements in any day never exceeds that day's available minutes. - A planner that shaves estimates to make the week fit is worse than a list, because it hands you a schedule that looks achievable, is not, and teaches you within a fortnight to ignore it. Never write to an estimate from the scheduler; produce a miss with a shortfall in minutes and let the person decide what gives. - Recomputing the whole plan whenever anything changes rearranges half your week at nine in the morning, and since each rearrangement is individually defensible you cannot argue with it, so by Wednesday you have stopped looking. Pin what has started and the near horizon, replan on triggers, and put a churn number on every plan so a restless scheduler is visible rather than merely annoying. ===== BUILD_PLAN.md ===== # Build plan · TimeHero A local work planner whose hard problems are computing capacity that really exists, placing tasks by an algorithm you can explain, and refusing to hide the fact that a deadline is going to be missed. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capacity that actually exists Available minutes come from working hours minus real commitments, computed per date. ### Steps 1. Expand working hours per date from zone rules Not from a fixed offset. Write both transition days as tests first. Files: `src/time/hours.ts`, `tests/dst.test.ts` 2. Subtract imported busy intervals Tag each one with its source so you can tell yours from theirs. Files: `src/time/capacity.ts` 3. Reject dependency cycles on write A cycle discovered inside the scheduler is a hang with a nice name. Files: `src/tasks/dependencies.ts`, `tests/cycle.test.ts` ### Done when - [ ] Daily capacity matches hand-computed examples - [ ] Both clock change days give the documented minutes - [ ] A dependency cycle is refused at write time - [ ] Daily capacity is visible before planning ### Watch out - Treating capacity as eight hours a day produces a plan built on time you do not have, and the first thing it does is quietly assume your Tuesday meetings are not happening. Subtract real busy intervals, expand hours from the zone's rules for each date so the two clock change days are right, and show the daily total so a fantasy week is visible before anything is scheduled. ## Phase 2 · A scheduler you can name The same inputs produce the same plan, and every placement carries the rule that caused it. ### Steps 1. Name the algorithm and its tie-break In the code and in the README. An unnamed heuristic drifts. Files: `src/plan/schedule.ts`, `docs/algorithm.md` 2. Consume capacity as you place The bug this phase exists to prevent is a day that quietly overfills. Files: `src/plan/schedule.ts`, `tests/capacity-invariant.test.ts` 3. Record a reason on every placement The reason is what makes the plan arguable instead of magical. Files: `src/plan/reasons.ts` ### Done when - [ ] No day exceeds its capacity - [ ] Identical inputs give an identical plan - [ ] Every placement carries a reason - [ ] Dependencies and earliest start are respected ### Watch out - A greedy pass that sorts by deadline and assigns each task a start time without decrementing the capacity it just consumed puts eleven hours of work into a seven hour Tuesday, and nothing errors because nobody asked whether the day was full. Consume capacity as you place, and assert that the sum of placements in any day never exceeds that day's available minutes. ## Phase 3 · Saying what will not fit A deadline that cannot be met is reported with a number, never absorbed. ### Steps 1. Forbid the scheduler from touching estimates A test that fails on any write to the field, not a comment. Files: `src/plan/schedule.ts`, `tests/no-estimate-writes.test.ts` 2. Emit a miss row with a shortfall In minutes. A number is arguable; a warning icon is not. Files: `src/plan/misses.ts` 3. Offer the smallest resolution, sized Add these hours, drop this task, or move this date. Files: `src/plan/resolutions.ts` ### Done when - [ ] Estimates are never written by the scheduler - [ ] Shortfalls sum to the overflow - [ ] Misses appear above the plan - [ ] Each miss carries a sized resolution ### Watch out - A planner that shaves estimates to make the week fit is worse than a list, because it hands you a schedule that looks achievable, is not, and teaches you within a fortnight to ignore it. Never write to an estimate from the scheduler; produce a miss with a shortfall in minutes and let the person decide what gives. ## Phase 4 · A plan that does not reshuffle every morning Replanning moves what it must and reports how much it moved. ### Steps 1. Pin started work and the near horizon Stability beats optimality once a human is following the plan. Files: `src/plan/pinning.ts`, `tests/stability.test.ts` 2. Compute and store churn per plan Minutes moved, against the previous plan. Files: `src/plan/churn.ts` 3. Show the diff and the trigger What changed, and what made it change. Files: `src/views/PlanDiff.tsx` ### Done when - [ ] Started work keeps its slot across a replan - [ ] A small addition moves less than the stated churn - [ ] Every plan stores churn and a viewable diff - [ ] Replans record their trigger ### Watch out - Recomputing the whole plan whenever anything changes rearranges half your week at nine in the morning, and since each rearrangement is individually defensible you cannot argue with it, so by Wednesday you have stopped looking. Pin what has started and the near horizon, replan on triggers, and put a churn number on every plan so a restless scheduler is visible rather than merely annoying. ## Not in this build - Task creation from email and chat. - Team planning and workload balancing. - Time tracking. - Completion dates presented as promises. ## After v1, if you want it - Estimate calibration from the history you already store, shown as a ratio rather than applied silently - A second scheduling rule selectable per project, once the first has held its invariants for a month - Recurring tasks, which are a generator in front of the same scheduler ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Tasks, plans and plan history. History is the part you cannot recompute. DATABASE_PATH=~/.local/share/planner/plan.db # Required. Working hours expand from this zone's rules per date. WORK_TIMEZONE=Europe/London # Required. How far ahead the scheduler places work. PLAN_HORIZON_DAYS=21 # Required. Placements inside this window survive a replan. PIN_HORIZON_HOURS=48 # Required. A small change moving more than this is a scheduler bug, and the test says so. CHURN_LIMIT_MINUTES=120 # Optional · secret. Optional. Read-only busy intervals. The app works without it. CALENDAR_ICS_URL=https://calendar.example.com/private/basic.ics # Required. Marks exported events so the importer can exclude them. EXPORT_ORIGIN_TAG=planner-generated
You are building a lean indie version of TimeHero. 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 ===== # TimeHero · indie build A local work planner whose hard problems are computing capacity that really exists, placing tasks by an algorithm you can explain, and refusing to hide the fact that a deadline is going to be missed. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, keyboard first | a planner opened twenty times a day has to answer instantly | | Scheduler | One named deterministic algorithm with a stated tie-break | a plan you cannot explain is a plan nobody follows | | Storage | SQLite where each plan is a version rather than an overwrite | this morning's reshuffle needs to be comparable with yesterday's | | Time | Instants in UTC, working hours as local rules plus a named zone | a working day is local, a deadline is an instant | | Calendar | Optional read-only import of busy intervals from an iCalendar file or URL | the planner must be useful with nothing connected | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 and the Rust toolchain** · free - Why: Tauri 2 needs both, and the desktop shell is what makes the keyboard-first behaviour possible. - Get it: nodejs.org and rustup.rs, then the Tauri prerequisites for your platform. - Verify: node --version prints v22 or higher and cargo --version succeeds - [ ] **An iCalendar file or subscription URL for your real commitments** (optional) · free - Why: Capacity computed without your actual meetings is fiction, and the whole build rests on capacity being true. - Get it: Most calendars publish a private subscription address; download the file or point the importer at it. - Verify: Imported busy intervals match a week you can check by eye - [ ] **Your scheduling rule and its tie-break** · free - Why: The plan is only arguable if the rule is written down, and picking the rule after seeing the output means picking whatever the code already did. - Get it: Choose one rule, such as earliest deadline first with a stated tie-break on priority then creation order, and write it into the documentation before implementing it. - Verify: The README names the rule and the tie-break, and the code refers to the same names - [ ] **Your near horizon, churn limit and working hours** · free - Why: Each is asserted by a test, and stability is a decision rather than an emergent property. - Get it: Write down how far ahead placements are pinned, how many minutes of movement is acceptable for a small change, and your working hours with their zone. - Verify: All three are in the configuration and named in tests - [ ] **A week of real tasks with honest estimates** · free - Why: Infeasibility is only testable against a week that genuinely does not fit, and a made-up one will not. - Get it: Take an actual busy week, enter the tasks and estimate them without flattering yourself. - Verify: The planner produces at least one miss with a shortfall you recognise ## 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: - Task creation from email and chat. - Team planning and workload balancing. - Time tracking. - Completion dates presented as promises. - deep operating-system edge-case coverage - polished shortcuts and accessibility - vendor-maintained updates - cross-device sync If one of those is essential to you, that is the reason to keep paying for TimeHero, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · TimeHero The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a planner that fits your tasks into the time you actually have and says out loud what will not fit to replace TimeHero. 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) - Shell: Tauri 2 with React and TypeScript, keyboard first · a planner opened twenty times a day has to answer instantly - Scheduler: One named deterministic algorithm with a stated tie-break · a plan you cannot explain is a plan nobody follows - Storage: SQLite where each plan is a version rather than an overwrite · this morning's reshuffle needs to be comparable with yesterday's - Time: Instants in UTC, working hours as local rules plus a named zone · a working day is local, a deadline is an instant - Calendar: Optional read-only import of busy intervals from an iCalendar file or URL · the planner must be useful with nothing connected ### Data model (create this before Phase 1) - tasks(id, title, estimate_minutes, remaining_minutes, deadline_at, earliest_start_at, priority, project_id, state, created_at) - dependencies(task_id, depends_on_task_id) · a cycle is rejected at write time, not discovered at plan time - hours(weekday, start_local, end_local, timezone) · when you can actually work, as local rules - busy_blocks(id, source, starts_at, ends_at, external_uid) · imported, times only, and tagged with where they came from - plans(id, computed_at, algorithm, params_json, horizon_end, feasible, churn_minutes) - placements(plan_id, task_id, starts_at, ends_at, ordinal, reason) · the reason names the rule that put it there - misses(plan_id, task_id, deadline_at, would_finish_at, shortfall_minutes) · the output that is never hidden - A plan is data with a reason attached to every placement, so 'why is this on Thursday' has an answer that is not a shrug. ### Phase 1 · Capacity that actually exists Build: Store working hours as local rules with a named zone and expand them per date from the zone's rules. Subtract imported busy intervals. Produce a list of free intervals over the horizon and show the total per day. Reject a dependency cycle when it is written rather than at plan time. Trap: Treating capacity as eight hours a day produces a plan built on time you do not have, and the first thing it does is quietly assume your Tuesday meetings are not happening. Subtract real busy intervals, expand hours from the zone's rules for each date so the two clock change days are right, and show the daily total so a fantasy week is visible before anything is scheduled. Done when: Daily capacity equals working hours minus busy intervals for that date, asserted against hand-computed examples including partial overlaps. Both clock change days produce the documented number of available minutes, tested with both dates. A dependency cycle is refused at write time with the cycle named. Capacity per day is visible before any plan exists. Do not build yet: No scheduling, misses, replanning or export. ### Phase 2 · A scheduler you can name Build: Implement one scheduling rule, name it in the code and the README, and state the tie-break explicitly. Walk free intervals in order, decrementing remaining capacity as each task is placed and respecting dependencies and earliest start. Record a reason string per placement. Trap: A greedy pass that sorts by deadline and assigns each task a start time without decrementing the capacity it just consumed puts eleven hours of work into a seven hour Tuesday, and nothing errors because nobody asked whether the day was full. Consume capacity as you place, and assert that the sum of placements in any day never exceeds that day's available minutes. Done when: The sum of placed minutes on any day never exceeds that day's capacity, asserted over generated inputs. Running the scheduler twice on identical inputs produces an identical plan, asserted by comparing serialised plans. Every placement has a non-empty reason naming the rule, and the reason is visible in the interface. A task cannot start before its dependencies finish or before its earliest start, tested. Do not build yet: No infeasibility reporting, stability or export yet. ### Phase 3 · Saying what will not fit Build: Place tasks without ever altering an estimate. When a task cannot finish before its deadline, write a miss row with the deadline, the projected finish and the shortfall in minutes. Show misses at the top of the plan, and offer the smallest changes that would resolve each one: hours to add, work to drop, or a deadline to move. Trap: A planner that shaves estimates to make the week fit is worse than a list, because it hands you a schedule that looks achievable, is not, and teaches you within a fortnight to ignore it. Never write to an estimate from the scheduler; produce a miss with a shortfall in minutes and let the person decide what gives. Done when: The scheduler never writes to a task's estimate, asserted by a test that fails if the field changes during planning. A week containing more work than capacity produces miss rows with shortfalls in minutes summing to the overflow, tested with a constructed example. Misses appear above the plan rather than inside it. Each miss lists at least one concrete resolution with its size. Do not build yet: No stability measures or export yet. ### Phase 4 · A plan that does not reshuffle every morning Build: Pin work already started and anything inside a near horizon you set. Replan on explicit triggers rather than continuously. Compute a churn figure against the previous plan in minutes moved, store it with the plan, and show what changed and why. Trap: Recomputing the whole plan whenever anything changes rearranges half your week at nine in the morning, and since each rearrangement is individually defensible you cannot argue with it, so by Wednesday you have stopped looking. Pin what has started and the near horizon, replan on triggers, and put a churn number on every plan so a restless scheduler is visible rather than merely annoying. Done when: Work already started keeps its slot across a replan, tested. Adding one small task moves less than a stated number of minutes of existing work, asserted over generated cases. Every plan stores a churn figure and a diff against the previous plan is viewable. Replanning happens on named triggers only, and the trigger is recorded with the plan. Do not build yet: No export or backup yet. ### Phase 5 · Getting the plan out without poisoning it Build: Export placements as iCalendar and CSV, tagging every exported event with an identifier that marks it as yours. On import, exclude anything carrying that tag. Export and import the whole task set as JSON. Snapshot the database and exercise a restore in the suite. Trap: Exporting placements to your calendar and then importing that calendar as busy intervals makes the plan block itself: every task appears as a commitment, capacity drops to nothing, and the next plan says the week is impossible. Tag exported events and filter them on import, and test the round trip specifically. Done when: A full export to calendar followed by an import leaves capacity unchanged, tested as an explicit round trip. Exported events carry an identifier marking their origin and imports exclude them. The JSON export re-imports into an empty instance with tasks, dependencies and history intact. A restore from snapshot is exercised in the suite rather than described. Do not build yet: Nothing after this. ### Out of scope (and why) - Automatic task creation from email and chat, which is a connector business and a stream of low quality tasks that make the plan worse. - Team planning and workload balancing across people, which turns one capacity calculation into a negotiation with its own politics. - Time tracking. Planning and tracking are different products, and bolting one onto the other usually ruins both. - A predicted completion date presented as a promise. The scheduler projects from estimates you typed, and the interface should say so rather than implying knowledge it does not have. ### README must contain - The name of the scheduling algorithm, its tie-break, and where to read it in the code - That capacity is working hours minus real commitments, computed per date from the zone's rules - That the scheduler never alters an estimate, and that a deadline it cannot meet becomes a miss with a shortfall in minutes - That replanning pins started work and reports churn, because a plan that reshuffles daily stops being followed - That exported calendar events are tagged and excluded on import, or the plan blocks itself ===== AGENTS.md ===== # Agent instructions · TimeHero indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, keyboard first, One named deterministic algorithm with a stated tie-break, SQLite where each plan is a version rather than an overwrite, Instants in UTC, working hours as local rules plus a named zone, Optional read-only import of busy intervals from an iCalendar file or URL. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Treating capacity as eight hours a day produces a plan built on time you do not have, and the first thing it does is quietly assume your Tuesday meetings are not happening. Subtract real busy intervals, expand hours from the zone's rules for each date so the two clock change days are right, and show the daily total so a fantasy week is visible before anything is scheduled. - A greedy pass that sorts by deadline and assigns each task a start time without decrementing the capacity it just consumed puts eleven hours of work into a seven hour Tuesday, and nothing errors because nobody asked whether the day was full. Consume capacity as you place, and assert that the sum of placements in any day never exceeds that day's available minutes. - A planner that shaves estimates to make the week fit is worse than a list, because it hands you a schedule that looks achievable, is not, and teaches you within a fortnight to ignore it. Never write to an estimate from the scheduler; produce a miss with a shortfall in minutes and let the person decide what gives. - Recomputing the whole plan whenever anything changes rearranges half your week at nine in the morning, and since each rearrangement is individually defensible you cannot argue with it, so by Wednesday you have stopped looking. Pin what has started and the near horizon, replan on triggers, and put a churn number on every plan so a restless scheduler is visible rather than merely annoying. ===== BUILD_PLAN.md ===== # Build plan · TimeHero A local work planner whose hard problems are computing capacity that really exists, placing tasks by an algorithm you can explain, and refusing to hide the fact that a deadline is going to be missed. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capacity that actually exists Available minutes come from working hours minus real commitments, computed per date. ### Steps 1. Expand working hours per date from zone rules Not from a fixed offset. Write both transition days as tests first. Files: `src/time/hours.ts`, `tests/dst.test.ts` 2. Subtract imported busy intervals Tag each one with its source so you can tell yours from theirs. Files: `src/time/capacity.ts` 3. Reject dependency cycles on write A cycle discovered inside the scheduler is a hang with a nice name. Files: `src/tasks/dependencies.ts`, `tests/cycle.test.ts` ### Done when - [ ] Daily capacity matches hand-computed examples - [ ] Both clock change days give the documented minutes - [ ] A dependency cycle is refused at write time - [ ] Daily capacity is visible before planning ### Watch out - Treating capacity as eight hours a day produces a plan built on time you do not have, and the first thing it does is quietly assume your Tuesday meetings are not happening. Subtract real busy intervals, expand hours from the zone's rules for each date so the two clock change days are right, and show the daily total so a fantasy week is visible before anything is scheduled. ## Phase 2 · A scheduler you can name The same inputs produce the same plan, and every placement carries the rule that caused it. ### Steps 1. Name the algorithm and its tie-break In the code and in the README. An unnamed heuristic drifts. Files: `src/plan/schedule.ts`, `docs/algorithm.md` 2. Consume capacity as you place The bug this phase exists to prevent is a day that quietly overfills. Files: `src/plan/schedule.ts`, `tests/capacity-invariant.test.ts` 3. Record a reason on every placement The reason is what makes the plan arguable instead of magical. Files: `src/plan/reasons.ts` ### Done when - [ ] No day exceeds its capacity - [ ] Identical inputs give an identical plan - [ ] Every placement carries a reason - [ ] Dependencies and earliest start are respected ### Watch out - A greedy pass that sorts by deadline and assigns each task a start time without decrementing the capacity it just consumed puts eleven hours of work into a seven hour Tuesday, and nothing errors because nobody asked whether the day was full. Consume capacity as you place, and assert that the sum of placements in any day never exceeds that day's available minutes. ## Phase 3 · Saying what will not fit A deadline that cannot be met is reported with a number, never absorbed. ### Steps 1. Forbid the scheduler from touching estimates A test that fails on any write to the field, not a comment. Files: `src/plan/schedule.ts`, `tests/no-estimate-writes.test.ts` 2. Emit a miss row with a shortfall In minutes. A number is arguable; a warning icon is not. Files: `src/plan/misses.ts` 3. Offer the smallest resolution, sized Add these hours, drop this task, or move this date. Files: `src/plan/resolutions.ts` ### Done when - [ ] Estimates are never written by the scheduler - [ ] Shortfalls sum to the overflow - [ ] Misses appear above the plan - [ ] Each miss carries a sized resolution ### Watch out - A planner that shaves estimates to make the week fit is worse than a list, because it hands you a schedule that looks achievable, is not, and teaches you within a fortnight to ignore it. Never write to an estimate from the scheduler; produce a miss with a shortfall in minutes and let the person decide what gives. ## Phase 4 · A plan that does not reshuffle every morning Replanning moves what it must and reports how much it moved. ### Steps 1. Pin started work and the near horizon Stability beats optimality once a human is following the plan. Files: `src/plan/pinning.ts`, `tests/stability.test.ts` 2. Compute and store churn per plan Minutes moved, against the previous plan. Files: `src/plan/churn.ts` 3. Show the diff and the trigger What changed, and what made it change. Files: `src/views/PlanDiff.tsx` ### Done when - [ ] Started work keeps its slot across a replan - [ ] A small addition moves less than the stated churn - [ ] Every plan stores churn and a viewable diff - [ ] Replans record their trigger ### Watch out - Recomputing the whole plan whenever anything changes rearranges half your week at nine in the morning, and since each rearrangement is individually defensible you cannot argue with it, so by Wednesday you have stopped looking. Pin what has started and the near horizon, replan on triggers, and put a churn number on every plan so a restless scheduler is visible rather than merely annoying. ## Not in this build - Task creation from email and chat. - Team planning and workload balancing. - Time tracking. - Completion dates presented as promises. ## After v1, if you want it - Estimate calibration from the history you already store, shown as a ratio rather than applied silently - A second scheduling rule selectable per project, once the first has held its invariants for a month - Recurring tasks, which are a generator in front of the same scheduler ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Tasks, plans and plan history. History is the part you cannot recompute. DATABASE_PATH=~/.local/share/planner/plan.db # Required. Working hours expand from this zone's rules per date. WORK_TIMEZONE=Europe/London # Required. How far ahead the scheduler places work. PLAN_HORIZON_DAYS=21 # Required. Placements inside this window survive a replan. PIN_HORIZON_HOURS=48 # Required. A small change moving more than this is a scheduler bug, and the test says so. CHURN_LIMIT_MINUTES=120 # Optional · secret. Optional. Read-only busy intervals. The app works without it. CALENDAR_ICS_URL=https://calendar.example.com/private/basic.ics # Required. Marks exported events so the importer can exclude them. EXPORT_ORIGIN_TAG=planner-generated
You are building a production product version of TimeHero. 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 ===== # TimeHero · product brief ## Problem The visible automatic work planning loop is buildable, but a credible replacement needs more than the first screen. TimeHero earns its keep through polish, OS integration, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A planner whose capacity is real, whose placements each have a reason, that tells you which deadline will be missed and by how many minutes, and that does not rearrange your week every morning. ## 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 - Task creation from email and chat. - Team planning and workload balancing. - Time tracking. - Completion dates presented as promises. - deep operating-system edge-case coverage - polished shortcuts and accessibility - vendor-maintained updates - cross-device sync ## Success criteria - No day is confirmed to exceed its capacity - Identical inputs are confirmed to give an identical plan - Estimates are confirmed never written by the scheduler - Started work is confirmed to survive a replan - An export and import round trip is confirmed to leave capacity unchanged ===== BRIEF.md ===== # Build brief · TimeHero The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a planner that fits your tasks into the time you actually have and says out loud what will not fit to replace TimeHero. 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) - Shell: Tauri 2 with React and TypeScript, keyboard first · a planner opened twenty times a day has to answer instantly - Scheduler: One named deterministic algorithm with a stated tie-break · a plan you cannot explain is a plan nobody follows - Storage: SQLite where each plan is a version rather than an overwrite · this morning's reshuffle needs to be comparable with yesterday's - Time: Instants in UTC, working hours as local rules plus a named zone · a working day is local, a deadline is an instant - Calendar: Optional read-only import of busy intervals from an iCalendar file or URL · the planner must be useful with nothing connected ### Data model (create this before Phase 1) - tasks(id, title, estimate_minutes, remaining_minutes, deadline_at, earliest_start_at, priority, project_id, state, created_at) - dependencies(task_id, depends_on_task_id) · a cycle is rejected at write time, not discovered at plan time - hours(weekday, start_local, end_local, timezone) · when you can actually work, as local rules - busy_blocks(id, source, starts_at, ends_at, external_uid) · imported, times only, and tagged with where they came from - plans(id, computed_at, algorithm, params_json, horizon_end, feasible, churn_minutes) - placements(plan_id, task_id, starts_at, ends_at, ordinal, reason) · the reason names the rule that put it there - misses(plan_id, task_id, deadline_at, would_finish_at, shortfall_minutes) · the output that is never hidden - A plan is data with a reason attached to every placement, so 'why is this on Thursday' has an answer that is not a shrug. ### Phase 1 · Capacity that actually exists Build: Store working hours as local rules with a named zone and expand them per date from the zone's rules. Subtract imported busy intervals. Produce a list of free intervals over the horizon and show the total per day. Reject a dependency cycle when it is written rather than at plan time. Trap: Treating capacity as eight hours a day produces a plan built on time you do not have, and the first thing it does is quietly assume your Tuesday meetings are not happening. Subtract real busy intervals, expand hours from the zone's rules for each date so the two clock change days are right, and show the daily total so a fantasy week is visible before anything is scheduled. Done when: Daily capacity equals working hours minus busy intervals for that date, asserted against hand-computed examples including partial overlaps. Both clock change days produce the documented number of available minutes, tested with both dates. A dependency cycle is refused at write time with the cycle named. Capacity per day is visible before any plan exists. Do not build yet: No scheduling, misses, replanning or export. ### Phase 2 · A scheduler you can name Build: Implement one scheduling rule, name it in the code and the README, and state the tie-break explicitly. Walk free intervals in order, decrementing remaining capacity as each task is placed and respecting dependencies and earliest start. Record a reason string per placement. Trap: A greedy pass that sorts by deadline and assigns each task a start time without decrementing the capacity it just consumed puts eleven hours of work into a seven hour Tuesday, and nothing errors because nobody asked whether the day was full. Consume capacity as you place, and assert that the sum of placements in any day never exceeds that day's available minutes. Done when: The sum of placed minutes on any day never exceeds that day's capacity, asserted over generated inputs. Running the scheduler twice on identical inputs produces an identical plan, asserted by comparing serialised plans. Every placement has a non-empty reason naming the rule, and the reason is visible in the interface. A task cannot start before its dependencies finish or before its earliest start, tested. Do not build yet: No infeasibility reporting, stability or export yet. ### Phase 3 · Saying what will not fit Build: Place tasks without ever altering an estimate. When a task cannot finish before its deadline, write a miss row with the deadline, the projected finish and the shortfall in minutes. Show misses at the top of the plan, and offer the smallest changes that would resolve each one: hours to add, work to drop, or a deadline to move. Trap: A planner that shaves estimates to make the week fit is worse than a list, because it hands you a schedule that looks achievable, is not, and teaches you within a fortnight to ignore it. Never write to an estimate from the scheduler; produce a miss with a shortfall in minutes and let the person decide what gives. Done when: The scheduler never writes to a task's estimate, asserted by a test that fails if the field changes during planning. A week containing more work than capacity produces miss rows with shortfalls in minutes summing to the overflow, tested with a constructed example. Misses appear above the plan rather than inside it. Each miss lists at least one concrete resolution with its size. Do not build yet: No stability measures or export yet. ### Phase 4 · A plan that does not reshuffle every morning Build: Pin work already started and anything inside a near horizon you set. Replan on explicit triggers rather than continuously. Compute a churn figure against the previous plan in minutes moved, store it with the plan, and show what changed and why. Trap: Recomputing the whole plan whenever anything changes rearranges half your week at nine in the morning, and since each rearrangement is individually defensible you cannot argue with it, so by Wednesday you have stopped looking. Pin what has started and the near horizon, replan on triggers, and put a churn number on every plan so a restless scheduler is visible rather than merely annoying. Done when: Work already started keeps its slot across a replan, tested. Adding one small task moves less than a stated number of minutes of existing work, asserted over generated cases. Every plan stores a churn figure and a diff against the previous plan is viewable. Replanning happens on named triggers only, and the trigger is recorded with the plan. Do not build yet: No export or backup yet. ### Phase 5 · Getting the plan out without poisoning it Build: Export placements as iCalendar and CSV, tagging every exported event with an identifier that marks it as yours. On import, exclude anything carrying that tag. Export and import the whole task set as JSON. Snapshot the database and exercise a restore in the suite. Trap: Exporting placements to your calendar and then importing that calendar as busy intervals makes the plan block itself: every task appears as a commitment, capacity drops to nothing, and the next plan says the week is impossible. Tag exported events and filter them on import, and test the round trip specifically. Done when: A full export to calendar followed by an import leaves capacity unchanged, tested as an explicit round trip. Exported events carry an identifier marking their origin and imports exclude them. The JSON export re-imports into an empty instance with tasks, dependencies and history intact. A restore from snapshot is exercised in the suite rather than described. Do not build yet: Nothing after this. ### Out of scope (and why) - Automatic task creation from email and chat, which is a connector business and a stream of low quality tasks that make the plan worse. - Team planning and workload balancing across people, which turns one capacity calculation into a negotiation with its own politics. - Time tracking. Planning and tracking are different products, and bolting one onto the other usually ruins both. - A predicted completion date presented as a promise. The scheduler projects from estimates you typed, and the interface should say so rather than implying knowledge it does not have. ### README must contain - The name of the scheduling algorithm, its tie-break, and where to read it in the code - That capacity is working hours minus real commitments, computed per date from the zone's rules - That the scheduler never alters an estimate, and that a deadline it cannot meet becomes a miss with a shortfall in minutes - That replanning pins started work and reports churn, because a plan that reshuffles daily stops being followed - That exported calendar events are tagged and excluded on import, or the plan blocks itself ===== ARCHITECTURE.md ===== # Architecture · TimeHero ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, keyboard first | a planner opened twenty times a day has to answer instantly | | Scheduler | One named deterministic algorithm with a stated tie-break | a plan you cannot explain is a plan nobody follows | | Storage | SQLite where each plan is a version rather than an overwrite | this morning's reshuffle needs to be comparable with yesterday's | | Time | Instants in UTC, working hours as local rules plus a named zone | a working day is local, a deadline is an instant | | Calendar | Optional read-only import of busy intervals from an iCalendar file or URL | the planner must be useful with nothing connected | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capacity | Zone-expanded hours minus busy intervals. | Any source of busy intervals, provided exported events are excluded. | | Scheduler | One named rule with a stated tie-break. | Any rule, provided it is named and the capacity invariant still holds. | | Misses | Infeasibility as a first-class output. | Not swappable for silence or for shrinking estimates. | | Stability | Pinning, triggers and the churn figure. | Any policy, provided churn is measured and shown. | | Portability | Calendar and JSON export, snapshots. | Any format, provided the round trip leaves capacity unchanged. | ## 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, plans and plan history. History is the part you cannot recompute. - `WORK_TIMEZONE` · required · Working hours expand from this zone's rules per date. - `PLAN_HORIZON_DAYS` · required · How far ahead the scheduler places work. - `PIN_HORIZON_HOURS` · required · Placements inside this window survive a replan. - `CHURN_LIMIT_MINUTES` · required · A small change moving more than this is a scheduler bug, and the test says so. - `CALENDAR_ICS_URL` · optional, secret · Optional. Read-only busy intervals. The app works without it. - `EXPORT_ORIGIN_TAG` · required · Marks exported events so the importer can exclude them. ## 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 · TimeHero product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, keyboard first, One named deterministic algorithm with a stated tie-break, SQLite where each plan is a version rather than an overwrite, Instants in UTC, working hours as local rules plus a named zone, Optional read-only import of busy intervals from an iCalendar file or URL. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Treating capacity as eight hours a day produces a plan built on time you do not have, and the first thing it does is quietly assume your Tuesday meetings are not happening. Subtract real busy intervals, expand hours from the zone's rules for each date so the two clock change days are right, and show the daily total so a fantasy week is visible before anything is scheduled. - A greedy pass that sorts by deadline and assigns each task a start time without decrementing the capacity it just consumed puts eleven hours of work into a seven hour Tuesday, and nothing errors because nobody asked whether the day was full. Consume capacity as you place, and assert that the sum of placements in any day never exceeds that day's available minutes. - A planner that shaves estimates to make the week fit is worse than a list, because it hands you a schedule that looks achievable, is not, and teaches you within a fortnight to ignore it. Never write to an estimate from the scheduler; produce a miss with a shortfall in minutes and let the person decide what gives. - Recomputing the whole plan whenever anything changes rearranges half your week at nine in the morning, and since each rearrangement is individually defensible you cannot argue with it, so by Wednesday you have stopped looking. Pin what has started and the near horizon, replan on triggers, and put a churn number on every plan so a restless scheduler is visible rather than merely annoying. - Exporting placements to your calendar and then importing that calendar as busy intervals makes the plan block itself: every task appears as a commitment, capacity drops to nothing, and the next plan says the week is impossible. Tag exported events and filter them on import, and test the round trip specifically. ===== MILESTONES.md ===== # Delivery milestones · TimeHero Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Capacity that actually exists Available minutes come from working hours minus real commitments, computed per date. ### Steps 1. Expand working hours per date from zone rules Not from a fixed offset. Write both transition days as tests first. Files: `src/time/hours.ts`, `tests/dst.test.ts` 2. Subtract imported busy intervals Tag each one with its source so you can tell yours from theirs. Files: `src/time/capacity.ts` 3. Reject dependency cycles on write A cycle discovered inside the scheduler is a hang with a nice name. Files: `src/tasks/dependencies.ts`, `tests/cycle.test.ts` ### Done when - [ ] Daily capacity matches hand-computed examples - [ ] Both clock change days give the documented minutes - [ ] A dependency cycle is refused at write time - [ ] Daily capacity is visible before planning ### Watch out - Treating capacity as eight hours a day produces a plan built on time you do not have, and the first thing it does is quietly assume your Tuesday meetings are not happening. Subtract real busy intervals, expand hours from the zone's rules for each date so the two clock change days are right, and show the daily total so a fantasy week is visible before anything is scheduled. ## M2 · A scheduler you can name The same inputs produce the same plan, and every placement carries the rule that caused it. ### Steps 1. Name the algorithm and its tie-break In the code and in the README. An unnamed heuristic drifts. Files: `src/plan/schedule.ts`, `docs/algorithm.md` 2. Consume capacity as you place The bug this phase exists to prevent is a day that quietly overfills. Files: `src/plan/schedule.ts`, `tests/capacity-invariant.test.ts` 3. Record a reason on every placement The reason is what makes the plan arguable instead of magical. Files: `src/plan/reasons.ts` ### Done when - [ ] No day exceeds its capacity - [ ] Identical inputs give an identical plan - [ ] Every placement carries a reason - [ ] Dependencies and earliest start are respected ### Watch out - A greedy pass that sorts by deadline and assigns each task a start time without decrementing the capacity it just consumed puts eleven hours of work into a seven hour Tuesday, and nothing errors because nobody asked whether the day was full. Consume capacity as you place, and assert that the sum of placements in any day never exceeds that day's available minutes. ## M3 · Saying what will not fit A deadline that cannot be met is reported with a number, never absorbed. ### Steps 1. Forbid the scheduler from touching estimates A test that fails on any write to the field, not a comment. Files: `src/plan/schedule.ts`, `tests/no-estimate-writes.test.ts` 2. Emit a miss row with a shortfall In minutes. A number is arguable; a warning icon is not. Files: `src/plan/misses.ts` 3. Offer the smallest resolution, sized Add these hours, drop this task, or move this date. Files: `src/plan/resolutions.ts` ### Done when - [ ] Estimates are never written by the scheduler - [ ] Shortfalls sum to the overflow - [ ] Misses appear above the plan - [ ] Each miss carries a sized resolution ### Watch out - A planner that shaves estimates to make the week fit is worse than a list, because it hands you a schedule that looks achievable, is not, and teaches you within a fortnight to ignore it. Never write to an estimate from the scheduler; produce a miss with a shortfall in minutes and let the person decide what gives. ## M4 · A plan that does not reshuffle every morning Replanning moves what it must and reports how much it moved. ### Steps 1. Pin started work and the near horizon Stability beats optimality once a human is following the plan. Files: `src/plan/pinning.ts`, `tests/stability.test.ts` 2. Compute and store churn per plan Minutes moved, against the previous plan. Files: `src/plan/churn.ts` 3. Show the diff and the trigger What changed, and what made it change. Files: `src/views/PlanDiff.tsx` ### Done when - [ ] Started work keeps its slot across a replan - [ ] A small addition moves less than the stated churn - [ ] Every plan stores churn and a viewable diff - [ ] Replans record their trigger ### Watch out - Recomputing the whole plan whenever anything changes rearranges half your week at nine in the morning, and since each rearrangement is individually defensible you cannot argue with it, so by Wednesday you have stopped looking. Pin what has started and the near horizon, replan on triggers, and put a churn number on every plan so a restless scheduler is visible rather than merely annoying. ## M5 · Getting the plan out without poisoning it (production only) The plan exports to a calendar and back without the export becoming input. ### Steps 1. Tag exported events with an origin identifier So the import can tell your plan from your meetings. Files: `src/export/ics.ts`, `tests/roundtrip-capacity.test.ts` 2. Filter tagged events on import Otherwise the plan eats its own capacity. Files: `src/import/ics.ts` 3. Snapshot and test the restore Plan history is the part you cannot recompute. Files: `src/backup/snapshot.ts`, `tests/restore.test.ts` ### Done when - [ ] An export then import leaves capacity unchanged - [ ] Exported events carry an origin identifier - [ ] The JSON export re-imports intact - [ ] Restore is exercised in the suite ### Watch out - Exporting placements to your calendar and then importing that calendar as busy intervals makes the plan block itself: every task appears as a commitment, capacity drops to nothing, and the next plan says the week is impossible. Tag exported events and filter them on import, and test the round trip specifically. ===== OPERATIONS.md ===== # Operations · TimeHero ## Backup The database, since it holds the tasks, the plan history and the churn record. Tasks could be retyped; the history of what was planned and what actually got missed could not, and it is the only evidence about your estimating. ## Restore Restore and recompute today's plan, comparing it with the last stored plan. If churn against the restored plan is large, something about capacity did not come back, most likely the calendar import. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the checks live in the suite: the capacity invariant on generated inputs, both clock change days, determinism across two runs, the churn limit on a small change, and the export and import round trip leaving capacity unchanged. ## Incident checklist A week that suddenly looks impossible usually means exported events came back in as busy intervals, and the origin tag will show it. A plan that moves constantly means pinning is not applied, and the churn figure per plan will show when it started. 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 day is confirmed to exceed its capacity - [ ] Identical inputs are confirmed to give an identical plan - [ ] Estimates are confirmed never written by the scheduler - [ ] Started work is confirmed to survive a replan - [ ] An export and import round trip is confirmed to leave capacity unchanged ## Launch constraint Do not market omitted TimeHero 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, plans and plan history. History is the part you cannot recompute. DATABASE_PATH=~/.local/share/planner/plan.db # Required. Working hours expand from this zone's rules per date. WORK_TIMEZONE=Europe/London # Required. How far ahead the scheduler places work. PLAN_HORIZON_DAYS=21 # Required. Placements inside this window survive a replan. PIN_HORIZON_HOURS=48 # Required. A small change moving more than this is a scheduler bug, and the test says so. CHURN_LIMIT_MINUTES=120 # Optional · secret. Optional. Read-only busy intervals. The app works without it. CALENDAR_ICS_URL=https://calendar.example.com/private/basic.ics # Required. Marks exported events so the importer can exclude them. EXPORT_ORIGIN_TAG=planner-generated
# TimeHero · indie build A local work planner whose hard problems are computing capacity that really exists, placing tasks by an algorithm you can explain, and refusing to hide the fact that a deadline is going to be missed. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, keyboard first | a planner opened twenty times a day has to answer instantly | | Scheduler | One named deterministic algorithm with a stated tie-break | a plan you cannot explain is a plan nobody follows | | Storage | SQLite where each plan is a version rather than an overwrite | this morning's reshuffle needs to be comparable with yesterday's | | Time | Instants in UTC, working hours as local rules plus a named zone | a working day is local, a deadline is an instant | | Calendar | Optional read-only import of busy intervals from an iCalendar file or URL | the planner must be useful with nothing connected | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 and the Rust toolchain** · free - Why: Tauri 2 needs both, and the desktop shell is what makes the keyboard-first behaviour possible. - Get it: nodejs.org and rustup.rs, then the Tauri prerequisites for your platform. - Verify: node --version prints v22 or higher and cargo --version succeeds - [ ] **An iCalendar file or subscription URL for your real commitments** (optional) · free - Why: Capacity computed without your actual meetings is fiction, and the whole build rests on capacity being true. - Get it: Most calendars publish a private subscription address; download the file or point the importer at it. - Verify: Imported busy intervals match a week you can check by eye - [ ] **Your scheduling rule and its tie-break** · free - Why: The plan is only arguable if the rule is written down, and picking the rule after seeing the output means picking whatever the code already did. - Get it: Choose one rule, such as earliest deadline first with a stated tie-break on priority then creation order, and write it into the documentation before implementing it. - Verify: The README names the rule and the tie-break, and the code refers to the same names - [ ] **Your near horizon, churn limit and working hours** · free - Why: Each is asserted by a test, and stability is a decision rather than an emergent property. - Get it: Write down how far ahead placements are pinned, how many minutes of movement is acceptable for a small change, and your working hours with their zone. - Verify: All three are in the configuration and named in tests - [ ] **A week of real tasks with honest estimates** · free - Why: Infeasibility is only testable against a week that genuinely does not fit, and a made-up one will not. - Get it: Take an actual busy week, enter the tasks and estimate them without flattering yourself. - Verify: The planner produces at least one miss with a shortfall you recognise ## 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: - Task creation from email and chat. - Team planning and workload balancing. - Time tracking. - Completion dates presented as promises. - deep operating-system edge-case coverage - polished shortcuts and accessibility - vendor-maintained updates - cross-device sync If one of those is essential to you, that is the reason to keep paying for TimeHero, and the README should say so rather than pretend.
# Build brief · TimeHero The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a planner that fits your tasks into the time you actually have and says out loud what will not fit to replace TimeHero. 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) - Shell: Tauri 2 with React and TypeScript, keyboard first · a planner opened twenty times a day has to answer instantly - Scheduler: One named deterministic algorithm with a stated tie-break · a plan you cannot explain is a plan nobody follows - Storage: SQLite where each plan is a version rather than an overwrite · this morning's reshuffle needs to be comparable with yesterday's - Time: Instants in UTC, working hours as local rules plus a named zone · a working day is local, a deadline is an instant - Calendar: Optional read-only import of busy intervals from an iCalendar file or URL · the planner must be useful with nothing connected ### Data model (create this before Phase 1) - tasks(id, title, estimate_minutes, remaining_minutes, deadline_at, earliest_start_at, priority, project_id, state, created_at) - dependencies(task_id, depends_on_task_id) · a cycle is rejected at write time, not discovered at plan time - hours(weekday, start_local, end_local, timezone) · when you can actually work, as local rules - busy_blocks(id, source, starts_at, ends_at, external_uid) · imported, times only, and tagged with where they came from - plans(id, computed_at, algorithm, params_json, horizon_end, feasible, churn_minutes) - placements(plan_id, task_id, starts_at, ends_at, ordinal, reason) · the reason names the rule that put it there - misses(plan_id, task_id, deadline_at, would_finish_at, shortfall_minutes) · the output that is never hidden - A plan is data with a reason attached to every placement, so 'why is this on Thursday' has an answer that is not a shrug. ### Phase 1 · Capacity that actually exists Build: Store working hours as local rules with a named zone and expand them per date from the zone's rules. Subtract imported busy intervals. Produce a list of free intervals over the horizon and show the total per day. Reject a dependency cycle when it is written rather than at plan time. Trap: Treating capacity as eight hours a day produces a plan built on time you do not have, and the first thing it does is quietly assume your Tuesday meetings are not happening. Subtract real busy intervals, expand hours from the zone's rules for each date so the two clock change days are right, and show the daily total so a fantasy week is visible before anything is scheduled. Done when: Daily capacity equals working hours minus busy intervals for that date, asserted against hand-computed examples including partial overlaps. Both clock change days produce the documented number of available minutes, tested with both dates. A dependency cycle is refused at write time with the cycle named. Capacity per day is visible before any plan exists. Do not build yet: No scheduling, misses, replanning or export. ### Phase 2 · A scheduler you can name Build: Implement one scheduling rule, name it in the code and the README, and state the tie-break explicitly. Walk free intervals in order, decrementing remaining capacity as each task is placed and respecting dependencies and earliest start. Record a reason string per placement. Trap: A greedy pass that sorts by deadline and assigns each task a start time without decrementing the capacity it just consumed puts eleven hours of work into a seven hour Tuesday, and nothing errors because nobody asked whether the day was full. Consume capacity as you place, and assert that the sum of placements in any day never exceeds that day's available minutes. Done when: The sum of placed minutes on any day never exceeds that day's capacity, asserted over generated inputs. Running the scheduler twice on identical inputs produces an identical plan, asserted by comparing serialised plans. Every placement has a non-empty reason naming the rule, and the reason is visible in the interface. A task cannot start before its dependencies finish or before its earliest start, tested. Do not build yet: No infeasibility reporting, stability or export yet. ### Phase 3 · Saying what will not fit Build: Place tasks without ever altering an estimate. When a task cannot finish before its deadline, write a miss row with the deadline, the projected finish and the shortfall in minutes. Show misses at the top of the plan, and offer the smallest changes that would resolve each one: hours to add, work to drop, or a deadline to move. Trap: A planner that shaves estimates to make the week fit is worse than a list, because it hands you a schedule that looks achievable, is not, and teaches you within a fortnight to ignore it. Never write to an estimate from the scheduler; produce a miss with a shortfall in minutes and let the person decide what gives. Done when: The scheduler never writes to a task's estimate, asserted by a test that fails if the field changes during planning. A week containing more work than capacity produces miss rows with shortfalls in minutes summing to the overflow, tested with a constructed example. Misses appear above the plan rather than inside it. Each miss lists at least one concrete resolution with its size. Do not build yet: No stability measures or export yet. ### Phase 4 · A plan that does not reshuffle every morning Build: Pin work already started and anything inside a near horizon you set. Replan on explicit triggers rather than continuously. Compute a churn figure against the previous plan in minutes moved, store it with the plan, and show what changed and why. Trap: Recomputing the whole plan whenever anything changes rearranges half your week at nine in the morning, and since each rearrangement is individually defensible you cannot argue with it, so by Wednesday you have stopped looking. Pin what has started and the near horizon, replan on triggers, and put a churn number on every plan so a restless scheduler is visible rather than merely annoying. Done when: Work already started keeps its slot across a replan, tested. Adding one small task moves less than a stated number of minutes of existing work, asserted over generated cases. Every plan stores a churn figure and a diff against the previous plan is viewable. Replanning happens on named triggers only, and the trigger is recorded with the plan. Do not build yet: No export or backup yet. ### Phase 5 · Getting the plan out without poisoning it Build: Export placements as iCalendar and CSV, tagging every exported event with an identifier that marks it as yours. On import, exclude anything carrying that tag. Export and import the whole task set as JSON. Snapshot the database and exercise a restore in the suite. Trap: Exporting placements to your calendar and then importing that calendar as busy intervals makes the plan block itself: every task appears as a commitment, capacity drops to nothing, and the next plan says the week is impossible. Tag exported events and filter them on import, and test the round trip specifically. Done when: A full export to calendar followed by an import leaves capacity unchanged, tested as an explicit round trip. Exported events carry an identifier marking their origin and imports exclude them. The JSON export re-imports into an empty instance with tasks, dependencies and history intact. A restore from snapshot is exercised in the suite rather than described. Do not build yet: Nothing after this. ### Out of scope (and why) - Automatic task creation from email and chat, which is a connector business and a stream of low quality tasks that make the plan worse. - Team planning and workload balancing across people, which turns one capacity calculation into a negotiation with its own politics. - Time tracking. Planning and tracking are different products, and bolting one onto the other usually ruins both. - A predicted completion date presented as a promise. The scheduler projects from estimates you typed, and the interface should say so rather than implying knowledge it does not have. ### README must contain - The name of the scheduling algorithm, its tie-break, and where to read it in the code - That capacity is working hours minus real commitments, computed per date from the zone's rules - That the scheduler never alters an estimate, and that a deadline it cannot meet becomes a miss with a shortfall in minutes - That replanning pins started work and reports churn, because a plan that reshuffles daily stops being followed - That exported calendar events are tagged and excluded on import, or the plan blocks itself
# Agent instructions · TimeHero indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, keyboard first, One named deterministic algorithm with a stated tie-break, SQLite where each plan is a version rather than an overwrite, Instants in UTC, working hours as local rules plus a named zone, Optional read-only import of busy intervals from an iCalendar file or URL. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Treating capacity as eight hours a day produces a plan built on time you do not have, and the first thing it does is quietly assume your Tuesday meetings are not happening. Subtract real busy intervals, expand hours from the zone's rules for each date so the two clock change days are right, and show the daily total so a fantasy week is visible before anything is scheduled. - A greedy pass that sorts by deadline and assigns each task a start time without decrementing the capacity it just consumed puts eleven hours of work into a seven hour Tuesday, and nothing errors because nobody asked whether the day was full. Consume capacity as you place, and assert that the sum of placements in any day never exceeds that day's available minutes. - A planner that shaves estimates to make the week fit is worse than a list, because it hands you a schedule that looks achievable, is not, and teaches you within a fortnight to ignore it. Never write to an estimate from the scheduler; produce a miss with a shortfall in minutes and let the person decide what gives. - Recomputing the whole plan whenever anything changes rearranges half your week at nine in the morning, and since each rearrangement is individually defensible you cannot argue with it, so by Wednesday you have stopped looking. Pin what has started and the near horizon, replan on triggers, and put a churn number on every plan so a restless scheduler is visible rather than merely annoying.
# Build plan · TimeHero A local work planner whose hard problems are computing capacity that really exists, placing tasks by an algorithm you can explain, and refusing to hide the fact that a deadline is going to be missed. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capacity that actually exists Available minutes come from working hours minus real commitments, computed per date. ### Steps 1. Expand working hours per date from zone rules Not from a fixed offset. Write both transition days as tests first. Files: `src/time/hours.ts`, `tests/dst.test.ts` 2. Subtract imported busy intervals Tag each one with its source so you can tell yours from theirs. Files: `src/time/capacity.ts` 3. Reject dependency cycles on write A cycle discovered inside the scheduler is a hang with a nice name. Files: `src/tasks/dependencies.ts`, `tests/cycle.test.ts` ### Done when - [ ] Daily capacity matches hand-computed examples - [ ] Both clock change days give the documented minutes - [ ] A dependency cycle is refused at write time - [ ] Daily capacity is visible before planning ### Watch out - Treating capacity as eight hours a day produces a plan built on time you do not have, and the first thing it does is quietly assume your Tuesday meetings are not happening. Subtract real busy intervals, expand hours from the zone's rules for each date so the two clock change days are right, and show the daily total so a fantasy week is visible before anything is scheduled. ## Phase 2 · A scheduler you can name The same inputs produce the same plan, and every placement carries the rule that caused it. ### Steps 1. Name the algorithm and its tie-break In the code and in the README. An unnamed heuristic drifts. Files: `src/plan/schedule.ts`, `docs/algorithm.md` 2. Consume capacity as you place The bug this phase exists to prevent is a day that quietly overfills. Files: `src/plan/schedule.ts`, `tests/capacity-invariant.test.ts` 3. Record a reason on every placement The reason is what makes the plan arguable instead of magical. Files: `src/plan/reasons.ts` ### Done when - [ ] No day exceeds its capacity - [ ] Identical inputs give an identical plan - [ ] Every placement carries a reason - [ ] Dependencies and earliest start are respected ### Watch out - A greedy pass that sorts by deadline and assigns each task a start time without decrementing the capacity it just consumed puts eleven hours of work into a seven hour Tuesday, and nothing errors because nobody asked whether the day was full. Consume capacity as you place, and assert that the sum of placements in any day never exceeds that day's available minutes. ## Phase 3 · Saying what will not fit A deadline that cannot be met is reported with a number, never absorbed. ### Steps 1. Forbid the scheduler from touching estimates A test that fails on any write to the field, not a comment. Files: `src/plan/schedule.ts`, `tests/no-estimate-writes.test.ts` 2. Emit a miss row with a shortfall In minutes. A number is arguable; a warning icon is not. Files: `src/plan/misses.ts` 3. Offer the smallest resolution, sized Add these hours, drop this task, or move this date. Files: `src/plan/resolutions.ts` ### Done when - [ ] Estimates are never written by the scheduler - [ ] Shortfalls sum to the overflow - [ ] Misses appear above the plan - [ ] Each miss carries a sized resolution ### Watch out - A planner that shaves estimates to make the week fit is worse than a list, because it hands you a schedule that looks achievable, is not, and teaches you within a fortnight to ignore it. Never write to an estimate from the scheduler; produce a miss with a shortfall in minutes and let the person decide what gives. ## Phase 4 · A plan that does not reshuffle every morning Replanning moves what it must and reports how much it moved. ### Steps 1. Pin started work and the near horizon Stability beats optimality once a human is following the plan. Files: `src/plan/pinning.ts`, `tests/stability.test.ts` 2. Compute and store churn per plan Minutes moved, against the previous plan. Files: `src/plan/churn.ts` 3. Show the diff and the trigger What changed, and what made it change. Files: `src/views/PlanDiff.tsx` ### Done when - [ ] Started work keeps its slot across a replan - [ ] A small addition moves less than the stated churn - [ ] Every plan stores churn and a viewable diff - [ ] Replans record their trigger ### Watch out - Recomputing the whole plan whenever anything changes rearranges half your week at nine in the morning, and since each rearrangement is individually defensible you cannot argue with it, so by Wednesday you have stopped looking. Pin what has started and the near horizon, replan on triggers, and put a churn number on every plan so a restless scheduler is visible rather than merely annoying. ## Not in this build - Task creation from email and chat. - Team planning and workload balancing. - Time tracking. - Completion dates presented as promises. ## After v1, if you want it - Estimate calibration from the history you already store, shown as a ratio rather than applied silently - A second scheduling rule selectable per project, once the first has held its invariants for a month - Recurring tasks, which are a generator in front of the same scheduler
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Tasks, plans and plan history. History is the part you cannot recompute. DATABASE_PATH=~/.local/share/planner/plan.db # Required. Working hours expand from this zone's rules per date. WORK_TIMEZONE=Europe/London # Required. How far ahead the scheduler places work. PLAN_HORIZON_DAYS=21 # Required. Placements inside this window survive a replan. PIN_HORIZON_HOURS=48 # Required. A small change moving more than this is a scheduler bug, and the test says so. CHURN_LIMIT_MINUTES=120 # Optional · secret. Optional. Read-only busy intervals. The app works without it. CALENDAR_ICS_URL=https://calendar.example.com/private/basic.ics # Required. Marks exported events so the importer can exclude them. EXPORT_ORIGIN_TAG=planner-generated
# TimeHero · product brief ## Problem The visible automatic work planning loop is buildable, but a credible replacement needs more than the first screen. TimeHero earns its keep through polish, OS integration, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A planner whose capacity is real, whose placements each have a reason, that tells you which deadline will be missed and by how many minutes, and that does not rearrange your week every morning. ## 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 - Task creation from email and chat. - Team planning and workload balancing. - Time tracking. - Completion dates presented as promises. - deep operating-system edge-case coverage - polished shortcuts and accessibility - vendor-maintained updates - cross-device sync ## Success criteria - No day is confirmed to exceed its capacity - Identical inputs are confirmed to give an identical plan - Estimates are confirmed never written by the scheduler - Started work is confirmed to survive a replan - An export and import round trip is confirmed to leave capacity unchanged
# Build brief · TimeHero The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a planner that fits your tasks into the time you actually have and says out loud what will not fit to replace TimeHero. 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) - Shell: Tauri 2 with React and TypeScript, keyboard first · a planner opened twenty times a day has to answer instantly - Scheduler: One named deterministic algorithm with a stated tie-break · a plan you cannot explain is a plan nobody follows - Storage: SQLite where each plan is a version rather than an overwrite · this morning's reshuffle needs to be comparable with yesterday's - Time: Instants in UTC, working hours as local rules plus a named zone · a working day is local, a deadline is an instant - Calendar: Optional read-only import of busy intervals from an iCalendar file or URL · the planner must be useful with nothing connected ### Data model (create this before Phase 1) - tasks(id, title, estimate_minutes, remaining_minutes, deadline_at, earliest_start_at, priority, project_id, state, created_at) - dependencies(task_id, depends_on_task_id) · a cycle is rejected at write time, not discovered at plan time - hours(weekday, start_local, end_local, timezone) · when you can actually work, as local rules - busy_blocks(id, source, starts_at, ends_at, external_uid) · imported, times only, and tagged with where they came from - plans(id, computed_at, algorithm, params_json, horizon_end, feasible, churn_minutes) - placements(plan_id, task_id, starts_at, ends_at, ordinal, reason) · the reason names the rule that put it there - misses(plan_id, task_id, deadline_at, would_finish_at, shortfall_minutes) · the output that is never hidden - A plan is data with a reason attached to every placement, so 'why is this on Thursday' has an answer that is not a shrug. ### Phase 1 · Capacity that actually exists Build: Store working hours as local rules with a named zone and expand them per date from the zone's rules. Subtract imported busy intervals. Produce a list of free intervals over the horizon and show the total per day. Reject a dependency cycle when it is written rather than at plan time. Trap: Treating capacity as eight hours a day produces a plan built on time you do not have, and the first thing it does is quietly assume your Tuesday meetings are not happening. Subtract real busy intervals, expand hours from the zone's rules for each date so the two clock change days are right, and show the daily total so a fantasy week is visible before anything is scheduled. Done when: Daily capacity equals working hours minus busy intervals for that date, asserted against hand-computed examples including partial overlaps. Both clock change days produce the documented number of available minutes, tested with both dates. A dependency cycle is refused at write time with the cycle named. Capacity per day is visible before any plan exists. Do not build yet: No scheduling, misses, replanning or export. ### Phase 2 · A scheduler you can name Build: Implement one scheduling rule, name it in the code and the README, and state the tie-break explicitly. Walk free intervals in order, decrementing remaining capacity as each task is placed and respecting dependencies and earliest start. Record a reason string per placement. Trap: A greedy pass that sorts by deadline and assigns each task a start time without decrementing the capacity it just consumed puts eleven hours of work into a seven hour Tuesday, and nothing errors because nobody asked whether the day was full. Consume capacity as you place, and assert that the sum of placements in any day never exceeds that day's available minutes. Done when: The sum of placed minutes on any day never exceeds that day's capacity, asserted over generated inputs. Running the scheduler twice on identical inputs produces an identical plan, asserted by comparing serialised plans. Every placement has a non-empty reason naming the rule, and the reason is visible in the interface. A task cannot start before its dependencies finish or before its earliest start, tested. Do not build yet: No infeasibility reporting, stability or export yet. ### Phase 3 · Saying what will not fit Build: Place tasks without ever altering an estimate. When a task cannot finish before its deadline, write a miss row with the deadline, the projected finish and the shortfall in minutes. Show misses at the top of the plan, and offer the smallest changes that would resolve each one: hours to add, work to drop, or a deadline to move. Trap: A planner that shaves estimates to make the week fit is worse than a list, because it hands you a schedule that looks achievable, is not, and teaches you within a fortnight to ignore it. Never write to an estimate from the scheduler; produce a miss with a shortfall in minutes and let the person decide what gives. Done when: The scheduler never writes to a task's estimate, asserted by a test that fails if the field changes during planning. A week containing more work than capacity produces miss rows with shortfalls in minutes summing to the overflow, tested with a constructed example. Misses appear above the plan rather than inside it. Each miss lists at least one concrete resolution with its size. Do not build yet: No stability measures or export yet. ### Phase 4 · A plan that does not reshuffle every morning Build: Pin work already started and anything inside a near horizon you set. Replan on explicit triggers rather than continuously. Compute a churn figure against the previous plan in minutes moved, store it with the plan, and show what changed and why. Trap: Recomputing the whole plan whenever anything changes rearranges half your week at nine in the morning, and since each rearrangement is individually defensible you cannot argue with it, so by Wednesday you have stopped looking. Pin what has started and the near horizon, replan on triggers, and put a churn number on every plan so a restless scheduler is visible rather than merely annoying. Done when: Work already started keeps its slot across a replan, tested. Adding one small task moves less than a stated number of minutes of existing work, asserted over generated cases. Every plan stores a churn figure and a diff against the previous plan is viewable. Replanning happens on named triggers only, and the trigger is recorded with the plan. Do not build yet: No export or backup yet. ### Phase 5 · Getting the plan out without poisoning it Build: Export placements as iCalendar and CSV, tagging every exported event with an identifier that marks it as yours. On import, exclude anything carrying that tag. Export and import the whole task set as JSON. Snapshot the database and exercise a restore in the suite. Trap: Exporting placements to your calendar and then importing that calendar as busy intervals makes the plan block itself: every task appears as a commitment, capacity drops to nothing, and the next plan says the week is impossible. Tag exported events and filter them on import, and test the round trip specifically. Done when: A full export to calendar followed by an import leaves capacity unchanged, tested as an explicit round trip. Exported events carry an identifier marking their origin and imports exclude them. The JSON export re-imports into an empty instance with tasks, dependencies and history intact. A restore from snapshot is exercised in the suite rather than described. Do not build yet: Nothing after this. ### Out of scope (and why) - Automatic task creation from email and chat, which is a connector business and a stream of low quality tasks that make the plan worse. - Team planning and workload balancing across people, which turns one capacity calculation into a negotiation with its own politics. - Time tracking. Planning and tracking are different products, and bolting one onto the other usually ruins both. - A predicted completion date presented as a promise. The scheduler projects from estimates you typed, and the interface should say so rather than implying knowledge it does not have. ### README must contain - The name of the scheduling algorithm, its tie-break, and where to read it in the code - That capacity is working hours minus real commitments, computed per date from the zone's rules - That the scheduler never alters an estimate, and that a deadline it cannot meet becomes a miss with a shortfall in minutes - That replanning pins started work and reports churn, because a plan that reshuffles daily stops being followed - That exported calendar events are tagged and excluded on import, or the plan blocks itself
# Architecture · TimeHero ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, keyboard first | a planner opened twenty times a day has to answer instantly | | Scheduler | One named deterministic algorithm with a stated tie-break | a plan you cannot explain is a plan nobody follows | | Storage | SQLite where each plan is a version rather than an overwrite | this morning's reshuffle needs to be comparable with yesterday's | | Time | Instants in UTC, working hours as local rules plus a named zone | a working day is local, a deadline is an instant | | Calendar | Optional read-only import of busy intervals from an iCalendar file or URL | the planner must be useful with nothing connected | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capacity | Zone-expanded hours minus busy intervals. | Any source of busy intervals, provided exported events are excluded. | | Scheduler | One named rule with a stated tie-break. | Any rule, provided it is named and the capacity invariant still holds. | | Misses | Infeasibility as a first-class output. | Not swappable for silence or for shrinking estimates. | | Stability | Pinning, triggers and the churn figure. | Any policy, provided churn is measured and shown. | | Portability | Calendar and JSON export, snapshots. | Any format, provided the round trip leaves capacity unchanged. | ## 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, plans and plan history. History is the part you cannot recompute. - `WORK_TIMEZONE` · required · Working hours expand from this zone's rules per date. - `PLAN_HORIZON_DAYS` · required · How far ahead the scheduler places work. - `PIN_HORIZON_HOURS` · required · Placements inside this window survive a replan. - `CHURN_LIMIT_MINUTES` · required · A small change moving more than this is a scheduler bug, and the test says so. - `CALENDAR_ICS_URL` · optional, secret · Optional. Read-only busy intervals. The app works without it. - `EXPORT_ORIGIN_TAG` · required · Marks exported events so the importer can exclude them. ## 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 · TimeHero product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, keyboard first, One named deterministic algorithm with a stated tie-break, SQLite where each plan is a version rather than an overwrite, Instants in UTC, working hours as local rules plus a named zone, Optional read-only import of busy intervals from an iCalendar file or URL. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Treating capacity as eight hours a day produces a plan built on time you do not have, and the first thing it does is quietly assume your Tuesday meetings are not happening. Subtract real busy intervals, expand hours from the zone's rules for each date so the two clock change days are right, and show the daily total so a fantasy week is visible before anything is scheduled. - A greedy pass that sorts by deadline and assigns each task a start time without decrementing the capacity it just consumed puts eleven hours of work into a seven hour Tuesday, and nothing errors because nobody asked whether the day was full. Consume capacity as you place, and assert that the sum of placements in any day never exceeds that day's available minutes. - A planner that shaves estimates to make the week fit is worse than a list, because it hands you a schedule that looks achievable, is not, and teaches you within a fortnight to ignore it. Never write to an estimate from the scheduler; produce a miss with a shortfall in minutes and let the person decide what gives. - Recomputing the whole plan whenever anything changes rearranges half your week at nine in the morning, and since each rearrangement is individually defensible you cannot argue with it, so by Wednesday you have stopped looking. Pin what has started and the near horizon, replan on triggers, and put a churn number on every plan so a restless scheduler is visible rather than merely annoying. - Exporting placements to your calendar and then importing that calendar as busy intervals makes the plan block itself: every task appears as a commitment, capacity drops to nothing, and the next plan says the week is impossible. Tag exported events and filter them on import, and test the round trip specifically.
# Delivery milestones · TimeHero Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Capacity that actually exists Available minutes come from working hours minus real commitments, computed per date. ### Steps 1. Expand working hours per date from zone rules Not from a fixed offset. Write both transition days as tests first. Files: `src/time/hours.ts`, `tests/dst.test.ts` 2. Subtract imported busy intervals Tag each one with its source so you can tell yours from theirs. Files: `src/time/capacity.ts` 3. Reject dependency cycles on write A cycle discovered inside the scheduler is a hang with a nice name. Files: `src/tasks/dependencies.ts`, `tests/cycle.test.ts` ### Done when - [ ] Daily capacity matches hand-computed examples - [ ] Both clock change days give the documented minutes - [ ] A dependency cycle is refused at write time - [ ] Daily capacity is visible before planning ### Watch out - Treating capacity as eight hours a day produces a plan built on time you do not have, and the first thing it does is quietly assume your Tuesday meetings are not happening. Subtract real busy intervals, expand hours from the zone's rules for each date so the two clock change days are right, and show the daily total so a fantasy week is visible before anything is scheduled. ## M2 · A scheduler you can name The same inputs produce the same plan, and every placement carries the rule that caused it. ### Steps 1. Name the algorithm and its tie-break In the code and in the README. An unnamed heuristic drifts. Files: `src/plan/schedule.ts`, `docs/algorithm.md` 2. Consume capacity as you place The bug this phase exists to prevent is a day that quietly overfills. Files: `src/plan/schedule.ts`, `tests/capacity-invariant.test.ts` 3. Record a reason on every placement The reason is what makes the plan arguable instead of magical. Files: `src/plan/reasons.ts` ### Done when - [ ] No day exceeds its capacity - [ ] Identical inputs give an identical plan - [ ] Every placement carries a reason - [ ] Dependencies and earliest start are respected ### Watch out - A greedy pass that sorts by deadline and assigns each task a start time without decrementing the capacity it just consumed puts eleven hours of work into a seven hour Tuesday, and nothing errors because nobody asked whether the day was full. Consume capacity as you place, and assert that the sum of placements in any day never exceeds that day's available minutes. ## M3 · Saying what will not fit A deadline that cannot be met is reported with a number, never absorbed. ### Steps 1. Forbid the scheduler from touching estimates A test that fails on any write to the field, not a comment. Files: `src/plan/schedule.ts`, `tests/no-estimate-writes.test.ts` 2. Emit a miss row with a shortfall In minutes. A number is arguable; a warning icon is not. Files: `src/plan/misses.ts` 3. Offer the smallest resolution, sized Add these hours, drop this task, or move this date. Files: `src/plan/resolutions.ts` ### Done when - [ ] Estimates are never written by the scheduler - [ ] Shortfalls sum to the overflow - [ ] Misses appear above the plan - [ ] Each miss carries a sized resolution ### Watch out - A planner that shaves estimates to make the week fit is worse than a list, because it hands you a schedule that looks achievable, is not, and teaches you within a fortnight to ignore it. Never write to an estimate from the scheduler; produce a miss with a shortfall in minutes and let the person decide what gives. ## M4 · A plan that does not reshuffle every morning Replanning moves what it must and reports how much it moved. ### Steps 1. Pin started work and the near horizon Stability beats optimality once a human is following the plan. Files: `src/plan/pinning.ts`, `tests/stability.test.ts` 2. Compute and store churn per plan Minutes moved, against the previous plan. Files: `src/plan/churn.ts` 3. Show the diff and the trigger What changed, and what made it change. Files: `src/views/PlanDiff.tsx` ### Done when - [ ] Started work keeps its slot across a replan - [ ] A small addition moves less than the stated churn - [ ] Every plan stores churn and a viewable diff - [ ] Replans record their trigger ### Watch out - Recomputing the whole plan whenever anything changes rearranges half your week at nine in the morning, and since each rearrangement is individually defensible you cannot argue with it, so by Wednesday you have stopped looking. Pin what has started and the near horizon, replan on triggers, and put a churn number on every plan so a restless scheduler is visible rather than merely annoying. ## M5 · Getting the plan out without poisoning it (production only) The plan exports to a calendar and back without the export becoming input. ### Steps 1. Tag exported events with an origin identifier So the import can tell your plan from your meetings. Files: `src/export/ics.ts`, `tests/roundtrip-capacity.test.ts` 2. Filter tagged events on import Otherwise the plan eats its own capacity. Files: `src/import/ics.ts` 3. Snapshot and test the restore Plan history is the part you cannot recompute. Files: `src/backup/snapshot.ts`, `tests/restore.test.ts` ### Done when - [ ] An export then import leaves capacity unchanged - [ ] Exported events carry an origin identifier - [ ] The JSON export re-imports intact - [ ] Restore is exercised in the suite ### Watch out - Exporting placements to your calendar and then importing that calendar as busy intervals makes the plan block itself: every task appears as a commitment, capacity drops to nothing, and the next plan says the week is impossible. Tag exported events and filter them on import, and test the round trip specifically.
# Operations · TimeHero ## Backup The database, since it holds the tasks, the plan history and the churn record. Tasks could be retyped; the history of what was planned and what actually got missed could not, and it is the only evidence about your estimating. ## Restore Restore and recompute today's plan, comparing it with the last stored plan. If churn against the restored plan is large, something about capacity did not come back, most likely the calendar import. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the checks live in the suite: the capacity invariant on generated inputs, both clock change days, determinism across two runs, the churn limit on a small change, and the export and import round trip leaving capacity unchanged. ## Incident checklist A week that suddenly looks impossible usually means exported events came back in as busy intervals, and the origin tag will show it. A plan that moves constantly means pinning is not applied, and the churn figure per plan will show when it started. 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 day is confirmed to exceed its capacity - [ ] Identical inputs are confirmed to give an identical plan - [ ] Estimates are confirmed never written by the scheduler - [ ] Started work is confirmed to survive a replan - [ ] An export and import round trip is confirmed to leave capacity unchanged ## Launch constraint Do not market omitted TimeHero 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, plans and plan history. History is the part you cannot recompute. DATABASE_PATH=~/.local/share/planner/plan.db # Required. Working hours expand from this zone's rules per date. WORK_TIMEZONE=Europe/London # Required. How far ahead the scheduler places work. PLAN_HORIZON_DAYS=21 # Required. Placements inside this window survive a replan. PIN_HORIZON_HOURS=48 # Required. A small change moving more than this is a scheduler bug, and the test says so. CHURN_LIMIT_MINUTES=120 # Optional · secret. Optional. Read-only busy intervals. The app works without it. CALENDAR_ICS_URL=https://calendar.example.com/private/basic.ics # Required. Marks exported events so the importer can exclude them. EXPORT_ORIGIN_TAG=planner-generated
$ choose a build depth, inspect the files, then open the complete pack in your agent
TimeHero: People keep paying because a utility used dozens of times a day must be instant, predictable, and compatible with every OS update.
xdeep operating-system edge-case coverage
xpolished shortcuts and accessibility
xvendor-maintained updates
xcross-device sync
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
TimeHero pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| basic | $5/user | $4.60/user | Personal task scheduling; public page does not state a numeric project or task cap. |
| professional | $12/user | $10/user | Unlimited projects, 2 guest users, and 2 smart templates. |
| premium | $27/user | $22/user | Unlimited smart templates and guest users, plus reports, timesheets, and Gantt views. |
free tierno free tier
billingmonthly + annual (annual saves up to 18%); 7-day Premium trial with no card
hidden costsProfessional limits guest users and smart templates to 2 each; larger deployments above 50 members require invoicing/contact.
verified 2026-08-14 · source ↗
Vibecode TimeHero
Kinda. The core of TimeHero is buildable in a weekend with the prompt on this page, but there are real gaps: deep operating-system edge-case coverage, polished shortcuts and accessibility. Read the honest list above before committing.
How much does TimeHero cost?
TimeHero costs about $5/month (Basic, checked 2026-07-31), which is $60 per year.
What do I lose by replacing TimeHero?
Honestly: deep operating-system edge-case coverage; polished shortcuts and accessibility; vendor-maintained updates; cross-device sync. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to TimeHero?
Yes: FlowSavvy Free (Automatically fits tasks around availability and deadlines; free planning looks only two weeks ahead.) Reclaim Lite (A rolling one-week automatic plan for tasks, habits, and focus blocks, free forever.) The prompt is for when you want it exactly your way.