Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save jeremylongshore/cd5d24298d05140eca8a3ef2cb2773f3 to your computer and use it in GitHub Desktop.

Select an option

Save jeremylongshore/cd5d24298d05140eca8a3ef2cb2773f3 to your computer and use it in GitHub Desktop.
gastown-viewer-intent v0.5.0 — one-pager + operator audit

gastown-viewer-intent v0.6.0 — one-pager + operator audit

gastown-viewer-intent v0.6.0

Local-first Mission Control dashboard for Beads + Gas Town, with a read-only-forever memory panel.

A single Go daemon + embedded React UI that surfaces bd (issue tracker) and gt (multi-agent orchestrator) state in one browser tab — so the engineer doesn't alt-tab to a terminal every two minutes to run bd ready or bd dolt status. Loopback bind enforced, Origin-allowlist gated, memory content classification-redacted by default.

CI Release License: MIT

Links: GitHub · Release v0.6.0 · Council Decision Record


What's New in v0.6.0 (TUI Tier-1)

Foundation refactor of the terminal client + two new read-only tabs:

  • Registry-driven keybindings. A single KeyRegistry feeds both the dispatcher and the help overlay — help cannot drift from the bindings the TUI actually responds to. Adopted from the Dicklesworthstone/beads_viewer reference repo (1,500+ ⭐).
  • Focus enum (Board / Memories / Triage / Detail) replaces the binary View toggle; Update() routes through Dispatch(focus, key).
  • Combo keysgg jump-to-top, G jump-to-bottom, 250 ms pending-key window.
  • Help overlay (?) grouped by category (Navigation / Tabs / Actions / Global), filtered by current focus.
  • Memories tab — read-only bd memories viewer; / triggers server-side substring search; Copy bd recall <key> hint surfaces the canonical reveal path. Reveal intentionally NOT exposed in the TUI — Council Q2 read-only-forever invariant.
  • Triage tab — read-only human-flag queue; enter jumps to the existing Board issue-detail viewport.
  • Foundation fixes — HTTP status-code enforcement, UTF-8 rune-safe truncation, dispatcher lock release-before-handler, ldflags-injected gvi-tui --version (was hardcoded 0.1.0 since day 1).

The TUI remains an HTTP-only client; the daemon at localhost:7070 continues to own all bd/gt shelling. Data-layer patterns from the reference repo (direct shell, file watcher, semantic search) were deliberately not adopted — out of scope by architecture.

One-Pager

The Problem

Running a multi-agent workflow means juggling two CLIs all day. You're on a Slack call and need to remember what a partner asked for last week — that's bd recall mudit-q3. You just kicked off a long-running agent and want to know if it's stuck — that's gt convoy list. You merged a batch of fixes and want to know if they actually pushed to the remote — that's bd dolt status. Three terminals, three contexts, every five minutes, all day. The dashboard tax isn't the individual commands — it's the relentless context-switch out of whatever you were actually thinking about.

For an engineer running a personal agent swarm, the workflow problem isn't power; it's surface. The CLIs already work. What's missing is a single pane that shows what's going on without forcing you to remember which subcommand exposes what.

The Solution

A local-first daemon that shells to bd and gt for data and presents it in a single browser tab — Board / Graph / Gas Town / Memory / Triage, with a Dolt sync state pill in the header. Everything is read-only by architectural invariant: the CLIs remain the canonical writers; the dashboard is a strict mirror with a "Copy bd recall <key>" passthrough button instead of in-UI editing. That decision came out of a 7-seat adversarial council review (the Decision Record is linked above) which ratified Option B-minus — ship the daily-value surfaces, harden the daemon, defer write-paths behind an auth-token gate that's installed but unwired.

The dashboard runs on loopback only. Origin allowlist middleware hard-rejects cross-origin requests with 403. Memory content is server-side redacted: partner names (whole-word matched to avoid false positives) and token-shaped prefixes (sk-, ghp_, AKIA, glpat-, etc.) get [REDACTED ...] placeholders before bytes ever reach the browser. Per-card reveal exists, doesn't persist across navigations.

Who / What / Where / When / Why

Aspect Details
Who A single engineer running their own multi-agent workspace. Not built for teams.
What A Go daemon + React/Vite SPA dashboard. Five tabs + header sync pill.
Where Local machine. Loopback bind. Single binary via go:embed. macOS/Linux.
When All-day driver while running bd/gt-mediated work. Replaces bd ready / bd dolt status / gt convoy list alt-tab churn.
Why Mission Control without inviting a CSRF / DNS-rebind / partner-name-leak threat model. Single-user-local + memory-redaction-by-default + read-only-mirror invariant is the design point.

Stack

Layer Technology Purpose
Daemon Go 1.22+ HTTP server, stdlib net/http.ServeMux with Go 1.22+ pattern routing
Adapters exec.CommandContext shell to bd/gt CLIs Decouples viewer from upstream storage internals
Web UI React 19 + Vite 7 + TypeScript 5.9 Single-page app, embedded via go:embed
Graph D3.js v7 Force-directed dependency visualization
TUI Bubble Tea v1 Terminal client with focus-based key registry, combo keys, help overlay, Memories + Triage tabs (Tier-1 in v0.6.0)
Security Origin allowlist + 256-bit session token (mode 0600) + loopback enforcement Server-side defense against DNS rebind, CSRF, accidental LAN exposure
Redaction Custom Class A + Class B classifier Memory content sanitized server-side per published policy
Test enforcement @intentsolutions/audit-harness Hash-pinned L1 hooks + escape-scan + repo-wide markdownlint + Vale + lychee
Release goreleaser v2 Multi-arch tarballs + .deb / .rpm / .apk

Key Differentiators

  1. Read-only-forever memories — architectural invariant, not a checkbox. Zero POST/PUT/PATCH/DELETE under /api/v1/memories/*; a tripwire test asserts this. The bd CLI is the canonical writer.
  2. Server-side classification redaction — Class A (partner names: kobiton/nixtla/mudit/polygon substring; lit/elm whole-word to avoid false-positives) + Class B (secret-pattern prefixes with 16-char min run length). Raw bytes never reach the rendered HTML by default. Per-card reveal does not persist across navigations.
  3. Hardened bind by defaultIsLoopbackHost("") returns false (empty host binds 0.0.0.0 in Go's net/http; that bypass was caught in code review). --host other than localhost/127.x/::1 refused at startup.
  4. Single binary distributiongo:embed mounts the web/dist/ inside the daemon binary. go install + direct download both work without a separate npm build step on the user's side.
  5. Council-ratified scope discipline — Phase 2 went through a 7-seat adversarial review (ISEDC); CFO Option-A dissent and CMO read-write dissent preserved verbatim in the Decision Record. Every architectural invariant has a documented "why" before it has code.

Operator-Grade System Analysis

See 000-docs/006-AA-AUDT-appaudit-devops-playbook.md for the full 15,000-word version. Condensed below.

Executive Summary

gastown-viewer-intent v0.6.0 is the shipped product: all Phase 2 surfaces on master (Memory panel, Sync pill, Triage queue), daemon hardening complete (Origin allowlist + session token + loopback bind), foundation fixes landed (bd defer --until round-trip, gt 0.9 wisps migration). 12 PRs in the burst; CI green. Released as v0.6.0 with 11 binary assets on the GitHub Releases page.

Biggest risk: single-user-local by design. No multi-user auth model, no scaling path. If the dashboard ever needs to be shared, every architectural decision needs revisiting.

Second-biggest risk: bd/gt CLI JSON-schema coupling. Council Q1 picked "honest lag with a supported-version matrix" over chase-cadence — except for security-flagged upstream releases, which carry a 48hr fast-path SLA.

Architecture (Condensed)

gvid daemon (127.0.0.1:7070)
    │
    ├─ Origin allowlist middleware → 403 on mismatch
    ├─ CORS middleware (sets headers on allowed origin)
    ├─ Request logging
    ↓
ServeMux (Go 1.22+ pattern routing)
    │
    ├─ /api/v1/issues, /board, /graph, /events     (read-only)
    ├─ /api/v1/sync                                (read-only; never errors)
    ├─ /api/v1/human                               (read-only; POST routes deferred)
    ├─ /api/v1/memories[/{key}|/search]            (read-only by invariant; redaction layer)
    ├─ /api/v1/town/*                              (rigs/agents/convoys/wisps/mail)
    └─ /  (embedded React UI via go:embed)
    │
    ├─ Beads adapter — exec.CommandContext("bd", ...)
    ├─ Gas Town adapter — exec.CommandContext("gt", ...) + ~/gt FS reads
    └─ Memory redaction — Class A partner names + Class B secret prefixes

Key Tradeoffs

Decision Chosen Over Why Cost
Memories panel Read-only forever Read-write UI Dual-write consistency with bd CLI = data loss; mutation surface on localhost is a CSRF/DNS-rebind target In-UI editing UX (mitigated by Copy-CLI-command button)
Data source CLI shell + JSON parse Direct .beads/ / ~/gt/ file reads Upstream storage moves (gt 0.9 retired molecule.json); shelling decouples viewer from internal format churn One subprocess per request; serialization tax
Bind Loopback only, empty host refused --host 0.0.0.0 with warning Empty host = 0.0.0.0 in Go's net/http silently bypassed the restriction in an earlier draft Container-test escape hatch needed (DisableLoopbackCheck flag)
Sync pill Adapter never errors Standard Go error propagation Pill is the smoke alarm; crashing the whole dashboard view defeats its purpose Anomalous interface signature (always-nil error)
Test coverage Audit-harness install + smoke-per-panel Full Vitest + RTL backfill in v0.6.0 Internal-use product, single user; defect cost is "alt-tab to terminal," not production incident Zero web unit tests today (gastown-6nw auto-escalates 2026-07-15)

Directory Structure

gastown-viewer-intent/
├── cmd/
│   ├── gvid/              # Daemon entrypoint
│   └── gvi-tui/           # TUI client entrypoint
├── internal/
│   ├── api/               # HTTP handlers + security middleware + memory redaction
│   ├── beads/             # bd CLI adapter (shells to `bd ... --json`)
│   ├── gastown/           # gt CLI adapter (+ ~/gt FS reads)
│   ├── model/             # Domain types (Memory, Issue, DoltSyncState, …)
│   └── tui/               # Bubble Tea Model (minimal — roadmap epic gastown-ey8)
├── web/                   # React + Vite, embedded via go:embed
├── 000-docs/              # Per /doc-filing v4.3 — PRD, ADR, API ref, AT-DECR, policy, audit
├── tests/TESTING.md       # Calibrated-investment test policy (CSO closeout artifact)
├── deploy/                # gvid.service + install.sh
├── scripts/               # Custom MD frontmatter validator
├── THREAT_MODEL.md        # Five in-scope threats + defense map
└── .github/workflows/     # ci.yaml + doc-quality.yml + release.yaml

Deployment & Operations

Task Command Notes
Run locally make dev Daemon at :7070, Vite at :5173 in parallel
Build single binary make build Web first, then Go binaries to bin/
Install via go-install go install github.com/jeremylongshore/gastown-viewer-intent/cmd/gvid@v0.6.0 Pinned tag
Install via package .deb / .rpm / .apk from Releases linux/amd64 + linux/arm64
Install via brew broken — HOMEBREW_TAP_TOKEN rotation pending (gastown-die) go install + direct download both work
Run as systemd user unit deploy/install.sh Optional
Tag release git tag -a vX.Y.Z -m "..." && git push origin vX.Y.Z Triggers goreleaser via .github/workflows/release.yaml
Rollback git checkout v(prev) && make build No deploy automation — local-first

Current State Assessment

Component Status Evidence
Daemon + read-only routes ✅ shipped cmd/gvid/main.go, all v0.6.0 routes; 30+ tests
Memory redaction (Class A + B) ✅ shipped + tested internal/api/memoryredact.go, 12 test groups
Security middleware ✅ shipped + tested internal/api/security.go, 16 tests
Web UI (Board / Graph / Gas Town / Memory / Triage + Sync pill) ✅ shipped web/src/App.tsx; eslint + production build green
TUI (gvi-tui) ✅ Tier-1 shipped v0.6.0 Focus enum + key registry + Memories/Triage tabs + help overlay; Tier-2/3 (swimlanes, status bar, ASCII DAG) under gastown-ey8
Doc-quality CI gates ✅ green on master markdownlint + frontmatter hard; Vale + lychee advisory
Audit-harness install ✅ shipped web/ devDep + .harness-hash manifest
Goreleaser release pipeline ✅ shipped (brews disabled) v0.6.0 published with 11 assets
Web-side unit tests ❌ deferred Bead gastown-6nw, auto-escalates 2026-07-15
Homebrew tap install path ❌ broken Bead gastown-die — token rotation needed
Reveal-event audit log ❌ not implemented Open follow-up in THREAT_MODEL.md

Quick Reference

Resource URL
Repo github.com/jeremylongshore/gastown-viewer-intent
v0.6.0 release v0.6.0 release
Council Decision Record 000-docs/004-AT-DECR-...
Memory classification policy 000-docs/005-PP-POLICY-...
Threat model THREAT_MODEL.md
Testing policy tests/TESTING.md
Full operator audit 000-docs/006-AA-AUDT-...

Changelog

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

v0.6.0 - 2026-05-26

Added

  • TUI Tier-1 build-out (#24): focus-based key registry, combo-key state machine (gg jump-to-top), and a help overlay rendered from the same registry the dispatcher consumes. Adopts the registry-driven pattern from Dicklesworthstone/beads_viewer.
  • TUI Memories tab (#24): read-only bd memories viewer with daemon-applied redaction markers; / triggers server-side substring search via /api/v1/memories/search; Copy bd recall <key> hint surfaces the canonical reveal path. Reveal is intentionally NOT exposed in the TUI per Council Q2 read-only-forever invariant.
  • TUI Triage tab (#24): read-only human-flag queue from /api/v1/human; enter jumps to the existing Board issue-detail viewport.
  • TUI status code enforcement: new Memories, SearchMemories, and HumanFlags HTTP client methods reject non-200 responses with a readable error before attempting JSON decode (shared decodeJSON helper).
  • Operator-grade DevOps playbook (000-docs/006-AA-AUDT-…) covering build, deploy, monitoring, secrets, and rollback for the viewer daemon (#23).
  • Tier-2/3 follow-up beads filed under epic gastown-ey8: gastown-6je (kanban swimlanes), gastown-yla (TUI status bar with sync pill), gastown-ay7 (ASCII DAG + insights/history/sprint).

Changed

  • TUI architecture: replaced the binary View enum (Board / Issue) with a richer Focus enum (Board / Memories / Triage / Detail); Update() routes through a centralized Dispatch(focus, key) so bindings, help, and tab bar stay in sync (#24).
  • cmd/gvi-tui/main.go now takes its version from goreleaser ldflags (-X main.version={{.Version}}) instead of a hardcoded constant. Previously every binary reported 0.1.0 regardless of release.
  • .goreleaser.yaml: wired ldflags injection for the gvi-tui build target so release binaries report the correct version.
  • README header synced with the canonical gist one-pager and new canonical URLs (#22).
  • go.sum cleanup: removed orphan transitive entries (go-udiff v0.2.0, x/exp/golden) that no module currently imports.

Fixed

  • TUI dispatcher held an RWMutex read lock across handler invocation — a latent deadlock if a handler ever re-entered the registry. Mirror DispatchCombo's release-before-invoke order.
  • UTF-8 byte-slice truncation across 4 sites (board card titles, issue descriptions, memory previews, triage row titles) could split a multi-byte codepoint and emit malformed bytes. Replaced with a new rune-safe truncateRunes() helper; locked in by TestTruncateRunes_UTF8Safe covering ASCII, Cyrillic, and emoji.
  • .goreleaser.yaml: temporarily disabled the Homebrew tap block until HOMEBREW_TAP_TOKEN rotation completes — release builds were failing on the brews step (#19).
  • .goreleaser.yaml: pointed at the canonical jeremylongshore/gastown-viewer-intent repo URL (#18).

v0.5.0 - 2026-05-23

Phase 2 (Option B-minus): daemon hardening, read-only memories panel, triage queue, sync pill.

Added

  • Memories panel: read-only bd memories viewer with policy-driven redaction (000-docs/005-PP-POLICY-memories-classification). Default view shows redaction markers; full content requires explicit ?reveal=true in the daemon URL.
  • Triage queue: /api/v1/human exposes beads carrying the human label (read-view; respond / dismiss deferred to a future bead behind the session-token gate).
  • Dolt sync pill: /api/v1/sync returns a composed DoltSyncState body; never errors (failures encode as health: "unknown" with a tooltip string). Header pill in the web UI is bound to this surface.
  • gt wisps list --json integration: molecules now read from gt 0.9's wisps surface rather than the legacy .beads/molecule.json file.
  • 7-layer test enforcement: @intentsolutions/audit-harness installed as a web devDep with a hash manifest at .harness-hash.
  • Doc-quality CI gates: markdownlint-cli2, frontmatter validator, Vale prose style, lychee link check on every PR touching **/*.md.

Changed

  • Daemon binds loopback-only by default; --host=0.0.0.0, ::, private LAN, and link-local addresses are rejected at startup with an actionable error message.
  • Origin-allowlist middleware sits outermost on every HTTP request; cross-origin requests are rejected with 403 ORIGIN_REJECTED (defense against DNS rebinding + CSRF from any tab on the dev box).
  • Session-token plumbing: ~/.config/gvid/token (mode 0600) is generated at first start; RequireTokenMiddleware is wired but registered against zero state-changing routes today — installed ahead of any future POST endpoints.

Security

  • THREAT_MODEL.md committed alongside the daemon hardening work; documents the loopback bind, origin allowlist, session token, and memory-classification invariants.
  • Memory redaction (internal/api/memoryredact.go) applies the partner-name + secret-pattern denylists from 005-PP-POLICY-memories-classification before any memory crosses the HTTP boundary.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment