Task status reuses the goal status shape, so one chip, one count and one tally serve chores, events and tasks alike. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
11 KiB
Weekly and Monthly Tasks (Phase 2a) Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Let the reader add their own weekly and monthly tasks per game — ticked N days per period, or a number against a target (1900 / 2100) — shown beside the dailies, and let an event's check-in goal be a number too.
Architecture: One status shape for everything periodic: GoalStatus (from goal.ts) gains month scope, an amount measure and the period's start key, and taskStatus returns it. That lets the Dailies chip, its count, label and tallying serve chores, events and tasks alike. Two new stores: tasks (raw records kept, validated for display only) and amounts (a value per item per period). Both travel in the export.
Tech Stack: Bun, TypeScript, React 19 (static-render tests), Zod 3, Tailwind 4.
Spec: docs/superpowers/specs/2026-10-13-periodic-tasks-design.md (phase 2a), building on docs/superpowers/specs/2026-10-03-check-in-goals-design.md.
Global Constraints
- No change to
GachaEvent, event IDs, day-log keys or existing storage keys. New keys:gacha-tracker:v1:tasks,gacha-tracker:v1:amounts. - Never drop a stored record on load: the task store keeps raw records and filters for display; the amount store never prunes a period.
- Import merges and never removes: tasks by id, later
updatedAtwins; amounts per item and period, lateratwins; malformed incoming entries are skipped. Task.gameisz.string(), neverGameId(AGENTS.md § retirement rule).mytaskjoinsRESERVED_ID_SEGMENTS(androutine, reserved now for 2b).- A chip is one target: an amount chip opens an editor, it never ticks.
- The header's
Today's dailies · n/totalcounts day-cadence items only; weekly and monthly tasks get their own counts; only a real tick today celebrates. - All calculations pure,
nowexplicit. Commits: one coherent change each, green, pathspec, one command per Bash call.
Review Focus
- A task record a later version wrote (unknown measure) — hidden, never removed from storage, and survives export/import.
- An amount typed as
1,900,1900.5, empty or negative — amounts are whole numbers: separators are stripped, decimals rounded, empty or unreadable is mid-edit (Save disabled), negative refused. - A monthly task with start day 31 in a 30-day month — period boundaries from
periodOf's clamping; the amount key is the clamped start. - Importing an older backup onto a device whose amount for the same week is newer — the newer value stays.
- A task on a game the reader has hidden or isn't focused on — not shown; its data untouched.
Tasks
Each task: write the named tests first and watch them fail, implement, run bun test + bun run typecheck, commit by pathspec.
Task 1: Amount goals and month scope in the goal model
Files: src/shared/goal.ts, test/goal.test.ts
Produces:
GoalSchemaadds{ period: "week", measure: "amount", target: number (>0, ≤1e9), weekStartsOn }and{ period: "event", measure: "amount", target }.type AmountLog = Record<string /*periodStart*/, { value: number; at: string }>.GoalStatusgainsmeasure: "days" | "amount",periodStart: string, andscope: "week" | "month" | "event".goalStatusinput gainsamounts?: AmountLog. For amount:count = amounts[periodStart]?.value ?? 0,required = target,open= remaining days in scope (null for an unknown-end event),shortfall = false.periodStart= the week's start, or the event's first game-day for event scope.- Copy:
goalCaptionamount →1,900 of 2,100 this week · 200 to go, met →2,100 of 2,100 · Done this week;goalCountamount →1,900/2,100(+wk/mo); month scope →this month/mo/done this month. Numbers viatoLocaleString("en-US"). goalRequired(goal): number(required or target) for callers that showedgoal.required.
Tests: amount week counts the stored value for this week only; last week's value ignored; met at ≥ target; event-scope amount keyed by first game-day; captions/count/label for amount and month; readGoal accepts amount shapes and still rejects {measure:"amount"} without target.
Task 2: The task model
Files: create src/shared/task.ts, test/task.test.ts; modify src/shared/custom.ts (RESERVED_ID_SEGMENTS += "mytask", "routine").
Produces:
TaskId = z.string().regex(/^mytask:[a-z0-9]{6,32}$/),mintTaskId(random = Math.random): string.MeasureSchema={ kind: "days", required: 1–31 }|{ kind: "amount", target: >0 ≤1e9, unit?: string ≤24 }.TaskSchema={ id, game: z.string().min(1), title: 1–80 trimmed, period: PeriodSchema, measure, from?: string, at, updatedAt },type Task.taskStatus({ task, region, now, logged, amounts }): GoalStatus— scope =period.unit; days: distinct ticks inperiodOf(...)≤ today; amount: value at the period start;open= period days ≥ today unticked (days) or remaining (amount);requiredfor days capped at the period length.taskCaption(task): string—every week from Monday · tick 1 day/every month from the 1st · reach 2,100 points.
Tests: weekly days task done once this week; monthly amount task reads its month's key with 31→30 clamping; per-region boundary at reset (Genshin Europe); reserved segments include mytask (extend the existing custom.test.ts loop expectation if needed); caption strings.
Task 3: Task and amount stores
Files: create src/client/state/useTasks.ts, src/client/state/useAmounts.ts, test/tasks-store.test.ts; modify src/client/state/storage.ts (KEYS tasks, amounts).
Produces:
mergeTasks(current: Record<string, unknown>, incoming: unknown): Record<string, unknown>— validates incoming withTaskSchema, skips bad ones, keeps current records (even unreadable), laterupdatedAtwins.visibleTasks(raw: Record<string, unknown>): Task[]— the parseable ones, sorted by game then title.useTasks()→{ tasks: Task[], addTask(draft), editTask(id, draft), removeTask(id), merge(incoming: unknown) }withtype TaskDraft = { game, title, period, measure }.mergeAmounts(current: AmountLogMap, incoming: unknown): AmountLogMap— per item and period, finite numbers only, lateratwins, never removes.useAmounts()→{ amounts: AmountLogMap, amountsFor(key): AmountLog, setAmount(key, periodStart, value: number | undefined), merge(incoming: unknown) }.setAmount(undefined)removes that one period's entry (reader's own action).
Tests: unreadable stored task kept by merge and hidden by visibleTasks; merge later-wins both directions; amounts merge per period (older backup cannot roll back a newer week; a period only on one side survives; strings/NaN skipped).
Task 4: Tasks and amounts in export and import
Files: src/client/state/export.ts, src/client/App.tsx, test/progress.test.ts.
Produces: ExportData.tasks?, ExportData.amounts?; buildExportData(..., own: { games, events, tasks?, amounts? }, prefs?) writes them when given; ParsedImport.tasks: unknown, ParsedImport.amounts: unknown; importProgress gains mergeExtra(tasks, amounts) before restorePrefs.
Tests: round trip keeps tasks (including an unreadable one) and amounts; an export without the keys parses with both undefined.
Task 5: Tasks and amounts in the Dailies strip
Files: src/client/components/Dailies.tsx, create src/client/components/AmountEditor.tsx, src/client/App.tsx, test/views.test.tsx.
Produces:
DailyItemgainscadence: "day" | "week" | "month"andperiod?: Period(tasks). Chores and events are"day".dailyGroups(..., goalFor, tasks: readonly Task[] = [], taskStatusFor: (t: Task) => GoalStatus = …)appends a lane's tasks after its events, only for lanes ingames(no stragglers: a hidden game's tasks stay hidden).dailyTallyreturns{ total, complete, tickedToday, week: { total, complete }, month: { total, complete } }, wheretotal/completecount"day"items only.- An item whose
goal.measure === "amount"renders as an amount chip: label,1,900/2,100, a 2px underline in the hue atcount/requiredwidth; click opensAmountEditorunder the strip (<label>{title} this week: <input> / 2,100 <unit>· Save · Cancel; Enter saves, Escape cancels; empty or invalid disables Save; negative refused; integers). - Header:
Today's dailies · n/totalthen, when present,· Weekly a/b · Monthly c/d. - Catch up: a days task gets
catchUpDayspips as a chore does; an amount task gets anAmountEditorfor the previous period. Dailiesprops:tasks?,taskStatusFor?,onSetAmount?: (key, periodStart, value) => void,amountFor?: (key, periodStart) => number | undefined. Tests: task placement after events; hidden-lane tasks absent; separate counts; amount chip renders1,900/2,100with noaria-pressed;chipLabelfor amount and month (…, 1,900 of 2,100 this week,…, done this month);AmountEditorstatic render with value and target.
Task 6: Amount goals on events in the detail sheet
Files: src/client/components/GoalControl.tsx, src/client/components/EventDetail.tsx, src/client/components/DailyChecklist.tsx (none if the caption already handles amount), src/client/App.tsx, test/views.test.tsx, test/custom-ui.test.tsx.
Produces: GoalControl shows, once a goal is set, a measure toggle Days · Points; Points shows [2100] points each week / in total with its own draft field. chooseGoal keeps the measure when switching period. EventDetail gets amount?: number, onSetAmount(id, periodStart, value), and renders an AmountEditor (always open, inline) under the goal control for an amount goal.
Tests: chooseGoal keeps measure; switching Days→Points carries a sensible default target (100); sheet renders the editor with the current value.
Task 7: Your tasks in settings
Files: create src/client/components/TaskForm.tsx; modify src/client/components/YourOwn.tsx, src/client/components/Controls.tsx only if props pass-through needs it, src/client/App.tsx, test/custom-ui.test.tsx.
Produces: "Your weekly and monthly tasks" list in the Your own group (game short name, title, taskCaption, Edit, Delete) and Add a task. TaskForm: game (select of lanes by name), title, Every week (weekday select, default Monday) / Every month (day 1–31, default 1), Tick it off ([1] days) / Reach a number ([target] + optional unit). Save disabled until valid against TaskSchema.
Tests: form renders defaults; list shows caption; empty state shows the add button.
Task 8: Docs and a look
PRD new feature entry (F17 — weekly and monthly tasks); DATA-MODEL keys, shapes, merge rules, reserved segments, export keys; AGENTS.md § Events the reader entered themselves (tasks join the reader's spaces; task store never drops on load); DESIGN.md amount chip + editor; spec status; recurring-events spec Phase C pointer. Build and screenshot the strip with a weekly amount task and a monthly tick task, and the settings form, dark and light, on a fresh port.