Plan: Multi-step (multi-action) habits + supplement linking¶
URL: https://mkdocs.justinsforge.com/memory/plans/multi-step-habits-intake-2026-07-04/
Date: 2026-07-04
Approved spec: A reusable "multi-step habit" primitive in CreatorTrack (forge-suite): a habit definition carries an ordered list of steps {id, label, intakeItemId?, dose?, unit?}. Any habit can have steps; a step that links an Intake item is the supplement case. Clicking a multi-step habit's checkbox opens a popup of its steps, each checkable as a plain check or with a dosage. Checking a step updates the parent habit's day-log (steps-done + value) and, for supplement steps, writes/removes an Intake dose, so it flows into the Intake Taken/History and the existing habit->task bridge. Each Intake supplement gets an "add to habit" action. The Intake tab's scheduled checklist and the habit popup coexist and sync (both call one toggleStep). Goal = step count; log value = steps done, so "done" + the progress ring reuse the existing valued-log logic.
Grounding (from the habits-model map): habits are collection-based (habits.defs + habits.logs, jsonb cells + a Habit relation on logs; idempotency key = (habit,date) via findOrCreateLogRow). "Done" = log Value >= effective goal. Habit<->task link already exists (Push To Tasks -> lib/habit-task-bridge.ts), a multi-step habit rides it unchanged. No steps/sub-structure exists today (only a flat Group select). Intake = dedicated app.intake_* tables; today links to habits only by name (scheduled item auto-ticks a same-named habit, lib/intake.ts:154).
Naming: the primitive = steps (multi-step habit). A supplement step = a step with intakeItemId.
Out of scope (this round): per-step reminders; step-level streak stats (parent-level only); drag-reorder polish (up/down buttons first); schedule inference on capture; item edit UI + delete/trash UX (all queued separately).
Task list¶
Task 1: Migration , link Intake items to a habit¶
- Files:
forge-suite/db/migrations/<ts>_intake_habit_link.sql(generate viacd ~/forge-suite && python3 ~/forge/scripts/forge_workspace_new_migration.py "intake item habit link") - What:
ALTER TABLE app.intake_items ADD COLUMN habit_def_id bigint;(nullable; no FK to the collection-engine node beyond documentation, since habit defs are database_row nodes , store the def node id). Optionalhabit_step_id textfor the step id. Keep RLS/grants intact (column add inherits table policy). - Verification: apply per the runner;
psql "$LIFEOS_DSN" -c "\d app.intake_items"showshabit_def_id. - Commit:
feat(habits): intake_items.habit_def_id for supplement<->habit link
Task 2: Habits schema , Steps + Steps Done props¶
- Files:
forge-suite/lib/system-apps.ts(ensureHabitsApp) - What: Idempotently
ensurePropaStepstext prop onhabits.defs(holds a JSON array of steps) and aSteps Donetext prop onhabits.logs(JSON array of done step ids). Mirror the existingensureHabitTaskPropslazy-backfill pattern so existing workspaces get them. - Verification:
npx tsc --noEmitclean; after a provision, the two props exist on the collections (psql oncollection_props). - Commit:
feat(habits): Steps (def) + Steps Done (log) props
Task 3: Habits lib , step CRUD + toggleStep + read state¶
- Files:
forge-suite/lib/habits.ts - What:
getSteps(defId)/setSteps(id, wsId, defId, steps[])/addStep/removeStep/reorderStep(read/write theStepsJSON on the def; when steps non-empty, set the defGoal Value=count,Goal Unit="steps",Goal Period="Day").getHabitStepState(id, wsId, defId, date)->{steps, doneStepIds}for the popup.toggleStep(id, wsId, {defId, stepId, date, checked, dose?}), the ONE sync point: read the day log'sSteps Done, add/removestepId, setValue=count via the existingupsertHabitLog/clearHabitLogpath; if the step hasintakeItemId, call intake create/delete for that item+date (Task 5) WITHOUT the name-based auto-tick. Fail loud; never leave log and intake half-updated. - Verification:
npx tsc --noEmitclean. - Commit:
feat(habits): step CRUD + toggleStep sync + step-state read
Task 4: Habits API ops¶
- Files:
forge-suite/app/api/apps/habits/route.ts - What: Add POST ops
set_steps,add_step,remove_step,reorder_step,toggle_step {defId, stepId, date, checked, dose?}dispatching to Task 3. Same auth as existing ops. - Verification: DB/HTTP on dev :3060:
add_stepthentoggle_step checked=true-> log Value increments + (for a linked step) an intake entry appears;checked=falsereverses both. - Commit:
feat(habits): step ops (set/add/remove/reorder/toggle)
Task 5: Intake lib , honor the habit link¶
- Files:
forge-suite/lib/intake.ts - What:
ensureIntakeItem/createIntakeEntryaccept + persisthabit_def_id. When an item hashabit_def_idset,createIntakeEntry/deleteTodayIntakemust NOT do the old name-based auto-tick; instead the parent habit is driven bytoggleStep(avoid double habits). AddlinkItemToHabit(id, wsId, itemId, defId, step?)(sets the column + adds the step to the def) andunlinkItemFromHabit. Read shape (getIntakeData) includeshabitDefIdon items. - Verification:
npx tsc --noEmitclean; linking an item then logging it ticks the parent step, not a separate habit. - Commit:
feat(intake): honor habit_def_id link; route linked logs to the parent step
Task 6: Habits UI , multi-step card + step popup + editor¶
- Files:
forge-suite/components/apps/habits/*(HabitCard/TodayView + a newStepPopup+ a step editor), habits CSS - What: A habit with steps renders its check as a popup trigger; the popup lists steps with a circular
Checkboxeach (+ an optional dose input for supplement steps), toggling viatoggle_step(optimistic); parent ring shows value/goal. A step editor (in the habit's edit affordance) to add/remove/reorder steps and link a step to an Intake item (search existing items or create). Generic steps (no intake link) are plain checks. - Verification: dev :3060 render (auth-gated; verify via tsc + API + a screenshot through Justin's browser).
- Commit:
feat(habits): multi-step habit popup + step editor
Task 7: Intake UI , "add to habit" + synced checklist toggle¶
- Files:
forge-suite/components/apps/intake/IntakeClient.tsx - What: Each supplement Item gets an "add to habit" action (pick/create a habit ->
linkItemToHabit). For an item already linked to a habit, the Intake checklist toggle routes throughtoggle_step(not a bare intake log) so the habit + intake stay synced. Show a small "in" hint on linked items. - Verification: dev :3060: checking a linked item in the Intake tab also advances the habit; checking it in the habit popup reflects in the Intake Taken list.
- Commit:
feat(intake): add-to-habit + synced checklist toggle
Task 8: Register and document¶
- Files:
forge/memory/general/reference_intake_tracking.md(+ a habits multi-step note), MEMORY.md index. - What: Document the steps model, the
toggleStepsync point, the intake link column, and the coexist behavior. Update the index. - Verification: doc renders; index line present.
- Commit:
docs(habits): multi-step habits + supplement linking