NexBoard Build Journal — Mon 29 Jun – Sun 05 Jul 2026

What shipped

  • The activity stream — an append-only, per-client log of everything that happens: goals, tasks, subtasks, commitments, sprints, calendar events, slips, promises. Each event points back to its source record and stores which client it belongs to, resolved at write time.
  • A coverage audit of that stream, then the fixes it surfaced: the mobile “mark done” path now emits, cascaded subtask completions emit, and a subtask inherits its parent goal’s client instead of falling into Unallocated.
  • Client management on the timesheet — edit a client’s details, archive one (blocked if it still has open work), unarchive. No delete; archiving keeps the history.
  • An open-item drill-in: the “3 open” pill on a client expands to show the actual tasks, goals, and commitments blocking its archive, each linking to where it lives.
  • Earlier in the week, email triage Pass 3 — pulling commitments out of Gmail threads.

The interesting problem

The activity stream is append-only and idempotent, which sounds simple until you wire it to a task board that re-scans constantly. Every scan normalises frontmatter — if a task’s stored status doesn’t match its derived stage, it gets rewritten. If I’d hung the “task completed” emit off that normalise step, every scan would re-fire the same completion forever.

The fix is two-part. Hooks live only at explicit actions — the toggle, the stage change, the field edit — never at the scan-time normaliser. And every event gets a deterministic id:

event_id = sha1(f"{source}:{id}:{verb}:{disc}").hexdigest()[:12]

Same transition, same id, so a re-run is a no-op. That let me add an emit to the mobile completion path — which bypassed the board’s toggle entirely and was silently dropping real completions — with no risk of double-counting against the path already covered. Read the old stage before the write, compare, emit only on a genuine change.

Decision of the week

Capture everything, drop nothing. When an event can’t be attributed to a client — no goal, no tag — it doesn’t get discarded, it lands in an unattributed bucket the UI labels “Unallocated”. The alternative was to only log attributed work and skip the rest, which keeps the store clean but means the moment something isn’t tagged, it’s gone. The tradeoff is an Unallocated pile that needs manual triage. I’ll take a pile I can sort over silent data loss. Client is resolved at emit — the goal’s client wins, then the item’s own tag, then Unallocated — so reassigning later is a field change, not a rewrite.

What surprised me

I leaked test data into the real vault. Twice. My verification scripts pointed the database at a temp file, which I assumed was isolation enough — but the emit path resolves its write directory from a separate module global I never redirected. So the goal rows went to the throwaway database while the activity events wrote straight into real client folders. I found a stray client directory and four phantom “Acme goal” events sitting on the Powercor timesheet, both needing manual cleanup. Redirecting one dependency isn’t a sandbox. Every path the code actually touches needs pointing at the temp location, and the only way I found out which ones didn’t was to leak.

What’s next

Finish the audit’s remaining tier — emit events for milestone and metric-progress changes, and deep-link the last couple of event kinds that still land on a section page instead of the exact record. Then widen the “move existing events” reunite so it works client-to-client, not only out of Unallocated.


Building NexBoard — a self-hosted AI Chief of Staff, built entirely with Claude Code.