diff --git a/CHANGELOG.md b/CHANGELOG.md index 87233991f..7b81847cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index d20a3563f..8b65c8a3a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/README.md b/README.md index d921e420d..252fe18a3 100644 --- a/README.md +++ b/README.md @@ -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. ``` diff --git a/SKILL.md b/SKILL.md index e6809b53d..29aeb03dd 100644 --- a/SKILL.md +++ b/SKILL.md @@ -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 diff --git a/SKILL.md.tmpl b/SKILL.md.tmpl index 23f5973c2..0c9859655 100644 --- a/SKILL.md.tmpl +++ b/SKILL.md.tmpl @@ -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 diff --git a/TODOS.md b/TODOS.md index 472f202c7..766c3a782 100644 --- a/TODOS.md +++ b/TODOS.md @@ -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 diff --git a/bin/gstack-diff-scope b/bin/gstack-diff-scope index ada66c0a5..f656732d2 100755 --- a/bin/gstack-diff-scope +++ b/bin/gstack-diff-scope @@ -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 diff --git a/bin/gstack-slug b/bin/gstack-slug index 7336b7b4e..6c0e80ef7 100755 --- a/bin/gstack-slug +++ b/bin/gstack-slug @@ -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 '/' '-') diff --git a/browse/src/browser-manager.ts b/browse/src/browser-manager.ts index 24cfda640..31a1f9def 100644 --- a/browse/src/browser-manager.ts +++ b/browse/src/browser-manager.ts @@ -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 { 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); diff --git a/browse/src/meta-commands.ts b/browse/src/meta-commands.ts index e628d6a3b..049ed69a0 100644 --- a/browse/src/meta-commands.ts +++ b/browse/src/meta-commands.ts @@ -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 '); 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); diff --git a/browse/src/read-commands.ts b/browse/src/read-commands.ts index 54877562e..e98233250 100644 --- a/browse/src/read-commands.ts +++ b/browse/src/read-commands.ts @@ -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 + '/')); diff --git a/browse/src/url-validation.ts b/browse/src/url-validation.ts new file mode 100644 index 000000000..1ce8c45b9 --- /dev/null +++ b/browse/src/url-validation.ts @@ -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.` + ); + } +} diff --git a/browse/src/write-commands.ts b/browse/src/write-commands.ts index 2b3849203..26a46a4b8 100644 --- a/browse/src/write-commands.ts +++ b/browse/src/write-commands.ts @@ -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 '); + validateNavigationUrl(url); const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 }); const status = response?.status() || 'unknown'; return `Navigated to ${url} (${status})`; diff --git a/browse/test/path-validation.test.ts b/browse/test/path-validation.test.ts new file mode 100644 index 000000000..ab25941ed --- /dev/null +++ b/browse/test/path-validation.test.ts @@ -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/); + }); +}); diff --git a/browse/test/url-validation.test.ts b/browse/test/url-validation.test.ts new file mode 100644 index 000000000..f87f4e84b --- /dev/null +++ b/browse/test/url-validation.test.ts @@ -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,

hi

')).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); + }); +}); diff --git a/codex/SKILL.md b/codex/SKILL.md index f03f0c53e..7d25061b1 100644 --- a/codex/SKILL.md +++ b/codex/SKILL.md @@ -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 diff --git a/codex/SKILL.md.tmpl b/codex/SKILL.md.tmpl index 6dd54902b..6b09d2041 100644 --- a/codex/SKILL.md.tmpl +++ b/codex/SKILL.md.tmpl @@ -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 diff --git a/design-consultation/SKILL.md b/design-consultation/SKILL.md index 7e23d470b..3fc231e5c 100644 --- a/design-consultation/SKILL.md +++ b/design-consultation/SKILL.md @@ -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 ``` diff --git a/design-consultation/SKILL.md.tmpl b/design-consultation/SKILL.md.tmpl index 2532126c4..1e8b0bff3 100644 --- a/design-consultation/SKILL.md.tmpl +++ b/design-consultation/SKILL.md.tmpl @@ -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 ``` diff --git a/design-review/SKILL.md b/design-review/SKILL.md index c4d102c36..ff0549ae6 100644 --- a/design-review/SKILL.md +++ b/design-review/SKILL.md @@ -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` diff --git a/design-review/SKILL.md.tmpl b/design-review/SKILL.md.tmpl index 7e1572873..13a27beb8 100644 --- a/design-review/SKILL.md.tmpl +++ b/design-review/SKILL.md.tmpl @@ -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` diff --git a/docs/skills.md b/docs/skills.md index 63ba1cfda..315b5ce7d 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -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 diff --git a/debug/SKILL.md b/investigate/SKILL.md similarity index 98% rename from debug/SKILL.md rename to investigate/SKILL.md index e3f2d57db..5bce8d2f4 100644 --- a/debug/SKILL.md +++ b/investigate/SKILL.md @@ -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 diff --git a/debug/SKILL.md.tmpl b/investigate/SKILL.md.tmpl similarity index 99% rename from debug/SKILL.md.tmpl rename to investigate/SKILL.md.tmpl index 683e1a0b2..4db09f300 100644 --- a/debug/SKILL.md.tmpl +++ b/investigate/SKILL.md.tmpl @@ -1,5 +1,5 @@ --- -name: debug +name: investigate version: 1.0.0 description: | Systematic debugging with root cause investigation. Four phases: investigate, diff --git a/office-hours/SKILL.md b/office-hours/SKILL.md index b21afd858..2e204dd22 100644 --- a/office-hours/SKILL.md +++ b/office-hours/SKILL.md @@ -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 diff --git a/office-hours/SKILL.md.tmpl b/office-hours/SKILL.md.tmpl index 03a8302c7..39636a28d 100644 --- a/office-hours/SKILL.md.tmpl +++ b/office-hours/SKILL.md.tmpl @@ -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 diff --git a/plan-ceo-review/SKILL.md b/plan-ceo-review/SKILL.md index 008a4e634..831726688 100644 --- a/plan-ceo-review/SKILL.md +++ b/plan-ceo-review/SKILL.md @@ -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" diff --git a/plan-ceo-review/SKILL.md.tmpl b/plan-ceo-review/SKILL.md.tmpl index 4f927880a..d942d5f00 100644 --- a/plan-ceo-review/SKILL.md.tmpl +++ b/plan-ceo-review/SKILL.md.tmpl @@ -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 ``` diff --git a/plan-design-review/SKILL.md b/plan-design-review/SKILL.md index 8b85f2dbd..01128d33d 100644 --- a/plan-design-review/SKILL.md +++ b/plan-design-review/SKILL.md @@ -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" diff --git a/plan-design-review/SKILL.md.tmpl b/plan-design-review/SKILL.md.tmpl index 256666280..0a763ba67 100644 --- a/plan-design-review/SKILL.md.tmpl +++ b/plan-design-review/SKILL.md.tmpl @@ -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 ``` diff --git a/plan-eng-review/SKILL.md b/plan-eng-review/SKILL.md index bbab36d10..aed9685b5 100644 --- a/plan-eng-review/SKILL.md +++ b/plan-eng-review/SKILL.md @@ -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" diff --git a/plan-eng-review/SKILL.md.tmpl b/plan-eng-review/SKILL.md.tmpl index a864324e5..cfd730737 100644 --- a/plan-eng-review/SKILL.md.tmpl +++ b/plan-eng-review/SKILL.md.tmpl @@ -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 ``` diff --git a/qa-only/SKILL.md b/qa-only/SKILL.md index 45b5a46be..310fd2d12 100644 --- a/qa-only/SKILL.md +++ b/qa-only/SKILL.md @@ -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` diff --git a/qa-only/SKILL.md.tmpl b/qa-only/SKILL.md.tmpl index 2e2bc4f78..af3822fae 100644 --- a/qa-only/SKILL.md.tmpl +++ b/qa-only/SKILL.md.tmpl @@ -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` diff --git a/qa/SKILL.md b/qa/SKILL.md index 590c18d2c..002e03ecd 100644 --- a/qa/SKILL.md +++ b/qa/SKILL.md @@ -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` diff --git a/qa/SKILL.md.tmpl b/qa/SKILL.md.tmpl index eae79605d..824c7c9a4 100644 --- a/qa/SKILL.md.tmpl +++ b/qa/SKILL.md.tmpl @@ -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` diff --git a/review/SKILL.md b/review/SKILL.md index b93ea79c2..2e2f50342 100644 --- a/review/SKILL.md +++ b/review/SKILL.md @@ -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 2>/dev/null) +source <(~/.claude/skills/gstack/bin/gstack-diff-scope 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 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 diff --git a/review/SKILL.md.tmpl b/review/SKILL.md.tmpl index a481754a8..56c44601a 100644 --- a/review/SKILL.md.tmpl +++ b/review/SKILL.md.tmpl @@ -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 diff --git a/review/design-checklist.md b/review/design-checklist.md index 900751656..99f9dc529 100644 --- a/review/design-checklist.md +++ b/review/design-checklist.md @@ -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 2>/dev/null) +source <(~/.claude/skills/gstack/bin/gstack-diff-scope 2>/dev/null) ``` If `SCOPE_FRONTEND=false`, skip the entire design review silently. diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts index 08136b49b..5997333ae 100644 --- a/scripts/gen-skill-docs.ts +++ b/scripts/gen-skill-docs.ts @@ -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 2>/dev/null) +source <(~/.claude/skills/gstack/bin/gstack-diff-scope 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 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'), diff --git a/setup b/setup index 607c27727..59706aaf5 100755 --- a/setup +++ b/setup @@ -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" diff --git a/ship/SKILL.md b/ship/SKILL.md index a6727a049..d63066685 100644 --- a/ship/SKILL.md +++ b/ship/SKILL.md @@ -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 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 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 2>/dev/null) +source <(~/.claude/skills/gstack/bin/gstack-diff-scope 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 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 diff --git a/ship/SKILL.md.tmpl b/ship/SKILL.md.tmpl index 4274e9c86..cc17bcb82 100644 --- a/ship/SKILL.md.tmpl +++ b/ship/SKILL.md.tmpl @@ -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 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 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 diff --git a/test/skill-routing-e2e.test.ts b/test/skill-routing-e2e.test.ts index ee2d84b41..7a4a56985 100644 --- a/test/skill-routing-e2e.test.ts +++ b/test/skill-routing-e2e.test.ts @@ -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, diff --git a/test/skill-validation.test.ts b/test/skill-validation.test.ts index dbba759a4..d3ab11e92 100644 --- a/test/skill-validation.test.ts +++ b/test/skill-validation.test.ts @@ -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', ];