Public presentation layer — project showcase, infrastructure dashboard, curated changelog, and internal wiki.
ABOUT
FEATURES
SHIPPED
- ✓Field Station intelligence dashboardMAY 2026
- ✓Research briefing viewJUL 2026
- ✓ballot 2026 voting guideOCT 2026
- ✓Ballot guide, multi-areaOCT 2026
Public Nov 3, 2026 ballot guide across Hendricks (Lincoln, Washington) and Marion (Pike, Lawrence): 77 source-cited decisions, ask-for-your-ballot requests, tap-to-pick, and Discord alerts for reader feedback.
CHANGELOG
UNDERSTORY-LABS-SITEfeaturetoolingbrainstorm
Art Studio — own-art pipeline, layered hero behind a flag, usability pilot
Features
- Art Studio built — a private learning home base for human-made site art (Procreate for every transparent export, Canva Free for mockups, no generative AI)
- 30-lesson quilt in six blocks, 76-entry free resource library (49 vetted, 27 candidates), mood board, style sheet and critique tab in one claude.ai page
/studiocommand —findsearches museum APIs and named illustrators' sites by source tier,keep/discard/status/sync/feedbackmanage the Tray and the page- Layered hero shipped behind
hero-art.json(enabled:false) — 4800x3000 stage, 4:5 story zone, 44 px hotspots,?calibrateoverlay; the live site is unchanged (PR #13) - Studio v9 usability layer — Start here tab, numbered lessons with Next up, search across lessons, guides, library and an 18-term glossary, ten-item try-it checklist, durable feedback box
- UI shipping standard drafted from the pilot — a first-time user can answer five questions without asking Claude
Bug Fixes
- Studio stuck on "Loading" — db
data()returns frozen objects, so assigningx.idthrew; copying withObject.assign({}, d.data())fixed it - Checklist count stayed "0 of 10" — the refresh guard treated a focused checkbox as a text field
- Lesson goals said 6000x3000 while the steps said 4800x3000; guide described two manifest files where the code reads one
Infrastructure
- Work isolated in worktree
feat/hero-layered-art;images.qualities: [75, 90]added for Next 16 - Leonardo subscription cancelled; its row removed from the resource inventory
- Retired AI icon guide moved to
retired/;docs/icons.mdand a project CLAUDE.md illustration section added
Lessons
- The
protect-generatedhook blocks any path containing/build—build_library.pyhad to becomemake_library.py - Artifact db
data()is read-only; copy before adding fields - Next 16 warns on any
qualitynot listed inimages.qualities - A dirty-region renderer needs its typing guard to exclude checkboxes, or counters go stale until blur
- Features that work in code can still be undiscoverable — clicking the real page found three bugs the code review missed
TODO
- Nicole runs the ten-item checklist, including Ask, photo uploads and Keep/Discard/Pin
- Test the Studio at phone width
- Merge PR #13, then remove the
understory-labs-site-artworktree - Promote the draft shipping standard after she uses the pilot
UNDERSTORY-LABS-SITEfeatureinfrastructure
Ballot guide live-page scrub — 48 rows corrected, court records stored
Features
- Live-page scrub of the public ballot guide complete — every reader-visible field scanned, three read-only verifiers compared cards and findings against claims and sources
- Judgment language about candidates rewritten as description, and 22 flagged or court-record cards now carry a visible source line
- Court-record snapshots stored for Bill, Moore, Johnson, Anderson, Delano, Forestal, O'Connor, Duarte and Rashid — published statements can be re-verified from the repo
- Wiki gains a
decisionspage for the project — identity bar, reporting standard, lens boundary and the scrub's open items in one place
Bug Fixes
- Leaked claim ID removed from the
compfull findings - Wrong facts corrected in
sos,pike-trustee,bsb-lt,treas,comm-1andhd-25cards — interview mislabeled as a debate, a quote that does not appear in the source, a reversed speaker, a search result stated as a fact about the world - Johnson's 2001 and 2011 civil cases reclassified — the case pages carry the middle initial, so they are attributed, not counted
- Methodology copy no longer names Lincoln and Brownsburg on every area (PR #7)
Infrastructure
- 48 rows updated in one transaction after a backup, with further backed-up passes of 1, 4 and 3 rows
- Boilerplate "pages are not stored as snapshots" removed only from claims that now have stored transcriptions
Lessons
- A repeat search can return a different count — Forestal went from 11 to 12 between runs, so the stored result list is the record, not the remembered number
- A claim can understate its own source — "judgment of conviction" hid a jury trial with a split verdict until the case page was opened
- A "name only" label is a hypothesis until the case page is read — both Johnson cases carried the middle initial
- A shared school with a candidate's biography does not clear the civil bar — the rule needs a middle initial, so Anderson stays a count
TODO
- Refresh campaign finance after the Oct 16 pre-election reports
- Replace statute hedges once the enrolled HEA 1033 text is captured
UNDERSTORY-LABS-SITESHIPPEDfeatureinfrastructurelaunch
Ballot guide, multi-area — shipped
Features
- Multi-area ballot guide shipped — 77 source-cited decisions live across Hendricks (Lincoln, Washington) and Marion (Pike, Lawrence), every research row
full - Marion Superior Court retentions (15 judges) and the Court of Appeals retention published under one shared standard for independent reporting, with a pending-discipline flag on one judge
- 18 uncontested races vetted — why each ballot row has one name, the nominee's record, and every audit and court-record result stated with its limits
- "Ask for your ballot" — readers give a county, township or town and ZIP (never a street address) and requests reach Discord within ten minutes
- Tap-to-pick — names under "On the ballot" set the pick directly, in sync with the picker below
Infrastructure
requestfeedback kind added toballot_2026_feedbackwith a widened check constraint and anotified_atcolumn- Alert job on CT 100 posts new rows once to
#ballot-guidethrough its own webhook, secrets in a mode-600 env file written from stdin - Verification ran as read-only agents per group, then a separate fix pass — 8 of 18 uncontested contests came back partly unsupported before correction
Lessons
- A verifier's recommendation can rest on a wrong assumption — excluding AI-assisted court digests did not leave one judge with zero reports, because the outlet's next eligible item took its place
- Drafter-reduced snapshots must say so in their metadata — a transcription labelled "original" was the first thing every verifier flagged
- Image-only PDF pages hide the party's own response — reading them in the viewer changed what a fair summary of the Pike audit said
- PowerShell strips inner quotes from a remote
sshcommand, so a secret has to travel on stdin to a script already on the host - A Vercel preview failing on a Google font module in a file nobody touched was transient — a redeploy built the same commit
TODO
- Refresh campaign finance after the Oct 16 pre-election reports
- Replace statute hedges once the enrolled HEA 1033 text is captured
UNDERSTORY-LABS-SITESHIPPEDfeatureinfrastructure
Ballot 2026 — shipped
Features
- Ballot 2026 ships — private, password-gated voting guide live at
/ballot-2026, covering all 23 decisions on the Nov 3, 2026 ballot - Election-day view at
/ballot-2026/picks— ballot order, mechanics, and saved picks only, built for quick scanning in the booth, not for reading - Live search across titles, candidates, categories, and the full unabridged findings text — not just title matching
- Flag badges now name the specific issue category (Campaign Finance, Conflict of Interest, Extremism/Hate Speech) instead of a generic label
Infrastructure
- Content lives in a dedicated
ballot_2026_decisionsSupabase table on self-hosted Taproot Postgres — RLS enabled with zero policies, deny-all for the public anon key, server-sideservice_roleonly - A scoped
claude_migratorPostgres role (create-only, not superuser) backs future schema changes throughscripts/run-migration.mjs
Lessons
- A
<details>element only auto-opens on a URL fragment when the target is a hidden descendant of it — never when the<details>itself is the fragment target, so "jump to section" links needed a manual.open = truebeforescrollIntoView - Kong evaluates plugins by priority, not declaration order —
ip-restriction(990) runs afterbasic-auth(1001), so a restricted route still shows its public auth prompt even though the actual content stays blocked - CRLF line endings in stored text make a literal
.replace()silently no-op — no error, the "successful" write just doesn't change anything - save-state's own
.env.localhad quietly pointed at a paused cloud Supabase project since the Taproot migration — production was fine, local tooling wasn't
UNDERSTORY-LABS-SITEfeatureinfrastructure
Cross-project todos — wiki page, public API, Current OS card
Features
getAllTodos()utility live — reads allcontent/wiki/**/*.mdxat request time, finds## TODO/## Next Stepssections, parses- [ ]/- [x]items with bold**title**extracted as a separate field- Wiki todos page at
/wiki/todos— stats grid (OUTSTANDING / PROJECTS / COMPLETE), per-project cards with item list; auto-gated by existingproxy.tsHMAC cookie auth, no extra code needed /api/todospublic endpoint live — CORS-open, no auth required; returns outstanding items grouped by project with display names from Supabase; consumed by life-automation dashboardWikiTodosCardwired into Current OS — focused variant groups items by project (uppercase label + count + ○ glyph per item); resting variant shows inline project+count badges; footer link to/wiki/todos- Intel: trending research dispatch added — when github-trending enrichment is approved,
dispatchTrendingResearch()creates a research-request item in the ad-hoc-research source;penpal-docbadge type added toSourceBadge(purple,◉ penpallabel)
Infrastructure
- Todos sidebar entry added as first VIEWS item in
WikiSidebar.tsx— Todos → Activity → Infra Map → Tech Radar
Lessons
proxy.tsmatcher covers/wiki/:path*— new pages under/wiki/inherit HMAC auth automatically; no need to add each new route- Keeping the todos API unauthenticated avoided env var overhead in life-automation; TODO text is non-sensitive and CORS headers are sufficient for a cross-origin React fetch
git statusbefore staging revealed parallel-session intel file changes already on disk — committing them in a separate commit before the todos feature kept the feature commit clean and attribution honest
UNDERSTORY-LABS-SITESHIPPEDfeatureai
Research briefing view — shipped
Features
- Research briefing view ships in Field Station — ad-hoc research requests submitted to the field-notes pipeline now surface as enriched cards in
/intelunder a dedicated Research section - Each card renders the Researcher's full output: key findings (string list), prose analysis, confidence chip with reasoning tooltip, recommendations, and cited sources with trust levels and excerpts
IntelShellroutes a virtual__research__slug to the new section — no URL changes, no new pages; sidebar and mobile chip handle navigation entirely within the existing intel layoutgetResearchBriefings()uses a two-step Supabase query — items fetched byitem_type, enrichments joined in JS — because Supabase cannot filter a parent table by a child table's column in one step
Infrastructure
briefing_contentJSONB onfn_enrichmentsis now typed and extracted insrc/lib/intel.ts—mapEnrichment()passes it through asRecord<string, unknown>with named field extraction at render timeSourceBadgeextended withresearch-requesttype — blue#82a0d2badge,◎ researchlabel
Lessons
- Assuming Researcher output matches deep-research output shape was wrong — the Researcher CCR uses a completely different schema (
key_findings[],analysis,relevance.recommendations[]) vs. the built-in workflow (findings[].claim,caveats,openQuestions[]). The right move was to trigger the Researcher manually, query the actualfn_enrichmentsrow, and build the render to match what came back. - TypeScript union cast through
unknownis the correct pattern when a discriminated union type overlaps with a sibling type in the same field:item.itemData as unknown as ResearchItemData, not a direct cast
UNDERSTORY-LABS-SITEfeatureaiinfrastructure
Field Notes pipeline complete — all 5 steps, end-to-end live
Features
- Field Notes pipeline end-to-end operational — GitHub Trending scan → enrichment → Field Station review → implementation
- Enrichment trigger live (
trig_01S3L3aKyThqy4aKPTyMVgUq) — Monday 10am EDT, reads unprocessed items, runs three-lens analysis, writes tofn_enrichments - Implementation trigger live (
trig_01CZNzNJAwgZQynAhUu7EL4s) — daily 8am EDT, reads approved items, generates implementation artifacts inaction_detailsJSONB - Field Station intelligence dashboard shipped — see ship entry for full detail
Infrastructure
- CCR trigger prompts include explicit
CRON_SECRETheader and Supabase URL — remote agents have zero local env access, all context must be in the prompt - Implementation trigger v1 stores note content in
action_detailsJSONB rather than committed files — CCR can clone the repo but cannot push without a GitHub PAT configured in the trigger prompt
Lessons
- CCR agents run in a fully isolated cloud environment — they can read the cloned repo but
git pushfails without a PAT explicitly embedded in the trigger prompt; design around this constraint rather than assuming git write access
UNDERSTORY-LABS-SITESHIPPEDfeatureailaunch
Field Station shipped — intelligence dashboard at /intel
Features
- Field Station intelligence dashboard shipped at
/intel— surfaces AI-enriched items from the field-notes pipeline for weekly review - Two-panel layout: 220px sticky source sidebar with amber active state + scrollable content area; source tabs collapse to a horizontal strip on mobile
- Three-lens scoring per item — Signal (amber), Learning (teal), Relevance (green), each on a 1–10 bar with fill transition and glow
- ItemCard expands three sections on demand — Why Trending, Insights, Relevance — each collapsed by default to keep the digest scannable
- Approve / Reject review flow per item — confirm step with optional note field, optimistic state update, reverts on server error
- Sort and filter bar sticks below the briefing header — sort by any score axis, filter by enrichment status (all / pending / approved / rejected)
- Own visual identity distinct from the Cybernetic Nature main site —
--intel-bg-deep #0f1117, Instrument Serif (editorial display), Geist Sans/Mono (UI + data labels), amber bridges to site accent
Infrastructure
/api/intel/reviewPOST route writes tofn_enrichmentsviaSUPABASE_SERVICE_ROLE_KEY— anon key correctly excluded from write path--intel-*CSS variables scoped inglobals.css— no color collisions with main site palette- Instrument Serif, Geist Sans, and Geist Mono added to root layout via
next/font/google
Lessons
- Tailwind v4 CSS variable tokens don't participate in opacity modifiers (
bg-intel-bg/80outputs solid color) — inlinestyleprops with raw CSS variables are the correct pattern for scoped design systems that aren't in the Tailwind config - Service role key must be a non-
NEXT_PUBLIC_env var in Vercel; the public key won't pass RLS write policies regardless of how the client is constructed
TODO
- Peer Sync source card design exists but source is not live — awaiting the peer
/api/peerendpoint from Naptown Labs ProjectIconimages still pending atpublic/icons/insert-release-note.tsschema mismatch — script usesbody/released_atbut table hascontent/published_at; fix when release notes are next used
UNDERSTORY-LABS-SITEfeaturetoolinginfrastructure
Wiki user guide system — six projects documented with drift detection
Features
- HOW TO USE page type added to the wiki — renders first on every project page, above architecture, always expanded
- Staleness indicator wired to drift data — "last updated X ago — N feature commits since" shown when a guide has fallen behind feature work
- Commit-specific drift classification:
feat:,ui:,design:,ship:prefixes trigger staleness;fix:,refactor:,chore:do not - Six project user guides written: life-automation (full Kitchen Module + companion + Home Projects coverage), kitchen-module, save-state, bud, codec, bark
/shipextended with guide update/create prompt after vault check — fires whenever a feature is marked shipped/wrapextended with ship check — scans for shippable commits before drafting the session entry, closing the/wrap→/ship→ guide update chain
Infrastructure
sync-wiki-state.tsextended withisFeatureCommit(),getGuideLastUpdated(), andsyncGuideDrift()— runs on every push via the existing pre-push hookguide_last_updated(timestamptz) andguide_drift_count(int, -1 = no guide) added toproject_statetable in the save-state Supabase projectWIKI_DIR_OVERRIDEmap added to handle project ID → wiki directory mismatches (current-os→life-automation)- Codec added to
PROJECT_PATHS— drift tracking now covers 10 projects
Lessons
project_statelives in the save-state Supabase project, not the nrisacher-lang project — the dashboard defaults to the wrong one- Reading
git logbefore writing a project's guide surfaces shipped work you've forgotten — codec had Layer 2 Steps 1–2 live; the guide would have listed them as planned replace_all: trueon Edit only catches the first match when a formatter has changed indentation on a second occurrence — target the second block directly
UNDERSTORY-LABS-SITEfeatureinfrastructuretooling
Peer API — outbound endpoint live, field-notes sync architecture
Features
/api/peerlive atunderstorylabs.co/api/peer— read-only outbound JSON endpoint returning project states, recent activity, and releases; consumed by Nathan's Claude Code setup- Bearer token auth via
Authorization: Bearerheader — 401 on missing or invalid token, validated againstPEER_API_KEYenv var before any Supabase queries run - Stack field derived from
connectionsJSONB —servicevalues deduplicated into an array; no schema change required - Peer sync architecture finalized — outbound lives here, inbound (weekly fetch, snapshot storage, diff logic, briefings) belongs in field-notes as a source module; brief at
~/.claude/plans/field-notes-peer-sync-brief.md
Infrastructure
PEER_API_KEYadded to Vercel production and.env.local- Vercel deploy confirmed —
/api/peerregisters as a dynamic route (ƒ) in build output, aliased tounderstorylabs.co
Lessons
- A new file showing "nothing to commit" in
git statususually means a parallel session already committed it — checkgit show HEAD --statbefore assuming a gitignore issue - A peer API doesn't need to match a pre-agreed schema when the source is richer — Nathan's API returns full markdown session logs with features, bug fixes, and lessons; consuming it directly outperforms translating to a flattened contract
TODO
- field-notes: implement
peer-naptownsource module — scan handler,intel_sourcesrow, diff logic against previous snapshot - Exchange
PEER_API_KEYwith Nathan so he can callunderstorylabs.co/api/peer
UNDERSTORY-LABS-SITEinfrastructuretoolingfeature
Wiki maintenance layer — automated status sync, Supabase infra map, JSON tech radar
Features
projects.statusnow auto-inferred bysync-wiki-state.ts— active (≤30 days), paused (>30 days), planned (no state row);completeis protected from overwrite and must be set manuallykitchen-moduleinherits status fromcurrent-osviaSTATUS_INHERITS_FROMmap — child projects track parent activity instead of syncing independently- Infra map migrated from hardcoded arrays to Supabase —
infra_nodestable withtype/parent_idhierarchy replaces three static constants; grid column count adapts dynamically to container count - Tech radar extracted from hardcoded page to
src/data/tech-radar.json— edit the file to update, commit to version-track ring movements;movedfield added per ThoughtWorks standard claude-codeproject added to sync pipeline —~/.claudeis a git repo, now tracked alongside the other 9 projects
Infrastructure
infra_nodestable created in Supabase with RLS enabled —SELECTpolicy grants anon key read access; all 4 node types (compute, service, monitoring, external) seeded from prior hardcoded dataprojects_status_checkconstraint updated to includecomplete— original constraint only allowed active/paused/plannedwiki/tech/page.tsxdropsforce-dynamic— JSON import makes the page statically generated at build timebackfill-wiki-activity.tsandseed-wiki-life-automation.tscommitted to save-state repo —claude-codemissing frombackfillPROJECT_PATHS, add before running
Lessons
- Supabase check constraints don't inherit from TypeScript types — a
ProjectStatustype withcompletedoesn't mean the DB constraint allows it; the constraint must be explicitly updated to match - Static generation is appropriate for config-file-backed pages —
force-dynamicadds server overhead with no benefit when the data source is a committed JSON file
UNDERSTORY-LABS-SITEfeatureinfrastructure
Wiki enrichment — MDX content, guest access, responsive layout redesign
Features
- Architecture and Decision Log MDX content written for all 7 remaining projects — save-state, bark, bud, taproot, lore, understory-labs-site, claude-code; every wiki project page now renders populated Architecture and Decision Log sections
- Guest password support added to wiki login —
WIKI_GUEST_PASSWORDenv var checked alongsideWIKI_PASSWORD; both produce the same HMAC auth cookie - Wiki project page layout redesigned — asymmetric two-column replaced with horizontal metadata card row (Operational, Connections, Related Projects) above full-width content sections capped at 700px for readable line length
- Wiki sidebar now sticky —
position: sticky; top: 0; height: 100vhkeeps navigation visible while content scrolls - Content area centered on wide screens — layout wrapper handles centering via flexbox with 1100px max-width
Bug Fixes
- Heredoc (
<<<) appends trailing newline to Vercel env var values — guest password stored with\nsuffix, silently failing login; fixed withprintf '%s' 'value' | vercel env add
Infrastructure
- Responsive CSS classes (
wiki-project-stats,wiki-card-row,wiki-landing-body) replace inline grid styles — stats grid collapses to 2-col at 900px, 1-col at 500px; card row usesauto-fillfor fluid adaptation - Landing page layout (
/wiki) uses dedicatedwiki-landing-bodytwo-column grid — Active Projects + Recent Activity have balanced content that suits side-by-side display, unlike the project detail page
Lessons
<<<(bash here-string) appends\nto piped values — invisible in shell output, breaks exact-match password comparisons;printf '%s'strips the newline- Different page types need different layouts — the wiki landing page has balanced columns (projects + activity), but the project detail page has asymmetric content (2000px of MDX vs one small card); forcing one grid pattern on both creates dead space or buried content
overflowY: autoon a container creates its own scroll context —position: stickychildren inside it stick relative to that container, not the viewport; removing it restores expected sticky behavior
UNDERSTORY-LABS-SITEfeatureinfrastructuretooling
Wiki complete — project pages, cross-project views, release notes, and auto-sync
Features
- Per-project wiki pages live at
/wiki/[project-id]— header with clickable vault link, at-a-glance stats grid, checklist with progress bar, activity feed, MDX slots for architecture and decision log, release notes preview - Three cross-project views: Activity timeline (events grouped by recency), Infra Map (Taproot LXC container hierarchy + external services tier), Tech Radar (adopt/trial/assess/hold rings across 6 categories with project tags)
- Release notes system — full timeline at
/wiki/[project-id]/releases,insert-release-note.tsscript,/shipStep A3.5 wired to prompt on every feature shipped - Connections panel on each project page — service name, type badge, usage note, and
⬡ vault_pathlinking tovault.rootstack.dev; seeded for all 7 projects with service URLs and Vaultwarden paths update-connection.tsscript — patches a single connection entry (add, update, or remove) by substring match without touching raw JSON;/shipStep A3.6 now prompts for vault path whenever new credentials are created
Infrastructure
sync-wiki-state.ts— readsgit log -1from all 7 local repos, upsertsproject_state(branch, commit, timestamp), inserts commit activity events only when hash changes;--dry-runand--project-idflags supportedwiki-sync.jsPostToolUse hook — fires aftergit commitin Claude Code, spawns sync non-blocking and unref'd; registered insettings.jsononBash(git commit:*)/startskill updated to surface the sync command when wiki state may be stale after recent commitsconnectionsJSONB column added toprojectstable; SQL seed covers all 7 projects;wiki.tsSELECT queries updated in bothgetAllWikiProjectsandgetWikiProject
Bug Fixes
2>/dev/nullinexecSyncgit commands silently fails on Windows cmd — replaced withstdio: ['pipe', 'pipe', 'pipe']to suppress stderr cross-platform- Top-level
awaitin tsx script fails with "cjs output format" error — wrapped inasync function main()+main().catch()to match the existing script pattern across the repo - SQL apostrophe in
Nathan'sbroke the connections seed — escaped toNathan''s
Lessons
2>/dev/nulldoes not suppress stderr fromexecSyncon Windows cmd —stdio: ['pipe', 'pipe', 'pipe']is the cross-platform fix- Top-level
awaitin a tsx script throws "cjs output format" unless the package has"type": "module"— theasync function main()+main().catch()pattern is required, not stylistic - Supabase's "UPDATE without WHERE" safety warning fires on any query containing UPDATE, even when every statement has a WHERE clause — blanket check, not a smart one
next-mdx-remote/rscis the correct App Router import path —next-mdx-remotedirectly does not work in server components