Skip to content

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

  • Files: forge-suite/db/migrations/<ts>_intake_habit_link.sql (generate via cd ~/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). Optional habit_step_id text for 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" shows habit_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 ensureProp a Steps text prop on habits.defs (holds a JSON array of steps) and a Steps Done text prop on habits.logs (JSON array of done step ids). Mirror the existing ensureHabitTaskProps lazy-backfill pattern so existing workspaces get them.
  • Verification: npx tsc --noEmit clean; after a provision, the two props exist on the collections (psql on collection_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 the Steps JSON on the def; when steps non-empty, set the def Goal 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's Steps Done, add/remove stepId, set Value=count via the existing upsertHabitLog/clearHabitLog path; if the step has intakeItemId, 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 --noEmit clean.
  • 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_step then toggle_step checked=true -> log Value increments + (for a linked step) an intake entry appears; checked=false reverses both.
  • Commit: feat(habits): step ops (set/add/remove/reorder/toggle)
  • Files: forge-suite/lib/intake.ts
  • What: ensureIntakeItem/createIntakeEntry accept + persist habit_def_id. When an item has habit_def_id set, createIntakeEntry/deleteTodayIntake must NOT do the old name-based auto-tick; instead the parent habit is driven by toggleStep (avoid double habits). Add linkItemToHabit(id, wsId, itemId, defId, step?) (sets the column + adds the step to the def) and unlinkItemFromHabit. Read shape (getIntakeData) includes habitDefId on items.
  • Verification: npx tsc --noEmit clean; 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 new StepPopup + a step editor), habits CSS
  • What: A habit with steps renders its check as a popup trigger; the popup lists steps with a circular Checkbox each (+ an optional dose input for supplement steps), toggling via toggle_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 through toggle_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 toggleStep sync 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