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(wasTask,Title,Project,Idea,Pillar,Brand,Goal,Entry,Dateetc.) - 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
BrandtoName - Areas:
Colorproperty 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):
Replacement:
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.
12. New tool: tool_link_task_to_project(task_query, project_query, brand)¶
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¶
Coordination¶
- Justin keeps services paused until you confirm fixes are in. Don't restart
forge-inbox-capture.serviceorforge-lifeos-coordinator.servicefrom 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]