3.3.0
Released
Features
The dashboard opens with Today: this cycle of each daily (or weekday, or weekly-and-active-today) Quest, its tasks as inline status controls, "N of M done", and the streak with its unit — "6 days", "3 weeks" — so the daily loop is one page: open the app, tick the list, watch the streak extend (#859). The controls are the task row's cycle through the same
PATCH /api/tasks/:idas Due this week, now shared asTaskStatusCycleButton; after a tick the page refreshes, and because finishing the last task completes the cycle in the database, the refreshed read brings back "Done for today" and the extended streak together. A cycle with no tasks points at its quest; a weekly cycle is labelled with its window; more than five templates link to/quests. On a phone Today is the first thing under the greeting and each tick is a 44px target; fromxlit heads the main column, with the rail now spanning two rows and the main column auto-placed so an absent Today leaves no blank row. "Up next" skips every cycle of a template Today shows, so tomorrow's daily cycle no longer occupies the card meant for the next other scheduled quest; "Recently updated" collapses a template's cycles to its latest one, labelled "Cycle N", so a daily Quest is one card rather than a new card every morning — the read pages in batches of three times the preview until it holds enough distinct entries, bounded at five pages, since a template's completed history is "recently updated" too. The rail's "Cycle streaks" now count in the cadence's unit: days, weekdays, weeks, or cycles for any other interval. A failed Today read is an error line, not an empty day. Everything is behindNEXT_PUBLIC_FEATURE_QUEST_RECURRENCE.The AI Quest Coach drafts routines as recurring Quests (#858). Its draft carries a
repeat—none,daily,weekdaysorweekly, with an optional session length, start time and, for a weekly routine, the day it falls on — in both the prompted JSON and the structured-output schema every provider is sent (COACH_QUEST_DRAFT_SCHEMA, still valid under OpenAI strict mode). The prompt tells the coach to set it when the user describes a routine, habit or practice ("every day", "each morning", "keep up"), to write the tasks as the 2–4 steps of ONE session with no frequency in their titles, to leave a project with an end asnone, to ask "once, or a routine you repeat?" only when the answers leave it unclear, and to treat "make it weekdays only" as a change torepeatalone. The parser reads anything missing or unrecognised asnone, so a project-shaped draft and every reply from before this behave exactly as they did; drops a session length longer than one cadence or a start that is not a 24-hour time; and strips frequency words ("daily", "every day", "each morning", "weekly"…) from a routine's task titles when a model writes them anyway. The draft preview states the cadence before Continue — "Repeats daily · 2 tasks each day · ~15 min · from 07:00" — and asking for a change redrafts it like any other. On the New Quest page, Continue on a routine swaps the form itself for the recurring-Quest form, pre-filled with the title, goal, context, constraints, cadence, session length, the next occurrence of the start time (on the named day for a weekly routine — "every Monday at 07:00" starts on the next Monday — rolled past the weekend for weekdays, and past a day whose clocks skip that time for daylight saving) and the session's tasks, under a "Recurring Quest" banner with "Create it once instead" — switching between the recurring and one-off forms by hand keeps everything typed, the shared fields carrying across and each form's own fields — reference links and a picked Quest Pack included — coming back when it does; the coach column stays beside it, so "Ask for changes" still works, and a redraft that comes back as a one-off swaps the ordinary form back in. Submitting creates the template — carrying the persona chosen on the coach's chip, so every cycle's investigations run on that model, refused with the usual upgrade message if the plan does not cover it — and like every recurring cycle now, each one counts toward the owner's Year Theme for its year (the banner names this year's) — and lands on its first cycle, as the recurring page does. With recurring Quests switched off the ordinary form is shown, with one line saying the repeat was dropped. The recurring-Quest form gains a Constraints field on/quests/recurring/newtoo. A routine draft costs the same coach turn as any other.A recurring Quest can start every cycle with the same checklist (#857). The recurring-Quest form gains "Each cycle starts with these tasks": a one-line add row in the style of the task page's quick-add, with details behind a toggle and Enter adding the task rather than submitting the form, then each task editable in place with move up, move down and remove, up to 20; a task still typed in the add row when the form is submitted is saved with the rest rather than dropped. Adding the first task sets "Complete each cycle when" to All tasks are done, and removing the last sets it back, unless the person has already chosen a mode. A template with tasks is created switched off, its tasks are saved, and then it is switched on and minted, so a dashboard read in another tab cannot mint a cycle without the checklist; if the tasks cannot be saved the template is removed and the form says so. On a minted cycle, the owner's Details card has a Repeats row linking to "Edit tasks for future cycles" (
/quests/recurring/[templateId]/tasks), which edits the template's list and returns to the cycle it was opened from; the page says that cycles already created, including the next one, keep the tasks they have. Applying an edit to the current cycle, and editing a template's schedule, are not part of this.A recurring Quest can repeat daily, on weekdays, weekly or on a custom cadence (#856). The recurring-Quest form gains a Repeats control — Daily · Weekdays · Weekly · Custom (every N days or weeks, up to 90 days) — in place of the "Cadence: Weekly" statement; Weekly stays the default, so nothing changes for anyone who ignores it. Duration presets follow the cadence (15 min · 30 min · 1 h · All day for a daily or weekdays template, today's set otherwise, each list trimmed to what fits inside one cycle, and the custom-minutes field capped the same way), a preset that no longer fits after a cadence change falls back to the first one that does rather than submitting a value the validator refuses, and the validator caps the duration at one cadence instead of one week. The live summary names the cadence ("Repeats: Weekdays", "Repeats: Every 3 days"), previews the pair the mint will create from the chosen cadence — so a weekdays template started on a Friday shows Friday and Monday — and, for a one-day cadence, lists the next three cycles under "Then" so the shape of the week is visible before submit; the "created immediately" line says today and tomorrow, today and the next weekday, this week and next, or this cycle and the next. The create action writes the cadence and skipped weekdays from that choice instead of a literal
'7 days'. The page header no longer calls it a weekly rhythm and the two "Repeat weekly" entry points read "Repeat on a schedule". Editing an existing template's cadence is not part of this: there is no template edit form yet.The AI Quest Coach on a phone asks one question at a time (#854). In the full-screen panel below
lg(#853) the interview was still a chat transcript: every turn stayed in the thread, each coach reply was a bubble carrying the preamble and the question, the user's answers were echoed back as bubbles, and the thread pinned its newest line to the bottom of a scroll box — so with the keyboard up the question or the answer field was off screen, and "Question 2 of ~5" sat under the greeting and question 1. The panel now shows the current step: the coach's latest question as a heading, taken from the bolded question of the canned openings (a bold run counts only when it is a whole sentence, so a model bolding one word of its question keeps the question whole) or the final question-mark sentence of a model turn (splitCoachQuestion()inlib/ai/coaching.ts, which also drops a "Let's start:" lead-in), its preamble as muted copy under it, and the answer field directly below. When an answer is sent, that question and answer move behind an "Earlier (N)" disclosure above the step, collapsed by default and collapsed again on every send, so the next question arrives with the field under it and nothing to scroll; opening it lists each answered turn as the question over the answer, with an Edit that puts that answer back in the field, focused; sent, it goes to the coach as a correction that names the question it re-answers (correctionMessage()) rather than as a reply to the question now showing, the row shows the new text marked "edited", and the progress count does not move — a "Keep original" control drops the change; anything already typed for the current question when Edit took the field comes back to it on "Keep original", and once the correction has been answered is offered back beside the field ("You were typing … · Restore / Discard") rather than put in it, since the coach may have moved on; the offer is dropped on the next send, on Start over, and when the correction's reply is the draft (Edit is not offered mid-turn or while a draft is waiting; editing the answer to the current question is just that answer again). While a reply is on its way the typing indicator stands in for the step; once a draft is ready the step is the draft summary with Continue / Start over below it in the field's place, and "Ask for changes" reopens on that summary. A failed send puts the text back in the field — still marked as a correction if it was one — rather than leaving the attempt in the history, where a retry would have gone out as a second answer to the same question (in the transcript atlgtoo); the substitution notice (#841) sits under the question it is about. Atlgand up the transcript is unchanged. Both views are rendered from the same messages and share the header and the answer field, so the window crossinglgmid-interview changes only the middle and the field keeps its focus and its text (CoachingInterviewpicks the view from itssheetprop).
Technical changes· 13 entries for database, tooling, CI, tests and docs
Database
20261110120000_release_330_review_fixes.sqlmakes the dashboard's Today section judge a cycle's window by instants (#859, PR #871 review).get_my_today_cycles()converted the cycle's start to local wall time and addedscheduled_durationthere, while the cycle card andget_quest_cycle_streaks()treat the duration as elapsed time, so across a daylight-saving change the two disagreed by an hour: a 24-hour Toronto cycle from midnight on spring-forward Sunday runs until 01:00 Monday but left Monday's Today at midnight, and on fall-back Sunday a cycle stayed in Monday's Today an hour after it ended. The rule now lives inquest_window_overlaps_local_day(_start, _duration, _timezone, _at), which compares the window's start and end instants with the local day's bounds converted back totimestamptz; the read calls it withnow(), and its signature, columns and grant are unchanged. The helper is not a client RPC — revoked frompublic,anonandauthenticated— and is in the generated types.supabase/tests/dashboard_today_cycles.sqlgains nine assertions on the 2026 Toronto transitions (both directions, a 23- and a 25-hour day, a day-valued duration) and the two grants, andrls.sqlasserts the helper is closed toanon.20261109120000_dashboard_today_cycles.sqladds the read behind the dashboard's Today section and puts the cadence beside every streak (#859).get_my_today_cycles(_limit)returns one row per recurring Quest cycle whose window overlaps today — today being the template's local day, so tomorrow's pre-minted cycle stays out and a weekly cycle whose window covers today comes in — for the cyclesuser_can_access_quest()admits (owned, or a party seat whose guild membership is still accepted). Each row carries the quest, its status (a completed cycle stays in, so "Done for today" and the streak it extended can render after the last tick), the template's cadence in days and skipped weekdays, the window, the streak fromget_quest_cycle_streaks(), and the tasks as JSON in sort order — only thoseuser_can_write_quest_task()admits, so an owner sees every task and a party member only their own seat's, and no tick the section offers can fail on permission. The limit is clamped to 1–20 and the default is 5.get_quest_cycle_streaks()is dropped and recreated withcadence_daysandskip_daysso the rail can say "7 days" or "3 weeks" rather than "cycles"; its body is otherwise the live one from20260910130000, and its grants are re-applied. Both areSECURITY DEFINER, pinned topublic, and callable only byauthenticated.20261108140000_recurrence_cycle_year_theme.sqlmakes every newly minted recurring cycle count toward its owner's Year Theme (#858).mint_quest_recurrence_cycles()setsquests.year_theme_idon each cycle it creates to the owner's theme for the year that cycle starts in, in the template's timezone, when the owner has one and it is not archived; a year without a theme leaves its cycles untagged. A user has at most one theme per year, so the template stores nothing: a routine that runs across New Year counts toward each year's theme in turn, December's cycles toward this year's and January's toward next year's once it is written. This applies to every recurring Quest, not only those the AI Quest Coach drafted; cycles minted before this migration are left as they are, and a cycle keeps its tag if the theme is archived later. The theme is always the template owner's own, whichquests_validate_year_theme_owneralso checks on the insert. Covered bysupabase/tests/quest_recurrence_cycle_year_theme.sql(12 assertions: no theme column on templates, December cycles tagged to the 2026 theme and New Year's Day to 2027's, another owner's cycles tagged only to their own theme, an owner without a theme minting untagged cycles, and an archived theme tagging nothing new while earlier cycles keep theirs).20261108130000_recurrence_template_persona.sqlgives a recurrence template the model persona its cycles run on (#858).quest_recurrence_templates.model_persona_idis a nullable reference tomodel_personas, cleared if the persona row is deleted, and carries the same plan-tier guard asquests.model_persona_id—guard_quest_model_persona()is attached to the template table as-is — so a signed-in owner cannot put a persona above their plan on a template, an edit that leaves the persona alone is grandfathered, and trusted callers pass.mint_quest_recurrence_cycles()copies the persona into each cycle it creates when the persona is active and the OWNER's plan, read from their profile, still covers it, and mints the cycle with no persona otherwise: a Pro template keeps minting after a downgrade, its new cycles on the default model, instead of the quest guard's 42501 aborting the mint for every template the owner has, and the pg_cron path andensure_quest_recurrence_cycles()decide the same way. Templates without a persona, which is every template created before this, mint as before. Covered bysupabase/tests/quest_recurrence_template_persona.sql(15 assertions: the column and foreign key, the guard on insert and update, the copy from both the signed-in and the cron mint, a downgrade degrading only later cycles without breaking the mint, and a retired persona not copied) and two assertions inrls.sql.20261108120000_recurrence_template_constraints.sqlgives a recurrence template aconstraintscolumn — time limits, budget, scope, things to avoid — that every newly minted cycle copies intoquests.constraints, beside the goal and context it already copied (#858). It is nullable text of up to 2000 characters, the same bound asdescriptionand as the quest form's constraints, and rides on the template's existing owner policies and table grant.mint_quest_recurrence_cycles()— the #857 body otherwise unchanged — adds it to the cycle insert; a template without constraints, which is every template created before this, mints exactly as before, and changing a template's constraints leaves cycles already minted alone. Covered bysupabase/tests/quest_recurrence_template_constraints.sql(eight assertions: the column, the 2000-character check, the copy into each cycle, a null staying null, and minted cycles untouched by a later edit) and four assertions inrls.sql(the owner can set it; another user can neither read nor change it).20261107120000_recurrence_template_tasks.sqlgives a recurrence template an ordered task list that every newly minted cycle starts with (#857).quest_recurrence_template_tasksholdstitle(1–200 characters), optionaldetails(up to 2000) and a zero-basedsort_order, cascades with its template, and is readable only by the template's owner; clients get no insert, update or delete grant. The one write path isset_quest_recurrence_template_tasks(_template_id, _tasks jsonb), asecurity definerRPC that locks the template, refuses anyone but its owner with 42501 (a missing template reads the same), trims titles and details, rejects more than 20 tasks or a blank or over-long title with22023, and replaces the whole list in one statement so a reorder never half-saves.mint_quest_recurrence_cycles()— the #856 body otherwise unchanged — copies the list intotasksfor each cycle it creates, keeping the positions, behind the samereturning idthat gates the party seeding, so re-running the mint for a cycle that exists adds nothing. The copies are ordinaryorigin = 'user'tasks the owner can reword, remove or add to, and nothing done to a cycle reaches the template. An edit therefore reaches cycles minted after it only — and since the next cycle is minted ahead of time, it keeps the list it was minted with. Templates with no task rows, which is every template created before this, keep minting task-less cycles. Covered bysupabase/tests/quest_recurrence_template_tasks.sql(38 assertions: the copy, the order and origin, the re-mint, edits reaching only later cycles, cycle edits leaving the template alone, the cap and every refusal, owner-only, clearing, and deletion) and nine assertions inrls.sql.20261106120000_recurrence_cadences.sqllets a recurrence template repeat daily, on weekdays, weekly or every N days (#856).cadencewas always a freeintervalthat only the app pinned to'7 days'; the one shape the schema could not hold was "every day except the weekend", which is now a one-day cadence plusquest_recurrence_templates.skip_days smallint[](weekdays 0 = Sunday … 6 = Saturday on which no cycle is minted), checked to 0–6, fewer than seven of them, and empty unlesscadence = interval '1 day'.mint_quest_recurrence_cycles(_owner_id, _now)— the body behindensure_quest_recurrence_cycles()and the reminder cron — walks cycle indices from the current one, passes over a skipped weekday, and stops once two eligible indices have been considered (capped at 14 indices), so on a Friday a weekdays template mints Friday and Monday and a Mondays-only one mints the next two Mondays; for a template with noskip_daysthe walk is exactly the old (current, next) pair, so existing weekly templates behave as before and nothing is backfilled. An eligible cycle whose window has already closed is still passed over without reaching further ahead, as before.get_quest_cycle_streaks()needs no change: it ranks minted cycles by index, so a skipped weekend has no row and cannot break a weekday streak. Covered bysupabase/tests/quest_recurrence_cadences.sql(daily, weekdays, six skipped days, every three days, daily across the November DST change, the weekly regression, the three constraints and the weekday streak) and four assertions inrls.sql.
Tests
supabase/tests/dashboard_today_cycles.sql(25 assertions) pinsget_my_today_cycles()and the recreatedget_quest_cycle_streaks()(#859): grants, the mint and the read agreeing on today's cycle with tomorrow's absent, a closed, an archived and a soft-deleted cycle out, a Tokyo template's last hour counting whatever the date is in Toronto, tasks as JSON in template order, the owner seeing every party task and a member only their seat's, a guild-backed seat seeing the cycle while its guild membership is accepted and nothing once departed, a completed cycle kept with its status, cadence, skipped weekdays and streak, the empty task list, the limit clamp, a stranger seeing nothing, and the streak list's new columns.rls.sqladds the two anonymous-caller refusals. On the app sidetoday-cycles.test.ts,streaks.test.ts,today-cycles.test.tsx,task-status-cycle-button.test.tsxand additions tobody.test.ts,queries.test.ts,quest-card.test.tsx,quest-cycle-streaks.test.tsxand the dashboardpage.test.tsxcover the read's parsing and overflow flag, the unit words, the section's states and ticks, the shared button, the template ids flowing into Up next, the grouped preview's paging (fills from a second page, stops when full, gives up at the page cap), the "Cycle N" line, a completed cycle's ticks disabled, and Today rendered before the rail and never read with the flag off.coaching.test.tscovers routine drafts on fixtures (#858): three routine payloads with "daily", "every day", "each morning" and "weekly" left in their task titles keep their cadence and come out with clean session titles and untouched details; two project payloads — one with a stray session length and a "daily" mid-title — staynonewith their titles as written; a missing, malformed or unknownrepeatreads asnone; session lengths are bounded to one cadence and starts to 24-hour times; a title that is only a frequency word is kept; the preview wording for each cadence; a weekly routine's day read from "monday", "Mondays", "tue" or 0–6, dropped on any other cadence, named in the preview ("Repeats weekly on Mondays") and stripped from task titles ("every Monday"); the prompt's routine, ambiguity and change-request guidance; the prompt's own example payload parsing as a one-off; and the schema pinned to the parser, includingrepeat's keys and cadences.coaching-interview.test.tsxcovers the cadence line and per-cycle task count in the preview, the recurring footnote, a one-off draft without either, and "Change it to weekdays" redrafting the line.coaching-quest-creator.test.tsxcovers the form swap, the bounded task copy, "Create it once instead" and back, a one-off redraft swapping the ordinary form back, recurring Quests off, and a project draft unchanged.quest-recurrence-form.test.tsx,validators.test.ts,actions.test.tsandpreview.test.tscover the pre-filled form, a session length no preset offers, the start time's next occurrence (on a named weekday, at the default hour when no time was given, past a Friday's weekend for weekdays, past the spring-forward gap inAmerica/New_York, on a fall-back day, and past the repeated fall-back hour once its first occurrence is over — the occurrence the server stores — with a sweep checking every offered start is stored in the future), the stacked layout, the constraints field and its storage, and the persona field — stored when chosen, refused above the plan or when unknown, none when empty or with personas off, and a tier-guard refusal reported as a persona problem rather than a guild one;coaching-quest-creator.test.tsxchecks the chip's persona reaches the recurring form and follows a new pick, and that edits survive a round trip between the recurring and one-off forms while a new draft starts both over;form-carryover.test.tscovers reading each form's submitted values, including the one-off schedule and reference links asQuestFormdefaults, the creator test keeps links and a picked Quest Pack across a round trip and names this year's theme on the recurring form whatever the toggle says (and none without one), and the recurring form takes a carried custom cadence, completion mode and guild.e2e/quest-coach-recurring.spec.tsdrives a routine draft through Continue to the pre-filled recurring form and a one-off redraft back to the ordinary form.supabase/tests/tasks_quest_list.sqlno longer fails intermittently on its two "newest first" assertions. Its fixture pinned each quest'supdated_atwith a plainupdate, butquests_set_updated_atre-stampsnow()on every update, so the four quests tied and their order came down to physical row placement; the pin now runs with that trigger disabled for the one statement (#857).validators.test.ts,actions.test.tsandqueries.test.tsunderlib/quest-recurrences/cover the task list (#857): the hidden JSON field parsed, trimmed and capped at 20, unreadable JSON reported rather than dropped, the return path accepted only as/quests/<uuid>, the create order (inactive insert, tasks, activate, mint) with the template removed when the tasks or the activation fail, and the edit action's save, clear, cap, refused-template and flag-off paths.recurrence-task-editor.test.tsx,recurrence-tasks-form.test.tsx,quest-recurrence-form.test.tsx,quest-details-card.test.tsx, the new edit page's test andquests/[id]/page.render.test.tsxcover adding with Enter inside a form, details, reorder, edit, remove, the 20-task stop, the completion-mode default and a chosen mode being kept, the future-only copy, and the link shown to the owner only.e2e/quest-recurrence-tasks.spec.tscreates a daily Quest with two tasks, checks both minted cycles start with them, edits the list from the cycle and checks the cycle keeps its tasks while the template changes. Theapps/webstatement and line floors move up to 94.7, measured at 95.01 / 91.85 / 95.03 / 95.01 across 4940 tests.validators.test.ts,actions.test.tsandpreview.test.tsunderlib/quest-recurrences/pin the repeat choices (#856): the interval andskip_dayseach one stores,cadence_daysread only for Custom and bounded to whole days from 1 to 90, the duration capped at one cadence with a daily-specific message, and a preview that mirrors the mint's walk — today and tomorrow plus the next three for Daily, Friday and Monday then Tuesday to Thursday for Weekdays, two Mondays for six skipped days, a closed window dropped without a third cycle pulled forward, three-day spacing for Custom, the local hour kept across the November DST change, and nothing shown for a duration longer than the cadence.quest-recurrence-form.test.tsxcovers the control's options andname, the daily presets with the selected one kept when it fits and replaced when it does not, the weekend skipped in the preview, the custom-minutes cap, the days/weeks custom cadence submitted as a day count, and the empty custom cadence previewing nothing.e2e/quests.spec.tsasserts the Repeats control defaults to Weekly before the existing flow runs.coaching.test.tspinssplitCoachQuestion()on the two canned openings (the bolded question promoted, the theme title's bold left in the preamble, the lead-in dropped), a model bolding part of its question or a whole sentence, a model turn (the last sentence), decimals and times that must not split a sentence, abbreviations inside the question ("6 a.m. work?", "e.g.", "U.S.") and before a capitalised word, a lower-case word after a full stop, a colon mid-sentence, a question-only reply, a reply with no question, and the empty string (#854).coaching-interview.test.tsxcovers the one-question view in panel mode: the heading over its preamble with nothing earlier, the next question replacing the previous one with the answered turn behind "Earlier (1)", the disclosure collapsing again on send, Edit refilling and focusing the field, a changed earlier answer posted as a correction naming its question with the row marked edited and the progress unchanged, Keep original clearing it and restoring a half-typed answer, the same draft offered back with Restore / Discard once the correction is answered and dropped on the next send or when the reply is the draft, Start over forgetting it, a failed send and a failed correction both returned to the field with the retry posted as one turn, the typing indicator in the step with Edit disabled mid-turn, the draft taking the step with Continue focused and no Edit, the draft kept after Continue for "Ask for changes", the substitution notice under the question, the transcript kept atlg, and the field surviving the switch acrosslg.e2e/quest-coach.mobile.spec.tsasserts the current question's heading, rather than the newest bubble, is inside the visual viewport with the keyboard up, and adds the step flow — only the current question showing, the answered turn filed and reopened, Edit sending a correction that names its question, the draft as the step — and a check that the transcript stays atlg. Theapps/webcoverage floors move up to 94.6 / 91.3 / 94.8 / 94.6 (statements / branches / functions / lines), measured at 94.94 / 91.83 / 95.13 / 94.94 across 4862 tests.