Skip to content

date: 2026-06-18 tags: [planner, daily-plan, events, completion, regen, celebrate-screen] status: active graduated_to:

A live "regenerate the whole plan" must not rebuild on completion — it wipes the completed-row state

Symptom — wiring a live-plan regenerate (#113) to fire on TaskCompleted broke the celebrate screen + sidebar badge: completing a task left its plan row's completed_at null (or the row gone). In tests it surfaced as a 500 — MissingCredentialException: Todoist API token — because completing a task in any test now triggered a plan rebuild that fetched Todoist (the sync queue runs the queued regen inline within the request).

Root causeDailyPlanner::generateFor() rebuilds by delete + recreate of daily_plan_tasks, and buildPlan() rejects completed tasks. So regenerating on completion deletes the just-stamped completed row and never recreates it — destroying the exact state MarkPlanTaskCompletedListener had stamped in place for the celebrate screen and the badge count. A completion changes no task set; it never needed a rebuild.

FixRegenerateTodayPlanListener (commit ec301c8): a completion refreshes the digest prose only (RecomposeNudgeJob); the full rebuild (RegenerateTodayPlanJob) fires on add/edit only. The regen also no-ops unless a plan already exists for the day — the morning job owns the baseline, so a stray pre-plan completion never materialises a plan (nor fetches Todoist).

Guardtests/Feature/LivePlanRegenTest.php"refreshes the digest but does not rebuild the plan when a task is completed" asserts RecomposeNudgeJob is queued and RegenerateTodayPlanJob is not, on TaskCompleted. Companion test pins the plan-exists no-op. General principle worth remembering when wiring a destructive rebuild to a domain event: a rebuild that drops "done"/terminal rows must not fire on the event that produces them — reflect that state in place instead.