NexBoard Build Journal — Mon 18 May – Sun 24 May 2026

What shipped

  • Vault semantic search — Nigel can now search the vault by meaning, not just keywords. ChromaDB (embedded) + nomic-embed-text via Ollama; nightly incremental reindex; fires when wiki keyword matching returns ≤1 result
  • Dashboard widget framework — Widget ABC with contract enforcement at class-definition time, per-widget HTML fragment endpoints, isolated CSS. Three widgets live: OpenCommitments, KanbanBoard (Done column capped at 10 most-recent), WinsThisWeek
  • Nigel prompt token overhaul — compact ledger (3,250 → 1,830 chars, 44% reduction), smart history truncation by turn type, context deduplication across kanban/loops/semantic sources, prompt section reorder for cache efficiency; Haiku model routing handles 83% of API calls
  • Subprocess dispatch auto-recoverycheck_task_completion.py now detects subprocess slips stranded in InProgress after 30 minutes and auto-recovers: routes to Complete if a deliverable exists, back to Approved for retry if not
  • Codebase audit: 102 findings, 10 fixed — full audit run (1 critical, 28 high, 43 medium, 30 low); 225/225 tests green; .env.example regenerated with REQUIRED/SECRET/Optional annotations

The interesting problem

Nigel was treating historical documents as current state — closed commitments and cancelled events kept appearing as open and active.

Three stale snapshot layers sit between live data and Nigel’s briefing context. Journal/_open-loops.md regenerates only at 06:00 and 20:00; a commitment resolved at 10:00 appears open until evening. workspace-cos/memory/YYYY-MM-DD.md — LLM-written prose, also updated twice daily — mentions commitments by ID; closing one doesn’t update the file. workspace-cos/memory/YYYY-MM-DD-calendar.md is written at 05:45 and never refreshed; events cancelled later persist in the snapshot all day.

What pointed to the root cause: identical symptoms across commitments and calendar, both injected as prose documents. The LLM wasn’t wrong about the data it received — it was getting old data.

The fixes were applied at injection rather than at the source files: _open-loops.md now regenerates whenever create_commitment, resolve_commitment, or archive_old_resolved saves. A new scrub_closed_commitment_refs() in memory_filter.py marks closed IDs in prose memory files with [STATUS: now closed, ignore from action list] before injection, leaving the source files intact. The morning brief now refreshes the calendar snapshot inline before loading it.

Decision of the week

Routing slips: Pending or Approved by default?

When Nigel delegates a task, it writes a routing slip. The original code wrote to Pending/ — requiring manual approval before dispatch. The CoS design gives Nigel autonomous delegation authority, so this was wrong from the start.

Changed nexboard_cli.py to write directly to Approved/. Pending/ is still available if Grant explicitly says “create as pending” or “I’ll review first.” Both repo and live install must be kept in sync — the live copy is what actually runs.

What surprised me

read_memory_files(days=2) was called on every interactive Nigel invocation — loading ~6,000 characters of memory files on every message. The data was never used. The prompt builder has a branch: if the compact ledger is present, use it; otherwise fall back to raw memory files. The compact ledger always returns content, so the else branch never executes. The function call, the 6,000 chars of I/O, and the memory parameter in _build_interactive_prompt’s signature were all dead code.

Removed the call. Zero behaviour change.

What’s next

The vault RAG trigger heuristic needs tuning — semantic search currently fires on queries that wiki search handles fine, adding latency for no gain. The morning brief architecture works but the daily plan output isn’t yet wired to goals and active commitments, so CoS context is broad rather than targeted.


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