Files
gstack/scripts/resolvers/constants.ts
T
Garry TanandClaude Fable 5.1 e34541c3bd refactor(design): typed slop catalog in lib/; AI_SLOP_BLACKLIST derived
lib/design-catalog.ts is the single source of truth for gstack's design
anti-pattern vocabulary: the 11 legacy blacklist lines (verbatim, flagged
`legacyBlacklist`), every one of impeccable's 61 registry ids with gstack
prose, tier, impact, confidence, grep heuristic, and /impeccable handoff,
plus the gstack-only tells the LLM pass judges (hero metrics, identical
cards, glassmorphism, missing states, unthemed browser surfaces, ...).

`impeccableId` is set only when the id exists in the registry fixture, and
`renderCatalog({style:'ids'})` brackets an id only then, so rendered prose
never shows an id the detector cannot emit. Role-scoped font lists
(OVERUSED_FONTS_DISPLAY, BANNED_FONTS, FONTS_BODY_UI_OK, FONTS_MONO_OK,
FONTS_VERIFIED_FREE) live beside the entries.

scripts/resolvers/constants.ts now derives AI_SLOP_BLACKLIST from the
catalog. Generated output is byte-identical (bun run gen:skill-docs is a
zero diff). Pure module: no I/O, no scripts/ imports, loading prints
nothing, so bin/ can import it at runtime on every host.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 15:30:29 +00:00

163 lines
11 KiB
TypeScript

// ─── Shared Design Constants ────────────────────────────────
import { DESIGN_SLOP_CATALOG } from '../../lib/design-catalog';
/**
* gstack's AI slop anti-patterns — shared between DESIGN_METHODOLOGY and DESIGN_HARD_RULES.
*
* Derived from the typed catalog in lib/design-catalog.ts: the 11 entries flagged
* `legacyBlacklist`, prose verbatim, in catalog order. Overused fonts live there
* too (OVERUSED_FONTS_DISPLAY), role-scoped: banned as the display voice, several
* still fine as body/UI on an Operate or Read surface.
*/
export const AI_SLOP_BLACKLIST: string[] = DESIGN_SLOP_CATALOG
.filter(e => e.legacyBlacklist)
.map(e => e.prose);
/** OpenAI hard rejection criteria (from "Designing Delightful Frontends with GPT-5.4", Mar 2026) */
export const OPENAI_HARD_REJECTIONS = [
'Generic SaaS card grid as first impression',
'Beautiful image with weak brand',
'Strong headline with no clear action',
'Busy imagery behind text',
'Sections repeating same mood statement',
'Carousel with no narrative purpose',
'App UI made of stacked cards instead of layout',
];
/** OpenAI litmus checks — 7 yes/no tests for cross-model consensus scoring */
export const OPENAI_LITMUS_CHECKS = [
'Brand/product unmistakable in first screen?',
'One strong visual anchor present?',
'Page understandable by scanning headlines only?',
'Each section has one job?',
'Are cards actually necessary?',
'Does motion improve hierarchy or atmosphere?',
'Would design feel premium with all decorative shadows removed?',
];
/**
* Web-search flag for every codex invocation (#2525).
*
* codex >=0.144 deprecated the legacy `--enable`-based web_search_cached
* spelling (web search is on by default; the deprecation notice says to set
* `web_search` to "live", "indexed", "cached", or "disabled" at the top
* level), and `--enable <FEATURE>` now means `-c features.<name>=true`
* (verified on 0.147.0), so the legacy spelling is headed for hard
* rejection. This is the ONE source
* of truth: resolvers interpolate it directly and templates reference it via
* the {{CODEX_WEB_SEARCH_FLAG}} token — never write the flag inline.
*
* Semantics note: unlike the legacy flag (which yielded to an existing
* top-level `web_search` in config.toml), the -c form explicitly overrides
* it. Deliberate: gstack wants deterministic cached search for review
* invocations. Native `codex review` disables web search regardless of
* configuration, so on that path the flag is a harmless no-op.
*/
export const CODEX_WEB_SEARCH_FLAG = `-c 'web_search="cached"'`;
/**
* Shared Codex error handling block for resolver output.
* Used by ADVERSARIAL_STEP, CODEX_PLAN_REVIEW, CODEX_SECOND_OPINION,
* DESIGN_OUTSIDE_VOICES, DESIGN_REVIEW_LITE, DESIGN_SKETCH.
*/
export function codexErrorHandling(feature: string): string {
return `**Error handling:** All errors are non-blocking — the ${feature} is informational.
- Auth failure (stderr contains "auth", "login", "unauthorized"): note and skip
- Timeout: note timeout duration and skip
- Empty response: note and skip
On any error: continue — ${feature} is informational, not a gate.`;
}
/**
* Shared Codex preflight bash block — the single source of truth for deciding
* whether a Codex review pass should run. Used by ADVERSARIAL_STEP,
* CODEX_PLAN_REVIEW, and CODEX_DOC_REVIEW so install/auth/config detection
* lives in exactly one place.
*
* Emits ONE self-contained bash block (the caller must place it in a single
* fenced block — CLAUDE.md: each block is a fresh shell, so functions sourced
* here do NOT persist to later blocks). It:
* 1. reads the `codex_reviews` master switch,
* 2. sources `gstack-codex-probe`,
* 3. runs `command -v codex` (literal — keeps the e2e substring assertion),
* then `_gstack_codex_auth_probe`, then `_gstack_codex_version_check`,
* 4. logs the relevant `_gstack_codex_log_event` for each non-ready outcome,
* 5. sets ONE canonical mode var and echoes `CODEX_MODE: <mode>` so the agent
* gates later blocks on the echoed value.
*
* Mode values: `disabled` (config off) | `not_installed` | `not_authed` | `ready`.
* The path is host-rewritten at gen-skill-docs time (pathRewrites), so the
* literal `~/.claude/skills/gstack` is correct here and becomes `$GSTACK_ROOT`
* etc. for non-Claude hosts.
*
* `disabledBehavior` controls the `disabled`-mode interpretation, which is the
* one branch that legitimately differs per caller (D1):
* - `skip-all` (plan / doc reviews): disabled means no extra review step at
* all — skip the section, no Claude fallback.
* - `codex-only` (diff adversarial): disabled gates only the Codex passes; the
* free Claude adversarial subagent still runs.
*/
export function codexPreflight(opts: { modeVar?: string; disabledBehavior: 'skip-all' | 'codex-only' }): string {
const m = opts.modeVar ?? '_CODEX_MODE';
const disabledLine = opts.disabledBehavior === 'codex-only'
? 'Skip the Codex passes only; the Claude adversarial subagent below STILL runs (it is free and fast). Print: "Codex passes skipped (codex_reviews disabled) — running Claude adversarial only."'
: 'Skip this section entirely; do NOT fall back to a Claude subagent — disabled means no extra review step. Print: "Codex review skipped (codex_reviews disabled). Re-enable: `gstack-config set codex_reviews enabled`."';
return `\`\`\`bash
# Codex preflight: one block (functions sourced here don't persist to later blocks).
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || echo off)
_CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get codex_reviews 2>/dev/null || echo enabled)
source ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null || true
if [ "$_CODEX_CFG" = "disabled" ]; then
${m}="disabled"
# Running-under-Codex presence probe (#2519): a live Codex session exports
# CODEX_THREAD_ID / CODEX_SANDBOX into every shell it spawns (verified
# against a live \`codex exec 'env | grep -i codex'\` capture, codex 0.147.0).
# Nested codex spawns from inside a Codex host multiply token burn
# (observed: one /review = 15M tokens). GSTACK_FORCE_CODEX_REVIEW=1 forces
# the nested passes anyway.
elif [ "\${GSTACK_FORCE_CODEX_REVIEW:-0}" != "1" ] && { [ -n "\${CODEX_THREAD_ID:-}" ] || [ -n "\${CODEX_SANDBOX:-}" ]; }; then
${m}="under_codex"
elif ! command -v codex >/dev/null 2>&1; then
${m}="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true
elif ! _gstack_codex_auth_probe >/dev/null 2>&1; then
${m}="not_authed"; _gstack_codex_log_event "codex_auth_failed" 2>/dev/null || true
else
# Capture the probe's code: 2 means the CLI cannot execute at all, which is a
# different problem (and a different fix) from a model the account can't use.
_gstack_codex_model_probe; _CODEX_MP=$?
if [ "$_CODEX_MP" -eq 2 ]; then
${m}="broken_install"
elif [ "$_CODEX_MP" -ne 0 ]; then
${m}="model_unusable"
else
${m}="ready"; _gstack_codex_version_check 2>/dev/null || true
fi
fi
echo "CODEX_MODE: $${m}"
\`\`\`
Branch on the echoed \`CODEX_MODE\`:
- **\`disabled\`** — the user turned Codex reviews off (\`codex_reviews=disabled\`). ${disabledLine}
- **\`not_installed\`** — Codex CLI absent. Print: "Codex not installed — falling back to a Claude subagent (fresh context, but the SAME model family — not an outside model). Install Codex for an actual outside-model read: \`npm install -g @openai/codex\`." Fall back to the Claude subagent path.
- **\`under_codex\`** — this session is already running INSIDE a Codex host, so spawning codex again is the same model reviewing itself at multiplied token cost (#2519). Print exactly one line: "[running under Codex — nested codex passes skipped; set GSTACK_FORCE_CODEX_REVIEW=1 to force]" and skip the codex invocations below; run the section's free in-host pass instead if it defines one.
- **\`not_authed\`** — installed but no credentials. Print: "Codex installed but not authenticated — falling back to a Claude subagent (same model family, not an outside model). Run \`codex login\` or set \`$CODEX_API_KEY\`." Fall back to the Claude subagent path.
- **\`broken_install\`** — the CLI is on PATH but cannot execute (spawn ENOENT, non-executable binary, missing vendor payload). Print: "Codex is installed but its binary cannot run — Codex passes skipped. Reinstall: \`npm install -g @openai/codex\`." Relay the probe's HINT lines and fall back to the Claude subagent path. This state exists because a missing binary used to land in the model probe's fail-open bucket and report \`ready\`, so every Codex pass was skipped silently (#2742).
- **\`model_unusable\`** — authed but the account cannot use its configured model (#2477: HTTP 400 on every call, usually a stale \`model =\` pin in \`~/.codex/config.toml\`). Relay the probe's HINT lines, tell the user the one-line fix (update the pin; \`[notice.model_migrations]\` names the replacement), and fall back to the Claude subagent path. The ~10s round trip is cached for 1h; timeouts fail open to \`ready\`.
- **\`ready\`** — run the Codex pass below.`;
}
/**
* Canonical foreground-dispatch guidance (#497 → #2440 → third recurrence at
* /ship Step 18). Claude Code v2.1.198 made Agent-tool subagents run in the
* BACKGROUND by default; a synchronous dispatch site must pass the flag
* explicitly or the parent waits on output that never arrives. Rendered via
* {{FOREGROUND_DISPATCH_NOTE}} in section templates; resolver sites may
* interpolate it directly. Same name as the placeholder for grep-ability.
*/
/** The Claude Code release that flipped Agent-tool subagents to background-by-default (#497/#2440 class). Interpolated at every RESOLVER site; three templates carry the literal inline (autoplan/sections/ceo-phase, cso, design-shotgun) — grep 'Claude Code v2.1' when bumping. */
export const CC_BACKGROUND_DEFAULT_SINCE = 'Claude Code v2.1.198';
export const FOREGROUND_DISPATCH_NOTE =
`**Foreground required:** pass \`run_in_background: false\` on the Agent call — subagents run in the BACKGROUND by default since ${CC_BACKGROUND_DEFAULT_SINCE}. (Merely omitting the flag no longer produces a foreground run; it must be explicitly false.) The dispatch happens ONLY via the Agent tool: invoking the target as a Skill, or executing its workflow inline in your own context, is WRONG even though the skill may appear in your available-skills list — inline execution forfeits the fresh-context isolation this dispatch exists for, and the explicit flag already makes the Agent call block. (Where a step defines an inline FALLBACK, it applies only after a dispatched subagent has failed.)`;