Skip to content

Survives being used: cold-start audit remediation (16 tasks) + Slack-level surface - #1

Open
dmfarcas wants to merge 75 commits into
mainfrom
feat/ux-overhaul
Open

Survives being used: cold-start audit remediation (16 tasks) + Slack-level surface#1
dmfarcas wants to merge 75 commits into
mainfrom
feat/ux-overhaul

Conversation

@dmfarcas

Copy link
Copy Markdown
Owner

Survives Being Used — cold-start audit remediation

A cold-start deep audit (first real end-to-end use of bool on the shipping path: fresh docker compose up, real signup, real TLS to Libera.Chat, a second scripted IRC client, a mid-session container restart) found the app demoed well but had a broken daily-driver spine — features "done, tests green" but wired to nothing. This branch fixes that spine and builds out the Slack-level surface, in 16 tasks, each individually reviewed (spec + quality) with a final whole-branch integration review.

Audit + plan: docs/superpowers/audits/2026-07-17-cold-start-audit.md · docs/superpowers/plans/2026-07-17-survives-being-used.md

Phase 1 — survives being used

  • DM identity — key incoming PMs by the counterparty, not our own nick (DMs were conversations with yourself)
  • Dead commands — wire /notice /away /invite /names through the server; honor the notice flag
  • WS auto-reconnect — capped backoff + jitter; hello re-sent per connection (the "reconnecting…" banner was a lie)
  • Session persistence — persist joined channels + auto_connect; resume IRC on boot; session:targets snapshot so channels/DMs/unreads survive restart and refresh
  • Mobile drawerdisplay:contents wrapper defeated .app > .rail selectors; fragment fix
  • Newcomer routing — zero-state routes to the network directory; join opens the channel; mention badges + honest tab-title counts; ident noise excluded
  • CI reality gatedocker-compose.ci.yml + tools/ci-smoke.mjs: builds the real image, stands up a real IRC server (ergo), and drives signup → connect → join → message → restart → world intact. This is the audit, automated.

Phase 2 — Slack-level surface

  • Click any nickUserPopover (Message/Whois/Invite + op actions) on member rows and message nicks, killing the slash-command dependency
  • Connection visibility — per-network Connecting/Reconnecting/Disconnected + Reconnect; fixed the silently-dead MOTD console (irc-framework prepends a message-tags blob that broke numeric parsing)
  • Channel browser on live /LIST results
  • Link previews — actually fire now (custom lookup didn't handle Node's Happy-Eyeballs {all:true}); SSRF IP-pin preserved and now compiler-enforced
  • Orientation — history-start divider + topic truncation
  • Polish batch + landed the ? shortcut cheatsheet
  • Message mock parity — day dividers, join/part/quit system lines, inline @mentions, code blocks
  • Presence parity — away-notify + member dimming, DM presence dots, sidebar filter, composer emoji, IRCv3 caps line

Verification

  • Per-task TDD (red → green); each task independently reviewed and its Critical/Important findings fixed and re-reviewed.
  • Final whole-branch integration review (Opus) — no Critical; one cross-task regression found (net:list wiped caps on reconnect) and fixed.
  • Tests green in isolation: shared 42 · server 237 · client 467 · desktop 58.

⚠️ Before merge (two known gates)

  1. CI unit job will be red until the pnpm-lock.yaml is synced — packages/desktop declares electron-builder but it was never added to the lockfile (pre-existing drift, milestone-8). Run pnpm install && git commit pnpm-lock.yaml in a networked env. The smoke job passes.
  2. No whole-branch visual pass yet — every per-task screenshot was blocked by a chrome-devtools browser-profile lock. A Ralph visual sweep (desktop + 390px, all overlays/themes) is the outstanding gate.

Deferred Minor roll-up (none block the spine) is tracked in .superpowers/sdd/survives-progress.md.

Note: this branch also carries the earlier UX-overhaul work; the above is the audit-driven remediation layered on top.

🤖 Generated with Claude Code

dmfarcas and others added 30 commits July 12, 2026 23:50
…ser, cmd-k, a11y

Newcomer-first redesign grounded in a cited competitive study (Slack/Discord/
Teams/Element/Zulip/The Lounge) and the /design mock system. Decomposes the work
into 9 workstreams, each closing with a visual+a11y+interaction parity loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
INDEX (roadmap, global constraints, shared Ralph loop) + WS-0 foundation,
WS-1 docker, WS-2 auth, WS-3 account menu, WS-4 network directory, WS-5 channel
browser, WS-6 command palette, WS-7 shell polish, WS-8 a11y sweep. ~67 TDD tasks,
each UI workstream closing with the visual+a11y+interaction parity loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Multi-stage Dockerfile + compose + entrypoint (auto-gen secret) + README quickstart.
Runtime docker-up verification deferred — sandbox Docker daemon unresponsive; files
pass static checks (bash -n, Dockerfile/compose well-formed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Field, EmptyState, Kbd, LiveLog, Menu accessible primitives + deterministic
seedDemo() on window.__bool. 283 client tests pass, typecheck clean.
Follow-up fixes (Menu arrow-key test, dev-seed assertion + messages) tracked next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ons + seeded messages, Kbd radius token, Menu unmount focus guard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tstrap server-side

Security review found the WS-2 auth guarantees were UI-only and bypassable:
consumeInvite was dead code and sign-up reached better-auth with no invite check,
so anyone could register without an invite and a code was reusable forever; the
first-user->admin bootstrap was never implemented.

Enforce it in better-auth via databaseHooks.user.create (proven to receive the
request body incl. inviteCode as ctx.body). The before-hook:
- lets the very first account through and promotes it to role 'admin'
  (re-checked at creation time against the real user table, not the UI/probe);
- for every subsequent PUBLIC sign-up (/sign-up/email) requires a valid, unused
  invite and throws APIError('FORBIDDEN') otherwise — non-bypassable even via a
  direct POST to /api/auth/sign-up/email;
- leaves admin-initiated creates (/admin/create-user) ungated.
The after-hook calls consumeInvite(code, user.id) to mark the invite used
(single-use, atomic single-row UPDATE).

Add POST /api/setup as the explicit server-gated first-admin route: 409 when
users already exist (re-checked at creation time). Centralize userCount() in
bootstrap-admin.ts. Remove the dead duplicate ensureInvitesSchema() (table lives
in schema.ts).

Server tests prove: first user becomes admin; second registration with no /
invalid / used code is rejected 403 (incl. the direct better-auth endpoint);
invites are single-use; POST /api/setup returns 409 when users exist. A test
helper (test/signup.ts) seeds a fresh single-use invite for non-first sign-ups so
existing multi-user tests exercise the real enforcement path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…by/aria-invalid

The custom <p role="alert"> invite error in LoginForm was not programmatically
linked to the invite input. Route it through the WS-0 Field primitive's validate
prop so Field wires aria-invalid + aria-describedby to its own error node.
Clear the error as the user edits. Strengthen the test to assert the input's
aria-describedby points at the element holding the error text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…otstrap)

Setup wizard (zero-users→admin), redesigned sign-in/register card on WS-0 Field,
pick-your-vibe theme step, invites. Security enforced server-side via better-auth
databaseHooks (invite-gated, single-use; first-user→admin; /api/setup 409 guard) —
reviewed (opus) + re-reviewed clean. server 204 + client 301 tests pass.

Follow-up (non-blocking, tracked for final review): consume-on-validate in the
before-hook to close the narrow invite-reuse/first-user race; fail-closed path check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wraps Virtuoso in the WS-0 LiveLog primitive (role=log, aria-live=polite, aria-label=Messages). Adds [role=log] flex layout in app.css so Virtuoso retains its height. Also migrates grouping tests to MessageRow direct-render (Virtuoso does not render in jsdom), and adds empty-state + skeleton tests with fake-timer settle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…S-0 EmptyState

Replaces inline .msgs-empty block in MainPane with EmptyState("Pick a channel to start chatting") and moves the pre-existing no-messages-yet inline div to EmptyState("No messages yet") in MessageList.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ageList

Adds HistorySkeleton component (role=status, aria-busy=true) shown while waiting for first history page on channel selection (4s settle heuristic). CSS shimmer animation disabled under prefers-reduced-motion (static bg-3 fallback). Token-driven sizing via --msg-fs, --group-gap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dmfarcas and others added 30 commits July 17, 2026 22:50
The Dockerfile installs deps only for client/server/shared, but COPY . .
brings in packages/desktop (added in milestone 8), so 'pnpm -r build' died
on the Electron package. Found by the cold-start audit: the documented
'docker compose up' path did not build at all.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ParsedCommand gained notice/away/back/invite/names in WS-6 but the
composer's dispatchInput switch was never extended: the new commands fell
through (TS2366 broke the production build) and typing them cleared the
input while doing nothing. Wire the five cases to the store actions.

Typecheck clean; 364/364 client tests pass. NOTE: away/invite/names are
still dropped server-side (no ws.ts cases) and chat:send ignores the
notice flag — see docs/superpowers/audits/2026-07-17-cold-start-audit.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First end-to-end use of bool on the shipping path: fresh docker volume,
real signup, real TLS to Libera.Chat, two-sided messaging via a scripted
probe client, container restart mid-session, mobile pass. 7 BOUNCE-class
findings (DM self-keying, no WS reconnect, no session restore, mobile
layout, docker build, dead commands, zero-state routing), 8 FRICTION,
6 POLISH, 3 process findings. Includes reprioritization vs WS-3/5/8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s silent, root cause unknown

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… CI reality gate

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nection visibility, channel browser, previews, orientation, polish)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…by-gap against design/direction-a)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion badges + honest counts (audit B7,F1-F3)
…al (audit process findings)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… on isChannel (Task 9 review fixes)

UserPopover previously received a click-time-frozen networkId but re-derived
channel/canOp from the live `selected` store value, letting a stale popover
(opened from a message sender, then followed by a keyboard channel switch that
fires no mousedown) dispatch Kick/Ban with an empty channel. It also always
showed "Invite to channel..." even when opened from a PM, sending the PM nick
as a fake channel.

Fix: MessageList and MemberList now capture a single frozen snapshot
{networkId, channel, isChannel, canOp} at click time from the current
selected-derived state, and pass it straight through. UserPopover is now a
pure renderer of that snapshot (no more internal useChat(selected) read) and
gates Invite + all op actions on isChannel. Both hosts also close the popover
on selection change as a second guard against the keyboard-switch race.
…ad numerics console (audit F4)

- NetworkState gains state: connecting|registered|reconnecting|closed;
  connected stays derived (state === 'registered') for existing consumers.
- Sidebar shows a zero-jargon status line (Connecting…/Reconnecting…/
  Disconnected) with a Reconnect button on closed, calling connectNetwork(id).
- Root cause of the silent numerics/MOTD pipeline: formatNumeric() didn't
  strip the leading IRCv3 message-tags blob ("@key=val;... ") that
  irc-framework's raw event carries on virtually every real inbound line,
  since irc-framework requests message-tags + server-time by default and
  nearly all modern servers (Libera, ergo, ...) grant them. The numeric
  regex never matched a tag-prefixed line, so 001-005/MOTD/LUSERS were
  silently dropped. Confirmed by reading irc-framework 4.14.0 source (raw
  IS proxied Connection->Client, from_server IS true) and corroborated live
  against both a synthetic CAP-negotiating ircd and a real ergo instance.
…pt (audit F5)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(no cast) (Task 12 review fixes)

- fetchPreview's catch blocks now capture the Error and attach an
  internal-only `reason` string to PreviewResult (never added to the
  zod previewResultSchema wire contract), so manager.ts's
  `[preview] not ok` log carries a diagnosable reason instead of a
  bare URL.
- Retype the SSRF-pinned `lookup` option directly as
  `import('node:net').LookupFunction` — @types/node already models
  both the array and single-address callback forms via
  `options.all`, so the arity-sniffing and the
  `pinnedLookup as unknown as http.RequestOptions['lookup']` double
  cast (which defeated tsc on the exact function enforcing the SSRF
  pin) are no longer needed. Behavior is unchanged: only the
  closure-captured pre-validated IP is ever handed back.
…elp, honest boot log, ? cheatsheet landed (audit POLISH batch)
…ew joins, not reconnect replays (Task 15 review fix)
…r emoji, caps statline

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ate history-start note to channels (final review)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant