~cytrogen/gstack

c0f3c3a91a8da5882d6c0f2aaf5d82ad91776910 — Garry Tan a month ago 3a315b3
fix: security hardening + issue triage (v0.8.3) (#205)

* fix: check for bun before running setup (#147)

Users without bun installed got a cryptic "command not found" error.
Now prints a clear message with install instructions.

Closes #147

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

* fix: block SSRF via URL validation in browse commands (#17)

Adds validateNavigationUrl() that blocks non-HTTP(S) schemes (file://,
javascript:, data:) and cloud metadata endpoints (169.254.169.254,
metadata.google.internal). Applied to goto, diff, and newTab commands.
Localhost and private IPs remain allowed for local dev QA.

Closes #17

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

* fix: replace eval $(gstack-slug) with source <(...) (#133)

Eliminates unnecessary use of eval across all skill templates and
generated files. source <(...) has identical behavior without the
shell injection surface. Also hardens gstack-diff-scope usage.

Closes #133

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

* fix: rename /debug to /investigate to avoid Claude Code conflict (#190)

Claude Code has a built-in /debug command that shadows the gstack skill.
Renaming to /investigate which better reflects the systematic root-cause
investigation methodology.

Closes #190

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

* test: add unit tests for path validation helpers

validateOutputPath() and validateReadPath() are security-critical
functions with zero test coverage. Adds 14 tests covering safe paths,
traversal attacks, and prefix collision edge cases.

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

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

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

* docs: update /debug → /investigate references in docs

CLAUDE.md, README.md, and docs/skills.md still referenced the old
/debug skill name after the rename.

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

* fix: harden URL validation against hostname bypasses (Codex P1)

Codex review found that metadata IPs could be reached via hex
(0xA9FEA9FE), decimal (2852039166), octal, trailing dot, and IPv6
bracket forms. Now normalizes hostnames before checking the blocklist
and probes numeric IP representations via URL constructor.

Also moves URL validation before page allocation in newTab() to
prevent zombie tabs on rejection (Codex P3).

5 new test cases for bypass variants.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
M CHANGELOG.md => CHANGELOG.md +10 -3
@@ 18,6 18,14 @@
- **`skip_eng_review` respected everywhere.** If you've opted out of eng review globally, the chaining recommendations won't nag you about it.
- **Design review lite now tracks commits too.** The lightweight design check that runs inside `/review` and `/ship` gets the same staleness tracking as full reviews.

### Fixed

- **Browse no longer navigates to dangerous URLs.** `goto`, `diff`, and `newtab` now block `file://`, `javascript:`, `data:` schemes and cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`). Localhost and private IPs are still allowed for local QA testing. (Closes #17)
- **Setup script tells you what's missing.** Running `./setup` without `bun` installed now shows a clear error with install instructions instead of a cryptic "command not found." (Closes #147)
- **`/debug` renamed to `/investigate`.** Claude Code has a built-in `/debug` command that shadowed the gstack skill. The systematic root-cause debugging workflow now lives at `/investigate`. (Closes #190)
- **Shell injection surface removed.** All skill templates now use `source <(gstack-slug)` instead of `eval $(gstack-slug)`. Same behavior, no `eval`. (Closes #133)
- **25 new security tests.** URL validation (16 tests) and path traversal validation (14 tests) now have dedicated unit test suites covering scheme blocking, metadata IP blocking, directory escapes, and prefix collision edge cases.

## [0.8.2] - 2026-03-19

### Added


@@ 86,7 94,6 @@ When both `/review` (Claude) and `/codex review` have run, you get a cross-model
### Fixed

- `/debug` and `/office-hours` were completely invisible to natural language — no trigger phrases at all. Now both have full reactive + proactive triggers.
>>>>>>> origin/main

## [0.7.0] - 2026-03-18 — YC Office Hours



@@ 124,7 131,7 @@ When something is broken and you don't know why, `/debug` is your systematic deb
### Added

- **Every PR touching frontend code now gets a design review automatically.** `/review` and `/ship` apply a 20-item design checklist against changed CSS, HTML, JSX, and view files. Catches AI slop patterns (purple gradients, 3-column icon grids, generic hero copy), typography issues (body text < 16px, blacklisted fonts), accessibility gaps (`outline: none`), and `!important` abuse. Mechanical CSS fixes are auto-applied; design judgment calls ask you first.
- **`gstack-diff-scope` categorizes what changed in your branch.** Run `eval $(gstack-diff-scope main)` and get `SCOPE_FRONTEND=true/false`, `SCOPE_BACKEND`, `SCOPE_PROMPTS`, `SCOPE_TESTS`, `SCOPE_DOCS`, `SCOPE_CONFIG`. Design review uses it to skip silently on backend-only PRs. Ship pre-flight uses it to recommend design review when frontend files are touched.
- **`gstack-diff-scope` categorizes what changed in your branch.** Run `source <(gstack-diff-scope main)` and get `SCOPE_FRONTEND=true/false`, `SCOPE_BACKEND`, `SCOPE_PROMPTS`, `SCOPE_TESTS`, `SCOPE_DOCS`, `SCOPE_CONFIG`. Design review uses it to skip silently on backend-only PRs. Ship pre-flight uses it to recommend design review when frontend files are touched.
- **Design review shows up in the Review Readiness Dashboard.** The dashboard now distinguishes between "LITE" (code-level, runs automatically in /review and /ship) and "FULL" (visual audit via /plan-design-review with browse binary). Both show up as Design Review entries.
- **E2E eval for design review detection.** Planted CSS/HTML fixtures with 7 known anti-patterns (Papyrus font, 14px body text, `outline: none`, `!important`, purple gradient, generic hero copy, 3-column feature grid). The eval verifies `/review` catches at least 4 of 7.



@@ 240,7 247,7 @@ Read the philosophy: https://garryslist.org/posts/boil-the-ocean
## 0.5.1 — 2026-03-17
- **Know where you stand before you ship.** Every `/plan-ceo-review`, `/plan-eng-review`, and `/plan-design-review` now logs its result to a review tracker. At the end of each review, you see a **Review Readiness Dashboard** showing which reviews are done, when they ran, and whether they're clean — with a clear CLEARED TO SHIP or NOT READY verdict.
- **`/ship` checks your reviews before creating the PR.** Pre-flight now reads the dashboard and asks if you want to continue when reviews are missing. Informational only — it won't block you, but you'll know what you skipped.
- **One less thing to copy-paste.** The SLUG computation (that opaque sed pipeline for computing `owner-repo` from git remote) is now a shared `bin/gstack-slug` helper. All 14 inline copies across templates replaced with `eval $(gstack-slug)`. If the format ever changes, fix it once.
- **One less thing to copy-paste.** The SLUG computation (that opaque sed pipeline for computing `owner-repo` from git remote) is now a shared `bin/gstack-slug` helper. All 14 inline copies across templates replaced with `source <(gstack-slug)`. If the format ever changes, fix it once.
- **Screenshots are now visible during QA and browse sessions.** When gstack takes screenshots, they now show up as clickable image elements in your output — no more invisible `/tmp/browse-screenshot.png` paths you can't see. Works in `/qa`, `/qa-only`, `/plan-design-review`, `/qa-design-review`, `/browse`, and `/gstack`.

### For contributors

M CLAUDE.md => CLAUDE.md +1 -1
@@ 59,7 59,7 @@ gstack/
├── plan-ceo-review/ # /plan-ceo-review skill
├── plan-eng-review/ # /plan-eng-review skill
├── office-hours/    # /office-hours skill (YC Office Hours — startup diagnostic + builder brainstorm)
├── debug/           # /debug skill (systematic root-cause debugging)
├── investigate/     # /investigate skill (systematic root-cause debugging)
├── retro/           # Retrospective skill
├── document-release/ # /document-release skill (post-ship doc updates)
├── setup            # One-time setup: build binary + symlink skills

M README.md => README.md +5 -5
@@ 48,11 48,11 @@ Expect first useful run in under 5 minutes on any repo with tests already set up

Open Claude Code and paste this. Claude does the rest.

> Install gstack: run **`git clone https://github.com/garrytan/gstack.git ~/.claude/skills/gstack && cd ~/.claude/skills/gstack && ./setup`** then add a "gstack" section to CLAUDE.md that says to use the /browse skill from gstack for all web browsing, never use mcp\_\_claude-in-chrome\_\_\* tools, and lists the available skills: /office-hours, /plan-ceo-review, /plan-eng-review, /plan-design-review, /design-consultation, /review, /ship, /browse, /qa, /qa-only, /design-review, /setup-browser-cookies, /retro, /debug, /document-release, /codex, /careful, /freeze, /guard, /unfreeze, /gstack-upgrade. Then ask the user if they also want to add gstack to the current project so teammates get it.
> Install gstack: run **`git clone https://github.com/garrytan/gstack.git ~/.claude/skills/gstack && cd ~/.claude/skills/gstack && ./setup`** then add a "gstack" section to CLAUDE.md that says to use the /browse skill from gstack for all web browsing, never use mcp\_\_claude-in-chrome\_\_\* tools, and lists the available skills: /office-hours, /plan-ceo-review, /plan-eng-review, /plan-design-review, /design-consultation, /review, /ship, /browse, /qa, /qa-only, /design-review, /setup-browser-cookies, /retro, /investigate, /document-release, /codex, /careful, /freeze, /guard, /unfreeze, /gstack-upgrade. Then ask the user if they also want to add gstack to the current project so teammates get it.

### Step 2: Add to your repo so teammates get it (optional)

> Add gstack to this project: run **`cp -Rf ~/.claude/skills/gstack .claude/skills/gstack && rm -rf .claude/skills/gstack/.git && cd .claude/skills/gstack && ./setup`** then add a "gstack" section to this project's CLAUDE.md that says to use the /browse skill from gstack for all web browsing, never use mcp\_\_claude-in-chrome\_\_\* tools, lists the available skills: /office-hours, /plan-ceo-review, /plan-eng-review, /plan-design-review, /design-consultation, /review, /ship, /browse, /qa, /qa-only, /design-review, /setup-browser-cookies, /retro, /debug, /document-release, /codex, /careful, /freeze, /guard, /unfreeze, /gstack-upgrade, and tells Claude that if gstack skills aren't working, run `cd .claude/skills/gstack && ./setup` to build the binary and register skills.
> Add gstack to this project: run **`cp -Rf ~/.claude/skills/gstack .claude/skills/gstack && rm -rf .claude/skills/gstack/.git && cd .claude/skills/gstack && ./setup`** then add a "gstack" section to this project's CLAUDE.md that says to use the /browse skill from gstack for all web browsing, never use mcp\_\_claude-in-chrome\_\_\* tools, lists the available skills: /office-hours, /plan-ceo-review, /plan-eng-review, /plan-design-review, /design-consultation, /review, /ship, /browse, /qa, /qa-only, /design-review, /setup-browser-cookies, /retro, /investigate, /document-release, /codex, /careful, /freeze, /guard, /unfreeze, /gstack-upgrade, and tells Claude that if gstack skills aren't working, run `cd .claude/skills/gstack && ./setup` to build the binary and register skills.

Real files get committed to your repo (not a submodule), so `git clone` just works. Everything lives inside `.claude/`. Nothing touches your PATH or runs in the background.



@@ 117,7 117,7 @@ One sprint, one person, one feature — that takes about 30 minutes with gstack.
| `/plan-design-review` | **Senior Designer** | Rates each design dimension 0-10, explains what a 10 looks like, then edits the plan to get there. AI Slop detection. Interactive — one AskUserQuestion per design choice. |
| `/design-consultation` | **Design Partner** | Build a complete design system from scratch. Knows the landscape, proposes creative risks, generates realistic product mockups. Design at the heart of all other phases. |
| `/review` | **Staff Engineer** | Find the bugs that pass CI but blow up in production. Auto-fixes the obvious ones. Flags completeness gaps. |
| `/debug` | **Debugger** | Systematic root-cause debugging. Iron Law: no fixes without investigation. Traces data flow, tests hypotheses, stops after 3 failed fixes. |
| `/investigate` | **Debugger** | Systematic root-cause debugging. Iron Law: no fixes without investigation. Traces data flow, tests hypotheses, stops after 3 failed fixes. |
| `/design-review` | **Designer Who Codes** | Same audit as /plan-design-review, then fixes what it finds. Atomic commits, before/after screenshots. |
| `/qa` | **QA Lead** | Test your app, find bugs, fix them with atomic commits, re-verify. Auto-generates regression tests for every fix. |
| `/qa-only` | **QA Reporter** | Same methodology as /qa but report only. Use when you want a pure bug report without code changes. |


@@ 158,7 158,7 @@ One sprint, one person, one feature — that takes about 30 minutes with gstack.

**Multi-AI second opinion.** `/codex` gets an independent review from OpenAI's Codex CLI — a completely different AI looking at the same diff. Three modes: code review with a pass/fail gate, adversarial challenge that actively tries to break your code, and open consultation with session continuity. When both `/review` (Claude) and `/codex` (OpenAI) have reviewed the same branch, you get a cross-model analysis showing which findings overlap and which are unique to each.

**Safety guardrails on demand.** Say "be careful" and `/careful` warns before any destructive command — rm -rf, DROP TABLE, force-push, git reset --hard. `/freeze` locks edits to one directory while debugging so Claude can't accidentally "fix" unrelated code. `/guard` activates both. `/debug` auto-freezes to the module being investigated.
**Safety guardrails on demand.** Say "be careful" and `/careful` warns before any destructive command — rm -rf, DROP TABLE, force-push, git reset --hard. `/freeze` locks edits to one directory while debugging so Claude can't accidentally "fix" unrelated code. `/guard` activates both. `/investigate` auto-freezes to the module being investigated.

**Proactive skill suggestions.** gstack notices what stage you're in — brainstorming, reviewing, debugging, testing — and suggests the right skill. Don't like it? Say "stop suggesting" and it remembers across sessions.



@@ 213,7 213,7 @@ Fifteen specialists and six power tools. All slash commands. All Markdown. All f
Use /browse from gstack for all web browsing. Never use mcp__claude-in-chrome__* tools.
Available skills: /office-hours, /plan-ceo-review, /plan-eng-review, /plan-design-review,
/design-consultation, /review, /ship, /browse, /qa, /qa-only, /design-review,
/setup-browser-cookies, /retro, /debug, /document-release, /codex, /careful,
/setup-browser-cookies, /retro, /investigate, /document-release, /codex, /careful,
/freeze, /guard, /unfreeze, /gstack-upgrade.
```


M SKILL.md => SKILL.md +1 -1
@@ 15,7 15,7 @@ description: |
  - Reviewing a plan (architecture) → suggest /plan-eng-review
  - Reviewing a plan (design) → suggest /plan-design-review
  - Creating a design system → suggest /design-consultation
  - Debugging errors → suggest /debug
  - Debugging errors → suggest /investigate
  - Testing the app → suggest /qa
  - Code review before merge → suggest /review
  - Visual design audit → suggest /design-review

M SKILL.md.tmpl => SKILL.md.tmpl +1 -1
@@ 15,7 15,7 @@ description: |
  - Reviewing a plan (architecture) → suggest /plan-eng-review
  - Reviewing a plan (design) → suggest /plan-design-review
  - Creating a design system → suggest /design-consultation
  - Debugging errors → suggest /debug
  - Debugging errors → suggest /investigate
  - Testing the app → suggest /qa
  - Code review before merge → suggest /review
  - Visual design audit → suggest /design-review

M TODOS.md => TODOS.md +6 -6
@@ 512,25 512,25 @@ Shipped as `/careful`, `/freeze`, `/guard`, and `/unfreeze` in v0.6.5. Includes 

Shipped in v0.6.5. TemplateContext in gen-skill-docs.ts bakes skill name into preamble telemetry line. Analytics CLI (`bun run analytics`) for querying. /retro integration shows skills-used-this-week.

### /debug scoped debugging enhancements (gated on telemetry)
### /investigate scoped debugging enhancements (gated on telemetry)

**What:** Six enhancements to /debug auto-freeze, contingent on telemetry showing the freeze hook actually fires in real debugging sessions.
**What:** Six enhancements to /investigate auto-freeze, contingent on telemetry showing the freeze hook actually fires in real debugging sessions.

**Why:** /debug v0.7.1 auto-freezes edits to the module being debugged. If telemetry shows the hook fires often, these enhancements make the experience smarter. If it never fires, the problem wasn't real and these aren't worth building.
**Why:** /investigate v0.7.1 auto-freezes edits to the module being debugged. If telemetry shows the hook fires often, these enhancements make the experience smarter. If it never fires, the problem wasn't real and these aren't worth building.

**Context:** All items are prose additions to `debug/SKILL.md.tmpl`. No new scripts.
**Context:** All items are prose additions to `investigate/SKILL.md.tmpl`. No new scripts.

**Items:**
1. Stack trace auto-detection for freeze directory (parse deepest app frame)
2. Freeze boundary widening (ask to widen instead of hard-block when hitting boundary)
3. Post-fix auto-unfreeze + full test suite run
4. Debug instrumentation cleanup (tag with DEBUG-TEMP, remove before commit)
5. Debug session persistence (~/.gstack/debug-sessions/ — save investigation for reuse)
5. Debug session persistence (~/.gstack/investigate-sessions/ — save investigation for reuse)
6. Investigation timeline in debug report (hypothesis log with timing)

**Effort:** M (all 6 combined)
**Priority:** P3
**Depends on:** Telemetry data showing freeze hook fires in real /debug sessions
**Depends on:** Telemetry data showing freeze hook fires in real /investigate sessions

## Completed


M bin/gstack-diff-scope => bin/gstack-diff-scope +1 -1
@@ 1,6 1,6 @@
#!/usr/bin/env bash
# gstack-diff-scope — categorize what changed in the diff against a base branch
# Usage: eval $(gstack-diff-scope main)  → sets SCOPE_FRONTEND=true SCOPE_BACKEND=false ...
# Usage: source <(gstack-diff-scope main)  → sets SCOPE_FRONTEND=true SCOPE_BACKEND=false ...
# Or:    gstack-diff-scope main           → prints SCOPE_*=... lines
set -euo pipefail


M bin/gstack-slug => bin/gstack-slug +1 -1
@@ 1,6 1,6 @@
#!/usr/bin/env bash
# gstack-slug — output project slug and sanitized branch name
# Usage: eval $(gstack-slug)  → sets SLUG and BRANCH variables
# Usage: source <(gstack-slug)  → sets SLUG and BRANCH variables
# Or:    gstack-slug           → prints SLUG=... and BRANCH=... lines
set -euo pipefail
SLUG=$(git remote get-url origin 2>/dev/null | sed 's|.*[:/]\([^/]*/[^/]*\)\.git$|\1|;s|.*[:/]\([^/]*/[^/]*\)$|\1|' | tr '/' '-')

M browse/src/browser-manager.ts => browse/src/browser-manager.ts +6 -0
@@ 17,6 17,7 @@

import { chromium, type Browser, type BrowserContext, type BrowserContextOptions, type Page, type Locator, type Cookie } from 'playwright';
import { addConsoleEntry, addNetworkEntry, addDialogEntry, networkBuffer, type DialogEntry } from './buffers';
import { validateNavigationUrl } from './url-validation';

export interface RefEntry {
  locator: Locator;


@@ 119,6 120,11 @@ export class BrowserManager {
  async newTab(url?: string): Promise<number> {
    if (!this.context) throw new Error('Browser not launched');

    // Validate URL before allocating page to avoid zombie tabs on rejection
    if (url) {
      validateNavigationUrl(url);
    }

    const page = await this.context.newPage();
    const id = this.nextTabId++;
    this.pages.set(id, page);

M browse/src/meta-commands.ts => browse/src/meta-commands.ts +4 -1
@@ 6,6 6,7 @@ import type { BrowserManager } from './browser-manager';
import { handleSnapshot } from './snapshot';
import { getCleanText } from './read-commands';
import { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS } from './commands';
import { validateNavigationUrl } from './url-validation';
import * as Diff from 'diff';
import * as fs from 'fs';
import * as path from 'path';


@@ 13,7 14,7 @@ import * as path from 'path';
// Security: Path validation to prevent path traversal attacks
const SAFE_DIRECTORIES = ['/tmp', process.cwd()];

function validateOutputPath(filePath: string): void {
export function validateOutputPath(filePath: string): void {
  const resolved = path.resolve(filePath);
  const isSafe = SAFE_DIRECTORIES.some(dir => resolved === dir || resolved.startsWith(dir + '/'));
  if (!isSafe) {


@@ 221,9 222,11 @@ export async function handleMetaCommand(
      if (!url1 || !url2) throw new Error('Usage: browse diff <url1> <url2>');

      const page = bm.getPage();
      validateNavigationUrl(url1);
      await page.goto(url1, { waitUntil: 'domcontentloaded', timeout: 15000 });
      const text1 = await getCleanText(page);

      validateNavigationUrl(url2);
      await page.goto(url2, { waitUntil: 'domcontentloaded', timeout: 15000 });
      const text2 = await getCleanText(page);


M browse/src/read-commands.ts => browse/src/read-commands.ts +1 -1
@@ 38,7 38,7 @@ function wrapForEvaluate(code: string): string {
// Security: Path validation to prevent path traversal attacks
const SAFE_DIRECTORIES = ['/tmp', process.cwd()];

function validateReadPath(filePath: string): void {
export function validateReadPath(filePath: string): void {
  if (path.isAbsolute(filePath)) {
    const resolved = path.resolve(filePath);
    const isSafe = SAFE_DIRECTORIES.some(dir => resolved === dir || resolved.startsWith(dir + '/'));

A browse/src/url-validation.ts => browse/src/url-validation.ts +67 -0
@@ 0,0 1,67 @@
/**
 * URL validation for navigation commands — blocks dangerous schemes and cloud metadata endpoints.
 * Localhost and private IPs are allowed (primary use case: QA testing local dev servers).
 */

const BLOCKED_METADATA_HOSTS = new Set([
  '169.254.169.254',  // AWS/GCP/Azure instance metadata
  'fd00::',           // IPv6 unique local (metadata in some cloud setups)
  'metadata.google.internal', // GCP metadata
]);

/**
 * Normalize hostname for blocklist comparison:
 * - Strip trailing dot (DNS fully-qualified notation)
 * - Strip IPv6 brackets (URL.hostname includes [] for IPv6)
 * - Resolve hex (0xA9FEA9FE) and decimal (2852039166) IP representations
 */
function normalizeHostname(hostname: string): string {
  // Strip IPv6 brackets
  let h = hostname.startsWith('[') && hostname.endsWith(']')
    ? hostname.slice(1, -1)
    : hostname;
  // Strip trailing dot
  if (h.endsWith('.')) h = h.slice(0, -1);
  return h;
}

/**
 * Check if a hostname resolves to the link-local metadata IP 169.254.169.254.
 * Catches hex (0xA9FEA9FE), decimal (2852039166), and octal (0251.0376.0251.0376) forms.
 */
function isMetadataIp(hostname: string): boolean {
  // Try to parse as a numeric IP via URL constructor — it normalizes all forms
  try {
    const probe = new URL(`http://${hostname}`);
    const normalized = probe.hostname;
    if (BLOCKED_METADATA_HOSTS.has(normalized)) return true;
    // Also check after stripping trailing dot
    if (normalized.endsWith('.') && BLOCKED_METADATA_HOSTS.has(normalized.slice(0, -1))) return true;
  } catch {
    // Not a valid hostname — can't be a metadata IP
  }
  return false;
}

export function validateNavigationUrl(url: string): void {
  let parsed: URL;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`Invalid URL: ${url}`);
  }

  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
    throw new Error(
      `Blocked: scheme "${parsed.protocol}" is not allowed. Only http: and https: URLs are permitted.`
    );
  }

  const hostname = normalizeHostname(parsed.hostname.toLowerCase());

  if (BLOCKED_METADATA_HOSTS.has(hostname) || isMetadataIp(hostname)) {
    throw new Error(
      `Blocked: ${parsed.hostname} is a cloud metadata endpoint. Access is denied for security.`
    );
  }
}

M browse/src/write-commands.ts => browse/src/write-commands.ts +2 -0
@@ 7,6 7,7 @@

import type { BrowserManager } from './browser-manager';
import { findInstalledBrowsers, importCookies } from './cookie-import-browser';
import { validateNavigationUrl } from './url-validation';
import * as fs from 'fs';
import * as path from 'path';



@@ 21,6 22,7 @@ export async function handleWriteCommand(
    case 'goto': {
      const url = args[0];
      if (!url) throw new Error('Usage: browse goto <url>');
      validateNavigationUrl(url);
      const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
      const status = response?.status() || 'unknown';
      return `Navigated to ${url} (${status})`;

A browse/test/path-validation.test.ts => browse/test/path-validation.test.ts +63 -0
@@ 0,0 1,63 @@
import { describe, it, expect } from 'bun:test';
import { validateOutputPath } from '../src/meta-commands';
import { validateReadPath } from '../src/read-commands';

describe('validateOutputPath', () => {
  it('allows paths within /tmp', () => {
    expect(() => validateOutputPath('/tmp/screenshot.png')).not.toThrow();
  });

  it('allows paths in subdirectories of /tmp', () => {
    expect(() => validateOutputPath('/tmp/browse/output.png')).not.toThrow();
  });

  it('allows paths within cwd', () => {
    expect(() => validateOutputPath(`${process.cwd()}/output.png`)).not.toThrow();
  });

  it('blocks paths outside safe directories', () => {
    expect(() => validateOutputPath('/etc/cron.d/backdoor.png')).toThrow(/Path must be within/);
  });

  it('blocks /tmpevil prefix collision', () => {
    expect(() => validateOutputPath('/tmpevil/file.png')).toThrow(/Path must be within/);
  });

  it('blocks home directory paths', () => {
    expect(() => validateOutputPath('/Users/someone/file.png')).toThrow(/Path must be within/);
  });

  it('blocks path traversal via ..', () => {
    expect(() => validateOutputPath('/tmp/../etc/passwd')).toThrow(/Path must be within/);
  });
});

describe('validateReadPath', () => {
  it('allows absolute paths within /tmp', () => {
    expect(() => validateReadPath('/tmp/script.js')).not.toThrow();
  });

  it('allows absolute paths within cwd', () => {
    expect(() => validateReadPath(`${process.cwd()}/test.js`)).not.toThrow();
  });

  it('allows relative paths without traversal', () => {
    expect(() => validateReadPath('src/index.js')).not.toThrow();
  });

  it('blocks absolute paths outside safe directories', () => {
    expect(() => validateReadPath('/etc/passwd')).toThrow(/Absolute path must be within/);
  });

  it('blocks /tmpevil prefix collision', () => {
    expect(() => validateReadPath('/tmpevil/file.js')).toThrow(/Absolute path must be within/);
  });

  it('blocks path traversal sequences', () => {
    expect(() => validateReadPath('../../../etc/passwd')).toThrow(/Path traversal/);
  });

  it('blocks nested path traversal', () => {
    expect(() => validateReadPath('src/../../etc/passwd')).toThrow(/Path traversal/);
  });
});

A browse/test/url-validation.test.ts => browse/test/url-validation.test.ts +68 -0
@@ 0,0 1,68 @@
import { describe, it, expect } from 'bun:test';
import { validateNavigationUrl } from '../src/url-validation';

describe('validateNavigationUrl', () => {
  it('allows http URLs', () => {
    expect(() => validateNavigationUrl('http://example.com')).not.toThrow();
  });

  it('allows https URLs', () => {
    expect(() => validateNavigationUrl('https://example.com/path?q=1')).not.toThrow();
  });

  it('allows localhost', () => {
    expect(() => validateNavigationUrl('http://localhost:3000')).not.toThrow();
  });

  it('allows 127.0.0.1', () => {
    expect(() => validateNavigationUrl('http://127.0.0.1:8080')).not.toThrow();
  });

  it('allows private IPs', () => {
    expect(() => validateNavigationUrl('http://192.168.1.1')).not.toThrow();
  });

  it('blocks file:// scheme', () => {
    expect(() => validateNavigationUrl('file:///etc/passwd')).toThrow(/scheme.*not allowed/i);
  });

  it('blocks javascript: scheme', () => {
    expect(() => validateNavigationUrl('javascript:alert(1)')).toThrow(/scheme.*not allowed/i);
  });

  it('blocks data: scheme', () => {
    expect(() => validateNavigationUrl('data:text/html,<h1>hi</h1>')).toThrow(/scheme.*not allowed/i);
  });

  it('blocks AWS/GCP metadata endpoint', () => {
    expect(() => validateNavigationUrl('http://169.254.169.254/latest/meta-data/')).toThrow(/cloud metadata/i);
  });

  it('blocks GCP metadata hostname', () => {
    expect(() => validateNavigationUrl('http://metadata.google.internal/computeMetadata/v1/')).toThrow(/cloud metadata/i);
  });

  it('blocks metadata hostname with trailing dot', () => {
    expect(() => validateNavigationUrl('http://metadata.google.internal./computeMetadata/v1/')).toThrow(/cloud metadata/i);
  });

  it('blocks metadata IP in hex form', () => {
    expect(() => validateNavigationUrl('http://0xA9FEA9FE/')).toThrow(/cloud metadata/i);
  });

  it('blocks metadata IP in decimal form', () => {
    expect(() => validateNavigationUrl('http://2852039166/')).toThrow(/cloud metadata/i);
  });

  it('blocks metadata IP in octal form', () => {
    expect(() => validateNavigationUrl('http://0251.0376.0251.0376/')).toThrow(/cloud metadata/i);
  });

  it('blocks IPv6 metadata with brackets', () => {
    expect(() => validateNavigationUrl('http://[fd00::]/')).toThrow(/cloud metadata/i);
  });

  it('throws on malformed URLs', () => {
    expect(() => validateNavigationUrl('not-a-url')).toThrow(/Invalid URL/i);
  });
});

M codex/SKILL.md => codex/SKILL.md +1 -1
@@ 279,7 279,7 @@ CROSS-MODEL ANALYSIS:

7. Persist the review result:
```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
BRANCH_SLUG=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-')
mkdir -p ~/.gstack/projects/"$SLUG"
echo '{"skill":"codex-review","timestamp":"TIMESTAMP","status":"STATUS","gate":"GATE","findings":N}' >> ~/.gstack/projects/"$SLUG"/"$BRANCH_SLUG"-reviews.jsonl

M codex/SKILL.md.tmpl => codex/SKILL.md.tmpl +1 -1
@@ 126,7 126,7 @@ CROSS-MODEL ANALYSIS:

7. Persist the review result:
```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
BRANCH_SLUG=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-')
mkdir -p ~/.gstack/projects/"$SLUG"
echo '{"skill":"codex-review","timestamp":"TIMESTAMP","status":"STATUS","gate":"GATE","findings":N}' >> ~/.gstack/projects/"$SLUG"/"$BRANCH_SLUG"-reviews.jsonl

M design-consultation/SKILL.md => design-consultation/SKILL.md +1 -1
@@ 188,7 188,7 @@ ls src/ app/ pages/ components/ 2>/dev/null | head -30
Look for office-hours output:

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
ls ~/.gstack/projects/$SLUG/*office-hours* 2>/dev/null | head -5
ls .context/*office-hours* .context/attachments/*office-hours* 2>/dev/null | head -5
```

M design-consultation/SKILL.md.tmpl => design-consultation/SKILL.md.tmpl +1 -1
@@ 52,7 52,7 @@ ls src/ app/ pages/ components/ 2>/dev/null | head -30
Look for office-hours output:

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
ls ~/.gstack/projects/$SLUG/*office-hours* 2>/dev/null | head -5
ls .context/*office-hours* .context/attachments/*office-hours* 2>/dev/null | head -5
```

M design-review/SKILL.md => design-review/SKILL.md +2 -2
@@ 635,7 635,7 @@ Compare screenshots and observations across pages for:

**Project-scoped:**
```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
mkdir -p ~/.gstack/projects/$SLUG
```
Write to: `~/.gstack/projects/{slug}/{user}-{branch}-design-audit-{datetime}.md`


@@ 854,7 854,7 @@ Write the report to both local and project-scoped locations:

**Project-scoped:**
```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
mkdir -p ~/.gstack/projects/$SLUG
```
Write to `~/.gstack/projects/{slug}/{user}-{branch}-design-audit-{datetime}.md`

M design-review/SKILL.md.tmpl => design-review/SKILL.md.tmpl +1 -1
@@ 220,7 220,7 @@ Write the report to both local and project-scoped locations:

**Project-scoped:**
```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
mkdir -p ~/.gstack/projects/$SLUG
```
Write to `~/.gstack/projects/{slug}/{user}-{branch}-design-audit-{datetime}.md`

M docs/skills.md => docs/skills.md +4 -4
@@ 10,7 10,7 @@ Detailed guides for every gstack skill — philosophy, workflow, and examples.
| [`/plan-design-review`](#plan-design-review) | **Senior Designer** | Interactive plan-mode design review. Rates each dimension 0-10, explains what a 10 looks like, fixes the plan. Works in plan mode. |
| [`/design-consultation`](#design-consultation) | **Design Partner** | Build a complete design system from scratch. Knows the landscape, proposes creative risks, generates realistic product mockups. Design at the heart of all other phases. |
| [`/review`](#review) | **Staff Engineer** | Find the bugs that pass CI but blow up in production. Auto-fixes the obvious ones. Flags completeness gaps. |
| [`/debug`](#debug) | **Debugger** | Systematic root-cause debugging. Iron Law: no fixes without investigation. Traces data flow, tests hypotheses, stops after 3 failed fixes. |
| [`/investigate`](#investigate) | **Debugger** | Systematic root-cause debugging. Iron Law: no fixes without investigation. Traces data flow, tests hypotheses, stops after 3 failed fixes. |
| [`/design-review`](#design-review) | **Designer Who Codes** | Live-site visual audit + fix loop. 80-item audit, then fixes what it finds. Atomic commits, before/after screenshots. |
| [`/qa`](#qa) | **QA Lead** | Test your app, find bugs, fix them with atomic commits, re-verify. Auto-generates regression tests for every fix. |
| [`/qa-only`](#qa) | **QA Reporter** | Same methodology as /qa but report only. Use when you want a pure bug report without code changes. |


@@ 450,9 450,9 @@ I want the model imagining the production incident before it happens.

---

## `/debug`
## `/investigate`

When something is broken and you don't know why, `/debug` is your systematic debugger. It follows the Iron Law: **no fixes without root cause investigation first.**
When something is broken and you don't know why, `/investigate` is your systematic debugger. It follows the Iron Law: **no fixes without root cause investigation first.**

Instead of guessing and patching, it traces data flow, matches against known bug patterns, and tests hypotheses one at a time. If three fix attempts fail, it stops and questions the architecture instead of thrashing. This prevents the "let me try one more thing" spiral that wastes hours.



@@ 747,7 747,7 @@ You can override any warning. The guardrails are accident prevention, not access

Restrict all file edits to a single directory. When you're debugging a billing bug, you don't want Claude accidentally "fixing" unrelated code in `src/auth/`. `/freeze src/billing` blocks all Edit and Write operations outside that path.

`/debug` activates this automatically — it detects the module being debugged and freezes edits to that directory.
`/investigate` activates this automatically — it detects the module being debugged and freezes edits to that directory.

```
You:   /freeze src/billing

R debug/SKILL.md => investigate/SKILL.md +2 -2
@@ 1,5 1,5 @@
---
name: debug
name: investigate
version: 1.0.0
description: |
  Systematic debugging with root cause investigation. Four phases: investigate,


@@ 49,7 49,7 @@ echo "PROACTIVE: $_PROACTIVE"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
mkdir -p ~/.gstack/analytics
echo '{"skill":"debug","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
echo '{"skill":"investigate","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
```

If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke

R debug/SKILL.md.tmpl => investigate/SKILL.md.tmpl +1 -1
@@ 1,5 1,5 @@
---
name: debug
name: investigate
version: 1.0.0
description: |
  Systematic debugging with root cause investigation. Four phases: investigate,

M office-hours/SKILL.md => office-hours/SKILL.md +2 -2
@@ 172,7 172,7 @@ You are a **YC office hours partner**. Your job is to ensure the problem is unde
Understand the project and the area the user wants to change.

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
```

1. Read `CLAUDE.md`, `TODOS.md` (if they exist).


@@ 445,7 445,7 @@ Count the signals. You'll use this count in Phase 6 to determine which tier of c
Write the design document to the project directory.

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
USER=$(whoami)
DATETIME=$(date +%Y%m%d-%H%M%S)
mkdir -p ~/.gstack/projects/$SLUG

M office-hours/SKILL.md.tmpl => office-hours/SKILL.md.tmpl +2 -2
@@ 36,7 36,7 @@ You are a **YC office hours partner**. Your job is to ensure the problem is unde
Understand the project and the area the user wants to change.

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
```

1. Read `CLAUDE.md`, `TODOS.md` (if they exist).


@@ 309,7 309,7 @@ Count the signals. You'll use this count in Phase 6 to determine which tier of c
Write the design document to the project directory.

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
USER=$(whoami)
DATETIME=$(date +%Y%m%d-%H%M%S)
mkdir -p ~/.gstack/projects/$SLUG

M plan-ceo-review/SKILL.md => plan-ceo-review/SKILL.md +3 -3
@@ 362,7 362,7 @@ Rules:
After the opt-in/cherry-pick ceremony, write the plan to disk so the vision and decisions survive beyond this conversation. Only run this step for EXPANSION and SELECTIVE EXPANSION modes.

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
mkdir -p ~/.gstack/projects/$SLUG/ceo-plans
```



@@ 792,7 792,7 @@ If any AskUserQuestion goes unanswered, note it here. Never silently default.
After producing the Completion Summary above, persist the review result:

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
mkdir -p ~/.gstack/projects/$SLUG
echo '{"skill":"plan-ceo-review","timestamp":"TIMESTAMP","status":"STATUS","unresolved":N,"critical_gaps":N,"mode":"MODE","commit":"COMMIT"}' >> ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl
```


@@ 810,7 810,7 @@ Before running this command, substitute the placeholder values from the Completi
After completing the review, read the review log and config to display the dashboard.

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
cat ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl 2>/dev/null || echo "NO_REVIEWS"
echo "---CONFIG---"
~/.claude/skills/gstack/bin/gstack-config get skip_eng_review 2>/dev/null || echo "false"

M plan-ceo-review/SKILL.md.tmpl => plan-ceo-review/SKILL.md.tmpl +2 -2
@@ 209,7 209,7 @@ Rules:
After the opt-in/cherry-pick ceremony, write the plan to disk so the vision and decisions survive beyond this conversation. Only run this step for EXPANSION and SELECTIVE EXPANSION modes.

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
mkdir -p ~/.gstack/projects/$SLUG/ceo-plans
```



@@ 639,7 639,7 @@ If any AskUserQuestion goes unanswered, note it here. Never silently default.
After producing the Completion Summary above, persist the review result:

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
mkdir -p ~/.gstack/projects/$SLUG
echo '{"skill":"plan-ceo-review","timestamp":"TIMESTAMP","status":"STATUS","unresolved":N,"critical_gaps":N,"mode":"MODE","commit":"COMMIT"}' >> ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl
```

M plan-design-review/SKILL.md => plan-design-review/SKILL.md +2 -2
@@ 422,7 422,7 @@ If any AskUserQuestion goes unanswered, note it here. Never silently default to 
After producing the Completion Summary above, persist the review result:

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
mkdir -p ~/.gstack/projects/$SLUG
echo '{"skill":"plan-design-review","timestamp":"TIMESTAMP","status":"STATUS","overall_score":N,"unresolved":N,"decisions_made":N,"commit":"COMMIT"}' >> ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl
```


@@ 440,7 440,7 @@ Substitute values from the Completion Summary:
After completing the review, read the review log and config to display the dashboard.

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
cat ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl 2>/dev/null || echo "NO_REVIEWS"
echo "---CONFIG---"
~/.claude/skills/gstack/bin/gstack-config get skip_eng_review 2>/dev/null || echo "false"

M plan-design-review/SKILL.md.tmpl => plan-design-review/SKILL.md.tmpl +1 -1
@@ 269,7 269,7 @@ If any AskUserQuestion goes unanswered, note it here. Never silently default to 
After producing the Completion Summary above, persist the review result:

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
mkdir -p ~/.gstack/projects/$SLUG
echo '{"skill":"plan-design-review","timestamp":"TIMESTAMP","status":"STATUS","overall_score":N,"unresolved":N,"decisions_made":N,"commit":"COMMIT"}' >> ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl
```

M plan-eng-review/SKILL.md => plan-eng-review/SKILL.md +3 -3
@@ 284,7 284,7 @@ For LLM/prompt changes: check the "Prompt/LLM changes" file patterns listed in C
After producing the test diagram, write a test plan artifact to the project directory so `/qa` and `/qa-only` can consume it as primary test input (replacing the lossy git-diff heuristic):

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
USER=$(whoami)
DATETIME=$(date +%Y%m%d-%H%M%S)
mkdir -p ~/.gstack/projects/$SLUG


@@ 393,7 393,7 @@ Check the git log for this branch. If there are prior commits suggesting a previ
After producing the Completion Summary above, persist the review result:

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
mkdir -p ~/.gstack/projects/$SLUG
echo '{"skill":"plan-eng-review","timestamp":"TIMESTAMP","status":"STATUS","unresolved":N,"critical_gaps":N,"mode":"MODE","commit":"COMMIT"}' >> ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl
```


@@ 411,7 411,7 @@ Substitute values from the Completion Summary:
After completing the review, read the review log and config to display the dashboard.

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
cat ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl 2>/dev/null || echo "NO_REVIEWS"
echo "---CONFIG---"
~/.claude/skills/gstack/bin/gstack-config get skip_eng_review 2>/dev/null || echo "false"

M plan-eng-review/SKILL.md.tmpl => plan-eng-review/SKILL.md.tmpl +2 -2
@@ 148,7 148,7 @@ For LLM/prompt changes: check the "Prompt/LLM changes" file patterns listed in C
After producing the test diagram, write a test plan artifact to the project directory so `/qa` and `/qa-only` can consume it as primary test input (replacing the lossy git-diff heuristic):

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
USER=$(whoami)
DATETIME=$(date +%Y%m%d-%H%M%S)
mkdir -p ~/.gstack/projects/$SLUG


@@ 257,7 257,7 @@ Check the git log for this branch. If there are prior commits suggesting a previ
After producing the Completion Summary above, persist the review result:

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
mkdir -p ~/.gstack/projects/$SLUG
echo '{"skill":"plan-eng-review","timestamp":"TIMESTAMP","status":"STATUS","unresolved":N,"critical_gaps":N,"mode":"MODE","commit":"COMMIT"}' >> ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl
```

M qa-only/SKILL.md => qa-only/SKILL.md +2 -2
@@ 206,7 206,7 @@ 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
   ```bash
   eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
   source <(~/.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


@@ 502,7 502,7 @@ Write the report to both local and project-scoped locations:

**Project-scoped:** Write test outcome artifact for cross-session context:
```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.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`

M qa-only/SKILL.md.tmpl => qa-only/SKILL.md.tmpl +2 -2
@@ 53,7 53,7 @@ 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
   ```bash
   eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
   source <(~/.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


@@ 73,7 73,7 @@ Write the report to both local and project-scoped locations:

**Project-scoped:** Write test outcome artifact for cross-session context:
```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.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`

M qa/SKILL.md => qa/SKILL.md +2 -2
@@ 410,7 410,7 @@ 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
   ```bash
   eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
   source <(~/.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


@@ 874,7 874,7 @@ Write the report to both local and project-scoped locations:

**Project-scoped:** Write test outcome artifact for cross-session context:
```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.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`

M qa/SKILL.md.tmpl => qa/SKILL.md.tmpl +2 -2
@@ 89,7 89,7 @@ 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
   ```bash
   eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
   source <(~/.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


@@ 277,7 277,7 @@ Write the report to both local and project-scoped locations:

**Project-scoped:** Write test outcome artifact for cross-session context:
```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.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`

M review/SKILL.md => review/SKILL.md +3 -3
@@ 271,7 271,7 @@ Follow the output format specified in the checklist. Respect the suppressions 
Check if the diff touches frontend files using `gstack-diff-scope`:

```bash
eval $(~/.claude/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)
```

**If `SCOPE_FRONTEND=false`:** Skip design review silently. No output.


@@ 294,7 294,7 @@ eval $(~/.claude/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)
6. **Log the result** for the Review Readiness Dashboard:

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
mkdir -p ~/.gstack/projects/$SLUG
echo '{"skill":"design-review-lite","timestamp":"TIMESTAMP","status":"STATUS","findings":N,"auto_fixed":M,"commit":"COMMIT"}' >> ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl
```


@@ 453,7 453,7 @@ Present the full output verbatim under a `CODEX SAYS (adversarial challenge):` h

**Only if a code review ran (user chose A or C):** Persist the Codex review result to the review log:
```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
BRANCH_SLUG=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-')
mkdir -p ~/.gstack/projects/"$SLUG"
echo '{"skill":"codex-review","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","status":"STATUS","gate":"GATE"}' >> ~/.gstack/projects/"$SLUG"/"$BRANCH_SLUG"-reviews.jsonl

M review/SKILL.md.tmpl => review/SKILL.md.tmpl +1 -1
@@ 267,7 267,7 @@ Present the full output verbatim under a `CODEX SAYS (adversarial challenge):` h

**Only if a code review ran (user chose A or C):** Persist the Codex review result to the review log:
```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
BRANCH_SLUG=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-')
mkdir -p ~/.gstack/projects/"$SLUG"
echo '{"skill":"codex-review","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","status":"STATUS","gate":"GATE"}' >> ~/.gstack/projects/"$SLUG"/"$BRANCH_SLUG"-reviews.jsonl

M review/design-checklist.md => review/design-checklist.md +1 -1
@@ 9,7 9,7 @@ This checklist applies to **source code in the diff** — not rendered output. R
**Trigger:** Only run this checklist if the diff touches frontend files. Use `gstack-diff-scope` to detect:

```bash
eval $(~/.claude/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)
```

If `SCOPE_FRONTEND=false`, skip the entire design review silently.

M scripts/gen-skill-docs.ts => scripts/gen-skill-docs.ts +5 -5
@@ 567,7 567,7 @@ function generateDesignReviewLite(_ctx: TemplateContext): string {
Check if the diff touches frontend files using \`gstack-diff-scope\`:

\`\`\`bash
eval $(~/.claude/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)
\`\`\`

**If \`SCOPE_FRONTEND=false\`:** Skip design review silently. No output.


@@ 590,7 590,7 @@ eval $(~/.claude/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)
6. **Log the result** for the Review Readiness Dashboard:

\`\`\`bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
mkdir -p ~/.gstack/projects/$SLUG
echo '{"skill":"design-review-lite","timestamp":"TIMESTAMP","status":"STATUS","findings":N,"auto_fixed":M,"commit":"COMMIT"}' >> ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl
\`\`\`


@@ 850,7 850,7 @@ Compare screenshots and observations across pages for:

**Project-scoped:**
\`\`\`bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
mkdir -p ~/.gstack/projects/$SLUG
\`\`\`
Write to: \`~/.gstack/projects/{slug}/{user}-{branch}-design-audit-{datetime}.md\`


@@ 940,7 940,7 @@ function generateReviewDashboard(_ctx: TemplateContext): string {
After completing the review, read the review log and config to display the dashboard.

\`\`\`bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
cat ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl 2>/dev/null || echo "NO_REVIEWS"
echo "---CONFIG---"
~/.claude/skills/gstack/bin/gstack-config get skip_eng_review 2>/dev/null || echo "false"


@@ 1208,7 1208,7 @@ function findTemplates(): string[] {
    path.join(ROOT, 'plan-eng-review', 'SKILL.md.tmpl'),
    path.join(ROOT, 'retro', 'SKILL.md.tmpl'),
    path.join(ROOT, 'office-hours', 'SKILL.md.tmpl'),
    path.join(ROOT, 'debug', 'SKILL.md.tmpl'),
    path.join(ROOT, 'investigate', 'SKILL.md.tmpl'),
    path.join(ROOT, 'gstack-upgrade', 'SKILL.md.tmpl'),
    path.join(ROOT, 'plan-design-review', 'SKILL.md.tmpl'),
    path.join(ROOT, 'design-review', 'SKILL.md.tmpl'),

M setup => setup +6 -0
@@ 2,6 2,12 @@
# gstack setup — build browser binary + register all skills with Claude Code
set -e

if ! command -v bun >/dev/null 2>&1; then
  echo "Error: bun is required but not installed." >&2
  echo "Install it: curl -fsSL https://bun.sh/install | bash" >&2
  exit 1
fi

GSTACK_DIR="$(cd "$(dirname "$0")" && pwd)"
SKILLS_DIR="$(dirname "$GSTACK_DIR")"
BROWSE_BIN="$GSTACK_DIR/browse/dist/browse"

M ship/SKILL.md => ship/SKILL.md +7 -7
@@ 213,7 213,7 @@ You are running the `/ship` workflow. This is a **non-interactive, fully automat
After completing the review, read the review log and config to display the dashboard.

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
cat ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl 2>/dev/null || echo "NO_REVIEWS"
echo "---CONFIG---"
~/.claude/skills/gstack/bin/gstack-config get skip_eng_review 2>/dev/null || echo "false"


@@ 260,7 260,7 @@ If the Eng Review is NOT "CLEAR":

1. **Check for a prior override on this branch:**
   ```bash
   eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
   source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
   grep '"skill":"ship-review-override"' ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl 2>/dev/null || echo "NO_OVERRIDE"
   ```
   If an override exists, display the dashboard and note "Review gate previously accepted — continuing." Do NOT ask again.


@@ 270,11 270,11 @@ If the Eng Review is NOT "CLEAR":
   - RECOMMENDATION: Choose C if the change is obviously trivial (< 20 lines, typo fix, config-only); Choose B for larger changes
   - Options: A) Ship anyway  B) Abort — run /plan-eng-review first  C) Change is too small to need eng review
   - If CEO Review is missing, mention as informational ("CEO Review not run — recommended for product changes") but do NOT block
   - For Design Review: run `eval $(~/.claude/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)`. If `SCOPE_FRONTEND=true` and no design review (plan-design-review or design-review-lite) exists in the dashboard, mention: "Design Review not run — this PR changes frontend code. The lite design check will run automatically in Step 3.5, but consider running /design-review for a full visual audit post-implementation." Still never block.
   - For Design Review: run `source <(~/.claude/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)`. If `SCOPE_FRONTEND=true` and no design review (plan-design-review or design-review-lite) exists in the dashboard, mention: "Design Review not run — this PR changes frontend code. The lite design check will run automatically in Step 3.5, but consider running /design-review for a full visual audit post-implementation." Still never block.

3. **If the user chooses A or C,** persist the decision so future `/ship` runs on this branch skip the gate:
   ```bash
   eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
   source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
   echo '{"skill":"ship-review-override","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","decision":"USER_CHOICE"}' >> ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl
   ```
   Substitute USER_CHOICE with "ship_anyway" or "not_relevant".


@@ 691,7 691,7 @@ Review the diff for structural issues that tests don't catch.
Check if the diff touches frontend files using `gstack-diff-scope`:

```bash
eval $(~/.claude/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)
```

**If `SCOPE_FRONTEND=false`:** Skip design review silently. No output.


@@ 714,7 714,7 @@ eval $(~/.claude/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)
6. **Log the result** for the Review Readiness Dashboard:

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
mkdir -p ~/.gstack/projects/$SLUG
echo '{"skill":"design-review-lite","timestamp":"TIMESTAMP","status":"STATUS","findings":N,"auto_fixed":M,"commit":"COMMIT"}' >> ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl
```


@@ 811,7 811,7 @@ Present the full output verbatim under a `CODEX SAYS:` header. Check for `[P1]` 
to determine pass/fail gate. Persist the result:

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
BRANCH_SLUG=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-')
mkdir -p ~/.gstack/projects/$SLUG
echo '{"skill":"codex-review","timestamp":"TIMESTAMP","status":"STATUS","gate":"GATE"}' >> ~/.gstack/projects/$SLUG/$BRANCH_SLUG-reviews.jsonl

M ship/SKILL.md.tmpl => ship/SKILL.md.tmpl +4 -4
@@ 61,7 61,7 @@ If the Eng Review is NOT "CLEAR":

1. **Check for a prior override on this branch:**
   ```bash
   eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
   source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
   grep '"skill":"ship-review-override"' ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl 2>/dev/null || echo "NO_OVERRIDE"
   ```
   If an override exists, display the dashboard and note "Review gate previously accepted — continuing." Do NOT ask again.


@@ 71,11 71,11 @@ If the Eng Review is NOT "CLEAR":
   - RECOMMENDATION: Choose C if the change is obviously trivial (< 20 lines, typo fix, config-only); Choose B for larger changes
   - Options: A) Ship anyway  B) Abort — run /plan-eng-review first  C) Change is too small to need eng review
   - If CEO Review is missing, mention as informational ("CEO Review not run — recommended for product changes") but do NOT block
   - For Design Review: run `eval $(~/.claude/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)`. If `SCOPE_FRONTEND=true` and no design review (plan-design-review or design-review-lite) exists in the dashboard, mention: "Design Review not run — this PR changes frontend code. The lite design check will run automatically in Step 3.5, but consider running /design-review for a full visual audit post-implementation." Still never block.
   - For Design Review: run `source <(~/.claude/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)`. If `SCOPE_FRONTEND=true` and no design review (plan-design-review or design-review-lite) exists in the dashboard, mention: "Design Review not run — this PR changes frontend code. The lite design check will run automatically in Step 3.5, but consider running /design-review for a full visual audit post-implementation." Still never block.

3. **If the user chooses A or C,** persist the decision so future `/ship` runs on this branch skip the gate:
   ```bash
   eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
   source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
   echo '{"skill":"ship-review-override","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","decision":"USER_CHOICE"}' >> ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl
   ```
   Substitute USER_CHOICE with "ship_anyway" or "not_relevant".


@@ 428,7 428,7 @@ Present the full output verbatim under a `CODEX SAYS:` header. Check for `[P1]` 
to determine pass/fail gate. Persist the result:

```bash
eval $(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)
BRANCH_SLUG=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-')
mkdir -p ~/.gstack/projects/$SLUG
echo '{"skill":"codex-review","timestamp":"TIMESTAMP","status":"STATUS","gate":"GATE"}' >> ~/.gstack/projects/$SLUG/$BRANCH_SLUG-reviews.jsonl

M test/skill-routing-e2e.test.ts => test/skill-routing-e2e.test.ts +2 -2
@@ 50,7 50,7 @@ function installSkills(tmpDir: string) {
    '', // root gstack SKILL.md
    'qa', 'qa-only', 'ship', 'review', 'plan-ceo-review', 'plan-eng-review',
    'plan-design-review', 'design-review', 'design-consultation', 'retro',
    'document-release', 'debug', 'office-hours', 'browse', 'setup-browser-cookies',
    'document-release', 'investigate', 'office-hours', 'browse', 'setup-browser-cookies',
    'gstack-upgrade', 'humanizer',
  ];



@@ 277,7 277,7 @@ export default app;
      run('git', ['checkout', '-b', 'feature/waitlist-api']);

      const testName = 'journey-debug';
      const expectedSkill = 'debug';
      const expectedSkill = 'investigate';
      const result = await runSkillTest({
        prompt: "The GET /api/waitlist endpoint was working fine yesterday but now it's returning 500 errors. The tests are passing locally but the endpoint fails when I hit it with curl. Can you figure out what's going on?",
        workingDirectory: tmpDir,

M test/skill-validation.test.ts => test/skill-validation.test.ts +6 -6
@@ 218,7 218,7 @@ describe('Update check preamble', () => {
    'ship/SKILL.md', 'review/SKILL.md',
    'plan-ceo-review/SKILL.md', 'plan-eng-review/SKILL.md',
    'retro/SKILL.md',
    'office-hours/SKILL.md', 'debug/SKILL.md',
    'office-hours/SKILL.md', 'investigate/SKILL.md',
    'plan-design-review/SKILL.md',
    'design-review/SKILL.md',
    'design-consultation/SKILL.md',


@@ 530,7 530,7 @@ describe('v0.4.1 preamble features', () => {
    'ship/SKILL.md', 'review/SKILL.md',
    'plan-ceo-review/SKILL.md', 'plan-eng-review/SKILL.md',
    'retro/SKILL.md',
    'office-hours/SKILL.md', 'debug/SKILL.md',
    'office-hours/SKILL.md', 'investigate/SKILL.md',
    'plan-design-review/SKILL.md',
    'design-review/SKILL.md',
    'design-consultation/SKILL.md',


@@ 646,8 646,8 @@ describe('office-hours skill structure', () => {
  });
});

describe('debug skill structure', () => {
  const content = fs.readFileSync(path.join(ROOT, 'debug', 'SKILL.md'), 'utf-8');
describe('investigate skill structure', () => {
  const content = fs.readFileSync(path.join(ROOT, 'investigate', 'SKILL.md'), 'utf-8');
  for (const section of ['Iron Law', 'Root Cause', 'Pattern Analysis', 'Hypothesis',
                          'DEBUG REPORT', '3-strike', 'BLOCKED']) {
    test(`contains ${section}`, () => expect(content).toContain(section));


@@ 1221,7 1221,7 @@ describe('Skill trigger phrases', () => {
  // Excluded: root gstack (browser tool), gstack-upgrade (gstack-specific),
  // humanizer (text tool)
  const SKILLS_REQUIRING_TRIGGERS = [
    'qa', 'qa-only', 'ship', 'review', 'debug', 'office-hours',
    'qa', 'qa-only', 'ship', 'review', 'investigate', 'office-hours',
    'plan-ceo-review', 'plan-eng-review', 'plan-design-review',
    'design-review', 'design-consultation', 'retro', 'document-release',
    'codex', 'browse', 'setup-browser-cookies',


@@ 1241,7 1241,7 @@ describe('Skill trigger phrases', () => {

  // Skills with proactive triggers should have "Proactively suggest" in description
  const SKILLS_REQUIRING_PROACTIVE = [
    'qa', 'qa-only', 'ship', 'review', 'debug', 'office-hours',
    'qa', 'qa-only', 'ship', 'review', 'investigate', 'office-hours',
    'plan-ceo-review', 'plan-eng-review', 'plan-design-review',
    'design-review', 'design-consultation', 'retro', 'document-release',
  ];