Claude Code prompt

Paste this into Claude Code in the ReKnock repo.

You are implementing **recurring tasks** in ReKnock, Rob's nagging-reminder app. You have the codebase; I (the designer) did not. Work autonomously. Do not stop to ask questions: when something is ambiguous, choose the most sensible option, keep going, and list your choices at the end.

## Step 0: Explore first (before writing code)
Read the codebase and find: the task model/schema and migration system; `mark_done`, `restore_task`, `list_done` and how done history is stored; the nag scheduler (nag_times, quadrant cadences, context-only 09:00 rule, start_date quiet logic, urgent-within-2-days); the 21-day Someday archive job; blocked_by handling; all MCP tool definitions; the daily summary email and weekly review builders; the test setup and any clock/timezone helpers; any web/mobile UI for editing tasks. Summarize in a few lines what you found, then implement. Match existing style, naming and patterns. Timezone is America/Toronto.

## Core idea
A recurring task is ONE ordinary task with a `repeat` rule. When it is marked done it goes quiet (future `start_date`) and wakes itself on the next occurrence, keeping id, text, contexts, nag_times, quadrant and blocked_by. A snapshot of each completion goes to done history. Reuse the existing "quiet until start_date" behavior; do not invent a new sleeping state.

## Data model (additive, idempotent migration, no backfill)
- `repeat`: nullable JSON on tasks. null = not recurring (every existing task; behavior must be unchanged).
- `next_occurrence`: nullable date (Toronto calendar date), queryable.
- `series_id` (nullable) on done-history rows, plus enough saved previous-state to undo.
- `repeat` JSON fields: `rrule` (RFC 5545 subset string), `mode` ("fixed"|"after_completion"), `lead_days` (int, default 0), `due_on_occurrence` (bool), `anchor` (first occurrence date), `count_left` (nullable int), `status` ("active"|"ended"|"stopped").

## Rule semantics
Supported RRULE subset: FREQ=DAILY|WEEKLY|MONTHLY|YEARLY; INTERVAL; BYDAY (weekly list like MO,TH; monthly with ordinal like -1FR, 2TU); BYMONTHDAY (1..31 or -1); BYMONTH (yearly); UNTIL (inclusive date); COUNT. Reject anything else with a friendly error.
Deliberate deviations from RFC 5545:
- Short months CLAMP instead of skipping: day 31 gives Feb 28/29, Apr 30, etc. Always compute from the anchor day (never from the previous result) so there is no drift: Jan 31, Feb 28, Mar 31.
- Yearly Feb 29 gives Feb 28 in non-leap years.
- "5th <weekday>" falls back to the last such weekday of the month.
After-completion mode: only FREQ+INTERVAL (no BYDAY/BYMONTHDAY, reject the combination clearly). Next = completion date in Toronto + N units, calendar math with month-end clamping (Jan 31 + 1 month = Feb 28/29).
Interval alignment (e.g. every 2 weeks, every 2 months) is measured from `anchor`.
Implement the date logic as a pure module with an injectable clock/"today", using `date` arithmetic only. NEVER add 24h to timestamps. "Today" = now converted to America/Toronto.

## Creating / editing
- First occurrence = given due_date, else given start_date, else first rule match on or after today (after-completion: today).
- `due_on_occurrence` default: true when the interval is 7 days or more (monthly, yearly, every N days>=7), false for daily/weekly habits. If true, due_date = occurrence.
- `start_date` = occurrence minus lead_days, never earlier than today. Set `next_occurrence`.
- Monthly/yearly with no day given use the first occurrence's day/month.
- Changing the rule recomputes from today. Editing due_date on a recurring task is a one-off override of the current occurrence only.
- `repeat: "none"` converts it to a normal one-off task (keeps current dates), status "stopped".

## mark_done on a recurring task
1. If the task is asleep (start_date in the future): change nothing and reply in friendly words, e.g. "Already done for this round. Next one is Nov 28. Say skip to jump ahead." (makes double-taps harmless).
2. Otherwise save a done-history snapshot (same shape as existing history so `list_done` works unchanged, plus series_id, occurrence date, previous state, count of collapsed missed occurrences).
3. Next occurrence: fixed = first occurrence strictly AFTER max(current occurrence, today). After-completion = today + interval. NEVER create a backlog of missed occurrences; jump to the next future one.
4. Decrement count_left. If UNTIL is exceeded, count is exhausted, or `stop_series: true` was passed, end the series: complete the task as a normal task, status "ended", no revival.
5. Else: set next_occurrence, start_date = next - lead_days, due_date = next (if due_on_occurrence), reset the context-only "unoffered" counter, keep text/contexts/nag_times/quadrant. Drop blocked_by links to tasks that are done; keep links to open tasks.
Early completion (inside the lead window) is allowed: done Sep 25 for a Sep 28 occurrence gives next year's Sep 28.

## skip_occurrence(id) (new MCP tool)
Advance exactly like mark_done for scheduling and COUNT, but history entry says skipped and it is not a completion. Works on awake tasks and on asleep ones (skips the upcoming occurrence). Error friendly if the task is not recurring.

## restore_task
On a recurring series snapshot, rewind the live task to the saved previous state if it has not been completed/skipped again since; otherwise explain politely.

## Interactions
- Quadrants: unchanged; cadence applies from start_date. Q4 recurring never nags.
- 21-day Someday archive: tasks with repeat.status == "active" are EXEMPT (asleep or awake). Non-recurring Q4 still archives.
- Context-only 3-day/09:00 rule: counter starts at start_date, resets on revival.
- Dependencies: a recurring blocker blocks dependents only while awake (start_date reached and not done). Reject cycles as today.
- Nag times are wall-clock HH:MM Toronto. A nag at 02:30 on spring-forward day fires once at the first valid time after the gap; a 01:30 nag on fall-back day fires once, not twice. Follow the existing scheduler policy, and assert exactly-once in tests.

## MCP tool changes (descriptions must read well aloud; plain, friendly, short)
- `add_task` and `update_task` accept `repeat`, one of:
  - structured object: `{freq: daily|weekly|monthly|yearly, interval?, weekdays?: ["mon","thu"], month_day?: 1-31 or -1, nth_weekday?: {n: -1|1..5, weekday}, month?: 1-12, mode?: fixed|after_completion, lead_days?, until?: "YYYY-MM-DD", count?, due_on_occurrence?}`
  - `rrule` string (advanced)
  - `"none"` to stop repeating
- `repeat_text` (string, plain English), parsed by a small DETERMINISTIC grammar (no LLM). Must handle at least: "every day"; "every weekday"; "every Tuesday and Thursday"; "every 2 weeks"; "every month on the 28th"; "last Friday of every month"; "2nd Tuesday of each month"; "every September 28"; "every year on Sept 28"; "every 30 days after I finish it" / "30 days after completion" (after_completion); "every 3 months"; optional suffixes "start 3 days early / 3 days ahead", "until June 2027" / "until 2027-06-30", "10 times". Reply echoes the interpretation: "Got it: repeats monthly on the 28th, knocks 3 days early. Next one is Nov 28, starting Nov 25." If unparseable, say what was not understood and give 3 examples. Never guess silently. If both `repeat` and `repeat_text` are given, `repeat` wins.
- `list_tasks`: each recurring task includes `repeat` (human summary, mode, status), `next_occurrence`, and `waiting: true` when asleep. Add filter `recurring` (bool). Asleep recurring tasks must remain findable. Non-recurring task output must be byte-identical to before.
- `skip_occurrence`: "Skip this round of a repeating task. It comes back at the next scheduled time."
- `mark_done`: optional `stop_series` bool ("Mark done and stop it repeating for good").
- `list_done`: recurring entries are marked repeating; skipped ones show `skipped: true`.
- `matrix_summary` / `weekly_review_summary` / `triage_next`: count asleep recurring tasks separately so the matrix and triage are not cluttered; triage must not offer asleep tasks.

## Email
- Daily summary: add "Coming up (next 7 days)" listing asleep or lead-window recurring tasks sorted by date, e.g. "Pay Walmart Mastercard, Sat Nov 28 (knocks from Nov 25)". Omit the block when empty.
- Weekly review: add "Repeating tasks": next 30 days of occurrences, completed this week, skipped this week, overdue repeaters (awake and past their occurrence), series that ended this week. Omit empty parts.

## UI (only if ReKnock has a task editor/list UI; otherwise skip and say so)
Editor: a "Repeat" row under Due date opening a sheet: Doesn't repeat / Daily / Weekly (7 weekday chips) / Monthly ("on day N" or "on the last Friday") / Yearly / Custom every N units; "Repeat from: Schedule | When I finish"; "Start knocking N days early"; "Ends: Never / On date / After N times"; live preview "Next: Sat Nov 28". List: small repeat badge and "Next Nov 28" on asleep tasks, shown dimmed in a collapsible Waiting group. Detail: Done, Skip this one, Stop repeating buttons. Done history: repeat badge, skipped struck through.

## Tests (required; add to the existing test suite, injectable clock, America/Toronto)
Month-end: monthly day 31 from 2026-01-31 gives Feb 28, Mar 31, Apr 30, May 31; from 2028-01-31 gives 2028-02-29; monthly day 30 from 2027-01-30 gives Feb 28 then Mar 30; BYMONTHDAY=-1; yearly Feb 29 from 2024 gives 2025-02-28, 2026-02-28, 2027-02-28, 2028-02-29; after-completion 1 month done 2026-01-31 gives Feb 28, done 2028-01-30 gives 2028-02-29; every 2 months anchored Dec 31 gives Feb 28 then Apr 30; 5th Friday fallback.
Ordinal: last Friday, today 2026-11-13: Nov 27, Dec 25, 2027-01-29.
Yearly smoke alarm Sep 28, lead 3, created 2026-11-13: next 2027-09-28, start 2027-09-25; done 2027-09-25 or 2027-10-05 gives 2028-09-28.
No backlog: monthly 28th, occurrence Aug 28, today Nov 13, done gives Nov 28 (not Sep 28) and notes 2 collapsed; done Nov 28 gives Dec 28; done Nov 29 gives Dec 28; weekly Tue/Thu done Fri 2026-11-13 gives Tue 11-17; daily missed 5 days then done gives tomorrow only; after-completion 30 days done 3 months late gives today+30.
DST: daily done 2026-03-08 23:30 local gives 2026-03-09; done 2026-11-01 23:30 gives 2026-11-02; completion at 2026-11-02 00:30 Toronto (05:30 UTC) counts as Nov 2; nag at 02:30 on 2026-03-08 and 01:30 on 2026-11-01 each fire exactly once; weekly Sunday series across both transitions keeps its weekday.
Lifecycle: COUNT=3 ends at the 3rd, skip counts toward COUNT; UNTIL inclusive; done on asleep task is a no-op with friendly message; skip on asleep skips the upcoming; stop_series and repeat "none"; restore_task rewinds, and refuses politely if completed since; due date shifts; contexts, nag times, quadrant, blocked_by to open tasks survive; Q4 recurring not archived at 21 days, non-recurring Q4 still is; recurring blocker blocks only while awake.
Parser: each phrase listed above, plus rejects ("every blue moon") and after_completion+BYDAY conflict.
Migration: old rows load; non-recurring list_tasks output unchanged; migration is idempotent.
Email: daily and weekly show upcoming recurring tasks and are unchanged when none exist.
Run the FULL existing suite; it must pass unmodified.

## Rob's real examples to smoke-test at the end
- "Pay the Walmart Mastercard minimum": monthly on the 28th, lead 3 days. (Adjust day if Rob's statement date differs; just demonstrate.)
- "Pay the Neo card minimum": monthly, lead 3 days.
- "Send monthly job-search log to employment lawyer": monthly on the 1st (or last day), lead 3 days.
- "Test the smoke alarms": every September 28, lead 7 days, quadrant q2.
- "Garbage night": every Tuesday evening, nag time 19:00. "Daily habit": every day.
Do NOT create these in the live data unless there is a safe dev/test database; show them in tests or a dry run.

## Finish
Commit in logical steps on a branch (no push unless told). Update README/docs and the MCP tool docs. End with a short report: what you built, files touched, how to run tests, deviations from this spec, and any assumptions you made.