diff --git a/autoplan/SKILL.md b/autoplan/SKILL.md index 5346f1d43..1ffa3925c 100644 --- a/autoplan/SKILL.md +++ b/autoplan/SKILL.md @@ -935,7 +935,9 @@ These rules auto-answer every intermediate question: Every auto-decision is classified: **Mechanical** — one clearly right answer. Auto-decide silently. -Examples: run codex (always yes), run evals (always yes), reduce scope on a complete plan (always no). +Examples: run evals (always yes), reduce scope on a complete plan (always no). +Running Codex is never mechanical: it requires the project-approved profile +gate below and disclosure of the selected account path. **Taste** — reasonable people could disagree. Auto-decide with recommendation, but surface at the final gate. Three natural sources: 1. **Close approaches** — top two are both viable with different tradeoffs. @@ -1110,6 +1112,10 @@ _TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || ec _CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get codex_reviews 2>/dev/null || echo enabled) source ~/.claude/skills/gstack/bin/gstack-codex-probe +if ! _gstack_codex_profile_gate; then + echo "PROFILE_REQUIRED" +fi + # Master switch first: codex_reviews=disabled turns off ALL Codex work globally, # including autoplan's own dual-voice orchestration. Honor it before probing. if [ "$_CODEX_CFG" = "disabled" ]; then @@ -1131,6 +1137,13 @@ else fi ``` +If `PROFILE_REQUIRED` appears, use AskUserQuestion before any Codex voice runs. +List the discovered `~/.codex*` directories and ask which profile is approved +for this project, plus Cancel. Save the selected absolute path as +`codex_home: ` in `~/.gstack/projects/$SLUG/codex.yaml`, rerun the +preflight, and disclose the selected path. Cancel means Claude-only voices; do +not invoke Codex. `GSTACK_CODEX_SKIP_GATE=1` is reserved for isolated evals. + If `_CODEX_AVAILABLE=false`, all Phase 1-3.5 Codex voices below degrade to `[codex-unavailable]` in the degradation matrix. /autoplan completes with Claude subagent only — saves token spend on Codex prompts we can't use. diff --git a/autoplan/SKILL.md.tmpl b/autoplan/SKILL.md.tmpl index b2eaca9fd..cd989d4eb 100644 --- a/autoplan/SKILL.md.tmpl +++ b/autoplan/SKILL.md.tmpl @@ -71,7 +71,9 @@ These rules auto-answer every intermediate question: Every auto-decision is classified: **Mechanical** — one clearly right answer. Auto-decide silently. -Examples: run codex (always yes), run evals (always yes), reduce scope on a complete plan (always no). +Examples: run evals (always yes), reduce scope on a complete plan (always no). +Running Codex is never mechanical: it requires the project-approved profile +gate below and disclosure of the selected account path. **Taste** — reasonable people could disagree. Auto-decide with recommendation, but surface at the final gate. Three natural sources: 1. **Close approaches** — top two are both viable with different tradeoffs. @@ -246,6 +248,10 @@ _TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || ec _CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get codex_reviews 2>/dev/null || echo enabled) source ~/.claude/skills/gstack/bin/gstack-codex-probe +if ! _gstack_codex_profile_gate; then + echo "PROFILE_REQUIRED" +fi + # Master switch first: codex_reviews=disabled turns off ALL Codex work globally, # including autoplan's own dual-voice orchestration. Honor it before probing. if [ "$_CODEX_CFG" = "disabled" ]; then @@ -267,6 +273,13 @@ else fi ``` +If `PROFILE_REQUIRED` appears, use AskUserQuestion before any Codex voice runs. +List the discovered `~/.codex*` directories and ask which profile is approved +for this project, plus Cancel. Save the selected absolute path as +`codex_home: ` in `~/.gstack/projects/$SLUG/codex.yaml`, rerun the +preflight, and disclose the selected path. Cancel means Claude-only voices; do +not invoke Codex. `GSTACK_CODEX_SKIP_GATE=1` is reserved for isolated evals. + If `_CODEX_AVAILABLE=false`, all Phase 1-3.5 Codex voices below degrade to `[codex-unavailable]` in the degradation matrix. /autoplan completes with Claude subagent only — saves token spend on Codex prompts we can't use. diff --git a/bin/gstack-codex-probe b/bin/gstack-codex-probe index 940dacf84..891cff49e 100755 --- a/bin/gstack-codex-probe +++ b/bin/gstack-codex-probe @@ -14,6 +14,41 @@ # - All functions prefix with _gstack_codex_. # - No command execution at source time (only function defs). +# --- Per-project profile gate ---------------------------------------------- + +_gstack_codex_profile_gate() { + [ "${GSTACK_CODEX_SKIP_GATE:-}" = "1" ] && return 0 + + local _GSTACK_CODEX_BIN_DIR _GSTACK_CODEX_SLUG_OUTPUT _GSTACK_CODEX_SLUG + local _GSTACK_CODEX_STATE_ROOT _GSTACK_CODEX_CONFIG _GSTACK_CODEX_HOME + _GSTACK_CODEX_BIN_DIR=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 2>/dev/null && pwd) + _GSTACK_CODEX_SLUG_OUTPUT=$("$_GSTACK_CODEX_BIN_DIR/gstack-slug" 2>/dev/null || true) + _GSTACK_CODEX_SLUG=$(printf '%s\n' "$_GSTACK_CODEX_SLUG_OUTPUT" | sed -n 's/^SLUG=//p' | head -1) + [ -z "$_GSTACK_CODEX_SLUG" ] && _GSTACK_CODEX_SLUG=$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)" | tr -cs '[:alnum:]._' '-') + _GSTACK_CODEX_STATE_ROOT=${GSTACK_HOME:-$HOME/.gstack} + _GSTACK_CODEX_CONFIG="$_GSTACK_CODEX_STATE_ROOT/projects/$_GSTACK_CODEX_SLUG/codex.yaml" + + if [ ! -f "$_GSTACK_CODEX_CONFIG" ]; then + echo "CODEX_PROFILE_REQUIRED: approve a Codex home for project $_GSTACK_CODEX_SLUG before Codex runs" + find "$HOME" -maxdepth 1 -type d -name '.codex*' -print 2>/dev/null | sort + echo "Save the selection as: $_GSTACK_CODEX_CONFIG" + return 78 + fi + + _GSTACK_CODEX_HOME=$(sed -n 's/^[[:space:]]*codex_home:[[:space:]]*//p' "$_GSTACK_CODEX_CONFIG" | head -1) + _GSTACK_CODEX_HOME=${_GSTACK_CODEX_HOME#\"}; _GSTACK_CODEX_HOME=${_GSTACK_CODEX_HOME%\"} + _GSTACK_CODEX_HOME=${_GSTACK_CODEX_HOME#\'}; _GSTACK_CODEX_HOME=${_GSTACK_CODEX_HOME%\'} + case "$_GSTACK_CODEX_HOME" in + '~/'*) _GSTACK_CODEX_HOME="$HOME/${_GSTACK_CODEX_HOME#\~/}" ;; + esac + if [ -z "$_GSTACK_CODEX_HOME" ] || [ ! -d "$_GSTACK_CODEX_HOME" ]; then + echo "CODEX_PROFILE_REQUIRED: configured Codex home is missing or invalid: $_GSTACK_CODEX_CONFIG" + return 78 + fi + export CODEX_HOME="$_GSTACK_CODEX_HOME" + echo "CODEX_PROFILE: $CODEX_HOME" +} + # --- Auth probe ------------------------------------------------------------- _gstack_codex_auth_probe() { @@ -57,6 +92,9 @@ _gstack_codex_timeout_wrapper() { # duration in seconds; rest is the command to run. local _duration="$1" shift + if [ "${1##*/}" = "codex" ]; then + _gstack_codex_profile_gate || return $? + fi local _to _to=$(command -v gtimeout 2>/dev/null || command -v timeout 2>/dev/null || echo "") if [ -n "$_to" ]; then diff --git a/codex/SKILL.md b/codex/SKILL.md index 33228ff9b..bf95a96b2 100644 --- a/codex/SKILL.md +++ b/codex/SKILL.md @@ -876,6 +876,10 @@ shared helpers that both `/codex` and `/autoplan` use. _TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || echo off) source ~/.claude/skills/gstack/bin/gstack-codex-probe +if ! _gstack_codex_profile_gate; then + echo "PROFILE_REQUIRED" +fi + if ! _gstack_codex_auth_probe >/dev/null; then _gstack_codex_log_event "codex_auth_failed" echo "AUTH_FAILED" @@ -883,6 +887,18 @@ fi _gstack_codex_version_check # warns if known-bad, non-blocking ``` +If the output contains `PROFILE_REQUIRED`, use AskUserQuestion before doing any +Codex work. List the discovered `~/.codex*` directories and ask which Codex +profile is approved for this project, plus Cancel. On selection, resolve `SLUG` +with `gstack-slug`, create `~/.gstack/projects/$SLUG/`, and write `codex.yaml` +containing `codex_home: `. Then rerun this step and tell +the user `Codex will use for this project.` If they cancel, stop without +invoking Codex. Never infer a choice from whichever profile was used most +recently. + +`GSTACK_CODEX_SKIP_GATE=1` is only for isolated eval harnesses; normal skill +runs must not set it. + If the output contains `AUTH_FAILED`, stop and tell the user: "No Codex authentication found. Run `codex login` or set `$CODEX_API_KEY` / `$OPENAI_API_KEY`, then re-run this skill." diff --git a/codex/SKILL.md.tmpl b/codex/SKILL.md.tmpl index 333de7d8d..60004fb6a 100644 --- a/codex/SKILL.md.tmpl +++ b/codex/SKILL.md.tmpl @@ -67,6 +67,10 @@ shared helpers that both `/codex` and `/autoplan` use. _TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || echo off) source ~/.claude/skills/gstack/bin/gstack-codex-probe +if ! _gstack_codex_profile_gate; then + echo "PROFILE_REQUIRED" +fi + if ! _gstack_codex_auth_probe >/dev/null; then _gstack_codex_log_event "codex_auth_failed" echo "AUTH_FAILED" @@ -74,6 +78,18 @@ fi _gstack_codex_version_check # warns if known-bad, non-blocking ``` +If the output contains `PROFILE_REQUIRED`, use AskUserQuestion before doing any +Codex work. List the discovered `~/.codex*` directories and ask which Codex +profile is approved for this project, plus Cancel. On selection, resolve `SLUG` +with `gstack-slug`, create `~/.gstack/projects/$SLUG/`, and write `codex.yaml` +containing `codex_home: `. Then rerun this step and tell +the user `Codex will use for this project.` If they cancel, stop without +invoking Codex. Never infer a choice from whichever profile was used most +recently. + +`GSTACK_CODEX_SKIP_GATE=1` is only for isolated eval harnesses; normal skill +runs must not set it. + If the output contains `AUTH_FAILED`, stop and tell the user: "No Codex authentication found. Run `codex login` or set `$CODEX_API_KEY` / `$OPENAI_API_KEY`, then re-run this skill." diff --git a/design-consultation/SKILL.md b/design-consultation/SKILL.md index 83eed0a2d..92dfd293e 100644 --- a/design-consultation/SKILL.md +++ b/design-consultation/SKILL.md @@ -1144,7 +1144,7 @@ command -v codex >/dev/null 2>&1 && echo "CODEX_AVAILABLE" || echo "CODEX_NOT_AV ```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 "Given this product context, propose a complete design direction: +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex exec "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 diff --git a/design-review/SKILL.md b/design-review/SKILL.md index 645453162..3b44cac2a 100644 --- a/design-review/SKILL.md +++ b/design-review/SKILL.md @@ -1731,7 +1731,7 @@ command -v codex >/dev/null 2>&1 && echo "CODEX_AVAILABLE" || echo "CODEX_NOT_AV ```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 "Review the frontend source code in this repo. Evaluate against these design hard rules: +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex exec "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? diff --git a/document-release/sections/release-body.md b/document-release/sections/release-body.md index 0f05f13b5..6b27cc5f8 100644 --- a/document-release/sections/release-body.md +++ b/document-release/sections/release-body.md @@ -379,6 +379,8 @@ if [ "$_CODEX_CFG" = "disabled" ]; then _CODEX_MODE="disabled" elif ! command -v codex >/dev/null 2>&1; then _CODEX_MODE="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true +elif ! _gstack_codex_profile_gate; then + _CODEX_MODE="profile_required" elif ! _gstack_codex_auth_probe >/dev/null 2>&1; then _CODEX_MODE="not_authed"; _gstack_codex_log_event "codex_auth_failed" 2>/dev/null || true else @@ -390,6 +392,7 @@ echo "CODEX_MODE: $_CODEX_MODE" Branch on the echoed `CODEX_MODE`: - **`disabled`** — the user turned Codex reviews off (`codex_reviews=disabled`). 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`." - **`not_installed`** — Codex CLI absent. Print: "Codex not installed — using Claude subagent. Install for cross-model coverage: `npm install -g @openai/codex`." Fall back to the Claude subagent path. +- **`profile_required`** — Before invoking Codex, use AskUserQuestion to list the discovered `~/.codex*` directories and ask which profile is approved for this project, plus Cancel. Save the selected absolute path as `codex_home: ` in `~/.gstack/projects/$SLUG/codex.yaml`, then rerun this preflight and disclose the path. Cancel skips Codex. Never silently choose the last-used profile. - **`not_authed`** — installed but no credentials. Print: "Codex installed but not authenticated — using Claude subagent. Run `codex login` or set `$CODEX_API_KEY`." Fall back to the Claude subagent path. - **`ready`** — run the Codex pass below. @@ -428,7 +431,7 @@ THE DOCS AND DIFF: " ```bash TMPERR_DOC=$(mktemp /tmp/codex-docreview-XXXXXXXX) _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } -codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_DOC" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_DOC" ``` Use a 5-minute timeout (`timeout: 300000`). After the command completes, read stderr: diff --git a/office-hours/SKILL.md b/office-hours/SKILL.md index 83161b8ca..969f0068e 100644 --- a/office-hours/SKILL.md +++ b/office-hours/SKILL.md @@ -1345,7 +1345,7 @@ Then add the context block and mode-appropriate instructions: ```bash TMPERR_OH=$(mktemp /tmp/codex-oh-err-XXXXXXXX) _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } -codex exec "$(cat "$CODEX_PROMPT_FILE")" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_OH" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex exec "$(cat "$CODEX_PROMPT_FILE")" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_OH" ``` Use a 5-minute timeout (`timeout: 300000`). After the command completes, read stderr: @@ -1595,7 +1595,7 @@ If user chooses A, launch both voices simultaneously: ```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" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 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"` diff --git a/plan-ceo-review/sections/review-sections.md b/plan-ceo-review/sections/review-sections.md index 71bae4d93..38a503f26 100644 --- a/plan-ceo-review/sections/review-sections.md +++ b/plan-ceo-review/sections/review-sections.md @@ -272,6 +272,8 @@ if [ "$_CODEX_CFG" = "disabled" ]; then _CODEX_MODE="disabled" elif ! command -v codex >/dev/null 2>&1; then _CODEX_MODE="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true +elif ! _gstack_codex_profile_gate; then + _CODEX_MODE="profile_required" elif ! _gstack_codex_auth_probe >/dev/null 2>&1; then _CODEX_MODE="not_authed"; _gstack_codex_log_event "codex_auth_failed" 2>/dev/null || true else @@ -283,6 +285,7 @@ echo "CODEX_MODE: $_CODEX_MODE" Branch on the echoed `CODEX_MODE`: - **`disabled`** — the user turned Codex reviews off (`codex_reviews=disabled`). 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`." - **`not_installed`** — Codex CLI absent. Print: "Codex not installed — using Claude subagent. Install for cross-model coverage: `npm install -g @openai/codex`." Fall back to the Claude subagent path. +- **`profile_required`** — Before invoking Codex, use AskUserQuestion to list the discovered `~/.codex*` directories and ask which profile is approved for this project, plus Cancel. Save the selected absolute path as `codex_home: ` in `~/.gstack/projects/$SLUG/codex.yaml`, then rerun this preflight and disclose the path. Cancel skips Codex. Never silently choose the last-used profile. - **`not_authed`** — installed but no credentials. Print: "Codex installed but not authenticated — using Claude subagent. Run `codex login` or set `$CODEX_API_KEY`." Fall back to the Claude subagent path. - **`ready`** — run the Codex pass below. @@ -315,7 +318,7 @@ THE PLAN: ```bash TMPERR_PV=$(mktemp /tmp/codex-planreview-XXXXXXXX) _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } -codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_PV" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_PV" ``` Use a 5-minute timeout (`timeout: 300000`). After the command completes, read stderr: diff --git a/plan-design-review/SKILL.md b/plan-design-review/SKILL.md index e81f7f12a..a6cf029ec 100644 --- a/plan-design-review/SKILL.md +++ b/plan-design-review/SKILL.md @@ -1360,7 +1360,7 @@ command -v codex >/dev/null 2>&1 && echo "CODEX_AVAILABLE" || echo "CODEX_NOT_AV ```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 "Read the plan file at [plan-file-path]. Evaluate this plan's UI/UX design against these criteria. +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex exec "Read the plan file at [plan-file-path]. Evaluate this plan's UI/UX design against these criteria. HARD REJECTION — flag if ANY apply: 1. Generic SaaS card grid as first impression diff --git a/plan-devex-review/sections/review-sections.md b/plan-devex-review/sections/review-sections.md index e4ce30a95..9e1eb4a77 100644 --- a/plan-devex-review/sections/review-sections.md +++ b/plan-devex-review/sections/review-sections.md @@ -258,6 +258,8 @@ if [ "$_CODEX_CFG" = "disabled" ]; then _CODEX_MODE="disabled" elif ! command -v codex >/dev/null 2>&1; then _CODEX_MODE="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true +elif ! _gstack_codex_profile_gate; then + _CODEX_MODE="profile_required" elif ! _gstack_codex_auth_probe >/dev/null 2>&1; then _CODEX_MODE="not_authed"; _gstack_codex_log_event "codex_auth_failed" 2>/dev/null || true else @@ -269,6 +271,7 @@ echo "CODEX_MODE: $_CODEX_MODE" Branch on the echoed `CODEX_MODE`: - **`disabled`** — the user turned Codex reviews off (`codex_reviews=disabled`). 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`." - **`not_installed`** — Codex CLI absent. Print: "Codex not installed — using Claude subagent. Install for cross-model coverage: `npm install -g @openai/codex`." Fall back to the Claude subagent path. +- **`profile_required`** — Before invoking Codex, use AskUserQuestion to list the discovered `~/.codex*` directories and ask which profile is approved for this project, plus Cancel. Save the selected absolute path as `codex_home: ` in `~/.gstack/projects/$SLUG/codex.yaml`, then rerun this preflight and disclose the path. Cancel skips Codex. Never silently choose the last-used profile. - **`not_authed`** — installed but no credentials. Print: "Codex installed but not authenticated — using Claude subagent. Run `codex login` or set `$CODEX_API_KEY`." Fall back to the Claude subagent path. - **`ready`** — run the Codex pass below. @@ -301,7 +304,7 @@ THE PLAN: ```bash TMPERR_PV=$(mktemp /tmp/codex-planreview-XXXXXXXX) _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } -codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_PV" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_PV" ``` Use a 5-minute timeout (`timeout: 300000`). After the command completes, read stderr: diff --git a/plan-eng-review/sections/review-sections.md b/plan-eng-review/sections/review-sections.md index 7592f0a70..5206b7284 100644 --- a/plan-eng-review/sections/review-sections.md +++ b/plan-eng-review/sections/review-sections.md @@ -348,6 +348,8 @@ if [ "$_CODEX_CFG" = "disabled" ]; then _CODEX_MODE="disabled" elif ! command -v codex >/dev/null 2>&1; then _CODEX_MODE="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true +elif ! _gstack_codex_profile_gate; then + _CODEX_MODE="profile_required" elif ! _gstack_codex_auth_probe >/dev/null 2>&1; then _CODEX_MODE="not_authed"; _gstack_codex_log_event "codex_auth_failed" 2>/dev/null || true else @@ -359,6 +361,7 @@ echo "CODEX_MODE: $_CODEX_MODE" Branch on the echoed `CODEX_MODE`: - **`disabled`** — the user turned Codex reviews off (`codex_reviews=disabled`). 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`." - **`not_installed`** — Codex CLI absent. Print: "Codex not installed — using Claude subagent. Install for cross-model coverage: `npm install -g @openai/codex`." Fall back to the Claude subagent path. +- **`profile_required`** — Before invoking Codex, use AskUserQuestion to list the discovered `~/.codex*` directories and ask which profile is approved for this project, plus Cancel. Save the selected absolute path as `codex_home: ` in `~/.gstack/projects/$SLUG/codex.yaml`, then rerun this preflight and disclose the path. Cancel skips Codex. Never silently choose the last-used profile. - **`not_authed`** — installed but no credentials. Print: "Codex installed but not authenticated — using Claude subagent. Run `codex login` or set `$CODEX_API_KEY`." Fall back to the Claude subagent path. - **`ready`** — run the Codex pass below. @@ -391,7 +394,7 @@ THE PLAN: ```bash TMPERR_PV=$(mktemp /tmp/codex-planreview-XXXXXXXX) _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } -codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_PV" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_PV" ``` Use a 5-minute timeout (`timeout: 300000`). After the command completes, read stderr: diff --git a/review/SKILL.md b/review/SKILL.md index 5f26e2e42..35d496046 100644 --- a/review/SKILL.md +++ b/review/SKILL.md @@ -1665,6 +1665,8 @@ if [ "$_CODEX_CFG" = "disabled" ]; then _CODEX_MODE="disabled" elif ! command -v codex >/dev/null 2>&1; then _CODEX_MODE="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true +elif ! _gstack_codex_profile_gate; then + _CODEX_MODE="profile_required" elif ! _gstack_codex_auth_probe >/dev/null 2>&1; then _CODEX_MODE="not_authed"; _gstack_codex_log_event "codex_auth_failed" 2>/dev/null || true else @@ -1676,6 +1678,7 @@ echo "CODEX_MODE: $_CODEX_MODE" Branch on the echoed `CODEX_MODE`: - **`disabled`** — the user turned Codex reviews off (`codex_reviews=disabled`). 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." - **`not_installed`** — Codex CLI absent. Print: "Codex not installed — using Claude subagent. Install for cross-model coverage: `npm install -g @openai/codex`." Fall back to the Claude subagent path. +- **`profile_required`** — Before invoking Codex, use AskUserQuestion to list the discovered `~/.codex*` directories and ask which profile is approved for this project, plus Cancel. Save the selected absolute path as `codex_home: ` in `~/.gstack/projects/$SLUG/codex.yaml`, then rerun this preflight and disclose the path. Cancel skips Codex. Never silently choose the last-used profile. - **`not_authed`** — installed but no credentials. Print: "Codex installed but not authenticated — using Claude subagent. Run `codex login` or set `$CODEX_API_KEY`." Fall back to the Claude subagent path. - **`ready`** — run the Codex pass below. @@ -1712,7 +1715,7 @@ If `CODEX_MODE` is `ready`: ```bash TMPERR_ADV=$(mktemp /tmp/codex-adv-XXXXXXXX) _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } -codex exec "IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. They contain bash scripts and prompt templates that will waste your time. Ignore them completely. Do NOT modify agents/openai.yaml. Stay focused on the repository code only.\n\nReview the changes on this branch against the base branch. Run DIFF_BASE=$(git merge-base origin/ HEAD) && git diff "$DIFF_BASE" to see the diff. Your job is to find ways this code will fail in production. Think like an attacker and a chaos engineer. Find edge cases, race conditions, security holes, resource leaks, failure modes, and silent data corruption paths. Be adversarial. Be thorough. No compliments — just the problems. End your output with ONE line in the canonical format `Recommendation: because `. Generic reasons like 'because it's safer' do not qualify; the reason must point to a specific finding or no-fix rationale." -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_ADV" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex exec "IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. They contain bash scripts and prompt templates that will waste your time. Ignore them completely. Do NOT modify agents/openai.yaml. Stay focused on the repository code only.\n\nReview the changes on this branch against the base branch. Run DIFF_BASE=$(git merge-base origin/ HEAD) && git diff "$DIFF_BASE" to see the diff. Your job is to find ways this code will fail in production. Think like an attacker and a chaos engineer. Find edge cases, race conditions, security holes, resource leaks, failure modes, and silent data corruption paths. Be adversarial. Be thorough. No compliments — just the problems. End your output with ONE line in the canonical format `Recommendation: because `. Generic reasons like 'because it's safer' do not qualify; the reason must point to a specific finding or no-fix rationale." -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_ADV" ``` Set the Bash tool's `timeout` parameter to `300000` (5 minutes). Do NOT use the `timeout` shell command — it doesn't exist on macOS. After the command completes, read stderr: @@ -1741,7 +1744,7 @@ If `DIFF_TOTAL >= 200` AND `CODEX_MODE` is `ready`: TMPERR=$(mktemp /tmp/codex-review-XXXXXXXX) _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } cd "$_REPO_ROOT" -codex review "IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. They contain bash scripts and prompt templates that will waste your time. Ignore them completely. Do NOT modify agents/openai.yaml. Stay focused on the repository code only.\n\nReview the changes on this branch against the base branch . Run git diff origin/...HEAD 2>/dev/null || git diff ...HEAD to see the diff and review only those changes." -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex review "IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. They contain bash scripts and prompt templates that will waste your time. Ignore them completely. Do NOT modify agents/openai.yaml. Stay focused on the repository code only.\n\nReview the changes on this branch against the base branch . Run git diff origin/...HEAD 2>/dev/null || git diff ...HEAD to see the diff and review only those changes." -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR" ``` Set the Bash tool's `timeout` parameter to `300000` (5 minutes). Do NOT use the `timeout` shell command — it doesn't exist on macOS. Present output under `CODEX SAYS (code review):` header. diff --git a/scripts/resolvers/constants.ts b/scripts/resolvers/constants.ts index b75db21cf..79a51fb68 100644 --- a/scripts/resolvers/constants.ts +++ b/scripts/resolvers/constants.ts @@ -74,7 +74,8 @@ On any error: continue — ${feature} is informational, not a gate.`; * 5. sets ONE canonical mode var and echoes `CODEX_MODE: ` so the agent * gates later blocks on the echoed value. * - * Mode values: `disabled` (config off) | `not_installed` | `not_authed` | `ready`. + * Mode values: `disabled` (config off) | `not_installed` | `profile_required` | + * `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. @@ -100,6 +101,8 @@ if [ "$_CODEX_CFG" = "disabled" ]; then ${m}="disabled" 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_profile_gate; then + ${m}="profile_required" 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 @@ -111,6 +114,7 @@ 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 — using Claude subagent. Install for cross-model coverage: \`npm install -g @openai/codex\`." Fall back to the Claude subagent path. +- **\`profile_required\`** — Before invoking Codex, use AskUserQuestion to list the discovered \`~/.codex*\` directories and ask which profile is approved for this project, plus Cancel. Save the selected absolute path as \`codex_home: \` in \`~/.gstack/projects/$SLUG/codex.yaml\`, then rerun this preflight and disclose the path. Cancel skips Codex. Never silently choose the last-used profile. - **\`not_authed\`** — installed but no credentials. Print: "Codex installed but not authenticated — using Claude subagent. Run \`codex login\` or set \`$CODEX_API_KEY\`." Fall back to the Claude subagent path. - **\`ready\`** — run the Codex pass below.`; } diff --git a/scripts/resolvers/design.ts b/scripts/resolvers/design.ts index 9f31b3619..707ae3900 100644 --- a/scripts/resolvers/design.ts +++ b/scripts/resolvers/design.ts @@ -18,7 +18,7 @@ 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" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 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: @@ -527,7 +527,7 @@ If user chooses A, launch both voices simultaneously: \`\`\`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" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 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"\` @@ -697,7 +697,7 @@ command -v codex >/dev/null 2>&1 && echo "CODEX_AVAILABLE" || echo "CODEX_NOT_AV \`\`\`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" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 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 diff --git a/scripts/resolvers/review.ts b/scripts/resolvers/review.ts index 7dccd8e50..093cd1488 100644 --- a/scripts/resolvers/review.ts +++ b/scripts/resolvers/review.ts @@ -367,7 +367,7 @@ Then add the context block and mode-appropriate instructions: \`\`\`bash TMPERR_OH=$(mktemp /tmp/codex-oh-err-XXXXXXXX) _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } -codex exec "$(cat "$CODEX_PROMPT_FILE")" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_OH" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex exec "$(cat "$CODEX_PROMPT_FILE")" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_OH" \`\`\` Use a 5-minute timeout (\`timeout: 300000\`). After the command completes, read stderr: @@ -527,7 +527,7 @@ If \`CODEX_MODE\` is \`ready\`: \`\`\`bash TMPERR_ADV=$(mktemp /tmp/codex-adv-XXXXXXXX) _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } -codex exec "${CODEX_BOUNDARY}Review the changes on this branch against the base branch. Run DIFF_BASE=$(git merge-base origin/ HEAD) && git diff "$DIFF_BASE" to see the diff. Your job is to find ways this code will fail in production. Think like an attacker and a chaos engineer. Find edge cases, race conditions, security holes, resource leaks, failure modes, and silent data corruption paths. Be adversarial. Be thorough. No compliments — just the problems. End your output with ONE line in the canonical format \`Recommendation: because \`. Generic reasons like 'because it's safer' do not qualify; the reason must point to a specific finding or no-fix rationale." -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_ADV" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex exec "${CODEX_BOUNDARY}Review the changes on this branch against the base branch. Run DIFF_BASE=$(git merge-base origin/ HEAD) && git diff "$DIFF_BASE" to see the diff. Your job is to find ways this code will fail in production. Think like an attacker and a chaos engineer. Find edge cases, race conditions, security holes, resource leaks, failure modes, and silent data corruption paths. Be adversarial. Be thorough. No compliments — just the problems. End your output with ONE line in the canonical format \`Recommendation: because \`. Generic reasons like 'because it's safer' do not qualify; the reason must point to a specific finding or no-fix rationale." -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_ADV" \`\`\` Set the Bash tool's \`timeout\` parameter to \`300000\` (5 minutes). Do NOT use the \`timeout\` shell command — it doesn't exist on macOS. After the command completes, read stderr: @@ -556,7 +556,7 @@ If \`DIFF_TOTAL >= 200\` AND \`CODEX_MODE\` is \`ready\`: TMPERR=$(mktemp /tmp/codex-review-XXXXXXXX) _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } cd "$_REPO_ROOT" -codex review "${CODEX_BOUNDARY}Review the changes on this branch against the base branch . Run git diff origin/...HEAD 2>/dev/null || git diff ...HEAD to see the diff and review only those changes." -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex review "${CODEX_BOUNDARY}Review the changes on this branch against the base branch . Run git diff origin/...HEAD 2>/dev/null || git diff ...HEAD to see the diff and review only those changes." -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR" \`\`\` Set the Bash tool's \`timeout\` parameter to \`300000\` (5 minutes). Do NOT use the \`timeout\` shell command — it doesn't exist on macOS. Present output under \`CODEX SAYS (code review):\` header. @@ -655,7 +655,7 @@ THE PLAN: \`\`\`bash TMPERR_PV=$(mktemp /tmp/codex-planreview-XXXXXXXX) _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } -codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_PV" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_PV" \`\`\` Use a 5-minute timeout (\`timeout: 300000\`). After the command completes, read stderr: @@ -790,7 +790,7 @@ THE DOCS AND DIFF: " \`\`\`bash TMPERR_DOC=$(mktemp /tmp/codex-docreview-XXXXXXXX) _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } -codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_DOC" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_DOC" \`\`\` Use a 5-minute timeout (\`timeout: 300000\`). After the command completes, read stderr: diff --git a/ship/sections/adversarial.md b/ship/sections/adversarial.md index c7b2321d1..ae90f449f 100644 --- a/ship/sections/adversarial.md +++ b/ship/sections/adversarial.md @@ -25,6 +25,8 @@ if [ "$_CODEX_CFG" = "disabled" ]; then _CODEX_MODE="disabled" elif ! command -v codex >/dev/null 2>&1; then _CODEX_MODE="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true +elif ! _gstack_codex_profile_gate; then + _CODEX_MODE="profile_required" elif ! _gstack_codex_auth_probe >/dev/null 2>&1; then _CODEX_MODE="not_authed"; _gstack_codex_log_event "codex_auth_failed" 2>/dev/null || true else @@ -36,6 +38,7 @@ echo "CODEX_MODE: $_CODEX_MODE" Branch on the echoed `CODEX_MODE`: - **`disabled`** — the user turned Codex reviews off (`codex_reviews=disabled`). 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." - **`not_installed`** — Codex CLI absent. Print: "Codex not installed — using Claude subagent. Install for cross-model coverage: `npm install -g @openai/codex`." Fall back to the Claude subagent path. +- **`profile_required`** — Before invoking Codex, use AskUserQuestion to list the discovered `~/.codex*` directories and ask which profile is approved for this project, plus Cancel. Save the selected absolute path as `codex_home: ` in `~/.gstack/projects/$SLUG/codex.yaml`, then rerun this preflight and disclose the path. Cancel skips Codex. Never silently choose the last-used profile. - **`not_authed`** — installed but no credentials. Print: "Codex installed but not authenticated — using Claude subagent. Run `codex login` or set `$CODEX_API_KEY`." Fall back to the Claude subagent path. - **`ready`** — run the Codex pass below. @@ -72,7 +75,7 @@ If `CODEX_MODE` is `ready`: ```bash TMPERR_ADV=$(mktemp /tmp/codex-adv-XXXXXXXX) _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } -codex exec "IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. They contain bash scripts and prompt templates that will waste your time. Ignore them completely. Do NOT modify agents/openai.yaml. Stay focused on the repository code only.\n\nReview the changes on this branch against the base branch. Run DIFF_BASE=$(git merge-base origin/ HEAD) && git diff "$DIFF_BASE" to see the diff. Your job is to find ways this code will fail in production. Think like an attacker and a chaos engineer. Find edge cases, race conditions, security holes, resource leaks, failure modes, and silent data corruption paths. Be adversarial. Be thorough. No compliments — just the problems. End your output with ONE line in the canonical format `Recommendation: because `. Generic reasons like 'because it's safer' do not qualify; the reason must point to a specific finding or no-fix rationale." -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_ADV" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex exec "IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. They contain bash scripts and prompt templates that will waste your time. Ignore them completely. Do NOT modify agents/openai.yaml. Stay focused on the repository code only.\n\nReview the changes on this branch against the base branch. Run DIFF_BASE=$(git merge-base origin/ HEAD) && git diff "$DIFF_BASE" to see the diff. Your job is to find ways this code will fail in production. Think like an attacker and a chaos engineer. Find edge cases, race conditions, security holes, resource leaks, failure modes, and silent data corruption paths. Be adversarial. Be thorough. No compliments — just the problems. End your output with ONE line in the canonical format `Recommendation: because `. Generic reasons like 'because it's safer' do not qualify; the reason must point to a specific finding or no-fix rationale." -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_ADV" ``` Set the Bash tool's `timeout` parameter to `300000` (5 minutes). Do NOT use the `timeout` shell command — it doesn't exist on macOS. After the command completes, read stderr: @@ -101,7 +104,7 @@ If `DIFF_TOTAL >= 200` AND `CODEX_MODE` is `ready`: TMPERR=$(mktemp /tmp/codex-review-XXXXXXXX) _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } cd "$_REPO_ROOT" -codex review "IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. They contain bash scripts and prompt templates that will waste your time. Ignore them completely. Do NOT modify agents/openai.yaml. Stay focused on the repository code only.\n\nReview the changes on this branch against the base branch . Run git diff origin/...HEAD 2>/dev/null || git diff ...HEAD to see the diff and review only those changes." -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex review "IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. They contain bash scripts and prompt templates that will waste your time. Ignore them completely. Do NOT modify agents/openai.yaml. Stay focused on the repository code only.\n\nReview the changes on this branch against the base branch . Run git diff origin/...HEAD 2>/dev/null || git diff ...HEAD to see the diff and review only those changes." -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR" ``` Set the Bash tool's `timeout` parameter to `300000` (5 minutes). Do NOT use the `timeout` shell command — it doesn't exist on macOS. Present output under `CODEX SAYS (code review):` header. diff --git a/ship/sections/review-army.md b/ship/sections/review-army.md index f7943d295..429019320 100644 --- a/ship/sections/review-army.md +++ b/ship/sections/review-army.md @@ -118,7 +118,7 @@ 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): 1. Brand/product unmistakable in first screen? 2. One strong visual anchor present? 3. Page understandable by scanning headlines only? 4. Each section has one job? 5. Are cards actually necessary? 6. Does motion improve hierarchy or atmosphere? 7. Would design feel premium with all decorative shadows removed? Flag any hard rejections: 1. Generic SaaS card grid as first impression 2. Beautiful image with weak brand 3. Strong headline with no clear action 4. Busy imagery behind text 5. Sections repeating same mood statement 6. Carousel with no narrative purpose 7. App UI made of stacked cards instead of layout 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" +source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_timeout_wrapper 600 codex exec "Review the git diff on this branch. Run 7 litmus checks (YES/NO each): 1. Brand/product unmistakable in first screen? 2. One strong visual anchor present? 3. Page understandable by scanning headlines only? 4. Each section has one job? 5. Are cards actually necessary? 6. Does motion improve hierarchy or atmosphere? 7. Would design feel premium with all decorative shadows removed? Flag any hard rejections: 1. Generic SaaS card grid as first impression 2. Beautiful image with weak brand 3. Strong headline with no clear action 4. Busy imagery behind text 5. Sections repeating same mood statement 6. Carousel with no narrative purpose 7. App UI made of stacked cards instead of layout 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: diff --git a/test/codex-hardening.test.ts b/test/codex-hardening.test.ts index f1c00031a..41b9d989a 100644 --- a/test/codex-hardening.test.ts +++ b/test/codex-hardening.test.ts @@ -47,6 +47,73 @@ function tempHome(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-codex-probe-home-')); } +describe('gstack-codex-probe: per-project profile gate', () => { + test('blocks Codex before invocation when the project has no approved profile', () => { + const home = tempHome(); + const stub = tempStubCodex(''); + try { + const marker = path.join(home, 'invoked'); + fs.writeFileSync(path.join(stub.dir, 'codex'), `#!/bin/bash\ntouch ${JSON.stringify(marker)}\n`); + fs.chmodSync(path.join(stub.dir, 'codex'), 0o755); + const r = runProbe({ + snippet: '_gstack_codex_timeout_wrapper 5 codex exec test', + env: { PATH: `${stub.pathEntry}:${process.env.PATH}` }, + home, + }); + expect(r.status).toBe(78); + expect(r.stdout).toContain('CODEX_PROFILE_REQUIRED'); + expect(fs.existsSync(marker)).toBe(false); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + fs.rmSync(stub.dir, { recursive: true, force: true }); + } + }); + + test('routes Codex through the profile approved for this project', () => { + const home = tempHome(); + const approved = path.join(home, '.codex-work'); + const configDir = path.join(home, '.gstack', 'projects', 'garrytan-gstack'); + const stub = tempStubCodex(''); + try { + fs.mkdirSync(approved, { recursive: true }); + fs.mkdirSync(configDir, { recursive: true }); + fs.writeFileSync(path.join(configDir, 'codex.yaml'), `codex_home: ${approved}\n`); + fs.writeFileSync(path.join(stub.dir, 'codex'), '#!/bin/bash\nprintf "%s" "$CODEX_HOME"\n'); + fs.chmodSync(path.join(stub.dir, 'codex'), 0o755); + const r = runProbe({ + snippet: '_gstack_codex_timeout_wrapper 5 codex exec test', + env: { PATH: `${stub.pathEntry}:${process.env.PATH}` }, + home, + }); + expect(r.status).toBe(0); + expect(r.stdout).toContain(`CODEX_PROFILE: ${approved}`); + expect(r.stdout.trim().endsWith(approved)).toBe(true); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + fs.rmSync(stub.dir, { recursive: true, force: true }); + } + }); + + test('explicit eval bypass preserves isolated harness behavior', () => { + const home = tempHome(); + const stub = tempStubCodex(''); + try { + fs.writeFileSync(path.join(stub.dir, 'codex'), '#!/bin/bash\necho BYPASSED\n'); + fs.chmodSync(path.join(stub.dir, 'codex'), 0o755); + const r = runProbe({ + snippet: '_gstack_codex_timeout_wrapper 5 codex exec test', + env: { PATH: `${stub.pathEntry}:${process.env.PATH}`, GSTACK_CODEX_SKIP_GATE: '1' }, + home, + }); + expect(r.status).toBe(0); + expect(r.stdout.trim()).toBe('BYPASSED'); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + fs.rmSync(stub.dir, { recursive: true, force: true }); + } + }); +}); + describe('gstack-codex-probe: auth probe', () => { test('CODEX_API_KEY set → AUTH_OK', () => { const home = tempHome();