← HOME

PROJECT

Understory Labs

ACTIVE

Next.jsTypeScriptTailwindSupabaseVercel

ABOUT

Public presentation layer — project showcase, infrastructure dashboard, curated changelog, and internal wiki.

FEATURES

SHIPPED

  • ✓
    Field Station intelligence dashboard
    MAY 2026
  • ✓
    Research briefing view
    JUL 2026
  • ✓
    ballot 2026 voting guide
    OCT 2026
  • ✓
    Ballot guide, multi-area

    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.

    OCT 2026

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
  • /studio command — find searches museum APIs and named illustrators' sites by source tier, keep/discard/status/sync/feedback manage the Tray and the page
  • Layered hero shipped behind hero-art.json (enabled:false) — 4800x3000 stage, 4:5 story zone, 44 px hotspots, ?calibrate overlay; 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 assigning x.id threw; copying with Object.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.md and a project CLAUDE.md illustration section added

Lessons

  • The protect-generated hook blocks any path containing /build — build_library.py had to become make_library.py
  • Artifact db data() is read-only; copy before adding fields
  • Next 16 warns on any quality not listed in images.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-art worktree
  • 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 decisions page 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 comp full findings
  • Wrong facts corrected in sos, pike-trustee, bsb-lt, treas, comm-1 and hd-25 cards — 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

  • request feedback kind added to ballot_2026_feedback with a widened check constraint and a notified_at column
  • Alert job on CT 100 posts new rows once to #ballot-guide through 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 ssh command, 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_decisions Supabase table on self-hosted Taproot Postgres — RLS enabled with zero policies, deny-all for the public anon key, server-side service_role only
  • A scoped claude_migrator Postgres role (create-only, not superuser) backs future schema changes through scripts/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 = true before scrollIntoView
  • Kong evaluates plugins by priority, not declaration order — ip-restriction (990) runs after basic-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.local had 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 all content/wiki/**/*.mdx at request time, finds ## TODO / ## Next Steps sections, 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 existing proxy.ts HMAC cookie auth, no extra code needed
  • /api/todos public endpoint live — CORS-open, no auth required; returns outstanding items grouped by project with display names from Supabase; consumed by life-automation dashboard
  • WikiTodosCard wired 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-doc badge type added to SourceBadge (purple, ◉ penpal label)

Infrastructure

  • Todos sidebar entry added as first VIEWS item in WikiSidebar.tsx — Todos → Activity → Infra Map → Tech Radar

Lessons

  • proxy.ts matcher 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 status before 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 /intel under 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
  • IntelShell routes 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 layout
  • getResearchBriefings() uses a two-step Supabase query — items fetched by item_type, enrichments joined in JS — because Supabase cannot filter a parent table by a child table's column in one step

Infrastructure

  • briefing_content JSONB on fn_enrichments is now typed and extracted in src/lib/intel.ts — mapEnrichment() passes it through as Record<string, unknown> with named field extraction at render time
  • SourceBadge extended with research-request type — blue #82a0d2 badge, ◎ research label

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 actual fn_enrichments row, and build the render to match what came back.
  • TypeScript union cast through unknown is 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 to fn_enrichments
  • Implementation trigger live (trig_01CZNzNJAwgZQynAhUu7EL4s) — daily 8am EDT, reads approved items, generates implementation artifacts in action_details JSONB
  • Field Station intelligence dashboard shipped — see ship entry for full detail

Infrastructure

  • CCR trigger prompts include explicit CRON_SECRET header 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_details JSONB 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 push fails 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/review POST route writes to fn_enrichments via SUPABASE_SERVICE_ROLE_KEY — anon key correctly excluded from write path
  • --intel-* CSS variables scoped in globals.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/80 outputs solid color) — inline style props 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/peer endpoint from Naptown Labs
  • ProjectIcon images still pending at public/icons/
  • insert-release-note.ts schema mismatch — script uses body/released_at but table has content/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
  • /ship extended with guide update/create prompt after vault check — fires whenever a feature is marked shipped
  • /wrap extended with ship check — scans for shippable commits before drafting the session entry, closing the /wrap → /ship → guide update chain

Infrastructure

  • sync-wiki-state.ts extended with isFeatureCommit(), getGuideLastUpdated(), and syncGuideDrift() — runs on every push via the existing pre-push hook
  • guide_last_updated (timestamptz) and guide_drift_count (int, -1 = no guide) added to project_state table in the save-state Supabase project
  • WIKI_DIR_OVERRIDE map 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_state lives in the save-state Supabase project, not the nrisacher-lang project — the dashboard defaults to the wrong one
  • Reading git log before 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: true on 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/peer live at understorylabs.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: Bearer header — 401 on missing or invalid token, validated against PEER_API_KEY env var before any Supabase queries run
  • Stack field derived from connections JSONB — service values 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_KEY added to Vercel production and .env.local
  • Vercel deploy confirmed — /api/peer registers as a dynamic route (ƒ) in build output, aliased to understorylabs.co

Lessons

  • A new file showing "nothing to commit" in git status usually means a parallel session already committed it — check git show HEAD --stat before 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-naptown source module — scan handler, intel_sources row, diff logic against previous snapshot
  • Exchange PEER_API_KEY with Nathan so he can call understorylabs.co/api/peer
UNDERSTORY-LABS-SITEinfrastructuretoolingfeature

Wiki maintenance layer — automated status sync, Supabase infra map, JSON tech radar

Features

  • projects.status now auto-inferred by sync-wiki-state.ts — active (≤30 days), paused (>30 days), planned (no state row); complete is protected from overwrite and must be set manually
  • kitchen-module inherits status from current-os via STATUS_INHERITS_FROM map — child projects track parent activity instead of syncing independently
  • Infra map migrated from hardcoded arrays to Supabase — infra_nodes table with type/parent_id hierarchy 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; moved field added per ThoughtWorks standard
  • claude-code project added to sync pipeline — ~/.claude is a git repo, now tracked alongside the other 9 projects

Infrastructure

  • infra_nodes table created in Supabase with RLS enabled — SELECT policy grants anon key read access; all 4 node types (compute, service, monitoring, external) seeded from prior hardcoded data
  • projects_status_check constraint updated to include complete — original constraint only allowed active/paused/planned
  • wiki/tech/page.tsx drops force-dynamic — JSON import makes the page statically generated at build time
  • backfill-wiki-activity.ts and seed-wiki-life-automation.ts committed to save-state repo — claude-code missing from backfill PROJECT_PATHS, add before running

Lessons

  • Supabase check constraints don't inherit from TypeScript types — a ProjectStatus type with complete doesn'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-dynamic adds 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_PASSWORD env var checked alongside WIKI_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: 100vh keeps 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 \n suffix, silently failing login; fixed with printf '%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 uses auto-fill for fluid adaptation
  • Landing page layout (/wiki) uses dedicated wiki-landing-body two-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 \n to 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: auto on a container creates its own scroll context — position: sticky children 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.ts script, /ship Step A3.5 wired to prompt on every feature shipped
  • Connections panel on each project page — service name, type badge, usage note, and ⬡ vault_path linking to vault.rootstack.dev; seeded for all 7 projects with service URLs and Vaultwarden paths
  • update-connection.ts script — patches a single connection entry (add, update, or remove) by substring match without touching raw JSON; /ship Step A3.6 now prompts for vault path whenever new credentials are created

Infrastructure

  • sync-wiki-state.ts — reads git log -1 from all 7 local repos, upserts project_state (branch, commit, timestamp), inserts commit activity events only when hash changes; --dry-run and --project-id flags supported
  • wiki-sync.js PostToolUse hook — fires after git commit in Claude Code, spawns sync non-blocking and unref'd; registered in settings.json on Bash(git commit:*)
  • /start skill updated to surface the sync command when wiki state may be stale after recent commits
  • connections JSONB column added to projects table; SQL seed covers all 7 projects; wiki.ts SELECT queries updated in both getAllWikiProjects and getWikiProject

Bug Fixes

  • 2>/dev/null in execSync git commands silently fails on Windows cmd — replaced with stdio: ['pipe', 'pipe', 'pipe'] to suppress stderr cross-platform
  • Top-level await in tsx script fails with "cjs output format" error — wrapped in async function main() + main().catch() to match the existing script pattern across the repo
  • SQL apostrophe in Nathan's broke the connections seed — escaped to Nathan''s

Lessons

  • 2>/dev/null does not suppress stderr from execSync on Windows cmd — stdio: ['pipe', 'pipe', 'pipe'] is the cross-platform fix
  • Top-level await in a tsx script throws "cjs output format" unless the package has "type": "module" — the async 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/rsc is the correct App Router import path — next-mdx-remote directly does not work in server components