NexBoard Build Journal — Mon 01 Jun – Sun 07 Jun 2026
What shipped
- Goals project detail page rebuilt from scratch: editable calendar event badges, working filter pills, subtask support via
blockedByfrontmatter, task table headers. - Commitments promote-to-task flow now offers a goal picker; resolved or cancelled commitments mark the linked task done automatically.
- Notes watcher: new cron script that scans the vault notes folder and daily journal for commitment patterns, including an implicit detection path using first-person verb patterns (
I need to,I'll,Need to fix). - All six agent workspaces redesigned to a three-file SOUL/TOOLS/SKILLS structure.
- Removed all OpenClaw binary dependencies from the dispatch pipeline; session cost tracking now reads Claude’s native JSONL format.
The interesting problem
The completion checker was silently dying mid-run every pass, leaving tasks stranded in InProgress indefinitely. The log showed nothing alarming. The task just sat.
The problem was in _parse_dispatched_after(). Most vault files store dispatched_at as a quoted string: "2026-06-01T08:00:43Z". But ruamel.yaml promotes unquoted ISO 8601 timestamps to native Python datetime objects automatically. The function called strptime() on the return value and got TypeError: strptime() argument 1 must be str, not datetime.datetime. The crash happened inside a broad try/except that logged at WARNING — easy to miss if you’re not tailing the log actively.
Every task dispatched from a file where ruamel had auto-promoted the timestamp was permanently stranded. The fix:
if hasattr(dispatched_at_val, "timestamp"):
if dispatched_at_val.tzinfo is None:
dispatched_at_val = dispatched_at_val.replace(tzinfo=timezone.utc)
return dispatched_at_val.timestamp()
Check for a native datetime before reaching for strptime(). One task had been stuck for two days before I caught it — manually recovered, then patched. The broader note: ruamel’s type promotion is silent and applies to any unquoted ISO field in frontmatter, so due, scheduled, created_at all need the same guard if they feed into date parsing.
Decision of the week
The agent workspace redesign settled on strict file separation: SOUL.md holds identity and decision rules only, TOOLS.md holds pure executable reference with no narrative, AGENTS.md is the session checklist. The previous approach — everything in one long context document — meant agents could implicitly deprioritize either the rules or the tool syntax depending on what the model attended to in a long sequence.
The cost of the split is more files to maintain. The benefit is that each file is short enough to read at a glance and has no job confusion. Whether that holds when the workspace grows is the open question.
What surprised me
The frontmatter parser in lib/frontmatter.py lowercases every key:
fm[k.strip().lower()] = str(v.strip())
fm.get("googleCalendarEventId") always returns None. The correct lookup is fm.get("googlecalendareventid"). This parser is used everywhere in the app. There’s no error, no log line — .get() on a missing key is silence.
I found this while debugging calendar badges that wouldn’t persist on page reload. The field was written correctly to the vault file. The parser silently threw it away by case mismatch. I’d written three different fixes before checking the parser itself.
What’s next
The notes watcher implicit detection is catching too many planning-mode lines from daily journal entries — the first-person pattern filter needs a tighter stopword list. Once that’s stable, the morning briefing context needs to pull from notes-derived commitments so the day starts with an accurate picture of what was written down yesterday, not just what was formally scheduled.
Building NexBoard — a self-hosted AI Chief of Staff, built entirely with Claude Code.

Comments