NexBoard Build Journal — Mon 26 May – Sun 01 Jun 2026
What shipped
- Mobile PWA at
/mobile— dedicated mobile view with passkey auth (WebAuthn), chat interface, scratchpad, task cards with due dates and priority, and a home screen icon - Commitment architecture v2 — single registry model; post-mutation triggers full re-fetch via
_loaded.delete(panel)+loadPanel()instead of optimistic local patching - Nigel timezone fix —
calendar_prep.pyand morning brief now injectdatetime.now(ZoneInfo("Australia/Adelaide"))into the LLM prompt; Nigel was previously showing all calendar times in UTC - Goal-task linking + TaskNotes-native wikilinks — 30 tasks migrated from
project: slugtoprojects: ["[slug](/slug/)"]; vault frontmatter is the source of truth, the DB junction table is a read cache rebuilt on each scan - CoS wiki indexer overhaul — keyword gate removed;
watch_keywordsdemoted from content filter to LLM hint; daily notes window extended from 14 to 30 days
The interesting problem
calendar_prep.py had a bug that meant it had never successfully written anything — for its entire existence.
The script calls run_cos_subprocess, which runs claude --print with cwd=VAULT_CWD (the vault root). The prompt contained a relative path:
Write to workspace-cos/memory/2026-05-26-calendar.md
Claude resolved that against the CWD, writing somewhere inside the vault. The Python code reads from COS_WS (the agentboard workspace directory). Two completely different trees. File never found.
The fix:
# In _build_prompt():
f"Write to {COS_WS}/memory/{today.isoformat()}-calendar.md"
COS_WS is a Path object — in an f-string it renders as the full absolute path. The rule now: any run_cos_subprocess prompt that tells Claude to write a file must inject an absolute path from Python. Never rely on the model to resolve a relative path when the working directory is somewhere else.
morning_brief._load_calendar_prep() had been silently returning nothing on every run since the feature shipped. It only surfaced when I went looking for why morning briefs had no calendar content.
Decision of the week
Wiki indexer: keyword gates vs hints
wiki-sources.yaml used watch_keywords as content filters — only daily note lines matching a keyword were fed to the LLM. The intent was relevance filtering. The effect was that anything written in a slightly different way got silently dropped.
Changed to: include all content from watch_paths files (up to 20 most-recent, 600 chars each), and demote watch_keywords to a “Recent mentions of key topics” block in the LLM prompt. The model decides what’s relevant rather than a regex.
More tokens per indexer run is the cost. The keyword regex is gone.
What surprised me
The lib/frontmatter.py parser issue. parse_frontmatter() is a flat line-by-line parser — it handles key: value scalars only. Any YAML list block returns '' silently. After migrating tasks to the wikilink project format:
projects:
- "[nexboard-cos](/nexboard-cos/)"
_scan_tasks read projects: '' on every task. The goal_tasks junction table stayed empty. Nothing errored.
I expected the parser to complain. Instead it quietly dropped the field on every task file in the vault. The fix was a targeted yaml.safe_load pass for list fields inside _scan_tasks_from_file, rather than replacing parse_frontmatter globally — its flat-string contract is relied upon in too many other places.
What’s next
Morning briefs now get calendar content — next is wiring goals and active commitments into the daily plan prompt so CoS output is actually useful. The passkey-gated mobile PWA is live; before it replaces the dashboard for day-to-day use it needs the voice-to-task path from Nigel.
Building NexBoard — a self-hosted AI Chief of Staff, built entirely with Claude Code.
Comments