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 fixcalendar_prep.py and morning brief now inject datetime.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: slug to projects: ["[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_keywords demoted 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.