NexBoard Build Journal — Mon 11 May – Sun 17 May 2026
What shipped
- Proactive Nigel —
proactive_nigel.pyruns 3× daily (09:00, 13:00, 16:30 weekdays); gathers context in Python, fires one LLM call, sends a 💡 Telegram nudge if there’s something actionable; suppresses when Grant is recently active or the nudge overlaps prior recent content - Promise tracking layer —
commitments.jsonstate file with full lifecycle (open → fulfilled/broken/cancelled);/promiseand/promisesTelegram commands; Haiku-driven extraction from#commitmenttags in daily notes; overdue markers injected into the open loops block - Wiki indexer + operational ledger live — nightly cron generates 9 wiki pages from the vault plus a compact operational ledger;
wiki_reader.pymatches 0–3 pages per query from message keywords - CoS security hardening —
_UNTRUSTED_PREAMBLEconstant injected at the top of every prompt containing external content;run_cos_subprocess()switched from passing the prompt as a CLI flag to stdin, so the full prompt no longer appears in the process list _dispatch_to_cos()quarantine chokepoint — any routing slip withassigned: cosis unconditionally archived with a CRITICAL log entry and Telegram alert, regardless of how the slip was created
The interesting problem
vault_reader.py, activity_ledger.py, and routing_slip.py — all in dispatch/cos/lib/ — were computing the workspace path using the wrong parents[] index.
dispatch/cos/lib/ sits four levels deep from the repo root. Building that root with Path(__file__).resolve().parents[]:
parents[0] # dispatch/cos/lib/
parents[1] # dispatch/cos/
parents[2] # dispatch/
parents[3] # repo root ✅
parents[4] # repo's parent directory ❌
All three files used parents[4]. Every path built from there landed one directory above the repo — resolving to a sibling path that doesn’t exist inside the project. Two months of operational data (activity ledger, state files, memory, wins) accumulated silently in the wrong place. Nothing raised an error; reads returned empty results.
Discovery came when I went looking for why the wins block was always empty despite weeks of recording wins through Telegram.
The rule going forward: files in dispatch/cos/lib/ use parents[3]. Scripts one level shallower in dispatch/cos/ use parents[2].
Decision of the week
Quarantine or raise for assigned: cos routing slips?
When dispatch sees assigned: cos, two options: raise an exception and leave the slip for manual cleanup, or unconditionally move it to quarantine and alert.
Went with quarantine. Nigel can write vault files directly via Bash tools, which bypasses any creation-time Python guard. The dispatch chokepoint is the only layer that covers all slip-creation paths. An exception leaves the slip in InProgress — visible, blocking, triggering stale-detection retries. Quarantine is idempotent and leaves a clear audit trail.
The source field on the slip is not trusted for gating — a confused Nigel could write source: cron on a self-created slip.
What surprised me
claude --print returns returncode 0 even when every MCP tool call in the session is denied. The model responds with prose explaining it needs permission, but the subprocess sees a clean exit code. Before this was caught, the morning brief, email triage, and calendar prep were all running — silently doing nothing — because their Gmail and Calendar calls never reached the MCP server.
The diagnostic signal: run with --output-format json and check the permission_denials field in the output.
What’s next
The promise tracking layer needs wiring into the morning brief — overdue commitments should surface in the daily plan, not just the open loops block. The wiki indexer is running but watch_paths needs auditing; several entries had missing vault prefixes and indexed nothing.
Building NexBoard — a self-hosted AI Chief of Staff, built entirely with Claude Code.
Comments