Files
gstack/qa/SKILL.md
T
Garry Tan 7665adf4fe feat: headed mode + sidebar agent + Chrome extension (v0.12.0) (#517)
* feat: CDP connect — control real Chrome/Comet via Playwright

Add `connectCDP()` to BrowserManager: connects to a running browser via
Chrome DevTools Protocol. All existing browse commands work unchanged
through Playwright's abstraction layer.

- chrome-launcher.ts: browser discovery, CDP probe, auto-relaunch with rollback
- browser-manager.ts: connectCDP(), mode guards (close/closeTab/recreateContext/handoff),
  auto-reconnect on browser restart, getRefMap() for extension API
- server.ts: CDP branch in start(), /health gains mode field, /refs endpoint,
  idle timer only resets on /command (not passive endpoints)

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

* feat: browse connect/disconnect/focus CLI commands

- connect: pre-server command that discovers browser, starts server in CDP mode
- disconnect: drops CDP connection, restarts in headless mode
- focus: brings browser window to foreground via osascript (macOS)
- status: now shows Mode: cdp | launched | headed
- startServer() accepts extra env vars for CDP URL/port passthrough

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

* feat: CDP-aware skill templates — skip cookie import in real browser mode

Skills now check `$B status` for CDP mode and skip:
- /qa: cookie import prompt, user-agent override, headless workarounds
- /design-review: cookie import for authenticated pages
- /setup-browser-cookies: returns "not needed" in CDP mode

Regenerated SKILL.md files from updated templates.

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

* feat: activity streaming — SSE endpoint for Chrome extension Side Panel

Real-time browse command feed via Server-Sent Events:
- activity.ts: ActivityEntry type, CircularBuffer (capacity 1000), privacy
  filtering (redacts passwords, auth tokens, sensitive URL params),
  cursor-based gap detection, async subscriber notification
- server.ts: /activity/stream SSE, /activity/history REST, handleCommand
  instrumented with command_start/command_end events
- 18 unit tests for filterArgs privacy, emitActivity, subscribe lifecycle

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

* feat: Chrome extension Side Panel + Conductor API proposal

Chrome extension (Manifest V3, sideload):
- Side Panel with live activity feed, @ref overlays, dark terminal aesthetic
- Background worker: health polling, SSE relay, ref fetching
- Popup: port config, connection status, side panel launcher
- Content script: floating ref panel with @ref badges

Conductor API proposal (docs/designs/CONDUCTOR_SESSION_API.md):
- SSE endpoint for full Claude Code session mirroring in Side Panel
- Discovery via HTTP endpoint (not filesystem — extensions can't read files)

TODOS.md: add $B watch, multi-agent tabs, cross-platform CDP, Web Store publishing.
Mark CDP mode as shipped.

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

* fix: detect Conductor runtime, skip osascript quit for sandboxed apps

macOS App Management blocks Electron apps (Conductor) from quitting
other apps via osascript. Now detects the runtime environment:
- terminal/claude-code/codex: can manage apps freely
- conductor: prints manual restart instructions + polls for 60s

detectRuntime() checks env vars and parent process. When Chrome needs
restart but we can't quit it, prints step-by-step instructions and
waits for the user to restart Chrome with --remote-debugging-port.

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

* fix: detect Conductor via actual env vars (CONDUCTOR_WORKSPACE_NAME)

Previous detection checked CONDUCTOR_WORKSPACE_ID which doesn't exist.
Conductor sets CONDUCTOR_WORKSPACE_NAME, CONDUCTOR_BIN_DIR, CONDUCTOR_PORT,
and __CFBundleIdentifier=com.conductor.app. Check these FIRST because
Conductor sessions also have ANTHROPIC_API_KEY (which was matching claude-code).

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

* feat: connection status pill — floating indicator when gstack controls Chrome

Small pill in bottom-right corner of every page: "● gstack · 3 refs"
Shows when connected via CDP, fades to 30% opacity after 3s, full on hover.
Disappears entirely when disconnected.

Background worker now notifies content scripts on connect/disconnect state
changes so the pill appears/disappears without polling.

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

* fix: Chrome requires --user-data-dir for remote debugging

Chrome refuses --remote-debugging-port without an explicit --user-data-dir.
Add userDataDir to BrowserBinary registry (macOS Application Support paths)
and pass it in both auto-launch and manual restart instructions.

Fix double-quoting in CLI manual restart instructions.

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

* fix: Chrome must be fully quit before launching with --remote-debugging-port

Chrome refuses to enable CDP on its default profile when another instance
is running (even with explicit --user-data-dir). The only reliable path:
fully quit Chrome first, then relaunch with the flag.

Updated instructions to emphasize this clearly with verification step.

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

* feat: bin/chrome-cdp — quit Chrome and relaunch with CDP in one command

Quits Chrome gracefully, waits for full exit, relaunches with
--remote-debugging-port, polls until CDP is ready. Usage: chrome-cdp [port]

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

* fix: use Playwright channel:chrome instead of broken connectOverCDP

Playwright's connectOverCDP hangs with Chrome 146 due to CDP protocol
version mismatch. Switch to channel:'chrome' which uses Playwright's
native pipe protocol to launch the system Chrome binary directly.

This is simpler and more reliable:
- No CDP port discovery needed
- No --remote-debugging-port or --user-data-dir hassles
- $B connect just works — launches real Chrome headed window
- All Playwright APIs (snapshot, click, fill) work unchanged

bin/chrome-cdp updated with symlinked profile approach (kept for
manual CDP use cases, but $B connect no longer needs it).

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

* feat: green border + gstack label on controlled Chrome window

Injects a 2px green border and small "gstack" label on every page
loaded in the controlled Chrome window via context.addInitScript().
Users can instantly tell which Chrome window Claude controls.

Also fixes close() for channel:chrome mode (uses browser.close()
not browser.disconnect() which doesn't exist).

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

* chore: cleanup chrome-launcher runtime detection, remove puppeteer-core dep

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

* style(design): redesign controlled Chrome indicator

Replace crude green border + label with polished indicator:
- 2px shimmer gradient at top edge (green→cyan→green, 3s loop)
- Floating pill bottom-right with frosted glass bg, fades to 25%
  opacity after 4s so it doesn't compete with page content
- prefers-reduced-motion disables shimmer animation
- Much more subtle — looks like a developer tool, not broken CSS

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

* docs: document real browser mode + Chrome extension in BROWSER.md and README.md

BROWSER.md: new sections for connect/disconnect/focus commands,
Chrome extension Side Panel install, CDP-aware skills, activity streaming.
Updated command reference table, key components, env vars, source map.

README.md: updated /browse description, added "Real browser mode" to
What's New section.

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

* docs: step-by-step Chrome extension install guide in BROWSER.md

Replace terse bullet points with numbered walkthrough covering:
developer mode toggle, load unpacked, macOS file picker tip (Cmd+Shift+G),
pin extension, configure port, open side panel. Added troubleshooting section.

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

* docs: add Cmd+Shift+. tip for hidden folders in macOS file picker

macOS hides folders starting with . by default. Added both shortcuts:
Cmd+Shift+G (paste path directly) and Cmd+Shift+. (show hidden files).

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

* docs: integrate hidden folder tips into the install flow naturally

Move Cmd+Shift+G and Cmd+Shift+. tips inline with the file picker
step instead of as a separate tip block after it.

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

* feat: auto-load Chrome extension when $B connect launches Chrome

Extension auto-loads via --load-extension flag — no manual chrome://extensions
install needed. findExtensionPath() checks repo root, global install, and dev
paths. Also adds bin/gstack-extension helper for manual install in regular
Chrome, and rewrites BROWSER.md install docs with auto-load as primary path.

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

* feat: /connect-chrome skill — one command to launch Chrome with Side Panel

New skill that runs $B connect, verifies the connection, guides the user
to open the Side Panel, and demos the live activity feed. Extension auto-loads
via --load-extension so no manual chrome://extensions install needed.

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

* fix: use launchPersistentContext for Chrome extension loading

Playwright's chromium.launch() silently ignores --load-extension.
Switch to launchPersistentContext with ignoreDefaultArgs to remove
--disable-extensions flag. Use bundled Chromium (real Chrome blocks
unpacked extensions). Fixed port 34567 for CDP mode so the extension
auto-connects.

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

* feat: sync extension to DESIGN.md — amber accent, zinc neutrals, grain texture

Import design system from gstack-website. Update all extension colors:
green (#4ade80) → amber (#F59E0B/#FBBF24), zinc gray neutrals, grain
texture overlay. Regenerate icons as amber "G" monogram on dark background.

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

* feat: sidebar chat with Claude Code — icon opens side panel directly

Replace popup flyout with direct side panel open on icon click. Primary
UI is now a chat interface that sends messages to Claude Code via file
queue. Activity/Refs tabs moved behind a debug toggle in the footer.
Command bar with history, auto-poll for responses, amber design system.

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

* feat: sidebar agent — Claude-powered chat backend via file queue

Add /sidebar-command, /sidebar-response, and /sidebar-chat endpoints
to the browse server. sidebar-agent.ts watches the command queue file,
spawns claude -p with browse context for each message, and streams
responses back to the sidebar chat.

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

* fix: remove duplicate gstack pill overlay, hide crash restore bubble

The addInitScript indicator and the extension's content script were both
injecting bottom-right pills, causing duplicates. Remove the pill from
addInitScript (extension handles it). Replace --restore-last-session with
--hide-crash-restore-bubble to suppress the "Chromium didn't shut down
correctly" dialog.

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

* fix: state file authority — CDP server cannot be silently replaced

Hardens the connect/disconnect lifecycle:
- ensureServer() refuses to auto-start headless when CDP server is alive
- $B connect does full cleanup: SIGTERM → 2s → SIGKILL, profile locks, state
- shutdown() cleans Chromium SingletonLock/Socket/Cookie files
- uncaughtException/unhandledRejection handlers do emergency cleanup

This prevents the bug where a headless server overwrites the CDP server's
state file, causing $B commands to hit the wrong browser.

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

* feat: sidebar agent streaming events + session state management

Enhance sidebar-agent.ts with:
- Live streaming of claude -p events (tool_use, text, result) to sidebar
- Session state file for BROWSE_STATE_FILE propagation to claude subprocess
- Improved logging (stderr, exit codes, event types)
- stdin.end() to prevent claude waiting for input
- summarizeToolInput() with path shortening for compact sidebar display

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

* feat: sidebar chat UI — streaming events, agent status, reconnect retry

Sidebar panel improvements:
- Chat tab renders streaming agent events (tool_use, text, result)
- Thinking dots animation while agent processes
- Agent error display with styled error blocks
- tryConnect() with 2s retry loop for initial connection
- Debug tabs (Activity/Refs) hidden behind gear toggle
- Clear chat button
- Compact tool call display with path shortening

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

* feat: server-integrated sidebar agent with sessions and message queue

Move the sidebar agent from a separate bun process into server.ts:
- Agent spawns claude -p directly when messages arrive via /sidebar-command
- In-memory chat buffer backed by per-session chat.jsonl on disk
- Session manager: create, load, persist, list sessions
- Message queue (cap 5) with agent status tracking (idle/processing/hung)
- Stop/kill endpoints with queue dismiss support
- /health now returns agent status + session info
- All sidebar endpoints require Bearer auth
- Agent killed on server shutdown
- 120s timeout detects hung claude processes

Eliminates: file-queue polling, separate sidebar-agent.ts process,
stale auth tokens, state file conflicts between processes.

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

* feat: extension auth + token flow for server-integrated agent

Update Chrome extension to use Bearer auth on all sidebar endpoints:
- background.js captures auth token from /health, exposes via getToken msg
- background.js sets openPanelOnActionClick for direct side panel access
- sidepanel.js gets token from background, sends in all fetch headers
- Health broadcasts include token so sidebar auto-authenticates
- Removes popup from manifest — icon click opens side panel directly

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

* feat: self-healing sidebar — reconnect banner, state machine, copy button

Sidebar UI now handles disconnection gracefully:
- Connection state machine: connected → reconnecting → dead
- Amber pulsing banner during reconnect (2s retry, 30 attempts)
- Red "Server offline" banner with Reconnect + Copy /connect-chrome buttons
- Green "Reconnected" toast that fades after 3s on successful reconnect
- Copy button lets user paste /connect-chrome into any Claude Code session

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

* fix: crash handling — save session, kill agent, distinct exit codes

Hardened shutdown/crash behavior:
- Browser disconnect exits with code 2 (distinct from crash code 1)
- emergencyCleanup kills agent subprocess and saves session state
- Clean shutdown saves session before exit (chat history persists)
- Clear user message on browser disconnect: "Run $B connect to reconnect"

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

* feat: worktree-per-session isolation for sidebar agent

Each sidebar session gets an isolated git worktree so the agent's file
operations don't conflict with the user's working directory:
- createWorktree() creates detached HEAD worktree in ~/.gstack/worktrees/
- Falls back to main cwd for non-git repos or on creation failure
- Handles collision cleanup from prior crashes
- removeWorktree() cleans up on session switch and shutdown
- worktreePath persisted in session.json

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

* fix(qa): ISSUE-001 — disconnect blocked by CDP guard in ensureServer

$B disconnect was routed through ensureServer() which refused to start a
headless server when a CDP state file existed. Disconnect is now handled
before ensureServer() (like connect), with force-kill + cleanup fallback
when the CDP server is unresponsive.

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

* fix: resolve claude binary path for daemon-spawned agent

The browse server runs as a daemon and may not inherit the user's shell
PATH. Add findClaudeBin() that checks ~/.local/bin/claude (standard
install location), which claude, and common system paths. Shows a clear
error in the sidebar chat if claude CLI is not found.

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

* fix: resolve claude symlinks + check Conductor bundled binary

posix_spawn fails on symlinks in compiled bun binaries. Now:
- Checks Conductor app's bundled binary first (not a symlink)
- Scans ~/.local/share/claude/versions/ for direct versioned binaries
- Uses fs.realpathSync() to resolve symlinks before spawning

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

* fix: compiled bun binary cannot posix_spawn — use external agent process

Compiled bun binaries fail posix_spawn on ALL executables (even /bin/bash).
The server now writes to an agent queue file, and a separate non-compiled
bun process (sidebar-agent.ts) reads the queue, spawns claude, and POSTs
events back via /sidebar-agent/event.

Changes:
- server.ts: spawnClaude writes to queue file instead of spawning directly
- server.ts: new /sidebar-agent/event endpoint for agent → server relay
- server.ts: fix result event field name (event.text vs event.result)
- sidebar-agent.ts: rewritten to poll queue file, relay events via HTTP
- cli.ts: $B connect auto-starts sidebar-agent as non-compiled bun process

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

* feat: loading spinner on sidebar open while connecting to server

Shows an amber spinner with "Connecting..." when the sidebar first opens,
replacing the empty state. After the first successful /sidebar-chat poll:
- If chat history exists: renders it immediately
- If no history: shows the welcome message

Prevents the jarring empty-then-populated flash on sidebar open.

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

* feat: zero-friction side panel — auto-open on install, pill is clickable

Three changes to eliminate manual side panel setup:
- Auto-open side panel on extension install/update (onInstalled listener)
- gstack pill (bottom-right) is now clickable — opens the side panel
- Pill has pointer-events: auto so clicks always register (was: none)

User no longer needs to find the puzzle piece icon, pin the extension,
or know the side panel exists. It opens automatically on first launch
and can be re-opened by clicking the floating gstack pill.

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

* refactor: kill CDP naming, delete chrome-launcher.ts dead code

The connectCDP() method and connectionMode: 'cdp' naming was a legacy
artifact — real Chrome was tried but failed (silently blocks
--load-extension), so the implementation already used Playwright's
bundled Chromium via launchPersistentContext(). The naming was
misleading.

Changes:
- Delete chrome-launcher.ts (361 LOC) — only import was in unreachable
  attemptReconnect() method
- Delete dead attemptReconnect() and reconnecting field
- Delete preExistingTabIds (was for protecting real Chrome tabs we
  never connect to)
- Rename connectCDP() → launchHeaded()
- Rename connectionMode: 'cdp' → 'headed' across all files
- Replace BROWSE_CDP_URL/BROWSE_CDP_PORT env vars with BROWSE_HEADED=1
- Regenerate SKILL.md files for updated command descriptions
- Move BrowserManager unit tests to browser-manager-unit.test.ts

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

* feat: converge handoff into connect — extension loads on handoff

Handoff now uses launchPersistentContext() with extension auto-loading,
same as the connect/launchHeaded() path. This means when the agent
gets stuck (2FA, CAPTCHA) and hands off to the user, the Chrome
extension + side panel are available automatically.

Before: handoff used chromium.launch() + newContext() — no extension
After: handoff uses chromium.launchPersistentContext() — extension loads

Also sets connectionMode to 'headed' and disables dialog auto-accept
on handoff, matching connect behavior.

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

* feat: gate sidebar chat behind --chat flag

$B connect (default): headed Chromium + extension with Activity + Refs
tabs only. No separate agent spawned. Clean, no confusion.

$B connect --chat: same + Chat tab with standalone claude -p agent.
Shows experimental banner: "Standalone mode — this is a separate
agent from your workspace."

Implementation:
- cli.ts: parse --chat, set BROWSE_SIDEBAR_CHAT env, conditionally
  spawn sidebar-agent
- server.ts: gate /sidebar-* routes behind chatEnabled, return 403
  when disabled, include chatEnabled in /health response
- sidepanel.js: applyChatEnabled() hides/shows Chat tab + banner
- background.js: forward chatEnabled from health response
- sidepanel.html/css: experimental banner with amber styling

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

* feat: file drop relay + $B inbox command

Sidebar agent now writes structured messages to .context/sidebar-inbox/
when processing user input. The workspace agent can read these via
$B inbox to see what the user reported from the browser.

File drop format:
  .context/sidebar-inbox/{timestamp}-observation.json
  { type, timestamp, page: {url}, userMessage, sidebarSessionId }

Atomic writes (tmp + rename) prevent partial reads. $B inbox --clear
removes messages after display.

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

* feat: $B watch — passive observation mode

Claude enters read-only mode and captures periodic snapshots (every 5s)
while the user browses. Mutation commands (click, fill, etc.) are
blocked during watch. $B watch stop exits and returns a summary with
the last snapshot.

Requires headed mode ($B connect). This is the inverse of the scout
pattern — the workspace agent watches through the browser instead of
the sidebar relaying to it.

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

* test: add coverage for sidebar-agent, file-drop, and watch mode

33 new tests covering:
- Sidebar agent queue parsing (valid/malformed/empty JSONL)
- writeToInbox file drop (directory creation, atomic writes, JSON format)
- Inbox command (display, sorting, --clear, malformed file handling)
- Watch mode state machine (start/stop cycles, snapshots, duration)

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

* docs: TODOS cleanup + Chrome vs Chromium exploration doc

- Update TODOS.md: mark CDP mode, $B watch, sidebar scout as SHIPPED
- Delete dead "cross-platform CDP browser discovery" TODO
- Rename dependencies from "CDP connect" to "headed mode"
- Add docs/designs/CHROME_VS_CHROMIUM_EXPLORATION.md memorializing
  the architecture exploration and decision to use Playwright Chromium

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

* docs: add Conductor Chrome sidebar integration design doc

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

* fix: sidebar-agent validates cwd before spawning claude

The queue entry may reference a worktree that was cleaned up between
sessions. Now falls back to process.cwd() if the path doesn't exist,
preventing silent spawn failures.

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

* fix: gen-skill-docs resolver merge + preamble tier gate + plan file discovery

The local RESOLVERS record in gen-skill-docs.ts was shadowing the imported
canonical resolvers, causing stale test coverage and preamble generators
to be used instead of the authoritative versions in resolvers/.

Changes:
- Merge imported RESOLVERS with local overrides (spread + override pattern)
- Fix preamble tier gate: tier 1 skills no longer get AskUserQuestion format
- Make plan file discovery host-agnostic (search multiple plan dirs)
- Add missing E2E tier entries for ship/review plan completion tests

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

* feat: ungate sidebar agent + raise timeout to 5 minutes (v0.12.0)

Sidebar chat is now always available in headed mode — no --chat flag needed.
Agent tasks get 5 minutes instead of 2, enabling multi-page workflows like
navigating directories and filling forms across pages.

Changes:
- cli.ts: remove --chat flag, always set BROWSE_SIDEBAR_CHAT=1, always spawn agent
- server.ts: remove chatEnabled gate (403 response), raise AGENT_TIMEOUT_MS to 300s
- sidebar-agent.ts: raise child process timeout from 120s to 300s

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

* docs: headed mode + sidebar agent documentation (v0.12.0)

- README: sidebar agent section, personal automation example (school parent
  portal), two auth paths (manual login + cookie import), DevTools MCP mention
- BROWSER.md: sidebar agent section with usage, timeout, session isolation,
  authentication, and random delay documentation
- connect-chrome template: add sidebar chat onboarding step
- CHANGELOG: v0.12.0 entry covering headed mode, sidebar agent, extension
- VERSION: bump to 0.12.0.0
- TODOS: Chrome DevTools MCP integration as P0

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

* chore: regenerate SKILL.md files

Generated from updated templates + resolver merge. Key changes:
- Tier 1 skills no longer include AskUserQuestion format section
- Ship/review skills now include coverage gate with thresholds
- Connect-chrome skill includes sidebar chat onboarding step
- Plan file discovery uses host-agnostic paths

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

* chore: regenerate Codex connect-chrome skill

Updated preamble with proactive prompt and sidebar chat onboarding step.

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

* feat: network idle, state persistence, iframe support, chain pipe format (v0.12.1.0) (#516)

* feat: network idle detection + chain pipe format

- Upgrade click/fill/select from domcontentloaded to networkidle wait
  (2s timeout, best-effort). Catches XHR/fetch triggered by interactions.
- Add pipe-delimited format to chain as JSON fallback:
  $B chain 'goto url | click @e5 | snapshot -ic'
- Add post-loop networkidle wait in chain when last command was a write.
- Frame-aware: commands use target (getActiveFrameOrPage) for locator ops,
  page-only ops (goto/back/forward/reload) guard against frame context.

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

* feat: $B state save/load + $B frame — new browse commands

- state save/load: persist cookies + URLs to .gstack/browse-states/{name}.json
  File perms 0o600, name sanitized to [a-zA-Z0-9_-]. V1 skips localStorage
  (breaks on load-before-navigate). Load replaces session via closeAllPages().
- frame: switch command context to iframe via CSS selector, @ref, --name, or
  --url. 'frame main' returns to main frame. Execution target abstraction
  (getActiveFrameOrPage) across read-commands, snapshot, and write-commands.
- Frame context cleared on tab switch, navigation, resume, and handoff.
- Snapshot shows [Context: iframe src="..."] header when in frame.

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

* test: add tests for network idle, chain pipe format, state, and frame

- Network idle: click on fetch button waits for XHR, static click is fast
- Chain pipe: pipe-delimited commands, quoted args, JSON still works
- State: save/load round-trip, name sanitization, missing state error
- Frame: switch to iframe + back, snapshot context header, fill in frame,
  goto-in-frame guard, usage error

New fixtures: network-idle.html (fetch + static buttons), iframe.html (srcdoc)

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

* fix: review fixes — iframe ref scoping, detached frame recovery, state validation

- snapshot.ts: ref locators, cursor-interactive scan, and cursor locator
  now use target (frame-aware) instead of page — fixes @ref clicking in iframes
- browser-manager.ts: getActiveFrameOrPage auto-recovers from detached frames
  via isDetached() check
- meta-commands.ts: state load resets activeFrame, elementHandle disposed after
  contentFrame(), state file schema validation (cookies + pages arrays),
  filter empty pipe segments in chain tokenizer
- write-commands.ts: upload command uses target.locator() for frame support

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

* chore: regenerate SKILL.md files + rebuild binary

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

* chore: bump version and changelog (v0.12.1.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:15:24 -06:00

44 KiB
Raw Blame History

name, preamble-tier, version, description, allowed-tools
name preamble-tier version description allowed-tools
qa 4 2.0.0 Systematically QA test a web application and fix bugs found. Runs QA testing, then iteratively fixes bugs in source code, committing each fix atomically and re-verifying. Use when asked to "qa", "QA", "test this site", "find bugs", "test and fix", or "fix what's broken". Proactively suggest when the user says a feature is ready for testing or asks "does this work?". Three tiers: Quick (critical/high only), Standard (+ medium), Exhaustive (+ cosmetic). Produces before/after health scores, fix evidence, and a ship-readiness summary. For report-only mode, use /qa-only.
Bash
Read
Write
Edit
Glob
Grep
AskUserQuestion
WebSearch

Preamble (run first)

_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true)
[ -n "$_UPD" ] && echo "$_UPD" || true
mkdir -p ~/.gstack/sessions
touch ~/.gstack/sessions/"$PPID"
_SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr -d ' ')
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
echo "BRANCH: $_BRANCH"
echo "PROACTIVE: $_PROACTIVE"
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
_TEL_PROMPTED=$([ -f ~/.gstack/.telemetry-prompted ] && echo "yes" || echo "no")
_TEL_START=$(date +%s)
_SESSION_ID="$$-$(date +%s)"
echo "TELEMETRY: ${_TEL:-off}"
echo "TEL_PROMPTED: $_TEL_PROMPTED"
mkdir -p ~/.gstack/analytics
echo '{"skill":"qa","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")'"}'  >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
# zsh-compatible: use find instead of glob to avoid NOMATCH error
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done

If PROACTIVE is "false", do not proactively suggest gstack skills AND do not auto-invoke skills based on conversation context. Only run skills the user explicitly types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say: "I think /skillname might help here — want me to run it?" and wait for confirmation. The user opted out of proactive behavior.

If output shows UPGRADE_AVAILABLE <old> <new>: read ~/.claude/skills/gstack/gstack-upgrade/SKILL.md and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If JUST_UPGRADED <from> <to>: tell user "Running gstack v{to} (just updated!)" and continue.

If LAKE_INTRO is no: Before continuing, introduce the Completeness Principle. Tell the user: "gstack follows the Boil the Lake principle — always do the complete thing when AI makes the marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Then offer to open the essay in their default browser:

open https://garryslist.org/posts/boil-the-ocean
touch ~/.gstack/.completeness-intro-seen

Only run open if the user says yes. Always run touch to mark as seen. This only happens once.

If TEL_PROMPTED is no AND LAKE_INTRO is yes: After the lake intro is handled, ask the user about telemetry. Use AskUserQuestion:

Help gstack get better! Community mode shares usage data (which skills you use, how long they take, crash info) with a stable device ID so we can track trends and fix bugs faster. No code, file paths, or repo names are ever sent. Change anytime with gstack-config set telemetry off.

Options:

  • A) Help gstack get better! (recommended)
  • B) No thanks

If A: run ~/.claude/skills/gstack/bin/gstack-config set telemetry community

If B: ask a follow-up AskUserQuestion:

How about anonymous mode? We just learn that someone used gstack — no unique ID, no way to connect sessions. Just a counter that helps us know if anyone's out there.

Options:

  • A) Sure, anonymous is fine
  • B) No thanks, fully off

If B→A: run ~/.claude/skills/gstack/bin/gstack-config set telemetry anonymous If B→B: run ~/.claude/skills/gstack/bin/gstack-config set telemetry off

Always run:

touch ~/.gstack/.telemetry-prompted

This only happens once. If TEL_PROMPTED is yes, skip this entirely.

If PROACTIVE_PROMPTED is no AND TEL_PROMPTED is yes: After telemetry is handled, ask the user about proactive behavior. Use AskUserQuestion:

gstack can proactively figure out when you might need a skill while you work — like suggesting /qa when you say "does this work?" or /investigate when you hit a bug. We recommend keeping this on — it speeds up every part of your workflow.

Options:

  • A) Keep it on (recommended)
  • B) Turn it off — I'll type /commands myself

If A: run ~/.claude/skills/gstack/bin/gstack-config set proactive true If B: run ~/.claude/skills/gstack/bin/gstack-config set proactive false

Always run:

touch ~/.gstack/.proactive-prompted

This only happens once. If PROACTIVE_PROMPTED is yes, skip this entirely.

AskUserQuestion Format

ALWAYS follow this structure for every AskUserQuestion call:

  1. Re-ground: State the project, the current branch (use the _BRANCH value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
  2. Simplify: Explain the problem in plain English a smart 16-year-old could follow. No raw function names, no internal jargon, no implementation details. Use concrete examples and analogies. Say what it DOES, not what it's called.
  3. Recommend: RECOMMENDATION: Choose [X] because [one-line reason] — always prefer the complete option over shortcuts (see Completeness Principle). Include Completeness: X/10 for each option. Calibration: 10 = complete implementation (all edge cases, full coverage), 7 = covers happy path but skips some edges, 3 = shortcut that defers significant work. If both options are 8+, pick the higher; if one is ≤5, flag it.
  4. Options: Lettered options: A) ... B) ... C) ... — when an option involves effort, show both scales: (human: ~X / CC: ~Y)

Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.

Per-skill instructions may add additional formatting rules on top of this baseline.

Completeness Principle — Boil the Lake

AI makes completeness near-free. Always recommend the complete option over shortcuts — the delta is minutes with CC+gstack. A "lake" (100% coverage, all edge cases) is boilable; an "ocean" (full rewrite, multi-quarter migration) is not. Boil lakes, flag oceans.

Effort reference — always show both scales:

Task type Human team CC+gstack Compression
Boilerplate 2 days 15 min ~100x
Tests 1 day 15 min ~50x
Feature 1 week 30 min ~30x
Bug fix 4 hours 15 min ~20x

Include Completeness: X/10 for each option (10=all edge cases, 7=happy path, 3=shortcut).

Repo Ownership — See Something, Say Something

REPO_MODE controls how to handle issues outside your branch:

  • solo — You own everything. Investigate and offer to fix proactively.
  • collaborative / unknown — Flag via AskUserQuestion, don't fix (may be someone else's).

Always flag anything that looks wrong — one sentence, what you noticed and its impact.

Search Before Building

Before building anything unfamiliar, search first. See ~/.claude/skills/gstack/ETHOS.md.

  • Layer 1 (tried and true) — don't reinvent. Layer 2 (new and popular) — scrutinize. Layer 3 (first principles) — prize above all.

Eureka: When first-principles reasoning contradicts conventional wisdom, name it and log:

jq -n --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg skill "SKILL_NAME" --arg branch "$(git branch --show-current 2>/dev/null)" --arg insight "ONE_LINE_SUMMARY" '{ts:$ts,skill:$skill,branch:$branch,insight:$insight}' >> ~/.gstack/analytics/eureka.jsonl 2>/dev/null || true

Contributor Mode

If _CONTRIB is true: you are in contributor mode. At the end of each major workflow step, rate your gstack experience 0-10. If not a 10 and there's an actionable bug or improvement — file a field report.

File only: gstack tooling bugs where the input was reasonable but gstack failed. Skip: user app bugs, network errors, auth failures on user's site.

To file: write ~/.gstack/contributor-logs/{slug}.md:

# {Title}
**What I tried:** {action} | **What happened:** {result} | **Rating:** {0-10}
## Repro
1. {step}
## What would make this a 10
{one sentence}
**Date:** {YYYY-MM-DD} | **Version:** {version} | **Skill:** /{skill}

Slug: lowercase hyphens, max 60 chars. Skip if exists. Max 3/session. File inline, don't stop.

Completion Status Protocol

When completing a skill workflow, report status using one of:

  • DONE — All steps completed successfully. Evidence provided for each claim.
  • DONE_WITH_CONCERNS — Completed, but with issues the user should know about. List each concern.
  • BLOCKED — Cannot proceed. State what is blocking and what was tried.
  • NEEDS_CONTEXT — Missing information required to continue. State exactly what you need.

Escalation

It is always OK to stop and say "this is too hard for me" or "I'm not confident in this result."

Bad work is worse than no work. You will not be penalized for escalating.

  • If you have attempted a task 3 times without success, STOP and escalate.
  • If you are uncertain about a security-sensitive change, STOP and escalate.
  • If the scope of work exceeds what you can verify, STOP and escalate.

Escalation format:

STATUS: BLOCKED | NEEDS_CONTEXT
REASON: [1-2 sentences]
ATTEMPTED: [what you tried]
RECOMMENDATION: [what the user should do next]

Telemetry (run last)

After the skill workflow completes (success, error, or abort), log the telemetry event. Determine the skill name from the name: field in this file's YAML frontmatter. Determine the outcome from the workflow result (success if completed normally, error if it failed, abort if the user interrupted).

PLAN MODE EXCEPTION — ALWAYS RUN: This command writes telemetry to ~/.gstack/analytics/ (user config directory, not project files). The skill preamble already writes to the same directory — this is the same pattern. Skipping this command loses session duration and outcome data.

Run this bash:

_TEL_END=$(date +%s)
_TEL_DUR=$(( _TEL_END - _TEL_START ))
rm -f ~/.gstack/analytics/.pending-"$_SESSION_ID" 2>/dev/null || true
~/.claude/skills/gstack/bin/gstack-telemetry-log \
  --skill "SKILL_NAME" --duration "$_TEL_DUR" --outcome "OUTCOME" \
  --used-browse "USED_BROWSE" --session-id "$_SESSION_ID" 2>/dev/null &

Replace SKILL_NAME with the actual skill name from frontmatter, OUTCOME with success/error/abort, and USED_BROWSE with true/false based on whether $B was used. If you cannot determine the outcome, use "unknown". This runs in the background and never blocks the user.

When you are in plan mode and about to call ExitPlanMode:

  1. Check if the plan file already has a ## GSTACK REVIEW REPORT section.
  2. If it DOES — skip (a review skill already wrote a richer report).
  3. If it does NOT — run this command:

```bash ~/.claude/skills/gstack/bin/gstack-review-read ```

Then write a ## GSTACK REVIEW REPORT section to the end of the plan file:

  • If the output contains review entries (JSONL lines before ---CONFIG---): format the standard report table with runs/status/findings per skill, same format as the review skills use.
  • If the output is NO_REVIEWS or empty: write this placeholder table:

```markdown

GSTACK REVIEW REPORT

Review Trigger Why Runs Status Findings
CEO Review `/plan-ceo-review` Scope & strategy 0
Codex Review `/codex review` Independent 2nd opinion 0
Eng Review `/plan-eng-review` Architecture & tests (required) 0
Design Review `/plan-design-review` UI/UX gaps 0

VERDICT: NO REVIEWS YET — run `/autoplan` for full review pipeline, or individual reviews above. ```

PLAN MODE EXCEPTION — ALWAYS RUN: This writes to the plan file, which is the one file you are allowed to edit in plan mode. The plan file review report is part of the plan's living status.

Step 0: Detect platform and base branch

First, detect the git hosting platform from the remote URL:

git remote get-url origin 2>/dev/null
  • If the URL contains "github.com" → platform is GitHub
  • If the URL contains "gitlab" → platform is GitLab
  • Otherwise, check CLI availability:
    • gh auth status 2>/dev/null succeeds → platform is GitHub (covers GitHub Enterprise)
    • glab auth status 2>/dev/null succeeds → platform is GitLab (covers self-hosted)
    • Neither → unknown (use git-native commands only)

Determine which branch this PR/MR targets, or the repo's default branch if no PR/MR exists. Use the result as "the base branch" in all subsequent steps.

If GitHub:

  1. gh pr view --json baseRefName -q .baseRefName — if succeeds, use it
  2. gh repo view --json defaultBranchRef -q .defaultBranchRef.name — if succeeds, use it

If GitLab:

  1. glab mr view -F json 2>/dev/null and extract the target_branch field — if succeeds, use it
  2. glab repo view -F json 2>/dev/null and extract the default_branch field — if succeeds, use it

Git-native fallback (if unknown platform, or CLI commands fail):

  1. git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||'
  2. If that fails: git rev-parse --verify origin/main 2>/dev/null → use main
  3. If that fails: git rev-parse --verify origin/master 2>/dev/null → use master

If all fail, fall back to main.

Print the detected base branch name. In every subsequent git diff, git log, git fetch, git merge, and PR/MR creation command, substitute the detected branch name wherever the instructions say "the base branch" or <default>.


/qa: Test → Fix → Verify

You are a QA engineer AND a bug-fix engineer. Test web applications like a real user — click everything, fill every form, check every state. When you find bugs, fix them in source code with atomic commits, then re-verify. Produce a structured report with before/after evidence.

Setup

Parse the user's request for these parameters:

Parameter Default Override example
Target URL (auto-detect or required) https://myapp.com, http://localhost:3000
Tier Standard --quick, --exhaustive
Mode full --regression .gstack/qa-reports/baseline.json
Output dir .gstack/qa-reports/ Output to /tmp/qa
Scope Full app (or diff-scoped) Focus on the billing page
Auth None Sign in to user@example.com, Import cookies from cookies.json

Tiers determine which issues get fixed:

  • Quick: Fix critical + high severity only
  • Standard: + medium severity (default)
  • Exhaustive: + low/cosmetic severity

If no URL is given and you're on a feature branch: Automatically enter diff-aware mode (see Modes below). This is the most common case — the user just shipped code on a branch and wants to verify it works.

CDP mode detection: Before starting, check if the browse server is connected to the user's real browser:

$B status 2>/dev/null | grep -q "Mode: cdp" && echo "CDP_MODE=true" || echo "CDP_MODE=false"

If CDP_MODE=true: skip cookie import prompts (the real browser already has cookies), skip user-agent overrides (real browser has real user-agent), and skip headless detection workarounds. The user's real auth sessions are already available.

Check for clean working tree:

git status --porcelain

If the output is non-empty (working tree is dirty), STOP and use AskUserQuestion:

"Your working tree has uncommitted changes. /qa needs a clean tree so each bug fix gets its own atomic commit."

  • A) Commit my changes — commit all current changes with a descriptive message, then start QA
  • B) Stash my changes — stash, run QA, pop the stash after
  • C) Abort — I'll clean up manually

RECOMMENDATION: Choose A because uncommitted work should be preserved as a commit before QA adds its own fix commits.

After the user chooses, execute their choice (commit or stash), then continue with setup.

Find the browse binary:

SETUP (run this check BEFORE any browse command)

_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
B=""
[ -n "$_ROOT" ] && [ -x "$_ROOT/.claude/skills/gstack/browse/dist/browse" ] && B="$_ROOT/.claude/skills/gstack/browse/dist/browse"
[ -z "$B" ] && B=~/.claude/skills/gstack/browse/dist/browse
if [ -x "$B" ]; then
  echo "READY: $B"
else
  echo "NEEDS_SETUP"
fi

If NEEDS_SETUP:

  1. Tell the user: "gstack browse needs a one-time build (~10 seconds). OK to proceed?" Then STOP and wait.
  2. Run: cd <SKILL_DIR> && ./setup
  3. If bun is not installed: curl -fsSL https://bun.sh/install | bash

Check test framework (bootstrap if needed):

Test Framework Bootstrap

Detect existing test framework and project runtime:

# Detect project runtime
[ -f Gemfile ] && echo "RUNTIME:ruby"
[ -f package.json ] && echo "RUNTIME:node"
[ -f requirements.txt ] || [ -f pyproject.toml ] && echo "RUNTIME:python"
[ -f go.mod ] && echo "RUNTIME:go"
[ -f Cargo.toml ] && echo "RUNTIME:rust"
[ -f composer.json ] && echo "RUNTIME:php"
[ -f mix.exs ] && echo "RUNTIME:elixir"
# Detect sub-frameworks
[ -f Gemfile ] && grep -q "rails" Gemfile 2>/dev/null && echo "FRAMEWORK:rails"
[ -f package.json ] && grep -q '"next"' package.json 2>/dev/null && echo "FRAMEWORK:nextjs"
# Check for existing test infrastructure
ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini pyproject.toml phpunit.xml 2>/dev/null
ls -d test/ tests/ spec/ __tests__/ cypress/ e2e/ 2>/dev/null
# Check opt-out marker
[ -f .gstack/no-test-bootstrap ] && echo "BOOTSTRAP_DECLINED"

If test framework detected (config files or test directories found): Print "Test framework detected: {name} ({N} existing tests). Skipping bootstrap." Read 2-3 existing test files to learn conventions (naming, imports, assertion style, setup patterns). Store conventions as prose context for use in Phase 8e.5 or Step 3.4. Skip the rest of bootstrap.

If BOOTSTRAP_DECLINED appears: Print "Test bootstrap previously declined — skipping." Skip the rest of bootstrap.

If NO runtime detected (no config files found): Use AskUserQuestion: "I couldn't detect your project's language. What runtime are you using?" Options: A) Node.js/TypeScript B) Ruby/Rails C) Python D) Go E) Rust F) PHP G) Elixir H) This project doesn't need tests. If user picks H → write .gstack/no-test-bootstrap and continue without tests.

If runtime detected but no test framework — bootstrap:

B2. Research best practices

Use WebSearch to find current best practices for the detected runtime:

  • "[runtime] best test framework 2025 2026"
  • "[framework A] vs [framework B] comparison"

If WebSearch is unavailable, use this built-in knowledge table:

Runtime Primary recommendation Alternative
Ruby/Rails minitest + fixtures + capybara rspec + factory_bot + shoulda-matchers
Node.js vitest + @testing-library jest + @testing-library
Next.js vitest + @testing-library/react + playwright jest + cypress
Python pytest + pytest-cov unittest
Go stdlib testing + testify stdlib only
Rust cargo test (built-in) + mockall
PHP phpunit + mockery pest
Elixir ExUnit (built-in) + ex_machina

B3. Framework selection

Use AskUserQuestion: "I detected this is a [Runtime/Framework] project with no test framework. I researched current best practices. Here are the options: A) [Primary] — [rationale]. Includes: [packages]. Supports: unit, integration, smoke, e2e B) [Alternative] — [rationale]. Includes: [packages] C) Skip — don't set up testing right now RECOMMENDATION: Choose A because [reason based on project context]"

If user picks C → write .gstack/no-test-bootstrap. Tell user: "If you change your mind later, delete .gstack/no-test-bootstrap and re-run." Continue without tests.

If multiple runtimes detected (monorepo) → ask which runtime to set up first, with option to do both sequentially.

B4. Install and configure

  1. Install the chosen packages (npm/bun/gem/pip/etc.)
  2. Create minimal config file
  3. Create directory structure (test/, spec/, etc.)
  4. Create one example test matching the project's code to verify setup works

If package installation fails → debug once. If still failing → revert with git checkout -- package.json package-lock.json (or equivalent for the runtime). Warn user and continue without tests.

B4.5. First real tests

Generate 3-5 real tests for existing code:

  1. Find recently changed files: git log --since=30.days --name-only --format="" | sort | uniq -c | sort -rn | head -10
  2. Prioritize by risk: Error handlers > business logic with conditionals > API endpoints > pure functions
  3. For each file: Write one test that tests real behavior with meaningful assertions. Never expect(x).toBeDefined() — test what the code DOES.
  4. Run each test. Passes → keep. Fails → fix once. Still fails → delete silently.
  5. Generate at least 1 test, cap at 5.

Never import secrets, API keys, or credentials in test files. Use environment variables or test fixtures.

B5. Verify

# Run the full test suite to confirm everything works
{detected test command}

If tests fail → debug once. If still failing → revert all bootstrap changes and warn user.

B5.5. CI/CD pipeline

# Check CI provider
ls -d .github/ 2>/dev/null && echo "CI:github"
ls .gitlab-ci.yml .circleci/ bitrise.yml 2>/dev/null

If .github/ exists (or no CI detected — default to GitHub Actions): Create .github/workflows/test.yml with:

  • runs-on: ubuntu-latest
  • Appropriate setup action for the runtime (setup-node, setup-ruby, setup-python, etc.)
  • The same test command verified in B5
  • Trigger: push + pull_request

If non-GitHub CI detected → skip CI generation with note: "Detected {provider} — CI pipeline generation supports GitHub Actions only. Add test step to your existing pipeline manually."

B6. Create TESTING.md

First check: If TESTING.md already exists → read it and update/append rather than overwriting. Never destroy existing content.

Write TESTING.md with:

  • Philosophy: "100% test coverage is the key to great vibe coding. Tests let you move fast, trust your instincts, and ship with confidence — without them, vibe coding is just yolo coding. With tests, it's a superpower."
  • Framework name and version
  • How to run tests (the verified command from B5)
  • Test layers: Unit tests (what, where, when), Integration tests, Smoke tests, E2E tests
  • Conventions: file naming, assertion style, setup/teardown patterns

B7. Update CLAUDE.md

First check: If CLAUDE.md already has a ## Testing section → skip. Don't duplicate.

Append a ## Testing section:

  • Run command and test directory
  • Reference to TESTING.md
  • Test expectations:
    • 100% test coverage is the goal — tests make vibe coding safe
    • When writing new functions, write a corresponding test
    • When fixing a bug, write a regression test
    • When adding error handling, write a test that triggers the error
    • When adding a conditional (if/else, switch), write tests for BOTH paths
    • Never commit code that makes existing tests fail

B8. Commit

git status --porcelain

Only commit if there are changes. Stage all bootstrap files (config, test directory, TESTING.md, CLAUDE.md, .github/workflows/test.yml if created): git commit -m "chore: bootstrap test framework ({framework name})"


Create output directories:

mkdir -p .gstack/qa-reports/screenshots

Test Plan Context

Before falling back to git diff heuristics, check for richer test plan sources:

  1. Project-scoped test plans: Check ~/.gstack/projects/ for recent *-test-plan-*.md files for this repo
    eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
    ls -t ~/.gstack/projects/$SLUG/*-test-plan-*.md 2>/dev/null | head -1
    
  2. Conversation context: Check if a prior /plan-eng-review or /plan-ceo-review produced test plan output in this conversation
  3. Use whichever source is richer. Fall back to git diff analysis only if neither is available.

Phases 1-6: QA Baseline

Modes

Diff-aware (automatic when on a feature branch with no URL)

This is the primary mode for developers verifying their work. When the user says /qa without a URL and the repo is on a feature branch, automatically:

  1. Analyze the branch diff to understand what changed:

    git diff main...HEAD --name-only
    git log main..HEAD --oneline
    
  2. Identify affected pages/routes from the changed files:

    • Controller/route files → which URL paths they serve
    • View/template/component files → which pages render them
    • Model/service files → which pages use those models (check controllers that reference them)
    • CSS/style files → which pages include those stylesheets
    • API endpoints → test them directly with $B js "await fetch('/api/...')"
    • Static pages (markdown, HTML) → navigate to them directly

    If no obvious pages/routes are identified from the diff: Do not skip browser testing. The user invoked /qa because they want browser-based verification. Fall back to Quick mode — navigate to the homepage, follow the top 5 navigation targets, check console for errors, and test any interactive elements found. Backend, config, and infrastructure changes affect app behavior — always verify the app still works.

  3. Detect the running app — check common local dev ports:

    $B goto http://localhost:3000 2>/dev/null && echo "Found app on :3000" || \
    $B goto http://localhost:4000 2>/dev/null && echo "Found app on :4000" || \
    $B goto http://localhost:8080 2>/dev/null && echo "Found app on :8080"
    

    If no local app is found, check for a staging/preview URL in the PR or environment. If nothing works, ask the user for the URL.

  4. Test each affected page/route:

    • Navigate to the page
    • Take a screenshot
    • Check console for errors
    • If the change was interactive (forms, buttons, flows), test the interaction end-to-end
    • Use snapshot -D before and after actions to verify the change had the expected effect
  5. Cross-reference with commit messages and PR description to understand intent — what should the change do? Verify it actually does that.

  6. Check TODOS.md (if it exists) for known bugs or issues related to the changed files. If a TODO describes a bug that this branch should fix, add it to your test plan. If you find a new bug during QA that isn't in TODOS.md, note it in the report.

  7. Report findings scoped to the branch changes:

    • "Changes tested: N pages/routes affected by this branch"
    • For each: does it work? Screenshot evidence.
    • Any regressions on adjacent pages?

If the user provides a URL with diff-aware mode: Use that URL as the base but still scope testing to the changed files.

Full (default when URL is provided)

Systematic exploration. Visit every reachable page. Document 5-10 well-evidenced issues. Produce health score. Takes 5-15 minutes depending on app size.

Quick (--quick)

30-second smoke test. Visit homepage + top 5 navigation targets. Check: page loads? Console errors? Broken links? Produce health score. No detailed issue documentation.

Regression (--regression <baseline>)

Run full mode, then load baseline.json from a previous run. Diff: which issues are fixed? Which are new? What's the score delta? Append regression section to report.


Workflow

Phase 1: Initialize

  1. Find browse binary (see Setup above)
  2. Create output directories
  3. Copy report template from qa/templates/qa-report-template.md to output dir
  4. Start timer for duration tracking

Phase 2: Authenticate (if needed)

If the user specified auth credentials:

$B goto <login-url>
$B snapshot -i                    # find the login form
$B fill @e3 "user@example.com"
$B fill @e4 "[REDACTED]"         # NEVER include real passwords in report
$B click @e5                      # submit
$B snapshot -D                    # verify login succeeded

If the user provided a cookie file:

$B cookie-import cookies.json
$B goto <target-url>

If 2FA/OTP is required: Ask the user for the code and wait.

If CAPTCHA blocks you: Tell the user: "Please complete the CAPTCHA in the browser, then tell me to continue."

Phase 3: Orient

Get a map of the application:

$B goto <target-url>
$B snapshot -i -a -o "$REPORT_DIR/screenshots/initial.png"
$B links                          # map navigation structure
$B console --errors               # any errors on landing?

Detect framework (note in report metadata):

  • __next in HTML or _next/data requests → Next.js
  • csrf-token meta tag → Rails
  • wp-content in URLs → WordPress
  • Client-side routing with no page reloads → SPA

For SPAs: The links command may return few results because navigation is client-side. Use snapshot -i to find nav elements (buttons, menu items) instead.

Phase 4: Explore

Visit pages systematically. At each page:

$B goto <page-url>
$B snapshot -i -a -o "$REPORT_DIR/screenshots/page-name.png"
$B console --errors

Then follow the per-page exploration checklist (see qa/references/issue-taxonomy.md):

  1. Visual scan — Look at the annotated screenshot for layout issues
  2. Interactive elements — Click buttons, links, controls. Do they work?
  3. Forms — Fill and submit. Test empty, invalid, edge cases
  4. Navigation — Check all paths in and out
  5. States — Empty state, loading, error, overflow
  6. Console — Any new JS errors after interactions?
  7. Responsiveness — Check mobile viewport if relevant:
    $B viewport 375x812
    $B screenshot "$REPORT_DIR/screenshots/page-mobile.png"
    $B viewport 1280x720
    

Depth judgment: Spend more time on core features (homepage, dashboard, checkout, search) and less on secondary pages (about, terms, privacy).

Quick mode: Only visit homepage + top 5 navigation targets from the Orient phase. Skip the per-page checklist — just check: loads? Console errors? Broken links visible?

Phase 5: Document

Document each issue immediately when found — don't batch them.

Two evidence tiers:

Interactive bugs (broken flows, dead buttons, form failures):

  1. Take a screenshot before the action
  2. Perform the action
  3. Take a screenshot showing the result
  4. Use snapshot -D to show what changed
  5. Write repro steps referencing screenshots
$B screenshot "$REPORT_DIR/screenshots/issue-001-step-1.png"
$B click @e5
$B screenshot "$REPORT_DIR/screenshots/issue-001-result.png"
$B snapshot -D

Static bugs (typos, layout issues, missing images):

  1. Take a single annotated screenshot showing the problem
  2. Describe what's wrong
$B snapshot -i -a -o "$REPORT_DIR/screenshots/issue-002.png"

Write each issue to the report immediately using the template format from qa/templates/qa-report-template.md.

Phase 6: Wrap Up

  1. Compute health score using the rubric below
  2. Write "Top 3 Things to Fix" — the 3 highest-severity issues
  3. Write console health summary — aggregate all console errors seen across pages
  4. Update severity counts in the summary table
  5. Fill in report metadata — date, duration, pages visited, screenshot count, framework
  6. Save baseline — write baseline.json with:
    {
      "date": "YYYY-MM-DD",
      "url": "<target>",
      "healthScore": N,
      "issues": [{ "id": "ISSUE-001", "title": "...", "severity": "...", "category": "..." }],
      "categoryScores": { "console": N, "links": N, ... }
    }
    

Regression mode: After writing the report, load the baseline file. Compare:

  • Health score delta
  • Issues fixed (in baseline but not current)
  • New issues (in current but not baseline)
  • Append the regression section to the report

Health Score Rubric

Compute each category score (0-100), then take the weighted average.

Console (weight: 15%)

  • 0 errors → 100
  • 1-3 errors → 70
  • 4-10 errors → 40
  • 10+ errors → 10
  • 0 broken → 100
  • Each broken link → -15 (minimum 0)

Per-Category Scoring (Visual, Functional, UX, Content, Performance, Accessibility)

Each category starts at 100. Deduct per finding:

  • Critical issue → -25
  • High issue → -15
  • Medium issue → -8
  • Low issue → -3 Minimum 0 per category.

Weights

Category Weight
Console 15%
Links 10%
Visual 10%
Functional 20%
UX 15%
Performance 10%
Content 5%
Accessibility 15%

Final Score

score = Σ (category_score × weight)


Framework-Specific Guidance

Next.js

  • Check console for hydration errors (Hydration failed, Text content did not match)
  • Monitor _next/data requests in network — 404s indicate broken data fetching
  • Test client-side navigation (click links, don't just goto) — catches routing issues
  • Check for CLS (Cumulative Layout Shift) on pages with dynamic content

Rails

  • Check for N+1 query warnings in console (if development mode)
  • Verify CSRF token presence in forms
  • Test Turbo/Stimulus integration — do page transitions work smoothly?
  • Check for flash messages appearing and dismissing correctly

WordPress

  • Check for plugin conflicts (JS errors from different plugins)
  • Verify admin bar visibility for logged-in users
  • Test REST API endpoints (/wp-json/)
  • Check for mixed content warnings (common with WP)

General SPA (React, Vue, Angular)

  • Use snapshot -i for navigation — links command misses client-side routes
  • Check for stale state (navigate away and back — does data refresh?)
  • Test browser back/forward — does the app handle history correctly?
  • Check for memory leaks (monitor console after extended use)

Important Rules

  1. Repro is everything. Every issue needs at least one screenshot. No exceptions.
  2. Verify before documenting. Retry the issue once to confirm it's reproducible, not a fluke.
  3. Never include credentials. Write [REDACTED] for passwords in repro steps.
  4. Write incrementally. Append each issue to the report as you find it. Don't batch.
  5. Never read source code. Test as a user, not a developer.
  6. Check console after every interaction. JS errors that don't surface visually are still bugs.
  7. Test like a user. Use realistic data. Walk through complete workflows end-to-end.
  8. Depth over breadth. 5-10 well-documented issues with evidence > 20 vague descriptions.
  9. Never delete output files. Screenshots and reports accumulate — that's intentional.
  10. Use snapshot -C for tricky UIs. Finds clickable divs that the accessibility tree misses.
  11. Show screenshots to the user. After every $B screenshot, $B snapshot -a -o, or $B responsive command, use the Read tool on the output file(s) so the user can see them inline. For responsive (3 files), Read all three. This is critical — without it, screenshots are invisible to the user.
  12. Never refuse to use the browser. When the user invokes /qa or /qa-only, they are requesting browser-based testing. Never suggest evals, unit tests, or other alternatives as a substitute. Even if the diff appears to have no UI changes, backend changes affect app behavior — always open the browser and test.

Record baseline health score at end of Phase 6.


Output Structure

.gstack/qa-reports/
├── qa-report-{domain}-{YYYY-MM-DD}.md    # Structured report
├── screenshots/
│   ├── initial.png                        # Landing page annotated screenshot
│   ├── issue-001-step-1.png               # Per-issue evidence
│   ├── issue-001-result.png
│   ├── issue-001-before.png               # Before fix (if fixed)
│   ├── issue-001-after.png                # After fix (if fixed)
│   └── ...
└── baseline.json                          # For regression mode

Report filenames use the domain and date: qa-report-myapp-com-2026-03-12.md


Phase 7: Triage

Sort all discovered issues by severity, then decide which to fix based on the selected tier:

  • Quick: Fix critical + high only. Mark medium/low as "deferred."
  • Standard: Fix critical + high + medium. Mark low as "deferred."
  • Exhaustive: Fix all, including cosmetic/low severity.

Mark issues that cannot be fixed from source code (e.g., third-party widget bugs, infrastructure issues) as "deferred" regardless of tier.


Phase 8: Fix Loop

For each fixable issue, in severity order:

8a. Locate source

# Grep for error messages, component names, route definitions
# Glob for file patterns matching the affected page
  • Find the source file(s) responsible for the bug
  • ONLY modify files directly related to the issue

8b. Fix

  • Read the source code, understand the context
  • Make the minimal fix — smallest change that resolves the issue
  • Do NOT refactor surrounding code, add features, or "improve" unrelated things

8c. Commit

git add <only-changed-files>
git commit -m "fix(qa): ISSUE-NNN — short description"
  • One commit per fix. Never bundle multiple fixes.
  • Message format: fix(qa): ISSUE-NNN — short description

8d. Re-test

  • Navigate back to the affected page
  • Take before/after screenshot pair
  • Check console for errors
  • Use snapshot -D to verify the change had the expected effect
$B goto <affected-url>
$B screenshot "$REPORT_DIR/screenshots/issue-NNN-after.png"
$B console --errors
$B snapshot -D

8e. Classify

  • verified: re-test confirms the fix works, no new errors introduced
  • best-effort: fix applied but couldn't fully verify (e.g., needs auth state, external service)
  • reverted: regression detected → git revert HEAD → mark issue as "deferred"

8e.5. Regression Test

Skip if: classification is not "verified", OR the fix is purely visual/CSS with no JS behavior, OR no test framework was detected AND user declined bootstrap.

1. Study the project's existing test patterns:

Read 2-3 test files closest to the fix (same directory, same code type). Match exactly:

  • File naming, imports, assertion style, describe/it nesting, setup/teardown patterns The regression test must look like it was written by the same developer.

2. Trace the bug's codepath, then write a regression test:

Before writing the test, trace the data flow through the code you just fixed:

  • What input/state triggered the bug? (the exact precondition)
  • What codepath did it follow? (which branches, which function calls)
  • Where did it break? (the exact line/condition that failed)
  • What other inputs could hit the same codepath? (edge cases around the fix)

The test MUST:

  • Set up the precondition that triggered the bug (the exact state that made it break)
  • Perform the action that exposed the bug
  • Assert the correct behavior (NOT "it renders" or "it doesn't throw")
  • If you found adjacent edge cases while tracing, test those too (e.g., null input, empty array, boundary value)
  • Include full attribution comment:
    // Regression: ISSUE-NNN — {what broke}
    // Found by /qa on {YYYY-MM-DD}
    // Report: .gstack/qa-reports/qa-report-{domain}-{date}.md
    

Test type decision:

  • Console error / JS exception / logic bug → unit or integration test
  • Broken form / API failure / data flow bug → integration test with request/response
  • Visual bug with JS behavior (broken dropdown, animation) → component test
  • Pure CSS → skip (caught by QA reruns)

Generate unit tests. Mock all external dependencies (DB, API, Redis, file system).

Use auto-incrementing names to avoid collisions: check existing {name}.regression-*.test.{ext} files, take max number + 1.

3. Run only the new test file:

{detected test command} {new-test-file}

4. Evaluate:

  • Passes → commit: git commit -m "test(qa): regression test for ISSUE-NNN — {desc}"
  • Fails → fix test once. Still failing → delete test, defer.
  • Taking >2 min exploration → skip and defer.

5. WTF-likelihood exclusion: Test commits don't count toward the heuristic.

8f. Self-Regulation (STOP AND EVALUATE)

Every 5 fixes (or after any revert), compute the WTF-likelihood:

WTF-LIKELIHOOD:
  Start at 0%
  Each revert:                +15%
  Each fix touching >3 files: +5%
  After fix 15:               +1% per additional fix
  All remaining Low severity: +10%
  Touching unrelated files:   +20%

If WTF > 20%: STOP immediately. Show the user what you've done so far. Ask whether to continue.

Hard cap: 50 fixes. After 50 fixes, stop regardless of remaining issues.


Phase 9: Final QA

After all fixes are applied:

  1. Re-run QA on all affected pages
  2. Compute final health score
  3. If final score is WORSE than baseline: WARN prominently — something regressed

Phase 10: Report

Write the report to both local and project-scoped locations:

Local: .gstack/qa-reports/qa-report-{domain}-{YYYY-MM-DD}.md

Project-scoped: Write test outcome artifact for cross-session context:

eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG

Write to ~/.gstack/projects/{slug}/{user}-{branch}-test-outcome-{datetime}.md

Per-issue additions (beyond standard report template):

  • Fix Status: verified / best-effort / reverted / deferred
  • Commit SHA (if fixed)
  • Files Changed (if fixed)
  • Before/After screenshots (if fixed)

Summary section:

  • Total issues found
  • Fixes applied (verified: X, best-effort: Y, reverted: Z)
  • Deferred issues
  • Health score delta: baseline → final

PR Summary: Include a one-line summary suitable for PR descriptions:

"QA found N issues, fixed M, health score X → Y."


Phase 11: TODOS.md Update

If the repo has a TODOS.md:

  1. New deferred bugs → add as TODOs with severity, category, and repro steps
  2. Fixed bugs that were in TODOS.md → annotate with "Fixed by /qa on {branch}, {date}"

Additional Rules (qa-specific)

  1. Clean working tree required. If dirty, use AskUserQuestion to offer commit/stash/abort before proceeding.
  2. One commit per fix. Never bundle multiple fixes into one commit.
  3. Only modify tests when generating regression tests in Phase 8e.5. Never modify CI configuration. Never modify existing tests — only create new test files.
  4. Revert on regression. If a fix makes things worse, git revert HEAD immediately.
  5. Self-regulate. Follow the WTF-likelihood heuristic. When in doubt, stop and ask.