Skip to content

Bot Tool Additions, 2026-04-29 (for bot-refiner)

Handoff from Notion architect (notion-architect_Opus47) to bot-refiner (bot-refiner_Opus47).

Notion has been refactored. Brain code (forge/scripts/forge_telegram_inbox_brain.py + persona facade forge/scripts/forge_telegram_brain.py) needs the changes below before capture + coordinator services can resume.

Source docs: notion-redesign-2026-04-29, notion-audit-2026-04-29.

Status: services PAUSED

forge-inbox-capture.service and forge-lifeos-coordinator.service have been stopped. Justin will resume them once these brain fixes ship and you confirm.

What Notion looks like now (after refactor)

  • All title fields are renamed to Name (was Task, Title, Project, Idea, Pillar, Brand, Goal, Entry, Date etc.)
  • Per-brand Tasks DBs: Life Tasks, JWVR Tasks, Nova Tasks, Fishing Tasks, Bass Tasks (all identical schema)
  • Per-brand Projects DBs: Life Projects, JWVR Projects, Nova Projects, Fishing Projects, Bass Projects (all identical schema)
  • New JWVR pipeline DBs: Series, Deals, Deliverables, Derivatives
  • New Nova DBs: Clients
  • New top-level DB: Habit Definitions (separate from Habits log)
  • New Inbox properties: Destination, Suggested Brand, Brain Notes
  • Status enums expanded on Reading, Sponsors, Production, Projects (see redesign doc Sections 7, 12, 3)
  • Sponsors title field renamed from Brand to Name
  • Areas: Color property dropped (was misnamed)
  • Daily Log: relations added to Tasks (multi), Habits, Wellness Daily

The new DB IDs land in ~/.claude/projects/-home-justinwieb-forge/memory/reference_notion_scaffold.md. Pull from there at brain start, do not hardcode IDs in forge_telegram_inbox_brain.py (load them from a Forge config file or read the scaffold file).

Brain changes required (in priority order)

1. Fix tool_query_notion filter shape (live bug)

File: forge/scripts/forge_telegram_inbox_brain.py line ~346.

Current (broken):

payload["filter"] = {
    "property": "title",
    "rich_text": {"contains": filter_text}
}

Replacement:

payload["filter"] = {
    "property": "Name",
    "title": {"contains": filter_text}
}

After title-field renames every DB has a Name title. The hard-coded property name now works for all DBs.

2. Fix tool_update_task filter shape + brand routing

File: forge/scripts/forge_telegram_inbox_brain.py line ~370.

Current (broken):

search_res = n8n("notion-query-database", {
    "database_id": NOTION_DBS["tasks"],
    "page_size": 3,
    "filter": {"property": "Task", "rich_text": {"contains": task_query}},
})

Replacement:

def tool_update_task(task_query, status=None, priority=None, due=None, brand="life"):
    db_key = f"{brand}_tasks"  # life_tasks / jwvr_tasks / nova_tasks / fishing_tasks / bass_tasks
    search_res = n8n("notion-query-database", {
        "database_id": NOTION_DBS[db_key],
        "page_size": 3,
        "filter": {"property": "Name", "title": {"contains": task_query}},
    })

Brand becomes a required arg (default life). Coordinator brain can also call tool_today_across_brands (see #11) when it needs to search all five.

3. Update NOTION_DBS map

Replace the current dict with the post-refactor IDs. Authoritative source: reference_notion_scaffold.md after Phase G runs. Keys to expect:

NOTION_DBS = {
    # Top-level singletons
    "inbox": "...",
    "knowledge": "...",
    "reading_watch": "...",
    "areas": "...",
    "goals": "...",
    "habits": "...",         # log of daily ticks
    "habit_defs": "...",     # NEW: definitions DB
    "daily_log": "...",
    "wellness_daily": "...",

    # Per-brand Tasks (5 DBs, NEW for nova/fishing/bass; renamed for life/jwvr)
    "life_tasks": "...",
    "jwvr_tasks": "...",
    "nova_tasks": "...",
    "fishing_tasks": "...",
    "bass_tasks": "...",

    # Per-brand Projects (5 DBs)
    "life_projects": "...",
    "jwvr_projects": "...",
    "nova_projects": "...",
    "fishing_projects": "...",
    "bass_projects": "...",

    # JWVR pipeline
    "jwvr_pillars": "...",
    "jwvr_content_ideas": "...",
    "jwvr_production": "...",
    "jwvr_published": "...",       # was MISSING from old map; now wired
    "jwvr_series": "...",          # NEW
    "jwvr_sponsors": "...",
    "jwvr_deals": "...",           # NEW
    "jwvr_deliverables": "...",    # NEW
    "jwvr_derivatives": "...",     # NEW

    # Nova
    "nova_clients": "...",         # NEW
}

The tasks and old jwvr_tasks keys are GONE. Anything that referenced NOTION_DBS["tasks"] becomes NOTION_DBS["life_tasks"].

4. Refactor tool_create_task brand routing

File: line ~191.

Current:

def tool_create_task(task, priority="Medium", due=None, brand="lifeos"):
    db_key = "jwvr_tasks" if brand.lower() in ("jwvr", "justinwieb-vr", "vr") else "tasks"
    props = {
        "Task": {"title": [...]},
        ...
    }

Replacement:

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

def tool_create_task(task, priority="Medium", due=None, brand="life"):
    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}}
    res = n8n("notion-create-page", {"database_id": NOTION_DBS[db_key], "properties": props})
    if res.get("ok"):
        return f"Task created in {brand}: {task[:80]}"
    return f"Task create failed: {res.get('error', 'unknown')}"

Note: title field is now Name (not Task).

5. Fix tool_save_to_inbox title field name

Already uses Title (line 172). After refactor it should write to Name instead. Same fix in tool_save_knowledge (line 280).

6. tool_log_habit (TickTick-style, with cron-instantiated rows)

The Habits log DB has been restructured 2026-04-29 to support TickTick-style tracking: - Title field: Entry -> Name (already covered by general rename) - Habit was a select, now a relation to the new habit_defs DB - New Quantity (number) field for partial completion (e.g., 5 of 8 cups of water) - New cron forge-habits-instantiate.timer (04:00 daily) creates one Habits row per Active Habit Definition for today, idempotent - Habit Definitions seeded with starter set + Goal Frequency property (Daily, X days/week, Weekly, Custom)

Critical: brain should NOT create new Habits rows. The cron already created today's rows. Brain UPDATES the existing row (find by Habit relation + Date, then PATCH).

New tool_log_habit(habit_name, done=True, quantity=None, notes=None):

def tool_log_habit(habit_name, done=True, quantity=None, notes=None):
    today = datetime.now(TZ).strftime("%Y-%m-%d")
    # Look up habit definition by name
    search = n8n("notion-query-database", {
        "database_id": NOTION_DBS["habit_defs"],
        "filter": {"property": "Name", "title": {"equals": habit_name}},
        "page_size": 1,
    })
    if not search.get("ok") or not search.get("results"):
        return f"Unknown habit '{habit_name}'. Add it to Habit Definitions first."
    habit_def_id = search["results"][0]["id"]

    # Find today's existing log row (created by the 04:00 cron)
    existing = n8n("notion-query-database", {
        "database_id": NOTION_DBS["habits"],
        "filter": {
            "and": [
                {"property": "Date", "date": {"equals": today}},
                {"property": "Habit", "relation": {"contains": habit_def_id}},
            ]
        },
        "page_size": 1,
    })

    update_props = {"Done?": {"checkbox": done}}
    if quantity is not None:
        update_props["Quantity"] = {"number": quantity}
    if notes:
        update_props["Notes"] = {"rich_text": [{"type": "text", "text": {"content": notes[:500]}}]}

    if existing.get("ok") and existing.get("results"):
        # UPDATE the cron-created row
        page_id = existing["results"][0]["id"]
        res = n8n("notion-update-page", {"page_id": page_id, "properties": update_props})
        action = "updated"
    else:
        # FALLBACK: cron hasn't run yet (or new habit added today). Create row.
        update_props.update({
            "Name": {"title": [{"type": "text", "text": {"content": f"{today}, {habit_name}"}}]},
            "Habit": {"relation": [{"id": habit_def_id}]},
            "Date": {"date": {"start": today}},
        })
        res = n8n("notion-create-page", {"database_id": NOTION_DBS["habits"], "properties": update_props})
        action = "created"

    if res.get("ok"):
        marker = "✓" if done else "✗"
        qty_str = f" ({quantity})" if quantity is not None else ""
        return f"Habit {action}: {habit_name}{qty_str} {marker}"
    return f"Habit log failed: {res.get('error', 'unknown')}"

Optional optimization: cache habit_defs query results at service start to avoid one round-trip per habit log. Not required for correctness.

7. New tool: tool_sort_inbox(count=5)

For the Telegram-brain Inbox sort flow (Section 5 of redesign).

def tool_sort_inbox(count=5):
    # 1. Query Inbox where Sorted? = false, ordered by Created asc, page_size=count
    # 2. For each row, call Claude with system prompt:
    #    "Decide destination for this captured note. Output JSON: {"destination": "...",
    #     "brand": "...", "title": "...", "priority": "...", "area": "..."}"
    #    Allowed destinations: Task, Knowledge, Reading, Project, Calendar, Archive, ?
    #    Allowed brands: life, jwvr, nova, fishing, bass (when destination requires brand)
    # 3. Update each Inbox row with Destination, Suggested Brand, Brain Notes
    # 4. Return formatted Telegram-friendly numbered list ready for Justin to approve

Used by coordinator (and optionally capture, as a low-friction alternative).

8. New tool: tool_execute_inbox_sorts(approved_ids=None)

def tool_execute_inbox_sorts(approved_ids=None):
    # If approved_ids is None, execute all rows where Destination is set and not "?"
    # For each: create target row in correct DB with fields from Brain Notes,
    # mark Inbox row Sorted? = true, archive Inbox original (move to archive section)
    # Return summary: "Sorted 4 items: 2 Tasks (Life, JWVR), 1 Knowledge, 1 Calendar"

9. New tool: tool_today_across_brands()

def tool_today_across_brands():
    # Query all 5 Tasks DBs in parallel, filter Today = true, merge results
    # Return formatted list grouped by brand, sorted by Priority within brand
    # "Today: 7 tasks across 4 brands. Life: 3, JWVR: 2, Nova: 1, Fishing: 1"

10. New tool: tool_create_published(production_query, url, posted_date, platform)

def tool_create_published(production_query, url, posted_date, platform):
    # Find Production row by name match
    # Create Published row with Source Production relation
    # Optionally update Production status to Published

11. New tool: tool_create_deliverable(deal_query, type_, due, ...)

For the Sponsors + Deals + Deliverables three-DB pipeline.

Links a Task to a Project within the same brand. Useful when brain decides during sort that an inbox item should be a Task AND should belong to an existing Project.

Schema reference (post-refactor; for tool authoring)

Common shape: every per-brand Tasks DB

Name (title), Status (status), Priority (select), Area (relation -> Areas),
Project (relation -> brand's Projects DB), Due (date), Scheduled (date),
Today (formula), Source (select), Notes (rich_text), Created (created_time),
Done At (date)

Common shape: every per-brand Projects DB

Name (title), Status (status), Area (relation -> Areas), Outcome (rich_text),
Start (date), Due (date), Goal (relation -> Goals), Tasks rollup, Notes (rich_text), Created

Inbox additions

Destination (select), Suggested Brand (select), Brain Notes (rich_text)

Coordination

  • Justin keeps services paused until you confirm fixes are in. Don't restart forge-inbox-capture.service or forge-lifeos-coordinator.service from your end; let Justin do it after a smoke test.
  • Coordinate with this Notion architect session if any schema turns out wrong; we can patch live DBs again.
  • After your changes ship + services resume, do a dry capture (Telegram message to capture bot) and verify it lands in Inbox with the new schema.

Eval harness

Run forge/scripts/forge_eval_harness.py --full --no-write after edits. The doctrine + naming + variable checks should pass. New checks worth adding (separate task for Phase 4.5): brain-DB-key drift detector that verifies NOTION_DBS keys against the canonical scaffold file.

ADDENDUM 2026-04-29: full-Notion brain capability

A unified Notion client now exists at forge/scripts/forge_notion_api.py (see notion-api-playbook). It wraps both the public API and the internal cookie-auth API behind one NotionAPI class. The lifeos coordinator brain should be able to use it for full-workspace edit power, not just the row-CRUD tools above.

How brain calls it

Two acceptable paths: 1. Subprocess the CLI (cleaner separation): subprocess.run(["forge_notion_api.py", "create-view", ...], ...). Each invocation is an independent process; failures don't take the brain down. 2. Import as module (faster, less overhead): from forge_notion_api import NotionAPI; api = NotionAPI(); api.search(...). Brain holds the client instance for the conversation. Slightly more coupling but minimal HTTP latency.

I recommend path 2 for the coordinator (the bot is long-running and the import is cheap; subprocess overhead matters at high tool-call rate).

New brain tools to add

Add these to the coordinator persona's tool surface (NOT capture, which stays minimal). Each is a thin wrapper around a forge_notion_api method.

Tool Wraps What it does
tool_search_notion(query, type=None, limit=10) api.search() Find pages/DBs by title text
tool_create_subpage(parent_page_id, title, content_blocks=None, icon=None) api.page_create(parent_type='page_id', ...) Create a regular page under a parent page
tool_archive_page(page_id) api.page_archive(page_id) Archive (soft-delete) a page
tool_unarchive_page(page_id) api.page_unarchive(page_id) Restore an archived page
tool_set_page_icon(record_id, emoji_or_url) api.set_icon() Static icon for any page or DB
tool_set_page_cover(record_id, image_url) api.set_cover() Set cover image
tool_add_comment(parent_page_id, text) api.comment_create() Add a top-level comment to a page
tool_list_comments(block_id) api.comment_list() Read existing comments
tool_append_to_page(page_id, blocks) api.block_append_children() Add new content blocks to end of page
tool_get_page(page_id) api.page_get() Fetch full page metadata + properties
tool_create_linked_view(page_id, db_id, name, type='table', filter=None, group_by_prop=None) api.create_linked_view() Add a filtered linked database view to a page
tool_configure_view(view_id, name=None, type=None, filter=None, sort=None) api.configure_view() Edit a view's filter/sort/group/layout
tool_favorite_page(page_id) api.favorite_page() Pin to sidebar Favorites
tool_move_block(block_id, parent_page_id, after_block_id=None, before_block_id=None) api.move_block() Non-destructive reorder

Optional: high-level "redesign" tool

For ambitious user requests like "rearrange JWVR teamspace", expose:

def tool_redesign_workspace(plan_description):
    """Justin describes a high-level Notion redesign in natural language;
    spawn a fresh Claude worker with the playbook + tool surface to execute it."""
    # Wraps spawn_remote_session with a pre-loaded Notion playbook context

This is a meta-tool: it triggers a /spawn of an Opus session with the playbook auto-loaded and a specific design brief. Lets Justin say "redesign Notion to X" via Telegram and the coordinator delegates.

Deferred / explicit out-of-scope

These are NOT on the brain's tool surface: - Cookie rotation (Justin manual, see playbook) - Direct integration token usage (brain uses the existing n8n webhooks for public API; the unified NotionAPI class can sub for n8n webhooks if desired, but bot-refiner can keep n8n for now to avoid double-credential use) - Anything destructive without explicit confirmation (delete entire DB, archive parent page with children)

Testing checklist for the new tools

After bot-refiner ships, test from a Telegram message to the coordinator: 1. "Search for VR review pages" → should call tool_search_notion("VR review", type='page') 2. "Create a sub-page under Life called 'Today's reflections'" → tool_create_subpage(life_id, "Today's reflections") 3. "Add a Today view of Life Tasks to my HQ" → tool_create_linked_view(hq_id, life_tasks_id, ...) 4. "Pin the JWVR page to favorites" → tool_favorite_page(jwvr_id) 5. "Comment on Personal HQ saying ...weekly review done" → tool_add_comment(hq_id, "...")

If any fails, the failure mode should be a clear error message; brain shouldn't crash.

[Claude Code, Pure Phoenix Notion architect, 2026-04-29]