mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-27 21:43:05 +02:00
7ca04d8ef0
* fix(gstack-paths): guard CLAUDE_PLUGIN_DATA against cross-plugin contamination (#1569) gstack-paths previously trusted CLAUDE_PLUGIN_DATA as a fallback for GSTACK_STATE_ROOT whenever GSTACK_HOME was unset. When another plugin (e.g. Codex) persists its own CLAUDE_PLUGIN_DATA into the session env via CLAUDE_ENV_FILE, gstack picked it up and wrote checkpoints, analytics, and learnings into that plugin's directory. Anyone with the Codex plugin installed alongside gstack hit this silently. Fix: guard the CLAUDE_PLUGIN_DATA branch so it only fires when CLAUDE_PLUGIN_ROOT confirms we're running as the gstack plugin (path contains "gstack"). Skill installs fall through to \$HOME/.gstack. Contributed by @ElliotDrel via #1570. Closes #1569. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(gbrain-sync): sourceLocalPath handles wrapped {sources:[...]} shape from gbrain v0.20+ gbrain v0.20+ changed `gbrain sources list --json` to return {sources: [...]} instead of a flat array. sourceLocalPath crashed upstream with `list.find is not a function` on every /sync-gbrain invocation against modern gbrain. Accept both shapes for forward/backward compat, matching probeSource/sourcePageCount in lib/gbrain-sources.ts. Contributed by @jakehann11 via #1571. Closes #1567. Supersedes #1564 (@tonyjzhou, same fix, different shape — credit retained). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(brain-context-load): probe gbrain via execFile, not shell builtin (#1559) gbrainAvailable() used `execFileSync("command", ["-v", "gbrain"])`, which fails in any environment where the `command` builtin isn't on the spawned process's PATH (most non-interactive shells). The probe then reported gbrain as missing even when it was installed, and context-load silently skipped vector/list queries. Fix: probe `gbrain --version` directly with a 500ms timeout (matching the rest of the file's MCP_TIMEOUT_MS). Same semantics, works everywhere execFile works. Contributed by @jbetala7 via #1560. Closes #1559. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(gbrain-doctor): pin schema_version:2 doctor parse path (#1418) Adds an exec-path regression test that runs a fake gbrain shim emitting the v0.25+ doctor JSON shape (schema_version: 2, status: "warnings", exit 1 for health_score < 100, no top-level `engine` field). Confirms freshDetectEngineTier recovers stdout from the non-zero exit and falls back to GBRAIN_HOME/config.json for the engine label. The pre-existing test for #1415 only stripped gbrain from PATH; this test exercises the actual doctor parse path, closing the gap that codex's plan review flagged. Also documents the schema_version separation in lib/gbrain-local-status.ts: the local CacheEntry stays at version 1, distinct from the doctor-output schema_version which we accept across versions in gstack-memory-helpers. Closes #1418 (credit @mvanhorn for surfacing the doctor + schema_v2 collapse). The fix landed pre-emptively in v1.29.x; this commit pins it with a stronger test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(memory-ingest): pin put_page regression + scrub stale name from --help and comments (#1346) #1346 reported that gstack-memory-ingest still called the renamed gbrain put_page subcommand on gbrain v0.18+. The actual code migrated to `gbrain put` and later to batch `gbrain import <dir>` before this report landed — only documentation lag remained. This commit: - Updates the --help string ("Skip gbrain put calls (still updates state file)") so user-facing docs match the shipped subcommand - Updates two inline comments that still referenced the old name - Adds test/memory-ingest-no-put_page.test.ts: a regression pin that strips comments from bin/gstack-memory-ingest.ts and fails the build if "put_page" appears in any active code or string literal, plus a sanity check that the file still calls a supported gbrain page-write verb (put or import) Closes #1346. Reporter @kylma-code surfaced the doc lag; the original code migration credit is on the v1.27.x wave. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(resolvers): rewrite all gbrain put_page instructions to canonical put <slug> scripts/resolvers/gbrain.ts emitted user-facing copy-paste instructions using the renamed `gbrain put_page` subcommand across 10 skills (office-hours, investigate, plan-ceo-review, retro, plan-eng-review, ship, cso, design-consultation, fallback, entity-stub). Every gstack user copying those snippets hit "unknown command: put_page" on gbrain v0.18+. This commit: - Rewrites all 10 instruction templates to use `gbrain put <slug> --content "$(cat <<EOF...EOF)"` with title/tags moved into YAML frontmatter inside --content, matching the v0.18+ subcommand shape - Updates README.md and USING_GBRAIN_WITH_GSTACK.md "common commands" table to reference `gbrain put` and `gbrain get` - Adds test/resolvers-gbrain-put-rewrite.test.ts pinning two invariants: (a) resolver source ships only canonical instructions, (b) every tracked SKILL.md file is free of `gbrain put_page` CHANGELOG entries are deliberately left untouched (historical record). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(build): extract package.json build to scripts/build.sh for Windows Bun compat (#1538, #1537, #1530, #1457, #1561) Bun's Windows shell parser rejects multiple constructs the inline package.json build chain used: brace groups `{ cmd; }`, subshells with redirection `( git ... ) > path/.version`, and (in Bun 1.3.x) subshells near redirections in general. Every Windows install + every auto-upgrade since v1.34.2.0 has failed on `bun run build`. Extracts the build chain to scripts/build.sh and the .version writes to scripts/write-version-files.sh. POSIX-portable, no Bun shell parsing involved. Also adds Windows-specific bun.exe handling for non-ASCII PATHs (a separate Windows footgun where Bun's --compile fails when the binary lives under a path with non-ASCII chars). Updates test/build-script-shell-compat.test.ts to assert the new shape: no subshells with redirections anywhere in the build chain, and build delegates to scripts/build.sh which delegates .version writes. Contributed by @Charlie-El via #1544. Supersedes #1531 (@scarson, fixed in build helper), #1480 (@mikepsinn, partial overlap), #1460 (@realcarsonterry, brace-group fix subsumed) — credit retained. Closes #1538, #1537, #1530, #1457, #1561. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(windows): .exe glob in .gitignore + .exe extension resolution in find-browse (#1554) bun build --compile on Windows appends .exe to the output filename, producing browse.exe instead of browse. find-browse's existsSync probe only checked the bare path and returned null on Windows even when the binary was correctly built. .gitignore similarly only excluded the bare bin/gstack-global-discover path, leaving the .exe variant tracked. This commit: - .gitignore: changes `bin/gstack-global-discover` → `bin/gstack-global-discover*` so the Windows .exe variant is ignored - browse/src/find-browse.ts: adds isExecutable + findExecutable helpers that fall back to .exe/.cmd/.bat probing on Windows, mirroring the same helper already in make-pdf/src/browseClient.ts and pdftotext.ts Contributed by @Mike-E-Log via #1554. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(windows): add fresh-install E2E gate that runs bun run build on windows-latest Adds .github/workflows/windows-setup-e2e.yml as the gate that catches Bun shell-parser regressions in the build chain before they reach users. Triggers on PRs touching package.json, scripts/build.sh, scripts/write-version-files.sh, setup, browse cli/find-browse, or gstack-paths. What it verifies: 1. bun run build completes on Windows (the previously-broken path that #1538/#1537/#1530/#1457/#1561 reported) 2. All compiled binaries land on disk (browse.exe, find-browse.exe, design.exe, gstack-global-discover.exe) 3. find-browse resolves to the .exe variant on Windows (regression gate for #1554) 4. gstack-paths returns non-empty GSTACK_STATE_ROOT/PLAN_ROOT/TMP_ROOT on Windows (regression gate for #1570) Complements the existing windows-free-tests.yml (curated unit subset); this new workflow exercises the install path itself. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(codex): move diff scope into prompt instead of --base (Codex CLI 0.130+ argv conflict) (#1209) Codex CLI ≥ 0.130.0 rejects passing a custom prompt and --base together (mutually exclusive at argv level). Every /codex review, /review, and /ship structured Codex review call ended with an argv error before the model ran. Fix: scope the diff in prompt text using "Run git diff origin/<base>...HEAD 2>/dev/null || git diff <base>...HEAD" instead of `--base <base>`. Preserves the filesystem boundary instruction across all invocations and keeps Codex's review prompt tuning. Touches: - codex/SKILL.md.tmpl + regenerated codex/SKILL.md - scripts/resolvers/review.ts + regenerated review/SKILL.md, ship/SKILL.md - test/gen-skill-docs.test.ts: new regression that fails if any of the five known files still contain the prompt+--base shape - test/skill-validation.test.ts: corresponding negative + positive pin on the rendered SKILL.md files Contributed by @jbetala7 via #1209. Closes #1479. Supersedes #1527 (@mvanhorn — same intent, different patch shape, CONFLICTING) and #1449 (@Gujiassh — broader refactor, CONFLICTING). Credit retained in CHANGELOG. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): diff from git merge-base, not git diff origin/<base> (#1492) git diff origin/<base> shows everything since the common ancestor in both directions — it includes commits that landed on origin/<base> after this branch was created as deletions. That made /review and /ship's pre-landing structured review report inflated diff totals and flagged "removed" code that was actually still present in the working tree. Fix: compute DIFF_BASE via git merge-base origin/<base> HEAD and diff the working tree against that point. Same coverage of uncommitted edits, no phantom deletions from out-of-order base advancement. Applies to /review's Step 1 (diff existence check), Step 3 (get the diff), the build-on-intent scope-creep check, the structured review DIFF_INS/DIFF_DEL stats, and the Claude adversarial subagent prompt. Same change flows into ship/SKILL.md via the shared resolver. Touches: - review/SKILL.md.tmpl + regenerated review/SKILL.md, ship/SKILL.md - scripts/resolvers/review.ts - scripts/resolvers/review-army.ts Contributed by @mvanhorn via #1492. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(codex): pin filesystem-boundary preservation across all codex review surfaces (#1503, #1522) #1503 reported that the bare codex review --base path stripped the filesystem boundary instruction, letting Codex spend tokens reading .claude/skills/ and agents/. #1522 proposed adding a skill-path detector that switched to the custom-instructions route when the diff touched skill files. After C10 (#1209) restructured codex review to always carry the boundary in the prompt (the prompt+--base argv conflict forced the restructure), the skill-path detector becomes redundant — every default call already preserves the boundary. This commit pins the post-#1209 invariant with a test that fails the build if any future refactor strips the boundary from codex/SKILL.md, review/SKILL.md, or ship/SKILL.md. Closes #1503 by regression test. #1522 (@genisis0x) is superseded by #1209 (the prompt rewrite covers its safety concern); credit retained in CHANGELOG. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(skills): use command -v instead of which for codex detection (#1197) `which` is not on PATH in every shell — some Windows shells, BusyBox- only containers, and minimal CI images all fail when skills probe codex availability via `which codex`. `command -v` is a POSIX builtin and always available where the skill is running. Touched: - codex/SKILL.md.tmpl: CODEX_BIN=$(command -v codex || echo "") - scripts/resolvers/review.ts and scripts/resolvers/design.ts: 3 + 3 sites each rewritten to `command -v codex >/dev/null 2>&1` - Regenerated all 10 affected SKILL.md files (codex, review, ship, design-consultation, design-review, office-hours, plan-ceo-review, plan-design-review, plan-devex-review, plan-eng-review) - test/skill-validation.test.ts: updated pin + defensive regression test that fails if `which codex` returns to codex/SKILL.md - test/skill-e2e-plan.test.ts: updated summary regex Contributed by @mvanhorn via #1197. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(codex): surface non-zero exits so wrappers stop reading as silent stalls (#1467, #1327) When codex exits non-zero (parse errors, arg-shape breaks, model API errors that propagate as non-zero status), the calling agent previously saw an empty output and burned 30-60 minutes misdiagnosing as a silent model/API stall. The hang-detection block only caught exit 124 (the timeout-wrapper signal). Adds elif blocks in all four codex invocation sites (Review default, Challenge, Consult new-session, Consult resume) that: - Echo "[codex exit N] <stderr first line>" to stdout - Indent the first 20 stderr lines for inline context - Log codex_nonzero_exit telemetry tagged with the call site Contributed by @genisis0x via #1467. Closes #1327. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(design): disclose OpenAI key source + warn on cwd .env match (#1278, closes #1248) The design binary previously called process.env.OPENAI_API_KEY without checking where the key came from. If a user ran $D inside someone else's project that had OPENAI_API_KEY in its .env, the resulting generation billed that project's account. Silent and irreversible. Fix: resolveApiKeyInfo() returns both the key and its source. When the env-var path matches an OPENAI_API_KEY entry in the current directory's .env, .env.<NODE_ENV>, or .env.local file, we set a warning. requireApiKey() prints "Using OpenAI key from <source>" plus the warning before the run — never the key itself. Adds 6 unit tests covering: config-vs-env precedence, env-only (no match), env+cwd .env match, quoted/exported values, value-mismatch (no false positive), and the no-leak invariant for requireApiKey stderr output. Contributed by @jbetala7 via #1278. Closes #1248. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(browse): guard full-page screenshots against Anthropic vision API >2000px brick (#1214) Full-page screenshots of tall pages routinely exceeded 2000px on the longest dimension, silently bricking the agent's session: the resulting base64 reached the Anthropic vision API which rejected the oversized image, leaving the agent burning turns on a useless blob with no stderr trace from the browse side. Adds browse/src/screenshot-size-guard.ts as a shared helper: - guardScreenshotBuffer(buf) → downscales in-memory if max(w,h) > 2000 - guardScreenshotPath(path) → file-mode variant that rewrites in place - Aspect ratio preserved via sharp's resize fit:inside - Stderr diagnostic on any downscale so callers can see when it fired - Lazy sharp import so non-screenshot paths pay no startup cost Wires the guard into all three full-page callsites codex review flagged: - browse/src/snapshot.ts: annotated + heatmap fullPage captures - browse/src/meta-commands.ts: screenshot command (path + base64 fullPage modes) plus the responsive 3-viewport sweep - browse/src/write-commands.ts: prettyscreenshot fullPage path Covers seven unit cases (pass-through, downscale, aspect ratio, exactly-2000px edge, file-mode rewrite) plus a static invariant test that fails the build if any of the three callsites stops importing the guard. Closes #1214. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(security): add Node sidecar entry for L4 prompt-injection classifier (#1370) The L4 TestSavant classifier in browse/src/security-classifier.ts can't be imported into the compiled browse server (onnxruntime-node dlopen fails from Bun's compile extract dir per CLAUDE.md). The agent that used to host it (sidebar-agent.ts) was removed when the PTY proved out — leaving the classifier file shipped but with zero callers. Exactly the gap codex flagged in #1370. Adds browse/src/security-sidecar-entry.ts: a Node script that runs the classifier as a subprocess of the browse server. It reads NDJSON requests from stdin and writes id-correlated NDJSON responses to stdout, supporting: - op: "scan-page-content" — full L4 classifier scan - op: "ping" — liveness probe for the client's health check - op: "status" — classifier readiness (used by /pty-inject-scan to surface l4 { available: bool } in its response) Plus browse/src/find-security-sidecar.ts: a resolver that locates node + the bundled JS entry (browse/dist/security-sidecar.js, built in a follow-up package.json change) or falls back to the dev TS entry. Returns null cleanly when node isn't on PATH so the calling endpoint can degrade per D7 (extension WARN + user confirm). C17 of the security-stack wave. C18 adds the IPC client + lifecycle management; C19 wires the endpoint; C20 routes the extension through it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(security): sidecar IPC client with lifecycle + circuit breaker (#1370) Adds browse/src/security-sidecar-client.ts to manage the Node L4 classifier subprocess from the compiled browse server: - Lazy spawn on first scan; reuses the same process across requests - Id-correlated request/response via NDJSON over stdio - 5s default per-scan timeout; 64KB payload cap (short-circuits before spawn so oversized requests don't waste a process) - 3-in-10-minutes respawn cap → trips circuit breaker; subsequent scans throw immediately so the /pty-inject-scan endpoint can surface l4 { available: false } to the extension and degrade to WARN+confirm - process.on('exit') sends SIGTERM to the child for clean teardown - isSidecarAvailable() lets the endpoint probe before scan calls so the response shape reflects degraded mode honestly Unit tests cover the payload cap, the availability probe, and the breaker-doesn't-crash invariant under repeated rejected calls. C18 of the security-stack wave. C19 adds POST /pty-inject-scan; C20 routes the extension through it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(security): add POST /pty-inject-scan endpoint for pre-PTY-inject scans (#1370) The sidebar's gstackInjectToTerminal callers (toolbar Cleanup, Inspector "Send to Code") were piping page-derived text directly into the live claude PTY with ZERO classifier processing — the gap codex flagged in #1370. The documented sidebar security stack had a hole the size of every Cleanup-button click. Adds POST /pty-inject-scan to browse/src/server.ts: - Local-only binding (NOT in TUNNEL_PATHS — tunnel attempts get the general 404 path; never reaches the scan logic) - Root-token auth via existing validateAuth() — 401 on unauth - 64KB request cap → 413 + payload-too-large body - 5s scan timeout via sidecar client - URL-blocklist forced to BLOCK in PTY context (page-derived REPL input is higher-risk than ordinary tool output) - L4 ML classifier via the sidecar when available; degrades to WARN per D7 when sidecar is unavailable - Response goes through JSON.stringify(..., sanitizeReplacer) per v1.38.0.0 Unicode-egress hardening - Imports only from security-sidecar-client.ts, never directly from security-classifier.ts (which would brick the compiled Bun binary) Seven static-invariant tests pin the POST verb, auth gate, 64KB cap, tunnel-listener exclusion, sanitizeReplacer wrapping, l4 availability shape, and the no-direct-classifier-import rule. C19 of the security-stack wave. C20 routes the extension through it; C21 adds the invariant AST check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(extension): route gstackInjectToTerminal through /pty-inject-scan (#1370) Closes the documented-vs-shipped gap codex flagged in #1370. The sidebar's two PTY-injection call sites (Inspector "Send to Code" and toolbar Cleanup) now pre-scan via the new /pty-inject-scan endpoint before writing to the live claude REPL. Adds window.gstackScanForPTYInject(text, origin) to extension/sidepanel-terminal.js: - Async, returns { allow, verdict, reasons, l4 } - POST to /pty-inject-scan with the existing root-token auth - WARN+confirm on scan failure (network down, sidecar absent, etc.) rather than silent PASS — D7 honest-degradation gstackInjectToTerminal stays synchronous, returns boolean. Per D6: keeping the inject sync means existing `const ok = ...?.()` callers don't break, and the invariant test in test/extension-pty-inject-invariant.test.ts can statically pin that every call goes through the scan first. extension/sidepanel.js call sites updated: - inspectorSendBtn click → await scan, BLOCK drops + WARN prompts via window.confirm, PASS injects silently - runCleanup() → same flow. Static cleanup prompt always PASSes but still routes through scan to honor the invariant. C20 of the security-stack wave. C21 adds the static invariant test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(security): invariant — extension PTY inject must be scan-gated (#1370) Static-analysis invariant test that fails the build if any extension/*.js path calls window.gstackInjectToTerminal without a preceding window.gstackScanForPTYInject in the same enclosing function. Closes the documented-vs-shipped gap codex demanded a machine check on. Rules: - Rule 1: any file that calls inject must also reference scan - Rule 2: in the enclosing function (function declaration, arrow, async (), event handler), a scan call must appear before the inject call by source position - Exemption: sidepanel-terminal.js (the file that DEFINES the inject function) is exempt from Rule 2 since the definition is not a call Plus two structural checks: - sidepanel-terminal.js defines both the inject and scan functions - inject stays SYNCHRONOUS (no `async` modifier) per D6 — async would silently break the `const ok = ...?.()` pattern at every caller C21 of the security-stack wave. The sidecar architecture (#1370) is complete: server-side L1-L3 + L4-via-sidecar (C17+C18+C19), extension pre-scan wiring (C20), and now the regression gate (C21). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(browse): opt-in extended stealth mode with 6 detection-vector patches (#1112) Rebases @garrytan's PR #1112 (Apr 2026, abandoned) onto the current browse/src/stealth.ts contract. The existing minimal "codex narrowed" stealth (webdriver-mask + AutomationControlled launch arg) stays the default. PR #1112's six additional patches are added behind an opt-in GSTACK_STEALTH=extended env flag. Extended-mode patches (applied AFTER the default mask, in order): 1. delete navigator.webdriver from prototype (not just the getter — detectors check `"webdriver" in navigator`) 2. WebGL renderer spoof to Apple M1 Pro (SwiftShader was the #1 software-GPU tell in containers) 3. navigator.plugins returns a PluginArray-prototype-passing array with MimeType objects and namedItem() 4. window.chrome populated with chrome.app, chrome.runtime, chrome.loadTimes(), chrome.csi() with realistic shapes 5. navigator.mediaDevices backfilled when headless drops it 6. CDP cdc_*-prefixed window globals cleared Why opt-in: the default mode's contract is fingerprint CONSISTENCY, which protects against detectors that flag spoofing mismatch. Extended mode actively lies about the environment; sites that reflect on these properties can break. Users who hit detection in default mode can flip GSTACK_STEALTH=extended for SannySoft 100% pass-rate. Twenty unit tests pin the env-flag semantics, all six patches' code presence, and the applyStealth wiring order. Live SannySoft pass-rate verification stays in the periodic-tier E2E suite. Contributed by @garrytan via #1112 (rebased — original PR opened before the codex-narrowed minimum landed; rebase preserves the narrowed default while adding the SannySoft-passing path as opt-in). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(fixtures): regenerate ship-SKILL.md golden baselines after C10-C13 + C16 templates Updates the three ship-SKILL.md golden baselines (claude, codex, factory hosts) to match the new shape produced by: - C10 #1209 codex argv (prompt + diff scope, no --base) - C11 #1492 merge-base diff (DIFF_BASE= preamble) - C13 #1197 command -v for codex detection - C12 + boundary preservation per regen-enforcing test Per CLAUDE.md SKILL.md workflow: edit the .tmpl, run gen:skill-docs, commit the regenerated outputs together. Goldens are part of the regen contract — without this commit, test/host-config.test.ts' golden-baseline checks fail with the diff codex review surfaced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(release): v1.41.0.0 — Daegu wave (24 bisect commits, 14 user-facing fixes) Bumps VERSION 1.40.0.0 → 1.41.0.0. CHANGELOG entry follows the release-summary format in CLAUDE.md: two-line headline, lead paragraph, "The numbers that matter" table, "What this means for builders" closer, then itemized Added/Changed/Fixed/For contributors with inline credit to every PR author and original issue reporter. Scale-aware bump per CLAUDE.md: 24 commits, ~6000 LOC net, substantial new capability across security (PTY sidecar wiring), install (Windows build chain), compat (gbrain 0.18-0.35, Codex CLI 0.130+), and quality (screenshot guard, design key disclosure, extended stealth opt-in). MINOR is the right call. Closes for users: #1567, #1559, #1569, #1346, #1418, #1538, #1537, #1530, #1457, #1561, #1554, #1479, #1503, #1248, #1214, #1370, #1327, #1193 pattern, #1152 pattern. Credit retained inline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(find-browse): resolve source-checkout layout <git-root>/browse/dist/browse[.exe] windows-setup-e2e.yml runs `bun browse/src/find-browse.ts` against a freshly-built repo where binaries land at browse/dist/browse.exe (no .claude/skills/gstack/ install layout). The previous markers chain only matched .codex/.agents/.claude prefixed paths, so find-browse exited "not found" even when the binary was present. Adds a source-checkout fallback after the marker scan: if no installed layout resolves but <git-root>/browse/dist/browse[.exe] exists, return that. Three real callers hit this path: - gstack repo dev workflow before `./setup` runs - windows-setup-e2e.yml CI (the breakage that surfaced this) - make-pdf consumers running from a sibling source checkout Smoke-verified: a fresh git repo with browse/dist/browse on disk now resolves through the source-checkout branch (was returning null before this commit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(release): bump v1.41.0.0 → v1.42.0.0 to clear queue collision with #1574 The version-gate workflow flagged a collision: PR #1574 (garrytan/colombo-v3) already claims v1.41.0.0, and #1592 (fix/audit-critical-high-bugs) claims v1.41.1.0. Per CLAUDE.md's workspace-aware ship rule, queue-advancing past a claimed version within the same bump level is permitted — MINOR work landing on top of a queued MINOR still reads as MINOR relative to main. Util's suggested next slot is v1.42.0.0; taking it. CHANGELOG entry header bumped + dated 2026-05-19; entry body unchanged (same wave content, same credit list). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1143 lines
57 KiB
TypeScript
1143 lines
57 KiB
TypeScript
import type { TemplateContext } from './types';
|
|
import { AI_SLOP_BLACKLIST, OPENAI_HARD_REJECTIONS, OPENAI_LITMUS_CHECKS } from './constants';
|
|
|
|
export function generateDesignReviewLite(ctx: TemplateContext): string {
|
|
const litmusList = OPENAI_LITMUS_CHECKS.map((item, i) => `${i + 1}. ${item}`).join(' ');
|
|
const rejectionList = OPENAI_HARD_REJECTIONS.map((item, i) => `${i + 1}. ${item}`).join(' ');
|
|
// Codex block only for Claude host
|
|
const codexBlock = ctx.host === 'codex' ? '' : `
|
|
|
|
7. **Codex design voice** (optional, automatic if available):
|
|
|
|
\`\`\`bash
|
|
command -v codex >/dev/null 2>&1 && echo "CODEX_AVAILABLE" || echo "CODEX_NOT_AVAILABLE"
|
|
\`\`\`
|
|
|
|
If Codex is available, run a lightweight design check on the diff:
|
|
|
|
\`\`\`bash
|
|
TMPERR_DRL=$(mktemp /tmp/codex-drl-XXXXXXXX)
|
|
_REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; }
|
|
codex exec "Review the git diff on this branch. Run 7 litmus checks (YES/NO each): ${litmusList} Flag any hard rejections: ${rejectionList} 5 most important design findings only. Reference file:line." -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_DRL"
|
|
\`\`\`
|
|
|
|
Use a 5-minute timeout (\`timeout: 300000\`). After the command completes, read stderr:
|
|
\`\`\`bash
|
|
cat "$TMPERR_DRL" && rm -f "$TMPERR_DRL"
|
|
\`\`\`
|
|
|
|
**Error handling:** All errors are non-blocking. On auth failure, timeout, or empty response — skip with a brief note and continue.
|
|
|
|
Present Codex output under a \`CODEX (design):\` header, merged with the checklist findings above.`;
|
|
|
|
return `## Design Review (conditional, diff-scoped)
|
|
|
|
Check if the diff touches frontend files using \`gstack-diff-scope\`:
|
|
|
|
\`\`\`bash
|
|
source <(${ctx.paths.binDir}/gstack-diff-scope <base> 2>/dev/null)
|
|
\`\`\`
|
|
|
|
**If \`SCOPE_FRONTEND=false\`:** Skip design review silently. No output.
|
|
|
|
**If \`SCOPE_FRONTEND=true\`:**
|
|
|
|
1. **Check for DESIGN.md.** If \`DESIGN.md\` or \`design-system.md\` exists in the repo root, read it. All design findings are calibrated against it — patterns blessed in DESIGN.md are not flagged. If not found, use universal design principles.
|
|
|
|
2. **Read \`.claude/skills/review/design-checklist.md\`.** If the file cannot be read, skip design review with a note: "Design checklist not found — skipping design review."
|
|
|
|
3. **Read each changed frontend file** (full file, not just diff hunks). Frontend files are identified by the patterns listed in the checklist.
|
|
|
|
4. **Apply the design checklist** against the changed files. For each item:
|
|
- **[HIGH] mechanical CSS fix** (\`outline: none\`, \`!important\`, \`font-size < 16px\`): classify as AUTO-FIX
|
|
- **[HIGH/MEDIUM] design judgment needed**: classify as ASK
|
|
- **[LOW] intent-based detection**: present as "Possible — verify visually or run /design-review"
|
|
|
|
5. **Include findings** in the review output under a "Design Review" header, following the output format in the checklist. Design findings merge with code review findings into the same Fix-First flow.
|
|
|
|
6. **Log the result** for the Review Readiness Dashboard:
|
|
|
|
\`\`\`bash
|
|
${ctx.paths.binDir}/gstack-review-log '{"skill":"design-review-lite","timestamp":"TIMESTAMP","status":"STATUS","findings":N,"auto_fixed":M,"commit":"COMMIT"}'
|
|
\`\`\`
|
|
|
|
Substitute: TIMESTAMP = ISO 8601 datetime, STATUS = "clean" if 0 findings or "issues_found", N = total findings, M = auto-fixed count, COMMIT = output of \`git rev-parse --short HEAD\`.${codexBlock}`;
|
|
}
|
|
|
|
// NOTE: design-checklist.md is a subset of this methodology for code-level detection.
|
|
// When adding items here, also update review/design-checklist.md, and vice versa.
|
|
export function generateDesignMethodology(_ctx: TemplateContext): string {
|
|
return `## Modes
|
|
|
|
### Full (default)
|
|
Systematic review of all pages reachable from homepage. Visit 5-8 pages. Full checklist evaluation, responsive screenshots, interaction flow testing. Produces complete design audit report with letter grades.
|
|
|
|
### Quick (\`--quick\`)
|
|
Homepage + 2 key pages only. First Impression + Design System Extraction + abbreviated checklist. Fastest path to a design score.
|
|
|
|
### Deep (\`--deep\`)
|
|
Comprehensive review: 10-15 pages, every interaction flow, exhaustive checklist. For pre-launch audits or major redesigns.
|
|
|
|
### Diff-aware (automatic when on a feature branch with no URL)
|
|
When on a feature branch, scope to pages affected by the branch changes:
|
|
1. Analyze the branch diff: \`git diff main...HEAD --name-only\`
|
|
2. Map changed files to affected pages/routes
|
|
3. Detect running app on common local ports (3000, 4000, 8080)
|
|
4. Audit only affected pages, compare design quality before/after
|
|
|
|
### Regression (\`--regression\` or previous \`design-baseline.json\` found)
|
|
Run full audit, then load previous \`design-baseline.json\`. Compare: per-category grade deltas, new findings, resolved findings. Output regression table in report.
|
|
|
|
---
|
|
|
|
## Phase 1: First Impression
|
|
|
|
The most uniquely designer-like output. Form a gut reaction before analyzing anything.
|
|
|
|
1. Navigate to the target URL
|
|
2. Take a full-page desktop screenshot: \`$B screenshot "$REPORT_DIR/screenshots/first-impression.png"\`
|
|
3. Write the **First Impression** using this structured critique format:
|
|
- "The site communicates **[what]**." (what it says at a glance — competence? playfulness? confusion?)
|
|
- "I notice **[observation]**." (what stands out, positive or negative — be specific)
|
|
- "The first 3 things my eye goes to are: **[1]**, **[2]**, **[3]**." (hierarchy check — are these the 3 things the designer intended? If not, the visual hierarchy is lying.)
|
|
- "If I had to describe this in one word: **[word]**." (gut verdict)
|
|
|
|
**Narration mode:** Write this section in first person, as if you are a user scanning the page for the first time. "I'm looking at this page... my eye goes to the logo, then a wall of text I skip entirely, then... wait, is that a button?" Name the specific element, its position, its visual weight. If you can't name it specifically, you're not actually scanning, you're generating platitudes.
|
|
|
|
**Page Area Test:** Point at each clearly defined area of the page. Can you instantly name its purpose? ("Things I can buy," "Today's deals," "How to search.") Areas you can't name in 2 seconds are poorly defined. List them.
|
|
|
|
This is the section users read first. Be opinionated. A designer doesn't hedge — they react.
|
|
|
|
---
|
|
|
|
## Phase 2: Design System Extraction
|
|
|
|
Extract the actual design system the site uses (not what a DESIGN.md says, but what's rendered):
|
|
|
|
\`\`\`bash
|
|
# Fonts in use (capped at 500 elements to avoid timeout)
|
|
$B js "JSON.stringify([...new Set([...document.querySelectorAll('*')].slice(0,500).map(e => getComputedStyle(e).fontFamily))])"
|
|
|
|
# Color palette in use
|
|
$B js "JSON.stringify([...new Set([...document.querySelectorAll('*')].slice(0,500).flatMap(e => [getComputedStyle(e).color, getComputedStyle(e).backgroundColor]).filter(c => c !== 'rgba(0, 0, 0, 0)'))])"
|
|
|
|
# Heading hierarchy
|
|
$B js "JSON.stringify([...document.querySelectorAll('h1,h2,h3,h4,h5,h6')].map(h => ({tag:h.tagName, text:h.textContent.trim().slice(0,50), size:getComputedStyle(h).fontSize, weight:getComputedStyle(h).fontWeight})))"
|
|
|
|
# Touch target audit (find undersized interactive elements)
|
|
$B js "JSON.stringify([...document.querySelectorAll('a,button,input,[role=button]')].filter(e => {const r=e.getBoundingClientRect(); return r.width>0 && (r.width<44||r.height<44)}).map(e => ({tag:e.tagName, text:(e.textContent||'').trim().slice(0,30), w:Math.round(e.getBoundingClientRect().width), h:Math.round(e.getBoundingClientRect().height)})).slice(0,20))"
|
|
|
|
# Performance baseline
|
|
$B perf
|
|
\`\`\`
|
|
|
|
Structure findings as an **Inferred Design System**:
|
|
- **Fonts:** list with usage counts. Flag if >3 distinct font families.
|
|
- **Colors:** palette extracted. Flag if >12 unique non-gray colors. Note warm/cool/mixed.
|
|
- **Heading Scale:** h1-h6 sizes. Flag skipped levels, non-systematic size jumps.
|
|
- **Spacing Patterns:** sample padding/margin values. Flag non-scale values.
|
|
|
|
After extraction, offer: *"Want me to save this as your DESIGN.md? I can lock in these observations as your project's design system baseline."*
|
|
|
|
---
|
|
|
|
## Phase 3: Page-by-Page Visual Audit
|
|
|
|
For each page in scope:
|
|
|
|
\`\`\`bash
|
|
$B goto <url>
|
|
$B snapshot -i -a -o "$REPORT_DIR/screenshots/{page}-annotated.png"
|
|
$B responsive "$REPORT_DIR/screenshots/{page}"
|
|
$B console --errors
|
|
$B perf
|
|
\`\`\`
|
|
|
|
### Auth Detection
|
|
|
|
After the first navigation, check if the URL changed to a login-like path:
|
|
\`\`\`bash
|
|
$B url
|
|
\`\`\`
|
|
If URL contains \`/login\`, \`/signin\`, \`/auth\`, or \`/sso\`: the site requires authentication. AskUserQuestion: "This site requires authentication. Want to import cookies from your browser? Run \`/setup-browser-cookies\` first if needed."
|
|
|
|
### Trunk Test (run on every page)
|
|
|
|
Imagine being dropped on this page with no context. Can you immediately answer:
|
|
1. What site is this? (Site ID visible and identifiable)
|
|
2. What page am I on? (Page name prominent, matches what I clicked)
|
|
3. What are the major sections? (Primary nav visible and clear)
|
|
4. What are my options at this level? (Local nav or content choices obvious)
|
|
5. Where am I in the scheme of things? ("You are here" indicator, breadcrumbs)
|
|
6. How can I search? (Search box findable without hunting)
|
|
|
|
Score: PASS (all 6 clear) / PARTIAL (4-5 clear) / FAIL (3 or fewer clear).
|
|
A FAIL on the trunk test is a HIGH-impact finding regardless of how polished the visual design is.
|
|
|
|
### Design Audit Checklist (10 categories, ~80 items)
|
|
|
|
Apply these at each page. Each finding gets an impact rating (high/medium/polish) and category.
|
|
|
|
**1. Visual Hierarchy & Composition** (8 items)
|
|
- Clear focal point? One primary CTA per view?
|
|
- Eye flows naturally top-left to bottom-right?
|
|
- Visual noise — competing elements fighting for attention?
|
|
- Information density appropriate for content type?
|
|
- Z-index clarity — nothing unexpectedly overlapping?
|
|
- Above-the-fold content communicates purpose in 3 seconds?
|
|
- Squint test: hierarchy still visible when blurred?
|
|
- White space is intentional, not leftover?
|
|
|
|
**2. Typography** (15 items)
|
|
- Font count <=3 (flag if more)
|
|
- Scale follows ratio (1.25 major third or 1.333 perfect fourth)
|
|
- Line-height: 1.5x body, 1.15-1.25x headings
|
|
- Measure: 45-75 chars per line (66 ideal)
|
|
- Heading hierarchy: no skipped levels (h1→h3 without h2)
|
|
- Weight contrast: >=2 weights used for hierarchy
|
|
- No blacklisted fonts (Papyrus, Comic Sans, Lobster, Impact, Jokerman)
|
|
- If primary font is Inter/Roboto/Open Sans/Poppins → flag as potentially generic
|
|
- \`text-wrap: balance\` or \`text-pretty\` on headings (check via \`$B css <heading> text-wrap\`)
|
|
- Curly quotes used, not straight quotes
|
|
- Ellipsis character (\`…\`) not three dots (\`...\`)
|
|
- \`font-variant-numeric: tabular-nums\` on number columns
|
|
- Body text >= 16px
|
|
- Caption/label >= 12px
|
|
- No letterspacing on lowercase text
|
|
|
|
**3. Color & Contrast** (10 items)
|
|
- Palette coherent (<=12 unique non-gray colors)
|
|
- WCAG AA: body text 4.5:1, large text (18px+) 3:1, UI components 3:1
|
|
- Semantic colors consistent (success=green, error=red, warning=yellow/amber)
|
|
- No color-only encoding (always add labels, icons, or patterns)
|
|
- Dark mode: surfaces use elevation, not just lightness inversion
|
|
- Dark mode: text off-white (~#E0E0E0), not pure white
|
|
- Primary accent desaturated 10-20% in dark mode
|
|
- \`color-scheme: dark\` on html element (if dark mode present)
|
|
- No red/green only combinations (8% of men have red-green deficiency)
|
|
- Neutral palette is warm or cool consistently — not mixed
|
|
|
|
**4. Spacing & Layout** (12 items)
|
|
- Grid consistent at all breakpoints
|
|
- Spacing uses a scale (4px or 8px base), not arbitrary values
|
|
- Alignment is consistent — nothing floats outside the grid
|
|
- Rhythm: related items closer together, distinct sections further apart
|
|
- Border-radius hierarchy (not uniform bubbly radius on everything)
|
|
- Inner radius = outer radius - gap (nested elements)
|
|
- No horizontal scroll on mobile
|
|
- Max content width set (no full-bleed body text)
|
|
- \`env(safe-area-inset-*)\` for notch devices
|
|
- URL reflects state (filters, tabs, pagination in query params)
|
|
- Flex/grid used for layout (not JS measurement)
|
|
- Breakpoints: mobile (375), tablet (768), desktop (1024), wide (1440)
|
|
|
|
**5. Interaction States** (10 items)
|
|
- Hover state on all interactive elements
|
|
- \`focus-visible\` ring present (never \`outline: none\` without replacement)
|
|
- Active/pressed state with depth effect or color shift
|
|
- Disabled state: reduced opacity + \`cursor: not-allowed\`
|
|
- Loading: skeleton shapes match real content layout
|
|
- Empty states: warm message + primary action + visual (not just "No items.")
|
|
- Error messages: specific + include fix/next step
|
|
- Success: confirmation animation or color, auto-dismiss
|
|
- Touch targets >= 44px on all interactive elements
|
|
- \`cursor: pointer\` on all clickable elements
|
|
- Mindless choice audit: every decision point (button, link, dropdown, modal choice) is a mindless click (obvious what happens). If a click requires thought about whether it's the right choice, flag as HIGH.
|
|
|
|
**6. Responsive Design** (8 items)
|
|
- Mobile layout makes *design* sense (not just stacked desktop columns)
|
|
- Touch targets sufficient on mobile (>= 44px)
|
|
- No horizontal scroll on any viewport
|
|
- Images handle responsive (srcset, sizes, or CSS containment)
|
|
- Text readable without zooming on mobile (>= 16px body)
|
|
- Navigation collapses appropriately (hamburger, bottom nav, etc.)
|
|
- Forms usable on mobile (correct input types, no autoFocus on mobile)
|
|
- No \`user-scalable=no\` or \`maximum-scale=1\` in viewport meta
|
|
|
|
**7. Motion & Animation** (6 items)
|
|
- Easing: ease-out for entering, ease-in for exiting, ease-in-out for moving
|
|
- Duration: 50-700ms range (nothing slower unless page transition)
|
|
- Purpose: every animation communicates something (state change, attention, spatial relationship)
|
|
- \`prefers-reduced-motion\` respected (check: \`$B js "matchMedia('(prefers-reduced-motion: reduce)').matches"\`)
|
|
- No \`transition: all\` — properties listed explicitly
|
|
- Only \`transform\` and \`opacity\` animated (not layout properties like width, height, top, left)
|
|
|
|
**8. Content & Microcopy** (8 items)
|
|
- Empty states designed with warmth (message + action + illustration/icon)
|
|
- Error messages specific: what happened + why + what to do next
|
|
- Button labels specific ("Save API Key" not "Continue" or "Submit")
|
|
- No placeholder/lorem ipsum text visible in production
|
|
- Truncation handled (\`text-overflow: ellipsis\`, \`line-clamp\`, or \`break-words\`)
|
|
- Active voice ("Install the CLI" not "The CLI will be installed")
|
|
- Loading states end with \`…\` ("Saving…" not "Saving...")
|
|
- Destructive actions have confirmation modal or undo window
|
|
- Happy talk detection: scan for introductory paragraphs that start with "Welcome to..." or tell users how great the site is. If you can hear "blah blah blah", it's happy talk. Flag for removal.
|
|
- Instructions detection: any visible instructions longer than one sentence. If users need to read instructions, the design has failed. Flag the instructions AND the interaction they're compensating for.
|
|
- Happy talk word count: count total visible words on the page. Classify each text block as "useful content" vs "happy talk" (welcome paragraphs, self-congratulatory text, instructions nobody reads). Report: "This page has X words. Y (Z%) are happy talk."
|
|
|
|
**9. AI Slop Detection** (10 anti-patterns — the blacklist)
|
|
|
|
The test: would a human designer at a respected studio ever ship this?
|
|
|
|
${AI_SLOP_BLACKLIST.map(item => `- ${item}`).join('\n')}
|
|
|
|
**10. Performance as Design** (6 items)
|
|
- LCP < 2.0s (web apps), < 1.5s (informational sites)
|
|
- CLS < 0.1 (no visible layout shifts during load)
|
|
- Skeleton quality: shapes match real content layout, shimmer animation
|
|
- Images: \`loading="lazy"\`, width/height dimensions set, WebP/AVIF format
|
|
- Fonts: \`font-display: swap\`, preconnect to CDN origins
|
|
- No visible font swap flash (FOUT) — critical fonts preloaded
|
|
|
|
---
|
|
|
|
## Phase 4: Interaction Flow Review
|
|
|
|
Walk 2-3 key user flows and evaluate the *feel*, not just the function:
|
|
|
|
\`\`\`bash
|
|
$B snapshot -i
|
|
$B click @e3 # perform action
|
|
$B snapshot -D # diff to see what changed
|
|
\`\`\`
|
|
|
|
Evaluate:
|
|
- **Response feel:** Does clicking feel responsive? Any delays or missing loading states?
|
|
- **Transition quality:** Are transitions intentional or generic/absent?
|
|
- **Feedback clarity:** Did the action clearly succeed or fail? Is the feedback immediate?
|
|
- **Form polish:** Focus states visible? Validation timing correct? Errors near the source?
|
|
|
|
**Narration mode:** Narrate the flow in first person. "I click 'Sign Up'... spinner appears... 3 seconds pass... still spinning... I'm getting nervous. Finally the dashboard loads, but where am I? The nav doesn't highlight anything." Name the specific element, its position, its visual weight. If you can't name it specifically, you're not actually experiencing the flow, you're generating platitudes.
|
|
|
|
### Goodwill Reservoir (track across the flow)
|
|
|
|
As you walk the user flow, maintain a mental goodwill meter (starts at 70/100).
|
|
These scores are heuristic, not measured. The value is in identifying specific
|
|
drains and fills, not in the final number.
|
|
|
|
Subtract points for:
|
|
- Hidden information the user would want (pricing, contact, shipping): subtract 15
|
|
- Format punishment (rejecting valid input like dashes in phone numbers): subtract 10
|
|
- Unnecessary information requests: subtract 10
|
|
- Interstitials, splash screens, forced tours blocking the task: subtract 15
|
|
- Sloppy or unprofessional appearance: subtract 10
|
|
- Ambiguous choices that require thinking: subtract 5 each
|
|
|
|
Add points for:
|
|
- Top user tasks are obvious and prominent: add 10
|
|
- Upfront about costs and limitations: add 5
|
|
- Saves steps (direct links, smart defaults, autofill): add 5 each
|
|
- Graceful error recovery with specific fix instructions: add 10
|
|
- Apologizes when things go wrong: add 5
|
|
|
|
Report the final goodwill score with a visual dashboard:
|
|
|
|
\`\`\`
|
|
Goodwill: 70 ████████████████████░░░░░░░░░░
|
|
Step 1: Login page 70 → 75 (+5 obvious primary action)
|
|
Step 2: Dashboard 75 → 60 (-15 interstitial tour popup)
|
|
Step 3: Settings 60 → 50 (-10 format punishment on phone)
|
|
Step 4: Billing 50 → 35 (-15 hidden pricing info)
|
|
FINAL: 35/100 ⚠️ CRITICAL UX DEBT
|
|
\`\`\`
|
|
|
|
Below 30 = critical UX debt. 30-60 = needs work. Above 60 = healthy.
|
|
Include the biggest drains and fills as specific findings.
|
|
|
|
---
|
|
|
|
## Phase 5: Cross-Page Consistency
|
|
|
|
Compare screenshots and observations across pages for:
|
|
- Navigation bar consistent across all pages?
|
|
- Footer consistent?
|
|
- Component reuse vs one-off designs (same button styled differently on different pages?)
|
|
- Tone consistency (one page playful while another is corporate?)
|
|
- Spacing rhythm carries across pages?
|
|
|
|
---
|
|
|
|
## Phase 6: Compile Report
|
|
|
|
### Output Locations
|
|
|
|
**Local:** \`.gstack/design-reports/design-audit-{domain}-{YYYY-MM-DD}.md\`
|
|
|
|
**Project-scoped:**
|
|
\`\`\`bash
|
|
eval "$(~/.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\`
|
|
|
|
**Baseline:** Write \`design-baseline.json\` for regression mode:
|
|
\`\`\`json
|
|
{
|
|
"date": "YYYY-MM-DD",
|
|
"url": "<target>",
|
|
"designScore": "B",
|
|
"aiSlopScore": "C",
|
|
"categoryGrades": { "hierarchy": "A", "typography": "B", ... },
|
|
"findings": [{ "id": "FINDING-001", "title": "...", "impact": "high", "category": "typography" }]
|
|
}
|
|
\`\`\`
|
|
|
|
### Scoring System
|
|
|
|
**Dual headline scores:**
|
|
- **Design Score: {A-F}** — weighted average of all 10 categories
|
|
- **AI Slop Score: {A-F}** — standalone grade with pithy verdict
|
|
|
|
**Per-category grades:**
|
|
- **A:** Intentional, polished, delightful. Shows design thinking.
|
|
- **B:** Solid fundamentals, minor inconsistencies. Looks professional.
|
|
- **C:** Functional but generic. No major problems, no design point of view.
|
|
- **D:** Noticeable problems. Feels unfinished or careless.
|
|
- **F:** Actively hurting user experience. Needs significant rework.
|
|
|
|
**Grade computation:** Each category starts at A. Each High-impact finding drops one letter grade. Each Medium-impact finding drops half a letter grade. Polish findings are noted but do not affect grade. Minimum is F.
|
|
|
|
**Category weights for Design Score:**
|
|
| Category | Weight |
|
|
|----------|--------|
|
|
| Visual Hierarchy | 15% |
|
|
| Typography | 15% |
|
|
| Spacing & Layout | 15% |
|
|
| Color & Contrast | 10% |
|
|
| Interaction States | 10% |
|
|
| Responsive | 10% |
|
|
| Content Quality | 10% |
|
|
| AI Slop | 5% |
|
|
| Motion | 5% |
|
|
| Performance Feel | 5% |
|
|
|
|
AI Slop is 5% of Design Score but also graded independently as a headline metric.
|
|
|
|
### Regression Output
|
|
|
|
When previous \`design-baseline.json\` exists or \`--regression\` flag is used:
|
|
- Load baseline grades
|
|
- Compare: per-category deltas, new findings, resolved findings
|
|
- Append regression table to report
|
|
|
|
---
|
|
|
|
## Design Critique Format
|
|
|
|
Use structured feedback, not opinions:
|
|
- "I notice..." — observation (e.g., "I notice the primary CTA competes with the secondary action")
|
|
- "I wonder..." — question (e.g., "I wonder if users will understand what 'Process' means here")
|
|
- "What if..." — suggestion (e.g., "What if we moved search to a more prominent position?")
|
|
- "I think... because..." — reasoned opinion (e.g., "I think the spacing between sections is too uniform because it doesn't create hierarchy")
|
|
|
|
Tie everything to user goals and product objectives. Always suggest specific improvements alongside problems.
|
|
|
|
---
|
|
|
|
## Important Rules
|
|
|
|
1. **Think like a designer, not a QA engineer.** You care whether things feel right, look intentional, and respect the user. You do NOT just care whether things "work."
|
|
2. **Screenshots are evidence.** Every finding needs at least one screenshot. Use annotated screenshots (\`snapshot -a\`) to highlight elements.
|
|
3. **Be specific and actionable.** "Change X to Y because Z" — not "the spacing feels off."
|
|
4. **Never read source code.** Evaluate the rendered site, not the implementation. (Exception: offer to write DESIGN.md from extracted observations.)
|
|
5. **AI Slop detection is your superpower.** Most developers can't evaluate whether their site looks AI-generated. You can. Be direct about it.
|
|
6. **Quick wins matter.** Always include a "Quick Wins" section — the 3-5 highest-impact fixes that take <30 minutes each.
|
|
7. **Use \`snapshot -C\` for tricky UIs.** Finds clickable divs that the accessibility tree misses.
|
|
8. **Responsive is design, not just "not broken."** A stacked desktop layout on mobile is not responsive design — it's lazy. Evaluate whether the mobile layout makes *design* sense.
|
|
9. **Document incrementally.** Write each finding to the report as you find it. Don't batch.
|
|
10. **Depth over breadth.** 5-10 well-documented findings with screenshots and specific suggestions > 20 vague observations.
|
|
11. **Show screenshots to the user.** After every \`$B screenshot\`, \`$B snapshot -a -o\`, or \`$B responsive\` command, use the Read tool on the output file(s) so the user can see them inline. For \`responsive\` (3 files), Read all three. This is critical — without it, screenshots are invisible to the user.`;
|
|
}
|
|
|
|
export function generateDesignSketch(_ctx: TemplateContext): string {
|
|
return `## Visual Sketch (UI ideas only)
|
|
|
|
If the chosen approach involves user-facing UI (screens, pages, forms, dashboards,
|
|
or interactive elements), generate a rough wireframe to help the user visualize it.
|
|
If the idea is backend-only, infrastructure, or has no UI component — skip this
|
|
section silently.
|
|
|
|
**Step 1: Gather design context**
|
|
|
|
1. Check if \`DESIGN.md\` exists in the repo root. If it does, read it for design
|
|
system constraints (colors, typography, spacing, component patterns). Use these
|
|
constraints in the wireframe.
|
|
2. Apply core design principles:
|
|
- **Information hierarchy** — what does the user see first, second, third?
|
|
- **Interaction states** — loading, empty, error, success, partial
|
|
- **Edge case paranoia** — what if the name is 47 chars? Zero results? Network fails?
|
|
- **Subtraction default** — "as little design as possible" (Rams). Every element earns its pixels.
|
|
- **Design for trust** — every interface element builds or erodes user trust.
|
|
|
|
**Step 2: Generate wireframe HTML**
|
|
|
|
Generate a single-page HTML file with these constraints:
|
|
- **Intentionally rough aesthetic** — use system fonts, thin gray borders, no color,
|
|
hand-drawn-style elements. This is a sketch, not a polished mockup.
|
|
- Self-contained — no external dependencies, no CDN links, inline CSS only
|
|
- Show the core interaction flow (1-3 screens/states max)
|
|
- Include realistic placeholder content (not "Lorem ipsum" — use content that
|
|
matches the actual use case)
|
|
- Add HTML comments explaining design decisions
|
|
|
|
Write to a temp file:
|
|
\`\`\`bash
|
|
SKETCH_FILE="/tmp/gstack-sketch-$(date +%s).html"
|
|
\`\`\`
|
|
|
|
**Step 3: Render and capture**
|
|
|
|
\`\`\`bash
|
|
$B goto "file://$SKETCH_FILE"
|
|
$B screenshot /tmp/gstack-sketch.png
|
|
\`\`\`
|
|
|
|
If \`$B\` is not available (browse binary not set up), skip the render step. Tell the
|
|
user: "Visual sketch requires the browse binary. Run the setup script to enable it."
|
|
|
|
**Step 4: Present and iterate**
|
|
|
|
Show the screenshot to the user. Ask: "Does this feel right? Want to iterate on the layout?"
|
|
|
|
If they want changes, regenerate the HTML with their feedback and re-render.
|
|
If they approve or say "good enough," proceed.
|
|
|
|
**Step 5: Include in design doc**
|
|
|
|
Reference the wireframe screenshot in the design doc's "Recommended Approach" section.
|
|
The screenshot file at \`/tmp/gstack-sketch.png\` can be referenced by downstream skills
|
|
(\`/plan-design-review\`, \`/design-review\`) to see what was originally envisioned.
|
|
|
|
**Step 6: Outside design voices** (optional)
|
|
|
|
After the wireframe is approved, offer outside design perspectives:
|
|
|
|
\`\`\`bash
|
|
command -v codex >/dev/null 2>&1 && echo "CODEX_AVAILABLE" || echo "CODEX_NOT_AVAILABLE"
|
|
\`\`\`
|
|
|
|
If Codex is available, use AskUserQuestion:
|
|
> "Want outside design perspectives on the chosen approach? Codex proposes a visual thesis, content plan, and interaction ideas. A Claude subagent proposes an alternative aesthetic direction."
|
|
>
|
|
> A) Yes — get outside design voices
|
|
> B) No — proceed without
|
|
|
|
If user chooses A, launch both voices simultaneously:
|
|
|
|
1. **Codex** (via Bash, \`model_reasoning_effort="medium"\`):
|
|
\`\`\`bash
|
|
TMPERR_SKETCH=$(mktemp /tmp/codex-sketch-XXXXXXXX)
|
|
_REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; }
|
|
codex exec "For this product approach, provide: a visual thesis (one sentence — mood, material, energy), a content plan (hero → support → detail → CTA), and 2 interaction ideas that change page feel. Apply beautiful defaults: composition-first, brand-first, cardless, poster not document. Be opinionated." -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="medium"' --enable web_search_cached < /dev/null 2>"$TMPERR_SKETCH"
|
|
\`\`\`
|
|
Use a 5-minute timeout (\`timeout: 300000\`). After completion: \`cat "$TMPERR_SKETCH" && rm -f "$TMPERR_SKETCH"\`
|
|
|
|
2. **Claude subagent** (via Agent tool):
|
|
"For this product approach, what design direction would you recommend? What aesthetic, typography, and interaction patterns fit? What would make this approach feel inevitable to the user? Be specific — font names, hex colors, spacing values."
|
|
|
|
Present Codex output under \`CODEX SAYS (design sketch):\` and subagent output under \`CLAUDE SUBAGENT (design direction):\`.
|
|
Error handling: all non-blocking. On failure, skip and continue.`;
|
|
}
|
|
|
|
export function generateDesignOutsideVoices(ctx: TemplateContext): string {
|
|
// Codex host: strip entirely — Codex should never invoke itself
|
|
if (ctx.host === 'codex') return '';
|
|
|
|
const rejectionList = OPENAI_HARD_REJECTIONS.map((item, i) => `${i + 1}. ${item}`).join('\n');
|
|
const litmusList = OPENAI_LITMUS_CHECKS.map((item, i) => `${i + 1}. ${item}`).join('\n');
|
|
|
|
// Skill-specific configuration
|
|
const isPlanDesignReview = ctx.skillName === 'plan-design-review';
|
|
const isDesignReview = ctx.skillName === 'design-review';
|
|
const isDesignConsultation = ctx.skillName === 'design-consultation';
|
|
|
|
// Determine opt-in behavior and reasoning effort
|
|
const isAutomatic = isDesignReview; // design-review runs automatically
|
|
const reasoningEffort = isDesignConsultation ? 'medium' : 'high'; // creative vs analytical
|
|
|
|
// Build skill-specific Codex prompt
|
|
let codexPrompt: string;
|
|
let subagentPrompt: string;
|
|
|
|
if (isPlanDesignReview) {
|
|
codexPrompt = `Read the plan file at [plan-file-path]. Evaluate this plan's UI/UX design against these criteria.
|
|
|
|
HARD REJECTION — flag if ANY apply:
|
|
${rejectionList}
|
|
|
|
LITMUS CHECKS — answer YES or NO for each:
|
|
${litmusList}
|
|
|
|
HARD RULES — first classify as MARKETING/LANDING PAGE vs APP UI vs HYBRID, then flag violations of the matching rule set:
|
|
- MARKETING: First viewport as one composition, brand-first hierarchy, full-bleed hero, 2-3 intentional motions, composition-first layout
|
|
- APP UI: Calm surface hierarchy, dense but readable, utility language, minimal chrome
|
|
- UNIVERSAL: CSS variables for colors, no default font stacks, one job per section, cards earn existence
|
|
|
|
For each finding: what's wrong, what will happen if it ships unresolved, and the specific fix. Be opinionated. No hedging.`;
|
|
|
|
subagentPrompt = `Read the plan file at [plan-file-path]. You are an independent senior product designer reviewing this plan. You have NOT seen any prior review. Evaluate:
|
|
|
|
1. Information hierarchy: what does the user see first, second, third? Is it right?
|
|
2. Missing states: loading, empty, error, success, partial — which are unspecified?
|
|
3. User journey: what's the emotional arc? Where does it break?
|
|
4. Specificity: does the plan describe SPECIFIC UI ("48px Söhne Bold header, #1a1a1a on white") or generic patterns ("clean modern card-based layout")?
|
|
5. What design decisions will haunt the implementer if left ambiguous?
|
|
|
|
For each finding: what's wrong, severity (critical/high/medium), and the fix.`;
|
|
} else if (isDesignReview) {
|
|
codexPrompt = `Review the frontend source code in this repo. Evaluate against these design hard rules:
|
|
- Spacing: systematic (design tokens / CSS variables) or magic numbers?
|
|
- Typography: expressive purposeful fonts or default stacks?
|
|
- Color: CSS variables with defined system, or hardcoded hex scattered?
|
|
- Responsive: breakpoints defined? calc(100svh - header) for heroes? Mobile tested?
|
|
- A11y: ARIA landmarks, alt text, contrast ratios, 44px touch targets?
|
|
- Motion: 2-3 intentional animations, or zero / ornamental only?
|
|
- Cards: used only when card IS the interaction? No decorative card grids?
|
|
|
|
First classify as MARKETING/LANDING PAGE vs APP UI vs HYBRID, then apply matching rules.
|
|
|
|
LITMUS CHECKS — answer YES/NO:
|
|
${litmusList}
|
|
|
|
HARD REJECTION — flag if ANY apply:
|
|
${rejectionList}
|
|
|
|
Be specific. Reference file:line for every finding.`;
|
|
|
|
subagentPrompt = `Review the frontend source code in this repo. You are an independent senior product designer doing a source-code design audit. Focus on CONSISTENCY PATTERNS across files rather than individual violations:
|
|
- Are spacing values systematic across the codebase?
|
|
- Is there ONE color system or scattered approaches?
|
|
- Do responsive breakpoints follow a consistent set?
|
|
- Is the accessibility approach consistent or spotty?
|
|
|
|
For each finding: what's wrong, severity (critical/high/medium), and the file:line.`;
|
|
} else if (isDesignConsultation) {
|
|
codexPrompt = `Given this product context, propose a complete design direction:
|
|
- Visual thesis: one sentence describing mood, material, and energy
|
|
- Typography: specific font names (not defaults — no Inter/Roboto/Arial/system) + hex colors
|
|
- Color system: CSS variables for background, surface, primary text, muted text, accent
|
|
- Layout: composition-first, not component-first. First viewport as poster, not document
|
|
- Differentiation: 2 deliberate departures from category norms
|
|
- Anti-slop: no purple gradients, no 3-column icon grids, no centered everything, no decorative blobs
|
|
|
|
Be opinionated. Be specific. Do not hedge. This is YOUR design direction — own it.`;
|
|
|
|
subagentPrompt = `Given this product context, propose a design direction that would SURPRISE. What would the cool indie studio do that the enterprise UI team wouldn't?
|
|
- Propose an aesthetic direction, typography stack (specific font names), color palette (hex values)
|
|
- 2 deliberate departures from category norms
|
|
- What emotional reaction should the user have in the first 3 seconds?
|
|
|
|
Be bold. Be specific. No hedging.`;
|
|
} else {
|
|
// Unknown skill — return empty
|
|
return '';
|
|
}
|
|
|
|
// Build the opt-in section
|
|
const optInSection = isAutomatic ? `
|
|
**Automatic:** Outside voices run automatically when Codex is available. No opt-in needed.` : `
|
|
Use AskUserQuestion:
|
|
> "Want outside design voices${isPlanDesignReview ? ' before the detailed review' : ''}? Codex evaluates against OpenAI's design hard rules + litmus checks; Claude subagent does an independent ${isDesignConsultation ? 'design direction proposal' : 'completeness review'}."
|
|
>
|
|
> A) Yes — run outside design voices
|
|
> B) No — proceed without
|
|
|
|
If user chooses B, skip this step and continue.`;
|
|
|
|
// Build the synthesis section
|
|
const synthesisSection = isPlanDesignReview ? `
|
|
**Synthesis — Litmus scorecard:**
|
|
|
|
\`\`\`
|
|
DESIGN OUTSIDE VOICES — LITMUS SCORECARD:
|
|
═══════════════════════════════════════════════════════════════
|
|
Check Claude Codex Consensus
|
|
─────────────────────────────────────── ─────── ─────── ─────────
|
|
1. Brand unmistakable in first screen? — — —
|
|
2. One strong visual anchor? — — —
|
|
3. Scannable by headlines only? — — —
|
|
4. Each section has one job? — — —
|
|
5. Cards actually necessary? — — —
|
|
6. Motion improves hierarchy? — — —
|
|
7. Premium without decorative shadows? — — —
|
|
─────────────────────────────────────── ─────── ─────── ─────────
|
|
Hard rejections triggered: — — —
|
|
═══════════════════════════════════════════════════════════════
|
|
\`\`\`
|
|
|
|
Fill in each cell from the Codex and subagent outputs. CONFIRMED = both agree. DISAGREE = models differ. NOT SPEC'D = not enough info to evaluate.
|
|
|
|
**Pass integration (respects existing 7-pass contract):**
|
|
- Hard rejections → raised as the FIRST items in Pass 1, tagged \`[HARD REJECTION]\`
|
|
- Litmus DISAGREE items → raised in the relevant pass with both perspectives
|
|
- Litmus CONFIRMED failures → pre-loaded as known issues in the relevant pass
|
|
- Passes can skip discovery and go straight to fixing for pre-identified issues` :
|
|
isDesignConsultation ? `
|
|
**Synthesis:** Claude main references both Codex and subagent proposals in the Phase 3 proposal. Present:
|
|
- Areas of agreement between all three voices (Claude main + Codex + subagent)
|
|
- Genuine divergences as creative alternatives for the user to choose from
|
|
- "Codex and I agree on X. Codex suggested Y where I'm proposing Z — here's why..."` : `
|
|
**Synthesis — Litmus scorecard:**
|
|
|
|
Use the same scorecard format as /plan-design-review (shown above). Fill in from both outputs.
|
|
Merge findings into the triage with \`[codex]\` / \`[subagent]\` / \`[cross-model]\` tags.`;
|
|
|
|
const escapedCodexPrompt = codexPrompt.replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
|
|
return `## Design Outside Voices (parallel)
|
|
${optInSection}
|
|
|
|
**Check Codex availability:**
|
|
\`\`\`bash
|
|
command -v codex >/dev/null 2>&1 && echo "CODEX_AVAILABLE" || echo "CODEX_NOT_AVAILABLE"
|
|
\`\`\`
|
|
|
|
**If Codex is available**, launch both voices simultaneously:
|
|
|
|
1. **Codex design voice** (via Bash):
|
|
\`\`\`bash
|
|
TMPERR_DESIGN=$(mktemp /tmp/codex-design-XXXXXXXX)
|
|
_REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; }
|
|
codex exec "${escapedCodexPrompt}" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="${reasoningEffort}"' --enable web_search_cached < /dev/null 2>"$TMPERR_DESIGN"
|
|
\`\`\`
|
|
Use a 5-minute timeout (\`timeout: 300000\`). After the command completes, read stderr:
|
|
\`\`\`bash
|
|
cat "$TMPERR_DESIGN" && rm -f "$TMPERR_DESIGN"
|
|
\`\`\`
|
|
|
|
2. **Claude design subagent** (via Agent tool):
|
|
Dispatch a subagent with this prompt:
|
|
"${subagentPrompt}"
|
|
|
|
**Error handling (all non-blocking):**
|
|
- **Auth failure:** If stderr contains "auth", "login", "unauthorized", or "API key": "Codex authentication failed. Run \`codex login\` to authenticate."
|
|
- **Timeout:** "Codex timed out after 5 minutes."
|
|
- **Empty response:** "Codex returned no response."
|
|
- On any Codex error: proceed with Claude subagent output only, tagged \`[single-model]\`.
|
|
- If Claude subagent also fails: "Outside voices unavailable — continuing with primary review."
|
|
|
|
Present Codex output under a \`CODEX SAYS (design ${isPlanDesignReview ? 'critique' : isDesignReview ? 'source audit' : 'direction'}):\` header.
|
|
Present subagent output under a \`CLAUDE SUBAGENT (design ${isPlanDesignReview ? 'completeness' : isDesignReview ? 'consistency' : 'direction'}):\` header.
|
|
${synthesisSection}
|
|
|
|
**Log the result:**
|
|
\`\`\`bash
|
|
${ctx.paths.binDir}/gstack-review-log '{"skill":"design-outside-voices","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","status":"STATUS","source":"SOURCE","commit":"'"$(git rev-parse --short HEAD)"'"}'
|
|
\`\`\`
|
|
Replace STATUS with "clean" or "issues_found", SOURCE with "codex+subagent", "codex-only", "subagent-only", or "unavailable".`;
|
|
}
|
|
|
|
// ─── Design Hard Rules (OpenAI framework + gstack slop blacklist) ───
|
|
export function generateDesignHardRules(_ctx: TemplateContext): string {
|
|
const slopItems = AI_SLOP_BLACKLIST.map((item, i) => `${i + 1}. ${item}`).join('\n');
|
|
const rejectionItems = OPENAI_HARD_REJECTIONS.map((item, i) => `${i + 1}. ${item}`).join('\n');
|
|
const litmusItems = OPENAI_LITMUS_CHECKS.map((item, i) => `${i + 1}. ${item}`).join('\n');
|
|
|
|
return `### Design Hard Rules
|
|
|
|
**Classifier — determine rule set before evaluating:**
|
|
- **MARKETING/LANDING PAGE** (hero-driven, brand-forward, conversion-focused) → apply Landing Page Rules
|
|
- **APP UI** (workspace-driven, data-dense, task-focused: dashboards, admin, settings) → apply App UI Rules
|
|
- **HYBRID** (marketing shell with app-like sections) → apply Landing Page Rules to hero/marketing sections, App UI Rules to functional sections
|
|
|
|
**Hard rejection criteria** (instant-fail patterns — flag if ANY apply):
|
|
${rejectionItems}
|
|
|
|
**Litmus checks** (answer YES/NO for each — used for cross-model consensus scoring):
|
|
${litmusItems}
|
|
|
|
**Landing page rules** (apply when classifier = MARKETING/LANDING):
|
|
- First viewport reads as one composition, not a dashboard
|
|
- Brand-first hierarchy: brand > headline > body > CTA
|
|
- Typography: expressive, purposeful — no default stacks (Inter, Roboto, Arial, system)
|
|
- No flat single-color backgrounds — use gradients, images, subtle patterns
|
|
- Hero: full-bleed, edge-to-edge, no inset/tiled/rounded variants
|
|
- Hero budget: brand, one headline, one supporting sentence, one CTA group, one image
|
|
- No cards in hero. Cards only when card IS the interaction
|
|
- One job per section: one purpose, one headline, one short supporting sentence
|
|
- Motion: 2-3 intentional motions minimum (entrance, scroll-linked, hover/reveal)
|
|
- Color: define CSS variables, avoid purple-on-white defaults, one accent color default
|
|
- Copy: product language not design commentary. "If deleting 30% improves it, keep deleting"
|
|
- Beautiful defaults: composition-first, brand as loudest text, two typefaces max, cardless by default, first viewport as poster not document
|
|
|
|
**App UI rules** (apply when classifier = APP UI):
|
|
- Calm surface hierarchy, strong typography, few colors
|
|
- Dense but readable, minimal chrome
|
|
- Organize: primary workspace, navigation, secondary context, one accent
|
|
- Avoid: dashboard-card mosaics, thick borders, decorative gradients, ornamental icons
|
|
- Copy: utility language — orientation, status, action. Not mood/brand/aspiration
|
|
- Cards only when card IS the interaction
|
|
- Section headings state what area is or what user can do ("Selected KPIs", "Plan status")
|
|
|
|
**Universal rules** (apply to ALL types):
|
|
- Define CSS variables for color system
|
|
- No default font stacks (Inter, Roboto, Arial, system)
|
|
- One job per section
|
|
- "If deleting 30% of the copy improves it, keep deleting"
|
|
- Cards earn their existence — no decorative card grids
|
|
- NEVER use small, low-contrast type (body text < 16px or contrast ratio < 4.5:1 on body text)
|
|
- NEVER put labels inside form fields as the only label (placeholder-as-label pattern — labels must be visible when the field has content)
|
|
- ALWAYS preserve visited vs unvisited link distinction (visited links must have a different color)
|
|
- NEVER float headings between paragraphs (heading must be visually closer to the section it introduces than to the preceding section)
|
|
|
|
**AI Slop blacklist** (the 10 patterns that scream "AI-generated"):
|
|
${slopItems}
|
|
|
|
Source: [OpenAI "Designing Delightful Frontends with GPT-5.4"](https://developers.openai.com/blog/designing-delightful-frontends-with-gpt-5-4) (Mar 2026) + gstack design methodology.`;
|
|
}
|
|
|
|
export function generateDesignSetup(ctx: TemplateContext): string {
|
|
return `## DESIGN SETUP (run this check BEFORE any design mockup command)
|
|
|
|
\`\`\`bash
|
|
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
D=""
|
|
[ -n "$_ROOT" ] && [ -x "$_ROOT/${ctx.paths.localSkillRoot}/design/dist/design" ] && D="$_ROOT/${ctx.paths.localSkillRoot}/design/dist/design"
|
|
[ -z "$D" ] && D="$HOME${ctx.paths.designDir.replace(/^~/, '')}/design"
|
|
if [ -x "$D" ]; then
|
|
echo "DESIGN_READY: $D"
|
|
else
|
|
echo "DESIGN_NOT_AVAILABLE"
|
|
fi
|
|
B=""
|
|
[ -n "$_ROOT" ] && [ -x "$_ROOT/${ctx.paths.localSkillRoot}/browse/dist/browse" ] && B="$_ROOT/${ctx.paths.localSkillRoot}/browse/dist/browse"
|
|
[ -z "$B" ] && B="$HOME${ctx.paths.browseDir.replace(/^~/, '')}/browse"
|
|
if [ -x "$B" ]; then
|
|
echo "BROWSE_READY: $B"
|
|
else
|
|
echo "BROWSE_NOT_AVAILABLE (will use 'open' to view comparison boards)"
|
|
fi
|
|
\`\`\`
|
|
|
|
If \`DESIGN_NOT_AVAILABLE\`: skip visual mockup generation and fall back to the
|
|
existing HTML wireframe approach (\`DESIGN_SKETCH\`). Design mockups are a
|
|
progressive enhancement, not a hard requirement.
|
|
|
|
If \`BROWSE_NOT_AVAILABLE\`: use \`open file://...\` instead of \`$B goto\` to open
|
|
comparison boards. The user just needs to see the HTML file in any browser.
|
|
|
|
If \`DESIGN_READY\`: the design binary is available for visual mockup generation.
|
|
Commands:
|
|
- \`$D generate --brief "..." --output /path.png\` — generate a single mockup
|
|
- \`$D variants --brief "..." --count 3 --output-dir /path/\` — generate N style variants
|
|
- \`$D compare --images "a.png,b.png,c.png" --output /path/board.html --serve\` — comparison board + HTTP server
|
|
- \`$D serve --html /path/board.html\` — serve comparison board and collect feedback via HTTP
|
|
- \`$D check --image /path.png --brief "..."\` — vision quality gate
|
|
- \`$D iterate --session /path/session.json --feedback "..." --output /path.png\` — iterate
|
|
|
|
**CRITICAL PATH RULE:** All design artifacts (mockups, comparison boards, approved.json)
|
|
MUST be saved to \`~/.gstack/projects/$SLUG/designs/\`, NEVER to \`.context/\`,
|
|
\`docs/designs/\`, \`/tmp/\`, or any project-local directory. Design artifacts are USER
|
|
data, not project files. They persist across branches, conversations, and workspaces.`;
|
|
}
|
|
|
|
export function generateDesignMockup(ctx: TemplateContext): string {
|
|
return `## Visual Design Exploration
|
|
|
|
\`\`\`bash
|
|
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
D=""
|
|
[ -n "$_ROOT" ] && [ -x "$_ROOT/${ctx.paths.localSkillRoot}/design/dist/design" ] && D="$_ROOT/${ctx.paths.localSkillRoot}/design/dist/design"
|
|
[ -z "$D" ] && D="$HOME${ctx.paths.designDir.replace(/^~/, '')}/design"
|
|
[ -x "$D" ] && echo "DESIGN_READY" || echo "DESIGN_NOT_AVAILABLE"
|
|
\`\`\`
|
|
|
|
**If \`DESIGN_NOT_AVAILABLE\`:** Fall back to the HTML wireframe approach below
|
|
(the existing DESIGN_SKETCH section). Visual mockups require the design binary.
|
|
|
|
**If \`DESIGN_READY\`:** Generate visual mockup explorations for the user.
|
|
|
|
Generating visual mockups of the proposed design... (say "skip" if you don't need visuals)
|
|
|
|
**Step 1: Set up the design directory**
|
|
|
|
\`\`\`bash
|
|
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
|
|
_DESIGN_DIR="$HOME/.gstack/projects/$SLUG/designs/mockup-$(date +%Y%m%d)"
|
|
mkdir -p "$_DESIGN_DIR"
|
|
echo "DESIGN_DIR: $_DESIGN_DIR"
|
|
\`\`\`
|
|
|
|
**Step 2: Construct the design brief**
|
|
|
|
Read DESIGN.md if it exists — use it to constrain the visual style. If no DESIGN.md,
|
|
explore wide across diverse directions.
|
|
|
|
**Step 3: Generate 3 variants**
|
|
|
|
\`\`\`bash
|
|
$D variants --brief "<assembled brief>" --count 3 --output-dir "$_DESIGN_DIR/"
|
|
\`\`\`
|
|
|
|
This generates 3 style variations of the same brief (~40 seconds total).
|
|
|
|
**Step 4: Show variants inline, then open comparison board**
|
|
|
|
Show each variant to the user inline first (read the PNGs with Read tool), then
|
|
create and serve the comparison board:
|
|
|
|
\`\`\`bash
|
|
$D compare --images "$_DESIGN_DIR/variant-A.png,$_DESIGN_DIR/variant-B.png,$_DESIGN_DIR/variant-C.png" --output "$_DESIGN_DIR/design-board.html" --serve
|
|
\`\`\`
|
|
|
|
This opens the board in the user's default browser and blocks until feedback is
|
|
received. Read stdout for the structured JSON result. No polling needed.
|
|
|
|
If \`$D serve\` is not available or fails, fall back to AskUserQuestion:
|
|
"I've opened the design board. Which variant do you prefer? Any feedback?"
|
|
|
|
**Step 5: Handle feedback**
|
|
|
|
If the JSON contains \`"regenerated": true\`:
|
|
1. Read \`regenerateAction\` (or \`remixSpec\` for remix requests)
|
|
2. Generate new variants with \`$D iterate\` or \`$D variants\` using updated brief
|
|
3. Create new board with \`$D compare\`
|
|
4. POST the new HTML to the running server via \`curl -X POST http://localhost:PORT/api/reload -H 'Content-Type: application/json' -d '{"html":"$_DESIGN_DIR/design-board.html"}'\`
|
|
(parse the port from stderr: look for \`SERVE_STARTED: port=XXXXX\`)
|
|
5. Board auto-refreshes in the same tab
|
|
|
|
If \`"regenerated": false\`: proceed with the approved variant.
|
|
|
|
**Step 6: Save approved choice**
|
|
|
|
\`\`\`bash
|
|
echo '{"approved_variant":"<VARIANT>","feedback":"<FEEDBACK>","date":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","screen":"mockup","branch":"'$(git branch --show-current 2>/dev/null)'"}' > "$_DESIGN_DIR/approved.json"
|
|
\`\`\`
|
|
|
|
Reference the saved mockup in the design doc or plan.`;
|
|
}
|
|
|
|
export function generateDesignShotgunLoop(_ctx: TemplateContext): string {
|
|
return `### Comparison Board + Feedback Loop
|
|
|
|
Create the comparison board and serve it over HTTP:
|
|
|
|
\`\`\`bash
|
|
$D compare --images "$_DESIGN_DIR/variant-A.png,$_DESIGN_DIR/variant-B.png,$_DESIGN_DIR/variant-C.png" --output "$_DESIGN_DIR/design-board.html" --serve
|
|
\`\`\`
|
|
|
|
This command generates the board HTML, starts an HTTP server on a random port,
|
|
and opens it in the user's default browser. **Run it in the background** with \`&\`
|
|
because the server needs to stay running while the user interacts with the board.
|
|
|
|
Parse the port from stderr output: \`SERVE_STARTED: port=XXXXX\`. You need this
|
|
for the board URL and for reloading during regeneration cycles.
|
|
|
|
**PRIMARY WAIT: AskUserQuestion with board URL**
|
|
|
|
After the board is serving, use AskUserQuestion to wait for the user. Include the
|
|
board URL so they can click it if they lost the browser tab:
|
|
|
|
"I've opened a comparison board with the design variants:
|
|
http://127.0.0.1:<PORT>/ — Rate them, leave comments, remix
|
|
elements you like, and click Submit when you're done. Let me know when you've
|
|
submitted your feedback (or paste your preferences here). If you clicked
|
|
Regenerate or Remix on the board, tell me and I'll generate new variants."
|
|
|
|
**Do NOT use AskUserQuestion to ask which variant the user prefers.** The comparison
|
|
board IS the chooser. AskUserQuestion is just the blocking wait mechanism.
|
|
|
|
**After the user responds to AskUserQuestion:**
|
|
|
|
Check for feedback files next to the board HTML:
|
|
- \`$_DESIGN_DIR/feedback.json\` — written when user clicks Submit (final choice)
|
|
- \`$_DESIGN_DIR/feedback-pending.json\` — written when user clicks Regenerate/Remix/More Like This
|
|
|
|
\`\`\`bash
|
|
if [ -f "$_DESIGN_DIR/feedback.json" ]; then
|
|
echo "SUBMIT_RECEIVED"
|
|
cat "$_DESIGN_DIR/feedback.json"
|
|
elif [ -f "$_DESIGN_DIR/feedback-pending.json" ]; then
|
|
echo "REGENERATE_RECEIVED"
|
|
cat "$_DESIGN_DIR/feedback-pending.json"
|
|
rm "$_DESIGN_DIR/feedback-pending.json"
|
|
else
|
|
echo "NO_FEEDBACK_FILE"
|
|
fi
|
|
\`\`\`
|
|
|
|
The feedback JSON has this shape:
|
|
\`\`\`json
|
|
{
|
|
"preferred": "A",
|
|
"ratings": { "A": 4, "B": 3, "C": 2 },
|
|
"comments": { "A": "Love the spacing" },
|
|
"overall": "Go with A, bigger CTA",
|
|
"regenerated": false
|
|
}
|
|
\`\`\`
|
|
|
|
**If \`feedback.json\` found:** The user clicked Submit on the board.
|
|
Read \`preferred\`, \`ratings\`, \`comments\`, \`overall\` from the JSON. Proceed with
|
|
the approved variant.
|
|
|
|
**If \`feedback-pending.json\` found:** The user clicked Regenerate/Remix on the board.
|
|
1. Read \`regenerateAction\` from the JSON (\`"different"\`, \`"match"\`, \`"more_like_B"\`,
|
|
\`"remix"\`, or custom text)
|
|
2. If \`regenerateAction\` is \`"remix"\`, read \`remixSpec\` (e.g. \`{"layout":"A","colors":"B"}\`)
|
|
3. Generate new variants with \`$D iterate\` or \`$D variants\` using updated brief
|
|
4. Create new board: \`$D compare --images "..." --output "$_DESIGN_DIR/design-board.html"\`
|
|
5. Reload the board in the user's browser (same tab):
|
|
\`curl -s -X POST http://127.0.0.1:PORT/api/reload -H 'Content-Type: application/json' -d '{"html":"$_DESIGN_DIR/design-board.html"}'\`
|
|
6. The board auto-refreshes. **AskUserQuestion again** with the same board URL to
|
|
wait for the next round of feedback. Repeat until \`feedback.json\` appears.
|
|
|
|
**If \`NO_FEEDBACK_FILE\`:** The user typed their preferences directly in the
|
|
AskUserQuestion response instead of using the board. Use their text response
|
|
as the feedback.
|
|
|
|
**POLLING FALLBACK:** Only use polling if \`$D serve\` fails (no port available).
|
|
In that case, show each variant inline using the Read tool (so the user can see them),
|
|
then use AskUserQuestion:
|
|
"The comparison board server failed to start. I've shown the variants above.
|
|
Which do you prefer? Any feedback?"
|
|
|
|
**After receiving feedback (any path):** Output a clear summary confirming
|
|
what was understood:
|
|
|
|
"Here's what I understood from your feedback:
|
|
PREFERRED: Variant [X]
|
|
RATINGS: [list]
|
|
YOUR NOTES: [comments]
|
|
DIRECTION: [overall]
|
|
|
|
Is this right?"
|
|
|
|
Use AskUserQuestion to verify before proceeding.
|
|
|
|
**Save the approved choice:**
|
|
\`\`\`bash
|
|
echo '{"approved_variant":"<V>","feedback":"<FB>","date":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","screen":"<SCREEN>","branch":"'$(git branch --show-current 2>/dev/null)'"}' > "$_DESIGN_DIR/approved.json"
|
|
\`\`\``;
|
|
}
|
|
|
|
export function generateTasteProfile(ctx: TemplateContext): string {
|
|
return `Read the persistent taste profile if it exists:
|
|
|
|
\`\`\`bash
|
|
_TASTE_PROFILE=~/.gstack/projects/$SLUG/taste-profile.json
|
|
if [ -f "$_TASTE_PROFILE" ]; then
|
|
# Schema v1: { dimensions: { fonts, colors, layouts, aesthetics }, sessions: [] }
|
|
# Each dimension has approved[] and rejected[] entries with
|
|
# { value, confidence, approved_count, rejected_count, last_seen }
|
|
# Confidence decays 5% per week of inactivity — computed at read time.
|
|
cat "$_TASTE_PROFILE" 2>/dev/null | head -200
|
|
echo "TASTE_PROFILE_FOUND"
|
|
else
|
|
echo "NO_TASTE_PROFILE"
|
|
fi
|
|
\`\`\`
|
|
|
|
**If TASTE_PROFILE_FOUND:** Summarize the strongest signals (top 3 approved entries
|
|
per dimension by confidence * approved_count). Include them in the design brief:
|
|
|
|
"Based on ${'\\${SESSION_COUNT}'} prior sessions, this user's taste leans toward:
|
|
fonts [top-3], colors [top-3], layouts [top-3], aesthetics [top-3]. Bias
|
|
generation toward these unless the user explicitly requests a different direction.
|
|
Also avoid their strong rejections: [top-3 rejected per dimension]."
|
|
|
|
**If NO_TASTE_PROFILE:** Fall through to per-session approved.json files (legacy).
|
|
|
|
**Conflict handling:** If the current user request contradicts a strong persistent
|
|
signal (e.g., "make it playful" when taste profile strongly prefers minimal), flag
|
|
it: "Note: your taste profile strongly prefers minimal. You're asking for playful
|
|
this time — I'll proceed, but want me to update the taste profile, or treat this
|
|
as a one-off?"
|
|
|
|
**Decay:** Confidence scores decay 5% per week. A font approved 6 months ago with
|
|
10 approvals has less weight than one approved last week. The decay calculation
|
|
happens at read time, not write time, so the file only grows on change.
|
|
|
|
**Schema migration:** If the file has no \`version\` field or \`version: 0\`, it's
|
|
the legacy approved.json aggregate — \`${ctx.paths.binDir}/gstack-taste-update\`
|
|
will migrate it to schema v1 on the next write.`;
|
|
}
|
|
|
|
// ─── UX Behavioral Foundations (Krug + HCI research) ───
|
|
export function generateUXPrinciples(_ctx: TemplateContext): string {
|
|
return `## UX Principles: How Users Actually Behave
|
|
|
|
These principles govern how real humans interact with interfaces. They are observed
|
|
behavior, not preferences. Apply them before, during, and after every design decision.
|
|
|
|
### The Three Laws of Usability
|
|
|
|
1. **Don't make me think.** Every page should be self-evident. If a user stops
|
|
to think "What do I click?" or "What does this mean?", the design has failed.
|
|
Self-evident > self-explanatory > requires explanation.
|
|
|
|
2. **Clicks don't matter, thinking does.** Three mindless, unambiguous clicks
|
|
beat one click that requires thought. Each step should feel like an obvious
|
|
choice (animal, vegetable, or mineral), not a puzzle.
|
|
|
|
3. **Omit, then omit again.** Get rid of half the words on each page, then get
|
|
rid of half of what's left. Happy talk (self-congratulatory text) must die.
|
|
Instructions must die. If they need reading, the design has failed.
|
|
|
|
### How Users Actually Behave
|
|
|
|
- **Users scan, they don't read.** Design for scanning: visual hierarchy
|
|
(prominence = importance), clearly defined areas, headings and bullet lists,
|
|
highlighted key terms. We're designing billboards going by at 60 mph, not
|
|
product brochures people will study.
|
|
- **Users satisfice.** They pick the first reasonable option, not the best.
|
|
Make the right choice the most visible choice.
|
|
- **Users muddle through.** They don't figure out how things work. They wing
|
|
it. If they accomplish their goal by accident, they won't seek the "right" way.
|
|
Once they find something that works, no matter how badly, they stick to it.
|
|
- **Users don't read instructions.** They dive in. Guidance must be brief,
|
|
timely, and unavoidable, or it won't be seen.
|
|
|
|
### Billboard Design for Interfaces
|
|
|
|
- **Use conventions.** Logo top-left, nav top/left, search = magnifying glass.
|
|
Don't innovate on navigation to be clever. Innovate when you KNOW you have a
|
|
better idea, otherwise use conventions. Even across languages and cultures,
|
|
web conventions let people identify the logo, nav, search, and main content.
|
|
- **Visual hierarchy is everything.** Related things are visually grouped. Nested
|
|
things are visually contained. More important = more prominent. If everything
|
|
shouts, nothing is heard. Start with the assumption everything is visual noise,
|
|
guilty until proven innocent.
|
|
- **Make clickable things obviously clickable.** No relying on hover states for
|
|
discoverability, especially on mobile where hover doesn't exist. Shape, location,
|
|
and formatting (color, underlining) must signal clickability without interaction.
|
|
- **Eliminate noise.** Three sources: too many things shouting for attention
|
|
(shouting), things not organized logically (disorganization), and too much stuff
|
|
(clutter). Fix noise by removal, not addition.
|
|
- **Clarity trumps consistency.** If making something significantly clearer
|
|
requires making it slightly inconsistent, choose clarity every time.
|
|
|
|
### Navigation as Wayfinding
|
|
|
|
Users on the web have no sense of scale, direction, or location. Navigation
|
|
must always answer: What site is this? What page am I on? What are the major
|
|
sections? What are my options at this level? Where am I? How can I search?
|
|
|
|
Persistent navigation on every page. Breadcrumbs for deep hierarchies.
|
|
Current section visually indicated. The "trunk test": cover everything except
|
|
the navigation. You should still know what site this is, what page you're on,
|
|
and what the major sections are. If not, the navigation has failed.
|
|
|
|
### The Goodwill Reservoir
|
|
|
|
Users start with a reservoir of goodwill. Every friction point depletes it.
|
|
|
|
**Deplete faster:** Hiding info users want (pricing, contact, shipping). Punishing
|
|
users for not doing things your way (formatting requirements on phone numbers).
|
|
Asking for unnecessary information. Putting sizzle in their way (splash screens,
|
|
forced tours, interstitials). Unprofessional or sloppy appearance.
|
|
|
|
**Replenish:** Know what users want to do and make it obvious. Tell them what they
|
|
want to know upfront. Save them steps wherever possible. Make it easy to recover
|
|
from errors. When in doubt, apologize.
|
|
|
|
### Mobile: Same Rules, Higher Stakes
|
|
|
|
All the above applies on mobile, just more so. Real estate is scarce, but never
|
|
sacrifice usability for space savings. Affordances must be VISIBLE: no cursor
|
|
means no hover-to-discover. Touch targets must be big enough (44px minimum).
|
|
Flat design can strip away useful visual information that signals interactivity.
|
|
Prioritize ruthlessly: things needed in a hurry go close at hand, everything
|
|
else a few taps away with an obvious path to get there.`;
|
|
}
|
|
|