constants.ts
242 lines · scripts/resolvers/constants.ts at 3f79d81
1// ─── Shared Design Constants ────────────────────────────────
3import { DESIGN_SLOP_CATALOG } from '../../lib/design-catalog';
5/**
6 * gstack's AI slop anti-patterns — shared between DESIGN_METHODOLOGY and DESIGN_HARD_RULES.
7 *
8 * Derived from the typed catalog in lib/design-catalog.ts: the 11 entries flagged
9 * `legacyBlacklist`, prose verbatim, in catalog order. Overused fonts live there
10 * too (OVERUSED_FONTS_DISPLAY), role-scoped: banned as the display voice, several
11 * still fine as body/UI on an Operate or Read surface.
12 */
13export const AI_SLOP_BLACKLIST: string[] = DESIGN_SLOP_CATALOG
14 .filter(e => e.legacyBlacklist)
15 .map(e => e.prose);
17/** OpenAI hard rejection criteria (from "Designing Delightful Frontends with GPT-5.4", Mar 2026) */
18export const OPENAI_HARD_REJECTIONS = [
19 'Generic SaaS card grid as first impression',
20 'Beautiful image with weak brand',
21 'Strong headline with no clear action',
22 'Busy imagery behind text',
23 'Sections repeating same mood statement',
24 'Carousel with no narrative purpose',
25 'App UI made of stacked cards instead of layout',
26];
28/** OpenAI litmus checks — 7 yes/no tests for cross-model consensus scoring */
29export const OPENAI_LITMUS_CHECKS = [
30 'Brand/product unmistakable in first screen?',
31 'One strong visual anchor present?',
32 'Page understandable by scanning headlines only?',
33 'Each section has one job?',
34 'Are cards actually necessary?',
35 'Does motion improve hierarchy or atmosphere?',
36 'Would design feel premium with all decorative shadows removed?',
37];
39/**
40 * Web-search flag for every codex invocation (#2525).
41 *
42 * codex >=0.144 deprecated the legacy `--enable`-based web_search_cached
43 * spelling (web search is on by default; the deprecation notice says to set
44 * `web_search` to "live", "indexed", "cached", or "disabled" at the top
45 * level), and `--enable <FEATURE>` now means `-c features.<name>=true`
46 * (verified on 0.147.0), so the legacy spelling is headed for hard
47 * rejection. This is the ONE source
48 * of truth: resolvers interpolate it directly and templates reference it via
49 * the {{CODEX_WEB_SEARCH_FLAG}} token — never write the flag inline.
50 *
51 * Semantics note: unlike the legacy flag (which yielded to an existing
52 * top-level `web_search` in config.toml), the -c form explicitly overrides
53 * it. Deliberate: gstack wants deterministic cached search for review
54 * invocations. Native `codex review` disables web search regardless of
55 * configuration, so on that path the flag is a harmless no-op.
56 */
57export const CODEX_WEB_SEARCH_FLAG = `-c 'web_search="cached"'`;
59/**
60 * Historical no-role Codex default and the pinned eval ruler some tests import.
61 * It does not follow the role catalog in lib/model-catalog.ts: plan-review
62 * calls resolve their model through lib/model-policy.ts at invocation time.
63 *
64 * A no-role runtime model is resolved per invocation kind by
65 * `gstack-codex-probe select-model exec|review` (backed by
66 * resolveCodexRuntimeModel in scripts/resolve-codex-generation-model.ts):
67 * explicit request (`--model`), then GSTACK_CODEX_MODEL, then Codex config.toml
68 * (`model`; `review_model` first for native review; honors CODEX_HOME), then this
69 * default (#2914). The selection is printed before the first paid call and the
70 * probe checks the same record the flags below pass.
71 */
72export const CODEX_FRONTIER_MODEL = 'gpt-6-astra';
73/**
74 * Nested gstack Codex calls are one-shot reviews: keep installed skills (gstack's
75 * own included) out of the model's context so the reviewer cannot re-run a whole
76 * skill workflow inside its budget (#2847). Read-only sandboxes do not prevent this.
77 */
78export const CODEX_SKILLS_ISOLATION_FLAG = '-c skills.include_instructions=false';
79/** Captured from `select-model` by codexSelect(); `:?` stops an unselected command. */
80const SELECTED_MODEL = '${_CODEX_SEL:?}';
81/** The sandbox codexSelect() captured; read-only unless GSTACK_CODEX_NO_SANDBOX=1. */
82export const CODEX_SANDBOX_REF = '${_CODEX_SANDBOX_MODE:?}';
83/** Requires codexSelect('exec') earlier in the same block. */
84export const CODEX_MODEL_CONFIG_FLAG = `-c "model=\\"${SELECTED_MODEL}\\"" ${CODEX_SKILLS_ISOLATION_FLAG}`;
85/** Requires codexSelect('review'); native review prefers review_model, so both carry the selection. */
86export const CODEX_REVIEW_MODEL_CONFIG_FLAG = `-c "review_model=\\"${SELECTED_MODEL}\\"" ${CODEX_MODEL_CONFIG_FLAG}`;
87/** The executed probe; pathRewrites map the Claude path per host. */
88export const CODEX_PROBE_PATH = '~/.claude/skills/gstack/bin/gstack-codex-probe';
90/**
91 * Runs `gstack-codex-probe select-model` and captures its KEY: value lines into
92 * `_CODEX_SEL` and `_CODEX_SANDBOX_MODE` for CODEX_MODEL_CONFIG_FLAG and
93 * CODEX_SANDBOX_REF. The probe is executed, never sourced, so no shell state
94 * crosses from the helper and the calling shell does not matter.
95 *
96 * With `role`, `role-ready` supplies the plan-review record instead: one
97 * selection, auth, sandbox and model probe. It and the block's later
98 * `run-with-timeout <budgetSecs>` dispatch share one exported deadline, so
99 * readiness time is charged to the review instead of extending it. `optional`
100 * (manual /codex) keeps select-model unless the agent sets `_CODEX_ROLE`.
101 */
102export function codexSelect(kind: 'exec' | 'review', probe: string = CODEX_PROBE_PATH, role?: { budgetSecs: number; optional?: boolean }): string {
103 const select = `_CODEX_OUT=$("$_CODEX_PROBE" select-model ${kind}) || exit 1`;
104 const ready = `export _CODEX_DEADLINE=$(($(date +%s)+${role?.budgetSecs})); _CODEX_OUT=$("$_CODEX_PROBE" role-ready ${kind}) || exit $?`;
105 return `_CODEX_PROBE=${probe}
106${!role ? select : role.optional ? `_CODEX_ROLE=''\nif [ -n "$_CODEX_ROLE" ]; then ${ready}\nelse ${select}; fi` : ready}
107_CODEX_SEL=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SEL: //p')
108_CODEX_SANDBOX_MODE=$(echo "$_CODEX_OUT" | sed -n 's/^CODEX_SANDBOX: //p')`;
109}
111/**
112 * Shared Codex error handling block for resolver output.
113 * Used by ADVERSARIAL_STEP, CODEX_PLAN_REVIEW, CODEX_SECOND_OPINION,
114 * DESIGN_OUTSIDE_VOICES, DESIGN_REVIEW_LITE, DESIGN_SKETCH.
115 */
116export function codexErrorHandling(feature: string): string {
117 return `**Error handling:** All errors are non-blocking — the ${feature} is informational.
118- Auth failure (stderr contains "auth", "login", "unauthorized"): note and skip
119- Timeout: note timeout duration and skip
120- Empty response: note and skip
121On any error: continue — ${feature} is informational, not a gate.`;
122}
124/**
125 * Shared Codex preflight bash block — the single source of truth for deciding
126 * whether a Codex review pass should run. Used by ADVERSARIAL_STEP,
127 * CODEX_PLAN_REVIEW, and CODEX_DOC_REVIEW so install/auth/config detection
128 * lives in exactly one place.
129 *
130 * Emits ONE self-contained bash block that runs `gstack-codex-probe` as a
131 * command (one subcommand per check), so it works from bash, zsh or sh and
132 * nothing it learns persists to later blocks except the echoed mode. It:
133 * 1. reads the `codex_reviews` master switch,
134 * 2. checks that the probe is installed and executable
135 * (`CODEX_MODE: helper_unavailable` with the fix otherwise),
136 * 3. runs `command -v codex` (literal — keeps the e2e substring assertion),
137 * then `check-auth`, `check-sandbox`, `probe-model` and `check-version`,
138 * 4. logs the relevant `log-event` for each non-ready outcome,
139 * 5. sets ONE canonical mode var and echoes `CODEX_MODE: <mode>` so the agent
140 * gates later blocks on the echoed value.
141 *
142 * Mode values: `disabled` (codex_reviews disabled or config unreadable; the
143 * send binaries refuse it as consent_missing whatever this block prints) | `helper_unavailable` (the probe is
144 * missing or not executable) | `not_installed` | `not_authed` |
145 * `broken_install` | `sandbox_unavailable` | `model_unusable` | `quota_exhausted` |
146 * `unverified` (echoed as `unverified (rate_limited)` after a probe-time 429) | `ready`.
147 * The path is host-rewritten at gen-skill-docs time (pathRewrites), so the
148 * literal `~/.claude/skills/gstack` is correct here and becomes `$GSTACK_ROOT`
149 * etc. for non-Claude hosts.
150 *
151 * `disabledBehavior` controls the `disabled`-mode interpretation, which is the
152 * one branch that legitimately differs per caller (D1):
153 * - `skip-all` (plan / doc reviews): disabled means no extra review step at
154 * all — skip the section, no Claude fallback.
155 * - `codex-only` (diff adversarial): disabled gates only the Codex passes; the
156 * free Claude adversarial subagent still runs.
157 */
158export function codexPreflight(opts: { modeVar?: string; disabledBehavior: 'skip-all' | 'codex-only'; nativeReview?: boolean; role?: boolean }): string {
159 const m = opts.modeVar ?? '_CODEX_MODE';
160 const disabledLine = opts.disabledBehavior === 'codex-only'
161 ? '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."'
162 : '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`."';
163 const nativeRoute = opts.disabledBehavior === 'codex-only'
164 ? 'Keep the required Claude adversarial pass; do not dispatch a duplicate.'
165 : 'Fall back to the Claude subagent path.';
166 return `\`\`\`bash
167# Codex preflight: the probe runs as a command, so any shell works.
168_CODEX_PROBE=${CODEX_PROBE_PATH}
169_CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get --stored codex_reviews 2>/dev/null)
170_gstack_helper_error=""
171[ -x "$_CODEX_PROBE" ] || _gstack_helper_error="gstack: cannot load gstack-codex-probe; re-run ./setup. https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#sourced-helper-location"
172if [ "$_CODEX_CFG" = "disabled" ] || [ "$_CODEX_CFG" = "unreadable" ]; then
173 ${m}="disabled"
174elif { [ -n "\${CODEX_THREAD_ID:-}" ] || [ -n "\${CODEX_SANDBOX:-}" ] || [ "\${GSTACK_ACTIVE_HOST:-}" = codex ]; }; then
175 ${m}="under_codex"
176elif ! command -v codex >/dev/null 2>&1; then
177 ${m}="not_installed"; "$_CODEX_PROBE" log-event codex_cli_missing 2>/dev/null || true
178elif [ -n "$_gstack_helper_error" ]; then
179 ${m}="helper_unavailable"; echo "$_gstack_helper_error"
180elif ! "$_CODEX_PROBE" check-auth >/dev/null 2>&1; then
181 ${m}="not_authed"; "$_CODEX_PROBE" log-event codex_auth_failed 2>/dev/null || true
182else${opts.role ? '' : `
183 # Free sandbox check before the paid probe; probe exit 2 = the CLI cannot run.`}
184 _CODEX_MP=0${opts.role ? '' : '; _CODEX_PS=""'}
185 "$_CODEX_PROBE" check-sandbox || _CODEX_MP=3${opts.role ? `
186 if [ "$_CODEX_MP" -eq 3 ]; then
187 ${m}="sandbox_unavailable"` : `
188 for _CODEX_KIND in exec${opts.nativeReview ? ' review' : ''}; do
189 [ "$_CODEX_MP" -eq 0 ] || break
190 _CODEX_PO=$("$_CODEX_PROBE" probe-model $_CODEX_KIND); _CODEX_MP=$?; printf '%s\\n' "$_CODEX_PO"
191 case "$_CODEX_PO" in *"STATE: inconclusive"*) _CODEX_PS=inconclusive ;; *"STATE: rate_limited"*) _CODEX_PS=rate_limited ;; esac
192 done
193 if [ "$_CODEX_MP" -eq 3 ]; then
194 ${m}="sandbox_unavailable"
195 elif [ "$_CODEX_MP" -eq 2 ]; then
196 ${m}="broken_install"
197 elif [ "$_CODEX_MP" -eq 4 ]; then
198 ${m}="quota_exhausted"
199 elif [ "$_CODEX_MP" -ne 0 ]; then
200 ${m}="model_unusable"
201 elif [ "$_CODEX_PS" = inconclusive ]; then
202 ${m}="unverified"
203 elif [ "$_CODEX_PS" = rate_limited ]; then
204 ${m}="unverified (rate_limited)"`}
205 else
206 ${m}="ready"; "$_CODEX_PROBE" check-version || true
207 fi
208fi
209echo "CODEX_MODE: $${m}"
210\`\`\`
212Branch on the echoed \`CODEX_MODE\`:
213- **\`disabled\`** (or a send's \`consent_missing\` line) — \`codex_reviews\` is off or unreadable. ${disabledLine}
214- **\`helper_unavailable\`** — the probe is missing or not executable; relay the line above (cause and fix). ${nativeRoute}
215- **\`not_installed\`** — Codex CLI absent. Print: "Codex not installed; outside coverage unavailable. Install: \`npm install -g @openai/codex\`." ${nativeRoute}
216- **\`under_codex\`** — stale artifact selected its own harness. Print: "Codex outside review unavailable: harness mismatch; no outside process started. Missing coverage. Repair: setup --host codex." Skip the outside invocation and follow the workflow's native-review instructions below. Conflicting inherited harness markers are not grounds to guess another provider.
217- **\`not_authed\`** — installed but no credentials. Print: "Codex not authenticated; outside coverage unavailable. Run \`codex login\` or set \`$CODEX_API_KEY\`." ${nativeRoute}
218- **\`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. ${nativeRoute}
219- **\`model_unusable\`** — ${opts.role ? 'the role invocation rejected policy, auth or model selection. Relay its reason and source-specific Repair/HINT lines, not a lower-priority setting. `AUTH_FAILED` needs `codex login`; never substitute a model.' : "the selected model (see `CODEX_MODEL:`) is invalid or unavailable to the account (HTTP 400 on every call). Relay the probe's HINT lines and the fix (`GSTACK_CODEX_MODEL=<supported-model>` or config.toml `model`); never substitute a model."} ${nativeRoute}${opts.role ? '' : ' The ~10s round trip is cached for 1h.'}
220- **\`quota_exhausted\`** — Codex usage limit: relay the probe's lines verbatim (reset time, retry); no more Codex calls this run. ${nativeRoute}
221- **\`sandbox_unavailable\`** — Codex's sandbox cannot start here (containers without user namespaces); the probe printed the reason and fix. No paid call ran; outside coverage is unavailable. ${nativeRoute}
222- **\`ready\`** or **\`unverified\`** — run the Codex pass below. \`unverified\` means the model check timed out or, with \`(rate_limited)\`, hit a 429; say so, and let the pass's own verdict decide.${opts.role ? `
223Plan-review readiness probes the [policy](https://github.com/garrytan/gstack/blob/main/docs/model-policy.md) model. Relay its diagnostics; never infer quota from the review block's exit status.` : ''}`;
224}
226/**
227 * Canonical foreground-dispatch guidance (#497 → #2440 → a third recurrence at
228 * a /ship documentation dispatch). Claude Code v2.1.198 made Agent-tool subagents run in the
229 * BACKGROUND by default; a synchronous dispatch site must pass the flag
230 * explicitly or the parent waits on output that never arrives. Rendered via
231 * {{FOREGROUND_DISPATCH_NOTE}} in section templates; resolver sites may
232 * interpolate it directly. Same name as the placeholder for grep-ability.
233 */
234/** The Claude Code release that flipped Agent-tool subagents to background-by-default (#497/#2440 class). Interpolated at every RESOLVER site — grep 'Claude Code v2.1' when bumping. */
235export const CC_BACKGROUND_DEFAULT_SINCE = 'Claude Code v2.1.198';
236/** Claude Code's fork-subagent schema has no run_in_background, so every foreground request is conditional (CEO-20). */
237export const FOREGROUND_IF_AVAILABLE = '`run_in_background: false` when available';
238/** The recovery when a requested foreground dispatch still ran in the background. */
239export const BACKGROUND_RECOVERY = 'A launch receipt means it went background: await its completion notice.';
241export const FOREGROUND_DISPATCH_NOTE =
242 `**Foreground required:** pass ${FOREGROUND_IF_AVAILABLE} on the Agent call — subagents run in the background by default since ${CC_BACKGROUND_DEFAULT_SINCE}, so omitting an available flag gives a background run. ${BACKGROUND_RECOVERY} Dispatch through the Agent tool only: invoking the target as a Skill, or executing its workflow inline in your own context, forfeits the fresh-context isolation this dispatch exists for, even though the skill may appear in your available-skills list. (Where a step defines an inline fallback, it applies only after a dispatched subagent has failed.)`;