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 AtCalendar Event ID(rich_text),Calendar Synced(checkbox)
Inbox¶
- Title
Name Sorted?(checkbox),Tags(multi_select),Source,NotesDestination(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¶
- lifeos-system-design-2026-04-29 — full vision + workflows + categories table
- notion-api-playbook — how to call Notion (forge_notion_api.py)
- notion-redesign-2026-04-29 — final Notion structure
- reference_habits_cron — habit cron internals
- reference_telegram_fleet — bot fleet topology
[Claude Code, Notion architect → bot-refiner handoff, 2026-04-29]