gastown-viewer-intent v0.6.0 — one-pager + operator audit
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.
Links: GitHub · Release v0.6.0 · Council Decision Record
Foundation refactor of the terminal client + two new read-only tabs:
- Registry-driven keybindings. A single
KeyRegistryfeeds 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
Viewtoggle;Update()routes throughDispatch(focus, key). - Combo keys —
ggjump-to-top,Gjump-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 memoriesviewer;/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;enterjumps 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 hardcoded0.1.0since 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.
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.
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.
| 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. |
| 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 |
- Read-only-forever memories — architectural invariant, not a checkbox. Zero
POST/PUT/PATCH/DELETEunder/api/v1/memories/*; a tripwire test asserts this. The bd CLI is the canonical writer. - Server-side classification redaction — Class A (partner names:
kobiton/nixtla/mudit/polygonsubstring;lit/elmwhole-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. - Hardened bind by default —
IsLoopbackHost("")returns false (empty host binds 0.0.0.0 in Go's net/http; that bypass was caught in code review).--hostother thanlocalhost/127.x/::1refused at startup. - Single binary distribution —
go:embedmounts the web/dist/ inside the daemon binary.go install+ direct download both work without a separatenpm buildstep on the user's side. - 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.
See 000-docs/006-AA-AUDT-appaudit-devops-playbook.md for the full 15,000-word version. Condensed below.
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.
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
| 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) |
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
| 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 |
| 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 |
| 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-... |
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
- TUI Tier-1 build-out (#24): focus-based key registry, combo-key state
machine (
ggjump-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 memoriesviewer 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;enterjumps to the existing Board issue-detail viewport. - TUI status code enforcement: new
Memories,SearchMemories, andHumanFlagsHTTP client methods reject non-200 responses with a readable error before attempting JSON decode (shareddecodeJSONhelper). - 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).
- TUI architecture: replaced the binary
Viewenum (Board / Issue) with a richerFocusenum (Board / Memories / Triage / Detail);Update()routes through a centralizedDispatch(focus, key)so bindings, help, and tab bar stay in sync (#24). cmd/gvi-tui/main.gonow takes its version from goreleaser ldflags (-X main.version={{.Version}}) instead of a hardcoded constant. Previously every binary reported0.1.0regardless of release..goreleaser.yaml: wired ldflags injection for thegvi-tuibuild target so release binaries report the correct version.- README header synced with the canonical gist one-pager and new canonical URLs (#22).
go.sumcleanup: removed orphan transitive entries (go-udiff v0.2.0,x/exp/golden) that no module currently imports.
- TUI dispatcher held an
RWMutexread lock across handler invocation — a latent deadlock if a handler ever re-entered the registry. MirrorDispatchCombo'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 byTestTruncateRunes_UTF8Safecovering ASCII, Cyrillic, and emoji. .goreleaser.yaml: temporarily disabled the Homebrew tap block untilHOMEBREW_TAP_TOKENrotation completes — release builds were failing on the brews step (#19)..goreleaser.yaml: pointed at the canonicaljeremylongshore/gastown-viewer-intentrepo URL (#18).
v0.5.0 - 2026-05-23
Phase 2 (Option B-minus): daemon hardening, read-only memories panel, triage queue, sync pill.
- Memories panel: read-only
bd memoriesviewer with policy-driven redaction (000-docs/005-PP-POLICY-memories-classification). Default view shows redaction markers; full content requires explicit?reveal=truein the daemon URL. - Triage queue:
/api/v1/humanexposes beads carrying thehumanlabel (read-view; respond / dismiss deferred to a future bead behind the session-token gate). - Dolt sync pill:
/api/v1/syncreturns a composedDoltSyncStatebody; never errors (failures encode ashealth: "unknown"with a tooltip string). Header pill in the web UI is bound to this surface. gt wisps list --jsonintegration: molecules now read from gt 0.9's wisps surface rather than the legacy.beads/molecule.jsonfile.- 7-layer test enforcement:
@intentsolutions/audit-harnessinstalled 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.
- 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;RequireTokenMiddlewareis wired but registered against zero state-changing routes today — installed ahead of any future POST endpoints.
- 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 from005-PP-POLICY-memories-classificationbefore any memory crosses the HTTP boundary.