Skip to content

Bot Restoration: Comprehensive Handoff for bot-refiner

This is the single canonical handoff to restore Justin's Telegram bot fleet and add the new capabilities. Supersedes bot-tool-additions-2026-04-29 (which is now stale; many property names + schemas have changed since it was written).

Reads: this doc + lifeos-system-design-2026-04-29 (intent + workflows) + notion-api-playbook (how to call Notion) before starting.

Status

  • Both Telegram services are PAUSED: forge-inbox-capture.service + forge-lifeos-coordinator.service (since 2026-04-29 03:09 UTC)
  • DO NOT restart them until brain code is updated. The current brain references stale schemas; will hard-fail every message.

What changed since the original handoff

Big stuff: - Notion was full-refactored: per-brand Tasks DBs (Life, JWVR, Nova, Fishing, Bass) + per-brand Projects, JWVR pipeline (Series + Deals + Deliverables + Derivatives), 7 specific habits, all renamed properties, page icons on Habits log rows - New unified Notion client: forge/scripts/forge_notion_api.py — wraps both public + internal Notion APIs; the brain should use it instead of n8n webhooks where possible (faster, more capability) - forge_habits_instantiate.py cron creates today's Habits log rows at 04:00 CT, with the related Habit's page icon - forge_habits_streaks.py cron computes Days hit this week + Current streak at 23:55 CT - 3 cleanup-required handoff docs preserved at forge/memory/handoffs/: notion-redesign, notion-audit, notion-research, lifeos-system-design (this one)

Authoritative DB ID map (for NOTION_DBS)

NOTION_DBS = {
    # Top-level singletons
    "inbox":          "3500950b-d7a9-81f8-b8ec-d3125e551973",
    "knowledge":      "3500950b-d7a9-8104-8b58-c290aa756b22",
    "reading_watch":  "3500950b-d7a9-8164-a9c9-e49684ebf939",
    "areas":          "3500950b-d7a9-811a-8708-c7d7e8f79757",
    "goals":          "3500950b-d7a9-8195-90e3-f9eaa3a16773",
    "habits":         "3500950b-d7a9-8125-8571-ca97e2369ea6",
    "habit_defs":     "3510950b-d7a9-8112-a196-cb92f2e5d7df",
    "daily_log":      "3500950b-d7a9-8124-99dc-f1a152e6ef62",
    "wellness_daily": "3500950b-d7a9-811c-8e10-f3065f763b35",

    # Per-brand Tasks
    "life_tasks":     "3510950b-d7a9-81db-ae95-e15ad2b1a25d",
    "jwvr_tasks":     "3510950b-d7a9-8151-83b7-fae4e92b39db",
    "nova_tasks":     "3510950b-d7a9-81c7-8d1d-c4190c2b43c9",
    "fishing_tasks":  "3510950b-d7a9-8131-b6dc-f942ebbeb1c4",
    "bass_tasks":     "3510950b-d7a9-8177-a93b-e52f655e969b",

    # Per-brand Projects
    "life_projects":     "3510950b-d7a9-81b8-93fd-e93fbe8935eb",
    "jwvr_projects":     "3510950b-d7a9-81f7-9bb0-e57b6ed74f03",
    "nova_projects":     "3510950b-d7a9-81bc-a062-f503328aa11d",
    "fishing_projects":  "3510950b-d7a9-81b0-a746-fed88b09b7b4",
    "bass_projects":     "3510950b-d7a9-81fe-9d19-deafd37f27bf",

    # JWVR pipeline
    "jwvr_pillars":      "3500950b-d7a9-814f-b8fa-df15226b0f27",
    "jwvr_content_ideas":"3500950b-d7a9-812d-81ed-c0e563839ea2",
    "jwvr_production":   "3500950b-d7a9-815a-8ab5-db3f2da3ba65",
    "jwvr_published":    "3500950b-d7a9-81b0-be05-f5d2160eac8c",
    "jwvr_series":       "3510950b-d7a9-81f4-93e3-de4a0149886d",
    "jwvr_sponsors":     "3500950b-d7a9-8138-85f5-c94938438fd5",
    "jwvr_deals":        "3510950b-d7a9-81b4-8a31-f573e6801fae",
    "jwvr_deliverables": "3510950b-d7a9-81f5-b0dc-f8310fc0ccf6",
    "jwvr_derivatives":  "3510950b-d7a9-81da-8e20-da38bd4036b0",

    # Nova
    "nova_clients":      "3510950b-d7a9-8187-8226-c398c7f1a91d",
}

BRAND_TO_TASKS_DB = {
    "life": "life_tasks", "lifeos": "life_tasks",
    "jwvr": "jwvr_tasks", "justinwieb-vr": "jwvr_tasks", "vr": "jwvr_tasks", "justinwieb": "jwvr_tasks",
    "nova": "nova_tasks", "nova-design": "nova_tasks",
    "fishing": "fishing_tasks", "wiebelhaus-fishing": "fishing_tasks",
    "bass": "bass_tasks", "gus-the-bass": "bass_tasks",
}
BRAND_TO_PROJECTS_DB = {
    "life": "life_projects", "jwvr": "jwvr_projects",
    "nova": "nova_projects", "fishing": "fishing_projects", "bass": "bass_projects",
}

Replace the entire current dict in forge_telegram_inbox_brain.py.

Authoritative property names (post-refactor)

Habits log

  • Title field: Name (just the habit name, no date prefix; cron sets it)
  • Habit (relation -> habit_defs)
  • Date, Quantity (number), Notes (rich_text)
  • Hit (formula: Quantity >= Target qty)
  • Today (formula: visual "✅ 5/15 cups")
  • Week (formula: "X / Y" days)
  • Streak (rollup of Habit.Streak)
  • Target qty, Unit, Week hits, Week goal (rollups; brain doesn't write these)
  • Done? (legacy checkbox; brain MAY write but Hit formula is primary)
  • Calendar Event ID (rich_text; for sync)
  • Calendar Synced (checkbox; for sync)

Habit Definitions

  • Title Name (just the habit name; page icon has the emoji)
  • Category, Cadence (Daily/Weekly/Custom), Active? (checkbox)
  • Cadence target (Daily / X days/week / Weekly / Custom)
  • Goal qty (number), Unit (rich_text)
  • Reminder (rich_text; display only, no notification system yet)
  • Color (select), Linked Area (relation)
  • Days per week target (number), Days hit this week (number, cron-written)
  • Streak (number, cron-written)
  • On track this week (formula)

All 5 Tasks DBs (identical schemas)

  • Title Name
  • Status (status: Inbox/Next/Doing/Waiting/Done)
  • Priority (select: Urgent/High/Medium/Low)
  • Area (relation → areas)
  • Project (relation → brand's projects DB)
  • Due (date), Scheduled (date), Source (select), Notes, Created, Done At
  • Calendar Event ID (rich_text), Calendar Synced (checkbox)

Inbox

  • Title Name
  • Sorted? (checkbox), Tags (multi_select), Source, Notes
  • Destination (select: Task/Knowledge/Reading/Project/Calendar/Archive/?)
  • Suggested Brand (select)
  • Brain Notes (rich_text)

Brain changes required (in priority order)

1. Update NOTION_DBS map (section above)

2. Fix tool_query_notion (live bug)

payload["filter"] = {"property": "Name", "title": {"contains": filter_text}}
# Title field is named "Name" everywhere now.

3. Refactor tool_create_task

def tool_create_task(task, brand="life", priority="Medium", due=None,
                    scheduled=None, notes=None, tags=None):
    db_key = BRAND_TO_TASKS_DB.get(brand.lower(), "life_tasks")
    props = {
        "Name": {"title": [{"type": "text", "text": {"content": task[:2000]}}]},
        "Priority": {"select": {"name": priority}},
        "Source": {"select": {"name": "Brain"}},
    }
    if due:
        props["Due"] = {"date": {"start": due}}
    if scheduled:
        props["Scheduled"] = {"date": {"start": scheduled}}
    if notes:
        props["Notes"] = {"rich_text": [{"type": "text", "text": {"content": notes[:1900]}}]}
    res = n8n("notion-create-page", {"database_id": NOTION_DBS[db_key], "properties": props})

    # If due includes a time, also create calendar event + sync back
    if res.get("ok") and due and "T" in due:
        page_id = res["page"]["id"]
        cal = n8n("create-calendar-event", {
            "summary": task[:200], "start": due, "timeZone": "America/Chicago",
        })
        if cal.get("ok"):
            event_id = cal.get("id") or cal.get("event_id")
            n8n("notion-update-page", {
                "page_id": page_id,
                "properties": {
                    "Calendar Event ID": {"rich_text": [{"type": "text", "text": {"content": event_id}}]},
                    "Calendar Synced": {"checkbox": True},
                }
            })

    return f"Task created in {brand}: {task[:80]}" if res.get("ok") else f"Failed: {res.get('error')}"

4. Refactor tool_log_habit (UPDATE row, don't create)

def tool_log_habit(habit_name, quantity_delta=None, quantity=None, notes=None):
    today = datetime.now(TZ).strftime("%Y-%m-%d")

    # Find Habit Definition by name (with or without leading emoji)
    search = n8n("notion-query-database", {
        "database_id": NOTION_DBS["habit_defs"],
        "filter": {"property": "Name", "title": {"contains": habit_name}},
        "page_size": 1,
    })
    if not search.get("ok") or not search.get("results"):
        return f"Unknown habit '{habit_name}'."
    def_id = search["results"][0]["id"]

    # Find today's row (cron pre-created it)
    today_row = n8n("notion-query-database", {
        "database_id": NOTION_DBS["habits"],
        "filter": {"and": [
            {"property": "Date", "date": {"equals": today}},
            {"property": "Habit", "relation": {"contains": def_id}},
        ]},
        "page_size": 1,
    })

    if not today_row.get("results"):
        # Cron hasn't run or new habit added today; create the row
        # (full path: include Name/Habit/Date)
        return _create_habit_row_today(def_id, habit_name, quantity or 1, notes)

    row = today_row["results"][0]
    page_id = row["id"]

    # Compute new Quantity
    if quantity_delta is not None:
        # Increment: add to existing
        existing_qty = (row.get("properties", {}).get("Quantity", {}) or {}).get("number") or 0
        new_qty = existing_qty + quantity_delta
    elif quantity is not None:
        new_qty = quantity
    else:
        # No qty specified — interpret as "did it"; set to 1 (or goal qty if known)
        new_qty = 1

    update_props = {"Quantity": {"number": new_qty}}
    if notes:
        update_props["Notes"] = {"rich_text": [{"type": "text", "text": {"content": notes[:500]}}]}

    res = n8n("notion-update-page", {"page_id": page_id, "properties": update_props})
    return f"{habit_name}: {new_qty}" if res.get("ok") else f"Failed: {res.get('error')}"

5. Capture persona: brand-routing system prompt

In capture's system prompt, add:

When the user says "Life task X", "JWVR task X", "Nova task X", "Fishing task X",
or "Bass task X", call tool_create_task with brand= the matching brand.
If only "task X" is given (no brand qualifier), use brand="life".

6. New tools (priority order)

tool_create_project(name, brand="life", outcome=None, due=None, area=None)

Creates a Project row in the brand's Projects DB. Returns project URL + id.

tool_create_video_project(name, base_offset_days=0)

Creates JWVR Project + 4 Tasks (Script, Shoot, Edit, Post) linked to it. Tasks get Scheduled offsets: 0d, 7d, 14d, 21d from today + base_offset_days.

def tool_create_video_project(name, base_offset_days=0):
    proj = tool_create_project(name, brand="jwvr",
                               outcome=f"Ship {name} video",
                               due=(datetime.now(TZ) + timedelta(days=21+base_offset_days)).date().isoformat())
    proj_id = proj["id"]
    today = datetime.now(TZ).date()
    stages = [("Script", 0), ("Shoot", 7), ("Edit", 14), ("Post", 21)]
    for stage, offset in stages:
        sched = (today + timedelta(days=offset + base_offset_days)).isoformat()
        n8n("notion-create-page", {
            "database_id": NOTION_DBS["jwvr_tasks"],
            "properties": {
                "Name": {"title": [{"type": "text", "text": {"content": f"{stage}{name}"}}]},
                "Project": {"relation": [{"id": proj_id}]},
                "Scheduled": {"date": {"start": sched}},
                "Priority": {"select": {"name": "Medium"}},
                "Source": {"select": {"name": "Brain"}},
            }
        })
    return f"Created JWVR video project '{name}' with 4 stage tasks."

tool_create_habit_definition(name, goal_qty, goal_unit, cadence="Daily", goal_freq=None, area=None, color=None, reminder=None)

Creates a Habit Definitions row (Active?=true). Tomorrow's instantiate cron picks it up.

tool_log_sleep(source="eight"|"garmin", date_str="last night")

Pulls from Forge Context API (or wellness pollers), creates calendar event with sleep block (colorId=1 for Lavender).

tool_log_time_block(activity, category, hour=None)

For hourly check-in. Creates calendar event for hour ending at hour (default = current hour). Maps category to colorId per lifeos-system-design table.

tool_briefing(window="next_2_hours")

Returns Telegram-formatted summary: upcoming calendar events + tasks due/scheduled in window + outstanding habits today.

tool_followup_unscheduled_tasks()

Pulls all 5 Tasks DBs filtered to (Status != Done AND Due is empty AND Scheduled is empty), returns list. Coordinator uses to ping Justin daily.

tool_weekly_review()

Heavy compose: pull past-7-days calendar events (group by colorId for hour totals), Habits log (week summary), completed Tasks count, Active Projects status. Format as multi-paragraph reply.

Notion-edit helpers (subprocess to forge_notion_api.py CLI, OR import the module)

  • tool_search_notion, tool_create_subpage, tool_archive_page, tool_set_page_icon,
  • tool_create_linked_view, tool_configure_view, tool_favorite_page, tool_move_block,
  • tool_add_comment

These lift directly from the API client. Subprocess cleanest; if shipping bandwidth tight, can defer.

7. Crons to build alongside

Cron Schedule Purpose
forge-telegram-hourly-checkin.timer top of hour 08:00–20:00 CT Push "what did you do this hour?" to Telegram
forge-followup-unscheduled.timer 18:00 CT daily Coordinator pings Justin about today's unscheduled tasks
forge-weekly-review-prep.timer Sun 17:30 CT Pre-stage week's data so review is fast

These are bot-refiner-adjacent; build whenever convenient.

Resumption procedure

After changes ship + tests pass:

sudo systemctl start forge-inbox-capture.service forge-lifeos-coordinator.service
sudo systemctl status forge-inbox-capture.service forge-lifeos-coordinator.service --no-pager

Smoke tests via Telegram: 1. To @forge_inbox_capture_bot: "Life task buy macro lens" → expect Notion row in Life Tasks 2. To @forge_inbox_capture_bot: "drank 5 cups" → expect Hydrate Quantity to update 3. To @forge_lifeos_coordinator_bot: "create JWVR video project: Quest 4 launch" → expect Project + 4 stage tasks 4. To @forge_lifeos_coordinator_bot: "what's on my plate today" → expect briefing reply

If any fail, capture the response and ping Justin.

References

[Claude Code, Notion architect → bot-refiner handoff, 2026-04-29]